markov-cli 1.0.4 → 1.0.6

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/interactive.js +87 -42
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "markov-cli",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
4
4
  "description": "LivingCloud's CLI AI Agent",
5
5
  "type": "module",
6
6
  "bin": {
@@ -106,15 +106,17 @@ function parseWriteCommands(text) {
106
106
  /**
107
107
  * Parse and apply all file operations from a model reply.
108
108
  * Shows diffs/confirmations for each op. Returns updated allFiles list.
109
+ * options.autoConfirm: if true, skip y/n prompts and apply all ops.
109
110
  */
110
- async function handleFileOps(reply, loadedFiles) {
111
+ async function handleFileOps(reply, loadedFiles, options = {}) {
112
+ const autoConfirm = options.autoConfirm === true;
111
113
  let allFiles = getFilesAndDirs();
112
114
 
113
115
  // Create folders first (so !!run: cd <folder> can succeed later)
114
116
  for (const folderPath of parseMkdirCommands(reply)) {
115
117
  const path = toRelativePath(folderPath);
116
118
  process.stdout.write(chalk.dim(` mkdir: ${path} — `));
117
- const confirmed = await confirm(chalk.bold(`Create folder ${chalk.cyan(path)}? [y/N] `));
119
+ const confirmed = autoConfirm ? true : await confirm(chalk.bold(`Create folder ${chalk.cyan(path)}? [y/N] `));
118
120
  if (confirmed) {
119
121
  try {
120
122
  mkdirSync(resolve(process.cwd(), path), { recursive: true });
@@ -131,7 +133,7 @@ async function handleFileOps(reply, loadedFiles) {
131
133
  // Create empty files
132
134
  for (const filePath of parseTouchCommands(reply)) {
133
135
  const path = toRelativePath(filePath);
134
- const confirmed = await confirm(chalk.bold(`Create file ${chalk.cyan(path)}? [y/N] `));
136
+ const confirmed = autoConfirm ? true : await confirm(chalk.bold(`Create file ${chalk.cyan(path)}? [y/N] `));
135
137
  if (confirmed) {
136
138
  try {
137
139
  const abs = resolve(process.cwd(), path);
@@ -152,7 +154,7 @@ async function handleFileOps(reply, loadedFiles) {
152
154
  for (const { filepath, content } of parseWriteCommands(reply)) {
153
155
  const path = toRelativePath(filepath);
154
156
  renderDiff(path, content);
155
- const confirmed = await confirm(chalk.bold(`Write ${chalk.cyan(path)}? [y/N] `));
157
+ const confirmed = autoConfirm ? true : await confirm(chalk.bold(`Write ${chalk.cyan(path)}? [y/N] `));
156
158
  if (confirmed) {
157
159
  try {
158
160
  applyEdit(path, content);
@@ -171,7 +173,7 @@ async function handleFileOps(reply, loadedFiles) {
171
173
  for (const { filepath, content } of edits) {
172
174
  const path = toRelativePath(filepath);
173
175
  renderDiff(path, content);
174
- const confirmed = await confirm(chalk.bold(`Write ${chalk.cyan(path)}? [y/N] `));
176
+ const confirmed = autoConfirm ? true : await confirm(chalk.bold(`Write ${chalk.cyan(path)}? [y/N] `));
175
177
  if (confirmed) {
176
178
  try {
177
179
  applyEdit(path, content);
@@ -187,7 +189,7 @@ async function handleFileOps(reply, loadedFiles) {
187
189
 
188
190
  // Run terminal commands (after folders/files exist, so cd works)
189
191
  for (const cmd of parseRunCommands(reply)) {
190
- const ok = await confirm(chalk.bold(`Run: ${chalk.cyan(cmd)}? [y/N] `));
192
+ const ok = autoConfirm ? true : await confirm(chalk.bold(`Run: ${chalk.cyan(cmd)}? [y/N] `));
191
193
  if (ok) {
192
194
  // cd must be handled in-process — child processes can't change the parent's cwd
193
195
  const cdMatch = cmd.match(/^cd\s+(.+)$/);
@@ -215,7 +217,7 @@ async function handleFileOps(reply, loadedFiles) {
215
217
  // Remove directories
216
218
  for (const dirPath of parseRmdirCommands(reply)) {
217
219
  const path = toRelativePath(dirPath);
218
- const confirmed = await confirm(chalk.bold(`Remove directory ${chalk.cyan(path)}? [y/N] `));
220
+ const confirmed = autoConfirm ? true : await confirm(chalk.bold(`Remove directory ${chalk.cyan(path)}? [y/N] `));
219
221
  if (confirmed) {
220
222
  const abs = resolve(process.cwd(), path);
221
223
  if (existsSync(abs)) {
@@ -237,7 +239,7 @@ async function handleFileOps(reply, loadedFiles) {
237
239
  // Delete files
238
240
  for (const filePath of parseDeleteCommands(reply)) {
239
241
  const path = toRelativePath(filePath);
240
- const confirmed = await confirm(chalk.bold(`Delete ${chalk.cyan(path)}? [y/N] `));
242
+ const confirmed = autoConfirm ? true : await confirm(chalk.bold(`Delete ${chalk.cyan(path)}? [y/N] `));
241
243
  if (confirmed) {
242
244
  const abs = resolve(process.cwd(), path);
243
245
  if (existsSync(abs)) {
@@ -429,11 +431,7 @@ function buildSystemMessage() {
429
431
  `\nSETUP NEXT.JS APP (or any new project in a subfolder):\n` +
430
432
  `1. Create the folder first: !!mkdir: next-app (or the requested name).\n` +
431
433
  `2. Change into it on its own line: !!run: cd next-app (nothing after the path).\n` +
432
- `3. Run each following command on its own !!run: line: e.g. !!run: npx create-next-app@latest . --yes, then !!run: git init, !!run: git add ., !!run: git commit -m "Initial commit".\n` +
433
- `\nSETUP LARAVEL API (output exactly this):\n` +
434
- `!!mkdir: laravel-api then !!run: cd laravel-api then !!run: composer create-project --prefer-dist laravel/laravel . then !!run: php artisan serve. One per line; nothing after the dot in composer; no composer run dev or dev-server. No custom routes.\n` +
435
- `\nSETUP ROUTES (API only — use when user asks to add routes, /health, /users, or API endpoints):\n` +
436
- `Only after the Laravel app exists. Add in routes/api.php only (not web.php): GET /api/health returning response()->json(['status' => 'ok']), and /api/users as a resource (index, show, store, update, destroy) using User model and a controller. Register api in bootstrap/app.php if needed. Use !!write: for routes/api.php and the controller; prefix with laravel-api/ if cwd is parent. Do not use routes/web.php.\n`;
434
+ `3. Run each following command on its own !!run: line: e.g. !!run: npx create-next-app@latest . --yes, then !!run: git init, !!run: git add ., !!run: git commit -m "Initial commit".\n`;
437
435
  return { role: 'system', content: `You are Markov, an AI coding assistant.\nWorking directory: ${process.cwd()}\n${fileList}${fileOpsInstructions}` };
438
436
  }
439
437
 
@@ -522,11 +520,12 @@ const HELP_TEXT =
522
520
  '\n' +
523
521
  chalk.bold('Commands:\n') +
524
522
  chalk.cyan(' /help') + chalk.dim(' show this help\n') +
525
- chalk.cyan(' /plan <message>') + chalk.dim(' ask AI to create a plan (no files written)\n') +
526
523
  chalk.cyan(' /build') + chalk.dim(' execute the stored plan\n') +
524
+ chalk.cyan(' /setup-nextjs') + chalk.dim(' scaffold a Next.js app\n') +
525
+ chalk.cyan(' /setup-laravel') + chalk.dim(' scaffold a Laravel API\n') +
527
526
  chalk.cyan(' /models') + chalk.dim(' switch the active AI model\n') +
528
527
  chalk.cyan(' /cd [path]') + chalk.dim(' change working directory\n') +
529
- chalk.dim('\nTips: use ') + chalk.cyan('@filename') + chalk.dim(' to attach a file · ctrl+q to cancel a response\n');
528
+ chalk.dim('\nNormal chat: generates a plan · /build to execute · ') + chalk.cyan('@filename') + chalk.dim(' to attach a file · ctrl+q to cancel\n');
530
529
 
531
530
  export async function startInteractive() {
532
531
  printLogo();
@@ -577,28 +576,18 @@ export async function startInteractive() {
577
576
  continue;
578
577
  }
579
578
 
580
- // /plan <message> ask LLM to produce a plan as a normal chat message, store it
581
- if (trimmed.startsWith('/plan ')) {
582
- const planRequest = trimmed.slice(6).trim();
583
- const planPrompt =
584
- `Create a detailed, numbered plan for the following task:\n\n${planRequest}\n\n` +
585
- `For each step, specify exactly what will happen and which syntax will be used:\n` +
586
- `- Writing or editing a file → !!write: path/to/file then fenced code block\n` +
587
- `- Creating an empty file → !!touch: path/to/file\n` +
588
- `- Creating a folder → !!mkdir: path/to/folder\n` +
589
- `- Removing a folder → !!rmdir: path/to/folder\n` +
590
- `- Deleting a file → !!delete: path/to/file\n\n` +
591
- `Do NOT output any actual file contents or commands yet — only the plan.`;
592
- chatMessages.push({ role: 'user', content: planPrompt });
579
+ // /setup-nextjsscaffold a Next.js app
580
+ if (trimmed === '/setup-nextjs') {
581
+ const msg = 'Set up a new Next.js app in a subfolder. Follow the SETUP NEXT.JS APP instructions exactly.';
582
+ chatMessages.push({ role: 'user', content: msg });
593
583
  const abortController = new AbortController();
594
584
  try {
595
585
  const reply = await streamWithViewport(chatMessages, abortController.signal);
596
- if (reply === null) {
597
- chatMessages.pop();
598
- } else {
586
+ if (reply === null) { chatMessages.pop(); }
587
+ else {
599
588
  chatMessages.push({ role: 'assistant', content: reply });
600
- currentPlan = { text: reply };
601
- console.log(chalk.green('\nPlan stored. Use /build to execute it.\n'));
589
+ allFiles = await handleFileOps(reply, [], { autoConfirm: true });
590
+ console.log(chalk.green('✓ Next.js app created.\n'));
602
591
  }
603
592
  } catch (err) {
604
593
  if (!abortController.signal.aborted) console.log(chalk.red(`\n${err.message}\n`));
@@ -606,13 +595,61 @@ export async function startInteractive() {
606
595
  continue;
607
596
  }
608
597
 
609
- // /buildexecute the stored plan with full file ops
598
+ // /setup-laravelscaffold a Laravel API (hardcoded, no AI)
599
+ if (trimmed === '/setup-laravel') {
600
+ const steps = [
601
+ { type: 'mkdir', path: 'laravel-api' },
602
+ { type: 'cd', path: 'laravel-api' },
603
+ { type: 'run', cmd: 'composer create-project --prefer-dist laravel/laravel .' },
604
+ { type: 'run', cmd: 'php artisan serve' },
605
+ ];
606
+ for (const step of steps) {
607
+ if (step.type === 'mkdir') {
608
+ process.stdout.write(chalk.dim(` mkdir: ${step.path}\n`));
609
+ try {
610
+ mkdirSync(resolve(process.cwd(), step.path), { recursive: true });
611
+ allFiles = getFilesAndDirs();
612
+ console.log(chalk.green(` ✓ created ${step.path}\n`));
613
+ } catch (err) {
614
+ console.log(chalk.red(` ✗ ${err.message}\n`));
615
+ break;
616
+ }
617
+ } else if (step.type === 'cd') {
618
+ try {
619
+ process.chdir(resolve(process.cwd(), step.path));
620
+ allFiles = getFilesAndDirs();
621
+ console.log(chalk.dim(` 📁 ${process.cwd()}\n`));
622
+ } catch (err) {
623
+ console.log(chalk.red(` ✗ no such directory: ${step.path}\n`));
624
+ break;
625
+ }
626
+ } else if (step.type === 'run') {
627
+ process.stdout.write(chalk.dim(` running: ${step.cmd}\n`));
628
+ const { stdout, stderr, exitCode } = await execCommand(step.cmd);
629
+ const output = [stdout, stderr].filter(Boolean).join('\n').trim();
630
+ if (output) console.log(chalk.dim(output));
631
+ if (exitCode !== 0) {
632
+ console.log(chalk.red(` ✗ command failed (exit ${exitCode})\n`));
633
+ break;
634
+ }
635
+ console.log(chalk.green(` ✓ done\n`));
636
+ allFiles = getFilesAndDirs();
637
+ }
638
+ }
639
+ console.log(chalk.green('✓ Laravel API created.\n'));
640
+ continue;
641
+ }
642
+
643
+
644
+ // /build — execute the stored plan with file ops
610
645
  if (trimmed === '/build') {
611
646
  if (!currentPlan) {
612
- console.log(chalk.yellow('\n⚠ No plan stored. Use /plan <message> first.\n'));
647
+ console.log(chalk.yellow('\n⚠ No plan stored. Describe what you want first.\n'));
613
648
  continue;
614
649
  }
615
650
 
651
+ const autoConfirm = await confirm(chalk.bold('Auto-confirm all operations? [y/N] '));
652
+
616
653
  const buildPrompt =
617
654
  `Execute the following plan. Use !!write: path then a fenced code block for file writes, !!mkdir: for folders, !!delete: for deletions.\n\n` +
618
655
  `Plan:\n${currentPlan.text}`;
@@ -625,7 +662,7 @@ export async function startInteractive() {
625
662
  chatMessages.pop();
626
663
  } else {
627
664
  chatMessages.push({ role: 'assistant', content: reply });
628
- allFiles = await handleFileOps(reply, []);
665
+ allFiles = await handleFileOps(reply, [], { autoConfirm });
629
666
  currentPlan = null;
630
667
  console.log(chalk.green('✓ Plan executed.\n'));
631
668
  }
@@ -635,7 +672,7 @@ export async function startInteractive() {
635
672
  continue;
636
673
  }
637
674
 
638
- // Normal chat — file ops always available
675
+ // Normal chat — generate a plan, store it, require /build to execute
639
676
  const { loaded, failed } = resolveFileRefs(trimmed);
640
677
 
641
678
  if (loaded.length > 0) {
@@ -645,18 +682,26 @@ export async function startInteractive() {
645
682
  console.log(chalk.yellow(`\n⚠ not found: ${failed.map(f => `@${f}`).join(', ')}`));
646
683
  }
647
684
 
648
- // Store raw message — @refs are re-resolved fresh on every API call
649
- chatMessages.push({ role: 'user', content: trimmed });
650
-
685
+ const planPrompt =
686
+ `Create a detailed, numbered plan for the following task:\n\n${trimmed}\n\n` +
687
+ `For each step, specify exactly what will happen and which syntax will be used:\n` +
688
+ `- Writing or editing a file → !!write: path/to/file then fenced code block\n` +
689
+ `- Creating an empty file → !!touch: path/to/file\n` +
690
+ `- Creating a folder → !!mkdir: path/to/folder\n` +
691
+ `- Removing a folder → !!rmdir: path/to/folder\n` +
692
+ `- Deleting a file → !!delete: path/to/file\n\n` +
693
+ `Do NOT output any actual file contents or commands yet — only the plan.`;
694
+
695
+ chatMessages.push({ role: 'user', content: planPrompt });
651
696
  const abortController = new AbortController();
652
697
  try {
653
698
  const reply = await streamWithViewport(chatMessages, abortController.signal);
654
-
655
699
  if (reply === null) {
656
700
  chatMessages.pop();
657
701
  } else {
658
702
  chatMessages.push({ role: 'assistant', content: reply });
659
- allFiles = await handleFileOps(reply, loaded);
703
+ currentPlan = { text: reply };
704
+ console.log(chalk.green('\n✓ Plan stored. Use /build to execute it.\n'));
660
705
  }
661
706
  } catch (err) {
662
707
  if (!abortController.signal.aborted) {