markov-cli 1.0.16 → 1.0.18
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 +1 -1
- package/src/claude.js +27 -0
- package/src/input.js +1 -1
- package/src/interactive.js +13 -18
- package/src/ollama.js +42 -6
- package/src/ui/logo.js +2 -2
- package/src/ui/spinner.js +37 -7
package/package.json
CHANGED
package/src/claude.js
CHANGED
|
@@ -24,6 +24,33 @@ function getHeaders() {
|
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* List available Claude models from Anthropic's Models API.
|
|
29
|
+
* Preserves API order so newer models appear first.
|
|
30
|
+
* Returns [{ label, model }].
|
|
31
|
+
*/
|
|
32
|
+
export async function listAvailableModels(signal = null) {
|
|
33
|
+
const res = await fetchWithRetry(
|
|
34
|
+
`${ANTHROPIC_API}/models`,
|
|
35
|
+
{ method: 'GET', headers: getHeaders() },
|
|
36
|
+
signal
|
|
37
|
+
);
|
|
38
|
+
if (!res.ok) {
|
|
39
|
+
const errBody = await res.text().catch(() => '');
|
|
40
|
+
throw new Error(`Anthropic API error ${res.status} ${res.statusText}${errBody ? ': ' + errBody : ''}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const data = await res.json();
|
|
44
|
+
const models = Array.isArray(data?.data) ? data.data : [];
|
|
45
|
+
|
|
46
|
+
return models
|
|
47
|
+
.filter((model) => model?.type === 'model' && typeof model?.id === 'string' && model.id.startsWith('claude-'))
|
|
48
|
+
.map((model) => ({
|
|
49
|
+
label: model.display_name || `Claude ${model.id}`,
|
|
50
|
+
model: model.id,
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
|
|
27
54
|
/**
|
|
28
55
|
* Fetch with retry on 429 (rate limit). Waits Retry-After seconds or default 60s, then retries up to RATE_LIMIT_MAX_RETRIES.
|
|
29
56
|
*/
|
package/src/input.js
CHANGED
|
@@ -7,7 +7,7 @@ const visibleLen = (s) => s.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
|
7
7
|
|
|
8
8
|
const PREFIX = '❯ ';
|
|
9
9
|
const HINT = chalk.dim(' Ask Markov anything...');
|
|
10
|
-
const STATUS_LEFT = chalk.dim('ctrl tab to switch mode');
|
|
10
|
+
const STATUS_LEFT = chalk.dim('ctrl + tab to switch mode');
|
|
11
11
|
const PICKER_MAX = 6;
|
|
12
12
|
|
|
13
13
|
function border() {
|
package/src/interactive.js
CHANGED
|
@@ -4,7 +4,7 @@ import { writeFileSync, readFileSync, existsSync } from 'fs';
|
|
|
4
4
|
import { homedir } from 'os';
|
|
5
5
|
import { resolve } from 'path';
|
|
6
6
|
import { printLogo } from './ui/logo.js';
|
|
7
|
-
import { chatWithTools, streamChat, streamChatWithTools, MODEL,
|
|
7
|
+
import { chatWithTools, streamChat, streamChatWithTools, MODEL, getModelOptions, setModelAndProvider, getModelDisplayName } from './ollama.js';
|
|
8
8
|
import { resolveFileRefs } from './files.js';
|
|
9
9
|
import { RUN_TERMINAL_COMMAND_TOOL, WEB_SEARCH_TOOL, runTool, execCommand, spawnCommand } from './tools.js';
|
|
10
10
|
import { chatPrompt } from './input.js';
|
|
@@ -41,35 +41,30 @@ const INTRO_TEXT =
|
|
|
41
41
|
chalk.bold('Quick start:\n') +
|
|
42
42
|
chalk.cyan(' /help') + chalk.dim(' show all commands\n') +
|
|
43
43
|
chalk.cyan(' /login') + chalk.dim(' authenticate with email & password\n') +
|
|
44
|
-
chalk.cyan(' /
|
|
44
|
+
chalk.cyan(' /cmd') + chalk.dim(' [command] run a shell command in the current folder (default)\n') +
|
|
45
|
+
chalk.cyan(' /agent') + chalk.dim('[prompt] run an agent with the current folder context\n') +
|
|
45
46
|
chalk.cyan(' /init') + chalk.dim(' [prompt] create markov.md with project summary\n') +
|
|
46
47
|
chalk.cyan(' /plan') + chalk.dim(' [prompt] stream a plan and save to plan.md\n') +
|
|
47
48
|
chalk.cyan(' /build') + chalk.dim(' execute plan from plan.md\n') +
|
|
48
|
-
chalk.cyan(' /yolo') + chalk.dim(' [prompt] plan in stream mode, then auto-run until done\n') +
|
|
49
|
-
chalk.dim('
|
|
49
|
+
chalk.cyan(' /yolo') + chalk.dim(' [prompt] plan in stream mode, then auto-run until done\n\n') +
|
|
50
|
+
chalk.dim(' Tips: Use ') + chalk.cyan('@filename') + chalk.dim(' to add file to context\n') +
|
|
51
|
+
chalk.dim(' Press ') + chalk.cyan('CTRL + TAB') + chalk.dim(' to switch mode\n');
|
|
50
52
|
|
|
51
53
|
const HELP_TEXT =
|
|
54
|
+
INTRO_TEXT +
|
|
52
55
|
'\n' +
|
|
53
|
-
chalk.bold('
|
|
56
|
+
chalk.bold('More commands:\n') +
|
|
54
57
|
chalk.cyan(' /intro') + chalk.dim(' show quick start (same as on first load)\n') +
|
|
55
|
-
chalk.cyan(' /help') + chalk.dim(' show this help\n') +
|
|
56
58
|
chalk.cyan(' /setup-nextjs') + chalk.dim(' scaffold a Next.js app\n') +
|
|
57
59
|
chalk.cyan(' /setup-tanstack') + chalk.dim(' scaffold a TanStack Start app\n') +
|
|
58
60
|
chalk.cyan(' /setup-laravel') + chalk.dim(' scaffold a Laravel API\n') +
|
|
59
61
|
chalk.cyan(' /laravel') + chalk.dim(' set up Laravel "my-blog" with blog route (agent)\n') +
|
|
60
62
|
chalk.cyan(' /models') + chalk.dim(' switch the active AI model\n') +
|
|
61
63
|
chalk.cyan(' /cd [path]') + chalk.dim(' change working directory\n') +
|
|
62
|
-
chalk.cyan(' /cmd [command]') + chalk.dim(' run a shell command in the current folder\n') +
|
|
63
|
-
chalk.cyan(' /login') + chalk.dim(' authenticate with email & password\n') +
|
|
64
64
|
chalk.cyan(' /logout') + chalk.dim(' clear saved auth token\n') +
|
|
65
65
|
chalk.cyan(' /clear') + chalk.dim(' clear chat history and stored plan\n') +
|
|
66
66
|
chalk.cyan(' /env') + chalk.dim(' show which .env vars are loaded (for debugging)\n') +
|
|
67
|
-
chalk.cyan(' /debug') + chalk.dim(' toggle full payload dump (env MARKOV_DEBUG)\n')
|
|
68
|
-
chalk.cyan(' /init') + chalk.dim(' [prompt] create markov.md with project summary\n') +
|
|
69
|
-
chalk.cyan(' /plan') + chalk.dim(' [prompt] stream a plan and save to plan.md\n') +
|
|
70
|
-
chalk.cyan(' /build') + chalk.dim(' execute plan from plan.md\n') +
|
|
71
|
-
chalk.cyan(' /yolo') + chalk.dim(' [prompt] plan in stream mode, then auto-run until done\n') +
|
|
72
|
-
chalk.dim('\nType a message · ') + chalk.cyan('@filename') + chalk.dim(' to attach · ctrl+q to cancel\n');
|
|
67
|
+
chalk.cyan(' /debug') + chalk.dim(' toggle full payload dump (env MARKOV_DEBUG)\n');
|
|
73
68
|
|
|
74
69
|
/** If MARKOV_DEBUG is set, print the raw model output after completion. */
|
|
75
70
|
function maybePrintRawModelOutput(rawText) {
|
|
@@ -86,8 +81,6 @@ export async function startInteractive() {
|
|
|
86
81
|
|
|
87
82
|
let allFiles = getFilesAndDirs();
|
|
88
83
|
const chatMessages = [];
|
|
89
|
-
|
|
90
|
-
console.log(chalk.dim(`Chat with Markov (${getModelDisplayName()}).`));
|
|
91
84
|
console.log(INTRO_TEXT);
|
|
92
85
|
|
|
93
86
|
if (!getToken()) {
|
|
@@ -673,10 +666,12 @@ export async function startInteractive() {
|
|
|
673
666
|
|
|
674
667
|
// /models — pick active model (Claude or Ollama)
|
|
675
668
|
if (trimmed === '/models') {
|
|
676
|
-
const
|
|
669
|
+
const { options, warning } = await getModelOptions();
|
|
670
|
+
if (warning) console.log(chalk.yellow(`\n${warning}\n`));
|
|
671
|
+
const labels = options.map((o) => o.label);
|
|
677
672
|
const chosen = await askSelect(labels, 'Select model:');
|
|
678
673
|
if (chosen) {
|
|
679
|
-
const opt =
|
|
674
|
+
const opt = options.find((o) => o.label === chosen);
|
|
680
675
|
if (opt) {
|
|
681
676
|
if (opt.provider === 'claude' && !getClaudeKey()) {
|
|
682
677
|
const prompted = await askSecret('Claude API key (paste then Enter): ');
|
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/ui/logo.js
CHANGED
|
@@ -40,8 +40,8 @@ C8888 888 e Y8b Y8b "8" 888 888 " 888 P d888 888b Y8b Y8P
|
|
|
40
40
|
`;
|
|
41
41
|
|
|
42
42
|
|
|
43
|
-
const markovGradient = gradient(['#6ee7b7','#
|
|
44
|
-
|
|
43
|
+
// const markovGradient = gradient(['#6ee7b7','#38bdf8']);
|
|
44
|
+
const markovGradient = gradient(['#38bdf8','#6ee7b7', '#38bdf8']);
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
export function printLogo() {
|
package/src/ui/spinner.js
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import gradient from 'gradient-string';
|
|
3
3
|
|
|
4
|
-
const agentGradient = gradient(['#
|
|
5
|
-
|
|
6
|
-
const
|
|
4
|
+
const agentGradient = gradient(['#6ee7b7', '#38bdf8']);
|
|
5
|
+
// const chars = ['A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','0','1','2','3','4','5','6','7','8','9','!','@','$','%','^','&','*'];
|
|
6
|
+
export const DOTS = ['■','□','▪','▫','□','■','▪','▫'];
|
|
7
|
+
// const randomChar = () => chars[Math.floor(Math.random() * chars.length)];
|
|
8
|
+
// const randomFrame = () => Array.from({ length: 2 }, randomChar).join('');
|
|
9
|
+
// const DOTS = ['. ', '.. ', '...', '.. ', '. '];
|
|
10
|
+
|
|
11
|
+
const SPINNER_INTERVAL_MS = 180;
|
|
12
|
+
const DOTS_INTERVAL_MS = 180;
|
|
13
|
+
const LABEL_INTERVAL_MIN_MS = 3000;
|
|
14
|
+
const LABEL_INTERVAL_MAX_MS = 8000;
|
|
7
15
|
const IDLE_SPINNER_DELAY_MS = 250;
|
|
8
16
|
const SPINNER_LABELS = [
|
|
9
17
|
'Squirming',
|
|
10
18
|
'Shadoodeling',
|
|
11
|
-
'
|
|
19
|
+
'Brainmunching',
|
|
12
20
|
'Brewing',
|
|
13
21
|
'Hacking',
|
|
14
22
|
'Debugging',
|
|
@@ -24,10 +32,32 @@ function pickSpinnerLabel() {
|
|
|
24
32
|
return `${randomLabel} `;
|
|
25
33
|
}
|
|
26
34
|
|
|
35
|
+
/** Deterministic pseudo-random segment duration in [min, max] for label rotation. */
|
|
36
|
+
function labelSegmentMs(segmentIndex) {
|
|
37
|
+
const range = LABEL_INTERVAL_MAX_MS - LABEL_INTERVAL_MIN_MS + 1;
|
|
38
|
+
return LABEL_INTERVAL_MIN_MS + ((segmentIndex * 2654435761) >>> 0) % range;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function getLabelSlot(elapsedMs) {
|
|
42
|
+
let total = 0;
|
|
43
|
+
let slot = 0;
|
|
44
|
+
while (total <= elapsedMs && slot < 1000) {
|
|
45
|
+
total += labelSegmentMs(slot);
|
|
46
|
+
slot++;
|
|
47
|
+
}
|
|
48
|
+
return (slot - 1) % SPINNER_LABELS.length;
|
|
49
|
+
}
|
|
50
|
+
|
|
27
51
|
function renderFrame(label, startTime, frameIdx, opts = {}) {
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
const elapsedMs = now - startTime;
|
|
54
|
+
const elapsed = (elapsedMs / 1000).toFixed(1);
|
|
55
|
+
const labelSlot = getLabelSlot(elapsedMs);
|
|
56
|
+
const currentLabel = SPINNER_LABELS[labelSlot] + ' ';
|
|
57
|
+
const labelText = opts.gradientLabel ? agentGradient(currentLabel) : chalk.dim(currentLabel);
|
|
58
|
+
const dotIdx = Math.floor(elapsedMs / DOTS_INTERVAL_MS) % DOTS.length;
|
|
59
|
+
const dots = DOTS[dotIdx];
|
|
60
|
+
process.stdout.write('\r' + labelText + agentGradient(dots + ' ') + chalk.dim(elapsed + 's ') + ' ');
|
|
31
61
|
}
|
|
32
62
|
|
|
33
63
|
/**
|