markov-cli 1.0.12 → 1.0.13

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.12",
3
+ "version": "1.0.13",
4
4
  "description": "Markov CLI",
5
5
  "type": "module",
6
6
  "bin": {
package/src/auth.js CHANGED
@@ -6,6 +6,7 @@ const CONFIG_DIR = join(homedir(), '.markov');
6
6
  const TOKEN_PATH = join(CONFIG_DIR, 'token');
7
7
  const ANTHROPIC_KEY_PATH = join(CONFIG_DIR, 'anthropic_api_key');
8
8
  const OPENAI_KEY_PATH = join(CONFIG_DIR, 'openai_api_key');
9
+ const OLLAMA_KEY_PATH = join(CONFIG_DIR, 'ollama_api_key');
9
10
 
10
11
  export const API_URL = 'https://api.livingcloud.app/api';
11
12
 
@@ -31,6 +32,28 @@ export function clearOpenAIKey() {
31
32
  delete process.env.OPENAI_API_KEY;
32
33
  }
33
34
 
35
+ export function getOllamaKey() {
36
+ const env = process.env.OLLAMA_API_KEY?.trim();
37
+ if (env) return env;
38
+ if (!existsSync(OLLAMA_KEY_PATH)) return null;
39
+ return readFileSync(OLLAMA_KEY_PATH, 'utf-8').trim() || null;
40
+ }
41
+
42
+ export function setOllamaKey(key) {
43
+ const trimmed = (key && String(key).trim()) || '';
44
+ if (!trimmed) return;
45
+ mkdirSync(CONFIG_DIR, { recursive: true });
46
+ writeFileSync(OLLAMA_KEY_PATH, trimmed, 'utf-8');
47
+ process.env.OLLAMA_API_KEY = trimmed;
48
+ }
49
+
50
+ export function clearOllamaKey() {
51
+ if (existsSync(OLLAMA_KEY_PATH)) {
52
+ writeFileSync(OLLAMA_KEY_PATH, '', 'utf-8');
53
+ }
54
+ delete process.env.OLLAMA_API_KEY;
55
+ }
56
+
34
57
  export function getClaudeKey() {
35
58
  const env = process.env.ANTHROPIC_API_KEY?.trim();
36
59
  if (env) return env;
@@ -68,5 +68,18 @@ export const TANSTACK_STEPS = [
68
68
 
69
69
  /** Laravel setup steps. */
70
70
  export const LARAVEL_STEPS = [
71
+ { type: 'run', cmd: 'mkdir laravel-be' },
72
+ { type: 'cd', path: 'laravel-be' },
71
73
  { type: 'run', cmd: 'composer create-project laravel/laravel .' },
74
+ { type: 'run', cmd: 'npm install && npm run build' },
72
75
  ];
76
+
77
+ /** Prompt for /laravel: steps as run_terminal_command + file edits only. */
78
+ export const LARAVEL_BLOG_PROMPT = `Set up a new Laravel project "laravel-be" with the following steps:
79
+
80
+ 1. run_terminal_command: composer --version
81
+ 2. run_terminal_command: php --version
82
+ 3. run_terminal_command: composer create-project laravel/laravel laravel-be
83
+ 4. run_terminal_command: cd laravel-be
84
+
85
+ If a step fails, stop and report the error.`;
@@ -10,7 +10,7 @@ import { resolveFileRefs } from './files.js';
10
10
  import { RUN_TERMINAL_COMMAND_TOOL, WEB_SEARCH_TOOL, runTool } from './tools.js';
11
11
  import { chatPrompt } from './input.js';
12
12
  import { getFilesAndDirs } from './ui/picker.js';
13
- import { getToken, login, clearToken, getClaudeKey, setClaudeKey, getOpenAIKey, setOpenAIKey } from './auth.js';
13
+ import { getToken, login, clearToken, getClaudeKey, setClaudeKey, getOpenAIKey, setOpenAIKey, getOllamaKey, setOllamaKey } from './auth.js';
14
14
 
15
15
  // Extracted UI modules
16
16
  import { selectFrom, confirm, promptLine, promptSecret } from './ui/prompts.js';
@@ -25,7 +25,7 @@ import { runAgentLoop, maybePrintFullPayload, AGENT_LOOP_MAX_ITERATIONS } from '
25
25
  import { applyCodeBlockEdits } from './editor/codeBlockEdits.js';
26
26
 
27
27
  // Extracted command modules
28
- import { runSetupSteps, NEXTJS_STEPS, TANSTACK_STEPS, LARAVEL_STEPS } from './commands/setup.js';
28
+ import { runSetupSteps, NEXTJS_STEPS, TANSTACK_STEPS, LARAVEL_STEPS, LARAVEL_BLOG_PROMPT } from './commands/setup.js';
29
29
 
30
30
  const agentGradient = gradient(['#22c55e', '#16a34a', '#4ade80']);
31
31
 
@@ -48,7 +48,7 @@ const INTRO_TEXT =
48
48
  chalk.cyan(' /init') + chalk.dim(' [prompt] create markov.md with project summary\n') +
49
49
  chalk.cyan(' /plan') + chalk.dim(' [prompt] stream a plan and save to plan.md\n') +
50
50
  chalk.cyan(' /build') + chalk.dim(' execute plan from plan.md\n') +
51
- chalk.cyan(' /yolo') + chalk.dim(' [prompt] plan in stream mode, then auto-run until done\n') +
51
+ chalk.cyan(' /yolo') + chalk.dim(' [prompt] plan in stream mode, then auto-run until done\n') +
52
52
  chalk.dim('\nType a message · ') + chalk.cyan('@filename') + chalk.dim(' to attach · ctrl+q to cancel\n');
53
53
 
54
54
  const HELP_TEXT =
@@ -59,6 +59,7 @@ const HELP_TEXT =
59
59
  chalk.cyan(' /setup-nextjs') + chalk.dim(' scaffold a Next.js app\n') +
60
60
  chalk.cyan(' /setup-tanstack') + chalk.dim(' scaffold a TanStack Start app\n') +
61
61
  chalk.cyan(' /setup-laravel') + chalk.dim(' scaffold a Laravel API\n') +
62
+ chalk.cyan(' /laravel') + chalk.dim(' set up Laravel "my-blog" with blog route (agent)\n') +
62
63
  chalk.cyan(' /models') + chalk.dim(' switch the active AI model\n') +
63
64
  chalk.cyan(' /cd [path]') + chalk.dim(' change working directory\n') +
64
65
  chalk.cyan(' /cmd [command]') + chalk.dim(' run a shell command in the current folder\n') +
@@ -196,6 +197,7 @@ export async function startInteractive() {
196
197
  console.log(chalk.yellow('No prompt given.\n'));
197
198
  continue;
198
199
  }
200
+ console.log(chalk.dim('You: ') + rawUserContent);
199
201
  const userContent = (await getLsContext()) + (await getGrepContext()) + 'Create a step-by-step plan for: ' + rawUserContent;
200
202
  chatMessages.push({ role: 'user', content: userContent });
201
203
  const planMessages = [buildPlanSystemMessage(), ...chatMessages];
@@ -265,7 +267,7 @@ export async function startInteractive() {
265
267
  continue;
266
268
  }
267
269
  }
268
- console.log(chalk.cyan(' ▶ ') + chalk.bold(name) + chalk.dim(' ') + (name === 'web_search' ? (args?.query ?? '') : (args?.command ?? '')));
270
+ console.log(chalk.cyan('\n ▶ ') + chalk.bold(name) + chalk.dim(' ') + (name === 'web_search' ? (args?.query ?? '') : (args?.command ?? '')));
269
271
  const result = await runTool(name, args ?? {}, { cwd: process.cwd(), confirmFn: () => Promise.resolve(true) });
270
272
  currentPlanMessages.push({
271
273
  role: 'tool',
@@ -302,6 +304,7 @@ export async function startInteractive() {
302
304
  console.log(chalk.yellow('No prompt given.\n'));
303
305
  continue;
304
306
  }
307
+ console.log(chalk.dim('You: ') + rawUserContent);
305
308
  const planUserContent = (await getLsContext()) + (await getGrepContext()) + 'Create a step-by-step plan for: ' + rawUserContent;
306
309
  chatMessages.push({ role: 'user', content: planUserContent });
307
310
  const planMessages = [buildPlanSystemMessage(), ...chatMessages];
@@ -348,7 +351,7 @@ export async function startInteractive() {
348
351
  continue;
349
352
  }
350
353
  }
351
- console.log(chalk.cyan(' ▶ ') + chalk.bold(name) + chalk.dim(' ') + (name === 'web_search' ? (args?.query ?? '') : (args?.command ?? '')));
354
+ console.log(chalk.cyan('\n ▶ ') + chalk.bold(name) + chalk.dim(' ') + (name === 'web_search' ? (args?.query ?? '') : (args?.command ?? '')));
352
355
  const result = await runTool(name, args ?? {}, { cwd: process.cwd(), confirmFn: () => Promise.resolve(true) });
353
356
  currentPlanMessages.push({
354
357
  role: 'tool',
@@ -407,7 +410,7 @@ export async function startInteractive() {
407
410
  },
408
411
  onToolCall: (name, args) => {
409
412
  const summary = formatToolCallSummary(name, args);
410
- console.log(chalk.cyan(' ▶ ') + chalk.bold(name) + chalk.dim(' ') + chalk.white(summary));
413
+ console.log(chalk.cyan('\n ▶ ') + chalk.bold(name) + chalk.dim(' ') + chalk.white(summary));
411
414
  },
412
415
  onToolResult: (name, resultStr) => {
413
416
  console.log(chalk.dim(' ') + formatToolResultSummary(name, resultStr));
@@ -503,7 +506,7 @@ export async function startInteractive() {
503
506
  continue;
504
507
  }
505
508
  }
506
- console.log(chalk.cyan(' ▶ ') + chalk.bold(name) + chalk.dim(' ') + (name === 'web_search' ? (args?.query ?? '') : (args?.command ?? '')));
509
+ console.log(chalk.cyan('\n ▶ ') + chalk.bold(name) + chalk.dim(' ') + (name === 'web_search' ? (args?.query ?? '') : (args?.command ?? '')));
507
510
  const result = await runTool(name, args ?? {}, { cwd: process.cwd(), confirmFn: () => Promise.resolve(true) });
508
511
  currentInitMessages.push({
509
512
  role: 'tool',
@@ -587,7 +590,7 @@ export async function startInteractive() {
587
590
  },
588
591
  onToolCall: (name, args) => {
589
592
  const summary = formatToolCallSummary(name, args);
590
- console.log(chalk.cyan(' ▶ ') + chalk.bold(name) + chalk.dim(' ') + chalk.white(summary));
593
+ console.log(chalk.cyan('\n ▶ ') + chalk.bold(name) + chalk.dim(' ') + chalk.white(summary));
591
594
  },
592
595
  onToolResult: (name, resultStr) => {
593
596
  console.log(chalk.dim(' ') + formatToolResultSummary(name, resultStr));
@@ -634,6 +637,14 @@ export async function startInteractive() {
634
637
  }
635
638
  setOpenAIKey(enteredKey);
636
639
  }
640
+ if (opt.provider === 'ollama' && opt.model.endsWith('-cloud') && !getOllamaKey()) {
641
+ const enteredKey = (await promptSecret('Ollama API key for cloud models (paste then Enter): ')).trim();
642
+ if (!enteredKey) {
643
+ console.log(chalk.yellow('\nNo key entered. Model not switched.\n'));
644
+ continue;
645
+ }
646
+ setOllamaKey(enteredKey);
647
+ }
637
648
  setModelAndProvider(opt.provider, opt.model);
638
649
  console.log(chalk.dim(`\n🤖 switched to ${chalk.cyan(getModelDisplayName())}\n`));
639
650
  }
@@ -706,6 +717,73 @@ export async function startInteractive() {
706
717
  continue;
707
718
  }
708
719
 
720
+ // /laravel — run agent with Laravel "my-blog" blog setup prompt (auto-confirm like /yolo)
721
+ if (trimmed === '/laravel') {
722
+ const userContent = (await getLsContext()) + (await getGrepContext()) + LARAVEL_BLOG_PROMPT;
723
+ chatMessages.push({ role: 'user', content: userContent });
724
+ const agentMessages = [buildAgentSystemMessage(), ...chatMessages];
725
+ maybePrintFullPayload(agentMessages);
726
+ const abortController = new AbortController();
727
+ const confirmFn = () => Promise.resolve(true);
728
+ const confirmFileEdit = async () => true;
729
+ const startTime = Date.now();
730
+ const DOTS = ['.', '..', '...'];
731
+ let dotIdx = 0;
732
+ let spinner = null;
733
+ const startSpinner = () => {
734
+ if (spinner) { clearInterval(spinner); spinner = null; }
735
+ dotIdx = 0;
736
+ process.stdout.write(chalk.dim('\nLaravel › '));
737
+ spinner = setInterval(() => {
738
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
739
+ process.stdout.write('\r' + chalk.dim('Laravel › ') + chalk.dim(elapsed + 's ') + agentGradient(DOTS[dotIdx % DOTS.length]) + ' ');
740
+ dotIdx++;
741
+ }, 400);
742
+ };
743
+ const stopSpinner = () => {
744
+ if (spinner) { clearInterval(spinner); spinner = null; }
745
+ process.stdout.write('\r\x1b[0J');
746
+ };
747
+ try {
748
+ const result = await runAgentLoop(agentMessages, {
749
+ signal: abortController.signal,
750
+ cwd: process.cwd(),
751
+ confirmFn,
752
+ confirmFileEdit,
753
+ onThinking: () => { startSpinner(); },
754
+ onBeforeToolRun: () => { stopSpinner(); },
755
+ onIteration: (iter, max, toolCount) => {
756
+ const w = process.stdout.columns || 80;
757
+ const label = ` Step ${iter} `;
758
+ const line = chalk.dim('──') + chalk.bold.white(label) + chalk.dim('─'.repeat(Math.max(0, w - label.length - 2)));
759
+ console.log(line);
760
+ },
761
+ onToolCall: (name, args) => {
762
+ const summary = formatToolCallSummary(name, args);
763
+ console.log(chalk.cyan('\n ▶ ') + chalk.bold(name) + chalk.dim(' ') + chalk.white(summary));
764
+ },
765
+ onToolResult: (name, resultStr) => {
766
+ console.log(chalk.dim(' ') + formatToolResultSummary(name, resultStr));
767
+ },
768
+ });
769
+ stopSpinner();
770
+ if (result) {
771
+ chatMessages.push(result.finalMessage);
772
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
773
+ const width = Math.min(process.stdout.columns || 80, 80);
774
+ process.stdout.write(formatResponseWithCodeBlocks(result.content, width) + '\n\n');
775
+ await applyCodeBlockEdits(result.content, []);
776
+ allFiles = getFilesAndDirs();
777
+ console.log(chalk.green('✓ Laravel blog setup done.') + chalk.dim(` (${elapsed}s)\n`));
778
+ maybePrintRawModelOutput(result.content);
779
+ }
780
+ } catch (err) {
781
+ stopSpinner();
782
+ if (!abortController.signal?.aborted) console.log(chalk.red(`\n${err.message}\n`));
783
+ }
784
+ continue;
785
+ }
786
+
709
787
  // Handle message with agent (tools)
710
788
  const { loaded, failed, content: resolvedContent } = await resolveFileRefs(trimmed);
711
789
  if (loaded.length > 0) {
@@ -715,6 +793,7 @@ export async function startInteractive() {
715
793
  console.log(chalk.yellow(`\n⚠ not found: ${failed.map(f => `@${f}`).join(', ')}`));
716
794
  }
717
795
  const rawUserContent = resolvedContent ?? trimmed;
796
+ console.log(chalk.dim('You: ') + rawUserContent);
718
797
  const userContent = (await getLsContext()) + (await getGrepContext()) + rawUserContent;
719
798
  chatMessages.push({ role: 'user', content: userContent });
720
799
 
@@ -768,7 +847,7 @@ export async function startInteractive() {
768
847
  },
769
848
  onToolCall: (name, args) => {
770
849
  const summary = formatToolCallSummary(name, args);
771
- console.log(chalk.cyan(' ▶ ') + chalk.bold(name) + chalk.dim(' ') + chalk.white(summary));
850
+ console.log(chalk.cyan('\n ▶ ') + chalk.bold(name) + chalk.dim(' ') + chalk.white(summary));
772
851
  },
773
852
  onToolResult: (name, resultStr) => {
774
853
  console.log(chalk.dim(' ') + formatToolResultSummary(name, resultStr));
package/src/ollama.js CHANGED
@@ -1,4 +1,4 @@
1
- import { API_URL, getToken, getClaudeKey, getOpenAIKey } from './auth.js';
1
+ import { API_URL, getToken, getClaudeKey, getOpenAIKey, getOllamaKey } from './auth.js';
2
2
  import * as openai from './openai.js';
3
3
  import * as claude from './claude.js';
4
4
 
@@ -33,7 +33,12 @@ function stripToolCallArtifacts(s) {
33
33
 
34
34
  const getHeaders = () => {
35
35
  const token = getToken();
36
- return { 'Content-Type': 'application/json', ...(token && { Authorization: `Bearer ${token}` }) };
36
+ const ollamaKey = getOllamaKey();
37
+ return {
38
+ 'Content-Type': 'application/json',
39
+ ...(token && { Authorization: `Bearer ${token}` }),
40
+ ...(ollamaKey && { 'X-Ollama-Api-Key': ollamaKey })
41
+ };
37
42
  };
38
43
 
39
44
  /** Claude models: { label, model } */
@@ -51,7 +56,7 @@ export const OPENAI_MODELS = [
51
56
  ];
52
57
 
53
58
  /** Ollama models (backend) */
54
- export const MODELS = ['qwen3.5:0.8b', 'qwen3.5:2b', 'qwen3.5:4b', 'qwen3.5:9b'];
59
+ export const MODELS = ['qwen3.5:0.8b', 'qwen3.5:2b', 'qwen3.5:4b', 'qwen3.5:9b', 'qwen3.5:397b-cloud'];
55
60
 
56
61
  /** Combined options for /models picker: { label, provider, model } */
57
62
  export const MODEL_OPTIONS = [
package/src/tools.js CHANGED
@@ -412,7 +412,9 @@ const TOOLS_MAP = {
412
412
  * @returns {Promise<{ stdout: string, stderr: string, exitCode: number }>}
413
413
  */
414
414
  export async function execCommand(command, cwd = process.cwd()) {
415
- if (!command.trim()) return { stdout: '', stderr: 'Error: empty command', exitCode: 1 };
415
+ if (command == null || typeof command !== 'string' || !command.trim()) {
416
+ return { stdout: '', stderr: 'Error: empty or invalid command', exitCode: 1 };
417
+ }
416
418
  const timeout = 30_000;
417
419
  const maxBuffer = 1024 * 1024;
418
420
  try {
package/src/ui/logo.js CHANGED
@@ -1,10 +1,28 @@
1
1
  import gradient from 'gradient-string';
2
2
 
3
3
  const ASCII_ART = `
4
- ▗▖ ▗▖▗▞▀▜▌ ▄▄▄ █ ▄ ▄▄▄ ▄
5
- ▐▛▚▞▜▌▝▚▄▟▌█ █▄▀ █ █
6
- ▐▌ ▐▌ █ █ ▀▄ ▀▄▄▄▀ ▀▄▀
7
- ▐▌ ▐▌ █ █
4
+ ███╗ ███╗ █████╗ ██████╗ ██╗ ██╗ ██████╗ ██╗ ██╗
5
+ ████╗ ████║██╔══██╗██╔══██╗██║ ██╔╝██╔═══██╗██║ ██║
6
+ ██╔████╔██║███████║██████╔╝█████╔╝ ██║ ██║██║ ██║
7
+ ██║╚██╔╝██║██╔══██║██╔══██╗██╔═██╗ ██║ ██║╚██╗ ██╔╝
8
+ ██║ ╚═╝ ██║██║ ██║██║ ██║██║ ██╗╚██████╔╝ ╚████╔╝
9
+ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝
10
+
11
+ ██████╗ ██████╗ ██████╗ ███████╗
12
+ ██╔════╝██╔═══██╗██╔══██╗██╔════╝
13
+ ██║ ██║ ██║██║ ██║█████╗
14
+ ██║ ██║ ██║██║ ██║██╔══╝
15
+ ╚██████╗╚██████╔╝██████╔╝███████╗
16
+ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
17
+ `;
18
+
19
+ const ASCII_ART4 = `
20
+ ██████╗██╗ ███╗ ███╗ █████╗ ██████╗ ██╗ ██╗ ██████╗ ██╗ ██╗
21
+ ██╔════╝██║ ████╗ ████║██╔══██╗██╔══██╗██║ ██╔╝██╔═══██╗██║ ██║
22
+ ██║ ██║ ██╔████╔██║███████║██████╔╝█████╔╝ ██║ ██║██║ ██║
23
+ ██║ ██║ ██║╚██╔╝██║██╔══██║██╔══██╗██╔═██╗ ██║ ██║╚██╗ ██╔╝
24
+ ╚██████╗███████╗██║ ╚═╝ ██║██║ ██║██║ ██║██║ ██╗╚██████╔╝ ╚████╔╝
25
+ ╚═════╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝
8
26
  `;
9
27
 
10
28
  const ASCII_ART3 = `
@@ -14,17 +32,26 @@ const ASCII_ART3 = `
14
32
  ██║╚██╔╝██║██╔══██║██╔══██╗██╔═██╗ ██║ ██║╚██╗ ██╔╝
15
33
  ██║ ╚═╝ ██║██║ ██║██║ ██║██║ ██╗╚██████╔╝ ╚████╔╝
16
34
  ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝
35
+
36
+ ██████╗ ██████╗ ██████╗ ███████╗
37
+ ██╔════╝██╔═══██╗██╔══██╗██╔════╝
38
+ ██║ ██║ ██║██║ ██║█████╗
39
+ ██║ ██║ ██║██║ ██║██╔══╝
40
+ ╚██████╗╚██████╔╝██████╔╝███████╗
41
+ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝
17
42
  `;
18
43
 
19
44
  const ASCII_ART2 = `
20
- ██▄ ▄██ ▄▄▄ ▄▄▄▄ ▄▄ ▄▄ ▄▄▄ ▄▄ ▄▄
21
- ██ ▀▀ ██ ██▀██ ██▄█▄ ██▄█▀ ██▀██ ██▄██
22
- ██ ██ ██▀██ ██ ██ ██ ██ ▀███▀ ▀█▀
45
+ e88'Y88 888 e e 888
46
+ d888 'Y 888 d8b d8b ,"Y88b 888,8, 888 ee e88 88e Y8b Y888P
47
+ C8888 888 e Y8b Y8b "8" 888 888 " 888 P d888 888b Y8b Y8P
48
+ Y888 ,d 888 ,d d8b Y8b Y8b ,ee 888 888 888 b Y888 888P Y8b "
49
+ "88,d88 888,d88 d888b Y8b Y8b "88 888 888 888 8b "88 88" Y8P
23
50
  `;
24
51
 
25
52
 
26
- const markovGradient = gradient(['#374151', '#4b5563', '#6b7280', '#4b5563', '#374151']);
53
+ const markovGradient = gradient(['#4ade80', '#6ee7b7', '#38bdf8']);
27
54
 
28
55
  export function printLogo() {
29
- console.log(markovGradient.multiline(ASCII_ART3));
56
+ console.log(markovGradient.multiline(ASCII_ART4));
30
57
  }