Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ claude mcp add devmap -- npx -y @flaid/devmap mcp
}
```

`devmap init --mcp opencode,claude,cursor` writes this entry for you
(`opencode.json`, `.mcp.json`, `.cursor/mcp.json`) and merges into an existing
file without touching other servers. Interactive `devmap init` offers it too.

The server is read-only and static by default (no AI provider is contacted unless
you start it with `--allow-ai`).

Expand Down Expand Up @@ -298,7 +302,7 @@ Use your preferred AI tool to change it.

DevMap is free and open source.

AI features require a provider API key. DevMap uses Groq by default — analysis runs on free-tier infrastructure.
The API key is optional. Static analysis, the generated files and the MCP server work without one; AI-written narration and `devmap explain` need a provider key. DevMap uses Groq by default — analysis runs on free-tier infrastructure.

| Provider | Status |
| ------------------------------- | ------- |
Expand All @@ -313,6 +317,10 @@ the selected model. For OpenRouter, pressing Enter at
model ID uses that free or paid model instead. Change it later with
`devmap config model <model-id>`.

Press Enter at the key prompt to skip AI and add a key later with
`devmap config key` (or `devmap config key <key>`; it also reads
`GROQ_API_KEY` / `OPENROUTER_API_KEY`).

API keys are stored locally:

```txt
Expand Down
75 changes: 59 additions & 16 deletions docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ configuration command:
* `devmap explain`
* `devmap mcp`
* `devmap doctor`
* `devmap config key`
* `devmap config model`

---
Expand All @@ -33,13 +34,19 @@ It should be run once per machine/project, or again when changing provider crede

```bash
devmap init
devmap init --mcp opencode,claude,cursor # also register the MCP server
```

The API key is optional. Press Enter at the key prompt (or run without
`GROQ_API_KEY` / `OPENROUTER_API_KEY` in a non-interactive shell) to skip AI
setup: the project files are still created and nothing is written to
`~/.devmap/config.json`. Add a key later with `devmap config key`.

### Responsibilities

* Confirm AI provider
* Input API key or read environment variable
* Validate API key
* Input API key or read environment variable (optional, Enter skips)
* Validate API key when one is given
* Ask for a Groq model from the provider model list
* Ask for an OpenRouter model; Enter defaults to `openrouter/free`
* Save global configuration to `~/.devmap/config.json`
Expand All @@ -48,6 +55,9 @@ devmap init
* Add `.devmap/` to `.gitignore`
* Generate `DEVMAP.md`
* Handle `AGENTS.md` safely
* Optionally register the MCP server (`--mcp`, or the interactive prompt):
`opencode.json`, `.mcp.json` or `.cursor/mcp.json`. An existing file is merged,
never overwritten, and a file that is not plain JSON is left alone

### Flow

Expand Down Expand Up @@ -360,7 +370,7 @@ It should help answer:
```bash
devmap flow # curated top flows from the snapshot
devmap flow --all # uncapped: also medium-confidence features and non-API routes
devmap flow authentication # one specific flow (match is case-insensitive)
devmap flow authentication # one specific flow (case-insensitive; falls back to the uncapped list when it isn't curated)
devmap flow --json
```

Expand Down Expand Up @@ -511,11 +521,19 @@ devmap doctor
* API key status
* Selected model, annotated with its source: `(project override)` when a
`.devmap/config.local.json` model is active, `(global)` otherwise
* Snapshot status
* Snapshot status and age (`up to date` or `stale`)
* MCP server self-check: the server is started in-process and its tools are listed
(nothing is written, stdio is not touched)
* MCP clients: which of `opencode.json`, `.mcp.json` and `.cursor/mcp.json`
already register `devmap`
* Project detection
* OS/platform
* Permission issues

A missing API key is not an issue because AI is optional; it is reported as a
note with the command that fixes it (`devmap config key`). A failing MCP
self-check is an issue.

### Output — No Issues

```txt
Expand All @@ -531,6 +549,9 @@ Package Manager npm
Provider groq
Config exists
Snapshot valid
Snapshot age up to date
MCP server ready (7 tools)
MCP clients opencode
API key valid
Model openai/gpt-oss-20b (project override)

Expand All @@ -549,19 +570,23 @@ Project my-app
Framework next.js
Workspace Frameworks express
Package Manager npm
Provider not configured
Config missing
Snapshot stale
API key missing
Model not configured
Provider groq
Config exists
Snapshot valid
Snapshot age stale
MCP server failed
MCP clients none registered
API key invalid or unreachable
Model openai/gpt-oss-20b (global)

Notes

Issues found:
The code changed since the last analysis. Run devmap analyze, or let your agent call devmap_analyze.
No agent is registered for the MCP server. Run: devmap init --mcp opencode|claude|cursor

Provider is not configured
Run devmap init to set up an AI provider.
Issues

Snapshot is stale
Run devmap analyze to refresh the snapshot.
The MCP server failed its self-check: <reason>.
```

### Rules
Expand All @@ -573,6 +598,24 @@ Snapshot is stale

---

## `devmap config key`

Set or replace the AI provider API key. AI is optional, so this is how a key
gets added after `devmap init` was run without one.

```bash
devmap config key # reads GROQ_API_KEY / OPENROUTER_API_KEY, else prompts
devmap config key gsk_xxx
devmap config key sk-or-xxx --provider openrouter
```

The key is validated against the provider before it is saved. The configured
provider and model are kept; with no config yet it defaults to Groq and
automatic model routing. A custom endpoint needs `devmap init` first because it
needs a base URL.

---

## `devmap config model`

Set a global model override for AI-powered commands.
Expand Down Expand Up @@ -679,8 +722,8 @@ Contract:
* runtime failures return a JSON object with `status`, `error`, and optional `hint`
* `devmap mcp` is the exception: it has no `--json` flag because stdout is the
MCP protocol stream
* `init --json` never prompts and therefore requires `GROQ_API_KEY`,
`OPENROUTER_API_KEY`, or an existing API key
* `init --json` never prompts; without `GROQ_API_KEY`, `OPENROUTER_API_KEY` or an
existing API key it skips AI setup (`aiConfigured: false`)
* package-manager wrapper warnings may appear on stderr and are not part of the
DevMap JSON document

Expand Down
19 changes: 10 additions & 9 deletions packages/cli/src/analyzers/detectors/trpcRouteDetector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,21 +121,22 @@ function parseRouterFile(

const routerBody = content.slice(startIdx - 1, endIdx);

// Extract procedures by finding "name: publicProcedure" then scanning forward for .query or .mutation
const procStartPattern = /(\w+)\s*:\s*publicProcedure\b/g;
// A procedure starts at "name: <base>Procedure" (publicProcedure, protectedProcedure,
// t.procedure, ...) and its kind is the first .query/.mutation before the next procedure.
const procStartPattern = /(\w+)\s*:\s*(?:\w+\.)?\w*[pP]rocedure\b/g;
const starts = [...routerBody.matchAll(procStartPattern)];
const procedures: Array<{ name: string; kind: ProcedureKind }> = [];
let procStartMatch = procStartPattern.exec(routerBody);
while (procStartMatch) {
const procName = procStartMatch[1];
const afterStart = routerBody.slice(procStartMatch.index + procStartMatch[0].length);
const kindMatch = afterStart.match(/\.(query|mutation)\s*\(/);
for (let index = 0; index < starts.length; index++) {
const start = starts[index];
const segmentStart = (start.index ?? 0) + start[0].length;
const segmentEnd = starts[index + 1]?.index ?? routerBody.length;
const kindMatch = routerBody.slice(segmentStart, segmentEnd).match(/\.(query|mutation)\s*\(/);
if (kindMatch) {
procedures.push({
name: procName,
name: start[1],
kind: kindMatch[1].toUpperCase() as ProcedureKind
});
}
procStartMatch = procStartPattern.exec(routerBody);
}

// Extract sub-router references: "subRouter" used as values
Expand Down
6 changes: 4 additions & 2 deletions packages/cli/src/analyzers/pipeline/projectMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -928,17 +928,19 @@ function generateMinimalFlows(
];
}

const CONFIDENCE_RANK: Record<FeatureInfo["confidence"], number> = { low: 0, medium: 1, high: 2 };

export function generateFeatureFlows(
features: FeatureInfo[],
fileIndex: Record<string, FileIndexEntry>,
options: { limit?: number; minConfidence?: "high" | "medium" } = {}
): FlowInfo[] {
const limit = options.limit ?? 3;
const minConfidence = options.minConfidence ?? "high";
const minimumRank = CONFIDENCE_RANK[options.minConfidence ?? "high"];

return features
.filter((feature) =>
feature.confidence === minConfidence
CONFIDENCE_RANK[feature.confidence] >= minimumRank
&& feature.businessFlow.length > 1
&& !feature.businessFlow.some((step) => /^Identify files related to /i.test(step))
)
Expand Down
24 changes: 21 additions & 3 deletions packages/cli/src/analyzers/pipeline/projectMetadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { isArchitectureSource } from "../graph/index.js";

export type ProjectLanguage = "typescript" | "javascript" | "mixed" | "unknown";
export type PackageManager = "pnpm" | "npm" | "yarn" | "bun" | "unknown";
export type ProjectType = "node-cli" | "web-app" | "api-service" | "library" | "unknown";
export type ProjectType = "node-cli" | "web-app" | "api-service" | "library" | "vscode-extension" | "unknown";

export type WorkspaceType = "monorepo" | "single-package";

Expand Down Expand Up @@ -60,6 +60,8 @@ type PackageManifest = {
bin?: unknown;
main?: unknown;
exports?: unknown;
engines?: unknown;
contributes?: unknown;
workspaces?: unknown;
packageManager?: unknown;
dependencies: Record<string, string>;
Expand All @@ -79,6 +81,8 @@ function readPackageManifests(files: ScannedFile[]): PackageManifest[] {
bin: parsed.bin,
main: parsed.main,
exports: parsed.exports,
engines: parsed.engines,
contributes: parsed.contributes,
workspaces: parsed.workspaces,
packageManager: parsed.packageManager,
dependencies: isStringRecord(parsed.dependencies) ? parsed.dependencies : {},
Expand Down Expand Up @@ -171,17 +175,31 @@ function detectProjectTypes(
if (manifests.some((manifest) => manifest.bin)) {
types.add("node-cli");
}
if (manifests.some(isVscodeExtension)) {
types.add("vscode-extension");
}
// A package that ships an app framework with its default "main" is an app,
// not a library.
// not a library. An editor extension's "main" is its activation entry, not a public API.
if (manifests.some((manifest) =>
(manifest.exports || manifest.main) && !manifest.bin && !declaresAnyFramework(manifest)
(manifest.exports || manifest.main)
&& !manifest.bin
&& !declaresAnyFramework(manifest)
&& !isVscodeExtension(manifest)
)) {
types.add("library");
}

return types.size > 0 ? [...types] : ["unknown"];
}

function isVscodeExtension(manifest: PackageManifest): boolean {
const engines = manifest.engines;
const declaresVscodeEngine = typeof engines === "object"
&& engines !== null
&& "vscode" in engines;
return declaresVscodeEngine || Boolean(manifest.contributes);
}

function selectPrimaryManifest(
manifests: PackageManifest[],
projectTypes: ProjectType[]
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/cache/agentNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ function describeProjectKind(snapshot: ProjectMap): string {
if (types.includes("web-app")) kinds.push("web application");
if (types.includes("api-service")) kinds.push("API service");
if (types.includes("library")) kinds.push("reusable library");
if (types.includes("vscode-extension")) kinds.push("VS Code extension");

if (kinds.length === 0) return workspace;
if (kinds.length === 1) {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/analyze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ async function printOrGenerateInterpretation(
?? (() => resolveEffectiveConfig(projectRoot));
const config = await loadConfig();
if (!config?.apiKey) {
output.note("AI architecture interpretation is not configured. Run devmap init to enable it.");
output.note("AI architecture interpretation is not configured. Run devmap config key to enable it.");
return snapshot;
}

Expand Down
Loading
Loading