prior-cli 1.1.3 → 1.2.0
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/bin/prior.js +179 -12
- package/lib/agent.js +15 -2
- package/package.json +1 -1
package/bin/prior.js
CHANGED
|
@@ -12,7 +12,7 @@ const { version } = require('../package.json');
|
|
|
12
12
|
const api = require('../lib/api');
|
|
13
13
|
const { renderMarkdown } = require('../lib/render');
|
|
14
14
|
const { getToken, getUsername, saveAuth, clearAuth } = require('../lib/config');
|
|
15
|
-
const { runAgent } = require('../lib/agent');
|
|
15
|
+
const { runAgent, CONFIRM_TOOLS } = require('../lib/agent');
|
|
16
16
|
|
|
17
17
|
// ── Theme ──────────────────────────────────────────────────────
|
|
18
18
|
const THEME = '#9CE2D4';
|
|
@@ -250,13 +250,130 @@ function toolIcon(name) {
|
|
|
250
250
|
|
|
251
251
|
let _toolStartTime = 0;
|
|
252
252
|
|
|
253
|
+
// ── Box drawing helpers ────────────────────────────────────────
|
|
254
|
+
function boxLine(text, width, color) {
|
|
255
|
+
const col = process.stdout.columns || 90;
|
|
256
|
+
const w = Math.min(width || 56, col - 8);
|
|
257
|
+
const pad = Math.max(0, w - text.length);
|
|
258
|
+
return ' │ ' + (color ? color(text) : text) + ' '.repeat(pad) + ' │';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function drawBox(lines, opts = {}) {
|
|
262
|
+
const col = process.stdout.columns || 90;
|
|
263
|
+
const w = Math.min(opts.width || 56, col - 8);
|
|
264
|
+
const top = ' ╭' + '─'.repeat(w + 4) + '╮';
|
|
265
|
+
const bot = ' ╰' + '─'.repeat(w + 4) + '╯';
|
|
266
|
+
process.stdout.write(c.muted(top) + '\n');
|
|
267
|
+
for (const { text, color, dim } of lines) {
|
|
268
|
+
const str = String(text || '').slice(0, w);
|
|
269
|
+
const pad = ' '.repeat(Math.max(0, w - str.length));
|
|
270
|
+
const styled = color ? color(str) : dim ? c.dim(str) : c.white(str);
|
|
271
|
+
process.stdout.write(c.muted(' │ ') + styled + pad + c.muted(' │') + '\n');
|
|
272
|
+
}
|
|
273
|
+
process.stdout.write(c.muted(bot) + '\n');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ── Per-tool rich preview ─────────────────────────────────────
|
|
253
277
|
function renderToolStart(name, args) {
|
|
254
278
|
_toolStartTime = Date.now();
|
|
255
|
-
const icon
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
279
|
+
const icon = toolIcon(name);
|
|
280
|
+
|
|
281
|
+
switch (name) {
|
|
282
|
+
|
|
283
|
+
case 'run_command': {
|
|
284
|
+
const cmd = args.command || '';
|
|
285
|
+
process.stdout.write(`\n ${icon} ${c.bold('run_command')}\n`);
|
|
286
|
+
drawBox([{ text: cmd, color: c.brand }]);
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
case 'file_write': {
|
|
291
|
+
const filePath = args.path || '';
|
|
292
|
+
const content = args.content || '';
|
|
293
|
+
const lines = content.split('\n');
|
|
294
|
+
const preview = lines.slice(0, 5);
|
|
295
|
+
const more = lines.length > 5 ? lines.length - 5 : 0;
|
|
296
|
+
process.stdout.write(`\n ${icon} ${c.bold('file_write')} ${c.muted('→')} ${c.brand(filePath)} ${c.muted(`${lines.length} line${lines.length !== 1 ? 's' : ''}`)}\n`);
|
|
297
|
+
drawBox([
|
|
298
|
+
...preview.map(l => ({ text: l, dim: true })),
|
|
299
|
+
...(more > 0 ? [{ text: `… ${more} more line${more !== 1 ? 's' : ''}`, dim: true }] : []),
|
|
300
|
+
]);
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
case 'file_append': {
|
|
305
|
+
const filePath = args.path || '';
|
|
306
|
+
const content = args.content || '';
|
|
307
|
+
const lines = content.split('\n');
|
|
308
|
+
process.stdout.write(`\n ${icon} ${c.bold('file_append')} ${c.muted('→')} ${c.brand(filePath)} ${c.muted(`+${lines.length} line${lines.length !== 1 ? 's' : ''}`)}\n`);
|
|
309
|
+
drawBox(lines.slice(0, 4).map(l => ({ text: l, dim: true })));
|
|
310
|
+
break;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
case 'file_delete': {
|
|
314
|
+
const filePath = args.path || '';
|
|
315
|
+
process.stdout.write(`\n ${icon} ${c.bold('file_delete')}\n`);
|
|
316
|
+
drawBox([{ text: filePath, color: chalk.red }]);
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
case 'file_read': {
|
|
321
|
+
const filePath = args.path || '';
|
|
322
|
+
process.stdout.write(`\n ${icon} ${c.bold('file_read')} ${c.muted(filePath)}\n`);
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
case 'file_list': {
|
|
327
|
+
const dirPath = args.path || '.';
|
|
328
|
+
process.stdout.write(`\n ${icon} ${c.bold('file_list')} ${c.muted(dirPath)}\n`);
|
|
329
|
+
break;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
case 'web_search': {
|
|
333
|
+
const query = args.query || '';
|
|
334
|
+
process.stdout.write(`\n ${icon} ${c.bold('web_search')}\n`);
|
|
335
|
+
drawBox([{ text: query, color: c.brand }]);
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
case 'url_fetch': {
|
|
340
|
+
const url = args.url || '';
|
|
341
|
+
process.stdout.write(`\n ${icon} ${c.bold('url_fetch')} ${c.muted(url.slice(0, 70))}\n`);
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
case 'generate_image': {
|
|
346
|
+
const prompt = args.prompt || '';
|
|
347
|
+
process.stdout.write(`\n ${icon} ${c.bold('generate_image')}\n`);
|
|
348
|
+
drawBox([{ text: prompt, color: c.brand }]);
|
|
349
|
+
process.stdout.write(c.muted(' This may take 1–3 minutes…\n'));
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
case 'clipboard_read':
|
|
354
|
+
process.stdout.write(`\n ${icon} ${c.bold('clipboard_read')}\n`);
|
|
355
|
+
break;
|
|
356
|
+
|
|
357
|
+
case 'clipboard_write': {
|
|
358
|
+
const text = String(args.text || '').slice(0, 60);
|
|
359
|
+
process.stdout.write(`\n ${icon} ${c.bold('clipboard_write')} ${c.muted(text)}\n`);
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
case 'prior_feed':
|
|
364
|
+
process.stdout.write(`\n ${icon} ${c.bold('prior_feed')} ${c.muted('fetching…')}\n`);
|
|
365
|
+
break;
|
|
366
|
+
|
|
367
|
+
case 'prior_profile':
|
|
368
|
+
process.stdout.write(`\n ${icon} ${c.bold('prior_profile')} ${c.muted('fetching…')}\n`);
|
|
369
|
+
break;
|
|
370
|
+
|
|
371
|
+
default: {
|
|
372
|
+
const preview = Object.values(args || {})[0];
|
|
373
|
+
const hint = preview ? c.muted(String(preview).slice(0, 80)) : '';
|
|
374
|
+
process.stdout.write(`\n ${icon} ${c.bold(name.padEnd(16))} ${hint}\n`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
260
377
|
}
|
|
261
378
|
|
|
262
379
|
function hyperlink(text, url) {
|
|
@@ -265,7 +382,6 @@ function hyperlink(text, url) {
|
|
|
265
382
|
|
|
266
383
|
function renderToolDone(name, summary) {
|
|
267
384
|
const took = _toolStartTime ? c.dim(` · ${elapsed(Date.now() - _toolStartTime)}`) : '';
|
|
268
|
-
const label = c.muted(name.padEnd(16));
|
|
269
385
|
let display = summary || '';
|
|
270
386
|
if (/^[a-zA-Z]:[/\\]/.test(display) || display.startsWith('/')) {
|
|
271
387
|
const fileUrl = 'file:///' + display.replace(/\\/g, '/');
|
|
@@ -273,13 +389,46 @@ function renderToolDone(name, summary) {
|
|
|
273
389
|
} else {
|
|
274
390
|
display = c.dim(display);
|
|
275
391
|
}
|
|
276
|
-
process.stdout.write(` ${c.ok('✓')} ${
|
|
392
|
+
process.stdout.write(` ${c.ok('✓')} ${c.muted(name)} ${display}${took}\n`);
|
|
277
393
|
}
|
|
278
394
|
|
|
279
395
|
function renderToolError(name, error) {
|
|
280
|
-
const took
|
|
281
|
-
|
|
282
|
-
|
|
396
|
+
const took = _toolStartTime ? c.dim(` · ${elapsed(Date.now() - _toolStartTime)}`) : '';
|
|
397
|
+
process.stdout.write(` ${c.err('✗')} ${c.muted(name)} ${c.err(error || 'failed')}${took}\n`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function renderToolSkip(name) {
|
|
401
|
+
process.stdout.write(` ${c.warn('⊘')} ${c.muted(name)} ${c.dim('skipped')}\n`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ── Single-keypress confirm prompt ────────────────────────────
|
|
405
|
+
function askConfirmKey(promptText) {
|
|
406
|
+
return new Promise(resolve => {
|
|
407
|
+
process.stdout.write(` ${c.muted('┤')} ${promptText} ${c.muted('[Y/n]')} `);
|
|
408
|
+
|
|
409
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
410
|
+
process.stdout.write(c.ok('y') + '\n');
|
|
411
|
+
return resolve(true);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
process.stdin.setRawMode(true);
|
|
415
|
+
process.stdin.resume();
|
|
416
|
+
process.stdin.setEncoding('utf8');
|
|
417
|
+
|
|
418
|
+
function handler(ch) {
|
|
419
|
+
process.stdin.setRawMode(false);
|
|
420
|
+
process.stdin.pause();
|
|
421
|
+
process.stdin.removeListener('data', handler);
|
|
422
|
+
|
|
423
|
+
const key = ch.toLowerCase();
|
|
424
|
+
if (key === '\u0003') { process.stdout.write('\n'); process.exit(0); }
|
|
425
|
+
if (key === 'n') { process.stdout.write(c.err('n') + '\n\n'); return resolve(false); }
|
|
426
|
+
process.stdout.write(c.ok('y') + '\n\n');
|
|
427
|
+
resolve(true);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
process.stdin.on('data', handler);
|
|
431
|
+
});
|
|
283
432
|
}
|
|
284
433
|
|
|
285
434
|
// ── Browser login via public URL ───────────────────────────────
|
|
@@ -773,12 +922,25 @@ Keep it under 350 words. Write prior.md now.`;
|
|
|
773
922
|
spinStart('thinking…');
|
|
774
923
|
|
|
775
924
|
try {
|
|
925
|
+
const confirm = async ({ name, args }) => {
|
|
926
|
+
spinStop();
|
|
927
|
+
const PROMPTS = {
|
|
928
|
+
run_command: 'Run this command?',
|
|
929
|
+
file_write: 'Write this file?',
|
|
930
|
+
file_delete: 'Delete this file?',
|
|
931
|
+
};
|
|
932
|
+
const approved = await askConfirmKey(PROMPTS[name] || `Execute ${name}?`);
|
|
933
|
+
if (approved) spinStart('working…');
|
|
934
|
+
return approved;
|
|
935
|
+
};
|
|
936
|
+
|
|
776
937
|
await runAgent({
|
|
777
938
|
messages: [...chatHistory, { role: 'user', content: input }],
|
|
778
939
|
model: currentModel,
|
|
779
940
|
uncensored,
|
|
780
941
|
cwd: process.cwd(),
|
|
781
942
|
projectContext,
|
|
943
|
+
confirm,
|
|
782
944
|
send: ev => {
|
|
783
945
|
switch (ev.type) {
|
|
784
946
|
|
|
@@ -790,7 +952,7 @@ Keep it under 350 words. Write prior.md now.`;
|
|
|
790
952
|
spinStop();
|
|
791
953
|
_progressStarted = false;
|
|
792
954
|
renderToolStart(ev.name, ev.args);
|
|
793
|
-
spinStart('working…');
|
|
955
|
+
if (!CONFIRM_TOOLS.has(ev.name)) spinStart('working…');
|
|
794
956
|
break;
|
|
795
957
|
|
|
796
958
|
case 'tool_progress': {
|
|
@@ -813,6 +975,11 @@ Keep it under 350 words. Write prior.md now.`;
|
|
|
813
975
|
renderToolDone(ev.name, ev.summary);
|
|
814
976
|
break;
|
|
815
977
|
|
|
978
|
+
case 'tool_skip':
|
|
979
|
+
spinStop();
|
|
980
|
+
renderToolSkip(ev.name);
|
|
981
|
+
break;
|
|
982
|
+
|
|
816
983
|
case 'tool_error':
|
|
817
984
|
spinStop();
|
|
818
985
|
renderToolError(ev.name, ev.error);
|
package/lib/agent.js
CHANGED
|
@@ -123,7 +123,9 @@ function stripThink(text) {
|
|
|
123
123
|
|
|
124
124
|
// ── Main agent loop ───────────────────────────────────────────
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
const CONFIRM_TOOLS = new Set(['run_command', 'file_delete', 'file_write']);
|
|
127
|
+
|
|
128
|
+
async function runAgent({ messages, model, uncensored, cwd, projectContext, send, confirm }) {
|
|
127
129
|
const token = getToken();
|
|
128
130
|
const username = getUsername() || 'user';
|
|
129
131
|
const sysPrompt = buildSystemPrompt(username, cwd, uncensored, projectContext);
|
|
@@ -181,6 +183,17 @@ async function runAgent({ messages, model, uncensored, cwd, projectContext, send
|
|
|
181
183
|
const resultParts = [];
|
|
182
184
|
for (const call of calls) {
|
|
183
185
|
send({ type: 'tool_start', name: call.name, args: call.args });
|
|
186
|
+
|
|
187
|
+
// Confirmation gate for destructive / side-effect tools
|
|
188
|
+
if (confirm && CONFIRM_TOOLS.has(call.name)) {
|
|
189
|
+
const approved = await confirm({ name: call.name, args: call.args });
|
|
190
|
+
if (!approved) {
|
|
191
|
+
send({ type: 'tool_skip', name: call.name });
|
|
192
|
+
resultParts.push(`<tool_result name="${call.name}">\nUser declined — action was not executed.\n</tool_result>`);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
184
197
|
try {
|
|
185
198
|
const toolResult = await executeTool(call.name, call.args, { cwd, token, send });
|
|
186
199
|
send({ type: 'tool_done', name: call.name, summary: toolResult.summary });
|
|
@@ -199,4 +212,4 @@ async function runAgent({ messages, model, uncensored, cwd, projectContext, send
|
|
|
199
212
|
send({ type: 'done' });
|
|
200
213
|
}
|
|
201
214
|
|
|
202
|
-
module.exports = { runAgent };
|
|
215
|
+
module.exports = { runAgent, CONFIRM_TOOLS };
|