prior-cli 1.7.12 → 1.7.14

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 (2) hide show
  1. package/bin/prior.js +278 -19
  2. package/package.json +1 -1
package/bin/prior.js CHANGED
@@ -15,6 +15,36 @@ const { renderMarkdown } = require('../lib/render');
15
15
  const { getToken, getUsername, saveAuth, clearAuth } = require('../lib/config');
16
16
  const { runAgent, CONFIRM_TOOLS } = require('../lib/agent');
17
17
 
18
+ // ── Packaged-executable detection + self-update support ─────────
19
+ // True when running as a bundled single-file binary (pkg) rather than via Node+npm.
20
+ const _execBase = path.basename(process.execPath).toLowerCase();
21
+ const IS_EXE = !!process.pkg || (_execBase !== 'node.exe' && _execBase !== 'node');
22
+ const GH_REPO = 'PriorNetwork/prior-cli';
23
+
24
+ // Best-effort cleanup of a leftover *.old.exe from a previous self-update.
25
+ if (IS_EXE) {
26
+ try {
27
+ const oldExe = process.execPath.replace(/\.exe$/i, '') + '.old.exe';
28
+ if (fs.existsSync(oldExe)) fs.unlinkSync(oldExe);
29
+ } catch { /* still locked from last run — retry next launch */ }
30
+ }
31
+
32
+ // Download the latest release exe and swap it in (Windows lets us rename a
33
+ // running exe, so: current → .old, downloaded → current; .old is deleted next launch).
34
+ async function selfUpdateExe(assetUrl) {
35
+ const _fetch = require('node-fetch');
36
+ const exePath = process.execPath;
37
+ const stem = exePath.replace(/\.exe$/i, '');
38
+ const tmp = stem + '.new.exe';
39
+ const old = stem + '.old.exe';
40
+ const res = await _fetch(assetUrl, { timeout: 180000, redirect: 'follow', headers: { 'User-Agent': 'prior-cli' } });
41
+ if (!res.ok) throw new Error(`download HTTP ${res.status}`);
42
+ fs.writeFileSync(tmp, Buffer.from(await res.arrayBuffer()));
43
+ try { if (fs.existsSync(old)) fs.unlinkSync(old); } catch {}
44
+ fs.renameSync(exePath, old); // rename the running exe out of the way
45
+ fs.renameSync(tmp, exePath); // move the new build into place
46
+ }
47
+
18
48
  function decodeToken(token) {
19
49
  try {
20
50
  const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
@@ -231,9 +261,9 @@ function expandFileRefs(input, cwd) {
231
261
  }
232
262
 
233
263
  // Live completions for an @file token being typed. `partial` is the text
234
- // after @ (e.g. "lib/to"). Returns up to `limit` matches, dirs first.
264
+ // after @ (e.g. "lib/to"). Returns up to `limit` matches, alphabetical (files + folders mixed).
235
265
  const REF_SKIP_DIRS = new Set(['node_modules', '.git', '.next', 'dist', 'build', '.cache', 'coverage', 'vendor']);
236
- function fileRefSuggestions(partial, cwd, limit = 6) {
266
+ function fileRefSuggestions(partial, cwd, limit = 10) {
237
267
  const slash = Math.max(partial.lastIndexOf('/'), partial.lastIndexOf('\\'));
238
268
  const dirPart = slash >= 0 ? partial.slice(0, slash) : '';
239
269
  const prefix = (slash >= 0 ? partial.slice(slash + 1) : partial).toLowerCase();
@@ -250,7 +280,7 @@ function fileRefSuggestions(partial, cwd, limit = 6) {
250
280
  const ref = (dirPart ? dirPart.replace(/\\/g, '/') + '/' : '') + e.name;
251
281
  out.push({ ref, isDir: e.isDirectory() });
252
282
  }
253
- out.sort((a, b) => (a.isDir === b.isDir ? a.ref.localeCompare(b.ref) : a.isDir ? -1 : 1));
283
+ out.sort((a, b) => a.ref.toLowerCase().localeCompare(b.ref.toLowerCase())); // alphabetical, files + folders mixed
254
284
  return out.slice(0, limit);
255
285
  }
256
286
 
@@ -1003,22 +1033,6 @@ async function startChat(opts = {}) {
1003
1033
  }
1004
1034
  }
1005
1035
 
1006
- // @file completions — when the token at the end of the line starts with @
1007
- const atMatch = (line || '').match(/(?:^|\s)@([^\s]*)$/);
1008
- if (atMatch) {
1009
- const sugg = fileRefSuggestions(atMatch[1], process.cwd());
1010
- for (const s of sugg) {
1011
- const label = s.isDir ? s.ref + '/' : s.ref;
1012
- const icon = s.isDir ? '📁' : '📄';
1013
- process.stdout.write(`\x1b[B\r\x1b[2K ${c.brand('@')}${c.white(label.padEnd(30))}${c.dim(icon)}`);
1014
- rows++;
1015
- }
1016
- if (sugg.length) {
1017
- process.stdout.write(`\x1b[B\r\x1b[2K ${c.dim('tab to complete · attaches file contents as context')}`);
1018
- rows++;
1019
- }
1020
- }
1021
-
1022
1036
  process.stdout.write('\x1b[u');
1023
1037
  _subRowCount = rows;
1024
1038
  }
@@ -1032,6 +1046,158 @@ async function startChat(opts = {}) {
1032
1046
  _subRowCount = 1;
1033
1047
  }
1034
1048
 
1049
+ // ── Interactive @file picker ─────────────────────────────────
1050
+ // While open, it OWNS the keyboard (↑/↓/Enter/Tab/Esc/typing) so the
1051
+ // arrow keys don't fall through to readline's history. We swap readline's
1052
+ // keypress listener out for ours, then restore it when the picker closes.
1053
+ let _atActive = false;
1054
+ let _atItems = [];
1055
+ let _atSel = 0;
1056
+ let _atToken = '';
1057
+ let _atStart = 0;
1058
+ let _savedKp = null;
1059
+ let _atRows = 0; // rows currently drawn below the input by the picker
1060
+ let _atView = 0; // index of the first visible item (scrolling viewport)
1061
+ const AT_MAX_VISIBLE = 8;
1062
+
1063
+ function currentAtToken() {
1064
+ const line = rl.line || '';
1065
+ const before = line.slice(0, rl.cursor);
1066
+ const m = before.match(/(?:^|\s)@([^\s]*)$/);
1067
+ if (!m) return null;
1068
+ return { token: m[1], start: rl.cursor - m[1].length - 1 };
1069
+ }
1070
+
1071
+ function refreshAtItems() {
1072
+ const t = currentAtToken();
1073
+ if (!t) return false;
1074
+ _atToken = t.token;
1075
+ _atStart = t.start;
1076
+ _atItems = fileRefSuggestions(_atToken, process.cwd(), 500); // fetch all; viewport scrolls
1077
+ _atView = 0;
1078
+ if (_atSel >= _atItems.length) _atSel = Math.max(0, _atItems.length - 1);
1079
+ return _atItems.length > 0;
1080
+ }
1081
+
1082
+ // Column of the readline cursor on the input line (visible prompt is 4 cols wide)
1083
+ function atInputCol() { return 4 + (rl.cursor || 0); }
1084
+
1085
+ // Return the terminal cursor to the input line, `fromRows` lines above us.
1086
+ // Uses RELATIVE moves so it survives the terminal scrolling when the picker
1087
+ // is drawn near the bottom of the screen (the bug where the hint overlapped @).
1088
+ function atMoveToInput(fromRows) {
1089
+ if (fromRows > 0) process.stdout.write(`\x1b[${fromRows}A`);
1090
+ process.stdout.write('\r');
1091
+ const col = atInputCol();
1092
+ if (col > 0) process.stdout.write(`\x1b[${col}C`);
1093
+ }
1094
+
1095
+ function clearAtRows() {
1096
+ if (_atRows > 0) {
1097
+ for (let i = 0; i < _atRows; i++) process.stdout.write('\x1b[B\r\x1b[2K');
1098
+ atMoveToInput(_atRows);
1099
+ _atRows = 0;
1100
+ }
1101
+ _subRowCount = 0;
1102
+ }
1103
+
1104
+ function renderAtList() {
1105
+ clearAtRows(); // assumes cursor is on the input line
1106
+ const total = _atItems.length;
1107
+ // Keep the selected item inside the visible window
1108
+ if (_atSel < _atView) _atView = _atSel;
1109
+ if (_atSel >= _atView + AT_MAX_VISIBLE) _atView = _atSel - AT_MAX_VISIBLE + 1;
1110
+ const end = Math.min(_atView + AT_MAX_VISIBLE, total);
1111
+
1112
+ const lines = [];
1113
+ for (let i = _atView; i < end; i++) {
1114
+ const it = _atItems[i];
1115
+ const label = it.isDir ? it.ref + '/' : it.ref;
1116
+ const arrow = (i === end - 1 && end < total) ? c.dim(' ↓')
1117
+ : (i === _atView && _atView > 0) ? c.dim(' ↑') : '';
1118
+ lines.push((i === _atSel
1119
+ ? c.brand('❯ ') + c.bold(label)
1120
+ : c.muted(' ') + c.white(label)) + arrow);
1121
+ }
1122
+ const pos = total > AT_MAX_VISIBLE ? ` · ${_atSel + 1}/${total}` : '';
1123
+ lines.push(c.dim('↑↓ select · enter/tab insert · esc dismiss' + pos));
1124
+
1125
+ for (const ln of lines) process.stdout.write(`\x1b[B\r\x1b[2K ${ln}`);
1126
+ atMoveToInput(lines.length); // relative — scroll-safe
1127
+ _atRows = lines.length;
1128
+ _subRowCount = lines.length;
1129
+ }
1130
+
1131
+ function openAtPicker() {
1132
+ _atSel = 0;
1133
+ if (!refreshAtItems()) return;
1134
+ _atActive = true;
1135
+ // Take over keypress handling from readline
1136
+ _savedKp = process.stdin.listeners('keypress').slice();
1137
+ process.stdin.removeAllListeners('keypress');
1138
+ process.stdin.on('keypress', atKeyHandler);
1139
+ renderAtList();
1140
+ }
1141
+
1142
+ function closeAtPicker() {
1143
+ if (!_atActive) return;
1144
+ _atActive = false;
1145
+ _atItems = [];
1146
+ _atSel = 0;
1147
+ if (_savedKp) {
1148
+ process.stdin.removeListener('keypress', atKeyHandler);
1149
+ for (const l of _savedKp) process.stdin.on('keypress', l);
1150
+ _savedKp = null;
1151
+ }
1152
+ clearAtRows();
1153
+ }
1154
+
1155
+ function acceptAt() {
1156
+ const it = _atItems[_atSel];
1157
+ if (!it) { closeAtPicker(); return; }
1158
+ const insert = it.isDir ? it.ref + '/' : it.ref;
1159
+ const line = rl.line;
1160
+ const before = line.slice(0, _atStart + 1); // up to & incl. '@'
1161
+ const after = line.slice(_atStart + 1 + _atToken.length);
1162
+ const newLine = before + insert + after;
1163
+ rl.line = newLine;
1164
+ rl.cursor = (before + insert).length;
1165
+ rl._refreshLine();
1166
+ if (it.isDir) { // drill into the folder, keep picking
1167
+ _atSel = 0;
1168
+ if (refreshAtItems()) { renderAtList(); return; }
1169
+ }
1170
+ closeAtPicker();
1171
+ }
1172
+
1173
+ function atKeyHandler(ch, key) {
1174
+ if (!key) return;
1175
+ if (key.name === 'up') { _atSel = (_atSel - 1 + _atItems.length) % _atItems.length; renderAtList(); return; }
1176
+ if (key.name === 'down') { _atSel = (_atSel + 1) % _atItems.length; renderAtList(); return; }
1177
+ if (key.name === 'escape') { closeAtPicker(); return; }
1178
+ if (key.name === 'tab' || key.name === 'return' || key.name === 'enter') { acceptAt(); return; }
1179
+ if (key.ctrl && key.name === 'c') { closeAtPicker(); process.exit(0); }
1180
+ if (key.name === 'backspace') {
1181
+ if (rl.cursor > 0) {
1182
+ rl.line = rl.line.slice(0, rl.cursor - 1) + rl.line.slice(rl.cursor);
1183
+ rl.cursor = rl.cursor - 1;
1184
+ rl._refreshLine();
1185
+ }
1186
+ if (!refreshAtItems()) { closeAtPicker(); return; }
1187
+ renderAtList();
1188
+ return;
1189
+ }
1190
+ // Printable character — insert it, keep filtering
1191
+ if (ch && !key.ctrl && !key.meta && ch.length === 1 && ch >= ' ') {
1192
+ rl.line = rl.line.slice(0, rl.cursor) + ch + rl.line.slice(rl.cursor);
1193
+ rl.cursor = rl.cursor + 1;
1194
+ rl._refreshLine();
1195
+ if (ch === ' ') { closeAtPicker(); return; } // space ends the @token
1196
+ if (!refreshAtItems()) { closeAtPicker(); return; }
1197
+ renderAtList();
1198
+ }
1199
+ }
1200
+
1035
1201
  process.stdin.on('keypress', (ch, key) => {
1036
1202
  if (!key) return;
1037
1203
 
@@ -1070,6 +1236,14 @@ async function startChat(opts = {}) {
1070
1236
  // Redraw sub-rows on every keypress so backspace / typing never wipes them
1071
1237
  _suggTimer = setTimeout(() => {
1072
1238
  _suggTimer = null;
1239
+ // If the user just started an @file token, hand control to the picker
1240
+ if (!_atActive) {
1241
+ const t = currentAtToken();
1242
+ if (t && fileRefSuggestions(t.token, process.cwd(), 1).length) {
1243
+ openAtPicker();
1244
+ return;
1245
+ }
1246
+ }
1073
1247
  renderSubRows(rl.line || '');
1074
1248
  }, 50);
1075
1249
  });
@@ -1505,6 +1679,49 @@ Keep it under 350 words. Write prior.md now.`;
1505
1679
  console.log('');
1506
1680
  process.stdout.write(c.dim(' Checking for updates…'));
1507
1681
  const _fetch = require('node-fetch');
1682
+
1683
+ // ── Standalone exe → self-update from GitHub Releases ──
1684
+ if (IS_EXE) {
1685
+ let _rel;
1686
+ try {
1687
+ const _r = await _fetch(`https://api.github.com/repos/${GH_REPO}/releases/latest`,
1688
+ { timeout: 8000, headers: { 'User-Agent': 'prior-cli', Accept: 'application/vnd.github+json' } });
1689
+ if (!_r.ok) throw new Error(`HTTP ${_r.status}`);
1690
+ _rel = await _r.json();
1691
+ } catch (err) {
1692
+ process.stdout.clearLine(0); process.stdout.cursorTo(0);
1693
+ console.log(c.err(` ✗ Could not reach GitHub Releases: ${err.message}\n`));
1694
+ return loop();
1695
+ }
1696
+ const _ghLatest = (_rel.tag_name || '').replace(/^v/, '');
1697
+ process.stdout.clearLine(0); process.stdout.cursorTo(0);
1698
+ if (!_ghLatest || _ghLatest === version) {
1699
+ console.log(c.ok(' ✓ Already up to date ') + c.muted(`v${version}\n`));
1700
+ return loop();
1701
+ }
1702
+ const _asset = (_rel.assets || []).find(a => /prior.*\.exe$/i.test(a.name));
1703
+ if (!_asset) {
1704
+ console.log(c.err(' ✗ Latest release has no .exe asset'));
1705
+ console.log(c.muted(` Get it manually: https://github.com/${GH_REPO}/releases/latest\n`));
1706
+ return loop();
1707
+ }
1708
+ console.log(` ${c.muted('Current :')} ${c.white(`v${version}`)}`);
1709
+ console.log(` ${c.muted('Latest :')} ${c.bold(`v${_ghLatest}`)}`);
1710
+ console.log('');
1711
+ process.stdout.write(c.dim(' Downloading update…'));
1712
+ try {
1713
+ await selfUpdateExe(_asset.browser_download_url);
1714
+ process.stdout.clearLine(0); process.stdout.cursorTo(0);
1715
+ console.log(c.ok(` ✓ Updated to v${_ghLatest} `) + c.muted('restart prior to apply\n'));
1716
+ } catch (err) {
1717
+ process.stdout.clearLine(0); process.stdout.cursorTo(0);
1718
+ console.log(c.err(` ✗ Update failed: ${err.message}`));
1719
+ console.log(c.muted(` Download manually: https://github.com/${GH_REPO}/releases/latest\n`));
1720
+ }
1721
+ return loop();
1722
+ }
1723
+
1724
+ // ── npm install → update via npm ──────────────────────
1508
1725
  let _latest;
1509
1726
  try {
1510
1727
  const _res = await _fetch('https://registry.npmjs.org/prior-cli/latest', { timeout: 8000 });
@@ -2141,6 +2358,48 @@ program
2141
2358
  process.stdout.write(c.dim(' Checking for updates…'));
2142
2359
 
2143
2360
  const fetch = require('node-fetch');
2361
+
2362
+ // Standalone exe → self-update from GitHub Releases
2363
+ if (IS_EXE) {
2364
+ let rel;
2365
+ try {
2366
+ const r = await fetch(`https://api.github.com/repos/${GH_REPO}/releases/latest`,
2367
+ { timeout: 8000, headers: { 'User-Agent': 'prior-cli', Accept: 'application/vnd.github+json' } });
2368
+ if (!r.ok) throw new Error(`HTTP ${r.status}`);
2369
+ rel = await r.json();
2370
+ } catch (err) {
2371
+ clearLine();
2372
+ console.error(c.err(` ✗ Could not reach GitHub Releases: ${err.message}\n`));
2373
+ return;
2374
+ }
2375
+ const ghLatest = (rel.tag_name || '').replace(/^v/, '');
2376
+ clearLine();
2377
+ if (!ghLatest || ghLatest === version) {
2378
+ console.log(c.ok(' ✓ Already up to date ') + c.muted(`v${version}\n`));
2379
+ return;
2380
+ }
2381
+ const asset = (rel.assets || []).find(a => /prior.*\.exe$/i.test(a.name));
2382
+ if (!asset) {
2383
+ console.error(c.err(' ✗ Latest release has no .exe asset'));
2384
+ console.error(c.muted(` Get it manually: https://github.com/${GH_REPO}/releases/latest\n`));
2385
+ return;
2386
+ }
2387
+ console.log(` ${c.muted('Current :')} ${c.white(`v${version}`)}`);
2388
+ console.log(` ${c.muted('Latest :')} ${c.bold(`v${ghLatest}`)}`);
2389
+ console.log('');
2390
+ process.stdout.write(c.dim(' Downloading update…'));
2391
+ try {
2392
+ await selfUpdateExe(asset.browser_download_url);
2393
+ clearLine();
2394
+ console.log(c.ok(` ✓ Updated to v${ghLatest} `) + c.muted('restart prior to apply\n'));
2395
+ } catch (err) {
2396
+ clearLine();
2397
+ console.error(c.err(` ✗ Update failed: ${err.message}`));
2398
+ console.error(c.muted(` Download manually: https://github.com/${GH_REPO}/releases/latest\n`));
2399
+ }
2400
+ return;
2401
+ }
2402
+
2144
2403
  let latest;
2145
2404
  try {
2146
2405
  const res = await fetch('https://registry.npmjs.org/prior-cli/latest', { timeout: 8000 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prior-cli",
3
- "version": "1.7.12",
3
+ "version": "1.7.14",
4
4
  "description": "Prior Network AI — command-line interface",
5
5
  "author": "Prior Network",
6
6
  "homepage": "https://priornetwork.com",