markov-cli 1.0.9 → 1.0.11
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 +356 -70
- package/src/ollama.js +10 -4
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';
|
|
@@ -242,6 +242,38 @@ async function getLsContext(cwd = process.cwd()) {
|
|
|
242
242
|
return `[Current directory: ${cwd}]\n(listing unavailable)\n\n`;
|
|
243
243
|
}
|
|
244
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
|
+
|
|
245
277
|
/** Shared system message base: Markov intro, cwd, file list. */
|
|
246
278
|
function getSystemMessageBase() {
|
|
247
279
|
const files = getFiles();
|
|
@@ -249,6 +281,15 @@ function getSystemMessageBase() {
|
|
|
249
281
|
return `You are Markov, an AI coding assistant.\nWorking directory: ${process.cwd()}\n${fileList}`;
|
|
250
282
|
}
|
|
251
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
|
+
|
|
252
293
|
/** System message for agent mode: tool-only instructions (no !!run / !!write). */
|
|
253
294
|
function buildAgentSystemMessage() {
|
|
254
295
|
const toolInstructions =
|
|
@@ -260,14 +301,29 @@ function buildAgentSystemMessage() {
|
|
|
260
301
|
`- search_replace: replace first occurrence of text in a file.\n` +
|
|
261
302
|
`- delete_file: delete a file.\n` +
|
|
262
303
|
`- list_dir: list directory contents (path optional, defaults to current dir).\n` +
|
|
263
|
-
`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
|
|
264
|
-
`
|
|
265
|
-
`
|
|
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`;
|
|
266
308
|
return { role: 'system', content: getSystemMessageBase() + toolInstructions };
|
|
267
309
|
}
|
|
268
310
|
|
|
269
311
|
const AGENT_LOOP_MAX_ITERATIONS = 20;
|
|
270
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
|
+
|
|
271
327
|
/** Preview of a file edit for confirmation (write_file / search_replace) */
|
|
272
328
|
function formatFileEditPreview(name, args) {
|
|
273
329
|
const path = args?.path ?? '(no path)';
|
|
@@ -424,17 +480,34 @@ async function runAgentLoop(messages, opts = {}) {
|
|
|
424
480
|
};
|
|
425
481
|
}
|
|
426
482
|
|
|
483
|
+
/** Short intro shown on first load; /intro re-displays this. */
|
|
484
|
+
const INTRO_TEXT =
|
|
485
|
+
'\n' +
|
|
486
|
+
chalk.bold('Quick start:\n') +
|
|
487
|
+
chalk.cyan(' /help') + chalk.dim(' show all commands\n') +
|
|
488
|
+
chalk.cyan(' /login') + chalk.dim(' authenticate with email & password\n') +
|
|
489
|
+
chalk.cyan(' /cd [path]') + chalk.dim(' change working directory\n') +
|
|
490
|
+
chalk.cyan(' /plan') + chalk.dim(' [prompt] stream a plan (no tools)\n') +
|
|
491
|
+
chalk.cyan(' /build') + chalk.dim(' execute last plan with tools\n') +
|
|
492
|
+
chalk.cyan(' /yolo') + chalk.dim(' [prompt] plan in stream mode, then auto-run until done\n') +
|
|
493
|
+
chalk.dim('\nType a message · ') + chalk.cyan('@filename') + chalk.dim(' to attach · ctrl+q to cancel\n');
|
|
494
|
+
|
|
427
495
|
const HELP_TEXT =
|
|
428
496
|
'\n' +
|
|
429
497
|
chalk.bold('Commands:\n') +
|
|
498
|
+
chalk.cyan(' /intro') + chalk.dim(' show quick start (same as on first load)\n') +
|
|
430
499
|
chalk.cyan(' /help') + chalk.dim(' show this help\n') +
|
|
431
|
-
chalk.cyan(' /agent') + chalk.dim(' [prompt] run with tools (run commands, create folders)\n') +
|
|
432
500
|
chalk.cyan(' /setup-nextjs') + chalk.dim(' scaffold a Next.js app\n') +
|
|
433
501
|
chalk.cyan(' /setup-laravel') + chalk.dim(' scaffold a Laravel API\n') +
|
|
434
502
|
chalk.cyan(' /models') + chalk.dim(' switch the active AI model\n') +
|
|
435
503
|
chalk.cyan(' /cd [path]') + chalk.dim(' change working directory\n') +
|
|
504
|
+
chalk.cyan(' /cmd [command]') + chalk.dim(' run a shell command in the current folder\n') +
|
|
436
505
|
chalk.cyan(' /login') + chalk.dim(' authenticate with email & password\n') +
|
|
437
506
|
chalk.cyan(' /logout') + chalk.dim(' clear saved auth token\n') +
|
|
507
|
+
chalk.cyan(' /debug') + chalk.dim(' toggle full payload dump (env MARKOV_DEBUG)\n') +
|
|
508
|
+
chalk.cyan(' /plan') + chalk.dim(' [prompt] stream a plan (no tools)\n') +
|
|
509
|
+
chalk.cyan(' /build') + chalk.dim(' execute last plan with tools\n') +
|
|
510
|
+
chalk.cyan(' /yolo') + chalk.dim(' [prompt] plan in stream mode, then auto-run until done\n') +
|
|
438
511
|
chalk.dim('\nType a message · ') + chalk.cyan('@filename') + chalk.dim(' to attach · ctrl+q to cancel\n');
|
|
439
512
|
|
|
440
513
|
/**
|
|
@@ -494,13 +567,14 @@ export async function startInteractive() {
|
|
|
494
567
|
const chatMessages = [];
|
|
495
568
|
|
|
496
569
|
console.log(chalk.dim(`Chat with Markov (${MODEL}).`));
|
|
497
|
-
console.log(
|
|
570
|
+
console.log(INTRO_TEXT);
|
|
498
571
|
|
|
499
572
|
if (!getToken()) {
|
|
500
573
|
console.log(chalk.yellow('⚠ Not logged in. Use /login to authenticate.\n'));
|
|
501
574
|
}
|
|
502
575
|
|
|
503
576
|
let pendingMessage = null;
|
|
577
|
+
let lastPlan = null;
|
|
504
578
|
|
|
505
579
|
while (true) {
|
|
506
580
|
let raw;
|
|
@@ -536,80 +610,217 @@ export async function startInteractive() {
|
|
|
536
610
|
continue;
|
|
537
611
|
}
|
|
538
612
|
|
|
613
|
+
// /intro — show quick start (same as on first load)
|
|
614
|
+
if (trimmed === '/intro') {
|
|
615
|
+
console.log(INTRO_TEXT);
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
|
|
539
619
|
// /help — list all commands
|
|
540
620
|
if (trimmed === '/help') {
|
|
541
621
|
console.log(HELP_TEXT);
|
|
542
622
|
continue;
|
|
543
623
|
}
|
|
544
624
|
|
|
545
|
-
// /
|
|
546
|
-
if (trimmed === '/
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
625
|
+
// /debug — toggle full payload dump before each request
|
|
626
|
+
if (trimmed === '/debug') {
|
|
627
|
+
if (process.env.MARKOV_DEBUG) {
|
|
628
|
+
delete process.env.MARKOV_DEBUG;
|
|
629
|
+
console.log(chalk.dim('Debug off — payload dump disabled.\n'));
|
|
630
|
+
} else {
|
|
631
|
+
process.env.MARKOV_DEBUG = '1';
|
|
632
|
+
console.log(chalk.dim('Debug on — full payload (system + messages) will be printed before each request.\n'));
|
|
551
633
|
}
|
|
552
634
|
continue;
|
|
553
635
|
}
|
|
554
636
|
|
|
555
|
-
// /
|
|
556
|
-
if (trimmed.startsWith('/
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
637
|
+
// /plan [prompt] — stream a plan (no tools), store as lastPlan
|
|
638
|
+
if (trimmed === '/plan' || trimmed.startsWith('/plan ')) {
|
|
639
|
+
const rawUserContent = trimmed.startsWith('/plan ')
|
|
640
|
+
? trimmed.slice(6).trim()
|
|
641
|
+
: (await promptLine(chalk.bold('What do you want to plan? '))).trim();
|
|
642
|
+
if (!rawUserContent) {
|
|
643
|
+
console.log(chalk.yellow('No prompt given.\n'));
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
const userContent = (await getLsContext()) + (await getGrepContext()) + 'Create a step-by-step plan for: ' + rawUserContent;
|
|
647
|
+
chatMessages.push({ role: 'user', content: userContent });
|
|
648
|
+
const planMessages = [buildPlanSystemMessage(), ...chatMessages];
|
|
649
|
+
const planAbort = new AbortController();
|
|
650
|
+
process.stdout.write(chalk.dim('\nPlan › '));
|
|
651
|
+
const DOTS = ['.', '..', '...'];
|
|
652
|
+
let dotIdx = 0;
|
|
653
|
+
const planStartTime = Date.now();
|
|
654
|
+
let planSpinner = setInterval(() => {
|
|
655
|
+
const elapsed = ((Date.now() - planStartTime) / 1000).toFixed(1);
|
|
656
|
+
process.stdout.write('\r' + chalk.dim('Plan › ') + chalk.dim(elapsed + 's ') + agentGradient(DOTS[dotIdx % DOTS.length]) + ' ');
|
|
657
|
+
dotIdx++;
|
|
658
|
+
}, 400);
|
|
659
|
+
let thinkingStarted = false;
|
|
660
|
+
let firstContent = true;
|
|
661
|
+
const clearPlanSpinner = () => {
|
|
662
|
+
if (planSpinner) {
|
|
663
|
+
clearInterval(planSpinner);
|
|
664
|
+
planSpinner = null;
|
|
665
|
+
process.stdout.write('\r\x1b[0J');
|
|
666
|
+
}
|
|
667
|
+
};
|
|
561
668
|
try {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
669
|
+
const fullPlanText = await streamChat(
|
|
670
|
+
planMessages,
|
|
671
|
+
(token) => {
|
|
672
|
+
if (firstContent) {
|
|
673
|
+
clearPlanSpinner();
|
|
674
|
+
firstContent = false;
|
|
675
|
+
}
|
|
676
|
+
process.stdout.write(token);
|
|
677
|
+
},
|
|
678
|
+
MODEL,
|
|
679
|
+
planAbort.signal,
|
|
680
|
+
{
|
|
681
|
+
onThinkingToken: (token) => {
|
|
682
|
+
if (!thinkingStarted) {
|
|
683
|
+
clearPlanSpinner();
|
|
684
|
+
process.stdout.write(chalk.dim('Thinking: '));
|
|
685
|
+
thinkingStarted = true;
|
|
686
|
+
}
|
|
687
|
+
process.stdout.write(chalk.dim(token));
|
|
688
|
+
},
|
|
689
|
+
}
|
|
690
|
+
);
|
|
691
|
+
chatMessages.push({ role: 'assistant', content: fullPlanText });
|
|
692
|
+
lastPlan = fullPlanText;
|
|
693
|
+
console.log('\n' + chalk.dim('Plan saved. Use /build to execute.\n'));
|
|
694
|
+
} catch (err) {
|
|
695
|
+
if (planSpinner) {
|
|
696
|
+
clearInterval(planSpinner);
|
|
697
|
+
planSpinner = null;
|
|
698
|
+
process.stdout.write('\r\x1b[0J');
|
|
699
|
+
}
|
|
700
|
+
if (!planAbort.signal?.aborted) console.log(chalk.red(`\n${err.message}\n`));
|
|
567
701
|
}
|
|
568
702
|
continue;
|
|
569
703
|
}
|
|
570
704
|
|
|
571
|
-
// /
|
|
572
|
-
if (trimmed === '/
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
705
|
+
// /yolo [prompt] — one plan in stream mode, then auto-accept and run agent until done
|
|
706
|
+
if (trimmed === '/yolo' || trimmed.startsWith('/yolo ')) {
|
|
707
|
+
const rawUserContent = trimmed.startsWith('/yolo ')
|
|
708
|
+
? trimmed.slice(6).trim()
|
|
709
|
+
: (await promptLine(chalk.bold('What do you want to yolo? '))).trim();
|
|
710
|
+
if (!rawUserContent) {
|
|
711
|
+
console.log(chalk.yellow('No prompt given.\n'));
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
714
|
+
const planUserContent = (await getLsContext()) + (await getGrepContext()) + 'Create a step-by-step plan for: ' + rawUserContent;
|
|
715
|
+
chatMessages.push({ role: 'user', content: planUserContent });
|
|
716
|
+
const planMessages = [buildPlanSystemMessage(), ...chatMessages];
|
|
717
|
+
const yoloAbort = new AbortController();
|
|
718
|
+
process.stdout.write(chalk.dim('\nYolo › Plan › '));
|
|
719
|
+
let thinkingStarted = false;
|
|
720
|
+
let fullPlanText = '';
|
|
721
|
+
try {
|
|
722
|
+
fullPlanText = await streamChat(
|
|
723
|
+
planMessages,
|
|
724
|
+
(token) => process.stdout.write(token),
|
|
725
|
+
MODEL,
|
|
726
|
+
yoloAbort.signal,
|
|
727
|
+
{
|
|
728
|
+
onThinkingToken: (token) => {
|
|
729
|
+
if (!thinkingStarted) {
|
|
730
|
+
process.stdout.write(chalk.dim('Thinking: '));
|
|
731
|
+
thinkingStarted = true;
|
|
732
|
+
}
|
|
733
|
+
process.stdout.write(chalk.dim(token));
|
|
734
|
+
},
|
|
735
|
+
}
|
|
736
|
+
);
|
|
737
|
+
chatMessages.push({ role: 'assistant', content: fullPlanText });
|
|
738
|
+
lastPlan = fullPlanText;
|
|
739
|
+
} catch (err) {
|
|
740
|
+
if (!yoloAbort.signal?.aborted) console.log(chalk.red(`\n${err.message}\n`));
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
const buildContent = (await getLsContext()) + (await getGrepContext()) +
|
|
744
|
+
'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.';
|
|
745
|
+
chatMessages.push({ role: 'user', content: buildContent });
|
|
746
|
+
const agentMessages = [buildAgentSystemMessage(), ...chatMessages];
|
|
747
|
+
maybePrintFullPayload(agentMessages);
|
|
748
|
+
const abortController = new AbortController();
|
|
749
|
+
const confirmFn = () => Promise.resolve(true);
|
|
750
|
+
const confirmFileEdit = async () => true;
|
|
751
|
+
const startTime = Date.now();
|
|
752
|
+
const DOTS = ['.', '..', '...'];
|
|
753
|
+
let dotIdx = 0;
|
|
754
|
+
let spinner = null;
|
|
755
|
+
const startSpinner = () => {
|
|
756
|
+
if (spinner) { clearInterval(spinner); spinner = null; }
|
|
757
|
+
dotIdx = 0;
|
|
758
|
+
process.stdout.write(chalk.dim('\nYolo › Run › '));
|
|
759
|
+
spinner = setInterval(() => {
|
|
760
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
761
|
+
process.stdout.write('\r' + chalk.dim('Yolo › Run › ') + chalk.dim(elapsed + 's ') + agentGradient(DOTS[dotIdx % DOTS.length]) + ' ');
|
|
762
|
+
dotIdx++;
|
|
763
|
+
}, 400);
|
|
764
|
+
};
|
|
765
|
+
const stopSpinner = () => {
|
|
766
|
+
if (spinner) { clearInterval(spinner); spinner = null; }
|
|
767
|
+
process.stdout.write('\r\x1b[0J');
|
|
768
|
+
};
|
|
769
|
+
try {
|
|
770
|
+
const result = await runAgentLoop(agentMessages, {
|
|
771
|
+
signal: abortController.signal,
|
|
772
|
+
cwd: process.cwd(),
|
|
773
|
+
confirmFn,
|
|
774
|
+
confirmFileEdit,
|
|
775
|
+
onThinking: () => { startSpinner(); },
|
|
776
|
+
onBeforeToolRun: () => { stopSpinner(); },
|
|
777
|
+
onIteration: (iter, max, toolCount) => {
|
|
778
|
+
const w = process.stdout.columns || 80;
|
|
779
|
+
const label = ` Step ${iter} `;
|
|
780
|
+
const line = chalk.dim('──') + chalk.bold.white(label) + chalk.dim('─'.repeat(Math.max(0, w - label.length - 2)));
|
|
781
|
+
console.log(line);
|
|
782
|
+
},
|
|
783
|
+
onToolCall: (name, args) => {
|
|
784
|
+
const summary = formatToolCallSummary(name, args);
|
|
785
|
+
console.log(chalk.cyan(' ▶ ') + chalk.bold(name) + chalk.dim(' ') + chalk.white(summary));
|
|
786
|
+
},
|
|
787
|
+
onToolResult: (name, resultStr) => {
|
|
788
|
+
console.log(chalk.dim(' ') + formatToolResultSummary(name, resultStr));
|
|
789
|
+
},
|
|
790
|
+
});
|
|
791
|
+
stopSpinner();
|
|
792
|
+
if (result) {
|
|
793
|
+
chatMessages.push(result.finalMessage);
|
|
794
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
795
|
+
const width = Math.min(process.stdout.columns || 80, 80);
|
|
796
|
+
process.stdout.write(formatResponseWithCodeBlocks(result.content, width) + '\n\n');
|
|
797
|
+
await applyCodeBlockEdits(result.content, []);
|
|
798
|
+
allFiles = getFilesAndDirs();
|
|
799
|
+
console.log(chalk.green(`✓ Yolo done.`) + chalk.dim(` (${elapsed}s)\n`));
|
|
800
|
+
const opts1 = extractNumberedOptions(result.content);
|
|
801
|
+
if (opts1.length >= 2) {
|
|
802
|
+
const chosen = await selectFrom(opts1, 'Select an option:');
|
|
803
|
+
if (chosen) pendingMessage = chosen;
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
} catch (err) {
|
|
807
|
+
stopSpinner();
|
|
808
|
+
if (!abortController.signal?.aborted) console.log(chalk.red(`\n${err.message}\n`));
|
|
809
|
+
}
|
|
598
810
|
continue;
|
|
599
811
|
}
|
|
600
812
|
|
|
601
|
-
// /
|
|
602
|
-
if (trimmed === '/
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
: (await promptLine(chalk.bold('Agent prompt: '))).trim();
|
|
606
|
-
if (!rawUserContent) {
|
|
607
|
-
console.log(chalk.yellow('No prompt given.\n'));
|
|
813
|
+
// /build — execute last plan with tools
|
|
814
|
+
if (trimmed === '/build') {
|
|
815
|
+
if (!lastPlan || lastPlan.trim() === '') {
|
|
816
|
+
console.log(chalk.yellow('Run /plan first to create a plan.\n'));
|
|
608
817
|
continue;
|
|
609
818
|
}
|
|
610
|
-
const
|
|
611
|
-
|
|
819
|
+
const buildContent = (await getLsContext()) + (await getGrepContext()) +
|
|
820
|
+
'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.';
|
|
821
|
+
chatMessages.push({ role: 'user', content: buildContent });
|
|
612
822
|
const agentMessages = [buildAgentSystemMessage(), ...chatMessages];
|
|
823
|
+
maybePrintFullPayload(agentMessages);
|
|
613
824
|
const abortController = new AbortController();
|
|
614
825
|
const confirmFn = (cmd) => confirm(chalk.bold(`Run: ${cmd}? [y/N] `));
|
|
615
826
|
const confirmFileEdit = async (name, args) => {
|
|
@@ -618,12 +829,10 @@ export async function startInteractive() {
|
|
|
618
829
|
console.log('');
|
|
619
830
|
return confirm(chalk.bold('Apply this change? [y/N] '));
|
|
620
831
|
};
|
|
621
|
-
|
|
622
832
|
const startTime = Date.now();
|
|
623
833
|
const DOTS = ['.', '..', '...'];
|
|
624
834
|
let dotIdx = 0;
|
|
625
835
|
let spinner = null;
|
|
626
|
-
|
|
627
836
|
const startSpinner = () => {
|
|
628
837
|
if (spinner) { clearInterval(spinner); spinner = null; }
|
|
629
838
|
dotIdx = 0;
|
|
@@ -638,19 +847,14 @@ export async function startInteractive() {
|
|
|
638
847
|
if (spinner) { clearInterval(spinner); spinner = null; }
|
|
639
848
|
process.stdout.write('\r\x1b[0J');
|
|
640
849
|
};
|
|
641
|
-
|
|
642
850
|
try {
|
|
643
851
|
const result = await runAgentLoop(agentMessages, {
|
|
644
852
|
signal: abortController.signal,
|
|
645
853
|
cwd: process.cwd(),
|
|
646
854
|
confirmFn,
|
|
647
855
|
confirmFileEdit,
|
|
648
|
-
onThinking: () => {
|
|
649
|
-
|
|
650
|
-
},
|
|
651
|
-
onBeforeToolRun: () => {
|
|
652
|
-
stopSpinner();
|
|
653
|
-
},
|
|
856
|
+
onThinking: () => { startSpinner(); },
|
|
857
|
+
onBeforeToolRun: () => { stopSpinner(); },
|
|
654
858
|
onIteration: (iter, max, toolCount) => {
|
|
655
859
|
const w = process.stdout.columns || 80;
|
|
656
860
|
const label = ` Step ${iter} `;
|
|
@@ -673,7 +877,7 @@ export async function startInteractive() {
|
|
|
673
877
|
process.stdout.write(formatResponseWithCodeBlocks(result.content, width) + '\n\n');
|
|
674
878
|
await applyCodeBlockEdits(result.content, []);
|
|
675
879
|
allFiles = getFilesAndDirs();
|
|
676
|
-
console.log(chalk.green(`✓
|
|
880
|
+
console.log(chalk.green(`✓ Build done.`) + chalk.dim(` (${elapsed}s)\n`));
|
|
677
881
|
const opts1 = extractNumberedOptions(result.content);
|
|
678
882
|
if (opts1.length >= 2) {
|
|
679
883
|
const chosen = await selectFrom(opts1, 'Select an option:');
|
|
@@ -687,6 +891,87 @@ export async function startInteractive() {
|
|
|
687
891
|
continue;
|
|
688
892
|
}
|
|
689
893
|
|
|
894
|
+
// /models — pick active model
|
|
895
|
+
if (trimmed === '/models') {
|
|
896
|
+
const chosen = await selectFrom(MODELS, 'Select model:');
|
|
897
|
+
if (chosen) {
|
|
898
|
+
setModel(chosen);
|
|
899
|
+
console.log(chalk.dim(`\n🤖 switched to ${chalk.cyan(chosen)}\n`));
|
|
900
|
+
}
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// /cd [path] — change working directory within this session
|
|
905
|
+
if (trimmed.startsWith('/cd')) {
|
|
906
|
+
const arg = trimmed.slice(3).trim();
|
|
907
|
+
const target = arg
|
|
908
|
+
? resolve(process.cwd(), arg.replace(/^~/, homedir()))
|
|
909
|
+
: homedir();
|
|
910
|
+
try {
|
|
911
|
+
process.chdir(target);
|
|
912
|
+
allFiles = getFilesAndDirs();
|
|
913
|
+
console.log(chalk.dim(`\n📁 ${process.cwd()}\n`));
|
|
914
|
+
} catch {
|
|
915
|
+
console.log(chalk.red(`\nno such directory: ${target}\n`));
|
|
916
|
+
}
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// /cmd [command] — run a shell command in the current folder
|
|
921
|
+
if (trimmed === '/cmd' || trimmed.startsWith('/cmd ')) {
|
|
922
|
+
const command = trimmed.startsWith('/cmd ')
|
|
923
|
+
? trimmed.slice(5).trim()
|
|
924
|
+
: (await promptLine(chalk.bold('Command: '))).trim();
|
|
925
|
+
if (!command) {
|
|
926
|
+
console.log(chalk.yellow('No command given. Use /cmd <command> e.g. /cmd ls -la\n'));
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
try {
|
|
930
|
+
const cwd = process.cwd();
|
|
931
|
+
const { stdout, stderr, exitCode } = await execCommand(command, cwd);
|
|
932
|
+
const out = [stdout, stderr].filter(Boolean).join(stderr ? '\n' : '').trim();
|
|
933
|
+
if (out) console.log(out);
|
|
934
|
+
if (exitCode !== 0) {
|
|
935
|
+
console.log(chalk.red(`\nExit code: ${exitCode}\n`));
|
|
936
|
+
} else {
|
|
937
|
+
console.log(chalk.dim('\n'));
|
|
938
|
+
}
|
|
939
|
+
} catch (err) {
|
|
940
|
+
console.log(chalk.red(`\n${err.message}\n`));
|
|
941
|
+
}
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// /setup-nextjs — scaffold a Next.js app via script
|
|
946
|
+
if (trimmed === '/setup-nextjs') {
|
|
947
|
+
const name = (await promptLine(chalk.bold('App folder name: '))).trim() || 'next-app';
|
|
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);
|
|
955
|
+
allFiles = getFilesAndDirs();
|
|
956
|
+
if (ok) console.log(chalk.green(`✓ Next.js app created in ${name}.\n`));
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// /setup-laravel — scaffold a Laravel API via script
|
|
961
|
+
if (trimmed === '/setup-laravel') {
|
|
962
|
+
const name = (await promptLine(chalk.bold('App folder name: '))).trim() || 'laravel-api';
|
|
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);
|
|
970
|
+
allFiles = getFilesAndDirs();
|
|
971
|
+
if (ok) console.log(chalk.green(`✓ Laravel API created in ${name}.\n`));
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
|
|
690
975
|
// Handle message with agent (tools)
|
|
691
976
|
const { loaded, failed, content: resolvedContent } = await resolveFileRefs(trimmed);
|
|
692
977
|
if (loaded.length > 0) {
|
|
@@ -696,10 +981,11 @@ export async function startInteractive() {
|
|
|
696
981
|
console.log(chalk.yellow(`\n⚠ not found: ${failed.map(f => `@${f}`).join(', ')}`));
|
|
697
982
|
}
|
|
698
983
|
const rawUserContent = resolvedContent ?? trimmed;
|
|
699
|
-
const userContent = (await getLsContext()) + rawUserContent;
|
|
984
|
+
const userContent = (await getLsContext()) + (await getGrepContext()) + rawUserContent;
|
|
700
985
|
chatMessages.push({ role: 'user', content: userContent });
|
|
701
986
|
|
|
702
987
|
const agentMessages = [buildAgentSystemMessage(), ...chatMessages];
|
|
988
|
+
maybePrintFullPayload(agentMessages);
|
|
703
989
|
const abortController = new AbortController();
|
|
704
990
|
const confirmFn = (cmd) => confirm(chalk.bold(`Run: ${cmd}? [y/N] `));
|
|
705
991
|
const confirmFileEdit = async (name, args) => {
|
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 = ['llama3:latest', 'gpt-oss:20b', 'qwen3:14b', 'qwen2.5:14b-instruct', 'qwen3.5:9b'];
|
|
7
|
+
export const MODELS = ['llama3:latest', 'gpt-oss:20b', 'qwen3:14b', 'qwen2.5:14b-instruct', 'qwen3.5:4b', 'qwen3.5:9b'];
|
|
8
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;
|