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/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
- ...CLAUDE_MODELS.map((o) => ({ label: o.label, provider: 'claude', model: o.model })),
64
- ...OPENAI_MODELS.map((o) => ({ label: o.label, provider: 'openai', model: o.model })),
65
- ...MODELS.map((m) => ({ label: `Ollama ${m}`, provider: 'ollama', model: m })),
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(['#4ade80', '#6ee7b7', '#38bdf8']);
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(ASCII_ART4));
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 === '\x03' || key === '\x11') { cleanup(); resolve(null); return; }
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 onKey = (key) => {
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
- process.stdin.removeListener('data', onData);
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
- process.stdin.removeListener('data', onData);
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 DOTS = ['.', '..', '...'];
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(chalk.dim(`\n${label}`));
49
+ process.stdout.write('\n\n');
50
+ renderFrame(resolvedLabel, startTime, dotIdx, renderOpts);
51
+ dotIdx++;
22
52
  interval = setInterval(() => {
23
- const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
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
- }, 400);
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
+ }