markov-cli 1.0.7 → 1.0.8

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.8",
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,38 @@ 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
+ /** Shared system message base: Markov intro, cwd, file list. */
190
234
  function getSystemMessageBase() {
191
235
  const files = getFiles();
192
236
  const fileList = files.length > 0 ? `\nFiles in working directory:\n${files.map(f => ` ${f}`).join('\n')}\n` : '';
@@ -286,10 +330,13 @@ async function runAgentLoop(messages, opts = {}) {
286
330
  const onBeforeToolRun = opts.onBeforeToolRun;
287
331
  const onToolCall = opts.onToolCall;
288
332
  const onToolResult = opts.onToolResult;
333
+ const onIteration = opts.onIteration;
334
+ const onThinking = opts.onThinking;
289
335
  let iteration = 0;
290
336
 
291
337
  while (iteration < AGENT_LOOP_MAX_ITERATIONS) {
292
338
  iteration += 1;
339
+ onThinking?.(iteration);
293
340
  const data = await chatWithTools(messages, AGENT_TOOLS, MODEL, opts.signal ?? null);
294
341
 
295
342
  const message = data?.message;
@@ -310,6 +357,7 @@ async function runAgentLoop(messages, opts = {}) {
310
357
  });
311
358
 
312
359
  onBeforeToolRun?.();
360
+ onIteration?.(iteration, AGENT_LOOP_MAX_ITERATIONS, toolCalls.length);
313
361
 
314
362
  for (const tc of toolCalls) {
315
363
  const name = tc?.function?.name;
@@ -440,8 +488,17 @@ export async function startInteractive() {
440
488
  console.log(chalk.yellow('⚠ Not logged in. Use /login to authenticate.\n'));
441
489
  }
442
490
 
491
+ let pendingMessage = null;
492
+
443
493
  while (true) {
444
- const raw = await chatPrompt(chalk.magenta('you> '), allFiles);
494
+ let raw;
495
+ if (pendingMessage) {
496
+ raw = pendingMessage;
497
+ pendingMessage = null;
498
+ console.log(chalk.magenta('you> ') + raw + '\n');
499
+ } else {
500
+ raw = await chatPrompt(chalk.magenta('you> '), allFiles);
501
+ }
445
502
  if (raw === null) continue;
446
503
  const trimmed = raw.trim();
447
504
 
@@ -549,13 +606,25 @@ export async function startInteractive() {
549
606
  return confirm(chalk.bold('Apply this change? [y/N] '));
550
607
  };
551
608
 
609
+ const startTime = Date.now();
552
610
  const DOTS = ['.', '..', '...'];
553
611
  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);
612
+ let spinner = null;
613
+
614
+ const startSpinner = () => {
615
+ if (spinner) { clearInterval(spinner); spinner = null; }
616
+ dotIdx = 0;
617
+ process.stdout.write(chalk.dim('\nAgent › '));
618
+ spinner = setInterval(() => {
619
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
620
+ process.stdout.write('\r' + chalk.dim('Agent › ') + chalk.dim(elapsed + 's ') + agentGradient(DOTS[dotIdx % DOTS.length]) + ' ');
621
+ dotIdx++;
622
+ }, 400);
623
+ };
624
+ const stopSpinner = () => {
625
+ if (spinner) { clearInterval(spinner); spinner = null; }
626
+ process.stdout.write('\r\x1b[0J');
627
+ };
559
628
 
560
629
  try {
561
630
  const result = await runAgentLoop(agentMessages, {
@@ -563,9 +632,17 @@ export async function startInteractive() {
563
632
  cwd: process.cwd(),
564
633
  confirmFn,
565
634
  confirmFileEdit,
635
+ onThinking: () => {
636
+ startSpinner();
637
+ },
566
638
  onBeforeToolRun: () => {
567
- clearInterval(spinner);
568
- process.stdout.write('\r\x1b[0J');
639
+ stopSpinner();
640
+ },
641
+ onIteration: (iter, max, toolCount) => {
642
+ const w = process.stdout.columns || 80;
643
+ const label = ` Step ${iter}/${max} `;
644
+ const line = chalk.dim('──') + chalk.bold.white(label) + chalk.dim('─'.repeat(Math.max(0, w - label.length - 2)));
645
+ console.log(line);
569
646
  },
570
647
  onToolCall: (name, args) => {
571
648
  const summary = formatToolCallSummary(name, args);
@@ -575,18 +652,23 @@ export async function startInteractive() {
575
652
  console.log(chalk.dim(' ') + formatToolResultSummary(name, resultStr));
576
653
  },
577
654
  });
578
- clearInterval(spinner);
579
- process.stdout.write('\r\x1b[0J');
655
+ stopSpinner();
580
656
  if (result) {
581
657
  chatMessages.push(result.finalMessage);
658
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
582
659
  const width = Math.min(process.stdout.columns || 80, 80);
583
660
  process.stdout.write(formatResponseWithCodeBlocks(result.content, width) + '\n\n');
661
+ await applyCodeBlockEdits(result.content, []);
584
662
  allFiles = getFilesAndDirs();
585
- console.log(chalk.green('✓ Agent done.\n'));
663
+ console.log(chalk.green(`✓ Agent done.`) + chalk.dim(` (${elapsed}s)\n`));
664
+ const opts1 = extractNumberedOptions(result.content);
665
+ if (opts1.length >= 2) {
666
+ const chosen = await selectFrom(opts1, 'Select an option:');
667
+ if (chosen) pendingMessage = chosen;
668
+ }
586
669
  }
587
670
  } catch (err) {
588
- clearInterval(spinner);
589
- process.stdout.write('\r\x1b[0J');
671
+ stopSpinner();
590
672
  if (!abortController.signal?.aborted) console.log(chalk.red(`\n${err.message}\n`));
591
673
  }
592
674
  continue;
@@ -612,22 +694,43 @@ export async function startInteractive() {
612
694
  console.log('');
613
695
  return confirm(chalk.bold('Apply this change? [y/N] '));
614
696
  };
697
+ const startTime = Date.now();
615
698
  const DOTS = ['.', '..', '...'];
616
699
  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);
700
+ let spinner = null;
701
+
702
+ const startSpinner = () => {
703
+ if (spinner) { clearInterval(spinner); spinner = null; }
704
+ dotIdx = 0;
705
+ process.stdout.write(chalk.dim('\nAgent › '));
706
+ spinner = setInterval(() => {
707
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
708
+ process.stdout.write('\r' + chalk.dim('Agent › ') + chalk.dim(elapsed + 's ') + agentGradient(DOTS[dotIdx % DOTS.length]) + ' ');
709
+ dotIdx++;
710
+ }, 400);
711
+ };
712
+ const stopSpinner = () => {
713
+ if (spinner) { clearInterval(spinner); spinner = null; }
714
+ process.stdout.write('\r\x1b[0J');
715
+ };
716
+
622
717
  try {
623
718
  const result = await runAgentLoop(agentMessages, {
624
719
  signal: abortController.signal,
625
720
  cwd: process.cwd(),
626
721
  confirmFn,
627
722
  confirmFileEdit,
723
+ onThinking: () => {
724
+ startSpinner();
725
+ },
628
726
  onBeforeToolRun: () => {
629
- clearInterval(spinner);
630
- process.stdout.write('\r\x1b[0J');
727
+ stopSpinner();
728
+ },
729
+ onIteration: (iter, max, toolCount) => {
730
+ const w = process.stdout.columns || 80;
731
+ const label = ` Step ${iter}/${max} `;
732
+ const line = chalk.dim('──') + chalk.bold.white(label) + chalk.dim('─'.repeat(Math.max(0, w - label.length - 2)));
733
+ console.log(line);
631
734
  },
632
735
  onToolCall: (name, args) => {
633
736
  const summary = formatToolCallSummary(name, args);
@@ -637,18 +740,23 @@ export async function startInteractive() {
637
740
  console.log(chalk.dim(' ') + formatToolResultSummary(name, resultStr));
638
741
  },
639
742
  });
640
- clearInterval(spinner);
641
- process.stdout.write('\r\x1b[0J');
743
+ stopSpinner();
642
744
  if (result) {
643
745
  chatMessages.push(result.finalMessage);
746
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
644
747
  const width = Math.min(process.stdout.columns || 80, 80);
645
748
  process.stdout.write(formatResponseWithCodeBlocks(result.content, width) + '\n\n');
749
+ await applyCodeBlockEdits(result.content, loaded);
646
750
  allFiles = getFilesAndDirs();
647
- console.log(chalk.green('✓ Agent done.\n'));
751
+ console.log(chalk.green(`✓ Agent done.`) + chalk.dim(` (${elapsed}s)\n`));
752
+ const opts2 = extractNumberedOptions(result.content);
753
+ if (opts2.length >= 2) {
754
+ const chosen = await selectFrom(opts2, 'Select an option:');
755
+ if (chosen) pendingMessage = chosen;
756
+ }
648
757
  }
649
758
  } catch (err) {
650
- clearInterval(spinner);
651
- process.stdout.write('\r\x1b[0J');
759
+ stopSpinner();
652
760
  if (!abortController.signal?.aborted) console.log(chalk.red(`\n${err.message}\n`));
653
761
  }
654
762
  }
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
  }