markov-cli 1.0.15 → 1.0.17
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/.env.example +2 -0
- package/package.json +1 -1
- package/src/claude.js +27 -0
- package/src/editor/codeBlockEdits.js +1 -1
- package/src/input.js +39 -13
- package/src/interactive.js +242 -190
- package/src/ollama.js +42 -6
- package/src/tools.js +30 -1
- package/src/ui/logo.js +9 -17
- package/src/ui/prompts.js +57 -9
- package/src/ui/spinner.js +91 -5
package/src/interactive.js
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
|
|
3
|
-
import gradient from 'gradient-string';
|
|
4
3
|
import { writeFileSync, readFileSync, existsSync } from 'fs';
|
|
5
4
|
import { homedir } from 'os';
|
|
6
5
|
import { resolve } from 'path';
|
|
7
6
|
import { printLogo } from './ui/logo.js';
|
|
8
|
-
import { chatWithTools, streamChat, streamChatWithTools, MODEL,
|
|
7
|
+
import { chatWithTools, streamChat, streamChatWithTools, MODEL, getModelOptions, setModelAndProvider, getModelDisplayName } from './ollama.js';
|
|
9
8
|
import { resolveFileRefs } from './files.js';
|
|
10
|
-
import { RUN_TERMINAL_COMMAND_TOOL, WEB_SEARCH_TOOL, runTool, execCommand } from './tools.js';
|
|
9
|
+
import { RUN_TERMINAL_COMMAND_TOOL, WEB_SEARCH_TOOL, runTool, execCommand, spawnCommand } from './tools.js';
|
|
11
10
|
import { chatPrompt } from './input.js';
|
|
12
11
|
import { getFilesAndDirs } from './ui/picker.js';
|
|
13
12
|
import { getToken, login, clearToken, getClaudeKey, setClaudeKey, getOpenAIKey, setOpenAIKey, getOllamaKey, setOllamaKey } from './auth.js';
|
|
14
13
|
|
|
15
14
|
// Extracted UI modules
|
|
16
|
-
import { selectFrom, confirm, promptLine, promptSecret } from './ui/prompts.js';
|
|
15
|
+
import { selectFrom, confirm, promptLine, promptSecret, PROMPT_INTERRUPT } from './ui/prompts.js';
|
|
17
16
|
import { formatResponseWithCodeBlocks, formatFileEditPreview, formatToolCallSummary, formatToolResultSummary, printTokenUsage } from './ui/formatting.js';
|
|
18
|
-
import {
|
|
17
|
+
import { createIdleSpinner } from './ui/spinner.js';
|
|
19
18
|
|
|
20
19
|
// Extracted agent modules
|
|
21
20
|
import { buildPlanSystemMessage, buildAgentSystemMessage, buildInitSystemMessage, getLsContext, getGrepContext } from './agent/context.js';
|
|
@@ -27,8 +26,6 @@ import { applyCodeBlockEdits } from './editor/codeBlockEdits.js';
|
|
|
27
26
|
// Extracted command modules
|
|
28
27
|
import { runSetupSteps, NEXTJS_STEPS, TANSTACK_STEPS, LARAVEL_STEPS, LARAVEL_BLOG_PROMPT } from './commands/setup.js';
|
|
29
28
|
|
|
30
|
-
const agentGradient = gradient(['#22c55e', '#16a34a', '#4ade80']);
|
|
31
|
-
|
|
32
29
|
const PLAN_FILE = 'plan.md';
|
|
33
30
|
const getPlanPath = () => resolve(process.cwd(), PLAN_FILE);
|
|
34
31
|
|
|
@@ -44,35 +41,30 @@ const INTRO_TEXT =
|
|
|
44
41
|
chalk.bold('Quick start:\n') +
|
|
45
42
|
chalk.cyan(' /help') + chalk.dim(' show all commands\n') +
|
|
46
43
|
chalk.cyan(' /login') + chalk.dim(' authenticate with email & password\n') +
|
|
47
|
-
chalk.cyan(' /
|
|
44
|
+
chalk.cyan(' /cmd') + chalk.dim(' [command] run a shell command in the current folder (default)\n') +
|
|
45
|
+
chalk.cyan(' /agent') + chalk.dim('[prompt] run an agent with the current folder context\n') +
|
|
48
46
|
chalk.cyan(' /init') + chalk.dim(' [prompt] create markov.md with project summary\n') +
|
|
49
47
|
chalk.cyan(' /plan') + chalk.dim(' [prompt] stream a plan and save to plan.md\n') +
|
|
50
48
|
chalk.cyan(' /build') + chalk.dim(' execute plan from plan.md\n') +
|
|
51
|
-
chalk.cyan(' /yolo') + chalk.dim(' [prompt] plan in stream mode, then auto-run until done\n') +
|
|
52
|
-
chalk.dim('
|
|
49
|
+
chalk.cyan(' /yolo') + chalk.dim(' [prompt] plan in stream mode, then auto-run until done\n\n') +
|
|
50
|
+
chalk.dim(' Tips: Use ') + chalk.cyan('@filename') + chalk.dim(' to add file to context\n') +
|
|
51
|
+
chalk.dim(' Press ') + chalk.cyan('CTRL + TAB') + chalk.dim(' to switch mode\n');
|
|
53
52
|
|
|
54
53
|
const HELP_TEXT =
|
|
54
|
+
INTRO_TEXT +
|
|
55
55
|
'\n' +
|
|
56
|
-
chalk.bold('
|
|
56
|
+
chalk.bold('More commands:\n') +
|
|
57
57
|
chalk.cyan(' /intro') + chalk.dim(' show quick start (same as on first load)\n') +
|
|
58
|
-
chalk.cyan(' /help') + chalk.dim(' show this help\n') +
|
|
59
58
|
chalk.cyan(' /setup-nextjs') + chalk.dim(' scaffold a Next.js app\n') +
|
|
60
59
|
chalk.cyan(' /setup-tanstack') + chalk.dim(' scaffold a TanStack Start app\n') +
|
|
61
60
|
chalk.cyan(' /setup-laravel') + chalk.dim(' scaffold a Laravel API\n') +
|
|
62
61
|
chalk.cyan(' /laravel') + chalk.dim(' set up Laravel "my-blog" with blog route (agent)\n') +
|
|
63
62
|
chalk.cyan(' /models') + chalk.dim(' switch the active AI model\n') +
|
|
64
63
|
chalk.cyan(' /cd [path]') + chalk.dim(' change working directory\n') +
|
|
65
|
-
chalk.cyan(' /cmd [command]') + chalk.dim(' run a shell command in the current folder\n') +
|
|
66
|
-
chalk.cyan(' /login') + chalk.dim(' authenticate with email & password\n') +
|
|
67
64
|
chalk.cyan(' /logout') + chalk.dim(' clear saved auth token\n') +
|
|
68
65
|
chalk.cyan(' /clear') + chalk.dim(' clear chat history and stored plan\n') +
|
|
69
66
|
chalk.cyan(' /env') + chalk.dim(' show which .env vars are loaded (for debugging)\n') +
|
|
70
|
-
chalk.cyan(' /debug') + chalk.dim(' toggle full payload dump (env MARKOV_DEBUG)\n')
|
|
71
|
-
chalk.cyan(' /init') + chalk.dim(' [prompt] create markov.md with project summary\n') +
|
|
72
|
-
chalk.cyan(' /plan') + chalk.dim(' [prompt] stream a plan and save to plan.md\n') +
|
|
73
|
-
chalk.cyan(' /build') + chalk.dim(' execute plan from plan.md\n') +
|
|
74
|
-
chalk.cyan(' /yolo') + chalk.dim(' [prompt] plan in stream mode, then auto-run until done\n') +
|
|
75
|
-
chalk.dim('\nType a message · ') + chalk.cyan('@filename') + chalk.dim(' to attach · ctrl+q to cancel\n');
|
|
67
|
+
chalk.cyan(' /debug') + chalk.dim(' toggle full payload dump (env MARKOV_DEBUG)\n');
|
|
76
68
|
|
|
77
69
|
/** If MARKOV_DEBUG is set, print the raw model output after completion. */
|
|
78
70
|
function maybePrintRawModelOutput(rawText) {
|
|
@@ -89,8 +81,6 @@ export async function startInteractive() {
|
|
|
89
81
|
|
|
90
82
|
let allFiles = getFilesAndDirs();
|
|
91
83
|
const chatMessages = [];
|
|
92
|
-
|
|
93
|
-
console.log(chalk.dim(`Chat with Markov (${getModelDisplayName()}).`));
|
|
94
84
|
console.log(INTRO_TEXT);
|
|
95
85
|
|
|
96
86
|
if (!getToken()) {
|
|
@@ -100,6 +90,77 @@ export async function startInteractive() {
|
|
|
100
90
|
let pendingMessage = null;
|
|
101
91
|
let lastPlan = null;
|
|
102
92
|
const inputHistory = [];
|
|
93
|
+
const availableModes = ['/cmd', '/agent', '/plan', '/build', '/yolo'];
|
|
94
|
+
let currentMode = '/cmd';
|
|
95
|
+
let interruptArmed = false;
|
|
96
|
+
let activeInterruptHandler = null;
|
|
97
|
+
|
|
98
|
+
const isInterrupted = (value) => value === PROMPT_INTERRUPT || (typeof value === 'object' && value !== null && value.type === 'interrupt');
|
|
99
|
+
const resetInterruptState = () => {
|
|
100
|
+
interruptArmed = false;
|
|
101
|
+
};
|
|
102
|
+
const exitInteractive = () => {
|
|
103
|
+
process.stdout.write('\n');
|
|
104
|
+
process.exit(0);
|
|
105
|
+
};
|
|
106
|
+
const registerInterruptHandler = (handler) => {
|
|
107
|
+
activeInterruptHandler = handler;
|
|
108
|
+
return () => {
|
|
109
|
+
if (activeInterruptHandler === handler) activeInterruptHandler = null;
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
const handleInterrupt = ({ alreadyPrinted = false } = {}) => {
|
|
113
|
+
if (activeInterruptHandler) {
|
|
114
|
+
const handler = activeInterruptHandler;
|
|
115
|
+
activeInterruptHandler = null;
|
|
116
|
+
handler();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (interruptArmed) exitInteractive();
|
|
120
|
+
interruptArmed = true;
|
|
121
|
+
if (!alreadyPrinted) console.log(chalk.dim('\n(interrupted)\n'));
|
|
122
|
+
};
|
|
123
|
+
const registerAbortController = (abortController, onAbort) => registerInterruptHandler(() => {
|
|
124
|
+
onAbort?.();
|
|
125
|
+
if (!abortController.signal.aborted) abortController.abort();
|
|
126
|
+
console.log(chalk.dim('\n(interrupted)\n'));
|
|
127
|
+
});
|
|
128
|
+
const askLine = async (label) => {
|
|
129
|
+
const value = await promptLine(label);
|
|
130
|
+
if (isInterrupted(value)) {
|
|
131
|
+
handleInterrupt({ alreadyPrinted: true });
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return value;
|
|
135
|
+
};
|
|
136
|
+
const askSecret = async (label) => {
|
|
137
|
+
const value = await promptSecret(label);
|
|
138
|
+
if (isInterrupted(value)) {
|
|
139
|
+
handleInterrupt({ alreadyPrinted: true });
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return value;
|
|
143
|
+
};
|
|
144
|
+
const askSelect = async (options, label) => {
|
|
145
|
+
const value = await selectFrom(options, label);
|
|
146
|
+
if (isInterrupted(value)) {
|
|
147
|
+
handleInterrupt();
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
return value;
|
|
151
|
+
};
|
|
152
|
+
const askConfirm = async (question) => {
|
|
153
|
+
const value = await confirm(question);
|
|
154
|
+
if (isInterrupted(value)) {
|
|
155
|
+
handleInterrupt({ alreadyPrinted: true });
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
return value;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
process.on('SIGINT', () => {
|
|
162
|
+
handleInterrupt();
|
|
163
|
+
});
|
|
103
164
|
|
|
104
165
|
while (true) {
|
|
105
166
|
let raw;
|
|
@@ -108,17 +169,38 @@ export async function startInteractive() {
|
|
|
108
169
|
pendingMessage = null;
|
|
109
170
|
console.log(chalk.magenta('you> ') + raw + '\n');
|
|
110
171
|
} else {
|
|
111
|
-
|
|
172
|
+
const result = await chatPrompt(chalk.magenta('you> '), allFiles, inputHistory, availableModes, currentMode);
|
|
173
|
+
if (isInterrupted(result)) {
|
|
174
|
+
handleInterrupt({ alreadyPrinted: true });
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (result === null) continue;
|
|
178
|
+
resetInterruptState();
|
|
179
|
+
if (typeof result === 'object' && result !== null) {
|
|
180
|
+
raw = result.text;
|
|
181
|
+
currentMode = result.mode;
|
|
182
|
+
} else {
|
|
183
|
+
raw = result;
|
|
184
|
+
}
|
|
112
185
|
}
|
|
113
186
|
if (raw === null) continue;
|
|
114
|
-
|
|
187
|
+
let trimmed = raw.trim();
|
|
188
|
+
|
|
189
|
+
if (!trimmed) {
|
|
190
|
+
if (currentMode === '/build') trimmed = '/build';
|
|
191
|
+
else continue;
|
|
192
|
+
} else if (!trimmed.startsWith('/') && currentMode !== '/agent') {
|
|
193
|
+
trimmed = `${currentMode} ${trimmed}`;
|
|
194
|
+
}
|
|
115
195
|
|
|
116
196
|
if (!trimmed) continue;
|
|
117
197
|
|
|
118
198
|
// /login — authenticate and save token
|
|
119
199
|
if (trimmed === '/login') {
|
|
120
|
-
const email
|
|
121
|
-
|
|
200
|
+
const email = await askLine('Email: ');
|
|
201
|
+
if (email === null) continue;
|
|
202
|
+
const password = await askSecret('Password: ');
|
|
203
|
+
if (password === null) continue;
|
|
122
204
|
try {
|
|
123
205
|
await login(email, password);
|
|
124
206
|
console.log(chalk.green('✓ logged in\n'));
|
|
@@ -190,9 +272,14 @@ export async function startInteractive() {
|
|
|
190
272
|
|
|
191
273
|
// /plan [prompt] — stream a plan (no tools), store as lastPlan
|
|
192
274
|
if (trimmed === '/plan' || trimmed.startsWith('/plan ')) {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
275
|
+
let rawUserContent;
|
|
276
|
+
if (trimmed.startsWith('/plan ')) {
|
|
277
|
+
rawUserContent = trimmed.slice(6).trim();
|
|
278
|
+
} else {
|
|
279
|
+
const prompted = await askLine(chalk.bold('What do you want to plan? '));
|
|
280
|
+
if (prompted === null) continue;
|
|
281
|
+
rawUserContent = prompted.trim();
|
|
282
|
+
}
|
|
196
283
|
if (!rawUserContent) {
|
|
197
284
|
console.log(chalk.yellow('No prompt given.\n'));
|
|
198
285
|
continue;
|
|
@@ -202,24 +289,11 @@ export async function startInteractive() {
|
|
|
202
289
|
chatMessages.push({ role: 'user', content: userContent });
|
|
203
290
|
const planMessages = [buildPlanSystemMessage(), ...chatMessages];
|
|
204
291
|
const planAbort = new AbortController();
|
|
205
|
-
process.stdout.write(chalk.dim('\nPlan › '));
|
|
206
|
-
const DOTS = ['.', '..', '...'];
|
|
207
|
-
let dotIdx = 0;
|
|
208
292
|
const planStartTime = Date.now();
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
dotIdx++;
|
|
213
|
-
}, 400);
|
|
293
|
+
const planSpinner = createIdleSpinner('Squirming ', { startTime: planStartTime });
|
|
294
|
+
const clearInterruptHandler = registerAbortController(planAbort, () => planSpinner.stop());
|
|
295
|
+
planSpinner.bump();
|
|
214
296
|
let thinkingStarted = false;
|
|
215
|
-
let firstContent = true;
|
|
216
|
-
const clearPlanSpinner = () => {
|
|
217
|
-
if (planSpinner) {
|
|
218
|
-
clearInterval(planSpinner);
|
|
219
|
-
planSpinner = null;
|
|
220
|
-
process.stdout.write('\r\x1b[0J');
|
|
221
|
-
}
|
|
222
|
-
};
|
|
223
297
|
try {
|
|
224
298
|
let currentPlanMessages = planMessages;
|
|
225
299
|
let fullPlanText = '';
|
|
@@ -232,15 +306,12 @@ export async function startInteractive() {
|
|
|
232
306
|
{
|
|
233
307
|
think: true, // plan mode only: request thinking from backend
|
|
234
308
|
onContent: (token) => {
|
|
235
|
-
|
|
236
|
-
clearPlanSpinner();
|
|
237
|
-
firstContent = false;
|
|
238
|
-
}
|
|
309
|
+
planSpinner.bump();
|
|
239
310
|
process.stdout.write(token);
|
|
240
311
|
},
|
|
241
312
|
onThinking: (token) => {
|
|
313
|
+
planSpinner.bump();
|
|
242
314
|
if (!thinkingStarted) {
|
|
243
|
-
clearPlanSpinner();
|
|
244
315
|
process.stdout.write(chalk.dim('Thinking: '));
|
|
245
316
|
thinkingStarted = true;
|
|
246
317
|
}
|
|
@@ -267,6 +338,7 @@ export async function startInteractive() {
|
|
|
267
338
|
continue;
|
|
268
339
|
}
|
|
269
340
|
}
|
|
341
|
+
planSpinner.pause();
|
|
270
342
|
console.log(chalk.cyan('\n ▶ ') + chalk.bold(name) + chalk.dim(' ') + (name === 'web_search' ? (args?.query ?? '') : (args?.command ?? '')));
|
|
271
343
|
const result = await runTool(name, args ?? {}, { cwd: process.cwd(), confirmFn: () => Promise.resolve(true) });
|
|
272
344
|
currentPlanMessages.push({
|
|
@@ -277,6 +349,7 @@ export async function startInteractive() {
|
|
|
277
349
|
}
|
|
278
350
|
}
|
|
279
351
|
chatMessages.push({ role: 'assistant', content: fullPlanText });
|
|
352
|
+
planSpinner.stop();
|
|
280
353
|
// Store only plan (stream content), not thinking; write to plan.md for /build.
|
|
281
354
|
if ((fullPlanText ?? '').trim()) {
|
|
282
355
|
lastPlan = fullPlanText;
|
|
@@ -285,21 +358,24 @@ export async function startInteractive() {
|
|
|
285
358
|
console.log('\n' + chalk.dim('Plan saved to plan.md. Use ') + chalk.green('/build') + chalk.dim(' to execute.\n'));
|
|
286
359
|
maybePrintRawModelOutput(fullPlanText);
|
|
287
360
|
} catch (err) {
|
|
288
|
-
|
|
289
|
-
clearInterval(planSpinner);
|
|
290
|
-
planSpinner = null;
|
|
291
|
-
process.stdout.write('\r\x1b[0J');
|
|
292
|
-
}
|
|
361
|
+
planSpinner.stop();
|
|
293
362
|
if (!planAbort.signal?.aborted) console.log(chalk.red(`\n${err.message}\n`));
|
|
363
|
+
} finally {
|
|
364
|
+
clearInterruptHandler();
|
|
294
365
|
}
|
|
295
366
|
continue;
|
|
296
367
|
}
|
|
297
368
|
|
|
298
369
|
// /yolo [prompt] — one plan in stream mode, then auto-accept and run agent until done
|
|
299
370
|
if (trimmed === '/yolo' || trimmed.startsWith('/yolo ')) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
371
|
+
let rawUserContent;
|
|
372
|
+
if (trimmed.startsWith('/yolo ')) {
|
|
373
|
+
rawUserContent = trimmed.slice(6).trim();
|
|
374
|
+
} else {
|
|
375
|
+
const prompted = await askLine(chalk.bold('What do you want to yolo? '));
|
|
376
|
+
if (prompted === null) continue;
|
|
377
|
+
rawUserContent = prompted.trim();
|
|
378
|
+
}
|
|
303
379
|
if (!rawUserContent) {
|
|
304
380
|
console.log(chalk.yellow('No prompt given.\n'));
|
|
305
381
|
continue;
|
|
@@ -309,9 +385,11 @@ export async function startInteractive() {
|
|
|
309
385
|
chatMessages.push({ role: 'user', content: planUserContent });
|
|
310
386
|
const planMessages = [buildPlanSystemMessage(), ...chatMessages];
|
|
311
387
|
const yoloAbort = new AbortController();
|
|
312
|
-
process.stdout.write(chalk.dim('\nYolo › Plan › '));
|
|
313
388
|
let thinkingStarted = false;
|
|
314
389
|
let fullPlanText = '';
|
|
390
|
+
const yoloPlanSpinner = createIdleSpinner('Squirming ');
|
|
391
|
+
const clearYoloPlanInterruptHandler = registerAbortController(yoloAbort, () => yoloPlanSpinner.stop());
|
|
392
|
+
yoloPlanSpinner.bump();
|
|
315
393
|
try {
|
|
316
394
|
let currentPlanMessages = planMessages;
|
|
317
395
|
const yoloPlanMaxIter = 10;
|
|
@@ -322,8 +400,12 @@ export async function startInteractive() {
|
|
|
322
400
|
MODEL,
|
|
323
401
|
{
|
|
324
402
|
think: true, // plan phase: request thinking from backend
|
|
325
|
-
onContent: (token) =>
|
|
403
|
+
onContent: (token) => {
|
|
404
|
+
yoloPlanSpinner.bump();
|
|
405
|
+
process.stdout.write(token);
|
|
406
|
+
},
|
|
326
407
|
onThinking: (token) => {
|
|
408
|
+
yoloPlanSpinner.bump();
|
|
327
409
|
if (!thinkingStarted) {
|
|
328
410
|
process.stdout.write(chalk.dim('Thinking: '));
|
|
329
411
|
thinkingStarted = true;
|
|
@@ -351,6 +433,7 @@ export async function startInteractive() {
|
|
|
351
433
|
continue;
|
|
352
434
|
}
|
|
353
435
|
}
|
|
436
|
+
yoloPlanSpinner.pause();
|
|
354
437
|
console.log(chalk.cyan('\n ▶ ') + chalk.bold(name) + chalk.dim(' ') + (name === 'web_search' ? (args?.query ?? '') : (args?.command ?? '')));
|
|
355
438
|
const result = await runTool(name, args ?? {}, { cwd: process.cwd(), confirmFn: () => Promise.resolve(true) });
|
|
356
439
|
currentPlanMessages.push({
|
|
@@ -361,12 +444,16 @@ export async function startInteractive() {
|
|
|
361
444
|
}
|
|
362
445
|
}
|
|
363
446
|
chatMessages.push({ role: 'assistant', content: fullPlanText });
|
|
447
|
+
yoloPlanSpinner.stop();
|
|
364
448
|
// Store only plan (stream content), not thinking, so build phase uses exactly this.
|
|
365
449
|
if ((fullPlanText ?? '').trim()) lastPlan = fullPlanText;
|
|
366
450
|
maybePrintRawModelOutput(fullPlanText);
|
|
367
451
|
} catch (err) {
|
|
452
|
+
yoloPlanSpinner.stop();
|
|
368
453
|
if (!yoloAbort.signal?.aborted) console.log(chalk.red(`\n${err.message}\n`));
|
|
369
454
|
continue;
|
|
455
|
+
} finally {
|
|
456
|
+
clearYoloPlanInterruptHandler();
|
|
370
457
|
}
|
|
371
458
|
const buildContent = (await getLsContext()) + (await getGrepContext()) +
|
|
372
459
|
'\n\nPlan:\n' + lastPlan + '\n\nExecute this plan using your tools. Run commands and edit files as needed.';
|
|
@@ -377,31 +464,16 @@ export async function startInteractive() {
|
|
|
377
464
|
const confirmFn = () => Promise.resolve(true);
|
|
378
465
|
const confirmFileEdit = async () => true;
|
|
379
466
|
const startTime = Date.now();
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
let spinner = null;
|
|
383
|
-
const startSpinner = () => {
|
|
384
|
-
if (spinner) { clearInterval(spinner); spinner = null; }
|
|
385
|
-
dotIdx = 0;
|
|
386
|
-
process.stdout.write(chalk.dim('\nYolo › Run › '));
|
|
387
|
-
spinner = setInterval(() => {
|
|
388
|
-
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
389
|
-
process.stdout.write('\r' + chalk.dim('Yolo › Run › ') + chalk.dim(elapsed + 's ') + agentGradient(DOTS[dotIdx % DOTS.length]) + ' ');
|
|
390
|
-
dotIdx++;
|
|
391
|
-
}, 400);
|
|
392
|
-
};
|
|
393
|
-
const stopSpinner = () => {
|
|
394
|
-
if (spinner) { clearInterval(spinner); spinner = null; }
|
|
395
|
-
process.stdout.write('\r\x1b[0J');
|
|
396
|
-
};
|
|
467
|
+
const idleSpinner = createIdleSpinner('Squirming ', { startTime });
|
|
468
|
+
const clearYoloBuildInterruptHandler = registerAbortController(abortController, () => idleSpinner.stop());
|
|
397
469
|
try {
|
|
398
470
|
const result = await runAgentLoop(agentMessages, {
|
|
399
471
|
signal: abortController.signal,
|
|
400
472
|
cwd: process.cwd(),
|
|
401
473
|
confirmFn,
|
|
402
474
|
confirmFileEdit,
|
|
403
|
-
onThinking: () => {
|
|
404
|
-
onBeforeToolRun: () => {
|
|
475
|
+
onThinking: () => { idleSpinner.bump(); },
|
|
476
|
+
onBeforeToolRun: () => { idleSpinner.pause(); },
|
|
405
477
|
onIteration: (iter, max, toolCount) => {
|
|
406
478
|
const w = process.stdout.columns || 80;
|
|
407
479
|
const label = ` Step ${iter} `;
|
|
@@ -416,7 +488,7 @@ export async function startInteractive() {
|
|
|
416
488
|
console.log(chalk.dim(' ') + formatToolResultSummary(name, resultStr));
|
|
417
489
|
},
|
|
418
490
|
});
|
|
419
|
-
|
|
491
|
+
idleSpinner.stop();
|
|
420
492
|
if (result) {
|
|
421
493
|
chatMessages.push(result.finalMessage);
|
|
422
494
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
@@ -428,39 +500,33 @@ export async function startInteractive() {
|
|
|
428
500
|
maybePrintRawModelOutput(result.content);
|
|
429
501
|
}
|
|
430
502
|
} catch (err) {
|
|
431
|
-
|
|
503
|
+
idleSpinner.stop();
|
|
432
504
|
if (!abortController.signal?.aborted) console.log(chalk.red(`\n${err.message}\n`));
|
|
505
|
+
} finally {
|
|
506
|
+
clearYoloBuildInterruptHandler();
|
|
433
507
|
}
|
|
434
508
|
continue;
|
|
435
509
|
}
|
|
436
510
|
|
|
437
511
|
// /init [prompt] — create markov.md with project summary (agent writes file via tools)
|
|
438
512
|
if (trimmed === '/init' || trimmed.startsWith('/init ')) {
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
513
|
+
let rawUserContent;
|
|
514
|
+
if (trimmed.startsWith('/init ')) {
|
|
515
|
+
rawUserContent = trimmed.slice(6).trim();
|
|
516
|
+
} else {
|
|
517
|
+
const prompted = await askLine(chalk.bold('Describe the project to summarize (optional): '));
|
|
518
|
+
if (prompted === null) continue;
|
|
519
|
+
rawUserContent = prompted.trim();
|
|
520
|
+
}
|
|
442
521
|
const userContent = (await getLsContext()) + (await getGrepContext()) +
|
|
443
522
|
(rawUserContent ? `Create markov.md with a project summary. Focus on: ${rawUserContent}` : 'Create markov.md with a concise project summary.');
|
|
444
523
|
const initMessages = [buildInitSystemMessage(), { role: 'user', content: userContent }];
|
|
445
524
|
const initAbort = new AbortController();
|
|
446
|
-
process.stdout.write(chalk.dim('\nInit › '));
|
|
447
|
-
const DOTS = ['.', '..', '...'];
|
|
448
|
-
let dotIdx = 0;
|
|
449
525
|
const initStartTime = Date.now();
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
dotIdx++;
|
|
454
|
-
}, 400);
|
|
526
|
+
const initSpinner = createIdleSpinner('Squirming ', { startTime: initStartTime });
|
|
527
|
+
const clearInitInterruptHandler = registerAbortController(initAbort, () => initSpinner.stop());
|
|
528
|
+
initSpinner.bump();
|
|
455
529
|
let thinkingStarted = false;
|
|
456
|
-
let firstContent = true;
|
|
457
|
-
const clearInitSpinner = () => {
|
|
458
|
-
if (initSpinner) {
|
|
459
|
-
clearInterval(initSpinner);
|
|
460
|
-
initSpinner = null;
|
|
461
|
-
process.stdout.write('\r\x1b[0J');
|
|
462
|
-
}
|
|
463
|
-
};
|
|
464
530
|
try {
|
|
465
531
|
let currentInitMessages = initMessages;
|
|
466
532
|
const initMaxIter = 10;
|
|
@@ -472,15 +538,12 @@ export async function startInteractive() {
|
|
|
472
538
|
{
|
|
473
539
|
think: true,
|
|
474
540
|
onContent: (token) => {
|
|
475
|
-
|
|
476
|
-
clearInitSpinner();
|
|
477
|
-
firstContent = false;
|
|
478
|
-
}
|
|
541
|
+
initSpinner.bump();
|
|
479
542
|
process.stdout.write(token);
|
|
480
543
|
},
|
|
481
544
|
onThinking: (token) => {
|
|
545
|
+
initSpinner.bump();
|
|
482
546
|
if (!thinkingStarted) {
|
|
483
|
-
clearInitSpinner();
|
|
484
547
|
process.stdout.write(chalk.dim('Thinking: '));
|
|
485
548
|
thinkingStarted = true;
|
|
486
549
|
}
|
|
@@ -506,6 +569,7 @@ export async function startInteractive() {
|
|
|
506
569
|
continue;
|
|
507
570
|
}
|
|
508
571
|
}
|
|
572
|
+
initSpinner.pause();
|
|
509
573
|
console.log(chalk.cyan('\n ▶ ') + chalk.bold(name) + chalk.dim(' ') + (name === 'web_search' ? (args?.query ?? '') : (args?.command ?? '')));
|
|
510
574
|
const result = await runTool(name, args ?? {}, { cwd: process.cwd(), confirmFn: () => Promise.resolve(true) });
|
|
511
575
|
currentInitMessages.push({
|
|
@@ -515,18 +579,17 @@ export async function startInteractive() {
|
|
|
515
579
|
});
|
|
516
580
|
}
|
|
517
581
|
}
|
|
582
|
+
initSpinner.stop();
|
|
518
583
|
if (existsSync(getMarkovPath())) {
|
|
519
584
|
console.log('\n' + chalk.green('✓ markov.md created. It will be included in the system message from now on.\n'));
|
|
520
585
|
} else {
|
|
521
586
|
console.log('\n' + chalk.yellow('Init finished but markov.md was not created. The agent may need another run or a clearer prompt.\n'));
|
|
522
587
|
}
|
|
523
588
|
} catch (err) {
|
|
524
|
-
|
|
525
|
-
clearInterval(initSpinner);
|
|
526
|
-
initSpinner = null;
|
|
527
|
-
process.stdout.write('\r\x1b[0J');
|
|
528
|
-
}
|
|
589
|
+
initSpinner.stop();
|
|
529
590
|
if (!initAbort.signal?.aborted) console.log(chalk.red(`\n${err.message}\n`));
|
|
591
|
+
} finally {
|
|
592
|
+
clearInitInterruptHandler();
|
|
530
593
|
}
|
|
531
594
|
continue;
|
|
532
595
|
}
|
|
@@ -549,39 +612,24 @@ export async function startInteractive() {
|
|
|
549
612
|
const agentMessages = [buildAgentSystemMessage(), { role: 'user', content: buildContent }];
|
|
550
613
|
maybePrintFullPayload(agentMessages);
|
|
551
614
|
const abortController = new AbortController();
|
|
552
|
-
const confirmFn = (cmd) =>
|
|
615
|
+
const confirmFn = async (cmd) => (await askConfirm(chalk.bold(`Run: ${cmd}? [y/N] `))) === true;
|
|
553
616
|
const confirmFileEdit = async (name, args) => {
|
|
554
617
|
console.log(chalk.dim('\n Proposed change:\n'));
|
|
555
618
|
console.log(formatFileEditPreview(name, args));
|
|
556
619
|
console.log('');
|
|
557
|
-
return
|
|
620
|
+
return (await askConfirm(chalk.bold('Apply this change? [y/N] '))) === true;
|
|
558
621
|
};
|
|
559
622
|
const startTime = Date.now();
|
|
560
|
-
const
|
|
561
|
-
|
|
562
|
-
let spinner = null;
|
|
563
|
-
const startSpinner = () => {
|
|
564
|
-
if (spinner) { clearInterval(spinner); spinner = null; }
|
|
565
|
-
dotIdx = 0;
|
|
566
|
-
process.stdout.write(chalk.dim('\nAgent › '));
|
|
567
|
-
spinner = setInterval(() => {
|
|
568
|
-
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
569
|
-
process.stdout.write('\r' + chalk.dim('Agent › ') + chalk.dim(elapsed + 's ') + agentGradient(DOTS[dotIdx % DOTS.length]) + ' ');
|
|
570
|
-
dotIdx++;
|
|
571
|
-
}, 400);
|
|
572
|
-
};
|
|
573
|
-
const stopSpinner = () => {
|
|
574
|
-
if (spinner) { clearInterval(spinner); spinner = null; }
|
|
575
|
-
process.stdout.write('\r\x1b[0J');
|
|
576
|
-
};
|
|
623
|
+
const idleSpinner = createIdleSpinner('Squirming ', { startTime });
|
|
624
|
+
const clearBuildInterruptHandler = registerAbortController(abortController, () => idleSpinner.stop());
|
|
577
625
|
try {
|
|
578
626
|
const result = await runAgentLoop(agentMessages, {
|
|
579
627
|
signal: abortController.signal,
|
|
580
628
|
cwd: process.cwd(),
|
|
581
629
|
confirmFn,
|
|
582
630
|
confirmFileEdit,
|
|
583
|
-
onThinking: () => {
|
|
584
|
-
onBeforeToolRun: () => {
|
|
631
|
+
onThinking: () => { idleSpinner.bump(); },
|
|
632
|
+
onBeforeToolRun: () => { idleSpinner.pause(); },
|
|
585
633
|
onIteration: (iter, max, toolCount) => {
|
|
586
634
|
const w = process.stdout.columns || 80;
|
|
587
635
|
const label = ` Step ${iter} `;
|
|
@@ -596,7 +644,7 @@ export async function startInteractive() {
|
|
|
596
644
|
console.log(chalk.dim(' ') + formatToolResultSummary(name, resultStr));
|
|
597
645
|
},
|
|
598
646
|
});
|
|
599
|
-
|
|
647
|
+
idleSpinner.stop();
|
|
600
648
|
if (result) {
|
|
601
649
|
chatMessages.push(result.finalMessage);
|
|
602
650
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
@@ -608,21 +656,27 @@ export async function startInteractive() {
|
|
|
608
656
|
maybePrintRawModelOutput(result.content);
|
|
609
657
|
}
|
|
610
658
|
} catch (err) {
|
|
611
|
-
|
|
659
|
+
idleSpinner.stop();
|
|
612
660
|
if (!abortController.signal?.aborted) console.log(chalk.red(`\n${err.message}\n`));
|
|
661
|
+
} finally {
|
|
662
|
+
clearBuildInterruptHandler();
|
|
613
663
|
}
|
|
614
664
|
continue;
|
|
615
665
|
}
|
|
616
666
|
|
|
617
667
|
// /models — pick active model (Claude or Ollama)
|
|
618
668
|
if (trimmed === '/models') {
|
|
619
|
-
const
|
|
620
|
-
|
|
669
|
+
const { options, warning } = await getModelOptions();
|
|
670
|
+
if (warning) console.log(chalk.yellow(`\n${warning}\n`));
|
|
671
|
+
const labels = options.map((o) => o.label);
|
|
672
|
+
const chosen = await askSelect(labels, 'Select model:');
|
|
621
673
|
if (chosen) {
|
|
622
|
-
const opt =
|
|
674
|
+
const opt = options.find((o) => o.label === chosen);
|
|
623
675
|
if (opt) {
|
|
624
676
|
if (opt.provider === 'claude' && !getClaudeKey()) {
|
|
625
|
-
const
|
|
677
|
+
const prompted = await askSecret('Claude API key (paste then Enter): ');
|
|
678
|
+
if (prompted === null) continue;
|
|
679
|
+
const enteredKey = prompted.trim();
|
|
626
680
|
if (!enteredKey) {
|
|
627
681
|
console.log(chalk.yellow('\nNo key entered. Model not switched.\n'));
|
|
628
682
|
continue;
|
|
@@ -630,7 +684,9 @@ export async function startInteractive() {
|
|
|
630
684
|
setClaudeKey(enteredKey);
|
|
631
685
|
}
|
|
632
686
|
if (opt.provider === 'openai' && !getOpenAIKey()) {
|
|
633
|
-
const
|
|
687
|
+
const prompted = await askSecret('OpenAI API key (paste then Enter): ');
|
|
688
|
+
if (prompted === null) continue;
|
|
689
|
+
const enteredKey = prompted.trim();
|
|
634
690
|
if (!enteredKey) {
|
|
635
691
|
console.log(chalk.yellow('\nNo key entered. Model not switched.\n'));
|
|
636
692
|
continue;
|
|
@@ -638,7 +694,9 @@ export async function startInteractive() {
|
|
|
638
694
|
setOpenAIKey(enteredKey);
|
|
639
695
|
}
|
|
640
696
|
if (opt.provider === 'ollama' && opt.model.endsWith('-cloud') && !getOllamaKey()) {
|
|
641
|
-
const
|
|
697
|
+
const prompted = await askSecret('Ollama API key for cloud models (paste then Enter): ');
|
|
698
|
+
if (prompted === null) continue;
|
|
699
|
+
const enteredKey = prompted.trim();
|
|
642
700
|
if (!enteredKey) {
|
|
643
701
|
console.log(chalk.yellow('\nNo key entered. Model not switched.\n'));
|
|
644
702
|
continue;
|
|
@@ -670,18 +728,39 @@ export async function startInteractive() {
|
|
|
670
728
|
|
|
671
729
|
// /cmd [command] — run a shell command in the current folder
|
|
672
730
|
if (trimmed === '/cmd' || trimmed.startsWith('/cmd ')) {
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
731
|
+
let command;
|
|
732
|
+
if (trimmed.startsWith('/cmd ')) {
|
|
733
|
+
command = trimmed.slice(5).trim();
|
|
734
|
+
} else {
|
|
735
|
+
const prompted = await askLine(chalk.bold('Command: '));
|
|
736
|
+
if (prompted === null) continue;
|
|
737
|
+
command = prompted.trim();
|
|
738
|
+
}
|
|
676
739
|
if (!command) {
|
|
677
740
|
console.log(chalk.yellow('No command given. Use /cmd <command> e.g. /cmd ls -la\n'));
|
|
678
741
|
continue;
|
|
679
742
|
}
|
|
743
|
+
|
|
744
|
+
// Intercept 'cd' to act like native /cd
|
|
745
|
+
if (command === 'cd' || command.startsWith('cd ')) {
|
|
746
|
+
const arg = command.startsWith('cd ') ? command.slice(3).trim() : '';
|
|
747
|
+
const target = arg
|
|
748
|
+
? resolve(process.cwd(), arg.replace(/^~/, homedir()))
|
|
749
|
+
: homedir();
|
|
750
|
+
try {
|
|
751
|
+
process.chdir(target);
|
|
752
|
+
allFiles = getFilesAndDirs();
|
|
753
|
+
console.log(chalk.dim(`\n📁 ${process.cwd()}\n`));
|
|
754
|
+
} catch {
|
|
755
|
+
console.log(chalk.red(`\nno such directory: ${target}\n`));
|
|
756
|
+
}
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
|
|
680
760
|
try {
|
|
681
761
|
const cwd = process.cwd();
|
|
682
|
-
|
|
683
|
-
const
|
|
684
|
-
if (out) console.log(out);
|
|
762
|
+
console.log(''); // Add a newline before output
|
|
763
|
+
const exitCode = await spawnCommand(command, cwd);
|
|
685
764
|
if (exitCode !== 0) {
|
|
686
765
|
console.log(chalk.red(`\nExit code: ${exitCode}\n`));
|
|
687
766
|
} else {
|
|
@@ -727,31 +806,16 @@ export async function startInteractive() {
|
|
|
727
806
|
const confirmFn = () => Promise.resolve(true);
|
|
728
807
|
const confirmFileEdit = async () => true;
|
|
729
808
|
const startTime = Date.now();
|
|
730
|
-
const
|
|
731
|
-
|
|
732
|
-
let spinner = null;
|
|
733
|
-
const startSpinner = () => {
|
|
734
|
-
if (spinner) { clearInterval(spinner); spinner = null; }
|
|
735
|
-
dotIdx = 0;
|
|
736
|
-
process.stdout.write(chalk.dim('\nLaravel › '));
|
|
737
|
-
spinner = setInterval(() => {
|
|
738
|
-
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
739
|
-
process.stdout.write('\r' + chalk.dim('Laravel › ') + chalk.dim(elapsed + 's ') + agentGradient(DOTS[dotIdx % DOTS.length]) + ' ');
|
|
740
|
-
dotIdx++;
|
|
741
|
-
}, 400);
|
|
742
|
-
};
|
|
743
|
-
const stopSpinner = () => {
|
|
744
|
-
if (spinner) { clearInterval(spinner); spinner = null; }
|
|
745
|
-
process.stdout.write('\r\x1b[0J');
|
|
746
|
-
};
|
|
809
|
+
const idleSpinner = createIdleSpinner('Squirming ', { startTime });
|
|
810
|
+
const clearLaravelInterruptHandler = registerAbortController(abortController, () => idleSpinner.stop());
|
|
747
811
|
try {
|
|
748
812
|
const result = await runAgentLoop(agentMessages, {
|
|
749
813
|
signal: abortController.signal,
|
|
750
814
|
cwd: process.cwd(),
|
|
751
815
|
confirmFn,
|
|
752
816
|
confirmFileEdit,
|
|
753
|
-
onThinking: () => {
|
|
754
|
-
onBeforeToolRun: () => {
|
|
817
|
+
onThinking: () => { idleSpinner.bump(); },
|
|
818
|
+
onBeforeToolRun: () => { idleSpinner.pause(); },
|
|
755
819
|
onIteration: (iter, max, toolCount) => {
|
|
756
820
|
const w = process.stdout.columns || 80;
|
|
757
821
|
const label = ` Step ${iter} `;
|
|
@@ -766,7 +830,7 @@ export async function startInteractive() {
|
|
|
766
830
|
console.log(chalk.dim(' ') + formatToolResultSummary(name, resultStr));
|
|
767
831
|
},
|
|
768
832
|
});
|
|
769
|
-
|
|
833
|
+
idleSpinner.stop();
|
|
770
834
|
if (result) {
|
|
771
835
|
chatMessages.push(result.finalMessage);
|
|
772
836
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
@@ -778,8 +842,10 @@ export async function startInteractive() {
|
|
|
778
842
|
maybePrintRawModelOutput(result.content);
|
|
779
843
|
}
|
|
780
844
|
} catch (err) {
|
|
781
|
-
|
|
845
|
+
idleSpinner.stop();
|
|
782
846
|
if (!abortController.signal?.aborted) console.log(chalk.red(`\n${err.message}\n`));
|
|
847
|
+
} finally {
|
|
848
|
+
clearLaravelInterruptHandler();
|
|
783
849
|
}
|
|
784
850
|
continue;
|
|
785
851
|
}
|
|
@@ -800,32 +866,16 @@ export async function startInteractive() {
|
|
|
800
866
|
const agentMessages = [buildAgentSystemMessage(), ...chatMessages];
|
|
801
867
|
maybePrintFullPayload(agentMessages);
|
|
802
868
|
const abortController = new AbortController();
|
|
803
|
-
const confirmFn = (cmd) =>
|
|
869
|
+
const confirmFn = async (cmd) => (await askConfirm(chalk.bold(`Run: ${cmd}? [y/N] `))) === true;
|
|
804
870
|
const confirmFileEdit = async (name, args) => {
|
|
805
871
|
console.log(chalk.dim('\n Proposed change:\n'));
|
|
806
872
|
console.log(formatFileEditPreview(name, args));
|
|
807
873
|
console.log('');
|
|
808
|
-
return
|
|
874
|
+
return (await askConfirm(chalk.bold('Apply this change? [y/N] '))) === true;
|
|
809
875
|
};
|
|
810
876
|
const startTime = Date.now();
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
let spinner = null;
|
|
814
|
-
|
|
815
|
-
const startSpinner = () => {
|
|
816
|
-
if (spinner) { clearInterval(spinner); spinner = null; }
|
|
817
|
-
dotIdx = 0;
|
|
818
|
-
process.stdout.write(chalk.dim('\nAgent › '));
|
|
819
|
-
spinner = setInterval(() => {
|
|
820
|
-
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
821
|
-
process.stdout.write('\r' + chalk.dim('Agent › ') + chalk.dim(elapsed + 's ') + agentGradient(DOTS[dotIdx % DOTS.length]) + ' ');
|
|
822
|
-
dotIdx++;
|
|
823
|
-
}, 400);
|
|
824
|
-
};
|
|
825
|
-
const stopSpinner = () => {
|
|
826
|
-
if (spinner) { clearInterval(spinner); spinner = null; }
|
|
827
|
-
process.stdout.write('\r\x1b[0J');
|
|
828
|
-
};
|
|
877
|
+
const idleSpinner = createIdleSpinner('Agent › ', { startTime });
|
|
878
|
+
const clearAgentInterruptHandler = registerAbortController(abortController, () => idleSpinner.stop());
|
|
829
879
|
|
|
830
880
|
try {
|
|
831
881
|
const result = await runAgentLoop(agentMessages, {
|
|
@@ -834,10 +884,10 @@ export async function startInteractive() {
|
|
|
834
884
|
confirmFn,
|
|
835
885
|
confirmFileEdit,
|
|
836
886
|
onThinking: () => {
|
|
837
|
-
|
|
887
|
+
idleSpinner.bump();
|
|
838
888
|
},
|
|
839
889
|
onBeforeToolRun: () => {
|
|
840
|
-
|
|
890
|
+
idleSpinner.pause();
|
|
841
891
|
},
|
|
842
892
|
onIteration: (iter, max, toolCount) => {
|
|
843
893
|
const w = process.stdout.columns || 80;
|
|
@@ -853,7 +903,7 @@ export async function startInteractive() {
|
|
|
853
903
|
console.log(chalk.dim(' ') + formatToolResultSummary(name, resultStr));
|
|
854
904
|
},
|
|
855
905
|
});
|
|
856
|
-
|
|
906
|
+
idleSpinner.stop();
|
|
857
907
|
if (result) {
|
|
858
908
|
chatMessages.push(result.finalMessage);
|
|
859
909
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
@@ -865,8 +915,10 @@ export async function startInteractive() {
|
|
|
865
915
|
maybePrintRawModelOutput(result.content);
|
|
866
916
|
}
|
|
867
917
|
} catch (err) {
|
|
868
|
-
|
|
918
|
+
idleSpinner.stop();
|
|
869
919
|
if (!abortController.signal?.aborted) console.log(chalk.red(`\n${err.message}\n`));
|
|
920
|
+
} finally {
|
|
921
|
+
clearAgentInterruptHandler();
|
|
870
922
|
}
|
|
871
923
|
}
|
|
872
924
|
}
|