markov-cli 1.0.15 → 1.0.17
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/.env.example +2 -0
- package/package.json +1 -1
- package/src/claude.js +27 -0
- package/src/editor/codeBlockEdits.js +1 -1
- package/src/input.js +39 -13
- package/src/interactive.js +242 -190
- package/src/ollama.js +42 -6
- package/src/tools.js +30 -1
- package/src/ui/logo.js +9 -17
- package/src/ui/prompts.js +57 -9
- package/src/ui/spinner.js +91 -5
package/src/ollama.js
CHANGED
|
@@ -43,9 +43,9 @@ const getHeaders = () => {
|
|
|
43
43
|
|
|
44
44
|
/** Claude models: { label, model } */
|
|
45
45
|
export const CLAUDE_MODELS = [
|
|
46
|
+
{ label: 'Claude Opus 4.6', model: 'claude-opus-4-6' },
|
|
47
|
+
{ label: 'Claude Sonnet 4.6', model: 'claude-sonnet-4-6' },
|
|
46
48
|
{ label: 'Claude Haiku 4.5', model: 'claude-haiku-4-5-20251001' },
|
|
47
|
-
{ label: 'Claude Sonnet 4', model: 'claude-sonnet-4-20250514' },
|
|
48
|
-
{ label: 'Claude Opus 4', model: 'claude-opus-4-20250514' },
|
|
49
49
|
];
|
|
50
50
|
|
|
51
51
|
/** OpenAI models: { label, model } */
|
|
@@ -58,13 +58,49 @@ export const OPENAI_MODELS = [
|
|
|
58
58
|
/** Ollama models (backend) */
|
|
59
59
|
export const MODELS = ['qwen3.5:0.8b', 'qwen3.5:2b', 'qwen3.5:4b', 'qwen3.5:9b', 'qwen3.5:397b-cloud'];
|
|
60
60
|
|
|
61
|
+
let cachedClaudeModels = [...CLAUDE_MODELS];
|
|
62
|
+
|
|
63
|
+
const mapOptions = (provider, models, labelPrefix = '') =>
|
|
64
|
+
models.map((entry) => ({
|
|
65
|
+
label: labelPrefix ? `${labelPrefix}${entry}` : entry.label,
|
|
66
|
+
provider,
|
|
67
|
+
model: labelPrefix ? entry : entry.model,
|
|
68
|
+
}));
|
|
69
|
+
|
|
61
70
|
/** Combined options for /models picker: { label, provider, model } */
|
|
62
71
|
export const MODEL_OPTIONS = [
|
|
63
|
-
...
|
|
64
|
-
...
|
|
65
|
-
...
|
|
72
|
+
...mapOptions('claude', CLAUDE_MODELS),
|
|
73
|
+
...mapOptions('openai', OPENAI_MODELS),
|
|
74
|
+
...mapOptions('ollama', MODELS, 'Ollama '),
|
|
66
75
|
];
|
|
67
76
|
|
|
77
|
+
export async function getModelOptions(signal = null) {
|
|
78
|
+
let claudeModels = cachedClaudeModels;
|
|
79
|
+
let warning = null;
|
|
80
|
+
|
|
81
|
+
if (hasClaudeKey()) {
|
|
82
|
+
try {
|
|
83
|
+
const liveClaudeModels = await claude.listAvailableModels(signal);
|
|
84
|
+
if (liveClaudeModels.length > 0) {
|
|
85
|
+
cachedClaudeModels = liveClaudeModels;
|
|
86
|
+
claudeModels = liveClaudeModels;
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
claudeModels = cachedClaudeModels;
|
|
90
|
+
warning = `Could not load latest Claude models; using fallback list. ${err.message}`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
options: [
|
|
96
|
+
...mapOptions('claude', claudeModels),
|
|
97
|
+
...mapOptions('openai', OPENAI_MODELS),
|
|
98
|
+
...mapOptions('ollama', MODELS, 'Ollama '),
|
|
99
|
+
],
|
|
100
|
+
warning,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
68
104
|
export let PROVIDER = 'ollama';
|
|
69
105
|
export let MODEL = 'qwen3.5:4b';
|
|
70
106
|
|
|
@@ -79,7 +115,7 @@ export function setModelAndProvider(provider, model) {
|
|
|
79
115
|
|
|
80
116
|
export function getModelDisplayName() {
|
|
81
117
|
if (PROVIDER === 'claude') {
|
|
82
|
-
const found = CLAUDE_MODELS.find((o) => o.model === MODEL);
|
|
118
|
+
const found = [...cachedClaudeModels, ...CLAUDE_MODELS].find((o) => o.model === MODEL);
|
|
83
119
|
return found ? found.label : `Claude ${MODEL}`;
|
|
84
120
|
}
|
|
85
121
|
if (PROVIDER === 'openai') {
|
package/src/tools.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { exec } from 'child_process';
|
|
1
|
+
import { exec, spawn } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import { mkdirSync, readFileSync, writeFileSync, existsSync, unlinkSync, readdirSync, statSync } from 'fs';
|
|
4
4
|
import { resolve, dirname } from 'path';
|
|
@@ -425,6 +425,35 @@ export async function execCommand(command, cwd = process.cwd()) {
|
|
|
425
425
|
}
|
|
426
426
|
}
|
|
427
427
|
|
|
428
|
+
/**
|
|
429
|
+
* Execute a shell command and stream its output.
|
|
430
|
+
* @param {string} command
|
|
431
|
+
* @param {string} [cwd]
|
|
432
|
+
* @returns {Promise<number>} exit code
|
|
433
|
+
*/
|
|
434
|
+
export function spawnCommand(command, cwd = process.cwd()) {
|
|
435
|
+
return new Promise((resolve) => {
|
|
436
|
+
if (command == null || typeof command !== 'string' || !command.trim()) {
|
|
437
|
+
return resolve(1);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const child = spawn(command, {
|
|
441
|
+
cwd,
|
|
442
|
+
shell: true,
|
|
443
|
+
stdio: 'inherit' // This pipes stdout and stderr directly to the terminal
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
child.on('error', (err) => {
|
|
447
|
+
console.error(`\nFailed to start command: ${err.message}`);
|
|
448
|
+
resolve(1);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
child.on('close', (code) => {
|
|
452
|
+
resolve(code ?? 1);
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
428
457
|
/**
|
|
429
458
|
* Run a tool by name with the given arguments.
|
|
430
459
|
* @param {string} name - Tool name (e.g. 'run_terminal_command')
|
package/src/ui/logo.js
CHANGED
|
@@ -6,14 +6,9 @@ const ASCII_ART = `
|
|
|
6
6
|
██╔████╔██║███████║██████╔╝█████╔╝ ██║ ██║██║ ██║
|
|
7
7
|
██║╚██╔╝██║██╔══██║██╔══██╗██╔═██╗ ██║ ██║╚██╗ ██╔╝
|
|
8
8
|
██║ ╚═╝ ██║██║ ██║██║ ██║██║ ██╗╚██████╔╝ ╚████╔╝
|
|
9
|
-
╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
██╔════╝██╔═══██╗██╔══██╗██╔════╝
|
|
13
|
-
██║ ██║ ██║██║ ██║█████╗
|
|
14
|
-
██║ ██║ ██║██║ ██║██╔══╝
|
|
15
|
-
╚██████╗╚██████╔╝██████╔╝███████╗
|
|
16
|
-
╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
|
|
9
|
+
╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝
|
|
10
|
+
▜▘█▌▛▘▛▛▌▌▛▌▀▌▐ ▜▘▛▌▛▌▐
|
|
11
|
+
▐▖▙▖▌ ▌▌▌▌▌▌█▌▐▖ ▐▖▙▌▙▌▐▖
|
|
17
12
|
`;
|
|
18
13
|
|
|
19
14
|
const ASCII_ART4 = `
|
|
@@ -32,13 +27,8 @@ const ASCII_ART3 = `
|
|
|
32
27
|
██║╚██╔╝██║██╔══██║██╔══██╗██╔═██╗ ██║ ██║╚██╗ ██╔╝
|
|
33
28
|
██║ ╚═╝ ██║██║ ██║██║ ██║██║ ██╗╚██████╔╝ ╚████╔╝
|
|
34
29
|
╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
██╔════╝██╔═══██╗██╔══██╗██╔════╝
|
|
38
|
-
██║ ██║ ██║██║ ██║█████╗
|
|
39
|
-
██║ ██║ ██║██║ ██║██╔══╝
|
|
40
|
-
╚██████╗╚██████╔╝██████╔╝███████╗
|
|
41
|
-
╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
|
|
30
|
+
▜▘█▌▛▘▛▛▌▌▛▌▀▌▐
|
|
31
|
+
▐▖▙▖▌ ▌▌▌▌▌▌█▌▐▖
|
|
42
32
|
`;
|
|
43
33
|
|
|
44
34
|
const ASCII_ART2 = `
|
|
@@ -50,8 +40,10 @@ C8888 888 e Y8b Y8b "8" 888 888 " 888 P d888 888b Y8b Y8P
|
|
|
50
40
|
`;
|
|
51
41
|
|
|
52
42
|
|
|
53
|
-
const markovGradient = gradient(['#
|
|
43
|
+
const markovGradient = gradient(['#6ee7b7','#6ee7b7']);
|
|
44
|
+
// const markovGradient = gradient(['#4ade80', '#6ee7b7', '#38bdf8']);
|
|
45
|
+
|
|
54
46
|
|
|
55
47
|
export function printLogo() {
|
|
56
|
-
console.log(markovGradient.multiline(
|
|
48
|
+
console.log(markovGradient.multiline(ASCII_ART));
|
|
57
49
|
}
|
package/src/ui/prompts.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
|
|
3
|
+
export const PROMPT_INTERRUPT = { type: 'interrupt' };
|
|
4
|
+
const CTRL_C = '\x03';
|
|
5
|
+
const CTRL_Q = '\x11';
|
|
6
|
+
|
|
3
7
|
/** Arrow-key selector. Returns the chosen string or null if cancelled. */
|
|
4
8
|
export function selectFrom(options, label) {
|
|
5
9
|
return new Promise((resolve) => {
|
|
@@ -32,7 +36,8 @@ export function selectFrom(options, label) {
|
|
|
32
36
|
if (key === '\x1b[A') { idx = (idx - 1 + options.length) % options.length; draw(); return; }
|
|
33
37
|
if (key === '\x1b[B') { idx = (idx + 1) % options.length; draw(); return; }
|
|
34
38
|
if (key === '\r' || key === '\n') { cleanup(); resolve(options[idx]); return; }
|
|
35
|
-
if (key ===
|
|
39
|
+
if (key === CTRL_C) { cleanup(); resolve(PROMPT_INTERRUPT); return; }
|
|
40
|
+
if (key === CTRL_Q) { cleanup(); resolve(null); return; }
|
|
36
41
|
};
|
|
37
42
|
|
|
38
43
|
process.stdin.setRawMode(true);
|
|
@@ -43,17 +48,30 @@ export function selectFrom(options, label) {
|
|
|
43
48
|
});
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
/** Prompt y/n in raw mode, returns true for y/Y. */
|
|
51
|
+
/** Prompt y/n in raw mode, returns true for y/Y, false for no/cancel, or PROMPT_INTERRUPT on Ctrl+C. */
|
|
47
52
|
export function confirm(question) {
|
|
48
53
|
return new Promise((resolve) => {
|
|
49
54
|
process.stdout.write(question);
|
|
50
55
|
process.stdin.setRawMode(true);
|
|
51
56
|
process.stdin.resume();
|
|
52
57
|
process.stdin.setEncoding('utf8');
|
|
53
|
-
const
|
|
58
|
+
const cleanup = () => {
|
|
54
59
|
process.stdin.removeListener('data', onKey);
|
|
55
60
|
process.stdin.setRawMode(false);
|
|
56
61
|
process.stdin.pause();
|
|
62
|
+
};
|
|
63
|
+
const onKey = (key) => {
|
|
64
|
+
cleanup();
|
|
65
|
+
if (key === CTRL_C) {
|
|
66
|
+
process.stdout.write(chalk.dim('(cancelled)\n'));
|
|
67
|
+
resolve(PROMPT_INTERRUPT);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (key === CTRL_Q) {
|
|
71
|
+
process.stdout.write(chalk.dim('(cancelled)\n'));
|
|
72
|
+
resolve(false);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
57
75
|
const answer = key.toLowerCase() === 'y';
|
|
58
76
|
process.stdout.write(answer ? chalk.green('y\n') : chalk.dim('n\n'));
|
|
59
77
|
resolve(answer);
|
|
@@ -67,12 +85,27 @@ export function promptLine(label) {
|
|
|
67
85
|
return new Promise((resolve) => {
|
|
68
86
|
process.stdout.write(label);
|
|
69
87
|
let buf = '';
|
|
88
|
+
const cleanup = () => {
|
|
89
|
+
process.stdin.removeListener('data', onData);
|
|
90
|
+
process.stdin.setRawMode(false);
|
|
91
|
+
process.stdin.pause();
|
|
92
|
+
};
|
|
70
93
|
const onData = (data) => {
|
|
71
94
|
const key = data.toString();
|
|
95
|
+
if (key === CTRL_C) {
|
|
96
|
+
cleanup();
|
|
97
|
+
process.stdout.write(chalk.dim('(cancelled)\n'));
|
|
98
|
+
resolve(PROMPT_INTERRUPT);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (key === CTRL_Q) {
|
|
102
|
+
cleanup();
|
|
103
|
+
process.stdout.write(chalk.dim('(cancelled)\n'));
|
|
104
|
+
resolve(null);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
72
107
|
if (key === '\r' || key === '\n') {
|
|
73
|
-
|
|
74
|
-
process.stdin.setRawMode(false);
|
|
75
|
-
process.stdin.pause();
|
|
108
|
+
cleanup();
|
|
76
109
|
process.stdout.write('\n');
|
|
77
110
|
resolve(buf);
|
|
78
111
|
} else if (key === '\x7f' || key === '\b') {
|
|
@@ -94,12 +127,27 @@ export function promptSecret(label) {
|
|
|
94
127
|
return new Promise((resolve) => {
|
|
95
128
|
process.stdout.write(label);
|
|
96
129
|
let buf = '';
|
|
130
|
+
const cleanup = () => {
|
|
131
|
+
process.stdin.removeListener('data', onData);
|
|
132
|
+
process.stdin.setRawMode(false);
|
|
133
|
+
process.stdin.pause();
|
|
134
|
+
};
|
|
97
135
|
const onData = (data) => {
|
|
98
136
|
const key = data.toString();
|
|
137
|
+
if (key === CTRL_C) {
|
|
138
|
+
cleanup();
|
|
139
|
+
process.stdout.write(chalk.dim('(cancelled)\n'));
|
|
140
|
+
resolve(PROMPT_INTERRUPT);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (key === CTRL_Q) {
|
|
144
|
+
cleanup();
|
|
145
|
+
process.stdout.write(chalk.dim('(cancelled)\n'));
|
|
146
|
+
resolve(null);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
99
149
|
if (key === '\r' || key === '\n') {
|
|
100
|
-
|
|
101
|
-
process.stdin.setRawMode(false);
|
|
102
|
-
process.stdin.pause();
|
|
150
|
+
cleanup();
|
|
103
151
|
process.stdout.write('\n');
|
|
104
152
|
resolve(buf);
|
|
105
153
|
} else if (key === '\x7f' || key === '\b') {
|
package/src/ui/spinner.js
CHANGED
|
@@ -2,6 +2,33 @@ import chalk from 'chalk';
|
|
|
2
2
|
import gradient from 'gradient-string';
|
|
3
3
|
|
|
4
4
|
const agentGradient = gradient(['#22c55e', '#16a34a', '#4ade80']);
|
|
5
|
+
export const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
6
|
+
const SPINNER_INTERVAL_MS = 200;
|
|
7
|
+
const IDLE_SPINNER_DELAY_MS = 250;
|
|
8
|
+
const SPINNER_LABELS = [
|
|
9
|
+
'Squirming',
|
|
10
|
+
'Shadoodeling',
|
|
11
|
+
'Braincrunching',
|
|
12
|
+
'Brewing',
|
|
13
|
+
'Hacking',
|
|
14
|
+
'Debugging',
|
|
15
|
+
'Refactoring',
|
|
16
|
+
'Tinkering',
|
|
17
|
+
'Sweating',
|
|
18
|
+
'Brainstorming',
|
|
19
|
+
'Spellcasting',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
function pickSpinnerLabel() {
|
|
23
|
+
const randomLabel = SPINNER_LABELS[Math.floor(Math.random() * SPINNER_LABELS.length)];
|
|
24
|
+
return `${randomLabel} `;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function renderFrame(label, startTime, frameIdx, opts = {}) {
|
|
28
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
29
|
+
const labelText = opts.gradientLabel ? agentGradient(label) : chalk.dim(label);
|
|
30
|
+
process.stdout.write('\r' + agentGradient(SPINNER_FRAMES[frameIdx % SPINNER_FRAMES.length]) + ' ' + labelText + chalk.dim(elapsed + 's ') + ' ');
|
|
31
|
+
}
|
|
5
32
|
|
|
6
33
|
/**
|
|
7
34
|
* Create a spinner with a given label.
|
|
@@ -10,20 +37,22 @@ const agentGradient = gradient(['#22c55e', '#16a34a', '#4ade80']);
|
|
|
10
37
|
* @returns {{ stop: () => void }} A spinner handle with a stop() method
|
|
11
38
|
*/
|
|
12
39
|
export function createSpinner(label) {
|
|
13
|
-
const
|
|
40
|
+
const resolvedLabel = pickSpinnerLabel();
|
|
14
41
|
let dotIdx = 0;
|
|
15
42
|
let interval = null;
|
|
16
43
|
const startTime = Date.now();
|
|
44
|
+
const renderOpts = { gradientLabel: true };
|
|
17
45
|
|
|
18
46
|
const start = () => {
|
|
19
47
|
if (interval) clearInterval(interval);
|
|
20
48
|
dotIdx = 0;
|
|
21
|
-
process.stdout.write(
|
|
49
|
+
process.stdout.write('\n\n');
|
|
50
|
+
renderFrame(resolvedLabel, startTime, dotIdx, renderOpts);
|
|
51
|
+
dotIdx++;
|
|
22
52
|
interval = setInterval(() => {
|
|
23
|
-
|
|
24
|
-
process.stdout.write('\r' + chalk.dim(label) + chalk.dim(elapsed + 's ') + agentGradient(DOTS[dotIdx % DOTS.length]) + ' ');
|
|
53
|
+
renderFrame(resolvedLabel, startTime, dotIdx, renderOpts);
|
|
25
54
|
dotIdx++;
|
|
26
|
-
},
|
|
55
|
+
}, SPINNER_INTERVAL_MS);
|
|
27
56
|
};
|
|
28
57
|
|
|
29
58
|
const stop = () => {
|
|
@@ -38,3 +67,60 @@ export function createSpinner(label) {
|
|
|
38
67
|
|
|
39
68
|
return { stop };
|
|
40
69
|
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Create a spinner that only appears after a quiet period.
|
|
73
|
+
* Call bump() whenever output is streamed to hide/snooze it.
|
|
74
|
+
* @param {string} label - The label to display before the spinner
|
|
75
|
+
* @param {{ startTime?: number, delayMs?: number }} [opts]
|
|
76
|
+
* @returns {{ bump: () => void, pause: () => void, stop: () => void }}
|
|
77
|
+
*/
|
|
78
|
+
export function createIdleSpinner(label, opts = {}) {
|
|
79
|
+
const resolvedLabel = pickSpinnerLabel();
|
|
80
|
+
const startTime = opts.startTime ?? Date.now();
|
|
81
|
+
const delayMs = opts.delayMs ?? IDLE_SPINNER_DELAY_MS;
|
|
82
|
+
let dotIdx = 0;
|
|
83
|
+
let interval = null;
|
|
84
|
+
let timeout = null;
|
|
85
|
+
const renderOpts = { gradientLabel: opts.gradientLabel ?? true };
|
|
86
|
+
|
|
87
|
+
const clearTimeoutIfNeeded = () => {
|
|
88
|
+
if (timeout) {
|
|
89
|
+
clearTimeout(timeout);
|
|
90
|
+
timeout = null;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const hide = () => {
|
|
95
|
+
clearTimeoutIfNeeded();
|
|
96
|
+
if (interval) {
|
|
97
|
+
clearInterval(interval);
|
|
98
|
+
interval = null;
|
|
99
|
+
process.stdout.write('\r\x1b[0J');
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const start = () => {
|
|
104
|
+
clearTimeoutIfNeeded();
|
|
105
|
+
if (interval) return;
|
|
106
|
+
dotIdx = 0;
|
|
107
|
+
process.stdout.write('\n\n');
|
|
108
|
+
renderFrame(resolvedLabel, startTime, dotIdx, renderOpts);
|
|
109
|
+
dotIdx++;
|
|
110
|
+
interval = setInterval(() => {
|
|
111
|
+
renderFrame(resolvedLabel, startTime, dotIdx, renderOpts);
|
|
112
|
+
dotIdx++;
|
|
113
|
+
}, SPINNER_INTERVAL_MS);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const bump = () => {
|
|
117
|
+
hide();
|
|
118
|
+
timeout = setTimeout(start, delayMs);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
bump,
|
|
123
|
+
pause: hide,
|
|
124
|
+
stop: hide,
|
|
125
|
+
};
|
|
126
|
+
}
|