diff --git a/AGENTS.md b/AGENTS.md index d617a06..710641d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -75,3 +75,14 @@ working on DevMap. Update the relevant document whenever implementation or verification changes its contents. Do not duplicate these personal notes into the public `docs/` folder unless the information is intended for users or contributors. + + +## DevMap Context + +Before working in this repository, read `DEVMAP.md` first. +Read `.devmap/index.json` first, then the relevant +`.devmap/features/*.json` map. Inspect files from `sourcePriority` before +exploring broadly. Use `.devmap/snapshot.json` only when those lightweight +navigation files are insufficient. If the navigation files are missing, run +`devmap analyze`. + diff --git a/DEVMAP.md b/DEVMAP.md new file mode 100644 index 0000000..56c6c0f --- /dev/null +++ b/DEVMAP.md @@ -0,0 +1,86 @@ +# DevMap + +This repository uses DevMap to create reusable project context for developers and AI agents. + +## Project Context + +- Detected framework: astro +- Agent navigation index: `.devmap/index.json` +- Feature maps: `.devmap/features/*.json` +- Full analysis archive: `.devmap/snapshot.json` +- DevMap config: `~/.devmap/config.json` + +## Recommended Workflow + +1. Run `devmap analyze` after cloning the repository. +2. Run `devmap analyze --fresh` after major architecture changes. +3. Use `devmap ask ""` before manually exploring unrelated files. +4. Treat DevMap output as an architecture overview, then verify critical behavior in source code. + +## Commands + +```bash +devmap analyze +devmap analyze --json +devmap ask "how does authentication work?" +devmap ask "where is authentication handled?" --json +devmap doctor +devmap doctor --json +``` + +## Agent Navigation Contract + +This repository uses DevMap as the primary navigation source. Use the lightweight +navigation files before broad repository exploration. + +Preferred reading order: + +1. Read `.devmap/index.json`. +2. Pick the relevant feature using its name and keywords. +3. Open the matching `.devmap/features/*.json` map. +4. Inspect only the files listed in `sourcePriority` first. +5. Read `.devmap/snapshot.json` only when the index and feature maps are + insufficient or full archive/debug context is required. + +Do not scan the whole repository first. + +Open source files only when: + +- the snapshot is missing; +- the snapshot is stale; +- the snapshot does not contain enough detail; +- exact implementation is required; +- the task is edit, debug, or refactor; +- the user explicitly asks for code changes. + +When source inspection is needed, inspect the smallest relevant set first. +Prefer feature entry points and flow steps over broad folder exploration. + +## Required Agent Workflow + +1. Read `DEVMAP.md`. +2. Read `.devmap/index.json`. +3. Open the relevant feature map. +4. Inspect at most the smallest relevant source-file set from `sourcePriority`. +5. Explain which navigation entry guided the decision when giving advice. +6. Avoid unrelated files unless the navigation data is incomplete or exact + code verification is required. + +If `.devmap/index.json` or `.devmap/snapshot.json` is missing, run +`devmap analyze` when DevMap is available and configured. If analyze fails +because DevMap is not initialized, ask the user to run `devmap init` and then +`devmap analyze`. + +If the snapshot may be stale, run `devmap analyze --fresh` before relying on +it. + +Use `--json` when calling DevMap programmatically so stdout remains one +parseable JSON document without ANSI or terminal decoration. + +Do not edit generated files inside `.devmap/`. + +## Repository Safety + +- `.devmap/` is local generated state and should stay out of Git. +- Never commit API keys or provider credentials. +- DevMap helps locate relevant code; it does not replace source-level verification. diff --git a/PRD.md b/PRD.md index 5f33edb..49f479b 100644 --- a/PRD.md +++ b/PRD.md @@ -327,16 +327,11 @@ Run static analysis, generate project snapshot, and output a readable project ov ```bash devmap analyze -devmap analyze --deep ``` -**`--deep` flag:** - -| | Standard | `--deep` | -|---|---|---| -| Analysis scope | Project-level overview | Per-module detailed explanation | -| AI usage | Lower | Higher | -| Best for | Quick mapping | Large/unfamiliar projects | +`devmap analyze` uses the provider and model stored in +`~/.devmap/config.json`. Groq users choose a model during setup and can change +it later with `devmap config model `. **Generated files:** @@ -576,9 +571,7 @@ ai/ | Command | Model | Reason | |---|---|---| | `analyze` | `openai/gpt-oss-20b` | Balanced architecture interpretation | -| `analyze --deep` | `openai/gpt-oss-120b` | Heavy cross-module reasoning | | `analyze` fallbacks | `qwen/qwen3.6-27b` -> `llama-3.3-70b-versatile` -> `llama-3.1-8b-instant` | Keep snapshot enrichment available across model-specific limits | -| `analyze --deep` fallbacks | `llama-3.3-70b-versatile` -> `qwen/qwen3.6-27b` -> `openai/gpt-oss-20b` | Degrade heavy reasoning gradually instead of failing immediately | DevMap retries a rate-limited model up to three times, then advances through the command-specific chain. It also advances when a model is unavailable or @@ -591,6 +584,10 @@ Model availability changes over time. Before changing the default routing, verify the current Groq model list and lifecycle status. Preview models must not be used as a primary default for a public DevMap release. +Groq setup lists the currently available Groq models after validating the API +key. Users choose one with arrow keys and Enter; the selected model is stored +as the user's preferred model. + OpenRouter setup asks for a model ID after validating the API key. Pressing Enter selects `openrouter/free`; entering another free or paid model stores that exact model as the user's preferred model. Explicit user selections take @@ -1017,7 +1014,7 @@ Rate limit reached. DevMap will retry in 12 seconds. -Tip: Use standard analyze instead of --deep for lower token usage. +Tip: Choose a smaller model with devmap config model for lower token usage. ``` All error messages must be: diff --git a/README.md b/README.md index 5cc5e57..4496357 100644 --- a/README.md +++ b/README.md @@ -253,10 +253,11 @@ AI features require a provider API key. DevMap uses Groq by default — analysis | OpenAI | Planned | | Gemini | Planned | -`devmap init` lets you choose Groq or OpenRouter with the arrow keys. For -OpenRouter, pressing Enter at `OpenRouter model [openrouter/free]:` keeps the -free router; typing another model ID uses that free or paid model instead. -Change it later with `devmap config model `. +`devmap init` lets you choose Groq or OpenRouter with the arrow keys. Groq +shows the available model list and stores the selected model. For OpenRouter, +pressing Enter at `OpenRouter model [openrouter/free]:` keeps the free router; +typing another model ID uses that free or paid model instead. Change it later +with `devmap config model `. API keys are stored locally: diff --git a/docs/architecture.md b/docs/architecture.md index 8f9f66b..407c481 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -643,7 +643,6 @@ MVP default model routing: | Command | Primary | Ordered fallbacks | | ---------------- | -------------------------- | ----------------- | | `analyze` | `openai/gpt-oss-20b` | `qwen/qwen3.6-27b` -> `llama-3.3-70b-versatile` -> `llama-3.1-8b-instant` | -| `analyze --deep` | `openai/gpt-oss-120b` | `llama-3.3-70b-versatile` -> `qwen/qwen3.6-27b` -> `openai/gpt-oss-20b` | Each model receives up to three exponential-backoff retries for HTTP 429. After those retries, or when a model is unavailable or returns HTTP 5xx, @@ -657,6 +656,10 @@ on 2026-06-20. Recheck provider lifecycle status before publishing a release. Users can override automatic routing with `devmap config model `. Running `devmap config model auto` restores the defaults above. +Groq setup lists available Groq models after API-key validation and stores the +selected model in the global config. The stored model is used as the primary +choice until the user changes it with `devmap config model`. + OpenRouter does not use the Groq command-routing table. During `devmap init`, Enter accepts `openrouter/free`, while a typed model ID is stored and used exactly as the user selected it. Explicit OpenRouter model choices receive no diff --git a/docs/commands.md b/docs/commands.md index efe8e63..62d1111 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -44,6 +44,7 @@ devmap init * Confirm AI provider * Input API key or read environment variable * Validate API key +* 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` * Detect current project framework @@ -120,17 +121,12 @@ Analyze the current project and generate a reusable project snapshot. ### Usage -```bash -devmap analyze -devmap analyze --deep -``` - -### Modes - -| Mode | Purpose | -| -------- | -------------------------------------- | -| Standard | Fast project overview | -| `--deep` | More detailed architecture explanation | +```bash +devmap analyze +``` + +`devmap analyze` uses the model stored in `~/.devmap/config.json`. Change the +stored model with `devmap config model `. ### Responsibilities @@ -219,33 +215,7 @@ Snapshot saved: ``` - -### Deep Output - -When using: - -```bash -devmap analyze --deep -``` - -DevMap adds a deeper module-level explanation. - -Example: - -```txt -Module Breakdown - -app/ -Main application routes and layouts. - -app/api/ -Server-side API routes. - -lib/ -Shared utilities, database access, authentication logic, and helpers. -``` - -### Rules +### Rules * Static analysis must run before AI interpretation * Do not send the entire project source to AI @@ -415,7 +385,10 @@ devmap config model auto `auto` restores command-based routing: * `analyze` uses `openai/gpt-oss-20b` -* `analyze --deep` uses `openai/gpt-oss-120b` + +For Groq, `devmap init` lists available models after API-key validation. Pick a +model with the arrow keys and press Enter. The selected model is stored in the +global config. For OpenRouter, `devmap init` prompts with: @@ -430,7 +403,6 @@ The typed model is stored as the primary choice and is not silently replaced. Automatic routing also uses ordered fallback chains: * `analyze`: `qwen/qwen3.6-27b`, `llama-3.3-70b-versatile`, then `llama-3.1-8b-instant` -* `analyze --deep`: `llama-3.3-70b-versatile`, `qwen/qwen3.6-27b`, then `openai/gpt-oss-20b` DevMap advances after model-unavailable and transient provider responses. For rate limits, it first retries the current model three times with exponential @@ -476,7 +448,6 @@ integration. ```bash devmap init --json devmap analyze --json -devmap analyze --deep --json devmap onboarding --json devmap doctor --json devmap config model auto --json diff --git a/docs/for-me-personal/PROGRESS.md b/docs/for-me-personal/PROGRESS.md index ed008f7..6f19f65 100644 --- a/docs/for-me-personal/PROGRESS.md +++ b/docs/for-me-personal/PROGRESS.md @@ -4,6 +4,24 @@ Terakhir diperbarui: 2026-06-23 ## Update 2026-06-23 +### Groq Model Picker And Analyze Deep Removal + +- `~/.devmap/config.json` tetap menyimpan `provider`, `apiKey`, dan `model`. + Config lama tanpa `provider` atau `model` dibaca sebagai `provider: "groq"` + dan `model: "auto"` agar tetap backward compatible. +- `devmap init` untuk Groq sekarang mengambil daftar model dari endpoint model + Groq setelah API key valid, lalu menampilkan picker arrow-key/Enter. +- Model Groq yang dipilih disimpan ke global config dan dapat diganti dengan + `devmap config model `. +- OpenRouter setup tidak diubah: tetap memakai prompt model text dengan default + `openrouter/free`. +- Flag `devmap analyze --deep` dihapus dari CLI dan dokumentasi aktif. Hasil + investigasi: analyzer static, snapshot, dan cache path-nya sama; perbedaan + sebelumnya hanya routing model/fallback, prompt/token limit, dan output + `Module Breakdown` kecil. + +--- + ### Ask Command — Complete Removal - **Seluruh fitur `devmap ask` dihapus permanen.** diff --git a/docs/for-me-personal/TEST.md b/docs/for-me-personal/TEST.md index c6da792..1b11c09 100644 --- a/docs/for-me-personal/TEST.md +++ b/docs/for-me-personal/TEST.md @@ -52,6 +52,25 @@ Remove-Item Env:OPENROUTER_API_KEY Jangan simpan atau menyalin API key nyata ke repository, snapshot, output test, atau dokumentasi debugging. +## Groq Model Picker And Analyze Command + +Focused automated tests: + +```powershell +pnpm --filter devmap exec tsx --test test/init-and-errors.test.ts test/config-command.test.ts test/analyze-ai.test.ts +``` + +Expected: + +- Config lama di `~/.devmap/config.json` yang hanya berisi `apiKey` tetap + terbaca sebagai Groq dengan `model: "auto"`. +- Setup Groq interaktif menampilkan daftar model Groq setelah API key valid. +- Pilih model dengan arrow key lalu Enter; model tersimpan di global config. +- `devmap config model ` tetap dapat mengganti model yang tersimpan. +- `devmap analyze --help` tidak menampilkan flag `--deep`. +- OpenRouter setup tetap memakai prompt model text dengan default + `openrouter/free`. + ## Mixed Workspace Snapshot Accuracy Jalankan static analyze pada root DevMap dengan config AI terisolasi: @@ -258,12 +277,8 @@ pnpm --filter devmap exec tsx --test test/config-command.test.ts test/analyze-ai Expected automatic routing: -- `ask`: `llama-3.1-8b-instant` - `analyze`: `openai/gpt-oss-20b` -- `analyze --deep`: `openai/gpt-oss-120b` -- `ask` fallback: Qwen 3.6 27B -> Llama 70B Versatile -> GPT-OSS 20B - `analyze` fallback: Qwen 3.6 27B -> Llama 70B Versatile -> Llama 8B Instant -- deep fallback: Llama 70B Versatile -> Qwen 3.6 27B -> GPT-OSS 20B Automated expectations: @@ -925,12 +940,6 @@ Pastikan: - raw provider error dan stack trace tidak muncul; - `doctor` menyatakan key dan model valid. -Tes deep model: - -```powershell -devmap analyze --deep --fresh -``` - Hapus key: ```powershell diff --git a/packages/cli/README.md b/packages/cli/README.md index 14d2c02..a90663a 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -90,7 +90,6 @@ DevMap-owned server. ```bash devmap init devmap analyze -devmap analyze --deep devmap analyze --fresh devmap onboarding --write devmap onboarding --write --language id @@ -98,9 +97,8 @@ devmap doctor devmap config model auto ``` -Groq automatic routing uses a fast model for focused questions and larger -models for architecture analysis. OpenRouter uses the model selected during -init. Change either provider's model with: +Groq and OpenRouter use the model selected during init. Change either +provider's model with: ```bash devmap config model diff --git a/packages/cli/src/ai/groq.ts b/packages/cli/src/ai/groq.ts index a7f749f..3364882 100644 --- a/packages/cli/src/ai/groq.ts +++ b/packages/cli/src/ai/groq.ts @@ -15,7 +15,6 @@ const MAX_RATE_LIMIT_RETRIES = 3; export const DEFAULT_AI_MODELS = { analyze: "openai/gpt-oss-20b", - deepAnalyze: "openai/gpt-oss-120b", fallback: "openai/gpt-oss-20b" } as const; @@ -24,14 +23,27 @@ export const DEFAULT_AI_FALLBACKS = { "qwen/qwen3.6-27b", "llama-3.3-70b-versatile", "llama-3.1-8b-instant" - ], - deepAnalyze: [ - "llama-3.3-70b-versatile", - "qwen/qwen3.6-27b", - "openai/gpt-oss-20b" ] } as const; +const EXCLUDED_MODEL_PATTERNS = [ + /whisper/i, + /prompt-guard/i, + /compound/i, + /tts/i, + /vision/i, +]; + +const PREFERRED_MODELS = [ + "llama-3.3-70b-versatile", + "llama-3.1-70b-versatile", + "openai/gpt-oss-20b", + "openai/gpt-oss-120b", + "qwen/qwen3.6-27b", + "qwen/qwen3-32b", + "llama-3.1-8b-instant", +]; + export type GroqClientDependencies = { fetch?: typeof fetch; sleep?: (milliseconds: number) => Promise; @@ -205,27 +217,35 @@ export async function validateGroqApiKey(apiKey: string): Promise { await inspectGroqProvider(apiKey); } -export async function inspectGroqProvider( +export async function listGroqModels( apiKey: string, - model?: string, dependencies: Pick = {} -): Promise { - const fetchImplementation = dependencies.fetch ?? fetch; - let response: Response; +): Promise { + const response = await fetchGroqModels(apiKey, dependencies); + if (response.status === 401 || response.status === 403) { + throw new DevmapError( + "The Groq API key is invalid.", + "Create or copy a valid key from https://console.groq.com/keys." + ); + } - try { - response = await fetchImplementation(GROQ_MODELS_URL, { - headers: { - Authorization: `Bearer ${apiKey}` - } - }); - } catch { + if (!response.ok) { throw new DevmapError( - "Could not connect to Groq.", - "Check your internet connection and run devmap init again." + `Groq model lookup failed with HTTP ${response.status}.`, + "Try again shortly or check https://status.groq.com." ); } + return readModelIds(response); +} + +export async function inspectGroqProvider( + apiKey: string, + model?: string, + dependencies: Pick = {} +): Promise { + const response = await fetchGroqModels(apiKey, dependencies); + if (response.status === 401 || response.status === 403) { throw new DevmapError( "The Groq API key is invalid.", @@ -247,6 +267,26 @@ export async function inspectGroqProvider( }; } +async function fetchGroqModels( + apiKey: string, + dependencies: Pick = {} +): Promise { + const fetchImplementation = dependencies.fetch ?? fetch; + + try { + return await fetchImplementation(GROQ_MODELS_URL, { + headers: { + Authorization: `Bearer ${apiKey}` + } + }); + } catch { + throw new DevmapError( + "Could not connect to Groq.", + "Check your internet connection and run devmap init again." + ); + } +} + type GroqCompletionPayload = { model?: string; choices: Array<{ @@ -502,7 +542,16 @@ async function readModelIds(response: Response): Promise { return (payload.data ?? []) .map((model) => model.id) - .filter((id): id is string => typeof id === "string"); + .filter((id): id is string => typeof id === "string") + .filter((id) => !EXCLUDED_MODEL_PATTERNS.some((pattern) => pattern.test(id))) + .sort((a, b) => { + const aIndex = PREFERRED_MODELS.indexOf(a); + const bIndex = PREFERRED_MODELS.indexOf(b); + if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex; + if (aIndex !== -1) return -1; + if (bIndex !== -1) return 1; + return a.localeCompare(b); + }); } catch { throw new DevmapError( "Groq returned an unreadable model list.", diff --git a/packages/cli/src/ai/prompts.ts b/packages/cli/src/ai/prompts.ts index 0f1153d..8778dc0 100644 --- a/packages/cli/src/ai/prompts.ts +++ b/packages/cli/src/ai/prompts.ts @@ -1,10 +1,7 @@ import type { ProjectMap } from "../analyzers/projectMap.js"; import type { AiMessage } from "./types.js"; -export function buildAnalyzeMessages( - snapshot: ProjectMap, - deep = false -): AiMessage[] { +export function buildAnalyzeMessages(snapshot: ProjectMap): AiMessage[] { const analysisData = { project: snapshot.project, stats: snapshot.stats, @@ -25,9 +22,7 @@ export function buildAnalyzeMessages( "Explain only facts supported by the supplied static analysis snapshot.", "Do not invent modules, flows, frameworks, or services.", "Clearly separate confirmed structure from reasonable interpretation.", - deep - ? "Provide a detailed module-oriented explanation and recommended reading order." - : "Provide a concise architecture overview, main entry points, and important relationships.", + "Provide a concise architecture overview, main entry points, and important relationships.", "Mention relevant file paths.", "Use clear Markdown." ].join(" ") diff --git a/packages/cli/src/ai/provider.ts b/packages/cli/src/ai/provider.ts index 9caba3d..397913a 100644 --- a/packages/cli/src/ai/provider.ts +++ b/packages/cli/src/ai/provider.ts @@ -12,7 +12,7 @@ import { OPENROUTER_FREE_MODEL } from "./openrouter.js"; -export type AiTask = "analyze" | "deepAnalyze"; +export type AiTask = "analyze"; export type ProviderInspection = { reachable: true; modelAvailable: boolean }; export function createAiClient(config: DevmapConfig): AiClient { diff --git a/packages/cli/src/commands/analyze.ts b/packages/cli/src/commands/analyze.ts index a7aa4ac..dc40cf3 100644 --- a/packages/cli/src/commands/analyze.ts +++ b/packages/cli/src/commands/analyze.ts @@ -15,7 +15,6 @@ import { DevmapError } from "../utils/errors.js"; import { output, withJsonOutput } from "../utils/output.js"; export type AnalyzeOptions = { - deep?: boolean; fresh?: boolean; json?: boolean; }; @@ -56,7 +55,7 @@ async function runAnalyze( if (previous.status === "valid" && previous.snapshot.fingerprint === snapshot.fingerprint) { await writeAgentNavigationFiles(projectRoot, previous.snapshot); - printSnapshot(previous.snapshot, options.deep); + printSnapshot(previous.snapshot); output.success("Project is unchanged. Reused existing snapshot."); return printOrGenerateInterpretation( projectRoot, @@ -69,7 +68,7 @@ async function runAnalyze( snapshot = await enrichSnapshot(snapshot, options, dependencies); await saveSnapshot(projectRoot, snapshot); await writeAgentNavigationFiles(projectRoot, snapshot); - printSnapshot(snapshot, options.deep); + printSnapshot(snapshot); output.success("Snapshot saved to .devmap/snapshot.json"); output.success("Agent navigation saved to .devmap/index.json and .devmap/features/"); @@ -94,10 +93,7 @@ async function enrichSnapshot( const createAiClient = dependencies.createAiClient ?? createDefaultAiClient; const client = createAiClient(config); - const routing = resolveAiRouting( - config, - options.deep ? "deepAnalyze" : "analyze" - ); + const routing = resolveAiRouting(config, "analyze"); const enriched = await enrichSnapshotWithAi( snapshot, @@ -114,8 +110,7 @@ async function enrichSnapshot( } function printSnapshot( - snapshot: Awaited>, - deep = false + snapshot: Awaited> ): void { output.keyValue("Project", snapshot.project.name); output.keyValue("Framework", snapshot.project.framework); @@ -147,12 +142,6 @@ function printSnapshot( output.item(snapshot.database.provider); } - if (deep) { - output.section("Module Breakdown"); - for (const file of snapshot.criticalFiles.slice(0, 5)) { - output.item(`${file.path}: ${file.reasons.join(", ")}`); - } - } } function printList(title: string, values: string[]): void { @@ -190,20 +179,17 @@ async function printOrGenerateInterpretation( const createAiClient = dependencies.createAiClient ?? createDefaultAiClient; const client = createAiClient(config); - const routing = resolveAiRouting( - config, - options.deep ? "deepAnalyze" : "analyze" - ); + const routing = resolveAiRouting(config, "analyze"); const model = routing.model; output.step(`Interpreting architecture with ${model}`); try { const execution = await completeWithOptionalStreaming(client, { - messages: buildAnalyzeMessages(snapshot, options.deep), + messages: buildAnalyzeMessages(snapshot), model, fallbackModels: routing.fallbackModels, - maxCompletionTokens: options.deep ? 1800 : 1000, + maxCompletionTokens: 2500, temperature: 0.2 }, !options.json, () => output.section("Architecture")); const interpretation = execution.result; diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 43bdb8e..f52cb7a 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -2,7 +2,7 @@ import { mkdir } from "node:fs/promises"; import { resolve } from "node:path"; import { scanFiles } from "../analyzers/fileScanner.js"; import { detectFramework } from "../analyzers/frameworkDetector.js"; -import { validateGroqApiKey } from "../ai/groq.js"; +import { listGroqModels, validateGroqApiKey } from "../ai/groq.js"; import { OPENROUTER_FREE_MODEL, validateOpenRouterApiKey @@ -29,6 +29,7 @@ export type InitDependencies = { apiKey: string, provider: DevmapConfig["provider"] ) => Promise; + listGroqModels?: (apiKey: string) => Promise; isInteractive?: boolean; environmentApiKey?: string; environmentOpenRouterApiKey?: string; @@ -97,9 +98,11 @@ async function runInit( const model = await resolveInitialModel({ provider, + apiKey, prompt, interactive, - existingConfig + existingConfig, + listModels: dependencies.listGroqModels ?? listGroqModels }); const agentsStatus = await inspectAgentsFile(projectRoot); @@ -130,6 +133,9 @@ async function runInit( if (provider === "openrouter") { output.note(`OpenRouter model: ${model}`); output.note("Change it later with: devmap config model "); + } else if (model !== "auto") { + output.note(`Groq model: ${model}`); + output.note("Change it later with: devmap config model "); } output.success(ignored ? "Added .devmap/ to .gitignore" : ".devmap/ already ignored"); output.success(devmapFileCreated ? "Created DEVMAP.md" : "DEVMAP.md already exists"); @@ -238,15 +244,39 @@ async function resolveProvider( type ResolveInitialModelOptions = { provider: DevmapConfig["provider"]; + apiKey: string; prompt: Prompt | null; interactive: boolean; existingConfig: DevmapConfig | null; + listModels: (apiKey: string) => Promise; }; async function resolveInitialModel( options: ResolveInitialModelOptions ): Promise { - if (options.provider === "groq") return "auto"; + if (options.provider === "groq") { + const existingModel = options.existingConfig?.provider === "groq" + ? options.existingConfig.model + : undefined; + if (!options.interactive || !options.prompt) return existingModel ?? "auto"; + + const models = await options.listModels(options.apiKey); + if (models.length === 0) { + throw new DevmapError( + "Groq did not return any available models.", + "Try again shortly or run devmap doctor." + ); + } + + const defaultModel = existingModel && models.includes(existingModel) + ? existingModel + : models[0]!; + return options.prompt.select( + "Groq model", + models.map((model) => ({ label: model, value: model })), + defaultModel + ); + } const existingModel = options.existingConfig?.provider === "openrouter" ? options.existingConfig.model diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 56fbd58..1db5fbf 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -27,7 +27,6 @@ program .command("analyze") .description("Analyze project structure and generate a static project map") .argument("[target]", "folder to analyze", ".") - .option("--deep", "show a deeper static breakdown") .option("--fresh", "ignore cache and run a fresh analysis") .option("--json", "output machine-readable JSON") .action((target, options) => analyzeCommand(target, options)); diff --git a/packages/cli/src/utils/config.ts b/packages/cli/src/utils/config.ts index 5077334..8fccd16 100644 --- a/packages/cli/src/utils/config.ts +++ b/packages/cli/src/utils/config.ts @@ -16,7 +16,7 @@ export async function readConfig(): Promise { try { const raw = await readFile(getConfigPath(), "utf8"); const parsed = JSON.parse(raw) as unknown; - return isDevmapConfig(parsed) ? parsed : null; + return normalizeConfig(parsed); } catch { return null; } @@ -28,14 +28,42 @@ export async function writeConfig(config: DevmapConfig): Promise { await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8"); } -function isDevmapConfig(value: unknown): value is DevmapConfig { - return ( - typeof value === "object" - && value !== null - && !Array.isArray(value) - && "provider" in value - && (value.provider === "groq" || value.provider === "openrouter") - && "model" in value - && typeof value.model === "string" - ); +function normalizeConfig(value: unknown): DevmapConfig | null { + if ( + typeof value !== "object" + || value === null + || Array.isArray(value) + ) { + return null; + } + + const record = value as Record; + const provider = record.provider === undefined + ? "groq" + : record.provider; + const model = record.model === undefined + ? "auto" + : record.model; + + if (provider !== "groq" && provider !== "openrouter") { + return null; + } + + if (typeof model !== "string") { + return null; + } + + const config: DevmapConfig = { + provider, + model + }; + + if (record.apiKey !== undefined) { + if (typeof record.apiKey !== "string") { + return null; + } + config.apiKey = record.apiKey; + } + + return config; } diff --git a/packages/cli/src/utils/devmapFile.ts b/packages/cli/src/utils/devmapFile.ts index 4fe48a7..0a2325c 100644 --- a/packages/cli/src/utils/devmapFile.ts +++ b/packages/cli/src/utils/devmapFile.ts @@ -40,7 +40,6 @@ This repository uses DevMap to create reusable project context for developers an \`\`\`bash devmap analyze -devmap analyze --deep devmap analyze --json devmap ask "how does authentication work?" devmap ask "where is authentication handled?" --json diff --git a/packages/cli/test/ai-client.test.ts b/packages/cli/test/ai-client.test.ts index c9c5e44..e788122 100644 --- a/packages/cli/test/ai-client.test.ts +++ b/packages/cli/test/ai-client.test.ts @@ -18,7 +18,7 @@ test("Groq client returns normalized content and token usage", async () => { fetch: async (url, init) => { requests.push({ url: String(url), init }); return jsonResponse({ - model: DEFAULT_AI_MODELS.deepAnalyze, + model: DEFAULT_AI_MODELS.analyze, choices: [{ message: { content: "Authentication uses a session handler." } }], usage: { prompt_tokens: 120, @@ -31,13 +31,13 @@ test("Groq client returns normalized content and token usage", async () => { const result = await client.complete({ messages: [{ role: "user", content: "Explain auth." }], - model: DEFAULT_AI_MODELS.deepAnalyze + model: DEFAULT_AI_MODELS.analyze }); assert.equal(requests.length, 1); assert.equal(requests[0]?.url, "https://api.groq.com/openai/v1/chat/completions"); assert.equal(result.content, "Authentication uses a session handler."); - assert.equal(result.model, DEFAULT_AI_MODELS.deepAnalyze); + assert.equal(result.model, DEFAULT_AI_MODELS.analyze); assert.deepEqual(result.usage, { promptTokens: 120, completionTokens: 18, @@ -60,10 +60,10 @@ test("Groq client streams split SSE deltas and returns the complete result", asy return new Response(new ReadableStream({ start(controller) { controller.enqueue(encoder.encode( - 'data: {"model":"openai/gpt-oss-120b","choices":[{"delta":{"content":"Auth"}}]}\n' + `data: {"model":"${DEFAULT_AI_MODELS.analyze}","choices":[{"delta":{"content":"Auth"}}]}\n` )); controller.enqueue(encoder.encode( - '\ndata: {"model":"openai/gpt-oss-120b","choices":[{"delta":{"content":" works."}}],"usage":{"prompt_tokens":10,"completion_tokens":2,"total_tokens":12}}\n\n' + `\ndata: {"model":"${DEFAULT_AI_MODELS.analyze}","choices":[{"delta":{"content":" works."}}],"usage":{"prompt_tokens":10,"completion_tokens":2,"total_tokens":12}}\n\n` )); controller.enqueue(encoder.encode("data: [DONE]\n\n")); controller.close(); @@ -76,14 +76,14 @@ test("Groq client streams split SSE deltas and returns the complete result", asy const result = await client.stream({ messages: [{ role: "user", content: "Explain auth." }], - model: DEFAULT_AI_MODELS.deepAnalyze + model: DEFAULT_AI_MODELS.analyze }, (delta) => { deltas.push(delta); }); assert.deepEqual(deltas, ["Auth", " works."]); assert.equal(result.content, "Auth works."); - assert.equal(result.model, DEFAULT_AI_MODELS.deepAnalyze); + assert.equal(result.model, DEFAULT_AI_MODELS.analyze); assert.deepEqual(result.usage, { promptTokens: 10, completionTokens: 2, @@ -106,7 +106,7 @@ test("Groq client retries rate limits with exponential backoff", async () => { } return jsonResponse({ - model: DEFAULT_AI_MODELS.deepAnalyze, + model: DEFAULT_AI_MODELS.analyze, choices: [{ message: { content: "Recovered." } }] }); }, @@ -118,7 +118,7 @@ test("Groq client retries rate limits with exponential backoff", async () => { const result = await client.complete({ messages: [{ role: "user", content: "Explain auth." }], - model: DEFAULT_AI_MODELS.deepAnalyze + model: DEFAULT_AI_MODELS.analyze }); assert.equal(requestCount, 4); @@ -145,7 +145,7 @@ test("Groq client stops after three rate-limit retries", async () => { await assert.rejects( client.complete({ messages: [{ role: "user", content: "Explain auth." }], - model: DEFAULT_AI_MODELS.deepAnalyze + model: DEFAULT_AI_MODELS.analyze }), (error: unknown) => error instanceof DevmapError && /rate limit reached after retrying/i.test(error.message) @@ -162,7 +162,7 @@ test("Groq client falls back when the primary model is unavailable", async () => const body = JSON.parse(String(init?.body)) as { model: string }; requestedModels.push(body.model); - if (body.model === DEFAULT_AI_MODELS.deepAnalyze) { + if (body.model === "retired-model") { return jsonResponse( { error: { message: "The model is not available." } }, 404 @@ -170,7 +170,7 @@ test("Groq client falls back when the primary model is unavailable", async () => } return jsonResponse({ - model: DEFAULT_AI_MODELS.fallback, + model: DEFAULT_AI_MODELS.analyze, choices: [{ message: { content: "Fallback answer." } }] }); } @@ -178,28 +178,28 @@ test("Groq client falls back when the primary model is unavailable", async () => const result = await client.complete({ messages: [{ role: "user", content: "Explain auth." }], - model: DEFAULT_AI_MODELS.deepAnalyze, - fallbackModel: DEFAULT_AI_MODELS.fallback + model: "retired-model", + fallbackModel: DEFAULT_AI_MODELS.analyze }); assert.deepEqual(requestedModels, [ - DEFAULT_AI_MODELS.deepAnalyze, - DEFAULT_AI_MODELS.fallback + "retired-model", + DEFAULT_AI_MODELS.analyze ]); assert.equal(result.content, "Fallback answer."); - assert.equal(result.model, DEFAULT_AI_MODELS.fallback); + assert.equal(result.model, DEFAULT_AI_MODELS.analyze); }); test("Groq client follows an ordered fallback chain after unavailable and rate-limited models", async () => { const requestedModels: string[] = []; const delays: number[] = []; - const [qwenModel, versatileModel] = DEFAULT_AI_FALLBACKS.deepAnalyze; + const [qwenModel, versatileModel] = DEFAULT_AI_FALLBACKS.analyze; const client = new GroqClient("gsk_test", { fetch: async (_url, init) => { const body = JSON.parse(String(init?.body)) as { model: string }; requestedModels.push(body.model); - if (body.model === DEFAULT_AI_MODELS.deepAnalyze) { + if (body.model === "retired-model") { return jsonResponse( { error: { message: "The model is not available." } }, 404 @@ -225,12 +225,12 @@ test("Groq client follows an ordered fallback chain after unavailable and rate-l const result = await client.complete({ messages: [{ role: "user", content: "Explain auth." }], - model: DEFAULT_AI_MODELS.deepAnalyze, - fallbackModels: DEFAULT_AI_FALLBACKS.deepAnalyze + model: "retired-model", + fallbackModels: DEFAULT_AI_FALLBACKS.analyze }); assert.deepEqual(requestedModels, [ - DEFAULT_AI_MODELS.deepAnalyze, + "retired-model", qwenModel, qwenModel, qwenModel, @@ -249,7 +249,7 @@ test("Groq client removes duplicate models from the fallback chain", async () => const body = JSON.parse(String(init?.body)) as { model: string }; requestedModels.push(body.model); - if (body.model === DEFAULT_AI_MODELS.deepAnalyze) { + if (body.model === "retired-model") { return jsonResponse( { error: { message: "The model is not available." } }, 404 @@ -257,7 +257,7 @@ test("Groq client removes duplicate models from the fallback chain", async () => } return jsonResponse({ - model: DEFAULT_AI_MODELS.fallback, + model: DEFAULT_AI_MODELS.analyze, choices: [{ message: { content: "Fallback answer." } }] }); } @@ -265,25 +265,25 @@ test("Groq client removes duplicate models from the fallback chain", async () => await client.complete({ messages: [{ role: "user", content: "Explain auth." }], - model: DEFAULT_AI_MODELS.deepAnalyze, + model: "retired-model", fallbackModels: [ - DEFAULT_AI_MODELS.deepAnalyze, - DEFAULT_AI_MODELS.fallback, - DEFAULT_AI_MODELS.fallback + "retired-model", + DEFAULT_AI_MODELS.analyze, + DEFAULT_AI_MODELS.analyze ], - fallbackModel: DEFAULT_AI_MODELS.fallback + fallbackModel: DEFAULT_AI_MODELS.analyze }); assert.deepEqual(requestedModels, [ - DEFAULT_AI_MODELS.deepAnalyze, - DEFAULT_AI_MODELS.fallback + "retired-model", + DEFAULT_AI_MODELS.analyze ]); }); test("Groq streaming follows the fallback chain before emitting deltas", async () => { const requestedModels: string[] = []; const deltas: string[] = []; - const fallbackModel = DEFAULT_AI_FALLBACKS.deepAnalyze[0]; + const fallbackModel = DEFAULT_AI_FALLBACKS.analyze[0]; const encoder = new TextEncoder(); const client = new GroqClient("gsk_test", { fetch: async (_url, init) => { @@ -294,7 +294,7 @@ test("Groq streaming follows the fallback chain before emitting deltas", async ( requestedModels.push(body.model); assert.equal(body.stream, true); - if (body.model === DEFAULT_AI_MODELS.deepAnalyze) { + if (body.model === DEFAULT_AI_MODELS.analyze) { return jsonResponse( { error: { message: "The model is not available." } }, 404 @@ -317,13 +317,13 @@ test("Groq streaming follows the fallback chain before emitting deltas", async ( const result = await client.stream({ messages: [{ role: "user", content: "Explain auth." }], - model: DEFAULT_AI_MODELS.deepAnalyze, - fallbackModels: DEFAULT_AI_FALLBACKS.deepAnalyze + model: DEFAULT_AI_MODELS.analyze, + fallbackModels: DEFAULT_AI_FALLBACKS.analyze }, (delta) => { deltas.push(delta); }); - assert.deepEqual(requestedModels, [DEFAULT_AI_MODELS.deepAnalyze, fallbackModel]); + assert.deepEqual(requestedModels, [DEFAULT_AI_MODELS.analyze, fallbackModel]); assert.deepEqual(deltas, ["Fallback stream."]); assert.equal(result.model, fallbackModel); }); @@ -343,8 +343,8 @@ test("Groq client does not fall back after authentication errors", async () => { await assert.rejects( client.complete({ messages: [{ role: "user", content: "Explain auth." }], - model: DEFAULT_AI_MODELS.deepAnalyze, - fallbackModels: DEFAULT_AI_FALLBACKS.deepAnalyze + model: DEFAULT_AI_MODELS.analyze, + fallbackModels: DEFAULT_AI_FALLBACKS.analyze }), (error: unknown) => error instanceof DevmapError && /API key is invalid/i.test(error.message) @@ -364,7 +364,7 @@ test("Groq client maps invalid credentials to an actionable error", async () => await assert.rejects( client.complete({ messages: [{ role: "user", content: "Explain auth." }], - model: DEFAULT_AI_MODELS.deepAnalyze + model: DEFAULT_AI_MODELS.analyze }), (error: unknown) => error instanceof DevmapError && /API key is invalid/i.test(error.message) diff --git a/packages/cli/test/analyze-ai.test.ts b/packages/cli/test/analyze-ai.test.ts index 07c4294..972da36 100644 --- a/packages/cli/test/analyze-ai.test.ts +++ b/packages/cli/test/analyze-ai.test.ts @@ -186,7 +186,7 @@ test("analyze warns and continues when package.json is malformed", async () => { } }); -test("analyze auto routing uses 20B normally and 120B for deep analysis", async () => { +test("analyze auto routing uses the configured Groq analyze chain", async () => { const projectRoot = await mkdtemp(join(tmpdir(), "devmap-model-routing-")); const requests: AiCompletionRequest[] = []; const client: AiClient = { @@ -221,18 +221,9 @@ test("analyze auto routing uses 20B normally and 120B for deep analysis", async { fresh: true }, dependencies )); - await captureOutput(() => analyzeCommand( - projectRoot, - { deep: true, fresh: true }, - dependencies - )); - assert.equal(requests[0]?.model, DEFAULT_AI_MODELS.analyze); assert.equal(requests[1]?.model, DEFAULT_AI_MODELS.analyze); - assert.equal(requests[2]?.model, DEFAULT_AI_MODELS.deepAnalyze); - assert.equal(requests[3]?.model, DEFAULT_AI_MODELS.deepAnalyze); assert.deepEqual(requests[0]?.fallbackModels, DEFAULT_AI_FALLBACKS.analyze); - assert.deepEqual(requests[2]?.fallbackModels, DEFAULT_AI_FALLBACKS.deepAnalyze); } finally { await rm(projectRoot, { recursive: true, force: true }); } diff --git a/packages/cli/test/init-and-errors.test.ts b/packages/cli/test/init-and-errors.test.ts index 7808c05..e31f13a 100644 --- a/packages/cli/test/init-and-errors.test.ts +++ b/packages/cli/test/init-and-errors.test.ts @@ -86,6 +86,33 @@ test("init uses environment API key and creates project setup files", async () = } }); +test("readConfig backfills provider and model for legacy Groq configs", async () => { + const temporaryHome = await mkdtemp(join(tmpdir(), "devmap-legacy-config-test-")); + const originalHome = process.env.HOME; + const originalUserProfile = process.env.USERPROFILE; + + try { + process.env.HOME = temporaryHome; + process.env.USERPROFILE = temporaryHome; + const configPath = getConfigPath(); + await mkdir(join(temporaryHome, ".devmap"), { recursive: true }); + + await writeFile(configPath, JSON.stringify({ + apiKey: "gsk_legacy_fixture" + }), "utf8"); + + assert.deepEqual(await readConfig(), { + provider: "groq", + apiKey: "gsk_legacy_fixture", + model: "auto" + }); + } finally { + restoreEnvironment("HOME", originalHome); + restoreEnvironment("USERPROFILE", originalUserProfile); + await rm(temporaryHome, { recursive: true, force: true }); + } +}); + test("interactive init appends DevMap instructions only after confirmation", async () => { const projectRoot = await mkdtemp(join(tmpdir(), "devmap-agents-confirm-test-")); const original = "# Existing Instructions\n\nKeep this content.\n"; @@ -99,7 +126,8 @@ test("interactive init appends DevMap instructions only after confirmation", asy isInteractive: true, loadConfig: async () => null, persistConfig: async () => undefined, - validateApiKey: async () => undefined + validateApiKey: async () => undefined, + listGroqModels: async () => ["openai/gpt-oss-20b"] }); const content = await readFile(join(projectRoot, "AGENTS.md"), "utf8"); @@ -124,7 +152,8 @@ test("interactive init preserves existing AGENTS.md when confirmation is decline isInteractive: true, loadConfig: async () => null, persistConfig: async () => undefined, - validateApiKey: async () => undefined + validateApiKey: async () => undefined, + listGroqModels: async () => ["openai/gpt-oss-20b"] }); assert.equal(await readFile(join(projectRoot, "AGENTS.md"), "utf8"), original); @@ -234,7 +263,7 @@ test("readConfig returns null for invalid config schemas", async () => { test("interactive init selects Groq with the provider menu", async () => { const projectRoot = await mkdtemp(join(tmpdir(), "devmap-init-provider-test-")); - const prompt = createFakePrompt(["gsk_fixture"], ["groq"]); + const prompt = createFakePrompt(["gsk_fixture"], ["groq", "llama-3.3-70b-versatile"]); let savedConfig: DevmapConfig | null = null; try { @@ -246,16 +275,20 @@ test("interactive init selects Groq with the provider menu", async () => { persistConfig: async (config) => { savedConfig = config; }, - validateApiKey: async () => undefined + validateApiKey: async () => undefined, + listGroqModels: async () => [ + "openai/gpt-oss-20b", + "llama-3.3-70b-versatile" + ] }); assert.equal(prompt.closed, true); assert.deepEqual(savedConfig, { provider: "groq", apiKey: "gsk_fixture", - model: "auto" + model: "llama-3.3-70b-versatile" }); - assert.deepEqual(prompt.selections, ["AI provider"]); + assert.deepEqual(prompt.selections, ["AI provider", "Groq model"]); } finally { await rm(projectRoot, { recursive: true, force: true }); } diff --git a/packages/cli/test/openrouter-client.test.ts b/packages/cli/test/openrouter-client.test.ts index 67c44f6..7b7227a 100644 --- a/packages/cli/test/openrouter-client.test.ts +++ b/packages/cli/test/openrouter-client.test.ts @@ -106,7 +106,7 @@ test("OpenRouter routing prioritizes an explicit user model", () => { provider: "openrouter", apiKey: "sk-or-fixture", model: "qwen/qwen3-coder" - }, "deepAnalyze"), { + }, "analyze"), { model: "qwen/qwen3-coder", fallbackModels: [] });