prior-cli 1.7.11 → 1.7.13
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 +191 -0
- package/package.json +1 -1
package/bin/prior.js
CHANGED
|
@@ -230,6 +230,30 @@ function expandFileRefs(input, cwd) {
|
|
|
230
230
|
return { message: `${input}\n\n[Attached file context]\n${ctx}`, attached };
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
+
// Live completions for an @file token being typed. `partial` is the text
|
|
234
|
+
// after @ (e.g. "lib/to"). Returns up to `limit` matches, alphabetical (files + folders mixed).
|
|
235
|
+
const REF_SKIP_DIRS = new Set(['node_modules', '.git', '.next', 'dist', 'build', '.cache', 'coverage', 'vendor']);
|
|
236
|
+
function fileRefSuggestions(partial, cwd, limit = 10) {
|
|
237
|
+
const slash = Math.max(partial.lastIndexOf('/'), partial.lastIndexOf('\\'));
|
|
238
|
+
const dirPart = slash >= 0 ? partial.slice(0, slash) : '';
|
|
239
|
+
const prefix = (slash >= 0 ? partial.slice(slash + 1) : partial).toLowerCase();
|
|
240
|
+
const baseDir = dirPart
|
|
241
|
+
? (path.isAbsolute(dirPart) ? dirPart : path.resolve(cwd, dirPart))
|
|
242
|
+
: cwd;
|
|
243
|
+
let entries;
|
|
244
|
+
try { entries = fs.readdirSync(baseDir, { withFileTypes: true }); } catch { return []; }
|
|
245
|
+
const out = [];
|
|
246
|
+
for (const e of entries) {
|
|
247
|
+
if (e.name.startsWith('.') && !prefix.startsWith('.')) continue; // hide dotfiles unless asked
|
|
248
|
+
if (e.isDirectory() && REF_SKIP_DIRS.has(e.name)) continue;
|
|
249
|
+
if (!e.name.toLowerCase().startsWith(prefix)) continue;
|
|
250
|
+
const ref = (dirPart ? dirPart.replace(/\\/g, '/') + '/' : '') + e.name;
|
|
251
|
+
out.push({ ref, isDir: e.isDirectory() });
|
|
252
|
+
}
|
|
253
|
+
out.sort((a, b) => a.ref.toLowerCase().localeCompare(b.ref.toLowerCase())); // alphabetical, files + folders mixed
|
|
254
|
+
return out.slice(0, limit);
|
|
255
|
+
}
|
|
256
|
+
|
|
233
257
|
function fmtElapsed(ms) {
|
|
234
258
|
const s = Math.floor(ms / 1000);
|
|
235
259
|
if (s < 60) return `${s}s`;
|
|
@@ -863,6 +887,13 @@ async function startChat(opts = {}) {
|
|
|
863
887
|
terminal: true,
|
|
864
888
|
historySize: 100,
|
|
865
889
|
completer: line => {
|
|
890
|
+
// @file completion — complete the @token at the end of the line
|
|
891
|
+
const at = line.match(/(?:^|\s)@([^\s]*)$/);
|
|
892
|
+
if (at) {
|
|
893
|
+
const matches = fileRefSuggestions(at[1], process.cwd(), 20)
|
|
894
|
+
.map(s => '@' + (s.isDir ? s.ref + '/' : s.ref));
|
|
895
|
+
return [matches, '@' + at[1]];
|
|
896
|
+
}
|
|
866
897
|
const cmds = ['/help', '/clear', '/censored', '/uncensored', '/login', '/logout', '/exit'];
|
|
867
898
|
if (!line.startsWith('/')) return [[], line];
|
|
868
899
|
const hits = cmds.filter(cmd => cmd.startsWith(line));
|
|
@@ -985,6 +1016,158 @@ async function startChat(opts = {}) {
|
|
|
985
1016
|
_subRowCount = 1;
|
|
986
1017
|
}
|
|
987
1018
|
|
|
1019
|
+
// ── Interactive @file picker ─────────────────────────────────
|
|
1020
|
+
// While open, it OWNS the keyboard (↑/↓/Enter/Tab/Esc/typing) so the
|
|
1021
|
+
// arrow keys don't fall through to readline's history. We swap readline's
|
|
1022
|
+
// keypress listener out for ours, then restore it when the picker closes.
|
|
1023
|
+
let _atActive = false;
|
|
1024
|
+
let _atItems = [];
|
|
1025
|
+
let _atSel = 0;
|
|
1026
|
+
let _atToken = '';
|
|
1027
|
+
let _atStart = 0;
|
|
1028
|
+
let _savedKp = null;
|
|
1029
|
+
let _atRows = 0; // rows currently drawn below the input by the picker
|
|
1030
|
+
let _atView = 0; // index of the first visible item (scrolling viewport)
|
|
1031
|
+
const AT_MAX_VISIBLE = 8;
|
|
1032
|
+
|
|
1033
|
+
function currentAtToken() {
|
|
1034
|
+
const line = rl.line || '';
|
|
1035
|
+
const before = line.slice(0, rl.cursor);
|
|
1036
|
+
const m = before.match(/(?:^|\s)@([^\s]*)$/);
|
|
1037
|
+
if (!m) return null;
|
|
1038
|
+
return { token: m[1], start: rl.cursor - m[1].length - 1 };
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function refreshAtItems() {
|
|
1042
|
+
const t = currentAtToken();
|
|
1043
|
+
if (!t) return false;
|
|
1044
|
+
_atToken = t.token;
|
|
1045
|
+
_atStart = t.start;
|
|
1046
|
+
_atItems = fileRefSuggestions(_atToken, process.cwd(), 500); // fetch all; viewport scrolls
|
|
1047
|
+
_atView = 0;
|
|
1048
|
+
if (_atSel >= _atItems.length) _atSel = Math.max(0, _atItems.length - 1);
|
|
1049
|
+
return _atItems.length > 0;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
// Column of the readline cursor on the input line (visible prompt is 4 cols wide)
|
|
1053
|
+
function atInputCol() { return 4 + (rl.cursor || 0); }
|
|
1054
|
+
|
|
1055
|
+
// Return the terminal cursor to the input line, `fromRows` lines above us.
|
|
1056
|
+
// Uses RELATIVE moves so it survives the terminal scrolling when the picker
|
|
1057
|
+
// is drawn near the bottom of the screen (the bug where the hint overlapped @).
|
|
1058
|
+
function atMoveToInput(fromRows) {
|
|
1059
|
+
if (fromRows > 0) process.stdout.write(`\x1b[${fromRows}A`);
|
|
1060
|
+
process.stdout.write('\r');
|
|
1061
|
+
const col = atInputCol();
|
|
1062
|
+
if (col > 0) process.stdout.write(`\x1b[${col}C`);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function clearAtRows() {
|
|
1066
|
+
if (_atRows > 0) {
|
|
1067
|
+
for (let i = 0; i < _atRows; i++) process.stdout.write('\x1b[B\r\x1b[2K');
|
|
1068
|
+
atMoveToInput(_atRows);
|
|
1069
|
+
_atRows = 0;
|
|
1070
|
+
}
|
|
1071
|
+
_subRowCount = 0;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
function renderAtList() {
|
|
1075
|
+
clearAtRows(); // assumes cursor is on the input line
|
|
1076
|
+
const total = _atItems.length;
|
|
1077
|
+
// Keep the selected item inside the visible window
|
|
1078
|
+
if (_atSel < _atView) _atView = _atSel;
|
|
1079
|
+
if (_atSel >= _atView + AT_MAX_VISIBLE) _atView = _atSel - AT_MAX_VISIBLE + 1;
|
|
1080
|
+
const end = Math.min(_atView + AT_MAX_VISIBLE, total);
|
|
1081
|
+
|
|
1082
|
+
const lines = [];
|
|
1083
|
+
for (let i = _atView; i < end; i++) {
|
|
1084
|
+
const it = _atItems[i];
|
|
1085
|
+
const label = it.isDir ? it.ref + '/' : it.ref;
|
|
1086
|
+
const arrow = (i === end - 1 && end < total) ? c.dim(' ↓')
|
|
1087
|
+
: (i === _atView && _atView > 0) ? c.dim(' ↑') : '';
|
|
1088
|
+
lines.push((i === _atSel
|
|
1089
|
+
? c.brand('❯ ') + c.bold(label)
|
|
1090
|
+
: c.muted(' ') + c.white(label)) + arrow);
|
|
1091
|
+
}
|
|
1092
|
+
const pos = total > AT_MAX_VISIBLE ? ` · ${_atSel + 1}/${total}` : '';
|
|
1093
|
+
lines.push(c.dim('↑↓ select · enter/tab insert · esc dismiss' + pos));
|
|
1094
|
+
|
|
1095
|
+
for (const ln of lines) process.stdout.write(`\x1b[B\r\x1b[2K ${ln}`);
|
|
1096
|
+
atMoveToInput(lines.length); // relative — scroll-safe
|
|
1097
|
+
_atRows = lines.length;
|
|
1098
|
+
_subRowCount = lines.length;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function openAtPicker() {
|
|
1102
|
+
_atSel = 0;
|
|
1103
|
+
if (!refreshAtItems()) return;
|
|
1104
|
+
_atActive = true;
|
|
1105
|
+
// Take over keypress handling from readline
|
|
1106
|
+
_savedKp = process.stdin.listeners('keypress').slice();
|
|
1107
|
+
process.stdin.removeAllListeners('keypress');
|
|
1108
|
+
process.stdin.on('keypress', atKeyHandler);
|
|
1109
|
+
renderAtList();
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function closeAtPicker() {
|
|
1113
|
+
if (!_atActive) return;
|
|
1114
|
+
_atActive = false;
|
|
1115
|
+
_atItems = [];
|
|
1116
|
+
_atSel = 0;
|
|
1117
|
+
if (_savedKp) {
|
|
1118
|
+
process.stdin.removeListener('keypress', atKeyHandler);
|
|
1119
|
+
for (const l of _savedKp) process.stdin.on('keypress', l);
|
|
1120
|
+
_savedKp = null;
|
|
1121
|
+
}
|
|
1122
|
+
clearAtRows();
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function acceptAt() {
|
|
1126
|
+
const it = _atItems[_atSel];
|
|
1127
|
+
if (!it) { closeAtPicker(); return; }
|
|
1128
|
+
const insert = it.isDir ? it.ref + '/' : it.ref;
|
|
1129
|
+
const line = rl.line;
|
|
1130
|
+
const before = line.slice(0, _atStart + 1); // up to & incl. '@'
|
|
1131
|
+
const after = line.slice(_atStart + 1 + _atToken.length);
|
|
1132
|
+
const newLine = before + insert + after;
|
|
1133
|
+
rl.line = newLine;
|
|
1134
|
+
rl.cursor = (before + insert).length;
|
|
1135
|
+
rl._refreshLine();
|
|
1136
|
+
if (it.isDir) { // drill into the folder, keep picking
|
|
1137
|
+
_atSel = 0;
|
|
1138
|
+
if (refreshAtItems()) { renderAtList(); return; }
|
|
1139
|
+
}
|
|
1140
|
+
closeAtPicker();
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function atKeyHandler(ch, key) {
|
|
1144
|
+
if (!key) return;
|
|
1145
|
+
if (key.name === 'up') { _atSel = (_atSel - 1 + _atItems.length) % _atItems.length; renderAtList(); return; }
|
|
1146
|
+
if (key.name === 'down') { _atSel = (_atSel + 1) % _atItems.length; renderAtList(); return; }
|
|
1147
|
+
if (key.name === 'escape') { closeAtPicker(); return; }
|
|
1148
|
+
if (key.name === 'tab' || key.name === 'return' || key.name === 'enter') { acceptAt(); return; }
|
|
1149
|
+
if (key.ctrl && key.name === 'c') { closeAtPicker(); process.exit(0); }
|
|
1150
|
+
if (key.name === 'backspace') {
|
|
1151
|
+
if (rl.cursor > 0) {
|
|
1152
|
+
rl.line = rl.line.slice(0, rl.cursor - 1) + rl.line.slice(rl.cursor);
|
|
1153
|
+
rl.cursor = rl.cursor - 1;
|
|
1154
|
+
rl._refreshLine();
|
|
1155
|
+
}
|
|
1156
|
+
if (!refreshAtItems()) { closeAtPicker(); return; }
|
|
1157
|
+
renderAtList();
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
// Printable character — insert it, keep filtering
|
|
1161
|
+
if (ch && !key.ctrl && !key.meta && ch.length === 1 && ch >= ' ') {
|
|
1162
|
+
rl.line = rl.line.slice(0, rl.cursor) + ch + rl.line.slice(rl.cursor);
|
|
1163
|
+
rl.cursor = rl.cursor + 1;
|
|
1164
|
+
rl._refreshLine();
|
|
1165
|
+
if (ch === ' ') { closeAtPicker(); return; } // space ends the @token
|
|
1166
|
+
if (!refreshAtItems()) { closeAtPicker(); return; }
|
|
1167
|
+
renderAtList();
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
988
1171
|
process.stdin.on('keypress', (ch, key) => {
|
|
989
1172
|
if (!key) return;
|
|
990
1173
|
|
|
@@ -1023,6 +1206,14 @@ async function startChat(opts = {}) {
|
|
|
1023
1206
|
// Redraw sub-rows on every keypress so backspace / typing never wipes them
|
|
1024
1207
|
_suggTimer = setTimeout(() => {
|
|
1025
1208
|
_suggTimer = null;
|
|
1209
|
+
// If the user just started an @file token, hand control to the picker
|
|
1210
|
+
if (!_atActive) {
|
|
1211
|
+
const t = currentAtToken();
|
|
1212
|
+
if (t && fileRefSuggestions(t.token, process.cwd(), 1).length) {
|
|
1213
|
+
openAtPicker();
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1026
1217
|
renderSubRows(rl.line || '');
|
|
1027
1218
|
}, 50);
|
|
1028
1219
|
});
|