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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "markov-cli",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "Markov CLI",
5
5
  "type": "module",
6
6
  "bin": {
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() {
@@ -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, MODEL_OPTIONS, setModelAndProvider, getModelDisplayName } from './ollama.js';
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(' /cd [path]') + chalk.dim(' change working directory\n') +
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('\nType a message · ') + chalk.cyan('@filename') + chalk.dim(' to attach · ctrl+q to cancel\n');
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('Commands:\n') +
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 labels = MODEL_OPTIONS.map((o) => o.label);
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 = MODEL_OPTIONS.find((o) => o.label === chosen);
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
- ...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/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','#6ee7b7']);
44
- // const markovGradient = gradient(['#4ade80', '#6ee7b7', '#38bdf8']);
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(['#22c55e', '#16a34a', '#4ade80']);
5
- export const SPINNER_FRAMES = ['', '', '', '', '', '', '', '', '', ''];
6
- const SPINNER_INTERVAL_MS = 200;
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
- 'Braincrunching',
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 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 ') + ' ');
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
  /**