markov-cli 1.0.8 → 1.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/interactive.js +351 -70
- package/src/ollama.js +11 -5
package/package.json
CHANGED
package/src/interactive.js
CHANGED
|
@@ -5,7 +5,7 @@ import { homedir } from 'os';
|
|
|
5
5
|
import { resolve } from 'path';
|
|
6
6
|
import { mkdirSync, writeFileSync } from 'fs';
|
|
7
7
|
import { printLogo } from './ui/logo.js';
|
|
8
|
-
import { chatWithTools, MODEL, MODELS, setModel } from './ollama.js';
|
|
8
|
+
import { chatWithTools, streamChat, MODEL, MODELS, setModel } from './ollama.js';
|
|
9
9
|
import { resolveFileRefs } from './files.js';
|
|
10
10
|
import { execCommand, AGENT_TOOLS, runTool } from './tools.js';
|
|
11
11
|
import { parseEdits, renderDiff, applyEdit } from './editor.js';
|
|
@@ -230,6 +230,50 @@ async function applyCodeBlockEdits(responseText, loadedFiles = []) {
|
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
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
|
+
|
|
233
277
|
/** Shared system message base: Markov intro, cwd, file list. */
|
|
234
278
|
function getSystemMessageBase() {
|
|
235
279
|
const files = getFiles();
|
|
@@ -237,6 +281,15 @@ function getSystemMessageBase() {
|
|
|
237
281
|
return `You are Markov, an AI coding assistant.\nWorking directory: ${process.cwd()}\n${fileList}`;
|
|
238
282
|
}
|
|
239
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
|
+
}
|
|
292
|
+
|
|
240
293
|
/** System message for agent mode: tool-only instructions (no !!run / !!write). */
|
|
241
294
|
function buildAgentSystemMessage() {
|
|
242
295
|
const toolInstructions =
|
|
@@ -248,14 +301,29 @@ function buildAgentSystemMessage() {
|
|
|
248
301
|
`- search_replace: replace first occurrence of text in a file.\n` +
|
|
249
302
|
`- delete_file: delete a file.\n` +
|
|
250
303
|
`- list_dir: list directory contents (path optional, defaults to current dir).\n` +
|
|
251
|
-
`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), call
|
|
252
|
-
`
|
|
253
|
-
`
|
|
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`;
|
|
254
308
|
return { role: 'system', content: getSystemMessageBase() + toolInstructions };
|
|
255
309
|
}
|
|
256
310
|
|
|
257
311
|
const AGENT_LOOP_MAX_ITERATIONS = 20;
|
|
258
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
|
+
|
|
259
327
|
/** Preview of a file edit for confirmation (write_file / search_replace) */
|
|
260
328
|
function formatFileEditPreview(name, args) {
|
|
261
329
|
const path = args?.path ?? '(no path)';
|
|
@@ -416,13 +484,17 @@ const HELP_TEXT =
|
|
|
416
484
|
'\n' +
|
|
417
485
|
chalk.bold('Commands:\n') +
|
|
418
486
|
chalk.cyan(' /help') + chalk.dim(' show this help\n') +
|
|
419
|
-
chalk.cyan(' /agent') + chalk.dim(' [prompt] run with tools (run commands, create folders)\n') +
|
|
420
487
|
chalk.cyan(' /setup-nextjs') + chalk.dim(' scaffold a Next.js app\n') +
|
|
421
488
|
chalk.cyan(' /setup-laravel') + chalk.dim(' scaffold a Laravel API\n') +
|
|
422
489
|
chalk.cyan(' /models') + chalk.dim(' switch the active AI model\n') +
|
|
423
490
|
chalk.cyan(' /cd [path]') + chalk.dim(' change working directory\n') +
|
|
491
|
+
chalk.cyan(' /cmd [command]') + chalk.dim(' run a shell command in the current folder\n') +
|
|
424
492
|
chalk.cyan(' /login') + chalk.dim(' authenticate with email & password\n') +
|
|
425
493
|
chalk.cyan(' /logout') + chalk.dim(' clear saved auth token\n') +
|
|
494
|
+
chalk.cyan(' /debug') + chalk.dim(' toggle full payload dump (env MARKOV_DEBUG)\n') +
|
|
495
|
+
chalk.cyan(' /plan') + chalk.dim(' [prompt] stream a plan (no tools)\n') +
|
|
496
|
+
chalk.cyan(' /build') + chalk.dim(' execute last plan with tools\n') +
|
|
497
|
+
chalk.cyan(' /yolo') + chalk.dim(' [prompt] plan in stream mode, then auto-run until done\n') +
|
|
426
498
|
chalk.dim('\nType a message · ') + chalk.cyan('@filename') + chalk.dim(' to attach · ctrl+q to cancel\n');
|
|
427
499
|
|
|
428
500
|
/**
|
|
@@ -489,6 +561,7 @@ export async function startInteractive() {
|
|
|
489
561
|
}
|
|
490
562
|
|
|
491
563
|
let pendingMessage = null;
|
|
564
|
+
let lastPlan = null;
|
|
492
565
|
|
|
493
566
|
while (true) {
|
|
494
567
|
let raw;
|
|
@@ -530,73 +603,205 @@ export async function startInteractive() {
|
|
|
530
603
|
continue;
|
|
531
604
|
}
|
|
532
605
|
|
|
533
|
-
// /
|
|
534
|
-
if (trimmed === '/
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
606
|
+
// /debug — toggle full payload dump before each request
|
|
607
|
+
if (trimmed === '/debug') {
|
|
608
|
+
if (process.env.MARKOV_DEBUG) {
|
|
609
|
+
delete process.env.MARKOV_DEBUG;
|
|
610
|
+
console.log(chalk.dim('Debug off — payload dump disabled.\n'));
|
|
611
|
+
} else {
|
|
612
|
+
process.env.MARKOV_DEBUG = '1';
|
|
613
|
+
console.log(chalk.dim('Debug on — full payload (system + messages) will be printed before each request.\n'));
|
|
539
614
|
}
|
|
540
615
|
continue;
|
|
541
616
|
}
|
|
542
617
|
|
|
543
|
-
// /
|
|
544
|
-
if (trimmed.startsWith('/
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
618
|
+
// /plan [prompt] — stream a plan (no tools), store as lastPlan
|
|
619
|
+
if (trimmed === '/plan' || trimmed.startsWith('/plan ')) {
|
|
620
|
+
const rawUserContent = trimmed.startsWith('/plan ')
|
|
621
|
+
? trimmed.slice(6).trim()
|
|
622
|
+
: (await promptLine(chalk.bold('What do you want to plan? '))).trim();
|
|
623
|
+
if (!rawUserContent) {
|
|
624
|
+
console.log(chalk.yellow('No prompt given.\n'));
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
const userContent = (await getLsContext()) + (await getGrepContext()) + 'Create a step-by-step plan for: ' + rawUserContent;
|
|
628
|
+
chatMessages.push({ role: 'user', content: userContent });
|
|
629
|
+
const planMessages = [buildPlanSystemMessage(), ...chatMessages];
|
|
630
|
+
const planAbort = new AbortController();
|
|
631
|
+
process.stdout.write(chalk.dim('\nPlan › '));
|
|
632
|
+
const DOTS = ['.', '..', '...'];
|
|
633
|
+
let dotIdx = 0;
|
|
634
|
+
const planStartTime = Date.now();
|
|
635
|
+
let planSpinner = setInterval(() => {
|
|
636
|
+
const elapsed = ((Date.now() - planStartTime) / 1000).toFixed(1);
|
|
637
|
+
process.stdout.write('\r' + chalk.dim('Plan › ') + chalk.dim(elapsed + 's ') + agentGradient(DOTS[dotIdx % DOTS.length]) + ' ');
|
|
638
|
+
dotIdx++;
|
|
639
|
+
}, 400);
|
|
640
|
+
let thinkingStarted = false;
|
|
641
|
+
let firstContent = true;
|
|
642
|
+
const clearPlanSpinner = () => {
|
|
643
|
+
if (planSpinner) {
|
|
644
|
+
clearInterval(planSpinner);
|
|
645
|
+
planSpinner = null;
|
|
646
|
+
process.stdout.write('\r\x1b[0J');
|
|
647
|
+
}
|
|
648
|
+
};
|
|
549
649
|
try {
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
650
|
+
const fullPlanText = await streamChat(
|
|
651
|
+
planMessages,
|
|
652
|
+
(token) => {
|
|
653
|
+
if (firstContent) {
|
|
654
|
+
clearPlanSpinner();
|
|
655
|
+
firstContent = false;
|
|
656
|
+
}
|
|
657
|
+
process.stdout.write(token);
|
|
658
|
+
},
|
|
659
|
+
MODEL,
|
|
660
|
+
planAbort.signal,
|
|
661
|
+
{
|
|
662
|
+
onThinkingToken: (token) => {
|
|
663
|
+
if (!thinkingStarted) {
|
|
664
|
+
clearPlanSpinner();
|
|
665
|
+
process.stdout.write(chalk.dim('Thinking: '));
|
|
666
|
+
thinkingStarted = true;
|
|
667
|
+
}
|
|
668
|
+
process.stdout.write(chalk.dim(token));
|
|
669
|
+
},
|
|
670
|
+
}
|
|
671
|
+
);
|
|
672
|
+
chatMessages.push({ role: 'assistant', content: fullPlanText });
|
|
673
|
+
lastPlan = fullPlanText;
|
|
674
|
+
console.log('\n' + chalk.dim('Plan saved. Use /build to execute.\n'));
|
|
675
|
+
} catch (err) {
|
|
676
|
+
if (planSpinner) {
|
|
677
|
+
clearInterval(planSpinner);
|
|
678
|
+
planSpinner = null;
|
|
679
|
+
process.stdout.write('\r\x1b[0J');
|
|
680
|
+
}
|
|
681
|
+
if (!planAbort.signal?.aborted) console.log(chalk.red(`\n${err.message}\n`));
|
|
555
682
|
}
|
|
556
683
|
continue;
|
|
557
684
|
}
|
|
558
685
|
|
|
559
|
-
// /
|
|
560
|
-
if (trimmed === '/
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
const
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
686
|
+
// /yolo [prompt] — one plan in stream mode, then auto-accept and run agent until done
|
|
687
|
+
if (trimmed === '/yolo' || trimmed.startsWith('/yolo ')) {
|
|
688
|
+
const rawUserContent = trimmed.startsWith('/yolo ')
|
|
689
|
+
? trimmed.slice(6).trim()
|
|
690
|
+
: (await promptLine(chalk.bold('What do you want to yolo? '))).trim();
|
|
691
|
+
if (!rawUserContent) {
|
|
692
|
+
console.log(chalk.yellow('No prompt given.\n'));
|
|
693
|
+
continue;
|
|
694
|
+
}
|
|
695
|
+
const planUserContent = (await getLsContext()) + (await getGrepContext()) + 'Create a step-by-step plan for: ' + rawUserContent;
|
|
696
|
+
chatMessages.push({ role: 'user', content: planUserContent });
|
|
697
|
+
const planMessages = [buildPlanSystemMessage(), ...chatMessages];
|
|
698
|
+
const yoloAbort = new AbortController();
|
|
699
|
+
process.stdout.write(chalk.dim('\nYolo › Plan › '));
|
|
700
|
+
let thinkingStarted = false;
|
|
701
|
+
let fullPlanText = '';
|
|
702
|
+
try {
|
|
703
|
+
fullPlanText = await streamChat(
|
|
704
|
+
planMessages,
|
|
705
|
+
(token) => process.stdout.write(token),
|
|
706
|
+
MODEL,
|
|
707
|
+
yoloAbort.signal,
|
|
708
|
+
{
|
|
709
|
+
onThinkingToken: (token) => {
|
|
710
|
+
if (!thinkingStarted) {
|
|
711
|
+
process.stdout.write(chalk.dim('Thinking: '));
|
|
712
|
+
thinkingStarted = true;
|
|
713
|
+
}
|
|
714
|
+
process.stdout.write(chalk.dim(token));
|
|
715
|
+
},
|
|
716
|
+
}
|
|
717
|
+
);
|
|
718
|
+
chatMessages.push({ role: 'assistant', content: fullPlanText });
|
|
719
|
+
lastPlan = fullPlanText;
|
|
720
|
+
} catch (err) {
|
|
721
|
+
if (!yoloAbort.signal?.aborted) console.log(chalk.red(`\n${err.message}\n`));
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
const buildContent = (await getLsContext()) + (await getGrepContext()) +
|
|
725
|
+
'Execute the plan above using your tools. Run commands and edit files as needed. Do not only describe—use run_terminal_command, write_file, etc.';
|
|
726
|
+
chatMessages.push({ role: 'user', content: buildContent });
|
|
727
|
+
const agentMessages = [buildAgentSystemMessage(), ...chatMessages];
|
|
728
|
+
maybePrintFullPayload(agentMessages);
|
|
729
|
+
const abortController = new AbortController();
|
|
730
|
+
const confirmFn = () => Promise.resolve(true);
|
|
731
|
+
const confirmFileEdit = async () => true;
|
|
732
|
+
const startTime = Date.now();
|
|
733
|
+
const DOTS = ['.', '..', '...'];
|
|
734
|
+
let dotIdx = 0;
|
|
735
|
+
let spinner = null;
|
|
736
|
+
const startSpinner = () => {
|
|
737
|
+
if (spinner) { clearInterval(spinner); spinner = null; }
|
|
738
|
+
dotIdx = 0;
|
|
739
|
+
process.stdout.write(chalk.dim('\nYolo › Run › '));
|
|
740
|
+
spinner = setInterval(() => {
|
|
741
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
742
|
+
process.stdout.write('\r' + chalk.dim('Yolo › Run › ') + chalk.dim(elapsed + 's ') + agentGradient(DOTS[dotIdx % DOTS.length]) + ' ');
|
|
743
|
+
dotIdx++;
|
|
744
|
+
}, 400);
|
|
745
|
+
};
|
|
746
|
+
const stopSpinner = () => {
|
|
747
|
+
if (spinner) { clearInterval(spinner); spinner = null; }
|
|
748
|
+
process.stdout.write('\r\x1b[0J');
|
|
749
|
+
};
|
|
750
|
+
try {
|
|
751
|
+
const result = await runAgentLoop(agentMessages, {
|
|
752
|
+
signal: abortController.signal,
|
|
753
|
+
cwd: process.cwd(),
|
|
754
|
+
confirmFn,
|
|
755
|
+
confirmFileEdit,
|
|
756
|
+
onThinking: () => { startSpinner(); },
|
|
757
|
+
onBeforeToolRun: () => { stopSpinner(); },
|
|
758
|
+
onIteration: (iter, max, toolCount) => {
|
|
759
|
+
const w = process.stdout.columns || 80;
|
|
760
|
+
const label = ` Step ${iter} `;
|
|
761
|
+
const line = chalk.dim('──') + chalk.bold.white(label) + chalk.dim('─'.repeat(Math.max(0, w - label.length - 2)));
|
|
762
|
+
console.log(line);
|
|
763
|
+
},
|
|
764
|
+
onToolCall: (name, args) => {
|
|
765
|
+
const summary = formatToolCallSummary(name, args);
|
|
766
|
+
console.log(chalk.cyan(' ▶ ') + chalk.bold(name) + chalk.dim(' ') + chalk.white(summary));
|
|
767
|
+
},
|
|
768
|
+
onToolResult: (name, resultStr) => {
|
|
769
|
+
console.log(chalk.dim(' ') + formatToolResultSummary(name, resultStr));
|
|
770
|
+
},
|
|
771
|
+
});
|
|
772
|
+
stopSpinner();
|
|
773
|
+
if (result) {
|
|
774
|
+
chatMessages.push(result.finalMessage);
|
|
775
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
776
|
+
const width = Math.min(process.stdout.columns || 80, 80);
|
|
777
|
+
process.stdout.write(formatResponseWithCodeBlocks(result.content, width) + '\n\n');
|
|
778
|
+
await applyCodeBlockEdits(result.content, []);
|
|
779
|
+
allFiles = getFilesAndDirs();
|
|
780
|
+
console.log(chalk.green(`✓ Yolo done.`) + chalk.dim(` (${elapsed}s)\n`));
|
|
781
|
+
const opts1 = extractNumberedOptions(result.content);
|
|
782
|
+
if (opts1.length >= 2) {
|
|
783
|
+
const chosen = await selectFrom(opts1, 'Select an option:');
|
|
784
|
+
if (chosen) pendingMessage = chosen;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
} catch (err) {
|
|
788
|
+
stopSpinner();
|
|
789
|
+
if (!abortController.signal?.aborted) console.log(chalk.red(`\n${err.message}\n`));
|
|
790
|
+
}
|
|
586
791
|
continue;
|
|
587
792
|
}
|
|
588
793
|
|
|
589
|
-
// /
|
|
590
|
-
if (trimmed === '/
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
: (await promptLine(chalk.bold('Agent prompt: '))).trim();
|
|
594
|
-
if (!userContent) {
|
|
595
|
-
console.log(chalk.yellow('No prompt given.\n'));
|
|
794
|
+
// /build — execute last plan with tools
|
|
795
|
+
if (trimmed === '/build') {
|
|
796
|
+
if (!lastPlan || lastPlan.trim() === '') {
|
|
797
|
+
console.log(chalk.yellow('Run /plan first to create a plan.\n'));
|
|
596
798
|
continue;
|
|
597
799
|
}
|
|
598
|
-
|
|
800
|
+
const buildContent = (await getLsContext()) + (await getGrepContext()) +
|
|
801
|
+
'Execute the plan above using your tools. Run commands and edit files as needed. Do not only describe—use run_terminal_command, write_file, etc.';
|
|
802
|
+
chatMessages.push({ role: 'user', content: buildContent });
|
|
599
803
|
const agentMessages = [buildAgentSystemMessage(), ...chatMessages];
|
|
804
|
+
maybePrintFullPayload(agentMessages);
|
|
600
805
|
const abortController = new AbortController();
|
|
601
806
|
const confirmFn = (cmd) => confirm(chalk.bold(`Run: ${cmd}? [y/N] `));
|
|
602
807
|
const confirmFileEdit = async (name, args) => {
|
|
@@ -605,12 +810,10 @@ export async function startInteractive() {
|
|
|
605
810
|
console.log('');
|
|
606
811
|
return confirm(chalk.bold('Apply this change? [y/N] '));
|
|
607
812
|
};
|
|
608
|
-
|
|
609
813
|
const startTime = Date.now();
|
|
610
814
|
const DOTS = ['.', '..', '...'];
|
|
611
815
|
let dotIdx = 0;
|
|
612
816
|
let spinner = null;
|
|
613
|
-
|
|
614
817
|
const startSpinner = () => {
|
|
615
818
|
if (spinner) { clearInterval(spinner); spinner = null; }
|
|
616
819
|
dotIdx = 0;
|
|
@@ -625,22 +828,17 @@ export async function startInteractive() {
|
|
|
625
828
|
if (spinner) { clearInterval(spinner); spinner = null; }
|
|
626
829
|
process.stdout.write('\r\x1b[0J');
|
|
627
830
|
};
|
|
628
|
-
|
|
629
831
|
try {
|
|
630
832
|
const result = await runAgentLoop(agentMessages, {
|
|
631
833
|
signal: abortController.signal,
|
|
632
834
|
cwd: process.cwd(),
|
|
633
835
|
confirmFn,
|
|
634
836
|
confirmFileEdit,
|
|
635
|
-
onThinking: () => {
|
|
636
|
-
|
|
637
|
-
},
|
|
638
|
-
onBeforeToolRun: () => {
|
|
639
|
-
stopSpinner();
|
|
640
|
-
},
|
|
837
|
+
onThinking: () => { startSpinner(); },
|
|
838
|
+
onBeforeToolRun: () => { stopSpinner(); },
|
|
641
839
|
onIteration: (iter, max, toolCount) => {
|
|
642
840
|
const w = process.stdout.columns || 80;
|
|
643
|
-
const label = ` Step ${iter}
|
|
841
|
+
const label = ` Step ${iter} `;
|
|
644
842
|
const line = chalk.dim('──') + chalk.bold.white(label) + chalk.dim('─'.repeat(Math.max(0, w - label.length - 2)));
|
|
645
843
|
console.log(line);
|
|
646
844
|
},
|
|
@@ -660,7 +858,7 @@ export async function startInteractive() {
|
|
|
660
858
|
process.stdout.write(formatResponseWithCodeBlocks(result.content, width) + '\n\n');
|
|
661
859
|
await applyCodeBlockEdits(result.content, []);
|
|
662
860
|
allFiles = getFilesAndDirs();
|
|
663
|
-
console.log(chalk.green(`✓
|
|
861
|
+
console.log(chalk.green(`✓ Build done.`) + chalk.dim(` (${elapsed}s)\n`));
|
|
664
862
|
const opts1 = extractNumberedOptions(result.content);
|
|
665
863
|
if (opts1.length >= 2) {
|
|
666
864
|
const chosen = await selectFrom(opts1, 'Select an option:');
|
|
@@ -674,6 +872,87 @@ export async function startInteractive() {
|
|
|
674
872
|
continue;
|
|
675
873
|
}
|
|
676
874
|
|
|
875
|
+
// /models — pick active model
|
|
876
|
+
if (trimmed === '/models') {
|
|
877
|
+
const chosen = await selectFrom(MODELS, 'Select model:');
|
|
878
|
+
if (chosen) {
|
|
879
|
+
setModel(chosen);
|
|
880
|
+
console.log(chalk.dim(`\n🤖 switched to ${chalk.cyan(chosen)}\n`));
|
|
881
|
+
}
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// /cd [path] — change working directory within this session
|
|
886
|
+
if (trimmed.startsWith('/cd')) {
|
|
887
|
+
const arg = trimmed.slice(3).trim();
|
|
888
|
+
const target = arg
|
|
889
|
+
? resolve(process.cwd(), arg.replace(/^~/, homedir()))
|
|
890
|
+
: homedir();
|
|
891
|
+
try {
|
|
892
|
+
process.chdir(target);
|
|
893
|
+
allFiles = getFilesAndDirs();
|
|
894
|
+
console.log(chalk.dim(`\n📁 ${process.cwd()}\n`));
|
|
895
|
+
} catch {
|
|
896
|
+
console.log(chalk.red(`\nno such directory: ${target}\n`));
|
|
897
|
+
}
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// /cmd [command] — run a shell command in the current folder
|
|
902
|
+
if (trimmed === '/cmd' || trimmed.startsWith('/cmd ')) {
|
|
903
|
+
const command = trimmed.startsWith('/cmd ')
|
|
904
|
+
? trimmed.slice(5).trim()
|
|
905
|
+
: (await promptLine(chalk.bold('Command: '))).trim();
|
|
906
|
+
if (!command) {
|
|
907
|
+
console.log(chalk.yellow('No command given. Use /cmd <command> e.g. /cmd ls -la\n'));
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
try {
|
|
911
|
+
const cwd = process.cwd();
|
|
912
|
+
const { stdout, stderr, exitCode } = await execCommand(command, cwd);
|
|
913
|
+
const out = [stdout, stderr].filter(Boolean).join(stderr ? '\n' : '').trim();
|
|
914
|
+
if (out) console.log(out);
|
|
915
|
+
if (exitCode !== 0) {
|
|
916
|
+
console.log(chalk.red(`\nExit code: ${exitCode}\n`));
|
|
917
|
+
} else {
|
|
918
|
+
console.log(chalk.dim('\n'));
|
|
919
|
+
}
|
|
920
|
+
} catch (err) {
|
|
921
|
+
console.log(chalk.red(`\n${err.message}\n`));
|
|
922
|
+
}
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// /setup-nextjs — scaffold a Next.js app via script
|
|
927
|
+
if (trimmed === '/setup-nextjs') {
|
|
928
|
+
const name = (await promptLine(chalk.bold('App folder name: '))).trim() || 'next-app';
|
|
929
|
+
const steps = [
|
|
930
|
+
{ type: 'mkdir', path: name },
|
|
931
|
+
{ type: 'cd', path: name },
|
|
932
|
+
{ type: 'run', cmd: 'npx create-next-app@latest . --yes' },
|
|
933
|
+
{ type: 'run', cmd: 'npm install sass' },
|
|
934
|
+
];
|
|
935
|
+
const ok = await runSetupSteps(steps);
|
|
936
|
+
allFiles = getFilesAndDirs();
|
|
937
|
+
if (ok) console.log(chalk.green(`✓ Next.js app created in ${name}.\n`));
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// /setup-laravel — scaffold a Laravel API via script
|
|
942
|
+
if (trimmed === '/setup-laravel') {
|
|
943
|
+
const name = (await promptLine(chalk.bold('App folder name: '))).trim() || 'laravel-api';
|
|
944
|
+
const steps = [
|
|
945
|
+
{ type: 'mkdir', path: name },
|
|
946
|
+
{ type: 'cd', path: name },
|
|
947
|
+
{ type: 'run', cmd: 'composer create-project --prefer-dist laravel/laravel .' },
|
|
948
|
+
{ type: 'run', cmd: 'php artisan serve' },
|
|
949
|
+
];
|
|
950
|
+
const ok = await runSetupSteps(steps);
|
|
951
|
+
allFiles = getFilesAndDirs();
|
|
952
|
+
if (ok) console.log(chalk.green(`✓ Laravel API created in ${name}.\n`));
|
|
953
|
+
continue;
|
|
954
|
+
}
|
|
955
|
+
|
|
677
956
|
// Handle message with agent (tools)
|
|
678
957
|
const { loaded, failed, content: resolvedContent } = await resolveFileRefs(trimmed);
|
|
679
958
|
if (loaded.length > 0) {
|
|
@@ -682,10 +961,12 @@ export async function startInteractive() {
|
|
|
682
961
|
if (failed.length > 0) {
|
|
683
962
|
console.log(chalk.yellow(`\n⚠ not found: ${failed.map(f => `@${f}`).join(', ')}`));
|
|
684
963
|
}
|
|
685
|
-
const
|
|
964
|
+
const rawUserContent = resolvedContent ?? trimmed;
|
|
965
|
+
const userContent = (await getLsContext()) + (await getGrepContext()) + rawUserContent;
|
|
686
966
|
chatMessages.push({ role: 'user', content: userContent });
|
|
687
967
|
|
|
688
968
|
const agentMessages = [buildAgentSystemMessage(), ...chatMessages];
|
|
969
|
+
maybePrintFullPayload(agentMessages);
|
|
689
970
|
const abortController = new AbortController();
|
|
690
971
|
const confirmFn = (cmd) => confirm(chalk.bold(`Run: ${cmd}? [y/N] `));
|
|
691
972
|
const confirmFileEdit = async (name, args) => {
|
|
@@ -728,7 +1009,7 @@ export async function startInteractive() {
|
|
|
728
1009
|
},
|
|
729
1010
|
onIteration: (iter, max, toolCount) => {
|
|
730
1011
|
const w = process.stdout.columns || 80;
|
|
731
|
-
const label = ` Step ${iter}
|
|
1012
|
+
const label = ` Step ${iter} `;
|
|
732
1013
|
const line = chalk.dim('──') + chalk.bold.white(label) + chalk.dim('─'.repeat(Math.max(0, w - label.length - 2)));
|
|
733
1014
|
console.log(line);
|
|
734
1015
|
},
|
package/src/ollama.js
CHANGED
|
@@ -4,22 +4,25 @@ const getHeaders = () => {
|
|
|
4
4
|
const token = getToken();
|
|
5
5
|
return { 'Content-Type': 'application/json', ...(token && { Authorization: `Bearer ${token}` }) };
|
|
6
6
|
};
|
|
7
|
-
export const MODELS = ['
|
|
8
|
-
export let MODEL = 'qwen3:
|
|
7
|
+
export const MODELS = ['llama3:latest', 'gpt-oss:20b', 'qwen3:14b', 'qwen2.5:14b-instruct', 'qwen3.5:4b', 'qwen3.5:9b'];
|
|
8
|
+
export let MODEL = 'qwen3.5:9b';
|
|
9
9
|
export function setModel(m) { MODEL = m; }
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Stream a chat response from Ollama.
|
|
13
|
-
* Calls onToken(string) for each token, returns the full response string.
|
|
13
|
+
* Calls onToken(string) for each content token, returns the full response string.
|
|
14
|
+
* When the backend sends thinking tokens, calls opts.onThinkingToken(string) if provided.
|
|
14
15
|
* Pass an AbortSignal to allow cancellation mid-stream.
|
|
16
|
+
* @param {object} [opts] - Optional. { onThinkingToken?: (token: string) => void }
|
|
15
17
|
*/
|
|
16
|
-
export async function streamChat(messages, onToken, _model = MODEL, signal = null) {
|
|
18
|
+
export async function streamChat(messages, onToken, _model = MODEL, signal = null, opts = {}) {
|
|
19
|
+
const onThinkingToken = opts.onThinkingToken;
|
|
17
20
|
let response;
|
|
18
21
|
try {
|
|
19
22
|
response = await fetch(`${API_URL}/ai/chat/stream`, {
|
|
20
23
|
method: 'POST',
|
|
21
24
|
headers: getHeaders(),
|
|
22
|
-
body: JSON.stringify({ messages, temperature: 0.2 }),
|
|
25
|
+
body: JSON.stringify({ messages, temperature: 0.2, think: true }),
|
|
23
26
|
signal,
|
|
24
27
|
});
|
|
25
28
|
} catch (err) {
|
|
@@ -54,6 +57,9 @@ export async function streamChat(messages, onToken, _model = MODEL, signal = nul
|
|
|
54
57
|
if (data === '[DONE]') return fullText;
|
|
55
58
|
try {
|
|
56
59
|
const parsed = JSON.parse(data);
|
|
60
|
+
if (parsed.thinking != null && parsed.thinking !== '' && onThinkingToken) {
|
|
61
|
+
onThinkingToken(parsed.thinking);
|
|
62
|
+
}
|
|
57
63
|
if (parsed.content) {
|
|
58
64
|
onToken(parsed.content);
|
|
59
65
|
fullText += parsed.content;
|