markov-cli 1.0.1 → 1.0.2
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 +4 -3
- package/src/input.js +14 -5
- package/src/interactive.js +351 -43
- package/src/ui/logo.js +1 -12
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "markov-cli",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "LivingCloud's AI Agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"markov": "./bin/markov.js"
|
|
7
|
+
"markov": "./bin/markov.js",
|
|
8
|
+
"markov-local": "./bin/markov.js"
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
11
|
"start": "node bin/markov.js"
|
package/src/input.js
CHANGED
|
@@ -7,7 +7,7 @@ const visibleLen = (s) => s.replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
|
7
7
|
|
|
8
8
|
const PREFIX = '❯ ';
|
|
9
9
|
const HINT = chalk.dim(' Ask Markov anything...');
|
|
10
|
-
const STATUS_LEFT = chalk.dim('
|
|
10
|
+
const STATUS_LEFT = chalk.dim('/help');
|
|
11
11
|
const PICKER_MAX = 6;
|
|
12
12
|
|
|
13
13
|
function border() {
|
|
@@ -83,13 +83,22 @@ export function chatPrompt(_promptStr, allFiles) {
|
|
|
83
83
|
stdout.write(rows.join('\n'));
|
|
84
84
|
|
|
85
85
|
// rows = [border, inputLine, border, ...pickerRows(N), statusBar]
|
|
86
|
-
//
|
|
87
|
-
//
|
|
86
|
+
// upAmount is always pickerRows.length + 2 regardless of input line wrapping
|
|
87
|
+
// because the extra visual lines from wrapping and the shifted target cancel out.
|
|
88
88
|
const upAmount = pickerRows.length + 2;
|
|
89
89
|
stdout.write(`\x1b[${upAmount}A\r`);
|
|
90
|
-
cursorLineOffset = 1; // inputLine is always 1 line from the top
|
|
91
90
|
|
|
92
|
-
|
|
91
|
+
// Account for input line wrapping: if the input is wider than the terminal,
|
|
92
|
+
// it takes multiple visual lines and we must move up by that many to reach
|
|
93
|
+
// the top border on the next redraw.
|
|
94
|
+
const w = process.stdout.columns || 80;
|
|
95
|
+
const inputVisualLen = visibleLen(chalk.cyan(PREFIX)) + visibleLen(buffer || HINT);
|
|
96
|
+
const inputVisualLines = Math.max(1, Math.ceil(inputVisualLen / w));
|
|
97
|
+
cursorLineOffset = inputVisualLines;
|
|
98
|
+
|
|
99
|
+
// Position cursor at the correct column on the last visual line of the input.
|
|
100
|
+
const totalInputLen = visibleLen(chalk.cyan(PREFIX)) + visibleLen(buffer);
|
|
101
|
+
const col = (totalInputLen % w) + 1;
|
|
93
102
|
stdout.write(`\x1b[${col}G`);
|
|
94
103
|
};
|
|
95
104
|
|
package/src/interactive.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import gradient from 'gradient-string';
|
|
2
3
|
import { homedir } from 'os';
|
|
3
|
-
import { resolve } from 'path';
|
|
4
|
+
import { resolve, dirname, join } from 'path';
|
|
5
|
+
import { readdirSync } from 'fs';
|
|
4
6
|
import { printLogo } from './ui/logo.js';
|
|
5
7
|
import { streamChat, MODEL, MODELS, setModel } from './ollama.js';
|
|
6
8
|
import { resolveFileRefs } from './files.js';
|
|
@@ -9,6 +11,8 @@ import { chatPrompt } from './input.js';
|
|
|
9
11
|
import { getFiles } from './ui/picker.js';
|
|
10
12
|
import { getToken, login } from './auth.js';
|
|
11
13
|
|
|
14
|
+
const markovGradient = gradient(['#6366f1', '#a855f7', '#ec4899']);
|
|
15
|
+
|
|
12
16
|
/** Arrow-key selector. Returns the chosen string or null if cancelled. */
|
|
13
17
|
function selectFrom(options, label) {
|
|
14
18
|
return new Promise((resolve) => {
|
|
@@ -124,13 +128,140 @@ function promptSecret(label) {
|
|
|
124
128
|
});
|
|
125
129
|
}
|
|
126
130
|
|
|
131
|
+
const VIEWPORT_LINES = 5;
|
|
132
|
+
const TERM_WIDTH = 80;
|
|
133
|
+
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
134
|
+
const visLen = (s) => s.replace(ANSI_RE, '').length;
|
|
135
|
+
|
|
136
|
+
/** Word-wrap plain text to a given column width, preserving existing newlines. */
|
|
137
|
+
function wrapText(text, width) {
|
|
138
|
+
return text.split('\n').map(line => {
|
|
139
|
+
if (visLen(line) <= width) return line;
|
|
140
|
+
const words = line.split(' ');
|
|
141
|
+
const wrapped = [];
|
|
142
|
+
let current = '';
|
|
143
|
+
for (const word of words) {
|
|
144
|
+
const test = current ? current + ' ' + word : word;
|
|
145
|
+
if (visLen(test) <= width) {
|
|
146
|
+
current = test;
|
|
147
|
+
} else {
|
|
148
|
+
if (current) wrapped.push(current);
|
|
149
|
+
current = word;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (current) wrapped.push(current);
|
|
153
|
+
return wrapped.join('\n');
|
|
154
|
+
}).join('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Count the number of terminal rows a list of lines occupies, accounting for wrapping. */
|
|
158
|
+
function countRows(lines, w) {
|
|
159
|
+
return lines.reduce((sum, l) => sum + Math.max(1, Math.ceil(visLen(l) / w)), 0);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Stream a chat response with a live 5-line viewport.
|
|
164
|
+
* While streaming, only the last VIEWPORT_LINES lines are shown in-place.
|
|
165
|
+
* After streaming finishes, the viewport is replaced with the full response.
|
|
166
|
+
* Returns the full reply string.
|
|
167
|
+
*/
|
|
168
|
+
async function streamWithViewport(chatMessages, signal) {
|
|
169
|
+
const DOTS = ['.', '..', '...'];
|
|
170
|
+
let dotIdx = 0;
|
|
171
|
+
let firstToken = true;
|
|
172
|
+
let fullText = '';
|
|
173
|
+
let viewportRows = 0; // actual terminal rows currently rendered in the viewport
|
|
174
|
+
|
|
175
|
+
process.stdout.write(chalk.dim('\nMarkov › '));
|
|
176
|
+
const spinner = setInterval(() => {
|
|
177
|
+
process.stdout.write('\r' + chalk.dim('Markov › ') + markovGradient(DOTS[dotIdx % DOTS.length]) + ' ');
|
|
178
|
+
dotIdx++;
|
|
179
|
+
}, 400);
|
|
180
|
+
|
|
181
|
+
const onCancel = (data) => { if (data.toString() === '\x11') signal.abort(); };
|
|
182
|
+
process.stdin.setRawMode(true);
|
|
183
|
+
process.stdin.resume();
|
|
184
|
+
process.stdin.setEncoding('utf8');
|
|
185
|
+
process.stdin.on('data', onCancel);
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const reply = await streamChat(chatMessages, (token) => {
|
|
189
|
+
if (firstToken) {
|
|
190
|
+
clearInterval(spinner);
|
|
191
|
+
firstToken = false;
|
|
192
|
+
process.stdout.write('\r\x1b[0J' + chalk.dim('Markov ›\n\n'));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fullText += token;
|
|
196
|
+
|
|
197
|
+
// Build the viewport: last VIEWPORT_LINES lines of buffered text.
|
|
198
|
+
const w = process.stdout.columns || TERM_WIDTH;
|
|
199
|
+
const displayWidth = Math.min(w, TERM_WIDTH);
|
|
200
|
+
const lines = wrapText(fullText, displayWidth).split('\n');
|
|
201
|
+
const viewLines = lines.slice(-VIEWPORT_LINES);
|
|
202
|
+
const rendered = viewLines.join('\n');
|
|
203
|
+
|
|
204
|
+
// Count actual terminal rows rendered (accounts for line wrapping).
|
|
205
|
+
const newRows = countRows(viewLines, w) - 1;
|
|
206
|
+
|
|
207
|
+
// Move cursor up to top of current viewport, clear down, rewrite.
|
|
208
|
+
if (viewportRows > 0) {
|
|
209
|
+
process.stdout.write(`\x1b[${viewportRows}A\r\x1b[0J`);
|
|
210
|
+
}
|
|
211
|
+
process.stdout.write(rendered);
|
|
212
|
+
viewportRows = newRows;
|
|
213
|
+
}, undefined, signal);
|
|
214
|
+
|
|
215
|
+
process.stdin.removeListener('data', onCancel);
|
|
216
|
+
process.stdin.setRawMode(false);
|
|
217
|
+
process.stdin.pause();
|
|
218
|
+
clearInterval(spinner);
|
|
219
|
+
|
|
220
|
+
if (signal.aborted) {
|
|
221
|
+
if (viewportRows > 0) process.stdout.write(`\x1b[${viewportRows}A\r\x1b[0J`);
|
|
222
|
+
console.log(chalk.dim('(cancelled)\n'));
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Replace viewport with the full response.
|
|
227
|
+
if (viewportRows > 0) process.stdout.write(`\x1b[${viewportRows}A\r\x1b[0J`);
|
|
228
|
+
const finalWidth = Math.min(process.stdout.columns || TERM_WIDTH, TERM_WIDTH);
|
|
229
|
+
process.stdout.write(wrapText(fullText, finalWidth) + '\n\n');
|
|
230
|
+
|
|
231
|
+
return reply;
|
|
232
|
+
} catch (err) {
|
|
233
|
+
clearInterval(spinner);
|
|
234
|
+
process.stdin.removeListener('data', onCancel);
|
|
235
|
+
process.stdin.setRawMode(false);
|
|
236
|
+
process.stdin.pause();
|
|
237
|
+
if (!signal.aborted) throw err;
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const HELP_TEXT =
|
|
243
|
+
'\n' +
|
|
244
|
+
chalk.bold('Commands:\n') +
|
|
245
|
+
chalk.cyan(' /help') + chalk.dim(' show this help\n') +
|
|
246
|
+
chalk.cyan(' /plan <message>') + chalk.dim(' ask AI to create a plan (no files written)\n') +
|
|
247
|
+
chalk.cyan(' /build') + chalk.dim(' execute the stored plan\n') +
|
|
248
|
+
chalk.cyan(' /yolo <message>') + chalk.dim(' AI can create/edit any file in cwd\n') +
|
|
249
|
+
chalk.cyan(' /edit <message>') + chalk.dim(' AI can edit files in @-referenced directories\n') +
|
|
250
|
+
chalk.cyan(' /new <file> [desc]') + chalk.dim(' create a new file with AI\n') +
|
|
251
|
+
chalk.cyan(' /models') + chalk.dim(' switch the active AI model\n') +
|
|
252
|
+
chalk.cyan(' /cd [path]') + chalk.dim(' change working directory\n') +
|
|
253
|
+
chalk.cyan(' /login') + chalk.dim(' authenticate with email & password\n') +
|
|
254
|
+
chalk.dim('\nTips: use ') + chalk.cyan('@filename') + chalk.dim(' to attach a file · ctrl+q to cancel a response\n');
|
|
255
|
+
|
|
127
256
|
export async function startInteractive() {
|
|
128
257
|
printLogo();
|
|
129
258
|
|
|
130
259
|
let allFiles = getFiles();
|
|
131
260
|
const chatMessages = [];
|
|
261
|
+
let currentPlan = null; // { text: string } | null
|
|
132
262
|
|
|
133
|
-
console.log(chalk.dim(`Chat with Markov (${MODEL})
|
|
263
|
+
console.log(chalk.dim(`Chat with Markov (${MODEL}).`));
|
|
264
|
+
console.log(HELP_TEXT);
|
|
134
265
|
|
|
135
266
|
if (!getToken()) {
|
|
136
267
|
console.log(chalk.yellow('⚠ Not logged in. Use /login to authenticate.\n'));
|
|
@@ -156,6 +287,12 @@ export async function startInteractive() {
|
|
|
156
287
|
continue;
|
|
157
288
|
}
|
|
158
289
|
|
|
290
|
+
// /help — list all commands
|
|
291
|
+
if (trimmed === '/help') {
|
|
292
|
+
console.log(HELP_TEXT);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
|
|
159
296
|
// /models — pick active model
|
|
160
297
|
if (trimmed === '/models') {
|
|
161
298
|
const chosen = await selectFrom(MODELS, 'Select model:');
|
|
@@ -166,6 +303,58 @@ export async function startInteractive() {
|
|
|
166
303
|
continue;
|
|
167
304
|
}
|
|
168
305
|
|
|
306
|
+
// /new <filename> [description] — create a new file with AI
|
|
307
|
+
if (trimmed.startsWith('/new ')) {
|
|
308
|
+
const args = trimmed.slice(5).trim();
|
|
309
|
+
const spaceIdx = args.indexOf(' ');
|
|
310
|
+
const filename = spaceIdx === -1 ? args : args.slice(0, spaceIdx);
|
|
311
|
+
const description = spaceIdx === -1 ? '' : args.slice(spaceIdx + 1).trim();
|
|
312
|
+
|
|
313
|
+
if (!filename) {
|
|
314
|
+
console.log(chalk.yellow('\nUsage: /new <filename> [description]\n'));
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const newFilePrompt = description
|
|
319
|
+
? `Create a new file called \`${filename}\` with the following requirements:\n${description}\n\nOutput the complete file content in a fenced code block tagged with the exact filename like this:\n\`\`\`${filename}\n// content here\n\`\`\``
|
|
320
|
+
: `Create a new file called \`${filename}\`. Output the complete file content in a fenced code block tagged with the exact filename like this:\n\`\`\`${filename}\n// content here\n\`\`\``;
|
|
321
|
+
|
|
322
|
+
chatMessages.push({ role: 'user', content: newFilePrompt });
|
|
323
|
+
|
|
324
|
+
const abortController = new AbortController();
|
|
325
|
+
try {
|
|
326
|
+
const reply = await streamWithViewport(chatMessages, abortController.signal);
|
|
327
|
+
|
|
328
|
+
if (reply === null) {
|
|
329
|
+
chatMessages.pop();
|
|
330
|
+
} else {
|
|
331
|
+
chatMessages.push({ role: 'assistant', content: reply });
|
|
332
|
+
|
|
333
|
+
const edits = parseEdits(reply, [filename]);
|
|
334
|
+
for (const { filepath, content } of edits) {
|
|
335
|
+
renderDiff(filepath, content);
|
|
336
|
+
const confirmed = await confirm(chalk.bold(`Create ${chalk.cyan(filepath)}? [y/N] `));
|
|
337
|
+
if (confirmed) {
|
|
338
|
+
try {
|
|
339
|
+
applyEdit(filepath, content);
|
|
340
|
+
allFiles = getFiles();
|
|
341
|
+
console.log(chalk.green(`✓ created ${filepath}\n`));
|
|
342
|
+
} catch (err) {
|
|
343
|
+
console.log(chalk.red(`✗ could not write ${filepath}: ${err.message}\n`));
|
|
344
|
+
}
|
|
345
|
+
} else {
|
|
346
|
+
console.log(chalk.dim('skipped\n'));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
} catch (err) {
|
|
351
|
+
if (!abortController.signal.aborted) {
|
|
352
|
+
console.log(chalk.red(`\n${err.message}\n`));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
|
|
169
358
|
// /cd [path] — change working directory within this session
|
|
170
359
|
if (trimmed.startsWith('/cd')) {
|
|
171
360
|
const arg = trimmed.slice(3).trim();
|
|
@@ -182,6 +371,140 @@ export async function startInteractive() {
|
|
|
182
371
|
continue;
|
|
183
372
|
}
|
|
184
373
|
|
|
374
|
+
// /plan <message> — ask LLM to produce a plan, store it (no files written)
|
|
375
|
+
if (trimmed.startsWith('/plan ')) {
|
|
376
|
+
const planRequest = trimmed.slice(6).trim();
|
|
377
|
+
const planPrompt =
|
|
378
|
+
`You are a planning assistant. Create a detailed, numbered plan for the following task:\n\n${planRequest}\n\n` +
|
|
379
|
+
`For each step specify what to do and which files to create or modify (with exact relative paths).\n` +
|
|
380
|
+
`Do NOT write any code yet — only the plan.`;
|
|
381
|
+
|
|
382
|
+
chatMessages.push({ role: 'user', content: planPrompt });
|
|
383
|
+
const abortController = new AbortController();
|
|
384
|
+
try {
|
|
385
|
+
const reply = await streamWithViewport(chatMessages, abortController.signal);
|
|
386
|
+
if (reply === null) {
|
|
387
|
+
chatMessages.pop();
|
|
388
|
+
} else {
|
|
389
|
+
chatMessages.push({ role: 'assistant', content: reply });
|
|
390
|
+
currentPlan = { text: reply };
|
|
391
|
+
console.log(chalk.green('\n✓ Plan stored. Use /build to execute it.\n'));
|
|
392
|
+
}
|
|
393
|
+
} catch (err) {
|
|
394
|
+
if (!abortController.signal.aborted) console.log(chalk.red(`\n${err.message}\n`));
|
|
395
|
+
}
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// /build — execute the stored plan
|
|
400
|
+
if (trimmed === '/build') {
|
|
401
|
+
if (!currentPlan) {
|
|
402
|
+
console.log(chalk.yellow('\n⚠ No plan stored. Use /plan <message> first.\n'));
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const buildPrompt =
|
|
407
|
+
`Execute the following plan by outputting ALL files to create or modify as fenced code blocks tagged with their exact relative filenames.\n\n` +
|
|
408
|
+
`Plan:\n${currentPlan.text}\n\n` +
|
|
409
|
+
`Output each file completely in a fenced code block like:\n\`\`\`path/to/file.ext\n// full content\n\`\`\``;
|
|
410
|
+
|
|
411
|
+
chatMessages.push({ role: 'user', content: buildPrompt });
|
|
412
|
+
const abortController = new AbortController();
|
|
413
|
+
try {
|
|
414
|
+
const reply = await streamWithViewport(chatMessages, abortController.signal);
|
|
415
|
+
if (reply === null) {
|
|
416
|
+
chatMessages.pop();
|
|
417
|
+
} else {
|
|
418
|
+
chatMessages.push({ role: 'assistant', content: reply });
|
|
419
|
+
const edits = parseEdits(reply, []);
|
|
420
|
+
const appliedEdits = [];
|
|
421
|
+
for (const { filepath, content } of edits) {
|
|
422
|
+
renderDiff(filepath, content);
|
|
423
|
+
const confirmed = await confirm(chalk.bold(`Write ${chalk.cyan(filepath)}? [y/N] `));
|
|
424
|
+
if (confirmed) {
|
|
425
|
+
try {
|
|
426
|
+
applyEdit(filepath, content);
|
|
427
|
+
allFiles = getFiles();
|
|
428
|
+
console.log(chalk.green(`✓ wrote ${filepath}\n`));
|
|
429
|
+
appliedEdits.push({ filepath, content });
|
|
430
|
+
} catch (err) {
|
|
431
|
+
console.log(chalk.red(`✗ could not write ${filepath}: ${err.message}\n`));
|
|
432
|
+
}
|
|
433
|
+
} else {
|
|
434
|
+
console.log(chalk.dim('skipped\n'));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (appliedEdits.length > 0) {
|
|
438
|
+
currentPlan = null;
|
|
439
|
+
console.log(chalk.green('✓ Plan executed.\n'));
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
} catch (err) {
|
|
443
|
+
if (!abortController.signal.aborted) console.log(chalk.red(`\n${err.message}\n`));
|
|
444
|
+
}
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// /yolo <message> — AI can create/edit any file in cwd; @refs loaded for context
|
|
449
|
+
if (trimmed.startsWith('/yolo ')) {
|
|
450
|
+
const yoloMessage = trimmed.slice(6).trim();
|
|
451
|
+
const { content: yoloContent, loaded: yoloLoaded, failed: yoloFailed } = resolveFileRefs(yoloMessage);
|
|
452
|
+
|
|
453
|
+
if (yoloLoaded.length > 0) {
|
|
454
|
+
console.log(chalk.dim(`\n📎 attached: ${yoloLoaded.map(f => chalk.cyan(`@${f}`)).join(', ')}`));
|
|
455
|
+
}
|
|
456
|
+
if (yoloFailed.length > 0) {
|
|
457
|
+
console.log(chalk.yellow(`\n⚠ not found: ${yoloFailed.map(f => `@${f}`).join(', ')}`));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const yoloInstruction =
|
|
461
|
+
`You have permission to create or modify ANY file in the current working directory.\n` +
|
|
462
|
+
`Output every file you create or modify as a fenced code block tagged with its exact relative path:\n` +
|
|
463
|
+
`\`\`\`path/to/file.ext\n// full content\n\`\`\`\n\n`;
|
|
464
|
+
|
|
465
|
+
const yoloFull = yoloContent.startsWith('The user referenced')
|
|
466
|
+
? yoloContent.replace('The user referenced', yoloInstruction + 'The user referenced')
|
|
467
|
+
: yoloInstruction + yoloContent;
|
|
468
|
+
|
|
469
|
+
chatMessages.push({ role: 'user', content: yoloFull });
|
|
470
|
+
const abortController = new AbortController();
|
|
471
|
+
try {
|
|
472
|
+
const reply = await streamWithViewport(chatMessages, abortController.signal);
|
|
473
|
+
if (reply === null) {
|
|
474
|
+
chatMessages.pop();
|
|
475
|
+
} else {
|
|
476
|
+
chatMessages.push({ role: 'assistant', content: reply });
|
|
477
|
+
const edits = parseEdits(reply, yoloLoaded);
|
|
478
|
+
const appliedEdits = [];
|
|
479
|
+
for (const { filepath, content } of edits) {
|
|
480
|
+
renderDiff(filepath, content);
|
|
481
|
+
const confirmed = await confirm(chalk.bold(`Write ${chalk.cyan(filepath)}? [y/N] `));
|
|
482
|
+
if (confirmed) {
|
|
483
|
+
try {
|
|
484
|
+
applyEdit(filepath, content);
|
|
485
|
+
allFiles = getFiles();
|
|
486
|
+
console.log(chalk.green(`✓ wrote ${filepath}\n`));
|
|
487
|
+
appliedEdits.push({ filepath, content });
|
|
488
|
+
} catch (err) {
|
|
489
|
+
console.log(chalk.red(`✗ could not write ${filepath}: ${err.message}\n`));
|
|
490
|
+
}
|
|
491
|
+
} else {
|
|
492
|
+
console.log(chalk.dim('skipped\n'));
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (appliedEdits.length > 0) {
|
|
496
|
+
const blocks = appliedEdits.map(({ filepath, content }) => `--- @${filepath} ---\n${content}\n---`);
|
|
497
|
+
chatMessages.push({ role: 'user', content: `Files saved:\n\n${blocks.join('\n\n')}` });
|
|
498
|
+
chatMessages.push({ role: 'assistant', content: 'Got it, I have the updated file contents.' });
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
} catch (err) {
|
|
502
|
+
if (!abortController.signal.aborted) console.log(chalk.red(`\n${err.message}\n`));
|
|
503
|
+
}
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Determine mode
|
|
185
508
|
const isEditMode = trimmed.startsWith('/edit ');
|
|
186
509
|
const message = isEditMode ? trimmed.slice(6).trim() : trimmed;
|
|
187
510
|
|
|
@@ -194,52 +517,40 @@ export async function startInteractive() {
|
|
|
194
517
|
console.log(chalk.yellow(`\n⚠ not found: ${failed.map(f => `@${f}`).join(', ')}`));
|
|
195
518
|
}
|
|
196
519
|
|
|
197
|
-
|
|
520
|
+
// For /edit, expand scope to all files in the directories of @-referenced files.
|
|
521
|
+
let editDirFiles = [];
|
|
522
|
+
let editDirInstruction = '';
|
|
523
|
+
if (isEditMode && loaded.length > 0) {
|
|
524
|
+
const dirs = [...new Set(loaded.map(f => dirname(f) || '.'))];
|
|
525
|
+
for (const dir of dirs) {
|
|
526
|
+
try {
|
|
527
|
+
const entries = readdirSync(join(process.cwd(), dir), { withFileTypes: true });
|
|
528
|
+
for (const e of entries) {
|
|
529
|
+
if (e.isFile()) editDirFiles.push(dir === '.' ? e.name : `${dir}/${e.name}`);
|
|
530
|
+
}
|
|
531
|
+
} catch { /* ignore unreadable dirs */ }
|
|
532
|
+
}
|
|
533
|
+
editDirInstruction =
|
|
534
|
+
`\nYou may edit any file in the following director${dirs.length > 1 ? 'ies' : 'y'}: ${dirs.join(', ')}.\n` +
|
|
535
|
+
`Output modified files as fenced code blocks tagged with their exact relative path.\n`;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const fullContent = isEditMode && editDirInstruction
|
|
539
|
+
? content + editDirInstruction
|
|
540
|
+
: content;
|
|
198
541
|
|
|
199
|
-
|
|
200
|
-
const DOTS = ['.', '..', '...'];
|
|
201
|
-
let dotIdx = 0;
|
|
202
|
-
let firstToken = true;
|
|
203
|
-
process.stdout.write(chalk.dim('\nMarkov › '));
|
|
204
|
-
const spinner = setInterval(() => {
|
|
205
|
-
process.stdout.write('\r' + chalk.dim('Markov › ' + DOTS[dotIdx % DOTS.length] + ' '));
|
|
206
|
-
dotIdx++;
|
|
207
|
-
}, 400);
|
|
542
|
+
chatMessages.push({ role: 'user', content: fullContent });
|
|
208
543
|
|
|
209
544
|
const abortController = new AbortController();
|
|
210
545
|
try {
|
|
546
|
+
const reply = await streamWithViewport(chatMessages, abortController.signal);
|
|
211
547
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (data.toString() === '\x11') abortController.abort();
|
|
215
|
-
};
|
|
216
|
-
process.stdin.setRawMode(true);
|
|
217
|
-
process.stdin.resume();
|
|
218
|
-
process.stdin.setEncoding('utf8');
|
|
219
|
-
process.stdin.on('data', onCancel);
|
|
220
|
-
|
|
221
|
-
const reply = await streamChat(chatMessages, (token) => {
|
|
222
|
-
if (firstToken) {
|
|
223
|
-
clearInterval(spinner);
|
|
224
|
-
firstToken = false;
|
|
225
|
-
process.stdout.write('\r\x1b[0J' + chalk.dim('Markov ›\n\n'));
|
|
226
|
-
}
|
|
227
|
-
process.stdout.write(token);
|
|
228
|
-
}, undefined, abortController.signal);
|
|
229
|
-
|
|
230
|
-
process.stdin.removeListener('data', onCancel);
|
|
231
|
-
process.stdin.setRawMode(false);
|
|
232
|
-
process.stdin.pause();
|
|
233
|
-
clearInterval(spinner);
|
|
234
|
-
|
|
235
|
-
if (abortController.signal.aborted) {
|
|
236
|
-
console.log(chalk.dim('\n(cancelled)\n'));
|
|
548
|
+
if (reply === null) {
|
|
549
|
+
chatMessages.pop();
|
|
237
550
|
} else {
|
|
238
|
-
console.log('\n');
|
|
239
551
|
chatMessages.push({ role: 'assistant', content: reply });
|
|
240
552
|
|
|
241
|
-
|
|
242
|
-
const edits = isEditMode ? parseEdits(reply, loaded) : [];
|
|
553
|
+
const edits = isEditMode ? parseEdits(reply, [...loaded, ...editDirFiles]) : [];
|
|
243
554
|
const appliedEdits = [];
|
|
244
555
|
for (const { filepath, content } of edits) {
|
|
245
556
|
renderDiff(filepath, content);
|
|
@@ -257,8 +568,6 @@ export async function startInteractive() {
|
|
|
257
568
|
}
|
|
258
569
|
}
|
|
259
570
|
|
|
260
|
-
// Refresh chat context with updated file contents so the model
|
|
261
|
-
// doesn't reference stale content from earlier in the conversation.
|
|
262
571
|
if (appliedEdits.length > 0) {
|
|
263
572
|
const blocks = appliedEdits.map(({ filepath, content }) => `--- @${filepath} ---\n${content}\n---`);
|
|
264
573
|
chatMessages.push({
|
|
@@ -269,7 +578,6 @@ export async function startInteractive() {
|
|
|
269
578
|
}
|
|
270
579
|
}
|
|
271
580
|
} catch (err) {
|
|
272
|
-
clearInterval(spinner);
|
|
273
581
|
if (!abortController.signal.aborted) {
|
|
274
582
|
console.log(chalk.red(`\n${err.message}\n`));
|
|
275
583
|
}
|
package/src/ui/logo.js
CHANGED
|
@@ -9,19 +9,8 @@ const ASCII_ART = `
|
|
|
9
9
|
╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═══╝
|
|
10
10
|
`;
|
|
11
11
|
|
|
12
|
-
const SMALL_MARKOV = `
|
|
13
|
-
█ █ ██ █ ██ █ █ ██ █ █
|
|
14
|
-
██ ██ █ ██ ██ ███ █ █ ███
|
|
15
|
-
█ █ █ █ █ █ █ ██ ██ █ █
|
|
16
|
-
`;
|
|
17
|
-
|
|
18
12
|
const markovGradient = gradient(['#6366f1', '#a855f7', '#ec4899']);
|
|
19
13
|
|
|
20
14
|
export function printLogo() {
|
|
21
|
-
console.log(markovGradient.multiline(ASCII_ART));
|
|
22
|
-
console.log(' A friendly CLI tool\n');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function printSmallMarkov() {
|
|
26
|
-
console.log(markovGradient.multiline(SMALL_MARKOV));
|
|
15
|
+
console.log(markovGradient.multiline(ASCII_ART + ' LivingCloud\'s AI Agent'));
|
|
27
16
|
}
|