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 +2 -2
- package/src/interactive.js +160 -38
- package/src/ollama.js +2 -2
- 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,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
|
|
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
|
+
/** 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
|
-
|
|
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
|
|
603
|
+
const rawUserContent = trimmed.startsWith('/agent ')
|
|
535
604
|
? trimmed.slice(7).trim()
|
|
536
605
|
: (await promptLine(chalk.bold('Agent prompt: '))).trim();
|
|
537
|
-
if (!
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
568
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
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
|
-
|
|
630
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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 = ['
|
|
8
|
-
export let MODEL = 'qwen3:
|
|
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
|
|
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
|
}
|