markov-cli 1.0.11 ā 1.0.13
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 +12 -0
- package/bin/markov.js +15 -0
- package/package.json +1 -1
- package/src/agent/agentLoop.js +131 -0
- package/src/agent/context.js +102 -0
- package/src/auth.js +69 -0
- package/src/claude.js +318 -0
- package/src/commands/setup.js +85 -0
- package/src/editor/codeBlockEdits.js +27 -0
- package/src/files.js +1 -1
- package/src/input.js +67 -13
- package/src/interactive.js +414 -605
- package/src/ollama.js +179 -7
- package/src/openai.js +258 -0
- package/src/tools.js +154 -36
- package/src/ui/formatting.js +125 -0
- package/src/ui/logo.js +36 -9
- package/src/ui/prompts.js +116 -0
- package/src/ui/spinner.js +40 -0
package/src/interactive.js
CHANGED
|
@@ -1,484 +1,42 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
|
|
3
3
|
import gradient from 'gradient-string';
|
|
4
|
+
import { writeFileSync, readFileSync, existsSync } from 'fs';
|
|
4
5
|
import { homedir } from 'os';
|
|
5
6
|
import { resolve } from 'path';
|
|
6
|
-
import { mkdirSync, writeFileSync } from 'fs';
|
|
7
7
|
import { printLogo } from './ui/logo.js';
|
|
8
|
-
import { chatWithTools, streamChat, MODEL,
|
|
8
|
+
import { chatWithTools, streamChat, streamChatWithTools, MODEL, MODEL_OPTIONS, setModelAndProvider, getModelDisplayName } from './ollama.js';
|
|
9
9
|
import { resolveFileRefs } from './files.js';
|
|
10
|
-
import {
|
|
11
|
-
import { parseEdits, renderDiff, applyEdit } from './editor.js';
|
|
10
|
+
import { RUN_TERMINAL_COMMAND_TOOL, WEB_SEARCH_TOOL, runTool } from './tools.js';
|
|
12
11
|
import { chatPrompt } from './input.js';
|
|
13
|
-
import {
|
|
14
|
-
import { getToken, login, clearToken } from './auth.js';
|
|
12
|
+
import { getFilesAndDirs } from './ui/picker.js';
|
|
13
|
+
import { getToken, login, clearToken, getClaudeKey, setClaudeKey, getOpenAIKey, setOpenAIKey, getOllamaKey, setOllamaKey } from './auth.js';
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return new Promise((resolve) => {
|
|
21
|
-
let idx = 0;
|
|
22
|
-
|
|
23
|
-
const draw = () => {
|
|
24
|
-
process.stdout.write('\r\x1b[0J');
|
|
25
|
-
process.stdout.write(chalk.dim(label) + '\n');
|
|
26
|
-
options.forEach((o, i) => {
|
|
27
|
-
process.stdout.write(
|
|
28
|
-
i === idx
|
|
29
|
-
? ' ' + chalk.bgCyan.black(` ${o} `) + '\n'
|
|
30
|
-
: ' ' + chalk.dim(o) + '\n'
|
|
31
|
-
);
|
|
32
|
-
});
|
|
33
|
-
// Move cursor back up to keep it stable
|
|
34
|
-
process.stdout.write(`\x1b[${options.length + 1}A`);
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
const cleanup = () => {
|
|
38
|
-
process.stdin.removeListener('data', onKey);
|
|
39
|
-
process.stdin.setRawMode(false);
|
|
40
|
-
process.stdin.pause();
|
|
41
|
-
// Clear the drawn lines
|
|
42
|
-
process.stdout.write('\r\x1b[0J');
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const onKey = (data) => {
|
|
46
|
-
const key = data.toString();
|
|
47
|
-
if (key === '\x1b[A') { idx = (idx - 1 + options.length) % options.length; draw(); return; }
|
|
48
|
-
if (key === '\x1b[B') { idx = (idx + 1) % options.length; draw(); return; }
|
|
49
|
-
if (key === '\r' || key === '\n') { cleanup(); resolve(options[idx]); return; }
|
|
50
|
-
if (key === '\x03' || key === '\x11') { cleanup(); resolve(null); return; }
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
process.stdin.setRawMode(true);
|
|
54
|
-
process.stdin.resume();
|
|
55
|
-
process.stdin.setEncoding('utf8');
|
|
56
|
-
process.stdin.on('data', onKey);
|
|
57
|
-
draw();
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Prompt y/n in raw mode, returns true for y/Y. */
|
|
62
|
-
function confirm(question) {
|
|
63
|
-
return new Promise((resolve) => {
|
|
64
|
-
process.stdout.write(question);
|
|
65
|
-
process.stdin.setRawMode(true);
|
|
66
|
-
process.stdin.resume();
|
|
67
|
-
process.stdin.setEncoding('utf8');
|
|
68
|
-
const onKey = (key) => {
|
|
69
|
-
process.stdin.removeListener('data', onKey);
|
|
70
|
-
process.stdin.setRawMode(false);
|
|
71
|
-
process.stdin.pause();
|
|
72
|
-
const answer = key.toLowerCase() === 'y';
|
|
73
|
-
process.stdout.write(answer ? chalk.green('y\n') : chalk.dim('n\n'));
|
|
74
|
-
resolve(answer);
|
|
75
|
-
};
|
|
76
|
-
process.stdin.on('data', onKey);
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/** Read a visible line of input. */
|
|
81
|
-
function promptLine(label) {
|
|
82
|
-
return new Promise((resolve) => {
|
|
83
|
-
process.stdout.write(label);
|
|
84
|
-
let buf = '';
|
|
85
|
-
const onData = (data) => {
|
|
86
|
-
const key = data.toString();
|
|
87
|
-
if (key === '\r' || key === '\n') {
|
|
88
|
-
process.stdin.removeListener('data', onData);
|
|
89
|
-
process.stdin.setRawMode(false);
|
|
90
|
-
process.stdin.pause();
|
|
91
|
-
process.stdout.write('\n');
|
|
92
|
-
resolve(buf);
|
|
93
|
-
} else if (key === '\x7f' || key === '\b') {
|
|
94
|
-
if (buf.length > 0) { buf = buf.slice(0, -1); process.stdout.write('\b \b'); }
|
|
95
|
-
} else if (key >= ' ') {
|
|
96
|
-
buf += key;
|
|
97
|
-
process.stdout.write(key);
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
process.stdin.setRawMode(true);
|
|
101
|
-
process.stdin.resume();
|
|
102
|
-
process.stdin.setEncoding('utf8');
|
|
103
|
-
process.stdin.on('data', onData);
|
|
104
|
-
});
|
|
105
|
-
}
|
|
15
|
+
// Extracted UI modules
|
|
16
|
+
import { selectFrom, confirm, promptLine, promptSecret } from './ui/prompts.js';
|
|
17
|
+
import { formatResponseWithCodeBlocks, formatFileEditPreview, formatToolCallSummary, formatToolResultSummary, printTokenUsage } from './ui/formatting.js';
|
|
18
|
+
import { createSpinner } from './ui/spinner.js';
|
|
106
19
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
process.stdout.write(label);
|
|
111
|
-
let buf = '';
|
|
112
|
-
const onData = (data) => {
|
|
113
|
-
const key = data.toString();
|
|
114
|
-
if (key === '\r' || key === '\n') {
|
|
115
|
-
process.stdin.removeListener('data', onData);
|
|
116
|
-
process.stdin.setRawMode(false);
|
|
117
|
-
process.stdin.pause();
|
|
118
|
-
process.stdout.write('\n');
|
|
119
|
-
resolve(buf);
|
|
120
|
-
} else if (key === '\x7f' || key === '\b') {
|
|
121
|
-
if (buf.length > 0) buf = buf.slice(0, -1);
|
|
122
|
-
} else if (key >= ' ') {
|
|
123
|
-
buf += key;
|
|
124
|
-
}
|
|
125
|
-
};
|
|
126
|
-
process.stdin.setRawMode(true);
|
|
127
|
-
process.stdin.resume();
|
|
128
|
-
process.stdin.setEncoding('utf8');
|
|
129
|
-
process.stdin.on('data', onData);
|
|
130
|
-
});
|
|
131
|
-
}
|
|
20
|
+
// Extracted agent modules
|
|
21
|
+
import { buildPlanSystemMessage, buildAgentSystemMessage, buildInitSystemMessage, getLsContext, getGrepContext } from './agent/context.js';
|
|
22
|
+
import { runAgentLoop, maybePrintFullPayload, AGENT_LOOP_MAX_ITERATIONS } from './agent/agentLoop.js';
|
|
132
23
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const visLen = (s) => s.replace(ANSI_RE, '').length;
|
|
24
|
+
// Extracted editor modules
|
|
25
|
+
import { applyCodeBlockEdits } from './editor/codeBlockEdits.js';
|
|
136
26
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
return text.split('\n').map(line => {
|
|
140
|
-
if (visLen(line) <= width) return line;
|
|
141
|
-
const words = line.split(' ');
|
|
142
|
-
const wrapped = [];
|
|
143
|
-
let current = '';
|
|
144
|
-
for (const word of words) {
|
|
145
|
-
const test = current ? current + ' ' + word : word;
|
|
146
|
-
if (visLen(test) <= width) {
|
|
147
|
-
current = test;
|
|
148
|
-
} else {
|
|
149
|
-
if (current) wrapped.push(current);
|
|
150
|
-
current = word;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
if (current) wrapped.push(current);
|
|
154
|
-
return wrapped.join('\n');
|
|
155
|
-
}).join('\n');
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** Extract numbered options (1. foo 2. bar 3. baz) from AI response text. Returns display strings or []. */
|
|
159
|
-
function extractNumberedOptions(text) {
|
|
160
|
-
if (!text) return [];
|
|
161
|
-
const re = /^\s*(\d+)\.\s+(.+)$/gm;
|
|
162
|
-
const items = [];
|
|
163
|
-
let match;
|
|
164
|
-
while ((match = re.exec(text)) !== null) {
|
|
165
|
-
items.push({ num: parseInt(match[1], 10), label: match[2].trim() });
|
|
166
|
-
}
|
|
167
|
-
let best = [];
|
|
168
|
-
for (let i = 0; i < items.length; i++) {
|
|
169
|
-
if (items[i].num !== 1) continue;
|
|
170
|
-
const run = [items[i]];
|
|
171
|
-
for (let j = i + 1; j < items.length; j++) {
|
|
172
|
-
if (items[j].num === run.length + 1) run.push(items[j]);
|
|
173
|
-
else break;
|
|
174
|
-
}
|
|
175
|
-
if (run.length > best.length) best = run;
|
|
176
|
-
}
|
|
177
|
-
return best.length >= 2 ? best.map(r => `${r.num}. ${r.label}`) : [];
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/** Parse fenced code blocks (```lang\n...\n```) and render them with plain styling. Non-code segments are wrapped. */
|
|
181
|
-
function formatResponseWithCodeBlocks(text, width) {
|
|
182
|
-
if (!text || typeof text !== 'string') return '';
|
|
183
|
-
const re = /```(\w*)\n([\s\S]*?)```/g;
|
|
184
|
-
const parts = [];
|
|
185
|
-
let lastIndex = 0;
|
|
186
|
-
let m;
|
|
187
|
-
while ((m = re.exec(text)) !== null) {
|
|
188
|
-
if (m.index > lastIndex) {
|
|
189
|
-
const textSegment = text.slice(lastIndex, m.index);
|
|
190
|
-
if (textSegment) parts.push({ type: 'text', content: textSegment });
|
|
191
|
-
}
|
|
192
|
-
parts.push({ type: 'code', lang: m[1], content: m[2].trim() });
|
|
193
|
-
lastIndex = re.lastIndex;
|
|
194
|
-
}
|
|
195
|
-
if (lastIndex < text.length) {
|
|
196
|
-
const textSegment = text.slice(lastIndex);
|
|
197
|
-
if (textSegment) parts.push({ type: 'text', content: textSegment });
|
|
198
|
-
}
|
|
199
|
-
if (parts.length === 0) return wrapText(text, width);
|
|
200
|
-
return parts.map((p) => {
|
|
201
|
-
if (p.type === 'text') return wrapText(p.content, width);
|
|
202
|
-
const label = p.lang ? p.lang : 'code';
|
|
203
|
-
const header = chalk.dim('āāā ') + chalk.cyan(label) + chalk.dim(' ' + 'ā'.repeat(Math.max(0, width - label.length - 5)));
|
|
204
|
-
const code = p.content.split('\n').map(l => chalk.dim(' ') + l).join('\n');
|
|
205
|
-
return header + '\n' + code + '\n' + chalk.dim('ā'.repeat(width));
|
|
206
|
-
}).join('\n\n');
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Scan AI response text for fenced code blocks that reference files.
|
|
211
|
-
* Show a diff preview for each and apply on user confirmation.
|
|
212
|
-
* @param {string} responseText - The AI's response content
|
|
213
|
-
* @param {string[]} loadedFiles - Files attached via @ref
|
|
214
|
-
*/
|
|
215
|
-
async function applyCodeBlockEdits(responseText, loadedFiles = []) {
|
|
216
|
-
const edits = parseEdits(responseText, loadedFiles);
|
|
217
|
-
if (edits.length === 0) return;
|
|
218
|
-
|
|
219
|
-
console.log(chalk.dim(`\nš Detected ${edits.length} file edit(s) in response:\n`));
|
|
220
|
-
|
|
221
|
-
for (const edit of edits) {
|
|
222
|
-
renderDiff(edit.filepath, edit.content);
|
|
223
|
-
const ok = await confirm(chalk.bold(`Apply changes to ${chalk.cyan(edit.filepath)}? [y/N] `));
|
|
224
|
-
if (ok) {
|
|
225
|
-
applyEdit(edit.filepath, edit.content);
|
|
226
|
-
console.log(chalk.green(` ā ${edit.filepath} updated\n`));
|
|
227
|
-
} else {
|
|
228
|
-
console.log(chalk.dim(` ā skipped ${edit.filepath}\n`));
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/** Run `ls -la` in cwd and return a context string to prepend to user messages. */
|
|
234
|
-
async function getLsContext(cwd = process.cwd()) {
|
|
235
|
-
try {
|
|
236
|
-
const { stdout, stderr, exitCode } = await execCommand('ls -la', cwd);
|
|
237
|
-
const out = [stdout, stderr].filter(Boolean).join('\n').trim();
|
|
238
|
-
if (exitCode === 0 && out) {
|
|
239
|
-
return `[Current directory: ${cwd}]\n$ ls -la\n${out}\n\n`;
|
|
240
|
-
}
|
|
241
|
-
} catch (_) {}
|
|
242
|
-
return `[Current directory: ${cwd}]\n(listing unavailable)\n\n`;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
const GREP_DEFAULT_PATTERN = 'function |class |export |def ';
|
|
246
|
-
const GREP_MAX_LINES_DEFAULT = 400;
|
|
247
|
-
const GREP_INCLUDE = "--include='*.js' --include='*.ts' --include='*.jsx' --include='*.tsx' --include='*.py'";
|
|
248
|
-
/** Portable: filter paths with grep -v (BSD grep may not support --exclude-dir). */
|
|
249
|
-
const GREP_FILTER_PATHS = "| grep -v '/node_modules/' | grep -v '/.git/' | grep -v '/.next/' | grep -v '/dist/' | grep -v '/build/' | grep -v '/coverage/'";
|
|
250
|
-
|
|
251
|
-
/** Safe chars for MARKOV_GREP_PATTERN (no shell metacharacters). */
|
|
252
|
-
function isValidGrepPattern(p) {
|
|
253
|
-
if (typeof p !== 'string' || p.length > 200) return false;
|
|
254
|
-
return /^[a-zA-Z0-9 \|\.\-_\\\[\]\(\)\?\+\*]+$/.test(p);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/** Run grep for key definitions; return context block or empty string. Only runs when MARKOV_GREP_CONTEXT=1. */
|
|
258
|
-
async function getGrepContext(cwd = process.cwd()) {
|
|
259
|
-
if (!process.env.MARKOV_GREP_CONTEXT) return '';
|
|
260
|
-
|
|
261
|
-
const rawPattern = process.env.MARKOV_GREP_PATTERN;
|
|
262
|
-
const pattern = (rawPattern && isValidGrepPattern(rawPattern.trim())) ? rawPattern.trim() : GREP_DEFAULT_PATTERN;
|
|
263
|
-
const maxLines = Math.min(2000, Math.max(1, parseInt(process.env.MARKOV_GREP_MAX_LINES, 10) || GREP_MAX_LINES_DEFAULT));
|
|
264
|
-
const escaped = pattern.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
265
|
-
const cmd = `grep -rn -E "${escaped}" ${GREP_INCLUDE} . 2>/dev/null ${GREP_FILTER_PATHS} | head -n ${maxLines}`;
|
|
266
|
-
|
|
267
|
-
try {
|
|
268
|
-
const { stdout, exitCode } = await execCommand(cmd, cwd);
|
|
269
|
-
const out = (stdout || '').trim();
|
|
270
|
-
if (exitCode === 0 && out) {
|
|
271
|
-
return `[Grep: key definitions in repo]\n$ grep -rn -E '${pattern}' ...\n${out}\n\n`;
|
|
272
|
-
}
|
|
273
|
-
} catch (_) {}
|
|
274
|
-
return `[Grep: key definitions in repo]\n(grep context unavailable)\n\n`;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
/** Shared system message base: Markov intro, cwd, file list. */
|
|
278
|
-
function getSystemMessageBase() {
|
|
279
|
-
const files = getFiles();
|
|
280
|
-
const fileList = files.length > 0 ? `\nFiles in working directory:\n${files.map(f => ` ${f}`).join('\n')}\n` : '';
|
|
281
|
-
return `You are Markov, an AI coding assistant.\nWorking directory: ${process.cwd()}\n${fileList}`;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/** System message for /plan: output a step-by-step plan only (no tools). */
|
|
285
|
-
function buildPlanSystemMessage() {
|
|
286
|
-
return {
|
|
287
|
-
role: 'system',
|
|
288
|
-
content: getSystemMessageBase() +
|
|
289
|
-
'\nOutput a clear, step-by-step plan only. Do not run any commands or edit filesājust describe the steps. No tools.',
|
|
290
|
-
};
|
|
291
|
-
}
|
|
27
|
+
// Extracted command modules
|
|
28
|
+
import { runSetupSteps, NEXTJS_STEPS, TANSTACK_STEPS, LARAVEL_STEPS, LARAVEL_BLOG_PROMPT } from './commands/setup.js';
|
|
292
29
|
|
|
293
|
-
|
|
294
|
-
function buildAgentSystemMessage() {
|
|
295
|
-
const toolInstructions =
|
|
296
|
-
`\nTOOL MODE ā you have tools; use them. \n` +
|
|
297
|
-
`- run_terminal_command: run shell commands (npm install, npx create-next-app, etc.). One command per call.\n` +
|
|
298
|
-
`- create_folder: create directories.\n` +
|
|
299
|
-
`- read_file: read file contents before editing.\n` +
|
|
300
|
-
`- write_file: create or overwrite a file with full content.\n` +
|
|
301
|
-
`- search_replace: replace first occurrence of text in a file.\n` +
|
|
302
|
-
`- delete_file: delete a file.\n` +
|
|
303
|
-
`- list_dir: list directory contents (path optional, defaults to current dir).\n` +
|
|
304
|
-
`When the user asks to run commands, create/edit/delete files, scaffold projects, or to "plan" or "start" an app (e.g. npm run dev), you MUST call the appropriate tool. Do not only describe steps in your reply.\n` +
|
|
305
|
-
`You MUST ALWAYS use write_file or search_replace for any file modification, creation, or fix. Never paste full file contents directly in your reply.\n` +
|
|
306
|
-
`All file operations must use RELATIVE paths.\n` +
|
|
307
|
-
`Do not output modified file contents in chat ā apply changes through tool calls only.\n`;
|
|
308
|
-
return { role: 'system', content: getSystemMessageBase() + toolInstructions };
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const AGENT_LOOP_MAX_ITERATIONS = 20;
|
|
312
|
-
|
|
313
|
-
/** If MARKOV_DEBUG is set, print the full message payload (system + conversation) before sending. */
|
|
314
|
-
function maybePrintFullPayload(messages) {
|
|
315
|
-
if (!process.env.MARKOV_DEBUG) return;
|
|
316
|
-
const payload = messages.map((m) => ({
|
|
317
|
-
role: m.role,
|
|
318
|
-
...(m.tool_calls && { tool_calls: m.tool_calls.length }),
|
|
319
|
-
...(m.tool_name && { tool_name: m.tool_name }),
|
|
320
|
-
content: typeof m.content === 'string' ? m.content : '(binary/object)',
|
|
321
|
-
}));
|
|
322
|
-
console.log(chalk.dim('\n--- MARKOV_DEBUG: full payload sent to API ---'));
|
|
323
|
-
console.log(JSON.stringify(payload, null, 2));
|
|
324
|
-
console.log(chalk.dim('--- end payload ---\n'));
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/** Preview of a file edit for confirmation (write_file / search_replace) */
|
|
328
|
-
function formatFileEditPreview(name, args) {
|
|
329
|
-
const path = args?.path ?? '(no path)';
|
|
330
|
-
if (name === 'search_replace') {
|
|
331
|
-
const oldStr = String(args?.old_string ?? '');
|
|
332
|
-
const newStr = String(args?.new_string ?? '');
|
|
333
|
-
const max = 120;
|
|
334
|
-
const oldPreview = oldStr.length > max ? oldStr.slice(0, max) + 'ā¦' : oldStr;
|
|
335
|
-
const newPreview = newStr.length > max ? newStr.slice(0, max) + 'ā¦' : newStr;
|
|
336
|
-
return (
|
|
337
|
-
chalk.cyan(path) + '\n' +
|
|
338
|
-
chalk.red(' - ' + (oldPreview || '(empty)').replace(/\n/g, '\n ')) + '\n' +
|
|
339
|
-
chalk.green(' + ' + (newPreview || '(empty)').replace(/\n/g, '\n '))
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
if (name === 'write_file') {
|
|
343
|
-
const content = String(args?.content ?? '');
|
|
344
|
-
const lines = content.split('\n');
|
|
345
|
-
const previewLines = lines.slice(0, 25);
|
|
346
|
-
const more = lines.length > 25 ? chalk.dim(` ... ${lines.length - 25} more lines`) : '';
|
|
347
|
-
return chalk.cyan(path) + '\n' + previewLines.map(l => ' ' + l).join('\n') + (more ? '\n' + more : '');
|
|
348
|
-
}
|
|
349
|
-
return path;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/** One-line summary of tool args for display */
|
|
353
|
-
function formatToolCallSummary(name, args) {
|
|
354
|
-
const a = args ?? {};
|
|
355
|
-
if (name === 'run_terminal_command') return (a.command ?? '').trim() || '(empty)';
|
|
356
|
-
if (name === 'write_file') return a.path ?? '(no path)';
|
|
357
|
-
if (name === 'search_replace') return (a.path ?? '') + (a.old_string ? ` "${String(a.old_string).slice(0, 30)}ā¦"` : '');
|
|
358
|
-
if (name === 'read_file' || name === 'delete_file') return a.path ?? '(no path)';
|
|
359
|
-
if (name === 'create_folder') return a.path ?? '(no path)';
|
|
360
|
-
if (name === 'list_dir') return (a.path ?? '.') || '.';
|
|
361
|
-
return JSON.stringify(a).slice(0, 50);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
/** One-line summary of tool result for display */
|
|
365
|
-
function formatToolResultSummary(name, resultJson) {
|
|
366
|
-
let obj;
|
|
367
|
-
try {
|
|
368
|
-
obj = typeof resultJson === 'string' ? JSON.parse(resultJson) : resultJson;
|
|
369
|
-
} catch {
|
|
370
|
-
return resultJson?.slice(0, 60) ?? 'ā';
|
|
371
|
-
}
|
|
372
|
-
if (obj.error) return chalk.red('ā ' + obj.error);
|
|
373
|
-
if (obj.declined) return chalk.yellow('ā declined');
|
|
374
|
-
if (name === 'run_terminal_command') {
|
|
375
|
-
const code = obj.exitCode ?? obj.exit_code;
|
|
376
|
-
if (code === 0) return chalk.green('ā exit 0') + (obj.stdout ? chalk.dim(' ' + String(obj.stdout).trim().slice(0, 80).replace(/\n/g, ' ')) : '');
|
|
377
|
-
return chalk.red(`ā exit ${code}`) + (obj.stderr ? chalk.dim(' ' + String(obj.stderr).trim().slice(0, 80)) : '');
|
|
378
|
-
}
|
|
379
|
-
if (name === 'write_file' || name === 'search_replace' || name === 'delete_file' || name === 'create_folder') {
|
|
380
|
-
return obj.success !== false ? chalk.green('ā ' + (obj.path ? obj.path : 'ok')) : chalk.red('ā ' + (obj.error || 'failed'));
|
|
381
|
-
}
|
|
382
|
-
if (name === 'read_file') return obj.content != null ? chalk.green('ā ' + (obj.path ?? '') + chalk.dim(` (${String(obj.content).length} chars)`)) : chalk.red('ā ' + (obj.error || ''));
|
|
383
|
-
if (name === 'list_dir') return obj.entries ? chalk.green('ā ' + (obj.entries.length ?? 0) + ' entries') : chalk.red('ā ' + (obj.error || ''));
|
|
384
|
-
return chalk.dim(JSON.stringify(obj).slice(0, 60));
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Run the agent loop: call chatWithTools, run any tool_calls locally, append results, repeat until the model returns a final response.
|
|
389
|
-
* For write_file and search_replace, confirmFileEdit is called first; if it returns false, the change is skipped and the model is told the user declined.
|
|
390
|
-
* @param {Array<{ role: string; content?: string; tool_calls?: unknown[]; tool_name?: string }>} messages - Full message list (system + conversation)
|
|
391
|
-
* @param {{ signal?: AbortSignal; cwd?: string; confirmFn?: (command: string) => Promise<boolean>; confirmFileEdit?: (name: string, args: object) => Promise<boolean>; onBeforeToolRun?: () => void; onToolCall?: (name: string, args: object) => void; onToolResult?: (name: string, result: string) => void }} opts
|
|
392
|
-
* @returns {Promise<{ content: string; finalMessage: object } | null>} Final assistant content and message, or null if cancelled/error
|
|
393
|
-
*/
|
|
394
|
-
async function runAgentLoop(messages, opts = {}) {
|
|
395
|
-
const cwd = opts.cwd ?? process.cwd();
|
|
396
|
-
const confirmFn = opts.confirmFn;
|
|
397
|
-
const confirmFileEdit = opts.confirmFileEdit;
|
|
398
|
-
const onBeforeToolRun = opts.onBeforeToolRun;
|
|
399
|
-
const onToolCall = opts.onToolCall;
|
|
400
|
-
const onToolResult = opts.onToolResult;
|
|
401
|
-
const onIteration = opts.onIteration;
|
|
402
|
-
const onThinking = opts.onThinking;
|
|
403
|
-
let iteration = 0;
|
|
404
|
-
|
|
405
|
-
while (iteration < AGENT_LOOP_MAX_ITERATIONS) {
|
|
406
|
-
iteration += 1;
|
|
407
|
-
onThinking?.(iteration);
|
|
408
|
-
const data = await chatWithTools(messages, AGENT_TOOLS, MODEL, opts.signal ?? null);
|
|
409
|
-
|
|
410
|
-
const message = data?.message;
|
|
411
|
-
if (!message) {
|
|
412
|
-
return { content: '', finalMessage: { role: 'assistant', content: '' } };
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const toolCalls = message.tool_calls;
|
|
416
|
-
if (!toolCalls || toolCalls.length === 0) {
|
|
417
|
-
return { content: message.content ?? '', finalMessage: message };
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Append assistant message with tool_calls
|
|
421
|
-
messages.push({
|
|
422
|
-
role: 'assistant',
|
|
423
|
-
content: message.content ?? '',
|
|
424
|
-
tool_calls: toolCalls,
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
onBeforeToolRun?.();
|
|
428
|
-
onIteration?.(iteration, AGENT_LOOP_MAX_ITERATIONS, toolCalls.length);
|
|
429
|
-
|
|
430
|
-
for (const tc of toolCalls) {
|
|
431
|
-
const name = tc?.function?.name;
|
|
432
|
-
const rawArgs = tc?.function?.arguments;
|
|
433
|
-
let args = rawArgs;
|
|
434
|
-
if (typeof rawArgs === 'string') {
|
|
435
|
-
try {
|
|
436
|
-
args = JSON.parse(rawArgs);
|
|
437
|
-
} catch {
|
|
438
|
-
messages.push({
|
|
439
|
-
role: 'tool',
|
|
440
|
-
tool_name: name ?? 'unknown',
|
|
441
|
-
content: JSON.stringify({ error: 'Invalid JSON in arguments' }),
|
|
442
|
-
});
|
|
443
|
-
if (onToolCall) onToolCall(name ?? 'unknown', {});
|
|
444
|
-
if (onToolResult) onToolResult(name ?? 'unknown', JSON.stringify({ error: 'Invalid JSON in arguments' }));
|
|
445
|
-
continue;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
if (onToolCall) onToolCall(name ?? 'unknown', args ?? {});
|
|
30
|
+
const agentGradient = gradient(['#22c55e', '#16a34a', '#4ade80']);
|
|
450
31
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
if (isFileEdit && confirmFileEdit) {
|
|
454
|
-
const ok = await confirmFileEdit(name, args ?? {});
|
|
455
|
-
if (!ok) {
|
|
456
|
-
result = JSON.stringify({ declined: true, message: 'User declined the change' });
|
|
457
|
-
if (onToolResult) onToolResult(name ?? 'unknown', result);
|
|
458
|
-
messages.push({
|
|
459
|
-
role: 'tool',
|
|
460
|
-
tool_name: name ?? 'unknown',
|
|
461
|
-
content: result,
|
|
462
|
-
});
|
|
463
|
-
continue;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
32
|
+
const PLAN_FILE = 'plan.md';
|
|
33
|
+
const getPlanPath = () => resolve(process.cwd(), PLAN_FILE);
|
|
466
34
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
messages.push({
|
|
470
|
-
role: 'tool',
|
|
471
|
-
tool_name: name ?? 'unknown',
|
|
472
|
-
content: typeof result === 'string' ? result : JSON.stringify(result),
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
|
-
}
|
|
35
|
+
const MARKOV_FILE = 'markov.md';
|
|
36
|
+
const getMarkovPath = () => resolve(process.cwd(), MARKOV_FILE);
|
|
476
37
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
finalMessage: { role: 'assistant', content: '(max iterations)' },
|
|
480
|
-
};
|
|
481
|
-
}
|
|
38
|
+
/** Tools allowed during /plan and /yolo plan phase: web search + run command for research only (e.g. ls, cat). */
|
|
39
|
+
const PLAN_YOLO_TOOLS = [WEB_SEARCH_TOOL, RUN_TERMINAL_COMMAND_TOOL];
|
|
482
40
|
|
|
483
41
|
/** Short intro shown on first load; /intro re-displays this. */
|
|
484
42
|
const INTRO_TEXT =
|
|
@@ -487,9 +45,10 @@ const INTRO_TEXT =
|
|
|
487
45
|
chalk.cyan(' /help') + chalk.dim(' show all commands\n') +
|
|
488
46
|
chalk.cyan(' /login') + chalk.dim(' authenticate with email & password\n') +
|
|
489
47
|
chalk.cyan(' /cd [path]') + chalk.dim(' change working directory\n') +
|
|
490
|
-
chalk.cyan(' /
|
|
491
|
-
chalk.cyan(' /
|
|
492
|
-
chalk.cyan(' /
|
|
48
|
+
chalk.cyan(' /init') + chalk.dim(' [prompt] create markov.md with project summary\n') +
|
|
49
|
+
chalk.cyan(' /plan') + chalk.dim(' [prompt] stream a plan and save to plan.md\n') +
|
|
50
|
+
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') +
|
|
493
52
|
chalk.dim('\nType a message Ā· ') + chalk.cyan('@filename') + chalk.dim(' to attach Ā· ctrl+q to cancel\n');
|
|
494
53
|
|
|
495
54
|
const HELP_TEXT =
|
|
@@ -498,66 +57,31 @@ const HELP_TEXT =
|
|
|
498
57
|
chalk.cyan(' /intro') + chalk.dim(' show quick start (same as on first load)\n') +
|
|
499
58
|
chalk.cyan(' /help') + chalk.dim(' show this help\n') +
|
|
500
59
|
chalk.cyan(' /setup-nextjs') + chalk.dim(' scaffold a Next.js app\n') +
|
|
60
|
+
chalk.cyan(' /setup-tanstack') + chalk.dim(' scaffold a TanStack Start app\n') +
|
|
501
61
|
chalk.cyan(' /setup-laravel') + chalk.dim(' scaffold a Laravel API\n') +
|
|
62
|
+
chalk.cyan(' /laravel') + chalk.dim(' set up Laravel "my-blog" with blog route (agent)\n') +
|
|
502
63
|
chalk.cyan(' /models') + chalk.dim(' switch the active AI model\n') +
|
|
503
64
|
chalk.cyan(' /cd [path]') + chalk.dim(' change working directory\n') +
|
|
504
65
|
chalk.cyan(' /cmd [command]') + chalk.dim(' run a shell command in the current folder\n') +
|
|
505
66
|
chalk.cyan(' /login') + chalk.dim(' authenticate with email & password\n') +
|
|
506
67
|
chalk.cyan(' /logout') + chalk.dim(' clear saved auth token\n') +
|
|
68
|
+
chalk.cyan(' /clear') + chalk.dim(' clear chat history and stored plan\n') +
|
|
69
|
+
chalk.cyan(' /env') + chalk.dim(' show which .env vars are loaded (for debugging)\n') +
|
|
507
70
|
chalk.cyan(' /debug') + chalk.dim(' toggle full payload dump (env MARKOV_DEBUG)\n') +
|
|
508
|
-
chalk.cyan(' /
|
|
509
|
-
chalk.cyan(' /
|
|
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') +
|
|
510
74
|
chalk.cyan(' /yolo') + chalk.dim(' [prompt] plan in stream mode, then auto-run until done\n') +
|
|
511
75
|
chalk.dim('\nType a message Ā· ') + chalk.cyan('@filename') + chalk.dim(' to attach Ā· ctrl+q to cancel\n');
|
|
512
76
|
|
|
513
|
-
/**
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
try {
|
|
522
|
-
mkdirSync(resolve(process.cwd(), step.path), { recursive: true });
|
|
523
|
-
console.log(chalk.green(` ā created ${step.path}\n`));
|
|
524
|
-
} catch (err) {
|
|
525
|
-
console.log(chalk.red(` ā ${err.message}\n`));
|
|
526
|
-
return false;
|
|
527
|
-
}
|
|
528
|
-
} else if (step.type === 'cd') {
|
|
529
|
-
try {
|
|
530
|
-
process.chdir(resolve(process.cwd(), step.path));
|
|
531
|
-
console.log(chalk.dim(` š ${process.cwd()}\n`));
|
|
532
|
-
} catch (err) {
|
|
533
|
-
console.log(chalk.red(` ā no such directory: ${step.path}\n`));
|
|
534
|
-
return false;
|
|
535
|
-
}
|
|
536
|
-
} else if (step.type === 'run') {
|
|
537
|
-
process.stdout.write(chalk.dim(` running: ${step.cmd}\n`));
|
|
538
|
-
const { stdout, stderr, exitCode } = await execCommand(step.cmd);
|
|
539
|
-
const output = [stdout, stderr].filter(Boolean).join('\n').trim();
|
|
540
|
-
if (output) console.log(chalk.dim(output));
|
|
541
|
-
if (exitCode !== 0) {
|
|
542
|
-
console.log(chalk.red(` ā command failed (exit ${exitCode})\n`));
|
|
543
|
-
return false;
|
|
544
|
-
}
|
|
545
|
-
console.log(chalk.green(` ā done\n`));
|
|
546
|
-
} else if (step.type === 'write') {
|
|
547
|
-
process.stdout.write(chalk.dim(` write: ${step.path}\n`));
|
|
548
|
-
try {
|
|
549
|
-
const abs = resolve(process.cwd(), step.path);
|
|
550
|
-
const dir = abs.split('/').slice(0, -1).join('/');
|
|
551
|
-
mkdirSync(dir, { recursive: true });
|
|
552
|
-
writeFileSync(abs, step.content);
|
|
553
|
-
console.log(chalk.green(` ā wrote ${step.path}\n`));
|
|
554
|
-
} catch (err) {
|
|
555
|
-
console.log(chalk.red(` ā ${err.message}\n`));
|
|
556
|
-
return false;
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
return true;
|
|
77
|
+
/** If MARKOV_DEBUG is set, print the raw model output after completion. */
|
|
78
|
+
function maybePrintRawModelOutput(rawText) {
|
|
79
|
+
if (!process.env.MARKOV_DEBUG || rawText == null) return;
|
|
80
|
+
const text = typeof rawText === 'string' ? rawText : String(rawText);
|
|
81
|
+
if (!text.trim()) return;
|
|
82
|
+
console.log(chalk.dim('\n--- MARKOV_DEBUG: raw model output ---'));
|
|
83
|
+
console.log(text);
|
|
84
|
+
console.log(chalk.dim('--- end raw output ---\n'));
|
|
561
85
|
}
|
|
562
86
|
|
|
563
87
|
export async function startInteractive() {
|
|
@@ -566,7 +90,7 @@ export async function startInteractive() {
|
|
|
566
90
|
let allFiles = getFilesAndDirs();
|
|
567
91
|
const chatMessages = [];
|
|
568
92
|
|
|
569
|
-
console.log(chalk.dim(`Chat with Markov (${
|
|
93
|
+
console.log(chalk.dim(`Chat with Markov (${getModelDisplayName()}).`));
|
|
570
94
|
console.log(INTRO_TEXT);
|
|
571
95
|
|
|
572
96
|
if (!getToken()) {
|
|
@@ -575,6 +99,7 @@ export async function startInteractive() {
|
|
|
575
99
|
|
|
576
100
|
let pendingMessage = null;
|
|
577
101
|
let lastPlan = null;
|
|
102
|
+
const inputHistory = [];
|
|
578
103
|
|
|
579
104
|
while (true) {
|
|
580
105
|
let raw;
|
|
@@ -583,7 +108,7 @@ export async function startInteractive() {
|
|
|
583
108
|
pendingMessage = null;
|
|
584
109
|
console.log(chalk.magenta('you> ') + raw + '\n');
|
|
585
110
|
} else {
|
|
586
|
-
raw = await chatPrompt(chalk.magenta('you> '), allFiles);
|
|
111
|
+
raw = await chatPrompt(chalk.magenta('you> '), allFiles, inputHistory);
|
|
587
112
|
}
|
|
588
113
|
if (raw === null) continue;
|
|
589
114
|
const trimmed = raw.trim();
|
|
@@ -610,6 +135,14 @@ export async function startInteractive() {
|
|
|
610
135
|
continue;
|
|
611
136
|
}
|
|
612
137
|
|
|
138
|
+
// /clear ā clear chat history and stored plan
|
|
139
|
+
if (trimmed === '/clear') {
|
|
140
|
+
chatMessages.length = 0;
|
|
141
|
+
lastPlan = null;
|
|
142
|
+
console.log(chalk.green('ā Chat and context cleared.\n'));
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
613
146
|
// /intro ā show quick start (same as on first load)
|
|
614
147
|
if (trimmed === '/intro') {
|
|
615
148
|
console.log(INTRO_TEXT);
|
|
@@ -634,6 +167,27 @@ export async function startInteractive() {
|
|
|
634
167
|
continue;
|
|
635
168
|
}
|
|
636
169
|
|
|
170
|
+
// /env ā show which .env vars are loaded (for debugging)
|
|
171
|
+
if (trimmed === '/env') {
|
|
172
|
+
const mask = (v) => (v && v.length > 8 ? v.slice(0, 8) + 'ā¦' + v.slice(-4) : v ? '***' : null);
|
|
173
|
+
const vars = [
|
|
174
|
+
['ANTHROPIC_API_KEY', process.env.ANTHROPIC_API_KEY],
|
|
175
|
+
['ANTHROPIC_MODEL', process.env.ANTHROPIC_MODEL],
|
|
176
|
+
['OPENAI_API_KEY', process.env.OPENAI_API_KEY],
|
|
177
|
+
['OPENAI_MODEL', process.env.OPENAI_MODEL],
|
|
178
|
+
['MARKOV_SEARCH_API_KEY', process.env.MARKOV_SEARCH_API_KEY],
|
|
179
|
+
['MARKOV_DEBUG', process.env.MARKOV_DEBUG],
|
|
180
|
+
];
|
|
181
|
+
console.log(chalk.dim('\nEnvironment (from .env or shell):\n'));
|
|
182
|
+
for (const [name, val] of vars) {
|
|
183
|
+
const status = val ? chalk.green('set') : chalk.red('not set');
|
|
184
|
+
const preview = val ? chalk.dim(' ' + mask(val)) : '';
|
|
185
|
+
console.log(chalk.dim(' ') + name + chalk.dim(': ') + status + preview);
|
|
186
|
+
}
|
|
187
|
+
console.log('');
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
637
191
|
// /plan [prompt] ā stream a plan (no tools), store as lastPlan
|
|
638
192
|
if (trimmed === '/plan' || trimmed.startsWith('/plan ')) {
|
|
639
193
|
const rawUserContent = trimmed.startsWith('/plan ')
|
|
@@ -643,6 +197,7 @@ export async function startInteractive() {
|
|
|
643
197
|
console.log(chalk.yellow('No prompt given.\n'));
|
|
644
198
|
continue;
|
|
645
199
|
}
|
|
200
|
+
console.log(chalk.dim('You: ') + rawUserContent);
|
|
646
201
|
const userContent = (await getLsContext()) + (await getGrepContext()) + 'Create a step-by-step plan for: ' + rawUserContent;
|
|
647
202
|
chatMessages.push({ role: 'user', content: userContent });
|
|
648
203
|
const planMessages = [buildPlanSystemMessage(), ...chatMessages];
|
|
@@ -666,31 +221,69 @@ export async function startInteractive() {
|
|
|
666
221
|
}
|
|
667
222
|
};
|
|
668
223
|
try {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
process.stdout.write(
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
224
|
+
let currentPlanMessages = planMessages;
|
|
225
|
+
let fullPlanText = '';
|
|
226
|
+
const planMaxIter = 10;
|
|
227
|
+
for (let planIter = 0; planIter < planMaxIter; planIter++) {
|
|
228
|
+
const { content, toolCalls, usage } = await streamChatWithTools(
|
|
229
|
+
currentPlanMessages,
|
|
230
|
+
PLAN_YOLO_TOOLS,
|
|
231
|
+
MODEL,
|
|
232
|
+
{
|
|
233
|
+
think: true, // plan mode only: request thinking from backend
|
|
234
|
+
onContent: (token) => {
|
|
235
|
+
if (firstContent) {
|
|
236
|
+
clearPlanSpinner();
|
|
237
|
+
firstContent = false;
|
|
238
|
+
}
|
|
239
|
+
process.stdout.write(token);
|
|
240
|
+
},
|
|
241
|
+
onThinking: (token) => {
|
|
242
|
+
if (!thinkingStarted) {
|
|
243
|
+
clearPlanSpinner();
|
|
244
|
+
process.stdout.write(chalk.dim('Thinking: '));
|
|
245
|
+
thinkingStarted = true;
|
|
246
|
+
}
|
|
247
|
+
process.stdout.write(chalk.dim(token));
|
|
248
|
+
},
|
|
688
249
|
},
|
|
250
|
+
planAbort.signal,
|
|
251
|
+
null
|
|
252
|
+
);
|
|
253
|
+
fullPlanText = content ?? '';
|
|
254
|
+
if (!toolCalls?.length) {
|
|
255
|
+
printTokenUsage(usage);
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
currentPlanMessages = [...currentPlanMessages, { role: 'assistant', content: content ?? '', tool_calls: toolCalls }];
|
|
259
|
+
for (const tc of toolCalls) {
|
|
260
|
+
const name = tc?.function?.name ?? 'unknown';
|
|
261
|
+
let args = tc?.function?.arguments;
|
|
262
|
+
if (typeof args === 'string') {
|
|
263
|
+
try {
|
|
264
|
+
args = JSON.parse(args);
|
|
265
|
+
} catch {
|
|
266
|
+
currentPlanMessages.push({ role: 'tool', tool_name: name, content: JSON.stringify({ error: 'Invalid JSON in arguments' }) });
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
console.log(chalk.cyan('\n ā¶ ') + chalk.bold(name) + chalk.dim(' ') + (name === 'web_search' ? (args?.query ?? '') : (args?.command ?? '')));
|
|
271
|
+
const result = await runTool(name, args ?? {}, { cwd: process.cwd(), confirmFn: () => Promise.resolve(true) });
|
|
272
|
+
currentPlanMessages.push({
|
|
273
|
+
role: 'tool',
|
|
274
|
+
tool_name: name,
|
|
275
|
+
content: typeof result === 'string' ? result : JSON.stringify(result),
|
|
276
|
+
});
|
|
689
277
|
}
|
|
690
|
-
|
|
278
|
+
}
|
|
691
279
|
chatMessages.push({ role: 'assistant', content: fullPlanText });
|
|
692
|
-
|
|
693
|
-
|
|
280
|
+
// Store only plan (stream content), not thinking; write to plan.md for /build.
|
|
281
|
+
if ((fullPlanText ?? '').trim()) {
|
|
282
|
+
lastPlan = fullPlanText;
|
|
283
|
+
writeFileSync(getPlanPath(), fullPlanText.trim(), 'utf-8');
|
|
284
|
+
}
|
|
285
|
+
console.log('\n' + chalk.dim('Plan saved to plan.md. Use ') + chalk.green('/build') + chalk.dim(' to execute.\n'));
|
|
286
|
+
maybePrintRawModelOutput(fullPlanText);
|
|
694
287
|
} catch (err) {
|
|
695
288
|
if (planSpinner) {
|
|
696
289
|
clearInterval(planSpinner);
|
|
@@ -711,6 +304,7 @@ export async function startInteractive() {
|
|
|
711
304
|
console.log(chalk.yellow('No prompt given.\n'));
|
|
712
305
|
continue;
|
|
713
306
|
}
|
|
307
|
+
console.log(chalk.dim('You: ') + rawUserContent);
|
|
714
308
|
const planUserContent = (await getLsContext()) + (await getGrepContext()) + 'Create a step-by-step plan for: ' + rawUserContent;
|
|
715
309
|
chatMessages.push({ role: 'user', content: planUserContent });
|
|
716
310
|
const planMessages = [buildPlanSystemMessage(), ...chatMessages];
|
|
@@ -719,31 +313,65 @@ export async function startInteractive() {
|
|
|
719
313
|
let thinkingStarted = false;
|
|
720
314
|
let fullPlanText = '';
|
|
721
315
|
try {
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
316
|
+
let currentPlanMessages = planMessages;
|
|
317
|
+
const yoloPlanMaxIter = 10;
|
|
318
|
+
for (let planIter = 0; planIter < yoloPlanMaxIter; planIter++) {
|
|
319
|
+
const { content: yoloPlanContent, toolCalls, usage } = await streamChatWithTools(
|
|
320
|
+
currentPlanMessages,
|
|
321
|
+
PLAN_YOLO_TOOLS,
|
|
322
|
+
MODEL,
|
|
323
|
+
{
|
|
324
|
+
think: true, // plan phase: request thinking from backend
|
|
325
|
+
onContent: (token) => process.stdout.write(token),
|
|
326
|
+
onThinking: (token) => {
|
|
327
|
+
if (!thinkingStarted) {
|
|
328
|
+
process.stdout.write(chalk.dim('Thinking: '));
|
|
329
|
+
thinkingStarted = true;
|
|
330
|
+
}
|
|
331
|
+
process.stdout.write(chalk.dim(token));
|
|
332
|
+
},
|
|
734
333
|
},
|
|
334
|
+
yoloAbort.signal,
|
|
335
|
+
null
|
|
336
|
+
);
|
|
337
|
+
fullPlanText = yoloPlanContent ?? '';
|
|
338
|
+
if (!toolCalls?.length) {
|
|
339
|
+
printTokenUsage(usage);
|
|
340
|
+
break;
|
|
735
341
|
}
|
|
736
|
-
|
|
342
|
+
currentPlanMessages = [...currentPlanMessages, { role: 'assistant', content: yoloPlanContent ?? '', tool_calls: toolCalls }];
|
|
343
|
+
for (const tc of toolCalls) {
|
|
344
|
+
const name = tc?.function?.name ?? 'unknown';
|
|
345
|
+
let args = tc?.function?.arguments;
|
|
346
|
+
if (typeof args === 'string') {
|
|
347
|
+
try {
|
|
348
|
+
args = JSON.parse(args);
|
|
349
|
+
} catch {
|
|
350
|
+
currentPlanMessages.push({ role: 'tool', tool_name: name, content: JSON.stringify({ error: 'Invalid JSON in arguments' }) });
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
console.log(chalk.cyan('\n ā¶ ') + chalk.bold(name) + chalk.dim(' ') + (name === 'web_search' ? (args?.query ?? '') : (args?.command ?? '')));
|
|
355
|
+
const result = await runTool(name, args ?? {}, { cwd: process.cwd(), confirmFn: () => Promise.resolve(true) });
|
|
356
|
+
currentPlanMessages.push({
|
|
357
|
+
role: 'tool',
|
|
358
|
+
tool_name: name,
|
|
359
|
+
content: typeof result === 'string' ? result : JSON.stringify(result),
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
}
|
|
737
363
|
chatMessages.push({ role: 'assistant', content: fullPlanText });
|
|
738
|
-
|
|
364
|
+
// Store only plan (stream content), not thinking, so build phase uses exactly this.
|
|
365
|
+
if ((fullPlanText ?? '').trim()) lastPlan = fullPlanText;
|
|
366
|
+
maybePrintRawModelOutput(fullPlanText);
|
|
739
367
|
} catch (err) {
|
|
740
368
|
if (!yoloAbort.signal?.aborted) console.log(chalk.red(`\n${err.message}\n`));
|
|
741
369
|
continue;
|
|
742
370
|
}
|
|
743
371
|
const buildContent = (await getLsContext()) + (await getGrepContext()) +
|
|
744
|
-
'
|
|
372
|
+
'\n\nPlan:\n' + lastPlan + '\n\nExecute this plan using your tools. Run commands and edit files as needed.';
|
|
745
373
|
chatMessages.push({ role: 'user', content: buildContent });
|
|
746
|
-
const agentMessages = [buildAgentSystemMessage(),
|
|
374
|
+
const agentMessages = [buildAgentSystemMessage(), { role: 'user', content: buildContent }];
|
|
747
375
|
maybePrintFullPayload(agentMessages);
|
|
748
376
|
const abortController = new AbortController();
|
|
749
377
|
const confirmFn = () => Promise.resolve(true);
|
|
@@ -782,7 +410,7 @@ export async function startInteractive() {
|
|
|
782
410
|
},
|
|
783
411
|
onToolCall: (name, args) => {
|
|
784
412
|
const summary = formatToolCallSummary(name, args);
|
|
785
|
-
console.log(chalk.cyan(' ā¶ ') + chalk.bold(name) + chalk.dim(' ') + chalk.white(summary));
|
|
413
|
+
console.log(chalk.cyan('\n ā¶ ') + chalk.bold(name) + chalk.dim(' ') + chalk.white(summary));
|
|
786
414
|
},
|
|
787
415
|
onToolResult: (name, resultStr) => {
|
|
788
416
|
console.log(chalk.dim(' ') + formatToolResultSummary(name, resultStr));
|
|
@@ -797,11 +425,7 @@ export async function startInteractive() {
|
|
|
797
425
|
await applyCodeBlockEdits(result.content, []);
|
|
798
426
|
allFiles = getFilesAndDirs();
|
|
799
427
|
console.log(chalk.green(`ā Yolo done.`) + chalk.dim(` (${elapsed}s)\n`));
|
|
800
|
-
|
|
801
|
-
if (opts1.length >= 2) {
|
|
802
|
-
const chosen = await selectFrom(opts1, 'Select an option:');
|
|
803
|
-
if (chosen) pendingMessage = chosen;
|
|
804
|
-
}
|
|
428
|
+
maybePrintRawModelOutput(result.content);
|
|
805
429
|
}
|
|
806
430
|
} catch (err) {
|
|
807
431
|
stopSpinner();
|
|
@@ -810,16 +434,119 @@ export async function startInteractive() {
|
|
|
810
434
|
continue;
|
|
811
435
|
}
|
|
812
436
|
|
|
813
|
-
// /
|
|
437
|
+
// /init [prompt] ā create markov.md with project summary (agent writes file via tools)
|
|
438
|
+
if (trimmed === '/init' || trimmed.startsWith('/init ')) {
|
|
439
|
+
const rawUserContent = trimmed.startsWith('/init ')
|
|
440
|
+
? trimmed.slice(6).trim()
|
|
441
|
+
: (await promptLine(chalk.bold('Describe the project to summarize (optional): '))).trim();
|
|
442
|
+
const userContent = (await getLsContext()) + (await getGrepContext()) +
|
|
443
|
+
(rawUserContent ? `Create markov.md with a project summary. Focus on: ${rawUserContent}` : 'Create markov.md with a concise project summary.');
|
|
444
|
+
const initMessages = [buildInitSystemMessage(), { role: 'user', content: userContent }];
|
|
445
|
+
const initAbort = new AbortController();
|
|
446
|
+
process.stdout.write(chalk.dim('\nInit āŗ '));
|
|
447
|
+
const DOTS = ['.', '..', '...'];
|
|
448
|
+
let dotIdx = 0;
|
|
449
|
+
const initStartTime = Date.now();
|
|
450
|
+
let initSpinner = setInterval(() => {
|
|
451
|
+
const elapsed = ((Date.now() - initStartTime) / 1000).toFixed(1);
|
|
452
|
+
process.stdout.write('\r' + chalk.dim('Init āŗ ') + chalk.dim(elapsed + 's ') + agentGradient(DOTS[dotIdx % DOTS.length]) + ' ');
|
|
453
|
+
dotIdx++;
|
|
454
|
+
}, 400);
|
|
455
|
+
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
|
+
try {
|
|
465
|
+
let currentInitMessages = initMessages;
|
|
466
|
+
const initMaxIter = 10;
|
|
467
|
+
for (let initIter = 0; initIter < initMaxIter; initIter++) {
|
|
468
|
+
const { content, toolCalls, usage } = await streamChatWithTools(
|
|
469
|
+
currentInitMessages,
|
|
470
|
+
PLAN_YOLO_TOOLS,
|
|
471
|
+
MODEL,
|
|
472
|
+
{
|
|
473
|
+
think: true,
|
|
474
|
+
onContent: (token) => {
|
|
475
|
+
if (firstContent) {
|
|
476
|
+
clearInitSpinner();
|
|
477
|
+
firstContent = false;
|
|
478
|
+
}
|
|
479
|
+
process.stdout.write(token);
|
|
480
|
+
},
|
|
481
|
+
onThinking: (token) => {
|
|
482
|
+
if (!thinkingStarted) {
|
|
483
|
+
clearInitSpinner();
|
|
484
|
+
process.stdout.write(chalk.dim('Thinking: '));
|
|
485
|
+
thinkingStarted = true;
|
|
486
|
+
}
|
|
487
|
+
process.stdout.write(chalk.dim(token));
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
initAbort.signal,
|
|
491
|
+
null
|
|
492
|
+
);
|
|
493
|
+
if (!toolCalls?.length) {
|
|
494
|
+
printTokenUsage(usage);
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
currentInitMessages = [...currentInitMessages, { role: 'assistant', content: content ?? '', tool_calls: toolCalls }];
|
|
498
|
+
for (const tc of toolCalls) {
|
|
499
|
+
const name = tc?.function?.name ?? 'unknown';
|
|
500
|
+
let args = tc?.function?.arguments;
|
|
501
|
+
if (typeof args === 'string') {
|
|
502
|
+
try {
|
|
503
|
+
args = JSON.parse(args);
|
|
504
|
+
} catch {
|
|
505
|
+
currentInitMessages.push({ role: 'tool', tool_name: name, content: JSON.stringify({ error: 'Invalid JSON in arguments' }) });
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
console.log(chalk.cyan('\n ā¶ ') + chalk.bold(name) + chalk.dim(' ') + (name === 'web_search' ? (args?.query ?? '') : (args?.command ?? '')));
|
|
510
|
+
const result = await runTool(name, args ?? {}, { cwd: process.cwd(), confirmFn: () => Promise.resolve(true) });
|
|
511
|
+
currentInitMessages.push({
|
|
512
|
+
role: 'tool',
|
|
513
|
+
tool_name: name,
|
|
514
|
+
content: typeof result === 'string' ? result : JSON.stringify(result),
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (existsSync(getMarkovPath())) {
|
|
519
|
+
console.log('\n' + chalk.green('ā markov.md created. It will be included in the system message from now on.\n'));
|
|
520
|
+
} else {
|
|
521
|
+
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
|
+
}
|
|
523
|
+
} catch (err) {
|
|
524
|
+
if (initSpinner) {
|
|
525
|
+
clearInterval(initSpinner);
|
|
526
|
+
initSpinner = null;
|
|
527
|
+
process.stdout.write('\r\x1b[0J');
|
|
528
|
+
}
|
|
529
|
+
if (!initAbort.signal?.aborted) console.log(chalk.red(`\n${err.message}\n`));
|
|
530
|
+
}
|
|
531
|
+
continue;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// /build ā execute plan from plan.md only
|
|
814
535
|
if (trimmed === '/build') {
|
|
815
|
-
|
|
816
|
-
|
|
536
|
+
const planPath = getPlanPath();
|
|
537
|
+
if (!existsSync(planPath)) {
|
|
538
|
+
console.log(chalk.yellow('plan.md not found. Run /plan first to create a plan.\n'));
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
const planToUse = readFileSync(planPath, 'utf-8').trim();
|
|
542
|
+
if (!planToUse) {
|
|
543
|
+
console.log(chalk.yellow('plan.md is empty. Run /plan first to create a plan.\n'));
|
|
817
544
|
continue;
|
|
818
545
|
}
|
|
819
546
|
const buildContent = (await getLsContext()) + (await getGrepContext()) +
|
|
820
|
-
'
|
|
547
|
+
'\n\nPlan:\n' + planToUse + '\n\nExecute this plan using your tools. Run commands and edit files as needed. ';
|
|
821
548
|
chatMessages.push({ role: 'user', content: buildContent });
|
|
822
|
-
const agentMessages = [buildAgentSystemMessage(),
|
|
549
|
+
const agentMessages = [buildAgentSystemMessage(), { role: 'user', content: buildContent }];
|
|
823
550
|
maybePrintFullPayload(agentMessages);
|
|
824
551
|
const abortController = new AbortController();
|
|
825
552
|
const confirmFn = (cmd) => confirm(chalk.bold(`Run: ${cmd}? [y/N] `));
|
|
@@ -863,7 +590,7 @@ export async function startInteractive() {
|
|
|
863
590
|
},
|
|
864
591
|
onToolCall: (name, args) => {
|
|
865
592
|
const summary = formatToolCallSummary(name, args);
|
|
866
|
-
console.log(chalk.cyan(' ā¶ ') + chalk.bold(name) + chalk.dim(' ') + chalk.white(summary));
|
|
593
|
+
console.log(chalk.cyan('\n ā¶ ') + chalk.bold(name) + chalk.dim(' ') + chalk.white(summary));
|
|
867
594
|
},
|
|
868
595
|
onToolResult: (name, resultStr) => {
|
|
869
596
|
console.log(chalk.dim(' ') + formatToolResultSummary(name, resultStr));
|
|
@@ -878,11 +605,7 @@ export async function startInteractive() {
|
|
|
878
605
|
await applyCodeBlockEdits(result.content, []);
|
|
879
606
|
allFiles = getFilesAndDirs();
|
|
880
607
|
console.log(chalk.green(`ā Build done.`) + chalk.dim(` (${elapsed}s)\n`));
|
|
881
|
-
|
|
882
|
-
if (opts1.length >= 2) {
|
|
883
|
-
const chosen = await selectFrom(opts1, 'Select an option:');
|
|
884
|
-
if (chosen) pendingMessage = chosen;
|
|
885
|
-
}
|
|
608
|
+
maybePrintRawModelOutput(result.content);
|
|
886
609
|
}
|
|
887
610
|
} catch (err) {
|
|
888
611
|
stopSpinner();
|
|
@@ -891,12 +614,40 @@ export async function startInteractive() {
|
|
|
891
614
|
continue;
|
|
892
615
|
}
|
|
893
616
|
|
|
894
|
-
// /models ā pick active model
|
|
617
|
+
// /models ā pick active model (Claude or Ollama)
|
|
895
618
|
if (trimmed === '/models') {
|
|
896
|
-
const
|
|
619
|
+
const labels = MODEL_OPTIONS.map((o) => o.label);
|
|
620
|
+
const chosen = await selectFrom(labels, 'Select model:');
|
|
897
621
|
if (chosen) {
|
|
898
|
-
|
|
899
|
-
|
|
622
|
+
const opt = MODEL_OPTIONS.find((o) => o.label === chosen);
|
|
623
|
+
if (opt) {
|
|
624
|
+
if (opt.provider === 'claude' && !getClaudeKey()) {
|
|
625
|
+
const enteredKey = (await promptSecret('Claude API key (paste then Enter): ')).trim();
|
|
626
|
+
if (!enteredKey) {
|
|
627
|
+
console.log(chalk.yellow('\nNo key entered. Model not switched.\n'));
|
|
628
|
+
continue;
|
|
629
|
+
}
|
|
630
|
+
setClaudeKey(enteredKey);
|
|
631
|
+
}
|
|
632
|
+
if (opt.provider === 'openai' && !getOpenAIKey()) {
|
|
633
|
+
const enteredKey = (await promptSecret('OpenAI API key (paste then Enter): ')).trim();
|
|
634
|
+
if (!enteredKey) {
|
|
635
|
+
console.log(chalk.yellow('\nNo key entered. Model not switched.\n'));
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
setOpenAIKey(enteredKey);
|
|
639
|
+
}
|
|
640
|
+
if (opt.provider === 'ollama' && opt.model.endsWith('-cloud') && !getOllamaKey()) {
|
|
641
|
+
const enteredKey = (await promptSecret('Ollama API key for cloud models (paste then Enter): ')).trim();
|
|
642
|
+
if (!enteredKey) {
|
|
643
|
+
console.log(chalk.yellow('\nNo key entered. Model not switched.\n'));
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
setOllamaKey(enteredKey);
|
|
647
|
+
}
|
|
648
|
+
setModelAndProvider(opt.provider, opt.model);
|
|
649
|
+
console.log(chalk.dim(`\nš¤ switched to ${chalk.cyan(getModelDisplayName())}\n`));
|
|
650
|
+
}
|
|
900
651
|
}
|
|
901
652
|
continue;
|
|
902
653
|
}
|
|
@@ -942,33 +693,94 @@ export async function startInteractive() {
|
|
|
942
693
|
continue;
|
|
943
694
|
}
|
|
944
695
|
|
|
945
|
-
// /setup-nextjs ā scaffold a Next.js app
|
|
696
|
+
// /setup-nextjs ā scaffold a Next.js app
|
|
946
697
|
if (trimmed === '/setup-nextjs') {
|
|
947
|
-
const
|
|
948
|
-
const steps = [
|
|
949
|
-
{ type: 'mkdir', path: name },
|
|
950
|
-
{ type: 'cd', path: name },
|
|
951
|
-
{ type: 'run', cmd: 'npx create-next-app@latest . --yes' },
|
|
952
|
-
{ type: 'run', cmd: 'npm install sass' },
|
|
953
|
-
];
|
|
954
|
-
const ok = await runSetupSteps(steps);
|
|
698
|
+
const ok = await runSetupSteps(NEXTJS_STEPS);
|
|
955
699
|
allFiles = getFilesAndDirs();
|
|
956
|
-
if (ok) console.log(chalk.green(
|
|
700
|
+
if (ok) console.log(chalk.green('ā Next.js app created.\n'));
|
|
957
701
|
continue;
|
|
958
702
|
}
|
|
959
703
|
|
|
960
|
-
// /setup-
|
|
704
|
+
// /setup-tanstack ā scaffold a TanStack Start app
|
|
705
|
+
if (trimmed === '/setup-tanstack') {
|
|
706
|
+
const ok = await runSetupSteps(TANSTACK_STEPS);
|
|
707
|
+
allFiles = getFilesAndDirs();
|
|
708
|
+
if (ok) console.log(chalk.green('ā TanStack Start app created.\n'));
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// /setup-laravel ā scaffold a Laravel API
|
|
961
713
|
if (trimmed === '/setup-laravel') {
|
|
962
|
-
const
|
|
963
|
-
const steps = [
|
|
964
|
-
{ type: 'mkdir', path: name },
|
|
965
|
-
{ type: 'cd', path: name },
|
|
966
|
-
{ type: 'run', cmd: 'composer create-project --prefer-dist laravel/laravel .' },
|
|
967
|
-
{ type: 'run', cmd: 'php artisan serve' },
|
|
968
|
-
];
|
|
969
|
-
const ok = await runSetupSteps(steps);
|
|
714
|
+
const ok = await runSetupSteps(LARAVEL_STEPS);
|
|
970
715
|
allFiles = getFilesAndDirs();
|
|
971
|
-
if (ok) console.log(chalk.green(
|
|
716
|
+
if (ok) console.log(chalk.green('ā Laravel API created.\n'));
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// /laravel ā run agent with Laravel "my-blog" blog setup prompt (auto-confirm like /yolo)
|
|
721
|
+
if (trimmed === '/laravel') {
|
|
722
|
+
const userContent = (await getLsContext()) + (await getGrepContext()) + LARAVEL_BLOG_PROMPT;
|
|
723
|
+
chatMessages.push({ role: 'user', content: userContent });
|
|
724
|
+
const agentMessages = [buildAgentSystemMessage(), ...chatMessages];
|
|
725
|
+
maybePrintFullPayload(agentMessages);
|
|
726
|
+
const abortController = new AbortController();
|
|
727
|
+
const confirmFn = () => Promise.resolve(true);
|
|
728
|
+
const confirmFileEdit = async () => true;
|
|
729
|
+
const startTime = Date.now();
|
|
730
|
+
const DOTS = ['.', '..', '...'];
|
|
731
|
+
let dotIdx = 0;
|
|
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
|
+
};
|
|
747
|
+
try {
|
|
748
|
+
const result = await runAgentLoop(agentMessages, {
|
|
749
|
+
signal: abortController.signal,
|
|
750
|
+
cwd: process.cwd(),
|
|
751
|
+
confirmFn,
|
|
752
|
+
confirmFileEdit,
|
|
753
|
+
onThinking: () => { startSpinner(); },
|
|
754
|
+
onBeforeToolRun: () => { stopSpinner(); },
|
|
755
|
+
onIteration: (iter, max, toolCount) => {
|
|
756
|
+
const w = process.stdout.columns || 80;
|
|
757
|
+
const label = ` Step ${iter} `;
|
|
758
|
+
const line = chalk.dim('āā') + chalk.bold.white(label) + chalk.dim('ā'.repeat(Math.max(0, w - label.length - 2)));
|
|
759
|
+
console.log(line);
|
|
760
|
+
},
|
|
761
|
+
onToolCall: (name, args) => {
|
|
762
|
+
const summary = formatToolCallSummary(name, args);
|
|
763
|
+
console.log(chalk.cyan('\n ā¶ ') + chalk.bold(name) + chalk.dim(' ') + chalk.white(summary));
|
|
764
|
+
},
|
|
765
|
+
onToolResult: (name, resultStr) => {
|
|
766
|
+
console.log(chalk.dim(' ') + formatToolResultSummary(name, resultStr));
|
|
767
|
+
},
|
|
768
|
+
});
|
|
769
|
+
stopSpinner();
|
|
770
|
+
if (result) {
|
|
771
|
+
chatMessages.push(result.finalMessage);
|
|
772
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
773
|
+
const width = Math.min(process.stdout.columns || 80, 80);
|
|
774
|
+
process.stdout.write(formatResponseWithCodeBlocks(result.content, width) + '\n\n');
|
|
775
|
+
await applyCodeBlockEdits(result.content, []);
|
|
776
|
+
allFiles = getFilesAndDirs();
|
|
777
|
+
console.log(chalk.green('ā Laravel blog setup done.') + chalk.dim(` (${elapsed}s)\n`));
|
|
778
|
+
maybePrintRawModelOutput(result.content);
|
|
779
|
+
}
|
|
780
|
+
} catch (err) {
|
|
781
|
+
stopSpinner();
|
|
782
|
+
if (!abortController.signal?.aborted) console.log(chalk.red(`\n${err.message}\n`));
|
|
783
|
+
}
|
|
972
784
|
continue;
|
|
973
785
|
}
|
|
974
786
|
|
|
@@ -981,6 +793,7 @@ export async function startInteractive() {
|
|
|
981
793
|
console.log(chalk.yellow(`\nā not found: ${failed.map(f => `@${f}`).join(', ')}`));
|
|
982
794
|
}
|
|
983
795
|
const rawUserContent = resolvedContent ?? trimmed;
|
|
796
|
+
console.log(chalk.dim('You: ') + rawUserContent);
|
|
984
797
|
const userContent = (await getLsContext()) + (await getGrepContext()) + rawUserContent;
|
|
985
798
|
chatMessages.push({ role: 'user', content: userContent });
|
|
986
799
|
|
|
@@ -1034,7 +847,7 @@ export async function startInteractive() {
|
|
|
1034
847
|
},
|
|
1035
848
|
onToolCall: (name, args) => {
|
|
1036
849
|
const summary = formatToolCallSummary(name, args);
|
|
1037
|
-
console.log(chalk.cyan(' ā¶ ') + chalk.bold(name) + chalk.dim(' ') + chalk.white(summary));
|
|
850
|
+
console.log(chalk.cyan('\n ā¶ ') + chalk.bold(name) + chalk.dim(' ') + chalk.white(summary));
|
|
1038
851
|
},
|
|
1039
852
|
onToolResult: (name, resultStr) => {
|
|
1040
853
|
console.log(chalk.dim(' ') + formatToolResultSummary(name, resultStr));
|
|
@@ -1049,11 +862,7 @@ export async function startInteractive() {
|
|
|
1049
862
|
await applyCodeBlockEdits(result.content, loaded);
|
|
1050
863
|
allFiles = getFilesAndDirs();
|
|
1051
864
|
console.log(chalk.green(`ā Agent done.`) + chalk.dim(` (${elapsed}s)\n`));
|
|
1052
|
-
|
|
1053
|
-
if (opts2.length >= 2) {
|
|
1054
|
-
const chosen = await selectFrom(opts2, 'Select an option:');
|
|
1055
|
-
if (chosen) pendingMessage = chosen;
|
|
1056
|
-
}
|
|
865
|
+
maybePrintRawModelOutput(result.content);
|
|
1057
866
|
}
|
|
1058
867
|
} catch (err) {
|
|
1059
868
|
stopSpinner();
|