prior-cli 1.6.6 → 1.7.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.
- package/bin/prior.js +205 -1
- package/package.json +1 -1
package/bin/prior.js
CHANGED
|
@@ -38,6 +38,91 @@ const c = {
|
|
|
38
38
|
|
|
39
39
|
const DIVIDER = c.muted(' ' + '─'.repeat(50));
|
|
40
40
|
|
|
41
|
+
// ── Session saves ──────────────────────────────────────────────
|
|
42
|
+
function savesDir(username) {
|
|
43
|
+
return path.join(os.homedir(), '.prior', 'saves', username || 'default');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function sanitizeName(name) {
|
|
47
|
+
return name.trim().toLowerCase().replace(/[^a-z0-9 _-]/g, '').replace(/\s+/g, '_').slice(0, 60);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function listSaves(username) {
|
|
51
|
+
const dir = savesDir(username);
|
|
52
|
+
if (!fs.existsSync(dir)) return [];
|
|
53
|
+
return fs.readdirSync(dir)
|
|
54
|
+
.filter(f => f.endsWith('.json'))
|
|
55
|
+
.map(f => {
|
|
56
|
+
try {
|
|
57
|
+
const data = JSON.parse(fs.readFileSync(path.join(dir, f), 'utf8'));
|
|
58
|
+
return { file: f, name: data.name, savedAt: data.savedAt, msgCount: (data.messages || []).length, model: data.model };
|
|
59
|
+
} catch { return null; }
|
|
60
|
+
})
|
|
61
|
+
.filter(Boolean)
|
|
62
|
+
.sort((a, b) => new Date(b.savedAt) - new Date(a.savedAt));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function writeSession(username, name, messages, model) {
|
|
66
|
+
const dir = savesDir(username);
|
|
67
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
68
|
+
const filename = sanitizeName(name) + '.json';
|
|
69
|
+
const data = { name, savedAt: new Date().toISOString(), model, messages };
|
|
70
|
+
fs.writeFileSync(path.join(dir, filename), JSON.stringify(data, null, 2), 'utf8');
|
|
71
|
+
return filename;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function readSession(username, name) {
|
|
75
|
+
const dir = savesDir(username);
|
|
76
|
+
const file = sanitizeName(name) + '.json';
|
|
77
|
+
const full = path.join(dir, file);
|
|
78
|
+
if (!fs.existsSync(full)) return null;
|
|
79
|
+
return JSON.parse(fs.readFileSync(full, 'utf8'));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Arrow-key picker for /load
|
|
83
|
+
async function showPicker(items, rl) {
|
|
84
|
+
if (!process.stdout.isTTY) return null;
|
|
85
|
+
return new Promise(resolve => {
|
|
86
|
+
let sel = 0;
|
|
87
|
+
const H = items.length;
|
|
88
|
+
|
|
89
|
+
const render = () => {
|
|
90
|
+
items.forEach((item, i) => {
|
|
91
|
+
process.stdout.clearLine(0);
|
|
92
|
+
process.stdout.cursorTo(0);
|
|
93
|
+
const prefix = i === sel ? c.brand(' ❯ ') : c.muted(' ');
|
|
94
|
+
const name = i === sel ? c.bold(item.name) : c.white(item.name);
|
|
95
|
+
const meta = c.muted(` · ${item.msgCount} msgs · ${new Date(item.savedAt).toLocaleDateString()}`);
|
|
96
|
+
process.stdout.write(prefix + name + meta + '\n');
|
|
97
|
+
});
|
|
98
|
+
process.stdout.write(c.muted(' ↑ ↓ navigate · Enter select · Esc cancel') + '\n');
|
|
99
|
+
// move cursor back up to top of list
|
|
100
|
+
process.stdout.moveCursor(0, -(H + 1));
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const cleanup = () => {
|
|
104
|
+
// move cursor past the list
|
|
105
|
+
process.stdout.moveCursor(0, H + 1);
|
|
106
|
+
process.stdout.clearLine(0);
|
|
107
|
+
process.stdin.removeListener('keypress', onKey);
|
|
108
|
+
rl.resume();
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const onKey = (str, key) => {
|
|
112
|
+
if (!key) return;
|
|
113
|
+
if (key.name === 'up') { sel = (sel - 1 + H) % H; render(); }
|
|
114
|
+
else if (key.name === 'down') { sel = (sel + 1) % H; render(); }
|
|
115
|
+
else if (key.name === 'return') { cleanup(); resolve(items[sel]); }
|
|
116
|
+
else if (key.name === 'escape' || key.ctrl) { cleanup(); resolve(null); }
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
rl.pause();
|
|
120
|
+
process.stdin.on('keypress', onKey);
|
|
121
|
+
console.log('');
|
|
122
|
+
render();
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
41
126
|
// ── Spinner ────────────────────────────────────────────────────
|
|
42
127
|
const SPIN_FRAMES = ['◐', '◓', '◑', '◒'];
|
|
43
128
|
let _spinTimer = null;
|
|
@@ -759,7 +844,7 @@ async function startChat(opts = {}) {
|
|
|
759
844
|
console.log(c.ok(' ◉') + c.muted(' Agent mode ') + c.dim('· file web shell image prior-network'));
|
|
760
845
|
|
|
761
846
|
console.log(DIVIDER);
|
|
762
|
-
console.log(c.muted(' /help /clear /compact /timer /
|
|
847
|
+
console.log(c.muted(' /help /clear /compact /timer /save /load /saves /exit'));
|
|
763
848
|
console.log(DIVIDER);
|
|
764
849
|
console.log('');
|
|
765
850
|
|
|
@@ -778,6 +863,9 @@ async function startChat(opts = {}) {
|
|
|
778
863
|
const SLASH_CMDS = [
|
|
779
864
|
{ cmd: '/compact', desc: 'Compact conversation to save context' },
|
|
780
865
|
{ cmd: '/timer', desc: 'Set a countdown timer e.g. /timer 30s' },
|
|
866
|
+
{ cmd: '/saves', desc: 'List all saved conversations' },
|
|
867
|
+
{ cmd: '/save', desc: 'Save current conversation e.g. /save my session' },
|
|
868
|
+
{ cmd: '/load', desc: 'Load a saved conversation' },
|
|
781
869
|
{ cmd: '/help', desc: 'Show help' },
|
|
782
870
|
{ cmd: '/clear', desc: 'Clear screen' },
|
|
783
871
|
{ cmd: '/censored', desc: 'Load standard model (qwen)' },
|
|
@@ -959,6 +1047,119 @@ async function startChat(opts = {}) {
|
|
|
959
1047
|
console.log(c.ok(' ✓ Logged out.\n'));
|
|
960
1048
|
return loop();
|
|
961
1049
|
|
|
1050
|
+
case '/saves': {
|
|
1051
|
+
const saves = listSaves(user);
|
|
1052
|
+
if (saves.length === 0) {
|
|
1053
|
+
console.log(c.muted('\n No saved conversations yet. Use /save <name> to save one.\n'));
|
|
1054
|
+
} else {
|
|
1055
|
+
console.log('');
|
|
1056
|
+
console.log(c.bold(` Saved conversations`) + c.muted(` (${saves.length})`));
|
|
1057
|
+
console.log(DIVIDER);
|
|
1058
|
+
saves.forEach((s, i) => {
|
|
1059
|
+
const num = c.muted(` ${String(i + 1).padStart(2)}. `);
|
|
1060
|
+
const name = c.white(s.name.padEnd(30));
|
|
1061
|
+
const meta = c.muted(`${s.msgCount} msgs · ${new Date(s.savedAt).toLocaleDateString('en-PH', { month: 'short', day: 'numeric', year: 'numeric' })}`);
|
|
1062
|
+
console.log(num + name + meta);
|
|
1063
|
+
});
|
|
1064
|
+
console.log(DIVIDER);
|
|
1065
|
+
console.log(c.muted(' Use /load <name> or /load <number> to restore\n'));
|
|
1066
|
+
}
|
|
1067
|
+
return loop();
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
case '/save': {
|
|
1071
|
+
const saveName = args.join(' ').trim();
|
|
1072
|
+
if (!saveName) {
|
|
1073
|
+
console.log(c.err(' Usage: /save <name> e.g. /save debugging session\n'));
|
|
1074
|
+
return loop();
|
|
1075
|
+
}
|
|
1076
|
+
if (chatHistory.length === 0) {
|
|
1077
|
+
console.log(c.muted(' Nothing to save yet — start a conversation first.\n'));
|
|
1078
|
+
return loop();
|
|
1079
|
+
}
|
|
1080
|
+
try {
|
|
1081
|
+
writeSession(user, saveName, chatHistory, currentModel);
|
|
1082
|
+
const savedPath = path.join(savesDir(user), sanitizeName(saveName) + '.json');
|
|
1083
|
+
console.log(c.ok(` ✓ Saved "${saveName}"`) + c.muted(` · ${chatHistory.length} messages`));
|
|
1084
|
+
console.log(c.muted(` ${savedPath}\n`));
|
|
1085
|
+
} catch (err) {
|
|
1086
|
+
console.log(c.err(` Failed to save: ${err.message}\n`));
|
|
1087
|
+
}
|
|
1088
|
+
return loop();
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
case '/load': {
|
|
1092
|
+
const saves = listSaves(user);
|
|
1093
|
+
if (saves.length === 0) {
|
|
1094
|
+
console.log(c.muted('\n No saved conversations yet. Use /save <name> to create one.\n'));
|
|
1095
|
+
return loop();
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
let chosen = null;
|
|
1099
|
+
const query = args.join(' ').trim();
|
|
1100
|
+
|
|
1101
|
+
if (query) {
|
|
1102
|
+
// Match by number or name
|
|
1103
|
+
const num = parseInt(query, 10);
|
|
1104
|
+
if (!isNaN(num) && num >= 1 && num <= saves.length) {
|
|
1105
|
+
chosen = saves[num - 1];
|
|
1106
|
+
} else {
|
|
1107
|
+
const q = query.toLowerCase();
|
|
1108
|
+
chosen = saves.find(s => sanitizeName(s.name) === sanitizeName(query))
|
|
1109
|
+
|| saves.find(s => s.name.toLowerCase().includes(q));
|
|
1110
|
+
}
|
|
1111
|
+
if (!chosen) {
|
|
1112
|
+
console.log(c.err(` No save found matching "${query}"\n`));
|
|
1113
|
+
return loop();
|
|
1114
|
+
}
|
|
1115
|
+
} else {
|
|
1116
|
+
// Numbered list prompt — reliable across all terminals
|
|
1117
|
+
console.log('');
|
|
1118
|
+
console.log(c.bold(' Load a conversation:'));
|
|
1119
|
+
console.log(DIVIDER);
|
|
1120
|
+
saves.forEach((s, i) => {
|
|
1121
|
+
const num = c.brand(` ${String(i + 1).padStart(2)}. `);
|
|
1122
|
+
const name = c.white(s.name.padEnd(28));
|
|
1123
|
+
const meta = c.muted(`${s.msgCount} msgs · ${new Date(s.savedAt).toLocaleDateString('en-PH', { month: 'short', day: 'numeric', year: 'numeric' })}`);
|
|
1124
|
+
console.log(num + name + meta);
|
|
1125
|
+
});
|
|
1126
|
+
console.log(DIVIDER);
|
|
1127
|
+
|
|
1128
|
+
const answer = await new Promise(res => rl.question(c.muted(' Enter number or name (Esc to cancel): '), res));
|
|
1129
|
+
const trimmed = (answer || '').trim();
|
|
1130
|
+
if (!trimmed) {
|
|
1131
|
+
console.log(c.muted(' Cancelled.\n'));
|
|
1132
|
+
return loop();
|
|
1133
|
+
}
|
|
1134
|
+
const n = parseInt(trimmed, 10);
|
|
1135
|
+
if (!isNaN(n) && n >= 1 && n <= saves.length) {
|
|
1136
|
+
chosen = saves[n - 1];
|
|
1137
|
+
} else {
|
|
1138
|
+
const q = trimmed.toLowerCase();
|
|
1139
|
+
chosen = saves.find(s => sanitizeName(s.name) === sanitizeName(trimmed))
|
|
1140
|
+
|| saves.find(s => s.name.toLowerCase().includes(q));
|
|
1141
|
+
}
|
|
1142
|
+
if (!chosen) {
|
|
1143
|
+
console.log(c.err(` No save found matching "${trimmed}"\n`));
|
|
1144
|
+
return loop();
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// Load it
|
|
1149
|
+
const session = readSession(user, chosen.name);
|
|
1150
|
+
if (!session) {
|
|
1151
|
+
console.log(c.err(` Could not read save file for "${chosen.name}"\n`));
|
|
1152
|
+
return loop();
|
|
1153
|
+
}
|
|
1154
|
+
chatHistory.length = 0;
|
|
1155
|
+
for (const msg of session.messages) chatHistory.push(msg);
|
|
1156
|
+
if (session.model) currentModel = session.model;
|
|
1157
|
+
console.log('');
|
|
1158
|
+
console.log(c.ok(` ✓ Loaded "${chosen.name}"`) + c.muted(` · ${chatHistory.length} messages · model: ${currentModel}`));
|
|
1159
|
+
console.log(c.muted(` Saved ${new Date(chosen.savedAt).toLocaleString('en-PH', { dateStyle: 'medium', timeStyle: 'short' })}\n`));
|
|
1160
|
+
return loop();
|
|
1161
|
+
}
|
|
1162
|
+
|
|
962
1163
|
case '/clear':
|
|
963
1164
|
console.clear();
|
|
964
1165
|
banner();
|
|
@@ -1276,6 +1477,9 @@ Be concise but thorough — this summary replaces the full history to save conte
|
|
|
1276
1477
|
console.log(c.bold(' Commands'));
|
|
1277
1478
|
console.log(c.muted(' /compact ') + 'Compact conversation to save context');
|
|
1278
1479
|
console.log(c.muted(' /timer <duration> ') + 'Countdown timer e.g. /timer 30s, /timer 5m');
|
|
1480
|
+
console.log(c.muted(' /saves ') + 'List all saved conversations');
|
|
1481
|
+
console.log(c.muted(' /save <name> ') + 'Save current conversation');
|
|
1482
|
+
console.log(c.muted(' /load [name|number] ') + 'Load a saved conversation (picker if no arg)');
|
|
1279
1483
|
console.log(c.muted(' /clear ') + 'Clear screen');
|
|
1280
1484
|
console.log(c.muted(' /censored ') + 'Load standard model (qwen)');
|
|
1281
1485
|
console.log(c.muted(' /uncensored ') + 'Load uncensored model (dolphin)');
|