SC

syahrul84/codemap

Developer tools
0 stars 0 forks Quality 82 Trend 20MIT

Code index for Laravel, Vue, Nuxt and Next.js projects, served to Claude over MCP: checks names before Claude uses them, finds usages and affected tests, runs a pre-release check and draws a code map.

Overview

codemap reads a Laravel, Vue 2, Vue 3 or Nuxt (2/3/4) project and serves facts about it to Claude over MCP, so Claude checks what exists instead of guessing: - classes, interfaces, traits, enums, methods, functions, properties, constants, enum cases. Names are fully resolved, with signatures and doc summaries. - static analysis of withRouting(), RouteServiceProvider and route files, including groups, resources and includes. Optionally php artisan route:list. - table, fillable/guarded/hidden, casts, appends, relations, accessors, scopes, traits. - rebuilt by replaying migrations (and a squashed schema:dump), with the migration and line that created or last changed each column. - validation rules, linked to the controller methods that use them.

README

codemap — project index MCP server for Laravel, Vue, Nuxt and Next.js

codemap reads a Laravel, Vue 2, Vue 3 or Nuxt (2/3/4) project and serves facts about it to Claude over MCP, so Claude checks what exists instead of guessing:

  • PHP symbols: classes, interfaces, traits, enums, methods, functions, properties, constants, enum cases. Names are fully resolved, with signatures and doc summaries.
  • Routes: static analysis of withRouting(), RouteServiceProvider and route files, including groups, resources and includes. Optionally php artisan route:list.
  • Eloquent models: table, fillable/guarded/hidden, casts, appends, relations, accessors, scopes, traits.
  • Database schema: rebuilt by replaying migrations (and a squashed schema:dump), with the migration and line that created or last changed each column.
  • Form Requests: validation rules, linked to the controller methods that use them.
  • Next.js / React: app router and pages router screens from the file system (route groups, [param], catch-alls, nearest layout), route handlers (app/**/route.ts exports, pages/api/**) as endpoints the frontend’s fetch calls are matched to, React function components with their props (destructured or typed), what they render and who renders them, use client / use server, and Supabase: tables used through supabase.from('x').select/insert/update/delete (find_usages rooms) and the schema replayed from supabase/migrations/*.sql (get_schema). Translation catalogs kept per text (checkIn: { en, ms }) are read too.
  • Vue / Nuxt components: props (type, required, default), emits, v-model, slots, which components each one renders and who renders it. This covers ``, the Options API, defineComponent and class components.
  • Component names: resolved the way the app resolves them. That means local imports, Vue.component() / app.component(), require.context / import.meta.glob and Vue.use() plugins. For Nuxt it also covers auto-imports (from .nuxt/components.d.ts when present) and libraries such as Vuetify, Nuxt UI and PrimeVue.
  • Pages / screens:
    • Nuxt file-based pages (Nuxt 2 _id, Nuxt 3+ [id], [[opt]], [...slug], route groups, nesting), with definePageMeta, layouts and middleware.
    • vue-router routes stitched together across files: spreads, imported arrays, nested children, lazy components, and simple helper functions that return a route.
  • Frontend ↔ backend:
    • HTTP calls in Vue / Nuxt code (axios, $fetch, useFetch, fetch, and project wrappers such as api.get or useApi()) are matched to Laravel routes, in the same repository or in a linked backend.
    • codemap reports calls to endpoints that don’t exist or use the wrong method.
    • It also reports fields a call’s TypeScript type requires (useApi()>(…), resolved across imports and generics; optional ?: fields ignored) that the endpoint never sends (typed_but_not_returned).
    • It also reports fields the frontend reads that the endpoint doesn’t return. Reads are followed through helper functions (fetchMalls() returning { malls: r.data }), list state and v-for loops, and into child components through their props (a card’s mall.name). On the Laravel side, lists built step by step are followed too: $this->helper() methods, Cache::remember(…), array_map(fn ($c) => $c + [...]), ->map(fn ($m) => [...]), filters and slices.
  • Translations:
    • Locale files are read for vue-i18n / nuxt-i18n (JSON, JS, TS; one file per locale, or split files merged) and for Laravel (lang/*.php, lang/*.json).
    • Keys are read from code: $t, t, $tc, v-t, ``, __(), trans(), @lang.
    • codemap reports keys missing per locale and keys used but defined nowhere, naming them (up to 20) in project_overview. A wrapper namespace such as message. is inferred automatically, and dynamic keys count as prefixes.
    • Only shipped locales are checked for missing keys: those in [i18n] locales, or (when not set) those with at least half of the base locale’s keys. Others are listed as partial with their coverage (th 6%).
  • Filament admin panels: each admin URL (/admin/merchants/{record}/edit) mapped to its page class, resource, model, form fields and table columns, plus custom pages such as settings screens. Filament registers these at runtime, so they are not in routes/; codemap follows Filament’s own naming rules (v3–v5).
  • Settings and their defaults (get_schema with settings: true):
    • Key/value settings read and written in code (Setting::get(KEY, DEFAULT), setting('x', 5), and helper methods that wrap them), with the default, where each is read and written, and the admin page that edits it.
    • config() keys backed by env(), with the variable name, default, how often code reads them, and whether .env.example lists the variable (only variable names are read from it, never values).
    • Config problems: config('…') keys read but defined nowhere, env() read in app code (null once config is cached).
    • .env values are never read.
  • Find usages: every use of a class, method, function, route name, component, composable or store, translation key, endpoint or file, with file:line. Tests that touch it are listed separately (including PHP tests that hit a controller’s routes through $this->getJson('/api/...')), so you know which tests to run.
  • Stores and composables: Pinia (options and setup stores), Vuex modules with their namespaces (including Nuxt 2 store/), composables and exported JS/TS functions.

Every answer is either a fact with file:line, or an explicit found: false with suggestions. Anything codemap cannot work out statically is reported as unresolved rather than guessed.

Supported:

  • Laravel 9 and later, both the 11+ bootstrap/app.php layout and the older Kernel/RouteServiceProvider layout (including apps upgraded to 11+ that kept it).
  • Vue 2.6/2.7 and Vue 3, built with Vue CLI, Vite, webpack or Laravel Mix (aliases come from vue.config.js, vite.config and tsconfig/jsconfig).
  • Nuxt 2, 3 and 4, with srcDir (Nuxt 4 app/ or the root), component dirs, pathPrefix and layers inside the project.

A repository can be both: a Laravel app with an Inertia/Vue frontend gets both sets of facts.

The full specification is in BUILD_PROMPT.md.

Install

codemap is one program with no dependencies. Pick one way to get it:

Needs Good for
Download (recommended) nothing most developers
Cargo Rust 1.99+ and a C compiler building from source
Docker Docker keeping the program away from your machine; the project is mounted read-only

On Windows, use WSL or Docker Desktop: see Windows.

Download

Ready-made programs for macOS (Apple Silicon and Intel) and Linux (x86-64 and ARM64) are on the Releases page. To install the latest one:

curl -fsSL https://github.com/syahrul84/codemap/releases/latest/download/codemap-aarch64-apple-darwin.tar.gz | tar xz

Use the file for your machine: codemap-aarch64-apple-darwin (Mac with Apple Silicon), codemap-x86_64-apple-darwin (Intel Mac), codemap-x86_64-unknown-linux-gnu or codemap-aarch64-unknown-linux-gnu (Linux). Then put it on your PATH and check it:

mkdir -p ~/.local/bin && mv codemap ~/.local/bin/
codemap --version

If macOS refuses to open a file downloaded with a browser, remove the download flag once: xattr -d com.apple.quarantine ~/.local/bin/codemap. Each release has a SHA256SUMS file to check the downloads against.

Windows

There is no Windows program yet. codemap runs on Windows through WSL (Windows’ built-in Linux) or Docker Desktop.

WSL (recommended)

  1. Install WSL once, in PowerShell as administrator, then restart:
    wsl --install
    
  2. Open Ubuntu from the Start menu and install codemap there (use codemap-aarch64-unknown-linux-gnu on an ARM PC):
    curl -fsSL https://github.com/syahrul84/codemap/releases/latest/download/codemap-x86_64-unknown-linux-gnu.tar.gz | tar xz
    mkdir -p ~/.local/bin && mv codemap ~/.local/bin/
    codemap --version
    
  3. Use it from your project folder. Projects inside WSL (~/code/my-app) are much faster to read than ones on the Windows drive (/mnt/c/Users/me/my-app), but both work.
  4. Connect it to Claude Code:
    • Claude Code running inside WSL (the usual setup): in the project folder, run claude mcp add codemap -- codemap serve --project "$(pwd)".
    • Claude Code running on Windows: let it start codemap through WSL, with the project’s path as WSL sees it. In PowerShell, in the project folder:
      claude mcp add codemap -- wsl bash -lc "codemap serve --project /mnt/c/Users/me/my-app"
      
      Replace the path with your project’s (wsl wslpath -a . prints it).

Docker Desktop

Install Docker Desktop, then in PowerShell:

git clone https://github.com/syahrul84/codemap; cd codemap
docker build -t codemap:latest --target runtime .

and in your project folder:

claude mcp add codemap -- docker run -i --rm -v "${PWD}:/workspace:ro" -v codemap-data:/data codemap:latest serve --project /workspace

The .mcp.json in With Docker works on Windows too.

Cargo

cargo install --locked --git https://github.com/syahrul84/codemap --tag v0.1.2 codemap-cli

Docker

git clone https://github.com/syahrul84/codemap && cd codemap
docker build -t codemap:latest --target runtime .

Updating

codemap update           # download and install the latest release (checked against its SHA256SUMS)
codemap update --check   # only say whether there is a newer one

Claude Code picks up the new version in its next session. Indexes update themselves the first time the new version runs. With Cargo, run the cargo install line again with the new tag; with Docker, git pull and build the image again.

Once a day, codemap serve and codemap doctor ask GitHub whether a newer release exists (a plain request for the public release list; nothing about your project is sent). When there is one, codemap doctor and project_overview say so, so Claude can tell you. To turn this off, set CODEMAP_NO_UPDATE_CHECK=1 (in your shell profile, or in the env of the MCP server entry).

Quick start

Inside a project folder:

codemap scan      # build the index (the first time takes a few seconds; later runs only re-read changed files)
codemap doctor    # what was detected, and anything codemap could not read
codemap map --out codemap.html   # the code map page; open it in a browser

The index lives outside the project (~/Library/Caches/codemap on macOS, ~/.cache/codemap on Linux; --data-dir or CODEMAP_DATA_DIR to change it). codemap only reads the project: it never writes to it and never runs its code (unless you turn on artisan mode).

Use it from Claude Code

Register it for a project (run inside that project’s folder):

claude mcp add codemap -- codemap serve --project "$(pwd)"

Or commit a .mcp.json in the project root, so everyone on the team gets it. Claude Code expands ${VAR} and ${VAR:-default} in command, args and env:

{
  "mcpServers": {
    "codemap": {
      "command": "codemap",
      "args": ["serve", "--project", "${PWD}"]
    }
  }
}

With Docker

claude mcp add codemap -- docker run -i --rm -v "$(pwd)":/workspace:ro -v codemap-data:/data -e CODEMAP_PROJECT_ID="$(pwd | shasum | cut -c1-12)" codemap:latest serve --project /workspace
{
  "mcpServers": {
    "codemap": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-v", "${PWD}:/workspace:ro",
        "-v", "codemap-data:/data",
        "codemap:latest", "serve", "--project", "/workspace"
      ]
    }
  }
}

Notes:

  • Project id: without CODEMAP_PROJECT_ID, the index id comes from the git remote + root commit, so it is stable per repository. Set it explicitly if you keep several checkouts of the same repository.
  • -i, never -t: a TTY corrupts the MCP stdio stream. All logs go to stderr.
  • Read-only: the project is mounted read-only and codemap never writes to it. The index lives in the codemap-data volume. Commands that write a file (codemap map) need a writable folder mounted for it (see Code map page).

Then add CLAUDE.snippet.md to the project’s CLAUDE.md, so Claude uses the tools at the right moments.

Other AI tools

codemap is an MCP server, and MCP is an open standard: Cursor, Windsurf, VS Code (Copilot), Zed, OpenAI Codex CLI, Gemini CLI and other MCP clients use the same tools. Register codemap serve --project (or the Docker command) as an MCP server in that tool’s settings. The tool descriptions and server instructions every client reads already say when to use each tool; for stronger guidance, put the contents of CLAUDE.snippet.md in that tool’s rules file (AGENTS.md, .cursor/rules, GEMINI.md, .github/copilot-instructions.md). The one feature that depends on the client is the question codemap asks before sending a wrong-answer report: a client that cannot show it simply never sends (see Wrong-answer reports).

Tools

Tool When Claude should use it
project_overview First, in every session: Laravel version and layout, counts, key directories, main namespaces, models, index health. About 1,200 tokens on an 1,200-file app.
search_symbol Before writing a class, method or helper, to reuse existing code. Matches short names, FQNs, Class::method and partial names. A kind filter that matches nothing falls back to other kinds (useCart as a composable finds the useCartStore store); typos and reworded names (StatusOfOrder) give found: false + suggestions.
get_symbol Instead of reading a whole file. Models include table/fillable/casts/relations; controllers their routes and Form Requests; Form Requests their rules. include_source adds the code.
get_component Before using, changing or creating a Vue component or Nuxt page: props, emits, v-model, slots, what it renders (resolved to files or libraries), who renders it, stores and composables it uses. Takes a name (OrderCard), tag (order-card) or path.
list_pages To find a screen and its code (frontend pages, and Filament admin pages with their form fields): URL, route name, the component file it renders, layout, middleware, and where the route is defined. Filters: path_contains, name_prefix, file.
list_routes To find Laravel endpoints or which controller handles a URI (filters: uri_contains, name_prefix, method, controller, middleware).
api_usage To link the frontend to the backend: which route each HTTP call hits, calls to missing endpoints (marked as dead code when the function around them is unused), wrong methods, and fields read but not returned. Calls that can’t be tied to one route are listed with file:line, the reason and the candidate routes. Pass endpoint for its callers and response fields, file for one screen, or status: missing / fields / unresolved.
find_usages Before changing, renaming or removing anything: every use with file:line, the routes involved, and the tests that touch it. Method calls on objects whose class is unknown are listed separately as possible. With changed_files (or git_changes: true) it summarises a whole change instead: what each file affects, the related tests, and the exact commands to run only those (php artisan test …, npx vitest run …, npx playwright test …). It also names the browser tests (Playwright, Cypress) that open the screens the change is close to, or look for test ids the changed code sets; tests that reach the change only through shared building blocks are listed separately for a run before release. The five browser tests that use the change most directly come first (browser_tests_top, run_top). A backend change is followed through its callers to the controllers, routes and the PHP tests that hit them (routes_via_callers, tests_via_route). Commands use the project’s package manager (pnpm exec, yarn, bunx, npx), or the commands set in [tests]. In a workspace it also lists the frontend calls (in every app) to the backend routes the change touches, and the browser tests for those screens. Code that exists but that nothing uses comes back as found: true, unused: true.
get_schema Before writing queries, migrations or model code (or, with settings: true, to see app settings and config defaults; with migrations: true, migrations not yet applied to your local / staging databases): tables, columns, keys, foreign keys, mapped models, where each column was created and changed.
verify Before finishing: checks classes, Class::method, functions, routes (name or METHOD uri), tables, table.column, Model::relation, Vue components, endpoints the frontend calls, translation keys (with the locales they are missing in), or a PHP snippet. Each item is exists, not_found (with suggestions), external (vendor / PHP built-in) or unresolved. With release_check: true: one list of problems before a release (see below).

Every tool takes detail: "brief" | "full" (default brief). List tools take limit (default 20, max 100) and cursor. Every response has the same envelope:

{ "data": { ... },
  "meta": { "project": "...", "indexed_at": "...", "git_commit": "...", "git_dirty": false,
            "stale": false, "reindexed_files": 0, "truncated": false, "next_cursor": null, "warnings": [] } }

Freshness

There is no file watcher; Docker Desktop bind mounts don’t deliver file events reliably. Instead, before answering a call, codemap checks file stats (at most every check_interval_ms, default 2 s) and re-indexes what changed. Typical costs through Docker Desktop:

  • On a 4,300-file Laravel project: about 0.1 s after editing a controller, about 0.25 s after editing a route file.
  • On a 2,500-file Vue 2 project: about 0.1 s for a check that finds nothing changed. A full first index takes about 1–2 s.

If re-indexing takes longer than 1 s, the answer comes from the previous index with stale: true and a warning. The very first index is always awaited, because an empty index would wrongly say “not found”.

CLI

--project defaults to the current folder.

codemap scan   [--project ] [--full] [--json]   # build/update the index
codemap serve  [--project ]                     # MCP server on stdio
codemap query  [--project ]  ''     # call any MCP tool from the shell
codemap stats  [--project ] [--json]            # counts, timings, index size
codemap doctor [--project ] [--json]            # detection reasons, warnings, unresolved routes, broken routes
codemap map    [--project ] --out    # the code map: one page to open in a browser
codemap misses [--project ] [--json] [--clear]  # names the AI asked for that don't exist, ranked
codemap reports [--project ] [--all] [--json] [--export ] [--close ] [--send ] [--clear]
codemap update [--check] [--force]                    # install the latest release

Global options: --name / CODEMAP_PROJECT_NAME (display name; inside Docker the folder is always workspace), --link name=path / CODEMAP_LINKS, --project-id / CODEMAP_PROJECT_ID, --data-dir / CODEMAP_DATA_DIR (default /data in Docker, the platform cache dir elsewhere), --log-level / CODEMAP_LOG.

With the Makefile: make scan PROJECT=/path, make doctor PROJECT=/path, make serve PROJECT=/path.

codemap doctor explains how the project was understood:

  • why it chose a version and layout
  • every route it could not resolve, and why
  • routes whose controller or method does not exist
  • grouped warnings (raw SQL in migrations, dynamic table names, code the parser could not read)
  • translations: keys missing per locale, keys used but not defined, plain-text keys and possibly unused keys
  • for Vue / Nuxt: how the frontend was detected, routes whose component file is missing, routes built at runtime, and component tags that resolve to nothing. These are usually imports of files that no longer exist, or components used without being registered.

Code map page

codemap map writes one HTML page that shows how the project fits together:

  • Screens (Nuxt / vue-router / Next.js pages, Filament admin pages) → the frontend files whose API calls they reach (following components, imports and the functions a screen uses) → the endpoints those calls hit → the controllers behind them → the models the controllers use → the tables they are stored in. Supabase apps link screens and route handlers straight to the tables they read.
  • Click anything to see its file and line, methods or columns, middleware, the API calls a file makes (and calls to endpoints that don’t exist), model relations and foreign keys, and everything it connects to.
  • Search, filter by app in a workspace, light and dark themes, and a Problems tab with the release check (problems, low-priority notes, lines marked as intended). Items in a file with a problem are marked red.

A frontend linked to its backend (--link, [[links]]) gets the backend’s endpoints, controllers, models and tables in its map too. The page is self-contained (no external scripts or fonts) and works offline; nothing in it is sent anywhere. The project is mounted read-only, so give the container a writable folder for the page:

docker run --rm -v "$PWD":/workspace:ro -v codemap-data:/data -v "$PWD/codemap-out":/out \
  codemap:latest map --project /workspace --out /out/codemap.html

or make map PROJECT=/path (writes ./codemap-out/codemap.html).

AI mistakes log

While Claude works through the MCP server, every name it asks for that does not exist is logged: a class, method, column, table, route, component, translation key or endpoint it wrote in verify (claims or a PHP snippet), and lookups (search_symbol, get_symbol, get_component, get_schema table, find_usages, api_usage endpoint) that came back found: false, with the closest real name. The log is misses.jsonl next to each app’s index in the data volume; it never leaves it.

  • codemap misses ranks them, most frequent first, with the real name and an idea: a name the AI keeps guessing wrong (orders.order_status when the column is orders.status) is often unclear to people too, so it is worth renaming or a line in CLAUDE.md; a name with nothing like it means the AI assumed something that was never built.
  • Every entry is asked again when the list is read, so names created since (the AI checked first, then built it) drop out.
  • project_overview lists the ones made more than once (often_mistaken), so Claude avoids them in the next session.
  • The code map has an AI mistakes tab, and marks the real item each wrong name was closest to.
  • codemap misses --clear empties the log.

Wrong-answer reports

When a codemap answer turns out to be wrong, the AI reports it with report_wrong_answer, pointing at the answer by its meta.call_id (every answer has one; the server keeps the last 50). The report keeps codemap’s real answer, what is right, the evidence (file:line), the codemap version and the git commit, and optionally:

  • an expect check codemap re-runs: { "verify": { "columns": ["orders.collected_at"] }, "is": "exists" }, or { "tool": "get_schema", "args": { "table": "orders" }, "contains": "collected_at" } (or "lacks"). When it passes, the report closes, noting the version that fixed it.
  • a made-up example: the smallest project that makes codemap give the same wrong answer, with neutral names (Post, posts.title) and none of the real code. codemap indexes it in a temporary folder and keeps it only if it really reproduces, and blocks it when it contains a name from your project, a long line copied from your files, or something that looks like a secret, an email, an IP address or a private link.

codemap also records some wrong answers itself: an answer saying something is missing that a later answer finds with no file changed in between, a tool that fails, and an answer slower than 5 seconds with nothing to re-index.

Reports stay in reports.jsonl next to each app’s index (at most 2 MB). project_overview and codemap doctor show how many are open.

codemap reports                 # open reports, re-checked, most frequent first
codemap reports --all           # with the closed ones and the version that fixed them
codemap reports --export dir    # one folder per report: report, cited lines, example, test name
codemap reports --close r_8f3a  # not a real bug
codemap reports --send r_8f3a   # show the issue text and ask y/n before sending
codemap reports --clear

Sending to the codemap repository. A report whose made-up example reproduces can become a GitHub issue on syahrul84/codemap, so the bug gets fixed for everyone:

  • codemap asks the person, never the AI: through the AI app’s question form (MCP elicitation) when the app supports it, or with codemap reports --send in a terminal. No answer means nothing is sent.
  • Only the made-up example, codemap’s answer on it, the codemap version, the operating system and the project type (e.g. “Laravel + Nuxt”) are sent. Your code, file paths and names, and codemap’s answers about your project are never sent.
  • It is filed with your own GitHub login through gh; a matching open issue gets a comment instead of a new issue. Without gh, you get a filled-in link to submit yourself.
  • Turn it off with [report] send = "never" in .codemap.toml, CODEMAP_NO_REPORT_SEND=1, or “don’t ask again” in the question (which means don’t send).

Renamed from pidx

The tool used to be called pidx. The old names keep working: .pidx.toml is read when there is no .codemap.toml, PIDX_* variables are used when the CODEMAP_* ones are unset (PIDX_DB_LOCAL too), pidx-ignore comments are honoured, and make build-image also tags the image pidx:latest, so existing MCP configs keep starting. Indexes are rebuilt once under the new codemap-data volume name if you switch to it.

Several apps in one folder (workspace)

Mount the folder that holds the apps (e.g. shop/ with api/, storefront/ and admin-app/):

"args": ["run", "-i", "--rm",
         "-v", "${PWD}:/workspace:ro", "-v", "codemap-data:/data",
         "codemap:latest", "serve", "--project", "/workspace", "--name", "shop"]

When the folder is not itself a project, codemap looks one level down for Laravel apps (artisan + composer.json) and frontend apps (package.json with vue, nuxt, next or react). To choose the apps yourself, list them in the workspace’s .codemap.toml: [workspace] apps = ["api", "storefront", "admin-app"].

How a workspace works:

  • Each app keeps its own index.
  • Vue / Nuxt apps are linked to the workspace’s Laravel apps automatically, sharing their index.
  • Every tool takes an optional app. Without it, the tool runs on every app:
    • an answer found in one app comes back tagged with that app
    • answers found in several apps are grouped per app
    • verify merges item by item
  • For find_usages with changed_files, give paths from the workspace root (api/app/Models/Order.php).
  • Settings in the workspace’s .codemap.toml ([i18n] locales, [[links]]) apply to every app unless the app’s own .codemap.toml sets them.

Linking a frontend to its backend

When the Vue / Nuxt app and the Laravel API live in separate repositories, link them. Mount the backend read-only and name it:

"args": ["run", "-i", "--rm",
         "-v", "${PWD}:/workspace:ro",
         "-v", "${PWD}/../backend:/links/backend:ro",
         "-v", "codemap-data:/data",
         "codemap:latest", "serve", "--project", "/workspace", "--link", "backend=/links/backend"]

Or put the link in the frontend’s .codemap.toml ([[links]] name = "backend", path = "../backend", optional api_base = "/api/v1"). CODEMAP_LINKS=backend=/links/backend works too.

How linking works:

  • The backend’s index is kept up to date in the same data volume; the backend repository is only read.
  • The API base path (e.g. /api/v1 when the frontend calls /orders) is inferred by trying the route prefixes shared by many routes and keeping the one that explains the most calls.
  • Small URL builders are evaluated, including their arguments and defaults: const url = (id, suffix = '') => `/outlets/${id}/templates${suffix}`.
  • Project wrappers that build the URL from their own parameters are filled in with the caller’s arguments: api.get(endpoint, id, '/history') with get(endpoint, id, suffix = '') { return http.get(`${endpoint}/${id}${suffix}`) } is {endpoint}/{id}/history, in object methods or plain functions, imported or local.
  • When the caller’s part is a variable, a route parameter is preferred over a fixed word among routes with the right method (/orders/{} is orders/{order}, not orders/export).
  • URLs that start with an unknown variable are matched by their known tail. When that is ambiguous, the call is reported as unresolved (with file:line, why, and the candidate routes) rather than missing.

How the response check works:

  • Fields read are found heuristically: property reads on a call’s result, template reads (including v-for) of the state the result is stored in, what child components read from props the data is passed to, and what callers of a helper function read from its return value.
  • Functions that return a bundle of helpers (return { list, remove }) are not mistaken for response data.
  • “Read but not returned” is only reported when the endpoint’s response is fully known: an API Resource, a literal array, or a list whose every building step is readable. Item keys are checked one level deep. Responses built from queries are reported by kind only.
  • Laravel’s _method spoofing (POST with _method: 'PUT') is understood.

Configuration — .codemap.toml (optional, project root)

[project]
name = "shop-api"            # display name (default: composer.json name, then git repo name)

[scan]
# Default: Laravel dirs plus Vue/Nuxt source dirs (missing ones are skipped):
include = ["app", "routes", "database", "config", "bootstrap", "lang", "src", "resources/js",
           "components", "pages", "layouts", "composables", "stores", "store", "plugins",
           "middleware", "utils", "mixins", "i18n", "locales", "tests", "test",
           "resources/lang", "resources/views"]
exclude = ["/vendor", "node_modules", "/storage", "/public", "/bootstrap/cache", "/dist", "/.output", "/coverage"]

[laravel]
artisan = "off"              # "off" | "auto" | "on"
artisan_command = "php artisan"
artisan_timeout_ms = 15000

[freshness]
check_interval_ms = 2000

[i18n]
locales = ["en", "ms"]       # locales you ship; others are reported as partial, not missing

[tests]                      # how tests are run; test files are appended
php = "docker compose exec -T app php artisan test"
# Default for PHP: when a docker-compose file (in the app, its parent folder or a
# sibling folder) mounts the app into a service, `docker compose exec -T 
# php artisan test`; otherwise `php artisan test` on the host.
# js = "pnpm exec vitest run"
# browser = "pnpm exec playwright test"
# In a workspace's .codemap.toml: [workspace.tests.] with the same keys.

[copy]                       # house copy rules, checked by the release check
banned = { ms = ["beg"], en = ["bag", "bags", "save food"], all = ["rescue"] }

[release]                    # release-check lines to leave out (substring match)
ignore = ["ALLOW_DB_WIPE"]

[[databases]]                # compare migrations with real databases (read-only)
name = "local"
env = "CODEMAP_DB_LOCAL"        # the URL comes from this environment variable, never from a file
[[databases]]
name = "staging"
env = "CODEMAP_DB_STAGING"
# app = "api"                # in a workspace: only for this app (default: every Laravel app)
# table = "migrations"       # default: config/database.php, else `migrations`

Background work and permissions

  • get_schema with background: true: the schedule (command, job or closure, frequency, the command’s class), queued jobs and where they are dispatched, events with their listeners and where they are fired, notifications and mails and where they are sent (or “nothing sends it”).
  • list_routes with permissions: true: per route, the middleware that can decide access, checks inside the action (authorize, Gate::…, ->can(…), abort_if), the Form Request’s authorize(), and the policy method a can: middleware or check lands on; routes with none are marked public.
  • list_pages with permissions: true: for admin (Filament) pages, the model’s policy methods and canAccess() / canViewAny() overrides.

Cache map

get_schema with cache: true lists every cache key (or key pattern, brands:{}, with constants resolved) with where it is written and its TTL, where it is read and where it is cleared (forget, tagged or full flush). It flags keys stored forever that nothing clears, keys read but never written, and keys cleared but never written (a typo or leftover). key_prefix narrows it.

Release check

verify with release_check: true returns only problems, per app (and one clean verdict for a workspace):

  • frontend calls to missing endpoints, wrong methods, fields read but not returned
  • translation keys missing in a shipped locale, or used but defined nowhere
  • unused code added on the branch (compared with origin/HEAD, main, master or develop, or base): components nothing renders, functions and classes nothing uses
  • migrations not run on the configured databases
  • config keys read but not defined, env() in app code, new env variables without a default missing from .env.example
  • settings read without a default that nothing in the app saves (a setting saved by code or an admin page, like a “last alert time”, is empty until then by design: it is listed under low_priority)
  • house copy rules: words banned per locale ([copy] banned), in translation files and admin labels
  • validation messages that would show a raw field key: a nested field (translations.*.description) whose last part has a name in validation.attributes (in that language, or in another shipped language when this one names nothing, like Laravel’s default en) but the full key does not, so its errors show the raw path (“translations…description …”)

Each app’s answer has problems, low_priority (worth a look, not blocking: e.g. fields a frontend type lists that the endpoint doesn’t send and nothing reads, or form fields with no translated name), notes and ignored. A line is ignored when it matches [release] ignore, or when the source line it points at (or the line above) has a codemap-ignore comment:

// codemap-ignore: only set on throwaway test databases
if (env('ALLOW_DB_WIPE')) { … }

Translations: empty lists in a lang file ('items' => []) are not keys; uses with a fallback ($t('a.b', 'Default text')) or behind an existence check (Lang::has('a.b'), $key === 'x') are not reported as missing; sentences used as Laravel keys (__('Save changes')) are plain text; a one-word key with no translation (__('canceled')) is reported, because speakers of the other shipped languages see the word as written. verify with claims: {"i18n_keys": ["*"]} lists every translation problem one row each.

Release steps, file rules, leftovers and live-data tests

The release check also answers:

  • release_steps: what the server needs for this release, from what changed on the branch: composer install (dependencies changed) or composer dump-autoload --optimize (classes added, deleted or renamed), php artisan migrate --force (new migrations), php artisan optimize (config, routes or listeners changed), filament:optimize, horizon:terminate or queue:restart (workers keep the old code), octane:reload or a PHP-FPM reload, the frontend install and build, restarting a Nuxt / Next.js server, and environment variables new in .env.example (public ones are read at build time). Each step says which files call for it.

  • rules: your project’s rules tied to files. In .codemap.toml:

    [[rules]]
    files = ["database/migrations/**"]
    when = "modified"            # added | modified | deleted | renamed | changed (default)
    note = "Never edit a migration that has already run; add a new one."
    # level = "problem"          # make the release check fail on it
    

    A rule whose files change comes back in the release check and in find_usages change impact. In a workspace’s file, app = "backend" limits a rule to one app.

  • Leftover mentions: classes, components, functions and methods deleted or renamed on the branch that are still named somewhere. Mentions in code and tests are problems; in comments and docs, low priority. A name that is still defined elsewhere (moved, or another class’s method of the same name) is not reported.

  • Tests that depend on live data (low priority, and marked in change impact as tests_on_live_data): browser tests that check page content without seeding, API setup or mocked responses, and PHP feature tests that check data without factories, seeders or inserts (in the test or its parent test class). They pass only while the database holds the right records.

New type errors only

codemap does not run the type check; Claude does, and passes the output to verify:

{ "app": "consumer", "type_errors": "" }

The first output becomes the starting point (kept next to the app’s index). After that, only errors that are new come back (new, new_total), with how many were fixed since. Line numbers are ignored when comparing, so an old error that moved is not new. Add "type_errors_save": true to make the current output the new starting point.

Migrations not yet applied

get_schema with migrations: true (and codemap doctor) compares the migration files with each database’s migrations table: migrations a database has not run yet, and rows whose file is gone (squashed into a schema dump, or deleted).

  • Connections come only from environment variables you pass to the container: the ones named in [[databases]], plus any CODEMAP_DB_ (found automatically). .env is never read.
  • Each check is a single SELECT on the migrations table in a read-only session, with a 5-second timeout. Passwords never appear in answers or errors.
  • PostgreSQL (postgres://user:pass@host:5432/db), MySQL / MariaDB (mysql://…) and SQLite (sqlite:///path). TLS is not used: reach staging through a tunnel or VPN.
  • A database whose rows mostly don’t match the app’s migration files is reported as other_app rather than listing false “pending” migrations.

Pass the URLs and, when the database runs in Docker, its network:

"args": ["run", "-i", "--rm", "--network", "myapp_default",
         "-e", "CODEMAP_DB_LOCAL", "-e", "CODEMAP_DB_STAGING",
         "-v", "${PWD}:/workspace:ro", "-v", "codemap-data:/data",
         "codemap:latest", "serve", "--project", "/workspace"],
"env": { "CODEMAP_DB_LOCAL": "postgres://user:pass@postgres:5432/app",
         "CODEMAP_DB_STAGING": "postgres://user:[email protected]:15432/app" }

Exclude patterns follow .gitignore rules. A leading / anchors a pattern to the project root, so a source folder that happens to be called vendor or public is still indexed. .env*, vendor/, storage/, bootstrap/cache/, node_modules/, *.d.ts and minified *.min.* bundles are never indexed. Secrets are never read.

Artisan mode

With artisan = "on" (or "auto" when php and vendor/ exist), codemap runs php artisan route:list --json and uses it as the source of truth, keeping file:line from static analysis. Laravel’s cache paths are pointed at a temporary directory, so this works on a read-only mount. Any failure or timeout falls back to static routes, with a warning.

This boots your application, which means it executes project PHP code, so it is off by default. In Docker it needs the PHP image: docker build -t codemap-php:latest --target runtime-php .

What is (and isn’t) understood

Routes: these are evaluated statically:

  • verbs, match, any, view, redirect, fallback
  • name / prefix / middleware / withoutMiddleware / namespace / controller / domain / where* / can, fluent and array-style groups, nested groups
  • require / include of other route files
  • resource, apiResource, resources, singleton with only, except, names, parameters, shallow, nesting
  • 'Controller@method' strings resolved through the provider’s $namespace

Routes inside loops, with variable URIs or with unknown macros are kept and marked unresolved. Routes inside if are kept and flagged as conditional.

Schema:

  • Schema::create / table / rename / drop* and the column types and modifiers from Laravel 9–13
  • change(): before Laravel 11 unstated attributes are kept; from 11 the column is redefined
  • table names read from config() (including packages’ default config, e.g. spatie/laravel-permission), class constants, properties, foreach over literal arrays, and string interpolation

Raw DB::statement() SQL is not parsed and is reported. Code inside conditions is applied with confidence: low.

Vue / Nuxt:

  • .vue files are split into their blocks.
  • Scripts are parsed with oxc. This covers JS, TS, `` macros (defineProps with runtime or type arguments, including local interfaces), withDefaults, destructured defaults, defineEmits, defineModel, defineSlots, defineOptions and definePageMeta.
  • Templates are read with a small tokenizer.

Some things are reported, not guessed:

  • props typed with an imported interface (props_unresolved)
  • Pug templates
  • component names built at runtime

Routes built at runtime (from .map(), loops, or helpers that aren’t simple) are listed as (dynamic) with the code that builds them.

Find usages:

  • PHP references come from the same parse as symbols: new, static calls, $this-> calls, calls on variables typed by parameters, closures or new, type hints, instanceof, catch, X::class, route('name'), helper functions and test HTTP calls.
  • JS / Vue references come from resolved imports (including lazy ones), component tags, and calls by name (Nuxt auto-imports).
  • Column usages are not tracked yet.

Not yet: column usages, config / settings defaults, embeddings and semantic search.

Troubleshooting

  • The index is empty or “no indexable files found”: Docker Desktop only mounts shared host paths, and an unshared one (e.g. /private/tmp) appears as an empty folder. Use a path under your home folder.
  • fatal: detected dubious ownership: the image already marks /workspace as a safe git directory. If you mount the project elsewhere, add git config --system --add safe.directory to your own image.
  • git_dirty / slow refreshes on macOS: codemap calls git status with core.checkStat=minimal, because inside a container the host’s inode/device numbers don’t match git’s index. Without that, git re-reads every tracked file (seconds on large repositories).
  • Responses say stale: true: re-indexing took more than 1 s (e.g. right after a large git checkout). The next call is usually fresh; meanwhile Claude should re-check files directly.
  • Routes look wrong: run codemap doctor, which shows where routes were read from and why. If the app can boot, consider artisan mode.
  • A docker build fails once and then succeeds: seen twice during development without a reproducible cause. Re-run the build; if it persists, run docker build --target runtime . 2>&1 | tail -50 and open an issue with the output.

Development

Everything runs in Docker; don’t run cargo on the host.

Command What it does
make dev-image Build the dev toolchain image (Rust 1.99, rustfmt, clippy, cargo-insta, git)
make lint cargo fmt --check + cargo clippy -D warnings
make test cargo test (unit, fixture snapshots, MCP-over-stdio integration)
make check lint + test
make build-image / make build-image-php Runtime images
make shell Shell in the dev container

Performance smoke tests (ignored by default): docker compose run --rm dev cargo test --release -p codemap-php --test index -- --ignored --nocapture. Measured: 2,000 classes / 82,000 symbols cold in about 1 s, no-op refresh about 10 ms.

Layout

crates/codemap-core     store (SQLite + migrations), scanner, git, symbols + search, envelope, adapter trait
crates/codemap-php      tree-sitter-php parsing, name resolution, symbol extraction
crates/codemap-laravel  detection, routes (static + artisan), models, schema replay, Form Requests, doctor
crates/codemap-js       oxc JS/TS analysis, Vue SFC splitter, template tokenizer, literal reader
crates/codemap-vue      Vue/Nuxt detection, component naming and registry, stores, get_component, doctor
crates/codemap-mcp      MCP server (rmcp, stdio) and the tool implementations shared with `codemap query`
crates/codemap-cli      the `codemap` binary
fixtures/            hand-written Laravel, Vue 2, Vue 3, Nuxt 2 and Nuxt 3 projects used by the tests

Extractor versions (PHP_EXTRACTOR_VERSION, ROUTES_EXTRACTOR_VERSION, DATA_EXTRACTOR_VERSION, DETECTION_VERSION, JS_EXTRACTOR_VERSION, REGISTRY_VERSION, VUE_DETECTION_VERSION) make existing indexes re-extract after an upgrade. Bump the relevant one whenever extraction output changes.

License

MIT — see LICENSE.

View this README on GitHub

Install

docker run -i --rm -v ${PWD}:/workspace:ro -v codemap-data:/data codemap:latest serve --project /workspace

Configuration

{ "mcpServers": { "codemap": { "command": "codemap", "args": ["serve", "--project", "${PWD}"] } } }