prior-cli 1.3.8 → 1.3.10

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 CHANGED
@@ -7,6 +7,7 @@ const readline = require('readline');
7
7
  const path = require('path');
8
8
  const os = require('os');
9
9
  const fs = require('fs');
10
+ const { execSync } = require('child_process');
10
11
  const { version } = require('../package.json');
11
12
 
12
13
  const api = require('../lib/api');
@@ -607,6 +608,7 @@ async function startChat(opts = {}) {
607
608
  const chatHistory = [];
608
609
  let currentModel = opts.model || null;
609
610
  let _currentAbortController = null;
611
+ let _pendingImageBase64 = null; // set by alt+v clipboard paste
610
612
 
611
613
  // ── Live slash-command suggestions ──────────────────────────
612
614
  let clearSuggestions = () => {};
@@ -626,27 +628,56 @@ async function startChat(opts = {}) {
626
628
  { cmd: '/exit', desc: 'Exit' },
627
629
  ];
628
630
 
629
- let _suggCount = 0;
630
- let _suggTimer = null;
631
+ // Unified sub-row tracker — covers image indicator + slash suggestions
632
+ let _subRowCount = 0;
633
+ let _suggTimer = null;
631
634
 
632
- clearSuggestions = function () {
633
- if (_suggCount === 0) return;
635
+ // Clear all rows rendered below the input line
636
+ function clearAllSubRows() {
637
+ if (!_subRowCount) return;
634
638
  process.stdout.write('\x1b[s');
635
- for (let i = 0; i < _suggCount; i++) process.stdout.write('\x1b[B\r\x1b[2K');
639
+ for (let i = 0; i < _subRowCount; i++) process.stdout.write('\x1b[B\r\x1b[2K');
636
640
  process.stdout.write('\x1b[u');
637
- _suggCount = 0;
638
- };
641
+ _subRowCount = 0;
642
+ }
639
643
 
640
- function renderSuggestions(matches) {
641
- clearSuggestions();
642
- if (!matches.length) return;
643
- _suggCount = matches.length;
644
- process.stdout.write('\x1b[s'); // save cursor at end of typed input
645
- for (const { cmd, desc } of matches) {
646
- // \x1b[B = cursor down 1 (no scroll), \r = col 0, \x1b[2K = clear line
647
- process.stdout.write(`\x1b[B\r\x1b[2K${c.brand(' ' + cmd.padEnd(14))}${c.dim(desc)}`);
644
+ // Alias used by the loop's rl.question callback
645
+ clearSuggestions = clearAllSubRows;
646
+
647
+ // Redraw image indicator + slash suggestions — always called together so
648
+ // they share the same row-count and never stomp on each other.
649
+ function renderSubRows(line) {
650
+ clearAllSubRows();
651
+ process.stdout.write('\x1b[s');
652
+ let rows = 0;
653
+
654
+ // Image indicator — always first, persists across backspace/typing
655
+ if (_pendingImageBase64) {
656
+ process.stdout.write(`\x1b[B\r\x1b[2K ${c.brand('◈')} ${c.dim('[Image] attached · alt+v to replace · type your prompt and press enter')}`);
657
+ rows++;
648
658
  }
649
- process.stdout.write('\x1b[u'); // restore cursor to end of typed input
659
+
660
+ // Slash-command suggestions
661
+ if ((line || '').startsWith('/')) {
662
+ const word = line.split(' ')[0];
663
+ const matches = SLASH_CMDS.filter(({ cmd }) => cmd.startsWith(word));
664
+ for (const { cmd, desc } of matches) {
665
+ process.stdout.write(`\x1b[B\r\x1b[2K${c.brand(' ' + cmd.padEnd(14))}${c.dim(desc)}`);
666
+ rows++;
667
+ }
668
+ }
669
+
670
+ process.stdout.write('\x1b[u');
671
+ _subRowCount = rows;
672
+ }
673
+
674
+ // Show a one-off message below the input (cleared on next keypress)
675
+ function flashSubRow(msg) {
676
+ clearAllSubRows();
677
+ process.stdout.write('\x1b[s');
678
+ process.stdout.write(`\x1b[B\r\x1b[2K ${msg}`);
679
+ process.stdout.write('\x1b[u');
680
+ _subRowCount = 1;
650
681
  }
651
682
 
652
683
  process.stdin.on('keypress', (ch, key) => {
@@ -659,27 +690,32 @@ async function startChat(opts = {}) {
659
690
  return;
660
691
  }
661
692
 
662
- if (key.name === 'return' || key.name === 'enter' || (key.ctrl && key.name === 'c')) {
663
- // Clear suggestions NOW before readline moves the cursor to the next line
664
- if (_suggCount > 0) {
665
- process.stdout.write('\x1b[s');
666
- for (let i = 0; i < _suggCount; i++) process.stdout.write('\x1b[B\r\x1b[2K');
667
- process.stdout.write('\x1b[u');
668
- _suggCount = 0;
693
+ // Alt+V grab image from Windows clipboard
694
+ if (key.meta && key.name === 'v') {
695
+ try {
696
+ const ps = `Add-Type -AssemblyName System.Windows.Forms; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img) { $ms = New-Object System.IO.MemoryStream; $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); [Convert]::ToBase64String($ms.ToArray()) } else { '' }`;
697
+ const b64 = execSync(`powershell -NoProfile -Command "${ps}"`, { timeout: 5000 }).toString().trim();
698
+ if (b64) {
699
+ _pendingImageBase64 = b64;
700
+ renderSubRows(rl.line || ''); // immediate redraw with indicator
701
+ } else {
702
+ flashSubRow(c.muted('✗ No image found in clipboard'));
703
+ }
704
+ } catch {
705
+ flashSubRow(c.muted('✗ Could not read clipboard'));
669
706
  }
670
707
  return;
671
708
  }
672
709
 
710
+ if (key.name === 'return' || key.name === 'enter' || (key.ctrl && key.name === 'c')) {
711
+ clearAllSubRows();
712
+ return;
713
+ }
714
+
715
+ // Redraw sub-rows on every keypress so backspace / typing never wipes them
673
716
  _suggTimer = setTimeout(() => {
674
717
  _suggTimer = null;
675
- const line = rl.line || '';
676
- if (!line.startsWith('/')) {
677
- clearSuggestions();
678
- return;
679
- }
680
- const word = line.split(' ')[0];
681
- const matches = SLASH_CMDS.filter(({ cmd }) => cmd.startsWith(word));
682
- renderSuggestions(matches);
718
+ renderSubRows(rl.line || '');
683
719
  }, 50);
684
720
  });
685
721
  }
@@ -944,6 +980,13 @@ Keep it under 350 words. Write prior.md now.`;
944
980
  console.log('');
945
981
 
946
982
  {
983
+ const imageForThisMsg = _pendingImageBase64;
984
+ _pendingImageBase64 = null;
985
+
986
+ if (imageForThisMsg) {
987
+ console.log(c.brand(' ◈') + c.dim(' image attached'));
988
+ }
989
+
947
990
  let responseText = '';
948
991
  let _progressStarted = false;
949
992
  const _thinkStart = Date.now();
@@ -969,6 +1012,7 @@ Keep it under 350 words. Write prior.md now.`;
969
1012
  model: currentModel,
970
1013
  cwd: process.cwd(),
971
1014
  projectContext,
1015
+ imageBase64: imageForThisMsg,
972
1016
  confirm,
973
1017
  signal: _currentAbortController.signal,
974
1018
  send: ev => {
package/lib/agent.js CHANGED
@@ -10,11 +10,11 @@ const MAX_ITER = 14;
10
10
 
11
11
  // ── Single inference call (server just runs Ollama + returns) ─
12
12
 
13
- async function infer(messages, model, token, { cwd, uncensored, projectContext } = {}, signal) {
13
+ async function infer(messages, model, token, { cwd, uncensored, projectContext, imageBase64 } = {}, signal) {
14
14
  const res = await fetch(`${CLI_BASE}/api/infer`, {
15
15
  method: 'POST',
16
16
  headers: { 'Content-Type': 'application/json' },
17
- body: JSON.stringify({ messages, model, token, cwd, uncensored, projectContext }),
17
+ body: JSON.stringify({ messages, model, token, cwd, uncensored, projectContext, imageBase64 }),
18
18
  timeout: 120000,
19
19
  signal,
20
20
  });
@@ -200,12 +200,13 @@ function stripToolTags(text) {
200
200
 
201
201
  const CONFIRM_TOOLS = new Set(['run_command', 'file_delete', 'file_write']);
202
202
 
203
- async function runAgent({ messages, model, uncensored, cwd, projectContext, send, confirm, signal }) {
203
+ async function runAgent({ messages, model, uncensored, cwd, projectContext, imageBase64, send, confirm, signal }) {
204
204
  const token = getToken();
205
205
  const history = [...messages];
206
206
 
207
207
  let totalPromptTokens = 0;
208
208
  let totalCompletionTokens = 0;
209
+ let pendingImage = imageBase64 || null; // only sent on first iteration
209
210
 
210
211
  for (let iter = 0; iter < MAX_ITER; iter++) {
211
212
 
@@ -218,8 +219,10 @@ async function runAgent({ messages, model, uncensored, cwd, projectContext, send
218
219
  send({ type: 'thinking' });
219
220
 
220
221
  let result;
222
+ const iterImage = pendingImage;
223
+ pendingImage = null; // clear after first use
221
224
  try {
222
- result = await infer(history, model || 'qwen3.5:4b', token, { cwd, uncensored, projectContext }, signal);
225
+ result = await infer(history, model || 'qwen3.5:4b', token, { cwd, uncensored, projectContext, imageBase64: iterImage }, signal);
223
226
  } catch (err) {
224
227
  await trackTokenUsage(token, totalPromptTokens, totalCompletionTokens);
225
228
  if (err.name === 'AbortError' || signal?.aborted) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prior-cli",
3
- "version": "1.3.8",
3
+ "version": "1.3.10",
4
4
  "description": "Prior Network AI — command-line interface",
5
5
  "bin": {
6
6
  "prior": "bin/prior.js"