@vortex-ai/cli 0.1.45 → 0.1.55

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vortex-ai/cli",
3
- "version": "0.1.45",
3
+ "version": "0.1.55",
4
4
  "description": "Vortex CLI - The main entry point",
5
5
  "main": "./dist/index.js",
6
6
  "bin": {
@@ -12,9 +12,9 @@ cacheCommand
12
12
  const stats = await LLMCacheManager.getStats();
13
13
  console.log("\nLLM Cache Statistics\n");
14
14
  console.log(`Entries: ${stats.entries.toLocaleString()}`);
15
- console.log(`Hits: ${stats.hits.toLocaleString()}`);
15
+ console.log(`Saved API Calls: ${stats.hits.toLocaleString()}`);
16
16
  const storageMB = (stats.storage / 1024 / 1024).toFixed(2);
17
- console.log(`Storage: ${storageMB} MB\n`);
17
+ console.log(`Storage Used: ${storageMB} MB\n`);
18
18
  } catch (err) {
19
19
  console.error("Failed to retrieve cache stats:", err);
20
20
  }
@@ -28,9 +28,11 @@ export async function configSet(provider: string, value: string) {
28
28
  envKey = "GEMINI_API_KEY";
29
29
  } else if (providerUpper === "GROQ") {
30
30
  envKey = "GROQ_API_KEY";
31
+ } else if (providerUpper === "OPENROUTER") {
32
+ envKey = "OPENROUTER_API_KEY";
31
33
  } else {
32
- console.log(chalk.red(`Invalid option: ${chalk.bold(provider)}. Supported options are 'gemini' and 'groq'.`));
33
- console.log(chalk.gray(`Usage: vortex config set gemini <your-key>`));
34
+ console.log(chalk.red(`Invalid option: ${chalk.bold(provider)}. Supported options are 'gemini', 'groq', and 'openrouter'.`));
35
+ console.log(chalk.gray(`Usage: vortex config set openrouter <your-key>`));
34
36
  process.exit(1);
35
37
  }
36
38
 
@@ -1,35 +1,50 @@
1
1
  import { Indexer } from "@vortex/engine";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
2
4
 
3
5
  export async function initCommand(options: any) {
4
6
  const { default: ora } = await import("ora");
5
7
  const { default: chalk } = await import("chalk");
6
8
 
7
- console.log(chalk.blue.bold("\nInitializing Vortex Intelligence Layer\n"));
8
-
9
- if (options.reindex) {
10
- console.log(chalk.yellow(" Reindex mode: Rebuilding embeddings and BM25 index"));
11
- console.log(chalk.yellow(" Historical review memory will be preserved\n"));
12
- }
9
+ console.log(`\n ${chalk.cyan('◆')} Vortex\n`);
13
10
 
11
+ const startTime = Date.now();
14
12
  const spinner = ora("Scanning repository and building indices...").start();
15
13
 
16
14
  const indexer = new Indexer();
17
15
  try {
18
16
  const stats = await indexer.indexRepository(process.cwd());
19
17
 
20
- spinner.succeed(chalk.green("Initialization complete!\n"));
21
-
22
- console.log(chalk.white.bold(" Index Statistics:"));
23
- console.log(chalk.gray(` Files processed: ${stats.filesProcessed}`));
24
- console.log(chalk.gray(` Chunks indexed: ${stats.chunksIndexed}`));
25
- console.log(chalk.cyan(` BM25 documents: ${stats.bm25Documents}`));
26
- console.log(chalk.gray(` Vector store: SQLite (Prisma)`));
27
- console.log(chalk.gray(` BM25 index: .vortex-bm25.json\n`));
18
+ const durationStr = ((Date.now() - startTime) / 1000).toFixed(1) + "s";
19
+ spinner.succeed(chalk.green(`${stats.filesProcessed} files indexed · ${durationStr}\n`));
20
+
21
+ // Automatically update .gitignore
22
+ const gitignorePath = path.join(process.cwd(), ".gitignore");
23
+ if (fs.existsSync(gitignorePath)) {
24
+ let content = fs.readFileSync(gitignorePath, "utf8");
25
+ const entriesToAdd = [
26
+ ".vortex.db",
27
+ ".vortex-bm25.json",
28
+ ".vortex_backup/"
29
+ ];
30
+
31
+ let changed = false;
32
+ for (const entry of entriesToAdd) {
33
+ if (!content.includes(entry)) {
34
+ if (!content.endsWith("\n") && content.length > 0) {
35
+ content += "\n";
36
+ }
37
+ content += entry + "\n";
38
+ changed = true;
39
+ }
40
+ }
41
+
42
+ if (changed) {
43
+ fs.writeFileSync(gitignorePath, content, "utf8");
44
+ console.log(chalk.gray(` Updated .gitignore`));
45
+ }
46
+ }
28
47
 
29
- console.log(chalk.green(" Ready for:"));
30
- console.log(chalk.gray(" • vortex search -q \"your query\" (hybrid search)"));
31
- console.log(chalk.gray(" • vortex review --pr <number> (multi-agent review)"));
32
- console.log(chalk.gray(" • vortex issue --id <number> (issue analysis)\n"));
33
48
  } catch (err) {
34
49
  spinner.fail(chalk.red("Failed to initialize"));
35
50
  console.error(err);
@@ -19,11 +19,8 @@ export async function reviewCommand(options: any) {
19
19
  renderer: new TerminalRenderer() as any,
20
20
  });
21
21
 
22
- if (options.pr) {
23
- console.log(chalk.blue.bold(`\nMulti-Agent Review for PR #${options.pr}\n`));
24
- } else {
25
- console.log(chalk.blue.bold(`\nMulti-Agent Review for Local Changes\n`));
26
- }
22
+ const reviewTitle = options.pr ? `PR #${options.pr}` : 'local';
23
+ console.log(`\n ${chalk.cyan('◆')} Vortex · review · ${reviewTitle}\n`);
27
24
 
28
25
  if (!process.env.GITHUB_TOKEN) {
29
26
  console.log(
@@ -68,28 +65,32 @@ export async function reviewCommand(options: any) {
68
65
  }
69
66
  }
70
67
 
68
+ const diffLines = diff.split('\n').filter(l => l.startsWith('+++') || l.startsWith('---')).length / 2 || 1;
69
+ spinner.succeed(`Fetched diff · ~${Math.floor(diffLines)} files changed`);
71
70
 
72
- spinner.text = "Extracting architectural queries from diff...";
73
71
  const agent = new IntelligenceAgent();
74
72
  const indexer = new Indexer();
75
73
 
74
+ spinner.start("Scanning repository and building indices for fresh context...");
75
+ await indexer.indexRepository(process.cwd());
76
+
77
+ spinner.start("Extracting architectural queries from diff...");
78
+
76
79
  const queries = await agent.extractSearchQueriesFromDiff(diff);
77
80
 
78
81
  const allChunks: any[] = [];
79
82
  if (queries.length > 0) {
80
- spinner.text = `Hybrid searching for: ${queries.join(", ")}...`;
81
- for (const query of queries) {
82
- const results = await indexer.hybridSearch(query, 3);
83
- allChunks.push(...results);
84
- }
83
+ spinner.text = `Hybrid searching for context (parallel)...`;
84
+ const allResults = await Promise.all(
85
+ queries.map(query => indexer.hybridSearch(query, 3))
86
+ );
87
+ allChunks.push(...allResults.flat());
85
88
  }
86
89
 
87
-
88
90
  const uniqueChunks = Array.from(
89
91
  new Map(allChunks.map((c) => [c.id, c])).values()
90
92
  );
91
93
 
92
-
93
94
  spinner.text = "Checking memory for relevant past reviews...";
94
95
  const memoryService = new MemoryService();
95
96
  const memories = await memoryService.recallRelevantMemories(
@@ -97,19 +98,17 @@ export async function reviewCommand(options: any) {
97
98
  3
98
99
  );
99
100
 
100
-
101
- spinner.text = `Running multi-agent review (Security + Architecture + Synthesis)...`;
101
+ spinner.text = `Running multi-agent review (Security, Architecture, Synthesis)...`;
102
102
  const review = await agent.generateMultiAgentReview(
103
103
  diff,
104
104
  uniqueChunks,
105
105
  memories
106
106
  );
107
107
 
108
- spinner.succeed(
109
- chalk.green(
110
- `Review complete in ${(review.durationMs / 1000).toFixed(1)}s!\n`
111
- )
112
- );
108
+ spinner.stop();
109
+ console.log(` ${chalk.green('✔')} Security Agent ${review.agentOutputs.security.findings.length} findings`);
110
+ console.log(` ${chalk.green('✔')} Architecture Agent ${review.agentOutputs.architecture.findings.length} suggestions`);
111
+ console.log(` ${chalk.green('✔')} Synthesizer Report ready\n`);
113
112
 
114
113
 
115
114
  const parsedReview = await marked.parse(review.markdownReport);
@@ -150,37 +149,23 @@ export async function reviewCommand(options: any) {
150
149
  );
151
150
 
152
151
 
153
- const { security, architecture } = review.agentOutputs;
154
-
155
- console.log(
156
- chalk.dim(
157
- `\n Security: ${security.riskLevel} (${security.findings.length} findings)`
158
- )
159
- );
160
- security.findings.forEach((f: any, i: number) => {
161
- const severityColor =
162
- f.severity === "critical" || f.severity === "high"
163
- ? chalk.red
164
- : f.severity === "medium"
165
- ? chalk.yellow
166
- : chalk.gray;
167
- console.log(severityColor(` [${f.severity.toUpperCase()}] ${f.title}`));
168
- });
169
-
170
- console.log(
171
- chalk.dim(
172
- `\n Architecture: ${architecture.consistencyScore} (${architecture.findings.length} findings)`
173
- )
174
- );
175
- architecture.findings.forEach((f: any, i: number) => {
176
- const severityColor =
177
- f.severity === "breaking"
178
- ? chalk.red
179
- : f.severity === "major"
180
- ? chalk.yellow
181
- : chalk.gray;
182
- console.log(severityColor(` [${f.severity.toUpperCase()}] ${f.title}`));
183
- });
152
+ const { security, architecture, synthesis } = review.agentOutputs;
153
+
154
+ const secCount = security.findings.length;
155
+ const archCount = architecture.findings.length;
156
+
157
+ // Draw the summary bar chart
158
+ console.log(` ────────────────────────────────────────`);
159
+
160
+ const secBar = secCount > 0 ? chalk.red('█'.repeat(Math.min(secCount, 10))) : chalk.green('░');
161
+ console.log(` SECURITY ${secBar.padEnd(20)} ${secCount} issues`);
162
+
163
+ const archBar = archCount > 0 ? chalk.yellow('█'.repeat(Math.min(archCount, 10))) : chalk.green('░');
164
+ console.log(` ARCH ${archBar.padEnd(20)} ${archCount} suggestions`);
165
+
166
+ const logicBar = synthesis.verdict === 'SAFE_TO_MERGE' ? chalk.green('░') : chalk.red('█');
167
+ console.log(` LOGIC ${logicBar.padEnd(20)} ${synthesis.verdict.toLowerCase().replace(/_/g, ' ')}`);
168
+ console.log(` ────────────────────────────────────────\n`);
184
169
 
185
170
  if (uniqueChunks.length > 0) {
186
171
  console.log(chalk.cyan.dim("\n Cross-Referenced Architecture:"));
@@ -15,21 +15,33 @@ export async function searchCommand(options: any) {
15
15
  const indexer = new Indexer();
16
16
 
17
17
  try {
18
- spinner.text = "Running hybrid search (vector + BM25 + cross-encoder)...";
19
- const results = await indexer.hybridSearch(options.query, parseInt(options.limit, 10));
18
+ spinner.text = options.expandQuery ? "Expanding query and running parallel hybrid search..." : "Running hybrid search (vector + BM25 + cross-encoder)...";
20
19
 
21
- if (results.length === 0) {
20
+ let queriesToSearch = [options.query];
21
+ const agent = new IntelligenceAgent();
22
+
23
+ if (options.expandQuery) {
24
+ queriesToSearch = await agent.expandQuery(options.query);
25
+ }
26
+
27
+ const allResults = await Promise.all(
28
+ queriesToSearch.map(q => indexer.hybridSearch(q, parseInt(options.limit, 10)))
29
+ );
30
+
31
+ const flatResults = allResults.flat();
32
+ const uniqueResults = Array.from(new Map(flatResults.map((c) => [c.id, c])).values()).sort((a, b) => b.score - a.score).slice(0, parseInt(options.limit, 10));
33
+
34
+ if (uniqueResults.length === 0) {
22
35
  spinner.fail("No relevant code found.");
23
36
  return;
24
37
  }
25
38
 
26
- spinner.text = `Found ${results.length} relevant code chunks. Analyzing with Gemini...`;
39
+ spinner.text = `Found ${uniqueResults.length} relevant code chunks. Analyzing with AI engine...`;
27
40
 
28
41
  const memoryService = new MemoryService();
29
42
  const memories = await memoryService.recallRelevantMemories(options.query, 3);
30
43
 
31
- const agent = new IntelligenceAgent();
32
- const answer = await agent.answerQueryWithContext(options.query, results);
44
+ const answer = await agent.answerQueryWithContext(options.query, uniqueResults);
33
45
 
34
46
  spinner.succeed("Analysis complete!\n");
35
47
 
@@ -47,7 +59,7 @@ export async function searchCommand(options: any) {
47
59
  console.log(formatted);
48
60
 
49
61
  console.log(chalk.cyan.dim(" Reference Material (Hybrid Retrieval)"));
50
- results.forEach((res: any, i: number) => {
62
+ uniqueResults.forEach((res: any, i: number) => {
51
63
  const sources = res.sources ? res.sources.join("+") : "vector";
52
64
  const scoreStr = res.score ? (res.score * 100).toFixed(1) + '%' : 'N/A';
53
65
  console.log(chalk.gray(` │ [${i + 1}] ${res.file.replace(process.cwd(), '')} ➔ ${res.symbolPath || '(anonymous)'} (${scoreStr}) [${sources}]`));
@@ -59,7 +59,8 @@ Please fix this issue in the codebase.`;
59
59
  await solveCommand(prompt, {
60
60
  autoApprove: options.autoApprove,
61
61
  maxSteps: options.maxSteps,
62
- contextChunks: contextChunks
62
+ contextChunks: contextChunks,
63
+ verify: options.verify
63
64
  });
64
65
 
65
66
  } catch (err) {
@@ -8,17 +8,21 @@ import {
8
8
  AutonomousAgent,
9
9
  FileReadTool,
10
10
  FileWriteTool,
11
+ FileEditTool,
11
12
  ShellExecuteTool,
12
13
  GrepTool,
13
14
  RagSearchTool,
15
+ WebSearchTool,
14
16
  ApprovalCallback,
15
- AgentContextChunk
17
+ AgentContextChunk,
18
+ IntelligenceAgent,
19
+ Indexer
16
20
  } from "@vortex/engine";
17
21
  import { initDatabase } from "@vortex/db";
18
22
  import { getGitRoot, isGitRepo } from "@vortex/git";
19
23
  import { VectorStore, LocalEmbedder, BM25Index, HybridRetriever } from "@vortex/retrieval";
20
24
 
21
- export async function solveCommand(prompt: string, options: { autoApprove?: boolean; maxSteps?: number; contextChunks?: AgentContextChunk[]; newProject?: string } = {}) {
25
+ export async function solveCommand(prompt: string, options: { autoApprove?: boolean; maxSteps?: number; contextChunks?: AgentContextChunk[]; newProject?: string; verify?: string | boolean } = {}) {
22
26
  let cwd = process.cwd();
23
27
 
24
28
  if (options.newProject) {
@@ -29,7 +33,7 @@ export async function solveCommand(prompt: string, options: { autoApprove?: bool
29
33
  }
30
34
  cwd = projectPath;
31
35
  process.chdir(cwd);
32
-
36
+
33
37
  if (!fs.existsSync(path.join(cwd, ".git"))) {
34
38
  try {
35
39
  execSync("git init", { cwd, stdio: "ignore" });
@@ -46,12 +50,11 @@ export async function solveCommand(prompt: string, options: { autoApprove?: bool
46
50
  rootPath = getGitRoot(cwd);
47
51
  }
48
52
 
49
- console.log(`\nVortex Autonomous Agent activated`);
50
- console.log(`Task: "${prompt}"\n`);
53
+ console.log(`\n ${chalk.cyan('◆')} Vortex · solve\n`);
51
54
 
52
55
  await initDatabase();
53
56
 
54
- const spinner = ora("Initializing Vector Store for Project Memory...").start();
57
+ const spinner = ora("Reading codebase...").start();
55
58
 
56
59
  try {
57
60
  const vectorStore = new VectorStore();
@@ -59,19 +62,92 @@ export async function solveCommand(prompt: string, options: { autoApprove?: bool
59
62
  const embedder = new LocalEmbedder();
60
63
  const hybridRetriever = new HybridRetriever(vectorStore, bm25Index, embedder);
61
64
  const agent = new AutonomousAgent();
62
-
65
+
63
66
  if (options.maxSteps !== undefined) {
64
67
  (agent as any).maxToolIterations = options.maxSteps;
65
68
  }
66
69
 
67
- spinner.text = "Thinking...";
70
+ if (!options.contextChunks || options.contextChunks.length === 0) {
71
+ try {
72
+ const indexer = new Indexer();
73
+ const relevantContext = await indexer.hybridSearch(prompt, 15);
74
+ options.contextChunks = relevantContext.map((c: any) => ({
75
+ file: c.file,
76
+ symbolPath: c.symbolPath || "anonymous",
77
+ content: c.content,
78
+ kind: c.kind || "unknown",
79
+ }));
80
+ } catch (e) {
81
+ console.warn(chalk.yellow("⚠️ Could not search vector store. Did you run 'vortex init'?"));
82
+ options.contextChunks = [];
83
+ }
84
+ }
85
+
86
+ spinner.text = "Planning approach...";
87
+ const intelligenceAgent = new IntelligenceAgent();
88
+ let executionPlanStr = await intelligenceAgent.generateExecutionPlan(prompt, options.contextChunks || []);
89
+
90
+ let planSummary = "task";
91
+ let stepCount = 0;
92
+ let executionPlan = "";
93
+ let filesToRead: string[] = [];
94
+ try {
95
+ const parsedPlan = JSON.parse(executionPlanStr);
96
+ planSummary = parsedPlan.summary || planSummary;
97
+ stepCount = parsedPlan.steps ? parsedPlan.steps.length : 0;
98
+ executionPlan = parsedPlan.steps ? parsedPlan.steps.map((s: any, i: number) => {
99
+ if (typeof s === 'string') return `${i + 1}. ${s}`;
100
+ return `${i + 1}. [${(s.action || 'MODIFY').toUpperCase()}] ${s.file || 'General'}\n ${s.description}`;
101
+ }).join("\n\n") : executionPlanStr;
102
+ filesToRead = parsedPlan.filesToRead || [];
103
+ } catch {
104
+ executionPlan = executionPlanStr;
105
+ }
106
+
107
+ if (filesToRead.length > 0) {
108
+ for (const file of filesToRead) {
109
+ try {
110
+ const fullPath = path.resolve(rootPath, file);
111
+ if (fs.existsSync(fullPath)) {
112
+ const content = fs.readFileSync(fullPath, "utf-8");
113
+ options.contextChunks!.push({ file, symbolPath: "file", content, kind: "file" });
114
+ }
115
+ } catch { }
116
+ }
117
+ }
118
+
119
+ spinner.stopAndPersist({ symbol: chalk.green('✔'), text: `Plan ready · ${planSummary} (${stepCount} steps)\n` });
120
+
121
+ console.log(`\n${chalk.cyan.dim("─── Execution Plan ───")}`);
122
+ console.log(chalk.gray(executionPlan));
123
+ console.log(`${chalk.cyan.dim("──────────────────────")}\n`);
124
+
125
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
126
+ const answer = await rl.question(` Proceed? (Y/n/edit) › `);
127
+
128
+ if (answer.toLowerCase() === 'n') {
129
+ console.log(chalk.red('\n ✖ Task aborted by user.\n'));
130
+ rl.close();
131
+ process.exit(0);
132
+ } else if (answer.toLowerCase() === 'edit') {
133
+ const edits = await rl.question(` Enter instructions › `);
134
+ if (edits.trim()) {
135
+ executionPlan += `\n\n### User Modifications\n${edits.trim()}`;
136
+ }
137
+ }
138
+ rl.close();
139
+
140
+ const enrichedPrompt = `Original Task:\n${prompt}\n\nExecution Plan generated by Architect:\n${executionPlan}`;
141
+
142
+ console.log(`\n ${chalk.cyan('◆')} Vortex · solving\n`);
143
+ spinner.start("Initializing agent...");
68
144
 
69
145
  const approvalCallback: ApprovalCallback = async (action: string, description: string): Promise<boolean> => {
70
146
  if (options.autoApprove) return true;
71
-
147
+
72
148
  const currentText = spinner.text;
73
149
  spinner.stop();
74
-
150
+
75
151
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
76
152
  let question = "";
77
153
  if (action === "shell_execute") {
@@ -81,10 +157,10 @@ export async function solveCommand(prompt: string, options: { autoApprove?: bool
81
157
  } else {
82
158
  question = `\n${chalk.yellow('⚠️ Agent wants to perform action:')} ${action} on ${description}\nAllow? (y/N) `;
83
159
  }
84
-
160
+
85
161
  const answer = await rl.question(question);
86
162
  rl.close();
87
-
163
+
88
164
  spinner.start(currentText);
89
165
  return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
90
166
  };
@@ -93,26 +169,38 @@ export async function solveCommand(prompt: string, options: { autoApprove?: bool
93
169
  agent.registerTools([
94
170
  new FileReadTool(rootPath),
95
171
  new FileWriteTool(rootPath, vectorStore, embedder, approvalCallback),
172
+ new FileEditTool(rootPath, vectorStore, embedder, approvalCallback),
96
173
  new ShellExecuteTool(rootPath, approvalCallback),
97
174
  new GrepTool(rootPath),
98
175
  new RagSearchTool(hybridRetriever),
176
+ new WebSearchTool(),
99
177
  ]);
100
178
 
179
+ const initialState = {
180
+ evidence: { filesRead: filesToRead, symbolsObserved: [], dependenciesObserved: [], externalSchemasFound: {}, confidence: 'LOW' as any },
181
+ plan: { steps: [], currentStepIndex: 0, completedSteps: [] },
182
+ execution: { filesModified: [], commandsRun: [], lastError: null, consecutiveFailures: 0 },
183
+ verification: { contractItems: [], passed: [], failed: [] },
184
+ verdict: 'IN_PROGRESS' as any
185
+ };
186
+
101
187
  const result = await agent.run(
102
188
  {
103
- diff: prompt,
189
+ diff: enrichedPrompt,
104
190
  contextChunks: options.contextChunks || [],
105
191
  },
106
192
  {
193
+ initialState,
194
+ verifyCommand: options.verify,
107
195
  onToolCall: (toolName, args) => {
108
196
  if (toolName === "write_file") {
109
- spinner.text = `Writing file: ${args.path}...`;
197
+ spinner.text = `Writing ${args.path}...`;
198
+ } else if (toolName === "read_file") {
199
+ spinner.text = `Inspecting ${args.path}...`;
110
200
  } else if (toolName === "shell_execute") {
111
- spinner.text = `Executing shell command...`;
112
- } else if (toolName === "rag_search") {
113
- spinner.text = `Searching project memory for context...`;
201
+ spinner.text = `Running verification...`;
114
202
  } else {
115
- spinner.text = `Agent is using tool: ${toolName}...`;
203
+ spinner.text = `Using ${toolName}...`;
116
204
  }
117
205
  },
118
206
  onToolResult: (toolName, toolResult) => {
@@ -121,15 +209,15 @@ export async function solveCommand(prompt: string, options: { autoApprove?: bool
121
209
  try {
122
210
  const match = toolResult.match(/Wrote to (.*) successfully/);
123
211
  if (match && match[1]) {
124
- const filePath = match[1];
125
- console.log(`\n${chalk.green('✓ File updated:')} ${filePath}`);
126
- const diffOut = execSync(`git diff --unified=1 ${filePath}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore']});
127
- if (diffOut) {
128
- console.log(chalk.gray(diffOut));
129
- }
212
+ const filePath = match[1];
213
+ const diffOut = execSync(`git diff --numstat ${filePath}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
214
+ const diffParts = diffOut.split(/\s+/);
215
+ const additions = diffParts[0] || '0';
216
+ const deletions = diffParts[1] || '0';
217
+ console.log(` ${chalk.cyan('●')} Writing ${filePath}`);
218
+ console.log(` ${chalk.green(`+ ${additions} lines`)} · ${chalk.red(`- ${deletions} lines`)}\n`);
130
219
  }
131
220
  } catch (e) {
132
-
133
221
  }
134
222
  spinner.start("Thinking...");
135
223
  }
@@ -137,38 +225,53 @@ export async function solveCommand(prompt: string, options: { autoApprove?: bool
137
225
  }
138
226
  );
139
227
 
140
- spinner.succeed("Task completed");
228
+ let iterationCount = 1;
229
+ if (result.summary) {
230
+ const iterMatch = result.summary.match(/Iter (\d+)/);
231
+ if (iterMatch) iterationCount = parseInt(iterMatch[1], 10);
232
+ }
141
233
 
234
+ let filesChangedCount = 0;
142
235
  try {
143
- const statOut = execSync(`git diff --stat`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
144
- if (statOut.trim()) {
145
- console.log(`\n${chalk.cyan('=== Changed Files Summary ===')}`);
146
- console.log(statOut);
236
+ const statOut = execSync(`git diff --shortstat`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
237
+ const filesMatch = statOut.match(/(\d+) file/);
238
+ if (filesMatch) {
239
+ filesChangedCount = parseInt(filesMatch[1], 10);
147
240
  }
148
241
  } catch (e) {}
149
242
 
150
- console.log(`\n${chalk.cyan('=== Final Output ===')}\n`);
151
- console.log(result.summary);
243
+ spinner.stop();
244
+ const isIncomplete = result.verdict === 'INCOMPLETE' || result.state?.verdict === 'INCOMPLETE';
245
+ const isComplete = !isIncomplete;
152
246
 
247
+ if (isIncomplete) {
248
+ console.log(`\n ${chalk.red('✖')} Task incomplete · stopped or failed`);
249
+ } else {
250
+ console.log(`\n ────────────────────────────────────────`);
251
+ console.log(` ${chalk.green('✔')} Task complete · ${filesChangedCount} files modified · ${iterationCount} iterations`);
252
+ console.log(` ────────────────────────────────────────\n`);
253
+ }
153
254
 
154
- const packageJsonPath = path.join(cwd, "package.json");
155
- if (fs.existsSync(packageJsonPath)) {
156
- try {
157
- const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
158
- if (pkg.scripts && pkg.scripts.build) {
159
- console.log(`\n${chalk.cyan('=== Verification ===')}`);
160
- const buildSpinner = ora("Running build command...").start();
161
- try {
162
- const env = { ...process.env };
163
- delete env.NODE_ENV;
164
- execSync("npm run build", { stdio: "pipe", cwd, env });
165
- buildSpinner.succeed("Build completed successfully.");
166
- } catch (e: any) {
167
- buildSpinner.fail("Build failed.");
168
- console.error(chalk.red(e.stdout ? e.stdout.toString() : e.message));
255
+ if (isComplete) {
256
+ const packageJsonPath = path.join(cwd, "package.json");
257
+ if (fs.existsSync(packageJsonPath)) {
258
+ try {
259
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
260
+ if (pkg.scripts && pkg.scripts.build) {
261
+ console.log(`\n${chalk.cyan('=== Verification ===')}`);
262
+ const buildSpinner = ora("Running build command...").start();
263
+ try {
264
+ const env = { ...process.env };
265
+ delete env.NODE_ENV;
266
+ execSync("npm run build", { stdio: "pipe", cwd, env });
267
+ buildSpinner.succeed("Build completed successfully.");
268
+ } catch (e: any) {
269
+ buildSpinner.fail("Build failed.");
270
+ console.error(chalk.red(e.stdout ? e.stdout.toString() : e.message));
271
+ }
169
272
  }
170
- }
171
- } catch (e) {}
273
+ } catch (e) { }
274
+ }
172
275
  }
173
276
  } catch (err: any) {
174
277
  spinner.fail("Agent encountered an error");