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.mjs CHANGED
@@ -2,7 +2,6 @@
2
2
  import { GoogleGenerativeAI } from "@google/generative-ai";
3
3
  import Anthropic from "@anthropic-ai/sdk";
4
4
  import OpenAI from "openai";
5
- import axios from "axios";
6
5
  import { ProxyAgent } from "undici";
7
6
 
8
7
  // prompts/spec.prompt.ts
@@ -116,9 +115,77 @@ CRITICAL \u2014 \u5386\u53F2\u6559\u8BAD\u5E94\u7528\uFF08Accumulated Lessons\uF
116
115
  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
117
116
  4. \u5982\u65E0\u76F8\u5173\u6559\u8BAD\uFF0C\xA78 \u4E0D\u5FC5\u8FFD\u52A0\u4EFB\u4F55\u5185\u5BB9`;
118
117
 
119
- // core/provider-utils.ts
118
+ // core/cli-ui.ts
120
119
  import chalk from "chalk";
121
- var sleep = (ms2) => new Promise((r) => setTimeout(r, ms2));
120
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
121
+ function startSpinner(text) {
122
+ const isTTY = process.stderr.isTTY;
123
+ let frame = 0;
124
+ let currentText = text;
125
+ let stopped = false;
126
+ function render() {
127
+ if (stopped) return;
128
+ const symbol = chalk.cyan(SPINNER_FRAMES[frame % SPINNER_FRAMES.length]);
129
+ if (isTTY) {
130
+ process.stderr.write(`\r ${symbol} ${currentText}${" ".repeat(10)}`);
131
+ }
132
+ frame++;
133
+ }
134
+ if (!isTTY) {
135
+ process.stderr.write(` \u2026 ${currentText}
136
+ `);
137
+ }
138
+ const timer = setInterval(render, 80);
139
+ render();
140
+ return {
141
+ update(newText) {
142
+ currentText = newText;
143
+ },
144
+ stop(finalText) {
145
+ if (stopped) return;
146
+ stopped = true;
147
+ clearInterval(timer);
148
+ if (isTTY) {
149
+ process.stderr.write(`\r${" ".repeat(currentText.length + 20)}\r`);
150
+ }
151
+ if (finalText) {
152
+ process.stderr.write(` ${finalText}
153
+ `);
154
+ }
155
+ },
156
+ succeed(successText) {
157
+ this.stop(chalk.green(`\u2714 ${successText}`));
158
+ },
159
+ fail(failText) {
160
+ this.stop(chalk.red(`\u2718 ${failText}`));
161
+ }
162
+ };
163
+ }
164
+ async function retryCountdown(opts) {
165
+ const { attempt, maxAttempts, waitMs, errorMessage, label } = opts;
166
+ const isTTY = process.stderr.isTTY;
167
+ const shortErr = errorMessage.length > 120 ? errorMessage.slice(0, 117) + "..." : errorMessage;
168
+ process.stderr.write("\n");
169
+ process.stderr.write(chalk.yellow(` \u250C\u2500 Retry ${attempt}/${maxAttempts} `) + chalk.gray(`[${label}]`) + chalk.yellow(` ${"\u2500".repeat(Math.max(1, 40 - label.length))}
170
+ `));
171
+ process.stderr.write(chalk.yellow(` \u2502 `) + chalk.white(shortErr) + "\n");
172
+ process.stderr.write(chalk.yellow(` \u2502 `) + chalk.gray(`Waiting before retry...`) + "\n");
173
+ const totalSeconds = Math.ceil(waitMs / 1e3);
174
+ for (let s = totalSeconds; s > 0; s--) {
175
+ const bar = chalk.green("\u2588".repeat(totalSeconds - s)) + chalk.gray("\u2591".repeat(s));
176
+ const line = chalk.yellow(` \u2502 `) + `${bar} ${chalk.bold.white(`${s}s`)}`;
177
+ if (isTTY) {
178
+ process.stderr.write(`\r${line}${" ".repeat(10)}`);
179
+ }
180
+ await new Promise((r) => setTimeout(r, 1e3));
181
+ }
182
+ if (isTTY) {
183
+ process.stderr.write(`\r${" ".repeat(70)}\r`);
184
+ }
185
+ process.stderr.write(chalk.yellow(` \u2514\u2500 `) + chalk.cyan(`Retrying now...`) + "\n\n");
186
+ }
187
+
188
+ // core/provider-utils.ts
122
189
  var ProviderError = class extends Error {
123
190
  constructor(message, kind, originalError) {
124
191
  super(message);
@@ -202,11 +269,14 @@ async function withReliability(fn, opts) {
202
269
  throw classifyError(err, label);
203
270
  }
204
271
  const waitMs = attempt === 0 ? 2e3 : 6e3;
205
- console.warn(
206
- chalk.yellow(` \u26A0 ${label} failed (attempt ${attempt + 1}/${retries + 1}), retrying in ${waitMs / 1e3}s`) + chalk.gray(` \u2014 ${err.message}`)
207
- );
208
272
  onRetry?.(attempt + 1, err);
209
- await sleep(waitMs);
273
+ await retryCountdown({
274
+ attempt: attempt + 1,
275
+ maxAttempts: retries + 1,
276
+ waitMs,
277
+ errorMessage: err.message ?? String(err),
278
+ label
279
+ });
210
280
  }
211
281
  }
212
282
  throw new Error("unreachable");
@@ -224,7 +294,9 @@ var PROVIDER_CATALOG = {
224
294
  displayName: "MiMo (Xiaomi)",
225
295
  description: "\u5C0F\u7C73 MiMo \u2014 mimo-v2-pro (Anthropic-compatible API)",
226
296
  models: ["mimo-v2-pro"],
227
- envKey: "MIMO_API_KEY"
297
+ envKey: "MIMO_API_KEY",
298
+ // Fallback env var — MiMo's token plan uses ANTHROPIC_AUTH_TOKEN
299
+ fallbackEnvKeys: ["ANTHROPIC_AUTH_TOKEN"]
228
300
  // baseURL not used — MiMo has a dedicated provider class
229
301
  },
230
302
  gemini: {
@@ -393,8 +465,8 @@ var ClaudeProvider = class {
393
465
  ...systemInstruction ? { system: systemInstruction } : {},
394
466
  messages: [{ role: "user", content: prompt }]
395
467
  });
396
- const block = message.content[0];
397
- if (block.type === "text") return block.text;
468
+ const textBlock = message.content.find((b) => b.type === "text");
469
+ if (textBlock) return textBlock.text;
398
470
  throw new Error("Unexpected response type from Claude API");
399
471
  },
400
472
  { label: `${this.providerName}/${this.modelName}` }
@@ -439,43 +511,29 @@ var OpenAICompatibleProvider = class {
439
511
  }
440
512
  };
441
513
  var MiMoProvider = class {
514
+ client;
442
515
  providerName = "mimo";
443
516
  modelName;
444
- apiKey;
445
- baseUrl = "https://api.xiaomimimo.com/anthropic/v1/messages";
446
517
  constructor(apiKey, modelName = PROVIDER_CATALOG.mimo.models[0]) {
447
- this.apiKey = apiKey;
518
+ const baseURL = process.env["MIMO_BASE_URL"] || process.env["ANTHROPIC_BASE_URL"] || "https://token-plan-cn.xiaomimimo.com/anthropic";
519
+ this.client = new Anthropic({ apiKey, baseURL });
448
520
  this.modelName = modelName;
449
521
  }
450
522
  async generate(prompt, systemInstruction) {
451
523
  return withReliability(
452
524
  async () => {
453
- const body = {
525
+ const stream = this.client.messages.stream({
454
526
  model: this.modelName,
455
- max_tokens: 16384,
456
- messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
457
- top_p: 0.95,
458
- stream: false,
459
- temperature: 1,
460
- stop_sequences: null
461
- };
462
- if (systemInstruction) {
463
- body.system = systemInstruction;
464
- }
465
- const response = await axios.post(this.baseUrl, body, {
466
- headers: {
467
- "api-key": this.apiKey,
468
- "Content-Type": "application/json"
469
- }
527
+ max_tokens: 65536,
528
+ ...systemInstruction ? { system: systemInstruction } : {},
529
+ messages: [{ role: "user", content: prompt }]
470
530
  });
471
- const data = response.data;
472
- const blocks = data?.content ?? [];
473
- const textBlock = blocks.find((b) => b.type === "text");
474
- if (textBlock?.text) return textBlock.text;
475
- if (data?.stop_reason === "max_tokens") {
476
- 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.`);
477
- }
478
- throw new Error(`Unexpected MiMo response: ${JSON.stringify(response.data).slice(0, 200)}`);
531
+ const message = await stream.finalMessage();
532
+ const textBlock = message.content.find((b) => b.type === "text");
533
+ if (textBlock) return textBlock.text;
534
+ const thinkBlock = message.content.find((b) => b.type === "thinking");
535
+ if (thinkBlock) return thinkBlock.thinking;
536
+ return message.content.map((b) => b.text ?? "").join("");
479
537
  },
480
538
  { label: `${this.providerName}/${this.modelName}` }
481
539
  );
@@ -5080,13 +5138,11 @@ function typeLabel(v2) {
5080
5138
 
5081
5139
  // core/token-budget.ts
5082
5140
  import chalk6 from "chalk";
5083
- var CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g;
5084
- function estimateTokens(text) {
5085
- if (!text) return 0;
5086
- const cjkCount = (text.match(CJK_RANGE) ?? []).length;
5087
- const nonCjkLength = text.length - cjkCount;
5088
- return Math.ceil(cjkCount + nonCjkLength / 4);
5089
- }
5141
+
5142
+ // core/config-defaults.ts
5143
+ var DEFAULT_REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
5144
+ var DEFAULT_MAX_CONSTITUTION_CHARS = 4e3;
5145
+ var DEFAULT_MAX_REVIEW_FILE_CHARS = 3e3;
5090
5146
  var DEFAULT_TOKEN_BUDGETS = {
5091
5147
  gemini: 9e5,
5092
5148
  claude: 18e4,
@@ -5094,8 +5150,18 @@ var DEFAULT_TOKEN_BUDGETS = {
5094
5150
  deepseek: 6e4,
5095
5151
  default: 1e5
5096
5152
  };
5153
+
5154
+ // core/token-budget.ts
5155
+ var CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf\u3000-\u303f\uff00-\uffef]/g;
5156
+ function estimateTokens(text) {
5157
+ if (!text) return 0;
5158
+ const cjkCount = (text.match(CJK_RANGE) ?? []).length;
5159
+ const nonCjkLength = text.length - cjkCount;
5160
+ return Math.ceil(cjkCount + nonCjkLength / 4);
5161
+ }
5162
+ var DEFAULT_TOKEN_BUDGETS2 = DEFAULT_TOKEN_BUDGETS;
5097
5163
  function getDefaultBudget(providerName) {
5098
- return DEFAULT_TOKEN_BUDGETS[providerName] ?? DEFAULT_TOKEN_BUDGETS.default;
5164
+ return DEFAULT_TOKEN_BUDGETS2[providerName] ?? DEFAULT_TOKEN_BUDGETS2.default;
5099
5165
  }
5100
5166
 
5101
5167
  // core/dsl-extractor.ts
@@ -6397,6 +6463,7 @@ ${spec}
6397
6463
  ${constitutionSection}
6398
6464
  === ${existingContent ? "Existing content (modify and return the complete file)" : "Create this file from scratch"} ===
6399
6465
  ${existingContent || "Output only the complete file content."}`;
6466
+ const fileSpinner = startSpinner(`${prefix}Generating ${chalk10.bold(item.file)}...`);
6400
6467
  try {
6401
6468
  const raw = await this.provider.generate(codePrompt, systemPrompt);
6402
6469
  const fileContent = stripCodeFences(raw);
@@ -6404,11 +6471,11 @@ ${existingContent || "Output only the complete file content."}`;
6404
6471
  await fs10.ensureDir(path6.dirname(fullPath));
6405
6472
  await fs10.writeFile(fullPath, fileContent, "utf-8");
6406
6473
  getActiveLogger()?.fileWritten(item.file);
6407
- console.log(`${prefix}${existingContent ? chalk10.yellow("~") : chalk10.green("+")} ${chalk10.bold(item.file)} ${chalk10.green("\u2714")}`);
6474
+ fileSpinner.succeed(`${existingContent ? chalk10.yellow("~") : chalk10.green("+")} ${chalk10.bold(item.file)}`);
6408
6475
  successCount++;
6409
6476
  writtenFiles.push(item.file);
6410
6477
  } catch (err) {
6411
- console.log(`${prefix}${chalk10.red("\u2718")} ${chalk10.bold(item.file)} \u2014 ${chalk10.red(err.message)}`);
6478
+ fileSpinner.fail(`${chalk10.bold(item.file)} \u2014 ${err.message}`);
6412
6479
  }
6413
6480
  }
6414
6481
  if (!taskLabel) {
@@ -6555,7 +6622,7 @@ ${context.routeSummary}
6555
6622
  }
6556
6623
  if (context.schema) {
6557
6624
  parts.push(`=== Prisma Schema ===
6558
- ${context.schema.slice(0, 4e3)}
6625
+ ${context.schema.slice(0, DEFAULT_MAX_CONSTITUTION_CHARS)}
6559
6626
  `);
6560
6627
  }
6561
6628
  if (context.errorPatterns) {
@@ -6619,7 +6686,7 @@ async function loadAccumulatedLessons(projectRoot) {
6619
6686
  const nextSection = section.slice(marker.length).match(/\n## \d/);
6620
6687
  return nextSection ? section.slice(0, marker.length + nextSection.index) : section;
6621
6688
  }
6622
- var REVIEW_HISTORY_FILE = ".ai-spec-reviews.json";
6689
+ var REVIEW_HISTORY_FILE = DEFAULT_REVIEW_HISTORY_FILE;
6623
6690
  async function loadReviewHistory(projectRoot) {
6624
6691
  const historyPath = path8.join(projectRoot, REVIEW_HISTORY_FILE);
6625
6692
  try {
@@ -6750,15 +6817,13 @@ ${specContent || "(No spec \u2014 review for general code quality)"}
6750
6817
  ${codeContext}`;
6751
6818
  const archReview = await this.provider.generate(archPrompt, reviewArchitectureSystemPrompt);
6752
6819
  console.log(chalk12.gray(" Pass 2/3: Implementation review..."));
6820
+ const specDigest = specContent && specContent.length > 600 ? specContent.slice(0, 600) + "\n... [spec truncated \u2014 see Pass 0/1 for full text]" : specContent || "(No spec)";
6753
6821
  const history = await loadReviewHistory(this.projectRoot);
6754
6822
  const historyContext = buildHistoryContext(history);
6755
6823
  const implPrompt = `Review the implementation details of this change.
6756
6824
 
6757
- === Feature Spec ===
6758
- ${specContent || "(No spec \u2014 review for general code quality)"}
6759
-
6760
- === Code ===
6761
- ${codeContext}
6825
+ === Feature Spec (digest \u2014 full spec was provided in Pass 0/1) ===
6826
+ ${specDigest}
6762
6827
 
6763
6828
  === Architecture Review (Pass 1 \u2014 do NOT repeat these findings) ===
6764
6829
  ${archReview}
@@ -6767,11 +6832,8 @@ ${historyContext}`;
6767
6832
  console.log(chalk12.gray(" Pass 3/3: Impact & complexity assessment..."));
6768
6833
  const impactPrompt = `Assess the impact and complexity of this change.
6769
6834
 
6770
- === Feature Spec ===
6771
- ${specContent || "(No spec \u2014 review for general code quality)"}
6772
-
6773
- === Code ===
6774
- ${codeContext}
6835
+ === Feature Spec (digest) ===
6836
+ ${specDigest}
6775
6837
 
6776
6838
  === Architecture Review (Pass 1 \u2014 do NOT repeat) ===
6777
6839
  ${archReview}
@@ -6845,8 +6907,8 @@ ${sep}
6845
6907
  filesSection += `
6846
6908
 
6847
6909
  === ${filePath} ===
6848
- ${content.slice(0, 3e3)}`;
6849
- if (content.length > 3e3) filesSection += `
6910
+ ${content.slice(0, DEFAULT_MAX_REVIEW_FILE_CHARS)}`;
6911
+ if (content.length > DEFAULT_MAX_REVIEW_FILE_CHARS) filesSection += `
6850
6912
  ... (truncated, ${content.length} chars total)`;
6851
6913
  } catch {
6852
6914
  filesSection += `