markov-cli 1.0.7 → 1.0.9

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,7 +1,7 @@
1
1
  {
2
2
  "name": "markov-cli",
3
- "version": "1.0.7",
4
- "description": "LivingCloud's CLI AI Agent",
3
+ "version": "1.0.9",
4
+ "description": "Markov CLI",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "markov": "./bin/markov.js"
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import boxen from 'boxen';
2
+
3
3
  import gradient from 'gradient-string';
4
4
  import { homedir } from 'os';
5
5
  import { resolve } from 'path';
@@ -8,6 +8,7 @@ import { printLogo } from './ui/logo.js';
8
8
  import { chatWithTools, MODEL, MODELS, setModel } from './ollama.js';
9
9
  import { resolveFileRefs } from './files.js';
10
10
  import { execCommand, AGENT_TOOLS, runTool } from './tools.js';
11
+ import { parseEdits, renderDiff, applyEdit } from './editor.js';
11
12
  import { chatPrompt } from './input.js';
12
13
  import { getFiles, getFilesAndDirs } from './ui/picker.js';
13
14
  import { getToken, login, clearToken } from './auth.js';
@@ -154,7 +155,29 @@ function wrapText(text, width) {
154
155
  }).join('\n');
155
156
  }
156
157
 
157
- /** Parse fenced code blocks (```lang\n...\n```) and render them with boxen. Non-code segments are wrapped. */
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. */
158
181
  function formatResponseWithCodeBlocks(text, width) {
159
182
  if (!text || typeof text !== 'string') return '';
160
183
  const re = /```(\w*)\n([\s\S]*?)```/g;
@@ -176,17 +199,50 @@ function formatResponseWithCodeBlocks(text, width) {
176
199
  if (parts.length === 0) return wrapText(text, width);
177
200
  return parts.map((p) => {
178
201
  if (p.type === 'text') return wrapText(p.content, width);
179
- const title = p.lang ? p.lang : 'code';
180
- return boxen(p.content, {
181
- borderColor: 'cyan',
182
- borderStyle: 'round',
183
- title,
184
- padding: 1,
185
- });
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));
186
206
  }).join('\n\n');
187
207
  }
188
208
 
189
- /** Shared system message base: Markov intro, cwd, file list (no !!run instructions). */
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
+ /** Shared system message base: Markov intro, cwd, file list. */
190
246
  function getSystemMessageBase() {
191
247
  const files = getFiles();
192
248
  const fileList = files.length > 0 ? `\nFiles in working directory:\n${files.map(f => ` ${f}`).join('\n')}\n` : '';
@@ -286,10 +342,13 @@ async function runAgentLoop(messages, opts = {}) {
286
342
  const onBeforeToolRun = opts.onBeforeToolRun;
287
343
  const onToolCall = opts.onToolCall;
288
344
  const onToolResult = opts.onToolResult;
345
+ const onIteration = opts.onIteration;
346
+ const onThinking = opts.onThinking;
289
347
  let iteration = 0;
290
348
 
291
349
  while (iteration < AGENT_LOOP_MAX_ITERATIONS) {
292
350
  iteration += 1;
351
+ onThinking?.(iteration);
293
352
  const data = await chatWithTools(messages, AGENT_TOOLS, MODEL, opts.signal ?? null);
294
353
 
295
354
  const message = data?.message;
@@ -310,6 +369,7 @@ async function runAgentLoop(messages, opts = {}) {
310
369
  });
311
370
 
312
371
  onBeforeToolRun?.();
372
+ onIteration?.(iteration, AGENT_LOOP_MAX_ITERATIONS, toolCalls.length);
313
373
 
314
374
  for (const tc of toolCalls) {
315
375
  const name = tc?.function?.name;
@@ -440,8 +500,17 @@ export async function startInteractive() {
440
500
  console.log(chalk.yellow('⚠ Not logged in. Use /login to authenticate.\n'));
441
501
  }
442
502
 
503
+ let pendingMessage = null;
504
+
443
505
  while (true) {
444
- const raw = await chatPrompt(chalk.magenta('you> '), allFiles);
506
+ let raw;
507
+ if (pendingMessage) {
508
+ raw = pendingMessage;
509
+ pendingMessage = null;
510
+ console.log(chalk.magenta('you> ') + raw + '\n');
511
+ } else {
512
+ raw = await chatPrompt(chalk.magenta('you> '), allFiles);
513
+ }
445
514
  if (raw === null) continue;
446
515
  const trimmed = raw.trim();
447
516
 
@@ -531,13 +600,14 @@ export async function startInteractive() {
531
600
 
532
601
  // /agent [prompt] — run with tools (run_terminal_command, create_folder, file tools), loop until final response
533
602
  if (trimmed === '/agent' || trimmed.startsWith('/agent ')) {
534
- const userContent = trimmed.startsWith('/agent ')
603
+ const rawUserContent = trimmed.startsWith('/agent ')
535
604
  ? trimmed.slice(7).trim()
536
605
  : (await promptLine(chalk.bold('Agent prompt: '))).trim();
537
- if (!userContent) {
606
+ if (!rawUserContent) {
538
607
  console.log(chalk.yellow('No prompt given.\n'));
539
608
  continue;
540
609
  }
610
+ const userContent = (await getLsContext()) + rawUserContent;
541
611
  chatMessages.push({ role: 'user', content: userContent });
542
612
  const agentMessages = [buildAgentSystemMessage(), ...chatMessages];
543
613
  const abortController = new AbortController();
@@ -549,13 +619,25 @@ export async function startInteractive() {
549
619
  return confirm(chalk.bold('Apply this change? [y/N] '));
550
620
  };
551
621
 
622
+ const startTime = Date.now();
552
623
  const DOTS = ['.', '..', '...'];
553
624
  let dotIdx = 0;
554
- process.stdout.write(chalk.dim('\nAgent '));
555
- const spinner = setInterval(() => {
556
- process.stdout.write('\r' + chalk.dim('Agent › ') + agentGradient(DOTS[dotIdx % DOTS.length]) + ' ');
557
- dotIdx++;
558
- }, 400);
625
+ let spinner = null;
626
+
627
+ const startSpinner = () => {
628
+ if (spinner) { clearInterval(spinner); spinner = null; }
629
+ dotIdx = 0;
630
+ process.stdout.write(chalk.dim('\nAgent › '));
631
+ spinner = setInterval(() => {
632
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
633
+ process.stdout.write('\r' + chalk.dim('Agent › ') + chalk.dim(elapsed + 's ') + agentGradient(DOTS[dotIdx % DOTS.length]) + ' ');
634
+ dotIdx++;
635
+ }, 400);
636
+ };
637
+ const stopSpinner = () => {
638
+ if (spinner) { clearInterval(spinner); spinner = null; }
639
+ process.stdout.write('\r\x1b[0J');
640
+ };
559
641
 
560
642
  try {
561
643
  const result = await runAgentLoop(agentMessages, {
@@ -563,9 +645,17 @@ export async function startInteractive() {
563
645
  cwd: process.cwd(),
564
646
  confirmFn,
565
647
  confirmFileEdit,
648
+ onThinking: () => {
649
+ startSpinner();
650
+ },
566
651
  onBeforeToolRun: () => {
567
- clearInterval(spinner);
568
- process.stdout.write('\r\x1b[0J');
652
+ stopSpinner();
653
+ },
654
+ onIteration: (iter, max, toolCount) => {
655
+ const w = process.stdout.columns || 80;
656
+ const label = ` Step ${iter} `;
657
+ const line = chalk.dim('──') + chalk.bold.white(label) + chalk.dim('─'.repeat(Math.max(0, w - label.length - 2)));
658
+ console.log(line);
569
659
  },
570
660
  onToolCall: (name, args) => {
571
661
  const summary = formatToolCallSummary(name, args);
@@ -575,18 +665,23 @@ export async function startInteractive() {
575
665
  console.log(chalk.dim(' ') + formatToolResultSummary(name, resultStr));
576
666
  },
577
667
  });
578
- clearInterval(spinner);
579
- process.stdout.write('\r\x1b[0J');
668
+ stopSpinner();
580
669
  if (result) {
581
670
  chatMessages.push(result.finalMessage);
671
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
582
672
  const width = Math.min(process.stdout.columns || 80, 80);
583
673
  process.stdout.write(formatResponseWithCodeBlocks(result.content, width) + '\n\n');
674
+ await applyCodeBlockEdits(result.content, []);
584
675
  allFiles = getFilesAndDirs();
585
- console.log(chalk.green('✓ Agent done.\n'));
676
+ console.log(chalk.green(`✓ Agent done.`) + chalk.dim(` (${elapsed}s)\n`));
677
+ const opts1 = extractNumberedOptions(result.content);
678
+ if (opts1.length >= 2) {
679
+ const chosen = await selectFrom(opts1, 'Select an option:');
680
+ if (chosen) pendingMessage = chosen;
681
+ }
586
682
  }
587
683
  } catch (err) {
588
- clearInterval(spinner);
589
- process.stdout.write('\r\x1b[0J');
684
+ stopSpinner();
590
685
  if (!abortController.signal?.aborted) console.log(chalk.red(`\n${err.message}\n`));
591
686
  }
592
687
  continue;
@@ -600,7 +695,8 @@ export async function startInteractive() {
600
695
  if (failed.length > 0) {
601
696
  console.log(chalk.yellow(`\n⚠ not found: ${failed.map(f => `@${f}`).join(', ')}`));
602
697
  }
603
- const userContent = resolvedContent ?? trimmed;
698
+ const rawUserContent = resolvedContent ?? trimmed;
699
+ const userContent = (await getLsContext()) + rawUserContent;
604
700
  chatMessages.push({ role: 'user', content: userContent });
605
701
 
606
702
  const agentMessages = [buildAgentSystemMessage(), ...chatMessages];
@@ -612,22 +708,43 @@ export async function startInteractive() {
612
708
  console.log('');
613
709
  return confirm(chalk.bold('Apply this change? [y/N] '));
614
710
  };
711
+ const startTime = Date.now();
615
712
  const DOTS = ['.', '..', '...'];
616
713
  let dotIdx = 0;
617
- process.stdout.write(chalk.dim('\nAgent '));
618
- const spinner = setInterval(() => {
619
- process.stdout.write('\r' + chalk.dim('Agent › ') + agentGradient(DOTS[dotIdx % DOTS.length]) + ' ');
620
- dotIdx++;
621
- }, 400);
714
+ let spinner = null;
715
+
716
+ const startSpinner = () => {
717
+ if (spinner) { clearInterval(spinner); spinner = null; }
718
+ dotIdx = 0;
719
+ process.stdout.write(chalk.dim('\nAgent › '));
720
+ spinner = setInterval(() => {
721
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
722
+ process.stdout.write('\r' + chalk.dim('Agent › ') + chalk.dim(elapsed + 's ') + agentGradient(DOTS[dotIdx % DOTS.length]) + ' ');
723
+ dotIdx++;
724
+ }, 400);
725
+ };
726
+ const stopSpinner = () => {
727
+ if (spinner) { clearInterval(spinner); spinner = null; }
728
+ process.stdout.write('\r\x1b[0J');
729
+ };
730
+
622
731
  try {
623
732
  const result = await runAgentLoop(agentMessages, {
624
733
  signal: abortController.signal,
625
734
  cwd: process.cwd(),
626
735
  confirmFn,
627
736
  confirmFileEdit,
737
+ onThinking: () => {
738
+ startSpinner();
739
+ },
628
740
  onBeforeToolRun: () => {
629
- clearInterval(spinner);
630
- process.stdout.write('\r\x1b[0J');
741
+ stopSpinner();
742
+ },
743
+ onIteration: (iter, max, toolCount) => {
744
+ const w = process.stdout.columns || 80;
745
+ const label = ` Step ${iter} `;
746
+ const line = chalk.dim('──') + chalk.bold.white(label) + chalk.dim('─'.repeat(Math.max(0, w - label.length - 2)));
747
+ console.log(line);
631
748
  },
632
749
  onToolCall: (name, args) => {
633
750
  const summary = formatToolCallSummary(name, args);
@@ -637,18 +754,23 @@ export async function startInteractive() {
637
754
  console.log(chalk.dim(' ') + formatToolResultSummary(name, resultStr));
638
755
  },
639
756
  });
640
- clearInterval(spinner);
641
- process.stdout.write('\r\x1b[0J');
757
+ stopSpinner();
642
758
  if (result) {
643
759
  chatMessages.push(result.finalMessage);
760
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
644
761
  const width = Math.min(process.stdout.columns || 80, 80);
645
762
  process.stdout.write(formatResponseWithCodeBlocks(result.content, width) + '\n\n');
763
+ await applyCodeBlockEdits(result.content, loaded);
646
764
  allFiles = getFilesAndDirs();
647
- console.log(chalk.green('✓ Agent done.\n'));
765
+ console.log(chalk.green(`✓ Agent done.`) + chalk.dim(` (${elapsed}s)\n`));
766
+ const opts2 = extractNumberedOptions(result.content);
767
+ if (opts2.length >= 2) {
768
+ const chosen = await selectFrom(opts2, 'Select an option:');
769
+ if (chosen) pendingMessage = chosen;
770
+ }
648
771
  }
649
772
  } catch (err) {
650
- clearInterval(spinner);
651
- process.stdout.write('\r\x1b[0J');
773
+ stopSpinner();
652
774
  if (!abortController.signal?.aborted) console.log(chalk.red(`\n${err.message}\n`));
653
775
  }
654
776
  }
package/src/ollama.js CHANGED
@@ -4,8 +4,8 @@ const getHeaders = () => {
4
4
  const token = getToken();
5
5
  return { 'Content-Type': 'application/json', ...(token && { Authorization: `Bearer ${token}` }) };
6
6
  };
7
- export const MODELS = ['gemma3:4b', 'qwen2.5-coder:0.5b', 'qwen2.5-coder:7b', 'qwen2.5:14b-instruct', 'qwen3:14b'];
8
- export let MODEL = 'qwen3:14b';
7
+ export const MODELS = ['llama3:latest', 'gpt-oss:20b', 'qwen3:14b', 'qwen2.5:14b-instruct', 'qwen3.5:9b'];
8
+ export let MODEL = 'qwen3.5:9b';
9
9
  export function setModel(m) { MODEL = m; }
10
10
 
11
11
  /**
package/src/ui/logo.js CHANGED
@@ -1,16 +1,30 @@
1
1
  import gradient from 'gradient-string';
2
2
 
3
3
  const ASCII_ART = `
4
- ███╗ ███╗ █████╗ ██████╗ ██╗ ██╗ ██████╗ ██╗ ██╗
5
- ████╗ ████║██╔══██╗██╔══██╗██║ ██╔╝██╔═══██╗██║ ██║
6
- ██╔████╔██║███████║██████╔╝█████╔╝ ██ ██║██║ ██║
7
- ██║╚██╔╝██║██╔══██║██╔══██╗██╔═██╗ ██║ ██║╚██╗ ██╔╝
8
- ██║ ╚═╝ ██║██║ ██║██║ ██║██║ ██╗╚██████╔╝ ╚████╔╝
9
- ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝
4
+ ▗▖ ▗▖▗▞▀▜▌ ▄▄▄ ▄ ▄▄▄ ▄ ▄
5
+ ▐▛▚▞▜▌▝▚▄▟▌█ █▄▀ █ █ █ █
6
+ ▐▌ ▐▌ █ ▀▄ ▀▄▄▄▀ ▀▄▀
7
+ ▐▌ ▐▌ █
10
8
  `;
11
9
 
12
- const markovGradient = gradient(['#6366f1', '#a855f7', '#ec4899']);
10
+ const ASCII_ART3 = `
11
+ ███╗ ███╗ █████╗ ██████╗ ██╗ ██╗ ██████╗ ██╗ ██╗
12
+ ████╗ ████║██╔══██╗██╔══██╗██║ ██╔╝██╔═══██╗██║ ██║
13
+ ██╔████╔██║███████║██████╔╝█████╔╝ ██║ ██║██║ ██║
14
+ ██║╚██╔╝██║██╔══██║██╔══██╗██╔═██╗ ██║ ██║╚██╗ ██╔╝
15
+ ██║ ╚═╝ ██║██║ ██║██║ ██║██║ ██╗╚██████╔╝ ╚████╔╝
16
+ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝
17
+ `;
18
+
19
+ const ASCII_ART2 = `
20
+ ██▄ ▄██ ▄▄▄ ▄▄▄▄ ▄▄ ▄▄ ▄▄▄ ▄▄ ▄▄
21
+ ██ ▀▀ ██ ██▀██ ██▄█▄ ██▄█▀ ██▀██ ██▄██
22
+ ██ ██ ██▀██ ██ ██ ██ ██ ▀███▀ ▀█▀
23
+ `;
24
+
25
+
26
+ const markovGradient = gradient(['#374151', '#4b5563', '#6b7280', '#4b5563', '#374151']);
13
27
 
14
28
  export function printLogo() {
15
- console.log(markovGradient.multiline(ASCII_ART + ' LivingCloud\'s AI Agent'));
29
+ console.log(markovGradient.multiline(ASCII_ART3));
16
30
  }