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/.ai-spec-workspace.json +17 -0
- package/.ai-spec.json +7 -0
- package/README.md +33 -17
- package/cli/commands/create.ts +232 -11
- package/cli/commands/init.ts +310 -107
- package/cli/commands/model.ts +7 -11
- package/cli/index.ts +1 -1
- package/cli/pipeline/single-repo.ts +19 -10
- package/cli/utils.ts +72 -4
- package/core/cli-ui.ts +136 -0
- package/core/code-generator.ts +4 -2
- package/core/config-defaults.ts +44 -0
- package/core/constitution-generator.ts +2 -1
- package/core/dsl-extractor.ts +2 -1
- package/core/error-feedback.ts +7 -4
- package/core/openapi-exporter.ts +3 -2
- package/core/provider-utils.ts +8 -7
- package/core/repo-store.ts +95 -0
- package/core/reviewer.ts +14 -13
- package/core/run-logger.ts +3 -4
- package/core/run-snapshot.ts +2 -3
- package/core/run-trend.ts +3 -4
- package/core/spec-generator.ts +27 -42
- package/core/token-budget.ts +3 -8
- package/core/vcr.ts +3 -1
- package/dist/cli/index.js +1042 -533
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/index.mjs +1042 -533
- package/dist/cli/index.mjs.map +1 -1
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +123 -61
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +123 -61
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/RELEASE_LOG.md +0 -2962
- package/purpose.md +0 -1434
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/
|
|
118
|
+
// core/cli-ui.ts
|
|
120
119
|
import chalk from "chalk";
|
|
121
|
-
var
|
|
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
|
|
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
|
|
397
|
-
if (
|
|
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
|
-
|
|
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
|
|
525
|
+
const stream = this.client.messages.stream({
|
|
454
526
|
model: this.modelName,
|
|
455
|
-
max_tokens:
|
|
456
|
-
|
|
457
|
-
|
|
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
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
if (
|
|
476
|
-
|
|
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
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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
|
-
${
|
|
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
|
-
${
|
|
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,
|
|
6849
|
-
if (content.length >
|
|
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 += `
|