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.
Files changed (3) hide show
  1. package/bin/prior.js +179 -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,46 @@ 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) {
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
- 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.0",
4
4
  "description": "Prior Network AI — command-line interface",
5
5
  "bin": {
6
6
  "prior": "bin/prior.js"