prior-cli 1.1.3 → 1.2.1

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 (3) hide show
  1. package/bin/prior.js +181 -12
  2. package/lib/agent.js +15 -2
  3. 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 = toolIcon(name);
256
- const label = c.bold(name.padEnd(16));
257
- const preview = Object.values(args || {})[0];
258
- const hint = preview ? c.muted(String(preview).slice(0, 80)) : '';
259
- process.stdout.write(` ${icon} ${label} ${hint}\n`);
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,48 @@ function renderToolDone(name, summary) {
273
389
  } else {
274
390
  display = c.dim(display);
275
391
  }
276
- process.stdout.write(` ${c.ok('✓')} ${label} ${display}${took}\n`);
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 = _toolStartTime ? c.dim(` · ${elapsed(Date.now() - _toolStartTime)}`) : '';
281
- const label = c.muted(name.padEnd(16));
282
- process.stdout.write(` ${c.err('✗')} ${label} ${c.err(error || 'failed')}${took}\n`);
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, rl) {
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
+ // Pause readline so it doesn't swallow the keypress or react to stdin events
415
+ if (rl) rl.pause();
416
+ process.stdin.setRawMode(true);
417
+ process.stdin.setEncoding('utf8');
418
+
419
+ function handler(ch) {
420
+ process.stdin.setRawMode(false);
421
+ process.stdin.removeListener('data', handler);
422
+ // Resume readline — do NOT call process.stdin.pause() or rl.close()
423
+ if (rl) rl.resume();
424
+
425
+ const key = ch.toLowerCase();
426
+ if (key === '\u0003') { process.stdout.write('\n'); process.exit(0); }
427
+ if (key === 'n') { process.stdout.write(c.err('n') + '\n\n'); return resolve(false); }
428
+ process.stdout.write(c.ok('y') + '\n\n');
429
+ resolve(true);
430
+ }
431
+
432
+ process.stdin.on('data', handler);
433
+ });
283
434
  }
284
435
 
285
436
  // ── Browser login via public URL ───────────────────────────────
@@ -773,12 +924,25 @@ Keep it under 350 words. Write prior.md now.`;
773
924
  spinStart('thinking…');
774
925
 
775
926
  try {
927
+ const confirm = async ({ name, args }) => {
928
+ spinStop();
929
+ const PROMPTS = {
930
+ run_command: 'Run this command?',
931
+ file_write: 'Write this file?',
932
+ file_delete: 'Delete this file?',
933
+ };
934
+ const approved = await askConfirmKey(PROMPTS[name] || `Execute ${name}?`, rl);
935
+ if (approved) spinStart('working…');
936
+ return approved;
937
+ };
938
+
776
939
  await runAgent({
777
940
  messages: [...chatHistory, { role: 'user', content: input }],
778
941
  model: currentModel,
779
942
  uncensored,
780
943
  cwd: process.cwd(),
781
944
  projectContext,
945
+ confirm,
782
946
  send: ev => {
783
947
  switch (ev.type) {
784
948
 
@@ -790,7 +954,7 @@ Keep it under 350 words. Write prior.md now.`;
790
954
  spinStop();
791
955
  _progressStarted = false;
792
956
  renderToolStart(ev.name, ev.args);
793
- spinStart('working…');
957
+ if (!CONFIRM_TOOLS.has(ev.name)) spinStart('working…');
794
958
  break;
795
959
 
796
960
  case 'tool_progress': {
@@ -813,6 +977,11 @@ Keep it under 350 words. Write prior.md now.`;
813
977
  renderToolDone(ev.name, ev.summary);
814
978
  break;
815
979
 
980
+ case 'tool_skip':
981
+ spinStop();
982
+ renderToolSkip(ev.name);
983
+ break;
984
+
816
985
  case 'tool_error':
817
986
  spinStop();
818
987
  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
- async function runAgent({ messages, model, uncensored, cwd, projectContext, send }) {
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prior-cli",
3
- "version": "1.1.3",
3
+ "version": "1.2.1",
4
4
  "description": "Prior Network AI — command-line interface",
5
5
  "bin": {
6
6
  "prior": "bin/prior.js"