prior-cli 1.6.5 → 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.
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;
@@ -49,6 +134,14 @@ let _spinLabel = '';
49
134
  // Detects keywords in user input and prepends a hard directive so
50
135
  // the model can't second-guess which tool to use.
51
136
  const TOOL_HINTS = [
137
+ {
138
+ tool: 'get_time',
139
+ patterns: [
140
+ /\bwhat time\b/i, /\bwhat('s| is) the time\b/i, /\bcurrent time\b/i,
141
+ /\btime (is it|now|right now)\b/i,
142
+ ],
143
+ hint: '[TOOL DIRECTIVE: You MUST call get_time — do NOT guess the time]',
144
+ },
52
145
  {
53
146
  tool: 'ssl_check',
54
147
  patterns: [
@@ -751,7 +844,7 @@ async function startChat(opts = {}) {
751
844
  console.log(c.ok(' ◉') + c.muted(' Agent mode ') + c.dim('· file web shell image prior-network'));
752
845
 
753
846
  console.log(DIVIDER);
754
- console.log(c.muted(' /help /clear /compact /timer /censored /uncensored /exit'));
847
+ console.log(c.muted(' /help /clear /compact /timer /save /load /saves /exit'));
755
848
  console.log(DIVIDER);
756
849
  console.log('');
757
850
 
@@ -770,6 +863,9 @@ async function startChat(opts = {}) {
770
863
  const SLASH_CMDS = [
771
864
  { cmd: '/compact', desc: 'Compact conversation to save context' },
772
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' },
773
869
  { cmd: '/help', desc: 'Show help' },
774
870
  { cmd: '/clear', desc: 'Clear screen' },
775
871
  { cmd: '/censored', desc: 'Load standard model (qwen)' },
@@ -951,6 +1047,96 @@ async function startChat(opts = {}) {
951
1047
  console.log(c.ok(' ✓ Logged out.\n'));
952
1048
  return loop();
953
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
+
954
1140
  case '/clear':
955
1141
  console.clear();
956
1142
  banner();
@@ -1268,6 +1454,9 @@ Be concise but thorough — this summary replaces the full history to save conte
1268
1454
  console.log(c.bold(' Commands'));
1269
1455
  console.log(c.muted(' /compact ') + 'Compact conversation to save context');
1270
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)');
1271
1460
  console.log(c.muted(' /clear ') + 'Clear screen');
1272
1461
  console.log(c.muted(' /censored ') + 'Load standard model (qwen)');
1273
1462
  console.log(c.muted(' /uncensored ') + 'Load uncensored model (dolphin)');
package/lib/tools.js CHANGED
@@ -417,6 +417,27 @@ const TOOLS = {
417
417
  return { output: data.output || JSON.stringify(data), summary: data.summary || `spider started for ${url}` };
418
418
  },
419
419
 
420
+ async get_time({}, {}) {
421
+ const now = new Date();
422
+ const iso = now.toISOString();
423
+ const local = now.toLocaleString('en-PH', {
424
+ timeZone: 'Asia/Manila',
425
+ weekday: 'long',
426
+ year: 'numeric',
427
+ month: 'long',
428
+ day: 'numeric',
429
+ hour: '2-digit',
430
+ minute: '2-digit',
431
+ second: '2-digit',
432
+ hour12: true,
433
+ });
434
+ const utc = now.toUTCString();
435
+ return {
436
+ output: `Local (PHT/GMT+8) : ${local}\nUTC : ${utc}\nISO 8601 : ${iso}`,
437
+ summary: local,
438
+ };
439
+ },
440
+
420
441
  async ip_lookup({ target }, {}) {
421
442
  if (!target) throw new Error('"target" is required — provide an IP address or domain');
422
443
  const encoded = encodeURIComponent(target.trim());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prior-cli",
3
- "version": "1.6.5",
3
+ "version": "1.7.0",
4
4
  "description": "Prior Network AI — command-line interface",
5
5
  "bin": {
6
6
  "prior": "bin/prior.js"