prior-cli 1.6.6 → 1.7.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 (2) hide show
  1. package/bin/prior.js +182 -1
  2. 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 /censored /uncensored /exit'));
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,96 @@ 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
+ console.log(c.ok(` ✓ Saved "${saveName}"`) + c.muted(` · ${chatHistory.length} messages · ~/.prior/saves/${user}/\n`));
1083
+ } catch (err) {
1084
+ console.log(c.err(` Failed to save: ${err.message}\n`));
1085
+ }
1086
+ return loop();
1087
+ }
1088
+
1089
+ case '/load': {
1090
+ const saves = listSaves(user);
1091
+ if (saves.length === 0) {
1092
+ console.log(c.muted('\n No saved conversations yet.\n'));
1093
+ return loop();
1094
+ }
1095
+
1096
+ let chosen = null;
1097
+ const query = args.join(' ').trim();
1098
+
1099
+ if (query) {
1100
+ // Match by number or name
1101
+ const num = parseInt(query, 10);
1102
+ if (!isNaN(num) && num >= 1 && num <= saves.length) {
1103
+ chosen = saves[num - 1];
1104
+ } else {
1105
+ // fuzzy name match
1106
+ const q = query.toLowerCase();
1107
+ chosen = saves.find(s => sanitizeName(s.name) === sanitizeName(query))
1108
+ || saves.find(s => s.name.toLowerCase().includes(q));
1109
+ }
1110
+ if (!chosen) {
1111
+ console.log(c.err(` No save found matching "${query}"\n`));
1112
+ return loop();
1113
+ }
1114
+ } else {
1115
+ // Interactive picker
1116
+ console.log('');
1117
+ console.log(c.bold(' Load a conversation:'));
1118
+ chosen = await showPicker(saves, rl);
1119
+ if (!chosen) {
1120
+ console.log(c.muted(' Cancelled.\n'));
1121
+ return loop();
1122
+ }
1123
+ }
1124
+
1125
+ // Load it
1126
+ const session = readSession(user, chosen.name);
1127
+ if (!session) {
1128
+ console.log(c.err(` Could not read save file for "${chosen.name}"\n`));
1129
+ return loop();
1130
+ }
1131
+ chatHistory.length = 0;
1132
+ for (const msg of session.messages) chatHistory.push(msg);
1133
+ if (session.model) currentModel = session.model;
1134
+ console.log('');
1135
+ console.log(c.ok(` ✓ Loaded "${chosen.name}"`) + c.muted(` · ${chatHistory.length} messages · model: ${currentModel}`));
1136
+ console.log(c.muted(` Saved ${new Date(chosen.savedAt).toLocaleString('en-PH', { dateStyle: 'medium', timeStyle: 'short' })}\n`));
1137
+ return loop();
1138
+ }
1139
+
962
1140
  case '/clear':
963
1141
  console.clear();
964
1142
  banner();
@@ -1276,6 +1454,9 @@ Be concise but thorough — this summary replaces the full history to save conte
1276
1454
  console.log(c.bold(' Commands'));
1277
1455
  console.log(c.muted(' /compact ') + 'Compact conversation to save context');
1278
1456
  console.log(c.muted(' /timer <duration> ') + 'Countdown timer e.g. /timer 30s, /timer 5m');
1457
+ console.log(c.muted(' /saves ') + 'List all saved conversations');
1458
+ console.log(c.muted(' /save <name> ') + 'Save current conversation');
1459
+ console.log(c.muted(' /load [name|number] ') + 'Load a saved conversation (picker if no arg)');
1279
1460
  console.log(c.muted(' /clear ') + 'Clear screen');
1280
1461
  console.log(c.muted(' /censored ') + 'Load standard model (qwen)');
1281
1462
  console.log(c.muted(' /uncensored ') + 'Load uncensored model (dolphin)');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prior-cli",
3
- "version": "1.6.6",
3
+ "version": "1.7.0",
4
4
  "description": "Prior Network AI — command-line interface",
5
5
  "bin": {
6
6
  "prior": "bin/prior.js"