markov-cli 1.0.4 → 1.0.5

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 +40 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "markov-cli",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
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)) {
@@ -526,7 +528,7 @@ const HELP_TEXT =
526
528
  chalk.cyan(' /build') + chalk.dim(' execute the stored plan\n') +
527
529
  chalk.cyan(' /models') + chalk.dim(' switch the active AI model\n') +
528
530
  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');
531
+ chalk.dim('\nNormal chat: plan then build (no y/n). Tips: ') + chalk.cyan('@filename') + chalk.dim(' to attach a file · ctrl+q to cancel\n');
530
532
 
531
533
  export async function startInteractive() {
532
534
  printLogo();
@@ -635,7 +637,7 @@ export async function startInteractive() {
635
637
  continue;
636
638
  }
637
639
 
638
- // Normal chat — file ops always available
640
+ // Normal chat — plan then build (two phases), apply file ops without y/n
639
641
  const { loaded, failed } = resolveFileRefs(trimmed);
640
642
 
641
643
  if (loaded.length > 0) {
@@ -645,19 +647,38 @@ export async function startInteractive() {
645
647
  console.log(chalk.yellow(`\n⚠ not found: ${failed.map(f => `@${f}`).join(', ')}`));
646
648
  }
647
649
 
648
- // Store raw message — @refs are re-resolved fresh on every API call
649
- chatMessages.push({ role: 'user', content: trimmed });
650
-
650
+ const planPrompt =
651
+ `Create a detailed, numbered plan for the following task:\n\n${trimmed}\n\n` +
652
+ `For each step, specify exactly what will happen and which syntax will be used:\n` +
653
+ `- Writing or editing a file → !!write: path/to/file then fenced code block\n` +
654
+ `- Creating an empty file → !!touch: path/to/file\n` +
655
+ `- Creating a folder → !!mkdir: path/to/folder\n` +
656
+ `- Removing a folder → !!rmdir: path/to/folder\n` +
657
+ `- Deleting a file → !!delete: path/to/file\n\n` +
658
+ `Do NOT output any actual file contents or commands yet — only the plan.`;
659
+
660
+ chatMessages.push({ role: 'user', content: planPrompt });
651
661
  const abortController = new AbortController();
652
662
  try {
653
- const reply = await streamWithViewport(chatMessages, abortController.signal);
654
-
655
- if (reply === null) {
663
+ const planReply = await streamWithViewport(chatMessages, abortController.signal);
664
+ if (planReply === null) {
656
665
  chatMessages.pop();
657
- } else {
658
- chatMessages.push({ role: 'assistant', content: reply });
659
- allFiles = await handleFileOps(reply, loaded);
666
+ continue;
667
+ }
668
+ chatMessages.push({ role: 'assistant', content: planReply });
669
+
670
+ const buildPrompt =
671
+ `Execute the following plan. Use !!write: path then a fenced code block for file writes, !!mkdir: for folders, !!delete: for deletions.\n\n` +
672
+ `Plan:\n${planReply}`;
673
+
674
+ chatMessages.push({ role: 'user', content: buildPrompt });
675
+ const buildReply = await streamWithViewport(chatMessages, abortController.signal);
676
+ if (buildReply === null) {
677
+ chatMessages.pop(); // remove build prompt
678
+ continue;
660
679
  }
680
+ chatMessages.push({ role: 'assistant', content: buildReply });
681
+ allFiles = await handleFileOps(buildReply, loaded, { autoConfirm: true });
661
682
  } catch (err) {
662
683
  if (!abortController.signal.aborted) {
663
684
  console.log(chalk.red(`\n${err.message}\n`));