ai-spec-dev 0.41.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.
package/dist/index.d.mts CHANGED
@@ -81,6 +81,8 @@ interface ProviderMeta {
81
81
  models: string[];
82
82
  /** Environment variable name for the API key */
83
83
  envKey: string;
84
+ /** Fallback env var names checked if envKey is not set */
85
+ fallbackEnvKeys?: string[];
84
86
  /**
85
87
  * Base URL for OpenAI-compatible providers.
86
88
  * Undefined means the provider has its own SDK (Gemini / Claude).
@@ -126,10 +128,9 @@ declare class OpenAICompatibleProvider implements AIProvider {
126
128
  generate(prompt: string, systemInstruction?: string): Promise<string>;
127
129
  }
128
130
  declare class MiMoProvider implements AIProvider {
131
+ private client;
129
132
  readonly providerName = "mimo";
130
133
  readonly modelName: string;
131
- private apiKey;
132
- private readonly baseUrl;
133
134
  constructor(apiKey: string, modelName?: string);
134
135
  generate(prompt: string, systemInstruction?: string): Promise<string>;
135
136
  }
package/dist/index.d.ts CHANGED
@@ -81,6 +81,8 @@ interface ProviderMeta {
81
81
  models: string[];
82
82
  /** Environment variable name for the API key */
83
83
  envKey: string;
84
+ /** Fallback env var names checked if envKey is not set */
85
+ fallbackEnvKeys?: string[];
84
86
  /**
85
87
  * Base URL for OpenAI-compatible providers.
86
88
  * Undefined means the provider has its own SDK (Gemini / Claude).
@@ -126,10 +128,9 @@ declare class OpenAICompatibleProvider implements AIProvider {
126
128
  generate(prompt: string, systemInstruction?: string): Promise<string>;
127
129
  }
128
130
  declare class MiMoProvider implements AIProvider {
131
+ private client;
129
132
  readonly providerName = "mimo";
130
133
  readonly modelName: string;
131
- private apiKey;
132
- private readonly baseUrl;
133
134
  constructor(apiKey: string, modelName?: string);
134
135
  generate(prompt: string, systemInstruction?: string): Promise<string>;
135
136
  }
package/dist/index.js CHANGED
@@ -68,7 +68,6 @@ module.exports = __toCommonJS(index_exports);
68
68
  var import_generative_ai = require("@google/generative-ai");
69
69
  var import_sdk = __toESM(require("@anthropic-ai/sdk"));
70
70
  var import_openai = __toESM(require("openai"));
71
- var import_axios = __toESM(require("axios"));
72
71
  var import_undici = require("undici");
73
72
 
74
73
  // prompts/spec.prompt.ts
@@ -182,9 +181,77 @@ CRITICAL \u2014 \u5386\u53F2\u6559\u8BAD\u5E94\u7528\uFF08Accumulated Lessons\uF
182
181
  3. \u5BF9\u4E8E\u6BCF\u6761\u76F4\u63A5\u76F8\u5173\u7684\u6559\u8BAD\uFF0C\u5728 \xA78 \u5B9E\u65BD\u8981\u70B9\u672B\u5C3E\u8FFD\u52A0\u4E00\u884C\uFF1A\u300C\u26A0 \u57FA\u4E8E\u5386\u53F2\u6559\u8BAD\uFF1A[\u7B80\u8FF0\u672C\u6B21 spec \u5982\u4F55\u89C4\u907F\u8BE5\u95EE\u9898]\u300D
183
182
  4. \u5982\u65E0\u76F8\u5173\u6559\u8BAD\uFF0C\xA78 \u4E0D\u5FC5\u8FFD\u52A0\u4EFB\u4F55\u5185\u5BB9`;
184
183
 
185
- // core/provider-utils.ts
184
+ // core/cli-ui.ts
186
185
  var import_chalk = __toESM(require("chalk"));
187
- var sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
186
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
187
+ function startSpinner(text) {
188
+ const isTTY = process.stderr.isTTY;
189
+ let frame = 0;
190
+ let currentText = text;
191
+ let stopped = false;
192
+ function render() {
193
+ if (stopped) return;
194
+ const symbol = import_chalk.default.cyan(SPINNER_FRAMES[frame % SPINNER_FRAMES.length]);
195
+ if (isTTY) {
196
+ process.stderr.write(`\r ${symbol} ${currentText}${" ".repeat(10)}`);
197
+ }
198
+ frame++;
199
+ }
200
+ if (!isTTY) {
201
+ process.stderr.write(` \u2026 ${currentText}
202
+ `);
203
+ }
204
+ const timer = setInterval(render, 80);
205
+ render();
206
+ return {
207
+ update(newText) {
208
+ currentText = newText;
209
+ },
210
+ stop(finalText) {
211
+ if (stopped) return;
212
+ stopped = true;
213
+ clearInterval(timer);
214
+ if (isTTY) {
215
+ process.stderr.write(`\r${" ".repeat(currentText.length + 20)}\r`);
216
+ }
217
+ if (finalText) {
218
+ process.stderr.write(` ${finalText}
219
+ `);
220
+ }
221
+ },
222
+ succeed(successText) {
223
+ this.stop(import_chalk.default.green(`\u2714 ${successText}`));
224
+ },
225
+ fail(failText) {
226
+ this.stop(import_chalk.default.red(`\u2718 ${failText}`));
227
+ }
228
+ };
229
+ }
230
+ async function retryCountdown(opts) {
231
+ const { attempt, maxAttempts, waitMs, errorMessage, label } = opts;
232
+ const isTTY = process.stderr.isTTY;
233
+ const shortErr = errorMessage.length > 120 ? errorMessage.slice(0, 117) + "..." : errorMessage;
234
+ process.stderr.write("\n");
235
+ process.stderr.write(import_chalk.default.yellow(` \u250C\u2500 Retry ${attempt}/${maxAttempts} `) + import_chalk.default.gray(`[${label}]`) + import_chalk.default.yellow(` ${"\u2500".repeat(Math.max(1, 40 - label.length))}
236
+ `));
237
+ process.stderr.write(import_chalk.default.yellow(` \u2502 `) + import_chalk.default.white(shortErr) + "\n");
238
+ process.stderr.write(import_chalk.default.yellow(` \u2502 `) + import_chalk.default.gray(`Waiting before retry...`) + "\n");
239
+ const totalSeconds = Math.ceil(waitMs / 1e3);
240
+ for (let s = totalSeconds; s > 0; s--) {
241
+ const bar = import_chalk.default.green("\u2588".repeat(totalSeconds - s)) + import_chalk.default.gray("\u2591".repeat(s));
242
+ const line = import_chalk.default.yellow(` \u2502 `) + `${bar} ${import_chalk.default.bold.white(`${s}s`)}`;
243
+ if (isTTY) {
244
+ process.stderr.write(`\r${line}${" ".repeat(10)}`);
245
+ }
246
+ await new Promise((r) => setTimeout(r, 1e3));
247
+ }
248
+ if (isTTY) {
249
+ process.stderr.write(`\r${" ".repeat(70)}\r`);
250
+ }
251
+ process.stderr.write(import_chalk.default.yellow(` \u2514\u2500 `) + import_chalk.default.cyan(`Retrying now...`) + "\n\n");
252
+ }
253
+
254
+ // core/provider-utils.ts
188
255
  var ProviderError = class extends Error {
189
256
  constructor(message, kind, originalError) {
190
257
  super(message);
@@ -268,11 +335,14 @@ async function withReliability(fn, opts) {
268
335
  throw classifyError(err, label);
269
336
  }
270
337
  const waitMs = attempt === 0 ? 2e3 : 6e3;
271
- console.warn(
272
- import_chalk.default.yellow(` \u26A0 ${label} failed (attempt ${attempt + 1}/${retries + 1}), retrying in ${waitMs / 1e3}s`) + import_chalk.default.gray(` \u2014 ${err.message}`)
273
- );
274
338
  onRetry?.(attempt + 1, err);
275
- await sleep(waitMs);
339
+ await retryCountdown({
340
+ attempt: attempt + 1,
341
+ maxAttempts: retries + 1,
342
+ waitMs,
343
+ errorMessage: err.message ?? String(err),
344
+ label
345
+ });
276
346
  }
277
347
  }
278
348
  throw new Error("unreachable");
@@ -290,7 +360,9 @@ var PROVIDER_CATALOG = {
290
360
  displayName: "MiMo (Xiaomi)",
291
361
  description: "\u5C0F\u7C73 MiMo \u2014 mimo-v2-pro (Anthropic-compatible API)",
292
362
  models: ["mimo-v2-pro"],
293
- envKey: "MIMO_API_KEY"
363
+ envKey: "MIMO_API_KEY",
364
+ // Fallback env var — MiMo's token plan uses ANTHROPIC_AUTH_TOKEN
365
+ fallbackEnvKeys: ["ANTHROPIC_AUTH_TOKEN"]
294
366
  // baseURL not used — MiMo has a dedicated provider class
295
367
  },
296
368
  gemini: {
@@ -459,8 +531,8 @@ var ClaudeProvider = class {
459
531
  ...systemInstruction ? { system: systemInstruction } : {},
460
532
  messages: [{ role: "user", content: prompt }]
461
533
  });
462
- const block = message.content[0];
463
- if (block.type === "text") return block.text;
534
+ const textBlock = message.content.find((b) => b.type === "text");
535
+ if (textBlock) return textBlock.text;
464
536
  throw new Error("Unexpected response type from Claude API");
465
537
  },
466
538
  { label: `${this.providerName}/${this.modelName}` }
@@ -505,43 +577,29 @@ var OpenAICompatibleProvider = class {
505
577
  }
506
578
  };
507
579
  var MiMoProvider = class {
580
+ client;
508
581
  providerName = "mimo";
509
582
  modelName;
510
- apiKey;
511
- baseUrl = "https://api.xiaomimimo.com/anthropic/v1/messages";
512
583
  constructor(apiKey, modelName = PROVIDER_CATALOG.mimo.models[0]) {
513
- this.apiKey = apiKey;
584
+ const baseURL = process.env["MIMO_BASE_URL"] || process.env["ANTHROPIC_BASE_URL"] || "https://token-plan-cn.xiaomimimo.com/anthropic";
585
+ this.client = new import_sdk.default({ apiKey, baseURL });
514
586
  this.modelName = modelName;
515
587
  }
516
588
  async generate(prompt, systemInstruction) {
517
589
  return withReliability(
518
590
  async () => {
519
- const body = {
591
+ const stream = this.client.messages.stream({
520
592
  model: this.modelName,
521
- max_tokens: 16384,
522
- messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
523
- top_p: 0.95,
524
- stream: false,
525
- temperature: 1,
526
- stop_sequences: null
527
- };
528
- if (systemInstruction) {
529
- body.system = systemInstruction;
530
- }
531
- const response = await import_axios.default.post(this.baseUrl, body, {
532
- headers: {
533
- "api-key": this.apiKey,
534
- "Content-Type": "application/json"
535
- }
593
+ max_tokens: 65536,
594
+ ...systemInstruction ? { system: systemInstruction } : {},
595
+ messages: [{ role: "user", content: prompt }]
536
596
  });
537
- const data = response.data;
538
- const blocks = data?.content ?? [];
539
- const textBlock = blocks.find((b) => b.type === "text");
540
- if (textBlock?.text) return textBlock.text;
541
- if (data?.stop_reason === "max_tokens") {
542
- 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.`);
543
- }
544
- throw new Error(`Unexpected MiMo response: ${JSON.stringify(response.data).slice(0, 200)}`);
597
+ const message = await stream.finalMessage();
598
+ const textBlock = message.content.find((b) => b.type === "text");
599
+ if (textBlock) return textBlock.text;
600
+ const thinkBlock = message.content.find((b) => b.type === "thinking");
601
+ if (thinkBlock) return thinkBlock.thinking;
602
+ return message.content.map((b) => b.text ?? "").join("");
545
603
  },
546
604
  { label: `${this.providerName}/${this.modelName}` }
547
605
  );
@@ -5146,13 +5204,11 @@ function typeLabel(v2) {
5146
5204
 
5147
5205
  // core/token-budget.ts
5148
5206
  var import_chalk6 = __toESM(require("chalk"));
5149
- var CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g;
5150
- function estimateTokens(text) {
5151
- if (!text) return 0;
5152
- const cjkCount = (text.match(CJK_RANGE) ?? []).length;
5153
- const nonCjkLength = text.length - cjkCount;
5154
- return Math.ceil(cjkCount + nonCjkLength / 4);
5155
- }
5207
+
5208
+ // core/config-defaults.ts
5209
+ var DEFAULT_REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
5210
+ var DEFAULT_MAX_CONSTITUTION_CHARS = 4e3;
5211
+ var DEFAULT_MAX_REVIEW_FILE_CHARS = 3e3;
5156
5212
  var DEFAULT_TOKEN_BUDGETS = {
5157
5213
  gemini: 9e5,
5158
5214
  claude: 18e4,
@@ -5160,8 +5216,18 @@ var DEFAULT_TOKEN_BUDGETS = {
5160
5216
  deepseek: 6e4,
5161
5217
  default: 1e5
5162
5218
  };
5219
+
5220
+ // core/token-budget.ts
5221
+ var CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g;
5222
+ function estimateTokens(text) {
5223
+ if (!text) return 0;
5224
+ const cjkCount = (text.match(CJK_RANGE) ?? []).length;
5225
+ const nonCjkLength = text.length - cjkCount;
5226
+ return Math.ceil(cjkCount + nonCjkLength / 4);
5227
+ }
5228
+ var DEFAULT_TOKEN_BUDGETS2 = DEFAULT_TOKEN_BUDGETS;
5163
5229
  function getDefaultBudget(providerName) {
5164
- return DEFAULT_TOKEN_BUDGETS[providerName] ?? DEFAULT_TOKEN_BUDGETS.default;
5230
+ return DEFAULT_TOKEN_BUDGETS2[providerName] ?? DEFAULT_TOKEN_BUDGETS2.default;
5165
5231
  }
5166
5232
 
5167
5233
  // core/dsl-extractor.ts
@@ -6463,6 +6529,7 @@ ${spec}
6463
6529
  ${constitutionSection}
6464
6530
  === ${existingContent ? "Existing content (modify and return the complete file)" : "Create this file from scratch"} ===
6465
6531
  ${existingContent || "Output only the complete file content."}`;
6532
+ const fileSpinner = startSpinner(`${prefix}Generating ${import_chalk10.default.bold(item.file)}...`);
6466
6533
  try {
6467
6534
  const raw = await this.provider.generate(codePrompt, systemPrompt);
6468
6535
  const fileContent = stripCodeFences(raw);
@@ -6470,11 +6537,11 @@ ${existingContent || "Output only the complete file content."}`;
6470
6537
  await fs10.ensureDir(path6.dirname(fullPath));
6471
6538
  await fs10.writeFile(fullPath, fileContent, "utf-8");
6472
6539
  getActiveLogger()?.fileWritten(item.file);
6473
- console.log(`${prefix}${existingContent ? import_chalk10.default.yellow("~") : import_chalk10.default.green("+")} ${import_chalk10.default.bold(item.file)} ${import_chalk10.default.green("\u2714")}`);
6540
+ fileSpinner.succeed(`${existingContent ? import_chalk10.default.yellow("~") : import_chalk10.default.green("+")} ${import_chalk10.default.bold(item.file)}`);
6474
6541
  successCount++;
6475
6542
  writtenFiles.push(item.file);
6476
6543
  } catch (err) {
6477
- console.log(`${prefix}${import_chalk10.default.red("\u2718")} ${import_chalk10.default.bold(item.file)} \u2014 ${import_chalk10.default.red(err.message)}`);
6544
+ fileSpinner.fail(`${import_chalk10.default.bold(item.file)} \u2014 ${err.message}`);
6478
6545
  }
6479
6546
  }
6480
6547
  if (!taskLabel) {
@@ -6621,7 +6688,7 @@ ${context.routeSummary}
6621
6688
  }
6622
6689
  if (context.schema) {
6623
6690
  parts.push(`=== Prisma Schema ===
6624
- ${context.schema.slice(0, 4e3)}
6691
+ ${context.schema.slice(0, DEFAULT_MAX_CONSTITUTION_CHARS)}
6625
6692
  `);
6626
6693
  }
6627
6694
  if (context.errorPatterns) {
@@ -6685,7 +6752,7 @@ async function loadAccumulatedLessons(projectRoot) {
6685
6752
  const nextSection = section.slice(marker.length).match(/\n## \d/);
6686
6753
  return nextSection ? section.slice(0, marker.length + nextSection.index) : section;
6687
6754
  }
6688
- var REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
6755
+ var REVIEW_HISTORY_FILE = DEFAULT_REVIEW_HISTORY_FILE;
6689
6756
  async function loadReviewHistory(projectRoot) {
6690
6757
  const historyPath = path8.join(projectRoot, REVIEW_HISTORY_FILE);
6691
6758
  try {
@@ -6816,15 +6883,13 @@ ${specContent || "(No spec \u2014 review for general code quality)"}
6816
6883
  ${codeContext}`;
6817
6884
  const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
6818
6885
  console.log(import_chalk12.default.gray(" Pass 2/3: Implementation review..."));
6886
+ const specDigest = specContent && specContent.length > 600 ? specContent.slice(0, 600) + "\n... [spec truncated \u2014 see Pass 0/1 for full text]" : specContent || "(No spec)";
6819
6887
  const history = await loadReviewHistory(this.projectRoot);
6820
6888
  const historyContext = buildHistoryContext(history);
6821
6889
  const implPrompt = `Review the implementation details of this change.
6822
6890
 
6823
- === Feature Spec ===
6824
- ${specContent || "(No spec \u2014 review for general code quality)"}
6825
-
6826
- === Code ===
6827
- ${codeContext}
6891
+ === Feature Spec (digest \u2014 full spec was provided in Pass 0/1) ===
6892
+ ${specDigest}
6828
6893
 
6829
6894
  === Architecture Review (Pass 1 \u2014 do NOT repeat these findings) ===
6830
6895
  ${archReview}
@@ -6833,11 +6898,8 @@ ${historyContext}`;
6833
6898
  console.log(import_chalk12.default.gray(" Pass 3/3: Impact & complexity assessment..."));
6834
6899
  const impactPrompt = `Assess the impact and complexity of this change.
6835
6900
 
6836
- === Feature Spec ===
6837
- ${specContent || "(No spec \u2014 review for general code quality)"}
6838
-
6839
- === Code ===
6840
- ${codeContext}
6901
+ === Feature Spec (digest) ===
6902
+ ${specDigest}
6841
6903
 
6842
6904
  === Architecture Review (Pass 1 \u2014 do NOT repeat) ===
6843
6905
  ${archReview}
@@ -6911,8 +6973,8 @@ ${sep}
6911
6973
  filesSection += `
6912
6974
 
6913
6975
  === ${filePath} ===
6914
- ${content.slice(0, 3e3)}`;
6915
- if (content.length > 3e3) filesSection += `
6976
+ ${content.slice(0, DEFAULT_MAX_REVIEW_FILE_CHARS)}`;
6977
+ if (content.length > DEFAULT_MAX_REVIEW_FILE_CHARS) filesSection += `
6916
6978
  ... (truncated, ${content.length} chars total)`;
6917
6979
  } catch {
6918
6980
  filesSection += `