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 +1 -1
- package/src/editor.js +2 -9
- package/src/interactive.js +132 -60
package/package.json
CHANGED
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
|
-
//
|
|
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];
|
package/src/interactive.js
CHANGED
|
@@ -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
|
-
//
|
|
75
|
-
for (const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
//
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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(`✓
|
|
142
|
+
console.log(chalk.green(`✓ created ${path}\n`));
|
|
111
143
|
} catch (err) {
|
|
112
|
-
console.log(chalk.red(`✗ could not
|
|
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
|
-
//
|
|
120
|
-
for (const
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
158
|
+
applyEdit(path, content);
|
|
126
159
|
allFiles = getFilesAndDirs();
|
|
127
|
-
console.log(chalk.green(`✓
|
|
160
|
+
console.log(chalk.green(`✓ wrote ${path}\n`));
|
|
128
161
|
} catch (err) {
|
|
129
|
-
console.log(chalk.red(`✗ could not
|
|
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
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
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(`✓
|
|
179
|
+
console.log(chalk.green(`✓ wrote ${path}\n`));
|
|
147
180
|
} catch (err) {
|
|
148
|
-
console.log(chalk.red(`✗ could not
|
|
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
|
|
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(),
|
|
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 ${
|
|
225
|
+
console.log(chalk.green(`✓ removed ${path}\n`));
|
|
165
226
|
} catch (err) {
|
|
166
|
-
console.log(chalk.red(`✗ could not remove ${
|
|
227
|
+
console.log(chalk.red(`✗ could not remove ${path}: ${err.message}\n`));
|
|
167
228
|
}
|
|
168
229
|
} else {
|
|
169
|
-
console.log(chalk.yellow(`⚠ not found: ${
|
|
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
|
|
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(),
|
|
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 ${
|
|
247
|
+
console.log(chalk.green(`✓ deleted ${path}\n`));
|
|
186
248
|
} catch (err) {
|
|
187
|
-
console.log(chalk.red(`✗ could not delete ${
|
|
249
|
+
console.log(chalk.red(`✗ could not delete ${path}: ${err.message}\n`));
|
|
188
250
|
}
|
|
189
251
|
} else {
|
|
190
|
-
console.log(chalk.yellow(`⚠ not found: ${
|
|
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
|
|
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 →
|
|
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
|
|
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 });
|