markov-cli 1.0.3 → 1.0.4

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.3",
3
+ "version": "1.0.4",
4
4
  "description": "LivingCloud's CLI AI Agent",
5
5
  "type": "module",
6
6
  "bin": {
package/src/editor.js CHANGED
@@ -141,16 +141,9 @@ export function renderDiff(filepath, newContent, cwd = process.cwd()) {
141
141
 
142
142
  export function parseEdits(responseText, loadedFiles = []) {
143
143
  const edits = [];
144
- // Match "FILE: path" followed by a fenced code block (so model can use FILE: path + ```lang)
145
- const filePrefixRegex = /FILE:\s*([^\n]+)\s*\n\s*```(?:[\w./\-]*)\n([\s\S]*?)```/g;
146
- let match;
147
- while ((match = filePrefixRegex.exec(responseText)) !== null) {
148
- const filepath = match[1].trim();
149
- const content = match[2];
150
- if (filepath) edits.push({ filepath, content });
151
- }
152
-
144
+ // File writes use !!write: in interactive.js; here we only handle fenced blocks with path/language or single attached file
153
145
  const regex = /```([\w./\-]*)\n([\s\S]*?)```/g;
146
+ let match;
154
147
  while ((match = regex.exec(responseText)) !== null) {
155
148
  const tag = match[1].trim();
156
149
  const content = match[2];
@@ -1,7 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import gradient from 'gradient-string';
3
3
  import { homedir } from 'os';
4
- import { resolve } from 'path';
4
+ import { resolve, relative, isAbsolute } from 'path';
5
5
  import { mkdirSync, rmSync, writeFileSync, unlinkSync, existsSync } from 'fs';
6
6
  import { join } from 'path';
7
7
  import { printLogo } from './ui/logo.js';
@@ -14,6 +14,15 @@ import { getFiles, getFilesAndDirs } from './ui/picker.js';
14
14
 
15
15
  const markovGradient = gradient(['#6366f1', '#a855f7', '#ec4899']);
16
16
 
17
+ /** If path is absolute and under cwd, return relative path; otherwise return as-is. */
18
+ function toRelativePath(p) {
19
+ const cwd = process.cwd();
20
+ if (!isAbsolute(p)) return p;
21
+ const rel = relative(cwd, p);
22
+ if (!rel.startsWith('..') && !isAbsolute(rel)) return rel;
23
+ return p;
24
+ }
25
+
17
26
  /** Extract every !!run: command from a model response. */
18
27
  function parseRunCommands(text) {
19
28
  const commands = [];
@@ -64,6 +73,36 @@ function parseDeleteCommands(text) {
64
73
  return paths;
65
74
  }
66
75
 
76
+ /** Extract every !!write: path and its following fenced code block from a model response. */
77
+ function parseWriteCommands(text) {
78
+ const edits = [];
79
+ const lines = text.split('\n');
80
+ let i = 0;
81
+ const blockRegex = /```(?:[\w./\-]*)\n([\s\S]*?)```/;
82
+ while (i < lines.length) {
83
+ const line = lines[i];
84
+ const writeMatch = line.match(/^!!write:\s*(.+)$/);
85
+ if (writeMatch) {
86
+ const filepath = writeMatch[1].trim();
87
+ if (filepath) {
88
+ const rest = lines.slice(i + 1).join('\n');
89
+ const blockMatch = rest.match(blockRegex);
90
+ if (blockMatch) {
91
+ edits.push({ filepath, content: blockMatch[1] });
92
+ const blockText = blockMatch[0];
93
+ const beforeBlock = rest.substring(0, rest.indexOf(blockText));
94
+ const linesBeforeBlock = beforeBlock.split('\n').length;
95
+ const linesInBlock = blockText.split('\n').length;
96
+ i += 1 + linesBeforeBlock + linesInBlock;
97
+ continue;
98
+ }
99
+ }
100
+ }
101
+ i++;
102
+ }
103
+ return edits;
104
+ }
105
+
67
106
  /**
68
107
  * Parse and apply all file operations from a model reply.
69
108
  * Shows diffs/confirmations for each op. Returns updated allFiles list.
@@ -71,102 +110,124 @@ function parseDeleteCommands(text) {
71
110
  async function handleFileOps(reply, loadedFiles) {
72
111
  let allFiles = getFilesAndDirs();
73
112
 
74
- // Run terminal commands
75
- for (const cmd of parseRunCommands(reply)) {
76
- const ok = await confirm(chalk.bold(`Run: ${chalk.cyan(cmd)}? [y/N] `));
77
- if (ok) {
78
- // cd must be handled in-process child processes can't change the parent's cwd
79
- const cdMatch = cmd.match(/^cd\s+(.+)$/);
80
- if (cdMatch) {
81
- const target = resolve(process.cwd(), cdMatch[1].trim().replace(/^~/, homedir()));
82
- try {
83
- process.chdir(target);
84
- allFiles = getFilesAndDirs();
85
- console.log(chalk.dim(` 📁 ${process.cwd()}\n`));
86
- } catch (err) {
87
- console.log(chalk.red(` no such directory: ${target}\n`));
88
- }
89
- continue;
113
+ // Create folders first (so !!run: cd <folder> can succeed later)
114
+ for (const folderPath of parseMkdirCommands(reply)) {
115
+ const path = toRelativePath(folderPath);
116
+ process.stdout.write(chalk.dim(` mkdir: ${path} — `));
117
+ const confirmed = await confirm(chalk.bold(`Create folder ${chalk.cyan(path)}? [y/N] `));
118
+ if (confirmed) {
119
+ try {
120
+ mkdirSync(resolve(process.cwd(), path), { recursive: true });
121
+ allFiles = getFilesAndDirs();
122
+ console.log(chalk.green(`✓ created ${path}\n`));
123
+ } catch (err) {
124
+ console.log(chalk.red(`✗ could not create ${path}: ${err.message}\n`));
90
125
  }
91
- process.stdout.write(chalk.dim(` running: ${cmd}\n`));
92
- const { stdout, stderr, exitCode } = await execCommand(cmd);
93
- const output = [stdout, stderr].filter(Boolean).join('\n').trim();
94
- console.log(output ? chalk.dim(output) + '\n' : chalk.dim(` (exit ${exitCode})\n`));
95
- allFiles = getFilesAndDirs();
96
126
  } else {
97
127
  console.log(chalk.dim('skipped\n'));
98
128
  }
99
129
  }
100
130
 
101
- // Write / edit files
102
- const edits = parseEdits(reply, loadedFiles);
103
- for (const { filepath, content } of edits) {
104
- renderDiff(filepath, content);
105
- const confirmed = await confirm(chalk.bold(`Write ${chalk.cyan(filepath)}? [y/N] `));
131
+ // Create empty files
132
+ for (const filePath of parseTouchCommands(reply)) {
133
+ const path = toRelativePath(filePath);
134
+ const confirmed = await confirm(chalk.bold(`Create file ${chalk.cyan(path)}? [y/N] `));
106
135
  if (confirmed) {
107
136
  try {
108
- applyEdit(filepath, content);
137
+ const abs = resolve(process.cwd(), path);
138
+ const parentDir = abs.split('/').slice(0, -1).join('/');
139
+ mkdirSync(parentDir, { recursive: true });
140
+ writeFileSync(abs, '', { flag: 'wx' });
109
141
  allFiles = getFilesAndDirs();
110
- console.log(chalk.green(`✓ wrote ${filepath}\n`));
142
+ console.log(chalk.green(`✓ created ${path}\n`));
111
143
  } catch (err) {
112
- console.log(chalk.red(`✗ could not write ${filepath}: ${err.message}\n`));
144
+ console.log(chalk.red(`✗ could not create ${path}: ${err.message}\n`));
113
145
  }
114
146
  } else {
115
147
  console.log(chalk.dim('skipped\n'));
116
148
  }
117
149
  }
118
150
 
119
- // Create folders
120
- for (const folderPath of parseMkdirCommands(reply)) {
121
- process.stdout.write(chalk.dim(` mkdir: ${folderPath} `));
122
- const confirmed = await confirm(chalk.bold(`Create folder ${chalk.cyan(folderPath)}? [y/N] `));
151
+ // Write files via !!write: path + fenced block
152
+ for (const { filepath, content } of parseWriteCommands(reply)) {
153
+ const path = toRelativePath(filepath);
154
+ renderDiff(path, content);
155
+ const confirmed = await confirm(chalk.bold(`Write ${chalk.cyan(path)}? [y/N] `));
123
156
  if (confirmed) {
124
157
  try {
125
- mkdirSync(resolve(process.cwd(), folderPath), { recursive: true });
158
+ applyEdit(path, content);
126
159
  allFiles = getFilesAndDirs();
127
- console.log(chalk.green(`✓ created ${folderPath}\n`));
160
+ console.log(chalk.green(`✓ wrote ${path}\n`));
128
161
  } catch (err) {
129
- console.log(chalk.red(`✗ could not create ${folderPath}: ${err.message}\n`));
162
+ console.log(chalk.red(`✗ could not write ${path}: ${err.message}\n`));
130
163
  }
131
164
  } else {
132
165
  console.log(chalk.dim('skipped\n'));
133
166
  }
134
167
  }
135
168
 
136
- // Create empty files
137
- for (const filePath of parseTouchCommands(reply)) {
138
- const confirmed = await confirm(chalk.bold(`Create file ${chalk.cyan(filePath)}? [y/N] `));
169
+ // Write / edit files (fenced blocks with path/language or single attached file)
170
+ const edits = parseEdits(reply, loadedFiles);
171
+ for (const { filepath, content } of edits) {
172
+ const path = toRelativePath(filepath);
173
+ renderDiff(path, content);
174
+ const confirmed = await confirm(chalk.bold(`Write ${chalk.cyan(path)}? [y/N] `));
139
175
  if (confirmed) {
140
176
  try {
141
- const abs = resolve(process.cwd(), filePath);
142
- const parentDir = abs.split('/').slice(0, -1).join('/');
143
- mkdirSync(parentDir, { recursive: true });
144
- writeFileSync(abs, '', { flag: 'wx' });
177
+ applyEdit(path, content);
145
178
  allFiles = getFilesAndDirs();
146
- console.log(chalk.green(`✓ created ${filePath}\n`));
179
+ console.log(chalk.green(`✓ wrote ${path}\n`));
147
180
  } catch (err) {
148
- console.log(chalk.red(`✗ could not create ${filePath}: ${err.message}\n`));
181
+ console.log(chalk.red(`✗ could not write ${path}: ${err.message}\n`));
149
182
  }
150
183
  } else {
151
184
  console.log(chalk.dim('skipped\n'));
152
185
  }
153
186
  }
154
187
 
188
+ // Run terminal commands (after folders/files exist, so cd works)
189
+ for (const cmd of parseRunCommands(reply)) {
190
+ const ok = await confirm(chalk.bold(`Run: ${chalk.cyan(cmd)}? [y/N] `));
191
+ if (ok) {
192
+ // cd must be handled in-process — child processes can't change the parent's cwd
193
+ const cdMatch = cmd.match(/^cd\s+(.+)$/);
194
+ if (cdMatch) {
195
+ const target = resolve(process.cwd(), cdMatch[1].trim().replace(/^~/, homedir()));
196
+ try {
197
+ process.chdir(target);
198
+ allFiles = getFilesAndDirs();
199
+ console.log(chalk.dim(` 📁 ${process.cwd()}\n`));
200
+ } catch (err) {
201
+ console.log(chalk.red(` no such directory: ${target}\n`));
202
+ }
203
+ continue;
204
+ }
205
+ process.stdout.write(chalk.dim(` running: ${cmd}\n`));
206
+ const { stdout, stderr, exitCode } = await execCommand(cmd);
207
+ const output = [stdout, stderr].filter(Boolean).join('\n').trim();
208
+ console.log(output ? chalk.dim(output) + '\n' : chalk.dim(` (exit ${exitCode})\n`));
209
+ allFiles = getFilesAndDirs();
210
+ } else {
211
+ console.log(chalk.dim('skipped\n'));
212
+ }
213
+ }
214
+
155
215
  // Remove directories
156
216
  for (const dirPath of parseRmdirCommands(reply)) {
157
- const confirmed = await confirm(chalk.bold(`Remove directory ${chalk.cyan(dirPath)}? [y/N] `));
217
+ const path = toRelativePath(dirPath);
218
+ const confirmed = await confirm(chalk.bold(`Remove directory ${chalk.cyan(path)}? [y/N] `));
158
219
  if (confirmed) {
159
- const abs = resolve(process.cwd(), dirPath);
220
+ const abs = resolve(process.cwd(), path);
160
221
  if (existsSync(abs)) {
161
222
  try {
162
223
  rmSync(abs, { recursive: true, force: true });
163
224
  allFiles = getFilesAndDirs();
164
- console.log(chalk.green(`✓ removed ${dirPath}\n`));
225
+ console.log(chalk.green(`✓ removed ${path}\n`));
165
226
  } catch (err) {
166
- console.log(chalk.red(`✗ could not remove ${dirPath}: ${err.message}\n`));
227
+ console.log(chalk.red(`✗ could not remove ${path}: ${err.message}\n`));
167
228
  }
168
229
  } else {
169
- console.log(chalk.yellow(`⚠ not found: ${dirPath}\n`));
230
+ console.log(chalk.yellow(`⚠ not found: ${path}\n`));
170
231
  }
171
232
  } else {
172
233
  console.log(chalk.dim('skipped\n'));
@@ -175,19 +236,20 @@ async function handleFileOps(reply, loadedFiles) {
175
236
 
176
237
  // Delete files
177
238
  for (const filePath of parseDeleteCommands(reply)) {
178
- const confirmed = await confirm(chalk.bold(`Delete ${chalk.cyan(filePath)}? [y/N] `));
239
+ const path = toRelativePath(filePath);
240
+ const confirmed = await confirm(chalk.bold(`Delete ${chalk.cyan(path)}? [y/N] `));
179
241
  if (confirmed) {
180
- const abs = resolve(process.cwd(), filePath);
242
+ const abs = resolve(process.cwd(), path);
181
243
  if (existsSync(abs)) {
182
244
  try {
183
245
  unlinkSync(abs);
184
246
  allFiles = getFilesAndDirs();
185
- console.log(chalk.green(`✓ deleted ${filePath}\n`));
247
+ console.log(chalk.green(`✓ deleted ${path}\n`));
186
248
  } catch (err) {
187
- console.log(chalk.red(`✗ could not delete ${filePath}: ${err.message}\n`));
249
+ console.log(chalk.red(`✗ could not delete ${path}: ${err.message}\n`));
188
250
  }
189
251
  } else {
190
- console.log(chalk.yellow(`⚠ not found: ${filePath}\n`));
252
+ console.log(chalk.yellow(`⚠ not found: ${path}\n`));
191
253
  }
192
254
  } else {
193
255
  console.log(chalk.dim('skipped\n'));
@@ -354,14 +416,24 @@ function buildSystemMessage() {
354
416
  const fileList = files.length > 0 ? `\nFiles in working directory:\n${files.map(f => ` ${f}`).join('\n')}\n` : '';
355
417
  const fileOpsInstructions =
356
418
  `\nFILE OPERATIONS — use these exact syntaxes when needed:\n` +
419
+ `- Always use RELATIVE paths (e.g. path/to/file or markov/next-app/README.md), never absolute paths like /Users/...\n` +
357
420
  `- Run a terminal command: output exactly on its own line: !!run: <command>\n` +
358
- `- Write or edit a file: output "FILE: path/to/file" followed by a fenced code block with the full file content.\n` +
421
+ `- Write or edit a file: output exactly on its own line !!write: path/to/file, then a fenced code block with the full file content.\n` +
359
422
  `- Create an empty file: output exactly on its own line: !!touch: path/to/file\n` +
360
423
  `- Create a folder: output exactly on its own line: !!mkdir: path/to/folder\n` +
361
424
  `- Remove a folder: output exactly on its own line: !!rmdir: path/to/folder\n` +
362
425
  `- Delete a file: output exactly on its own line: !!delete: path/to/file\n` +
363
426
  `- You may combine multiple operations in one response.\n` +
364
- `- NEVER put commands in fenced code blocks — always use !!run: syntax for commands.\n`;
427
+ `- NEVER put commands in fenced code blocks — always use !!run: syntax for commands.\n` +
428
+ `- NEVER use && inside !!run: — use one !!run: per command. For "cd then do X", output !!run: cd <path> on one line, then !!run: <command> on the next (cd changes the working directory for all following commands).\n` +
429
+ `\nSETUP NEXT.JS APP (or any new project in a subfolder):\n` +
430
+ `1. Create the folder first: !!mkdir: next-app (or the requested name).\n` +
431
+ `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`;
365
437
  return { role: 'system', content: `You are Markov, an AI coding assistant.\nWorking directory: ${process.cwd()}\n${fileList}${fileOpsInstructions}` };
366
438
  }
367
439
 
@@ -511,7 +583,7 @@ export async function startInteractive() {
511
583
  const planPrompt =
512
584
  `Create a detailed, numbered plan for the following task:\n\n${planRequest}\n\n` +
513
585
  `For each step, specify exactly what will happen and which syntax will be used:\n` +
514
- `- Writing or editing a file → FILE: path/to/file + fenced code block\n` +
586
+ `- Writing or editing a file → !!write: path/to/file then fenced code block\n` +
515
587
  `- Creating an empty file → !!touch: path/to/file\n` +
516
588
  `- Creating a folder → !!mkdir: path/to/folder\n` +
517
589
  `- Removing a folder → !!rmdir: path/to/folder\n` +
@@ -542,7 +614,7 @@ export async function startInteractive() {
542
614
  }
543
615
 
544
616
  const buildPrompt =
545
- `Execute the following plan. Use FILE: syntax for file writes, !!mkdir: for folders, !!delete: for deletions.\n\n` +
617
+ `Execute the following plan. Use !!write: path then a fenced code block for file writes, !!mkdir: for folders, !!delete: for deletions.\n\n` +
546
618
  `Plan:\n${currentPlan.text}`;
547
619
 
548
620
  chatMessages.push({ role: 'user', content: buildPrompt });