markov-cli 1.0.9 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "markov-cli",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "Markov CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 these tools do not only list commands or describe steps in your reply. Execute with tool calls.\n` +
264
- `When the user has ATTACHED FILES (FILE: path ... in the message), any edit, add, or fix they ask for in those files MUST be done with write_file or search_replace — never by pasting the modified file content in your reply.\n` +
265
- `Use RELATIVE paths.\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`;
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)';
@@ -428,13 +484,17 @@ const HELP_TEXT =
428
484
  '\n' +
429
485
  chalk.bold('Commands:\n') +
430
486
  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
487
  chalk.cyan(' /setup-nextjs') + chalk.dim(' scaffold a Next.js app\n') +
433
488
  chalk.cyan(' /setup-laravel') + chalk.dim(' scaffold a Laravel API\n') +
434
489
  chalk.cyan(' /models') + chalk.dim(' switch the active AI model\n') +
435
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') +
436
492
  chalk.cyan(' /login') + chalk.dim(' authenticate with email & password\n') +
437
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') +
438
498
  chalk.dim('\nType a message · ') + chalk.cyan('@filename') + chalk.dim(' to attach · ctrl+q to cancel\n');
439
499
 
440
500
  /**
@@ -501,6 +561,7 @@ export async function startInteractive() {
501
561
  }
502
562
 
503
563
  let pendingMessage = null;
564
+ let lastPlan = null;
504
565
 
505
566
  while (true) {
506
567
  let raw;
@@ -542,74 +603,205 @@ export async function startInteractive() {
542
603
  continue;
543
604
  }
544
605
 
545
- // /modelspick active model
546
- if (trimmed === '/models') {
547
- const chosen = await selectFrom(MODELS, 'Select model:');
548
- if (chosen) {
549
- setModel(chosen);
550
- console.log(chalk.dim(`\n🤖 switched to ${chalk.cyan(chosen)}\n`));
606
+ // /debugtoggle 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'));
551
614
  }
552
615
  continue;
553
616
  }
554
617
 
555
- // /cd [path] — change working directory within this session
556
- if (trimmed.startsWith('/cd')) {
557
- const arg = trimmed.slice(3).trim();
558
- const target = arg
559
- ? resolve(process.cwd(), arg.replace(/^~/, homedir()))
560
- : homedir();
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
+ };
561
649
  try {
562
- process.chdir(target);
563
- allFiles = getFilesAndDirs();
564
- console.log(chalk.dim(`\n📁 ${process.cwd()}\n`));
565
- } catch {
566
- console.log(chalk.red(`\nno such directory: ${target}\n`));
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`));
567
682
  }
568
683
  continue;
569
684
  }
570
685
 
571
- // /setup-nextjsscaffold a Next.js app via script
572
- if (trimmed === '/setup-nextjs') {
573
- const name = (await promptLine(chalk.bold('App folder name: '))).trim() || 'next-app';
574
- const steps = [
575
- { type: 'mkdir', path: name },
576
- { type: 'cd', path: name },
577
- { type: 'run', cmd: 'npx create-next-app@latest . --yes' },
578
- { type: 'run', cmd: 'npm install sass' },
579
- ];
580
- const ok = await runSetupSteps(steps);
581
- allFiles = getFilesAndDirs();
582
- if (ok) console.log(chalk.green(`✓ Next.js app created in ${name}.\n`));
583
- continue;
584
- }
585
-
586
- // /setup-laravel scaffold a Laravel API via script
587
- if (trimmed === '/setup-laravel') {
588
- const name = (await promptLine(chalk.bold('App folder name: '))).trim() || 'laravel-api';
589
- const steps = [
590
- { type: 'mkdir', path: name },
591
- { type: 'cd', path: name },
592
- { type: 'run', cmd: 'composer create-project --prefer-dist laravel/laravel .' },
593
- { type: 'run', cmd: 'php artisan serve' },
594
- ];
595
- const ok = await runSetupSteps(steps);
596
- allFiles = getFilesAndDirs();
597
- if (ok) console.log(chalk.green(`✓ Laravel API created in ${name}.\n`));
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
+ }
598
791
  continue;
599
792
  }
600
793
 
601
- // /agent [prompt] run with tools (run_terminal_command, create_folder, file tools), loop until final response
602
- if (trimmed === '/agent' || trimmed.startsWith('/agent ')) {
603
- const rawUserContent = trimmed.startsWith('/agent ')
604
- ? trimmed.slice(7).trim()
605
- : (await promptLine(chalk.bold('Agent prompt: '))).trim();
606
- if (!rawUserContent) {
607
- console.log(chalk.yellow('No prompt given.\n'));
794
+ // /buildexecute 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'));
608
798
  continue;
609
799
  }
610
- const userContent = (await getLsContext()) + rawUserContent;
611
- chatMessages.push({ role: 'user', content: userContent });
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 });
612
803
  const agentMessages = [buildAgentSystemMessage(), ...chatMessages];
804
+ maybePrintFullPayload(agentMessages);
613
805
  const abortController = new AbortController();
614
806
  const confirmFn = (cmd) => confirm(chalk.bold(`Run: ${cmd}? [y/N] `));
615
807
  const confirmFileEdit = async (name, args) => {
@@ -618,12 +810,10 @@ export async function startInteractive() {
618
810
  console.log('');
619
811
  return confirm(chalk.bold('Apply this change? [y/N] '));
620
812
  };
621
-
622
813
  const startTime = Date.now();
623
814
  const DOTS = ['.', '..', '...'];
624
815
  let dotIdx = 0;
625
816
  let spinner = null;
626
-
627
817
  const startSpinner = () => {
628
818
  if (spinner) { clearInterval(spinner); spinner = null; }
629
819
  dotIdx = 0;
@@ -638,19 +828,14 @@ export async function startInteractive() {
638
828
  if (spinner) { clearInterval(spinner); spinner = null; }
639
829
  process.stdout.write('\r\x1b[0J');
640
830
  };
641
-
642
831
  try {
643
832
  const result = await runAgentLoop(agentMessages, {
644
833
  signal: abortController.signal,
645
834
  cwd: process.cwd(),
646
835
  confirmFn,
647
836
  confirmFileEdit,
648
- onThinking: () => {
649
- startSpinner();
650
- },
651
- onBeforeToolRun: () => {
652
- stopSpinner();
653
- },
837
+ onThinking: () => { startSpinner(); },
838
+ onBeforeToolRun: () => { stopSpinner(); },
654
839
  onIteration: (iter, max, toolCount) => {
655
840
  const w = process.stdout.columns || 80;
656
841
  const label = ` Step ${iter} `;
@@ -673,7 +858,7 @@ export async function startInteractive() {
673
858
  process.stdout.write(formatResponseWithCodeBlocks(result.content, width) + '\n\n');
674
859
  await applyCodeBlockEdits(result.content, []);
675
860
  allFiles = getFilesAndDirs();
676
- console.log(chalk.green(`✓ Agent done.`) + chalk.dim(` (${elapsed}s)\n`));
861
+ console.log(chalk.green(`✓ Build done.`) + chalk.dim(` (${elapsed}s)\n`));
677
862
  const opts1 = extractNumberedOptions(result.content);
678
863
  if (opts1.length >= 2) {
679
864
  const chosen = await selectFrom(opts1, 'Select an option:');
@@ -687,6 +872,87 @@ export async function startInteractive() {
687
872
  continue;
688
873
  }
689
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
+
690
956
  // Handle message with agent (tools)
691
957
  const { loaded, failed, content: resolvedContent } = await resolveFileRefs(trimmed);
692
958
  if (loaded.length > 0) {
@@ -696,10 +962,11 @@ export async function startInteractive() {
696
962
  console.log(chalk.yellow(`\n⚠ not found: ${failed.map(f => `@${f}`).join(', ')}`));
697
963
  }
698
964
  const rawUserContent = resolvedContent ?? trimmed;
699
- const userContent = (await getLsContext()) + rawUserContent;
965
+ const userContent = (await getLsContext()) + (await getGrepContext()) + rawUserContent;
700
966
  chatMessages.push({ role: 'user', content: userContent });
701
967
 
702
968
  const agentMessages = [buildAgentSystemMessage(), ...chatMessages];
969
+ maybePrintFullPayload(agentMessages);
703
970
  const abortController = new AbortController();
704
971
  const confirmFn = (cmd) => confirm(chalk.bold(`Run: ${cmd}? [y/N] `));
705
972
  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;