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 +190 -1
- package/lib/tools.js +21 -0
- 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;
|
|
@@ -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 /
|
|
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());
|