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 +2 -2
- package/src/interactive.js +143 -35
- package/src/ui/logo.js +22 -8
package/package.json
CHANGED
package/src/interactive.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
|
|
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
|
-
/**
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
568
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
630
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
29
|
+
console.log(markovGradient.multiline(ASCII_ART3));
|
|
16
30
|
}
|