ai-spec-dev 0.42.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +33 -17
  2. package/cli/commands/create.ts +232 -11
  3. package/cli/commands/init.ts +310 -107
  4. package/cli/commands/model.ts +7 -11
  5. package/cli/index.ts +1 -1
  6. package/cli/utils.ts +72 -4
  7. package/core/config-defaults.ts +44 -0
  8. package/core/constitution-generator.ts +2 -1
  9. package/core/dsl-extractor.ts +2 -1
  10. package/core/error-feedback.ts +3 -2
  11. package/core/openapi-exporter.ts +3 -2
  12. package/core/repo-store.ts +95 -0
  13. package/core/reviewer.ts +14 -13
  14. package/core/run-logger.ts +3 -4
  15. package/core/run-snapshot.ts +2 -3
  16. package/core/run-trend.ts +3 -4
  17. package/core/spec-generator.ts +27 -42
  18. package/core/token-budget.ts +3 -8
  19. package/core/vcr.ts +3 -1
  20. package/dist/cli/index.js +919 -519
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/cli/index.mjs +912 -512
  23. package/dist/cli/index.mjs.map +1 -1
  24. package/dist/index.d.mts +3 -2
  25. package/dist/index.d.ts +3 -2
  26. package/dist/index.js +43 -53
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.mjs +43 -53
  29. package/dist/index.mjs.map +1 -1
  30. package/package.json +1 -1
  31. package/demo-backend/.ai-spec-constitution.md +0 -65
  32. package/demo-backend/package.json +0 -21
  33. package/demo-backend/prisma/schema.prisma +0 -22
  34. package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.dsl.json +0 -186
  35. package/demo-backend/specs/feature-1-bookmark-id-uuid-title-string-required-url-str-v1.md +0 -211
  36. package/demo-backend/src/controllers/bookmark.controller.test.ts +0 -255
  37. package/demo-backend/src/controllers/bookmark.controller.ts +0 -187
  38. package/demo-backend/src/index.ts +0 -17
  39. package/demo-backend/src/routes/bookmark.routes.test.ts +0 -264
  40. package/demo-backend/src/routes/bookmark.routes.ts +0 -11
  41. package/demo-backend/src/routes/index.ts +0 -8
  42. package/demo-backend/src/services/bookmark.service.test.ts +0 -433
  43. package/demo-backend/src/services/bookmark.service.ts +0 -261
  44. package/demo-backend/tsconfig.json +0 -12
  45. package/demo-frontend/.ai-spec-constitution.md +0 -95
  46. package/demo-frontend/package.json +0 -23
  47. package/demo-frontend/src/App.tsx +0 -12
  48. package/demo-frontend/src/main.tsx +0 -9
  49. package/demo-frontend/tsconfig.json +0 -13
@@ -0,0 +1,44 @@
1
+ /**
2
+ * config-defaults.ts — Centralized default values for all configurable constants.
3
+ *
4
+ * Modules import their defaults from here instead of defining local magic numbers.
5
+ * The pipeline can override any value via AiSpecConfig at runtime.
6
+ */
7
+
8
+ // ─── Directory & File Names ─────────────────────────────────────────────────
9
+
10
+ export const DEFAULT_LOG_DIR = ".ai-spec-logs";
11
+ export const DEFAULT_VCR_DIR = ".ai-spec-vcr";
12
+ export const DEFAULT_BACKUP_DIR = ".ai-spec-backup";
13
+ export const DEFAULT_REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
14
+
15
+ // ─── URLs ───────────────────────────────────────────────────────────────────
16
+
17
+ export const DEFAULT_OPENAPI_SERVER_URL = "http://localhost:3000";
18
+
19
+ // ─── Numeric Limits ─────────────────────────────────────────────────────────
20
+
21
+ /** Max chars captured from build/test/lint command output before parsing. */
22
+ export const DEFAULT_MAX_COMMAND_OUTPUT_CHARS = 30_000;
23
+
24
+ /** Max chars of an existing file sent to the AI for auto-fix. */
25
+ export const DEFAULT_MAX_FIX_FILE_CHARS = 60_000;
26
+
27
+ /** Max DSL extraction retries on parse failure. */
28
+ export const DEFAULT_DSL_MAX_RETRIES = 2;
29
+
30
+ /** Max constitution chars in codegen prompts (trimmed if exceeded). */
31
+ export const DEFAULT_MAX_CONSTITUTION_CHARS = 4_000;
32
+
33
+ /** Max chars of file content sent per file in review (reviewFiles mode). */
34
+ export const DEFAULT_MAX_REVIEW_FILE_CHARS = 3_000;
35
+
36
+ // ─── Token Budgets ──────────────────────────────────────────────────────────
37
+
38
+ export const DEFAULT_TOKEN_BUDGETS: Record<string, number> = {
39
+ gemini: 900_000,
40
+ claude: 180_000,
41
+ openai: 120_000,
42
+ deepseek: 60_000,
43
+ default: 100_000,
44
+ };
@@ -4,6 +4,7 @@ import * as path from "path";
4
4
  import { AIProvider } from "./spec-generator";
5
5
  import { ContextLoader, ProjectContext } from "./context-loader";
6
6
  import { constitutionSystemPrompt } from "../prompts/constitution.prompt";
7
+ import { DEFAULT_MAX_CONSTITUTION_CHARS } from "./config-defaults";
7
8
 
8
9
  export const CONSTITUTION_FILE = ".ai-spec-constitution.md";
9
10
 
@@ -41,7 +42,7 @@ function buildConstitutionPrompt(context: ProjectContext, projectRoot: string):
41
42
  }
42
43
 
43
44
  if (context.schema) {
44
- parts.push(`=== Prisma Schema ===\n${context.schema.slice(0, 4000)}\n`);
45
+ parts.push(`=== Prisma Schema ===\n${context.schema.slice(0, DEFAULT_MAX_CONSTITUTION_CHARS)}\n`);
45
46
  }
46
47
 
47
48
  if (context.errorPatterns) {
@@ -13,6 +13,7 @@ import {
13
13
  } from "../prompts/dsl.prompt";
14
14
  import { estimateTokens, getDefaultBudget } from "./token-budget";
15
15
  import { parseJsonFromAiOutput } from "./safe-json";
16
+ import { DEFAULT_DSL_MAX_RETRIES } from "./config-defaults";
16
17
 
17
18
  // ─── DSL Sanitizer ───────────────────────────────────────────────────────────
18
19
 
@@ -50,7 +51,7 @@ function sanitizeDsl(raw: unknown): unknown {
50
51
  // ─── Constants ────────────────────────────────────────────────────────────────
51
52
 
52
53
  /** Maximum AI attempts (1 initial + up to this many retries). */
53
- const MAX_RETRIES = 2;
54
+ const MAX_RETRIES = DEFAULT_DSL_MAX_RETRIES;
54
55
 
55
56
  /** Default maximum spec length passed to AI. Overridden by token budget when provider is known. */
56
57
  const DEFAULT_MAX_SPEC_CHARS = 12_000;
@@ -8,6 +8,7 @@ import { SpecDSL } from "./dsl-types";
8
8
  import { buildDslContextSection } from "./dsl-extractor";
9
9
  import { getActiveSnapshot } from "./run-snapshot";
10
10
  import { startSpinner } from "./cli-ui";
11
+ import { DEFAULT_MAX_COMMAND_OUTPUT_CHARS, DEFAULT_MAX_FIX_FILE_CHARS } from "./config-defaults";
11
12
 
12
13
  // ─── Types ──────────────────────────────────────────────────────────────────────
13
14
 
@@ -30,14 +31,14 @@ interface FixResult {
30
31
  * ~10K tokens — enough for any realistic error listing; prevents a pathological
31
32
  * build output (e.g. 10MB of warnings) from ballooning the AI context.
32
33
  */
33
- const MAX_COMMAND_OUTPUT_CHARS = 50_000;
34
+ const MAX_COMMAND_OUTPUT_CHARS = DEFAULT_MAX_COMMAND_OUTPUT_CHARS;
34
35
 
35
36
  /**
36
37
  * Maximum characters of an existing file sent to the AI for auto-fix.
37
38
  * ~12K tokens — covers large files; content beyond this is truncated with a
38
39
  * notice so the AI knows it may be seeing an incomplete file.
39
40
  */
40
- const MAX_FIX_FILE_CHARS = 60_000;
41
+ const MAX_FIX_FILE_CHARS = DEFAULT_MAX_FIX_FILE_CHARS;
41
42
 
42
43
  // ─── Error Detection ────────────────────────────────────────────────────────────
43
44
 
@@ -1,6 +1,7 @@
1
1
  import * as path from "path";
2
2
  import * as fs from "fs-extra";
3
3
  import { SpecDSL, ApiEndpoint, DataModel, ModelField, FieldMap } from "./dsl-types";
4
+ import { DEFAULT_OPENAPI_SERVER_URL } from "./config-defaults";
4
5
 
5
6
  // ─── Types ────────────────────────────────────────────────────────────────────
6
7
 
@@ -272,7 +273,7 @@ function buildYamlDoc(obj: Record<string, unknown>): string {
272
273
  * Convert a SpecDSL to an OpenAPI 3.1.0 document.
273
274
  * Returns the document as a plain JS object (can be serialised to YAML or JSON).
274
275
  */
275
- export function dslToOpenApi(dsl: SpecDSL, serverUrl = "http://localhost:3000"): Record<string, unknown> {
276
+ export function dslToOpenApi(dsl: SpecDSL, serverUrl = DEFAULT_OPENAPI_SERVER_URL): Record<string, unknown> {
276
277
  // ── Info ──────────────────────────────────────────────────────────────────
277
278
  const info = {
278
279
  title: dsl.feature.title,
@@ -338,7 +339,7 @@ export async function exportOpenApi(
338
339
  opts: OpenApiExportOptions = {}
339
340
  ): Promise<string> {
340
341
  const format = opts.format ?? "yaml";
341
- const serverUrl = opts.serverUrl ?? "http://localhost:3000";
342
+ const serverUrl = opts.serverUrl ?? DEFAULT_OPENAPI_SERVER_URL;
342
343
  const defaultName = `openapi.${format}`;
343
344
  const outputPath = opts.outputPath
344
345
  ? path.isAbsolute(opts.outputPath)
@@ -0,0 +1,95 @@
1
+ import * as fs from "fs-extra";
2
+ import * as path from "path";
3
+ import * as os from "os";
4
+ import { RepoType, RepoRole } from "./workspace-loader";
5
+
6
+ const REPO_STORE_FILE = path.join(os.homedir(), ".ai-spec-repos.json");
7
+
8
+ // ─── Types ───────────────────────────────────────────────────────────────────
9
+
10
+ export interface RegisteredRepo {
11
+ /** Display name (defaults to directory basename) */
12
+ name: string;
13
+ /** Absolute path to the repo root */
14
+ path: string;
15
+ /** Auto-detected repo type */
16
+ type: RepoType;
17
+ /** Auto-detected repo role */
18
+ role: RepoRole;
19
+ /** Whether a project constitution has been generated */
20
+ hasConstitution: boolean;
21
+ /** ISO timestamp of registration */
22
+ registeredAt: string;
23
+ }
24
+
25
+ interface RepoStoreData {
26
+ repos: RegisteredRepo[];
27
+ }
28
+
29
+ // ─── Read / Write ────────────────────────────────────────────────────────────
30
+
31
+ async function readStore(): Promise<RepoStoreData> {
32
+ try {
33
+ if (await fs.pathExists(REPO_STORE_FILE)) {
34
+ return await fs.readJson(REPO_STORE_FILE);
35
+ }
36
+ } catch (err) {
37
+ console.warn(`Warning: Could not read repo store at ${REPO_STORE_FILE}: ${(err as Error).message}.`);
38
+ }
39
+ return { repos: [] };
40
+ }
41
+
42
+ async function writeStore(store: RepoStoreData): Promise<void> {
43
+ await fs.ensureFile(REPO_STORE_FILE);
44
+ await fs.writeJson(REPO_STORE_FILE, store, { spaces: 2 });
45
+ }
46
+
47
+ // ─── Public API ──────────────────────────────────────────────────────────────
48
+
49
+ /** Get all registered repos. */
50
+ export async function getRegisteredRepos(): Promise<RegisteredRepo[]> {
51
+ const store = await readStore();
52
+ return store.repos;
53
+ }
54
+
55
+ /** Find a registered repo by its absolute path. */
56
+ export async function getRepoByPath(absPath: string): Promise<RegisteredRepo | undefined> {
57
+ const store = await readStore();
58
+ return store.repos.find((r) => r.path === absPath);
59
+ }
60
+
61
+ /** Register a new repo. If already registered (by path), update it. */
62
+ export async function registerRepo(repo: RegisteredRepo): Promise<void> {
63
+ const store = await readStore();
64
+ const idx = store.repos.findIndex((r) => r.path === repo.path);
65
+ if (idx >= 0) {
66
+ store.repos[idx] = repo;
67
+ } else {
68
+ store.repos.push(repo);
69
+ }
70
+ await writeStore(store);
71
+ }
72
+
73
+ /** Remove a registered repo by path. */
74
+ export async function unregisterRepo(absPath: string): Promise<boolean> {
75
+ const store = await readStore();
76
+ const before = store.repos.length;
77
+ store.repos = store.repos.filter((r) => r.path !== absPath);
78
+ if (store.repos.length < before) {
79
+ await writeStore(store);
80
+ return true;
81
+ }
82
+ return false;
83
+ }
84
+
85
+ /** Update the hasConstitution flag for a repo. */
86
+ export async function markRepoConstitution(absPath: string, has: boolean): Promise<void> {
87
+ const store = await readStore();
88
+ const repo = store.repos.find((r) => r.path === absPath);
89
+ if (repo) {
90
+ repo.hasConstitution = has;
91
+ await writeStore(store);
92
+ }
93
+ }
94
+
95
+ export { REPO_STORE_FILE };
package/core/reviewer.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  reviewImpactComplexitySystemPrompt,
11
11
  } from "../prompts/codegen.prompt";
12
12
  import { CONSTITUTION_FILE } from "./constitution-generator";
13
+ import { DEFAULT_REVIEW_HISTORY_FILE, DEFAULT_MAX_REVIEW_FILE_CHARS } from "./config-defaults";
13
14
 
14
15
  // ─── Constitution Lessons Helper ──────────────────────────────────────────────
15
16
 
@@ -48,7 +49,7 @@ interface ReviewHistoryEntry {
48
49
  complexityLevel?: "低" | "中" | "高";
49
50
  }
50
51
 
51
- const REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
52
+ const REVIEW_HISTORY_FILE = DEFAULT_REVIEW_HISTORY_FILE;
52
53
 
53
54
  async function loadReviewHistory(projectRoot: string): Promise<ReviewHistoryEntry[]> {
54
55
  const historyPath = path.join(projectRoot, REVIEW_HISTORY_FILE);
@@ -225,16 +226,19 @@ ${codeContext}`;
225
226
  console.log(chalk.gray(" Pass 2/3: Implementation review..."));
226
227
 
227
228
  // ── Pass 2: Implementation + History ─────────────────────────────────────
229
+ // Token savings: Pass 2/3 receive a spec digest instead of the full spec,
230
+ // and omit the raw code context (Pass 1 already analyzed it).
231
+ const specDigest = specContent && specContent.length > 600
232
+ ? specContent.slice(0, 600) + "\n... [spec truncated — see Pass 0/1 for full text]"
233
+ : specContent || "(No spec)";
234
+
228
235
  const history = await loadReviewHistory(this.projectRoot);
229
236
  const historyContext = buildHistoryContext(history);
230
237
 
231
238
  const implPrompt = `Review the implementation details of this change.
232
239
 
233
- === Feature Spec ===
234
- ${specContent || "(No spec — review for general code quality)"}
235
-
236
- === Code ===
237
- ${codeContext}
240
+ === Feature Spec (digest — full spec was provided in Pass 0/1) ===
241
+ ${specDigest}
238
242
 
239
243
  === Architecture Review (Pass 1 — do NOT repeat these findings) ===
240
244
  ${archReview}
@@ -246,11 +250,8 @@ ${historyContext}`;
246
250
  // ── Pass 3: Impact & Complexity ───────────────────────────────────────────
247
251
  const impactPrompt = `Assess the impact and complexity of this change.
248
252
 
249
- === Feature Spec ===
250
- ${specContent || "(No spec — review for general code quality)"}
251
-
252
- === Code ===
253
- ${codeContext}
253
+ === Feature Spec (digest) ===
254
+ ${specDigest}
254
255
 
255
256
  === Architecture Review (Pass 1 — do NOT repeat) ===
256
257
  ${archReview}
@@ -338,8 +339,8 @@ ${implReview}`;
338
339
  const fullPath = path.join(workingDir, filePath);
339
340
  try {
340
341
  const content = await fs.readFile(fullPath, "utf-8");
341
- filesSection += `\n\n=== ${filePath} ===\n${content.slice(0, 3000)}`;
342
- if (content.length > 3000) filesSection += `\n... (truncated, ${content.length} chars total)`;
342
+ filesSection += `\n\n=== ${filePath} ===\n${content.slice(0, DEFAULT_MAX_REVIEW_FILE_CHARS)}`;
343
+ if (content.length > DEFAULT_MAX_REVIEW_FILE_CHARS) filesSection += `\n... (truncated, ${content.length} chars total)`;
343
344
  } catch {
344
345
  filesSection += `\n\n=== ${filePath} ===\n(file not found)`;
345
346
  }
@@ -1,8 +1,7 @@
1
1
  import * as fs from "fs-extra";
2
2
  import * as path from "path";
3
3
  import chalk from "chalk";
4
-
5
- const LOG_DIR = ".ai-spec-logs";
4
+ import { DEFAULT_LOG_DIR } from "./config-defaults";
6
5
 
7
6
  // ─── JSONL helpers ────────────────────────────────────────────────────────────
8
7
  // Each event is synchronously appended as one JSON line to a `.jsonl` shadow
@@ -131,8 +130,8 @@ export class RunLogger {
131
130
  meta?: { provider?: string; model?: string; specPath?: string }
132
131
  ) {
133
132
  this.startMs = Date.now();
134
- this.logPath = path.join(workingDir, LOG_DIR, `${runId}.json`);
135
- this.jsonlPath = path.join(workingDir, LOG_DIR, `${runId}.jsonl`);
133
+ this.logPath = path.join(workingDir, DEFAULT_LOG_DIR, `${runId}.json`);
134
+ this.jsonlPath = path.join(workingDir, DEFAULT_LOG_DIR, `${runId}.jsonl`);
136
135
  this.log = {
137
136
  runId,
138
137
  startedAt: new Date().toISOString(),
@@ -1,7 +1,6 @@
1
1
  import * as fs from "fs-extra";
2
2
  import * as path from "path";
3
-
4
- const BACKUP_DIR = ".ai-spec-backup";
3
+ import { DEFAULT_BACKUP_DIR } from "./config-defaults";
5
4
 
6
5
  // ─── RunSnapshot ──────────────────────────────────────────────────────────────
7
6
  /**
@@ -18,7 +17,7 @@ export class RunSnapshot {
18
17
  private readonly workingDir: string,
19
18
  readonly runId: string
20
19
  ) {
21
- this.backupRoot = path.join(workingDir, BACKUP_DIR, runId);
20
+ this.backupRoot = path.join(workingDir, DEFAULT_BACKUP_DIR, runId);
22
21
  }
23
22
 
24
23
  /**
package/core/run-trend.ts CHANGED
@@ -2,8 +2,7 @@ import * as fs from "fs-extra";
2
2
  import * as path from "path";
3
3
  import chalk from "chalk";
4
4
  import { RunLog, reconstructRunLogFromJsonl } from "./run-logger";
5
-
6
- const LOG_DIR = ".ai-spec-logs";
5
+ import { DEFAULT_LOG_DIR } from "./config-defaults";
7
6
 
8
7
  // ─── Types ────────────────────────────────────────────────────────────────────
9
8
 
@@ -45,7 +44,7 @@ export interface TrendReport {
45
44
  * Silently skips unreadable / corrupt files.
46
45
  */
47
46
  export async function loadRunLogs(workingDir: string): Promise<RunLog[]> {
48
- const logDir = path.join(workingDir, LOG_DIR);
47
+ const logDir = path.join(workingDir, DEFAULT_LOG_DIR);
49
48
  if (!(await fs.pathExists(logDir))) return [];
50
49
 
51
50
  const files = await fs.readdir(logDir);
@@ -255,7 +254,7 @@ export function printTrendReport(report: TrendReport, workingDir: string): void
255
254
  }
256
255
 
257
256
  // ── Footer ────────────────────────────────────────────────────────
258
- const logRelDir = path.relative(workingDir, path.join(workingDir, LOG_DIR));
257
+ const logRelDir = path.relative(workingDir, path.join(workingDir, DEFAULT_LOG_DIR));
259
258
  console.log(chalk.gray(`\n ${entries.length} run(s) shown · logs: ${logRelDir}/`));
260
259
  console.log(chalk.cyan("─".repeat(63)));
261
260
  }
@@ -1,16 +1,15 @@
1
1
  import { GoogleGenerativeAI } from "@google/generative-ai";
2
2
  import Anthropic from "@anthropic-ai/sdk";
3
3
  import OpenAI from "openai";
4
- import axios from "axios";
5
4
  import { ProxyAgent } from "undici";
6
5
  import { specPrompt } from "../prompts/spec.prompt";
7
6
  import { ProjectContext } from "./context-loader";
8
7
  import { withReliability } from "./provider-utils";
9
8
 
10
9
  // ─── Proxy Helper ─────────────────────────────────────────────────────────────
11
- // 仅用于 Gemini:其他 SDK(Anthropic / OpenAI)会自动读取 HTTPS_PROXY。
12
10
  // Gemini SDK 使用 Node.js 原生 fetch(undici),不会自动读代理环境变量,
13
11
  // 需要手动创建 ProxyAgent 并通过 fetchOptions 注入。
12
+ // Anthropic SDK (node-fetch) 也不会自动读代理环境变量。
14
13
  // 这是 in-process 级别的配置,完全不影响 execSync 启动的子进程(如 claude CLI)。
15
14
 
16
15
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -47,6 +46,8 @@ export interface ProviderMeta {
47
46
  models: string[];
48
47
  /** Environment variable name for the API key */
49
48
  envKey: string;
49
+ /** Fallback env var names checked if envKey is not set */
50
+ fallbackEnvKeys?: string[];
50
51
  /**
51
52
  * Base URL for OpenAI-compatible providers.
52
53
  * Undefined means the provider has its own SDK (Gemini / Claude).
@@ -72,6 +73,8 @@ export const PROVIDER_CATALOG: Record<string, ProviderMeta> = {
72
73
  description: "小米 MiMo — mimo-v2-pro (Anthropic-compatible API)",
73
74
  models: ["mimo-v2-pro"],
74
75
  envKey: "MIMO_API_KEY",
76
+ // Fallback env var — MiMo's token plan uses ANTHROPIC_AUTH_TOKEN
77
+ fallbackEnvKeys: ["ANTHROPIC_AUTH_TOKEN"],
75
78
  // baseURL not used — MiMo has a dedicated provider class
76
79
  },
77
80
  gemini: {
@@ -244,8 +247,8 @@ export class ClaudeProvider implements AIProvider {
244
247
  ...(systemInstruction ? { system: systemInstruction } : {}),
245
248
  messages: [{ role: "user", content: prompt }],
246
249
  });
247
- const block = message.content[0];
248
- if (block.type === "text") return block.text;
250
+ const textBlock = message.content.find((b) => b.type === "text");
251
+ if (textBlock) return textBlock.text;
249
252
  throw new Error("Unexpected response type from Claude API");
250
253
  },
251
254
  { label: `${this.providerName}/${this.modelName}` }
@@ -307,58 +310,40 @@ export class OpenAICompatibleProvider implements AIProvider {
307
310
  // ─── MiMo Provider ─────────────────────────────────────────────────────────────
308
311
  // MiMo uses the Anthropic messages format but with a different base URL
309
312
  // and a custom "api-key" auth header (not "x-api-key" / "Authorization: Bearer").
310
- // The Anthropic SDK does not support custom auth headers, so we call the API
311
- // directly via axios.
313
+ // MiMo's token-plan API is Anthropic-compatible we reuse the Anthropic SDK
314
+ // directly, reading base URL from env (MIMO_BASE_URL / ANTHROPIC_BASE_URL).
312
315
 
313
316
  export class MiMoProvider implements AIProvider {
317
+ private client: Anthropic;
314
318
  readonly providerName = "mimo";
315
319
  readonly modelName: string;
316
- private apiKey: string;
317
- private readonly baseUrl = "https://api.xiaomimimo.com/anthropic/v1/messages";
318
320
 
319
321
  constructor(apiKey: string, modelName = PROVIDER_CATALOG.mimo.models[0]) {
320
- this.apiKey = apiKey;
322
+ const baseURL = process.env["MIMO_BASE_URL"]
323
+ || process.env["ANTHROPIC_BASE_URL"]
324
+ || "https://token-plan-cn.xiaomimimo.com/anthropic";
325
+ this.client = new Anthropic({ apiKey, baseURL });
321
326
  this.modelName = modelName;
322
327
  }
323
328
 
324
329
  async generate(prompt: string, systemInstruction?: string): Promise<string> {
325
330
  return withReliability(
326
331
  async () => {
327
- const body: Record<string, unknown> = {
332
+ // Use streaming to avoid timeout errors with large max_tokens
333
+ const stream = this.client.messages.stream({
328
334
  model: this.modelName,
329
- max_tokens: 16384,
330
- messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
331
- top_p: 0.95,
332
- stream: false,
333
- temperature: 1.0,
334
- stop_sequences: null,
335
- };
336
-
337
- if (systemInstruction) {
338
- body.system = systemInstruction;
339
- }
340
-
341
- const response = await axios.post(this.baseUrl, body, {
342
- headers: {
343
- "api-key": this.apiKey,
344
- "Content-Type": "application/json",
345
- },
335
+ max_tokens: 65536,
336
+ ...(systemInstruction ? { system: systemInstruction } : {}),
337
+ messages: [{ role: "user", content: prompt }],
346
338
  });
347
-
348
- // Response follows Anthropic format: { content: [{ type: "text"|"thinking", ... }] }
349
- // MiMo may return a "thinking" block before the actual "text" block skip it.
350
- const data = response.data as { stop_reason?: string; content?: Array<{ type: string; text?: string; thinking?: string }> };
351
- const blocks = data?.content ?? [];
352
-
353
- const textBlock = blocks.find((b) => b.type === "text");
354
- if (textBlock?.text) return textBlock.text;
355
-
356
- // If stop_reason is max_tokens, the model was cut off mid-generation (thinking block only)
357
- if (data?.stop_reason === "max_tokens") {
358
- throw new Error(`MiMo response truncated (max_tokens reached). The prompt may be too long. Try a shorter spec or switch to a model with larger context.`);
359
- }
360
-
361
- throw new Error(`Unexpected MiMo response: ${JSON.stringify(response.data).slice(0, 200)}`);
339
+ const message = await stream.finalMessage();
340
+ // MiMo may return "thinking" blocks before or instead of "text" blocks.
341
+ // Extract the first text block; fall back to thinking content; last resort: concatenate all.
342
+ const textBlock = message.content.find((b) => b.type === "text");
343
+ if (textBlock) return textBlock.text;
344
+ const thinkBlock = message.content.find((b) => b.type === "thinking");
345
+ if (thinkBlock) return (thinkBlock as unknown as { thinking: string }).thinking;
346
+ return message.content.map((b: { type: string; text?: string }) => b.text ?? "").join("");
362
347
  },
363
348
  { label: `${this.providerName}/${this.modelName}` }
364
349
  );
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import chalk from "chalk";
9
+ import { DEFAULT_TOKEN_BUDGETS as _CONFIG_BUDGETS } from "./config-defaults";
9
10
 
10
11
  // ─── Token Estimation ────────────────────────────────────────────────────────
11
12
 
@@ -110,14 +111,8 @@ export function assembleSections(
110
111
 
111
112
  // ─── Default Budgets ─────────────────────────────────────────────────────────
112
113
 
113
- /** Default context token budgets per provider. */
114
- export const DEFAULT_TOKEN_BUDGETS: Record<string, number> = {
115
- gemini: 900_000,
116
- claude: 180_000,
117
- openai: 120_000,
118
- deepseek: 60_000,
119
- default: 100_000,
120
- };
114
+ /** Default context token budgets per provider (sourced from config-defaults). */
115
+ export const DEFAULT_TOKEN_BUDGETS = _CONFIG_BUDGETS;
121
116
 
122
117
  export function getDefaultBudget(providerName: string): number {
123
118
  return DEFAULT_TOKEN_BUDGETS[providerName] ?? DEFAULT_TOKEN_BUDGETS.default;
package/core/vcr.ts CHANGED
@@ -27,7 +27,9 @@ import * as fs from "fs-extra";
27
27
  import * as path from "path";
28
28
  import { AIProvider } from "./spec-generator";
29
29
 
30
- export const VCR_DIR = ".ai-spec-vcr";
30
+ import { DEFAULT_VCR_DIR } from "./config-defaults";
31
+
32
+ export const VCR_DIR = DEFAULT_VCR_DIR;
31
33
 
32
34
  // ─── Types ────────────────────────────────────────────────────────────────────
33
35