prior-cli 1.0.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 +1092 -0
- package/lib/api.js +244 -0
- package/lib/config.js +38 -0
- package/lib/render.js +96 -0
- package/package.json +30 -0
package/bin/prior.js
ADDED
|
@@ -0,0 +1,1092 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { program } = require('commander');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const os = require('os');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const { version } = require('../package.json');
|
|
11
|
+
|
|
12
|
+
const api = require('../lib/api');
|
|
13
|
+
const { renderMarkdown } = require('../lib/render');
|
|
14
|
+
const { getToken, getUsername, saveAuth, clearAuth } = require('../lib/config');
|
|
15
|
+
|
|
16
|
+
// ── Theme ──────────────────────────────────────────────────────
|
|
17
|
+
const THEME = '#9CE2D4';
|
|
18
|
+
const c = {
|
|
19
|
+
brand: t => chalk.hex(THEME)(t),
|
|
20
|
+
bold: t => chalk.hex(THEME).bold(t),
|
|
21
|
+
dim: t => chalk.dim(t),
|
|
22
|
+
muted: t => chalk.gray(t),
|
|
23
|
+
err: t => chalk.red(t),
|
|
24
|
+
warn: t => chalk.yellow(t),
|
|
25
|
+
ok: t => chalk.green(t),
|
|
26
|
+
link: t => chalk.cyan.underline(t),
|
|
27
|
+
white: t => chalk.white(t),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const DIVIDER = c.muted(' ' + '─'.repeat(50));
|
|
31
|
+
|
|
32
|
+
// ── Spinner ────────────────────────────────────────────────────
|
|
33
|
+
const SPIN_FRAMES = ['◐', '◓', '◑', '◒'];
|
|
34
|
+
let _spinTimer = null;
|
|
35
|
+
let _spinIdx = 0;
|
|
36
|
+
|
|
37
|
+
function spinStart(label = '') {
|
|
38
|
+
spinStop();
|
|
39
|
+
if (!process.stdout.isTTY) return;
|
|
40
|
+
_spinTimer = setInterval(() => {
|
|
41
|
+
process.stdout.clearLine(0);
|
|
42
|
+
process.stdout.cursorTo(0);
|
|
43
|
+
process.stdout.write(` ${c.brand(SPIN_FRAMES[_spinIdx++ % 4])} ${c.dim(label)}`);
|
|
44
|
+
}, 100);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function spinStop() {
|
|
48
|
+
if (_spinTimer) { clearInterval(_spinTimer); _spinTimer = null; }
|
|
49
|
+
if (process.stdout.isTTY) {
|
|
50
|
+
process.stdout.clearLine(0);
|
|
51
|
+
process.stdout.cursorTo(0);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Helpers ────────────────────────────────────────────────────
|
|
56
|
+
function requireAuth() {
|
|
57
|
+
if (!getToken()) {
|
|
58
|
+
console.error(c.err(' ✗ Not logged in. Run: ') + c.brand('prior login'));
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function progressBar(pct, width = 22) {
|
|
64
|
+
const filled = Math.round((pct / 100) * width);
|
|
65
|
+
return c.brand('█'.repeat(filled)) + c.muted('░'.repeat(width - filled));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function clearLine() {
|
|
69
|
+
if (process.stdout.isTTY) {
|
|
70
|
+
process.stdout.clearLine(0);
|
|
71
|
+
process.stdout.cursorTo(0);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── /learn scanner animation ───────────────────────────────────
|
|
76
|
+
async function runLearnAnimation(cwd) {
|
|
77
|
+
const SKIP = new Set(['node_modules', '.git', '__pycache__', '.next', 'dist', 'build',
|
|
78
|
+
'.venv', 'venv', '.cache', 'coverage', '.nyc_output', 'vendor']);
|
|
79
|
+
const TEXT = new Set(['.js','.ts','.jsx','.tsx','.py','.json','.md','.txt','.html',
|
|
80
|
+
'.css','.scss','.yml','.yaml','.sh','.bat','.go','.rs','.java',
|
|
81
|
+
'.c','.cpp','.h','.php','.rb','.vue','.svelte','.env','.toml','.ini']);
|
|
82
|
+
|
|
83
|
+
const files = [];
|
|
84
|
+
function walk(dir, depth = 0) {
|
|
85
|
+
if (depth > 6) return;
|
|
86
|
+
try {
|
|
87
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
88
|
+
if (e.name.startsWith('.') || SKIP.has(e.name)) continue;
|
|
89
|
+
const full = path.join(dir, e.name);
|
|
90
|
+
if (e.isDirectory()) walk(full, depth + 1);
|
|
91
|
+
else files.push({ full, rel: path.relative(cwd, full), ext: path.extname(e.name).toLowerCase() });
|
|
92
|
+
}
|
|
93
|
+
} catch {}
|
|
94
|
+
}
|
|
95
|
+
walk(cwd);
|
|
96
|
+
if (!files.length) return 0;
|
|
97
|
+
|
|
98
|
+
const HEIGHT = 8;
|
|
99
|
+
const cols = Math.min(process.stdout.columns || 100, 120);
|
|
100
|
+
const delay = Math.max(15, Math.min(90, 3200 / files.length));
|
|
101
|
+
const window = [];
|
|
102
|
+
|
|
103
|
+
// Reserve space
|
|
104
|
+
process.stdout.write('\n'.repeat(HEIGHT));
|
|
105
|
+
process.stdout.write(`\x1b[${HEIGHT}A\x1b[s`);
|
|
106
|
+
|
|
107
|
+
for (let i = 0; i < files.length; i++) {
|
|
108
|
+
const f = files[i];
|
|
109
|
+
|
|
110
|
+
// Read snippet for text files only
|
|
111
|
+
if (TEXT.has(f.ext)) {
|
|
112
|
+
try {
|
|
113
|
+
if (fs.statSync(f.full).size < 150 * 1024) {
|
|
114
|
+
f.snippet = fs.readFileSync(f.full, 'utf8').replace(/\s+/g, ' ').trim().slice(0, 70);
|
|
115
|
+
}
|
|
116
|
+
} catch {}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
window.push(f);
|
|
120
|
+
if (window.length > HEIGHT - 1) window.shift();
|
|
121
|
+
|
|
122
|
+
process.stdout.write('\x1b[u');
|
|
123
|
+
|
|
124
|
+
// Header
|
|
125
|
+
const spin = SPIN_FRAMES[Math.floor(i / 2) % 4];
|
|
126
|
+
const pct = Math.round(((i + 1) / files.length) * 100);
|
|
127
|
+
const bar = progressBar(pct, 14);
|
|
128
|
+
process.stdout.write(`\x1b[2K ${c.brand(spin)} ${c.bold('Learning directory')} ${bar} ${c.muted(`${i + 1}/${files.length}`)}\n`);
|
|
129
|
+
|
|
130
|
+
// File lines
|
|
131
|
+
for (let j = 0; j < HEIGHT - 1; j++) {
|
|
132
|
+
process.stdout.write('\x1b[2K');
|
|
133
|
+
const entry = window[j];
|
|
134
|
+
if (!entry) { process.stdout.write('\n'); continue; }
|
|
135
|
+
|
|
136
|
+
const isActive = j === window.length - 1;
|
|
137
|
+
const connector = isActive ? '└' : '├';
|
|
138
|
+
const maxName = 30;
|
|
139
|
+
const name = entry.rel.length > maxName
|
|
140
|
+
? '…' + entry.rel.slice(-(maxName - 1))
|
|
141
|
+
: entry.rel;
|
|
142
|
+
const snipLen = cols - maxName - 16;
|
|
143
|
+
const snippet = (entry.snippet || '').slice(0, snipLen);
|
|
144
|
+
|
|
145
|
+
if (isActive) {
|
|
146
|
+
process.stdout.write(` ${c.brand(connector)} ${c.bold(name.padEnd(maxName))} ${c.muted(snippet)}\n`);
|
|
147
|
+
} else {
|
|
148
|
+
process.stdout.write(` ${c.muted(connector)} ${c.muted(name.padEnd(maxName))} ${c.dim(snippet.slice(0, 35))}\n`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await new Promise(r => setTimeout(r, delay));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Final frame — full bar
|
|
156
|
+
process.stdout.write('\x1b[u');
|
|
157
|
+
process.stdout.write(`\x1b[2K ${c.brand('◈')} ${c.bold('Learning directory')} ${progressBar(100, 14)} ${c.ok(`${files.length} files`)}\n`);
|
|
158
|
+
for (let i = 0; i < HEIGHT - 1; i++) process.stdout.write('\x1b[2K\n');
|
|
159
|
+
process.stdout.write('\x1b[u');
|
|
160
|
+
await new Promise(r => setTimeout(r, 400));
|
|
161
|
+
|
|
162
|
+
// Clear
|
|
163
|
+
for (let i = 0; i < HEIGHT; i++) process.stdout.write('\x1b[2K\n');
|
|
164
|
+
process.stdout.write(`\x1b[${HEIGHT}A`);
|
|
165
|
+
|
|
166
|
+
return files.length;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function promptPassword(question) {
|
|
170
|
+
return new Promise(resolve => {
|
|
171
|
+
if (!process.stdin.isTTY) {
|
|
172
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
173
|
+
rl.question(question, ans => { rl.close(); resolve(ans.trim()); });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
process.stdout.write(question);
|
|
177
|
+
process.stdin.setRawMode(true);
|
|
178
|
+
process.stdin.resume();
|
|
179
|
+
process.stdin.setEncoding('utf8');
|
|
180
|
+
let input = '';
|
|
181
|
+
function handler(ch) {
|
|
182
|
+
switch (ch) {
|
|
183
|
+
case '\n': case '\r': case '\u0004':
|
|
184
|
+
process.stdin.setRawMode(false);
|
|
185
|
+
process.stdin.removeListener('data', handler);
|
|
186
|
+
process.stdin.pause();
|
|
187
|
+
process.stdout.write('\n');
|
|
188
|
+
resolve(input);
|
|
189
|
+
break;
|
|
190
|
+
case '\u0003': process.exit(); break;
|
|
191
|
+
case '\u007f':
|
|
192
|
+
if (input.length) { input = input.slice(0, -1); process.stdout.write('\b \b'); }
|
|
193
|
+
break;
|
|
194
|
+
default:
|
|
195
|
+
input += ch;
|
|
196
|
+
process.stdout.write('*');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
process.stdin.on('data', handler);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function ask(rl, question) {
|
|
204
|
+
return new Promise(resolve => rl.question(question, ans => resolve(ans.trim())));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function banner() {
|
|
208
|
+
console.log('');
|
|
209
|
+
console.log(c.brand(' ██████╗ ██████╗ ██╗ ██████╗ ██████╗ '));
|
|
210
|
+
console.log(c.brand(' ██╔══██╗██╔══██╗██║██╔═══██╗██╔══██╗'));
|
|
211
|
+
console.log(c.brand(' ██████╔╝██████╔╝██║██║ ██║██████╔╝'));
|
|
212
|
+
console.log(c.brand(' ██╔═══╝ ██╔══██╗██║██║ ██║██╔══██╗'));
|
|
213
|
+
console.log(c.brand(' ██║ ██║ ██║██║╚██████╔╝██║ ██║'));
|
|
214
|
+
console.log(c.brand(' ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═╝'));
|
|
215
|
+
console.log('');
|
|
216
|
+
console.log(c.muted(` v${version} · prior.ngrok.app`));
|
|
217
|
+
console.log('');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── Time helpers ───────────────────────────────────────────────
|
|
221
|
+
function timeNow() {
|
|
222
|
+
return new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: true });
|
|
223
|
+
}
|
|
224
|
+
function elapsed(ms) {
|
|
225
|
+
if (ms < 1000) return `${ms}ms`;
|
|
226
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── Tool call display ──────────────────────────────────────────
|
|
230
|
+
const TOOL_ICONS = {
|
|
231
|
+
file_read: '📄',
|
|
232
|
+
file_write: '✏️ ',
|
|
233
|
+
file_append: '📝',
|
|
234
|
+
file_list: '📁',
|
|
235
|
+
file_delete: '🗑 ',
|
|
236
|
+
web_search: '🔍',
|
|
237
|
+
url_fetch: '🌐',
|
|
238
|
+
run_command: '⚡',
|
|
239
|
+
clipboard_read: '📋',
|
|
240
|
+
clipboard_write: '📋',
|
|
241
|
+
generate_image: '🎨',
|
|
242
|
+
prior_feed: '📰',
|
|
243
|
+
prior_profile: '👤',
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
function toolIcon(name) {
|
|
247
|
+
return TOOL_ICONS[name] || '⎔ ';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let _toolStartTime = 0;
|
|
251
|
+
|
|
252
|
+
function renderToolStart(name, args) {
|
|
253
|
+
_toolStartTime = Date.now();
|
|
254
|
+
const icon = toolIcon(name);
|
|
255
|
+
const label = c.bold(name.padEnd(16));
|
|
256
|
+
const preview = Object.values(args || {})[0];
|
|
257
|
+
const hint = preview ? c.muted(String(preview).slice(0, 80)) : '';
|
|
258
|
+
process.stdout.write(` ${icon} ${label} ${hint}\n`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function hyperlink(text, url) {
|
|
262
|
+
return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function renderToolDone(name, summary) {
|
|
266
|
+
const took = _toolStartTime ? c.dim(` · ${elapsed(Date.now() - _toolStartTime)}`) : '';
|
|
267
|
+
const label = c.muted(name.padEnd(16));
|
|
268
|
+
let display = summary || '';
|
|
269
|
+
if (/^[a-zA-Z]:[/\\]/.test(display) || display.startsWith('/')) {
|
|
270
|
+
const fileUrl = 'file:///' + display.replace(/\\/g, '/');
|
|
271
|
+
display = hyperlink(c.dim(display), fileUrl);
|
|
272
|
+
} else {
|
|
273
|
+
display = c.dim(display);
|
|
274
|
+
}
|
|
275
|
+
process.stdout.write(` ${c.ok('✓')} ${label} ${display}${took}\n`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function renderToolError(name, error) {
|
|
279
|
+
const took = _toolStartTime ? c.dim(` · ${elapsed(Date.now() - _toolStartTime)}`) : '';
|
|
280
|
+
const label = c.muted(name.padEnd(16));
|
|
281
|
+
process.stdout.write(` ${c.err('✗')} ${label} ${c.err(error || 'failed')}${took}\n`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Browser login via public URL ───────────────────────────────
|
|
285
|
+
async function loginViaBrowser() {
|
|
286
|
+
const open = require('open');
|
|
287
|
+
const crypto = require('crypto');
|
|
288
|
+
|
|
289
|
+
// Generate a random state token to match the browser session to this CLI wait
|
|
290
|
+
const state = crypto.randomBytes(16).toString('hex');
|
|
291
|
+
const url = `https://prior.ngrok.app/cli-auth?state=${state}`;
|
|
292
|
+
|
|
293
|
+
await open(url).catch(() => {
|
|
294
|
+
process.stdout.write('\n');
|
|
295
|
+
console.log(c.muted(` Open in browser: ${url}\n`));
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// Long-poll the CLI backend until browser completes login (3 min timeout handled server-side)
|
|
299
|
+
const fetch = require('node-fetch');
|
|
300
|
+
const res = await fetch(`http://127.0.0.1:7100/wait?state=${state}`, {
|
|
301
|
+
timeout: 185000,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
if (!res.ok) throw new Error('Login timed out or was cancelled');
|
|
305
|
+
const data = await res.json();
|
|
306
|
+
if (!data.token) throw new Error('No token received');
|
|
307
|
+
return { token: data.token, username: data.username };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── Inline login flow ──────────────────────────────────────────
|
|
311
|
+
async function doLoginFlow() {
|
|
312
|
+
console.log(c.bold(' Sign in\n'));
|
|
313
|
+
console.log(` ${c.brand('[1]')} Login manually`);
|
|
314
|
+
console.log(` ${c.brand('[2]')} Login in browser`);
|
|
315
|
+
console.log('');
|
|
316
|
+
|
|
317
|
+
const choiceRl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
318
|
+
const choice = await ask(choiceRl, c.muted(' Choice [1/2] : '));
|
|
319
|
+
choiceRl.close();
|
|
320
|
+
console.log('');
|
|
321
|
+
|
|
322
|
+
if (choice === '2') {
|
|
323
|
+
process.stdout.write(c.dim(' Opening browser…'));
|
|
324
|
+
try {
|
|
325
|
+
const { token, username } = await loginViaBrowser();
|
|
326
|
+
saveAuth(token, username);
|
|
327
|
+
clearLine();
|
|
328
|
+
console.log(c.ok(' ✓ Logged in as ') + c.bold(username));
|
|
329
|
+
console.log('');
|
|
330
|
+
return username;
|
|
331
|
+
} catch (err) {
|
|
332
|
+
clearLine();
|
|
333
|
+
throw err;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Default: manual
|
|
338
|
+
const manualRl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
339
|
+
const username = await ask(manualRl, c.muted(' Username : '));
|
|
340
|
+
manualRl.close();
|
|
341
|
+
const password = await promptPassword(c.muted(' Password : '));
|
|
342
|
+
console.log('');
|
|
343
|
+
process.stdout.write(c.dim(' Authenticating…'));
|
|
344
|
+
const data = await api.login(username, password);
|
|
345
|
+
saveAuth(data.token, username);
|
|
346
|
+
clearLine();
|
|
347
|
+
console.log(c.ok(' ✓ Logged in as ') + c.bold(username));
|
|
348
|
+
console.log('');
|
|
349
|
+
return username;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ── Interactive Chat ───────────────────────────────────────────
|
|
353
|
+
async function startChat(opts = {}) {
|
|
354
|
+
// If not logged in, prompt inline instead of erroring out
|
|
355
|
+
if (!getToken()) {
|
|
356
|
+
banner();
|
|
357
|
+
try {
|
|
358
|
+
await doLoginFlow();
|
|
359
|
+
} catch (err) {
|
|
360
|
+
clearLine();
|
|
361
|
+
console.error(c.err(` ✗ ${err.message}`));
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const user = getUsername();
|
|
367
|
+
|
|
368
|
+
// Check if agent backend is running
|
|
369
|
+
spinStart('connecting…');
|
|
370
|
+
const backendHealth = await api.checkBackend();
|
|
371
|
+
spinStop();
|
|
372
|
+
|
|
373
|
+
const agentMode = !!backendHealth;
|
|
374
|
+
|
|
375
|
+
console.clear();
|
|
376
|
+
banner();
|
|
377
|
+
console.log(DIVIDER);
|
|
378
|
+
|
|
379
|
+
// Header row
|
|
380
|
+
const modelLabel = opts.model || 'default';
|
|
381
|
+
const cwdShort = process.cwd().replace(os.homedir(), '~');
|
|
382
|
+
console.log(
|
|
383
|
+
c.brand(' Prior AI') +
|
|
384
|
+
c.muted(' · ') +
|
|
385
|
+
c.bold(`@${user}`) +
|
|
386
|
+
c.muted(` · ${modelLabel}`)
|
|
387
|
+
);
|
|
388
|
+
console.log(c.muted(` ${cwdShort}`));
|
|
389
|
+
|
|
390
|
+
if (agentMode) {
|
|
391
|
+
console.log(c.ok(' ◉') + c.muted(' Agent mode ') + c.dim('· file web shell image prior-network'));
|
|
392
|
+
} else {
|
|
393
|
+
console.log(c.warn(' ◎') + c.muted(' Basic mode ') + c.dim('· run prior-cli-backend for agent mode'));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (opts.uncensored) console.log(c.warn(' ⚠ Uncensored mode active'));
|
|
397
|
+
console.log(DIVIDER);
|
|
398
|
+
console.log(c.muted(' /help /clear /model <name> /tools /uncensored /exit'));
|
|
399
|
+
console.log(DIVIDER);
|
|
400
|
+
console.log('');
|
|
401
|
+
|
|
402
|
+
// Conversation history (for agent mode, keeps full multi-turn context)
|
|
403
|
+
const chatHistory = [];
|
|
404
|
+
let currentModel = opts.model || null;
|
|
405
|
+
let uncensored = opts.uncensored || false;
|
|
406
|
+
|
|
407
|
+
const rl = readline.createInterface({
|
|
408
|
+
input: process.stdin,
|
|
409
|
+
output: process.stdout,
|
|
410
|
+
terminal: true,
|
|
411
|
+
historySize: 100,
|
|
412
|
+
completer: line => {
|
|
413
|
+
const cmds = ['/help', '/clear', '/model ', '/tools', '/uncensored', '/censored', '/login', '/logout', '/exit'];
|
|
414
|
+
if (!line.startsWith('/')) return [[], line];
|
|
415
|
+
const hits = cmds.filter(cmd => cmd.startsWith(line));
|
|
416
|
+
return [hits, line];
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// ── Live slash-command suggestions ──────────────────────────
|
|
421
|
+
let clearSuggestions = () => {};
|
|
422
|
+
|
|
423
|
+
if (process.stdout.isTTY) {
|
|
424
|
+
readline.emitKeypressEvents(process.stdin, rl);
|
|
425
|
+
|
|
426
|
+
const SLASH_CMDS = [
|
|
427
|
+
{ cmd: '/help', desc: 'Show help' },
|
|
428
|
+
{ cmd: '/clear', desc: 'Clear screen' },
|
|
429
|
+
{ cmd: '/model', desc: 'Switch model' },
|
|
430
|
+
{ cmd: '/tools', desc: 'List tools' },
|
|
431
|
+
{ cmd: '/uncensored', desc: 'Uncensored mode' },
|
|
432
|
+
{ cmd: '/censored', desc: 'Standard mode' },
|
|
433
|
+
{ cmd: '/usage', desc: 'Token usage today' },
|
|
434
|
+
{ cmd: '/learn', desc: 'Learn this directory → prior.md' },
|
|
435
|
+
{ cmd: '/login', desc: 'Sign in' },
|
|
436
|
+
{ cmd: '/logout', desc: 'Sign out' },
|
|
437
|
+
{ cmd: '/exit', desc: 'Exit' },
|
|
438
|
+
];
|
|
439
|
+
|
|
440
|
+
let _suggCount = 0;
|
|
441
|
+
let _suggTimer = null;
|
|
442
|
+
|
|
443
|
+
clearSuggestions = function () {
|
|
444
|
+
if (_suggCount === 0) return;
|
|
445
|
+
process.stdout.write('\x1b[s');
|
|
446
|
+
for (let i = 0; i < _suggCount; i++) process.stdout.write('\x1b[B\r\x1b[2K');
|
|
447
|
+
process.stdout.write('\x1b[u');
|
|
448
|
+
_suggCount = 0;
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
function renderSuggestions(matches) {
|
|
452
|
+
clearSuggestions();
|
|
453
|
+
if (!matches.length) return;
|
|
454
|
+
_suggCount = matches.length;
|
|
455
|
+
// Print newlines to scroll terminal and guarantee room below the prompt
|
|
456
|
+
process.stdout.write('\n'.repeat(matches.length));
|
|
457
|
+
process.stdout.write(`\x1b[${matches.length}A`); // move cursor back up
|
|
458
|
+
process.stdout.write('\x1b[s'); // save cursor (now at prompt line)
|
|
459
|
+
for (const { cmd, desc } of matches) {
|
|
460
|
+
process.stdout.write(`\x1b[B\r\x1b[2K${c.brand(' ' + cmd.padEnd(14))}${c.dim(desc)}`);
|
|
461
|
+
}
|
|
462
|
+
process.stdout.write('\x1b[u');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
process.stdin.on('keypress', (ch, key) => {
|
|
466
|
+
if (!key) return;
|
|
467
|
+
|
|
468
|
+
if (_suggTimer) { clearTimeout(_suggTimer); _suggTimer = null; }
|
|
469
|
+
|
|
470
|
+
if (key.name === 'return' || key.name === 'enter' || (key.ctrl && key.name === 'c')) {
|
|
471
|
+
clearSuggestions();
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
_suggTimer = setTimeout(() => {
|
|
476
|
+
_suggTimer = null;
|
|
477
|
+
const line = rl.line || '';
|
|
478
|
+
if (!line.startsWith('/')) {
|
|
479
|
+
clearSuggestions();
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const word = line.split(' ')[0];
|
|
483
|
+
const matches = SLASH_CMDS.filter(({ cmd }) => cmd.startsWith(word));
|
|
484
|
+
renderSuggestions(matches);
|
|
485
|
+
}, 50);
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// ── Load prior.md if present ────────────────────────────────
|
|
490
|
+
const priorMdPath = path.join(process.cwd(), 'prior.md');
|
|
491
|
+
let projectContext = null;
|
|
492
|
+
try {
|
|
493
|
+
projectContext = fs.readFileSync(priorMdPath, 'utf8');
|
|
494
|
+
console.log(c.brand(' ◈') + c.muted(' prior.md loaded — project context active'));
|
|
495
|
+
console.log('');
|
|
496
|
+
} catch { /* no prior.md, fine */ }
|
|
497
|
+
|
|
498
|
+
let restarting = false;
|
|
499
|
+
let msgCount = 0;
|
|
500
|
+
const sessionStart = Date.now();
|
|
501
|
+
|
|
502
|
+
rl.on('close', () => {
|
|
503
|
+
if (restarting) return;
|
|
504
|
+
const dur = elapsed(Date.now() - sessionStart);
|
|
505
|
+
console.log('');
|
|
506
|
+
console.log(c.muted(` ─────────────────────────────────────────────────`));
|
|
507
|
+
console.log(c.muted(` Session ended · ${msgCount} message${msgCount !== 1 ? 's' : ''} · ${dur}`));
|
|
508
|
+
console.log('');
|
|
509
|
+
process.exit(0);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const PROMPT = () => c.brand(' ❯ ');
|
|
513
|
+
|
|
514
|
+
const loop = () => {
|
|
515
|
+
rl.question(PROMPT(), async raw => {
|
|
516
|
+
clearSuggestions();
|
|
517
|
+
const input = raw.trim();
|
|
518
|
+
if (!input) return loop();
|
|
519
|
+
|
|
520
|
+
// ── Slash commands ──────────────────────────────────────
|
|
521
|
+
if (input.startsWith('/')) {
|
|
522
|
+
const [cmd, ...args] = input.split(' ');
|
|
523
|
+
switch (cmd) {
|
|
524
|
+
|
|
525
|
+
case '/exit':
|
|
526
|
+
case '/quit':
|
|
527
|
+
return rl.close();
|
|
528
|
+
|
|
529
|
+
case '/login': {
|
|
530
|
+
restarting = true;
|
|
531
|
+
rl.close();
|
|
532
|
+
console.log('');
|
|
533
|
+
try {
|
|
534
|
+
await doLoginFlow();
|
|
535
|
+
} catch (err) {
|
|
536
|
+
clearLine();
|
|
537
|
+
console.error(c.err(` ✗ ${err.message}\n`));
|
|
538
|
+
}
|
|
539
|
+
return startChat({ model: currentModel, uncensored });
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
case '/logout':
|
|
543
|
+
clearAuth();
|
|
544
|
+
console.log(c.ok(' ✓ Logged out.\n'));
|
|
545
|
+
return loop();
|
|
546
|
+
|
|
547
|
+
case '/clear':
|
|
548
|
+
console.clear();
|
|
549
|
+
banner();
|
|
550
|
+
console.log(DIVIDER);
|
|
551
|
+
console.log(c.brand(' Prior AI') + c.muted(' · ') + c.muted(`@${user}`));
|
|
552
|
+
if (agentMode) console.log(c.ok(' ◉') + c.muted(' Agent mode'));
|
|
553
|
+
console.log(DIVIDER);
|
|
554
|
+
console.log('');
|
|
555
|
+
return loop();
|
|
556
|
+
|
|
557
|
+
case '/uncensored':
|
|
558
|
+
uncensored = true;
|
|
559
|
+
console.log(c.warn(' ⚠ Uncensored mode enabled\n'));
|
|
560
|
+
return loop();
|
|
561
|
+
|
|
562
|
+
case '/censored':
|
|
563
|
+
uncensored = false;
|
|
564
|
+
console.log(c.ok(' ✓ Standard mode restored\n'));
|
|
565
|
+
return loop();
|
|
566
|
+
|
|
567
|
+
case '/model':
|
|
568
|
+
if (args[0]) {
|
|
569
|
+
currentModel = args[0];
|
|
570
|
+
console.log(c.ok(` ✓ Model → ${currentModel}\n`));
|
|
571
|
+
} else {
|
|
572
|
+
console.log(c.muted(` Current model: ${currentModel || 'default'}\n`));
|
|
573
|
+
}
|
|
574
|
+
return loop();
|
|
575
|
+
|
|
576
|
+
case '/tools':
|
|
577
|
+
console.log('');
|
|
578
|
+
console.log(c.bold(' Tools available in agent mode\n'));
|
|
579
|
+
const tools = [
|
|
580
|
+
['file_read', 'Read a file'],
|
|
581
|
+
['file_write', 'Create or overwrite a file'],
|
|
582
|
+
['file_append', 'Append to a file'],
|
|
583
|
+
['file_list', 'List directory contents'],
|
|
584
|
+
['file_delete', 'Delete a file'],
|
|
585
|
+
['web_search', 'Search the web (Google)'],
|
|
586
|
+
['url_fetch', 'Fetch a webpage (Puppeteer)'],
|
|
587
|
+
['run_command', 'Execute a shell command'],
|
|
588
|
+
['clipboard_read', 'Read from clipboard'],
|
|
589
|
+
['clipboard_write', 'Write to clipboard'],
|
|
590
|
+
['generate_image', 'Generate an image (Prior Diffusion)'],
|
|
591
|
+
['prior_feed', 'Get Prior Network feed'],
|
|
592
|
+
['prior_profile', 'Get your Prior Network profile'],
|
|
593
|
+
];
|
|
594
|
+
tools.forEach(([name, desc]) => {
|
|
595
|
+
console.log(` ${c.brand('▸')} ${c.bold(name.padEnd(16))} ${c.muted(desc)}`);
|
|
596
|
+
});
|
|
597
|
+
console.log('');
|
|
598
|
+
if (!agentMode) console.log(c.warn(' ⚠ Backend offline — start prior-cli-backend to enable\n'));
|
|
599
|
+
return loop();
|
|
600
|
+
|
|
601
|
+
case '/learn': {
|
|
602
|
+
// Sanity check — warn if not a project directory
|
|
603
|
+
const cwdBase = path.basename(process.cwd()).toLowerCase();
|
|
604
|
+
const NON_PROJECT = ['downloads', 'desktop', 'documents', 'pictures', 'videos', 'music', 'temp', 'tmp'];
|
|
605
|
+
if (NON_PROJECT.includes(cwdBase)) {
|
|
606
|
+
console.log('');
|
|
607
|
+
console.log(c.warn(` ⚠ You're in "${path.basename(process.cwd())}" — this doesn't look like a project directory.`));
|
|
608
|
+
console.log(c.muted(' cd into your project folder first, then run /learn.'));
|
|
609
|
+
console.log('');
|
|
610
|
+
return loop();
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
console.log('');
|
|
614
|
+
await runLearnAnimation(process.cwd());
|
|
615
|
+
console.log('');
|
|
616
|
+
|
|
617
|
+
// ── Build flat file list ──────────────────────────
|
|
618
|
+
const flatFiles = [];
|
|
619
|
+
function collectFiles(dir, depth = 0) {
|
|
620
|
+
if (depth > 4) return;
|
|
621
|
+
const SKIP = new Set(['node_modules','.git','__pycache__','.next','dist','build','.venv','venv','.cache']);
|
|
622
|
+
try {
|
|
623
|
+
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
624
|
+
if (e.name.startsWith('.') || SKIP.has(e.name)) continue;
|
|
625
|
+
const full = path.join(dir, e.name);
|
|
626
|
+
const rel = path.relative(process.cwd(), full).replace(/\\/g, '/');
|
|
627
|
+
if (e.isDirectory()) collectFiles(full, depth + 1);
|
|
628
|
+
else flatFiles.push(rel);
|
|
629
|
+
}
|
|
630
|
+
} catch {}
|
|
631
|
+
}
|
|
632
|
+
collectFiles(process.cwd());
|
|
633
|
+
|
|
634
|
+
// ── Read key files locally (no agent tool calls needed) ──
|
|
635
|
+
const KEY_NAMES = ['package.json','README.md','readme.md','README.txt',
|
|
636
|
+
'server.js','Server.js','app.js','App.js','index.js',
|
|
637
|
+
'main.js','main.py','app.py','index.ts','main.ts',
|
|
638
|
+
'pyproject.toml','requirements.txt','Cargo.toml','go.mod'];
|
|
639
|
+
|
|
640
|
+
const readSnippets = [];
|
|
641
|
+
const MAX_READ = 6;
|
|
642
|
+
const MAX_BYTES = 2000;
|
|
643
|
+
|
|
644
|
+
// First pass: exact key name matches at root or one level deep
|
|
645
|
+
const keyMatches = flatFiles.filter(f => {
|
|
646
|
+
const base = path.basename(f);
|
|
647
|
+
return KEY_NAMES.includes(base);
|
|
648
|
+
}).slice(0, MAX_READ);
|
|
649
|
+
|
|
650
|
+
for (const rel of keyMatches) {
|
|
651
|
+
try {
|
|
652
|
+
const full = path.join(process.cwd(), rel);
|
|
653
|
+
const content = fs.readFileSync(full, 'utf8').slice(0, MAX_BYTES);
|
|
654
|
+
readSnippets.push(`### ${rel}\n\`\`\`\n${content}\n\`\`\``);
|
|
655
|
+
} catch {}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const fileList = flatFiles.slice(0, 120).join('\n');
|
|
659
|
+
|
|
660
|
+
const learnPrompt = `You are analyzing a project directory. Based on the file list and file contents below, write a prior.md file that documents this project.
|
|
661
|
+
|
|
662
|
+
## File list
|
|
663
|
+
${fileList}${flatFiles.length > 120 ? `\n… (${flatFiles.length - 120} more files)` : ''}
|
|
664
|
+
|
|
665
|
+
## Key file contents
|
|
666
|
+
${readSnippets.length ? readSnippets.join('\n\n') : '(no key files found)'}
|
|
667
|
+
|
|
668
|
+
## Instructions
|
|
669
|
+
Write prior.md immediately using the <write path="prior.md"> tag. Include:
|
|
670
|
+
- Project name and purpose (1-2 sentences)
|
|
671
|
+
- Tech stack (languages, frameworks, key libraries)
|
|
672
|
+
- Key files and what they do
|
|
673
|
+
- Any important conventions or notes
|
|
674
|
+
|
|
675
|
+
Keep it under 350 words. Write prior.md now.`;
|
|
676
|
+
|
|
677
|
+
let learnTextBuffer = '';
|
|
678
|
+
|
|
679
|
+
try {
|
|
680
|
+
await api.agentChat(
|
|
681
|
+
[{ role: 'user', content: learnPrompt }],
|
|
682
|
+
{ model: currentModel, uncensored, cwd: process.cwd() },
|
|
683
|
+
ev => {
|
|
684
|
+
switch (ev.type) {
|
|
685
|
+
case 'thinking': spinStart('writing…'); break;
|
|
686
|
+
case 'tool_start':
|
|
687
|
+
spinStop();
|
|
688
|
+
renderToolStart(ev.name, ev.args);
|
|
689
|
+
spinStart('working…');
|
|
690
|
+
break;
|
|
691
|
+
case 'tool_done': spinStop(); renderToolDone(ev.name, ev.summary); break;
|
|
692
|
+
case 'tool_error': spinStop(); renderToolError(ev.name, ev.error); break;
|
|
693
|
+
case 'text':
|
|
694
|
+
spinStop();
|
|
695
|
+
learnTextBuffer += (ev.content || '');
|
|
696
|
+
break;
|
|
697
|
+
case 'done': spinStop(); break;
|
|
698
|
+
case 'error': spinStop(); console.error(c.err(` ✗ ${ev.message}`)); break;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
// Text fallback: if model returned markdown content but didn't use write tag
|
|
704
|
+
if (!fs.existsSync(priorMdPath) && learnTextBuffer.length > 80) {
|
|
705
|
+
try { fs.writeFileSync(priorMdPath, learnTextBuffer, 'utf8'); } catch {}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Reload prior.md into context
|
|
709
|
+
try {
|
|
710
|
+
projectContext = fs.readFileSync(priorMdPath, 'utf8');
|
|
711
|
+
console.log(c.ok(' ✓') + c.muted(' prior.md written — context active for this session'));
|
|
712
|
+
} catch {
|
|
713
|
+
console.log(c.warn(' ⚠ prior.md was not created'));
|
|
714
|
+
}
|
|
715
|
+
} catch (err) {
|
|
716
|
+
spinStop();
|
|
717
|
+
console.error(c.err(` ✗ ${err.message}`));
|
|
718
|
+
}
|
|
719
|
+
console.log('');
|
|
720
|
+
return loop();
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
case '/usage': {
|
|
724
|
+
try {
|
|
725
|
+
const data = await api.getUsage();
|
|
726
|
+
const used = data.used ?? data.tokens_used ?? data.tokensUsed ?? data.totalTokens ?? 0;
|
|
727
|
+
const limit = data.limit ?? data.token_limit ?? data.dailyLimit ?? null;
|
|
728
|
+
const pct = limit ? Math.min(100, Math.round((used / limit) * 100)) : null;
|
|
729
|
+
console.log('');
|
|
730
|
+
if (pct !== null) {
|
|
731
|
+
console.log(` ${progressBar(pct)}`);
|
|
732
|
+
console.log(` ${c.bold(used.toLocaleString())} ${c.muted('/')} ${limit.toLocaleString()} tokens ${c.muted(`(${pct}%)`)}`);
|
|
733
|
+
} else {
|
|
734
|
+
console.log(` ${c.bold(used.toLocaleString())} tokens used today`);
|
|
735
|
+
}
|
|
736
|
+
console.log('');
|
|
737
|
+
} catch (err) {
|
|
738
|
+
console.error(c.err(` ✗ ${err.message}\n`));
|
|
739
|
+
}
|
|
740
|
+
return loop();
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
case '/help':
|
|
744
|
+
console.log('');
|
|
745
|
+
console.log(c.bold(' Commands'));
|
|
746
|
+
console.log(c.muted(' /clear ') + 'Clear screen');
|
|
747
|
+
console.log(c.muted(' /model <name> ') + 'Switch AI model');
|
|
748
|
+
console.log(c.muted(' /tools ') + 'List available tools');
|
|
749
|
+
console.log(c.muted(' /uncensored ') + 'Enable uncensored mode');
|
|
750
|
+
console.log(c.muted(' /censored ') + 'Disable uncensored mode');
|
|
751
|
+
console.log(c.muted(' /usage ') + 'Token usage for today');
|
|
752
|
+
console.log(c.muted(' /learn ') + 'Scan directory and write prior.md context file');
|
|
753
|
+
console.log(c.muted(' /login ') + 'Sign in to a different account');
|
|
754
|
+
console.log(c.muted(' /logout ') + 'Sign out');
|
|
755
|
+
console.log(c.muted(' /exit ') + 'Exit Prior');
|
|
756
|
+
console.log(c.muted(' ↑ ↓ ') + 'Browse message history');
|
|
757
|
+
console.log('');
|
|
758
|
+
console.log(c.bold(' Other commands ') + c.muted('(run outside chat)'));
|
|
759
|
+
console.log(c.muted(' prior imagine <prompt> ') + 'Generate an image');
|
|
760
|
+
console.log(c.muted(' prior models ') + 'List AI models');
|
|
761
|
+
console.log(c.muted(' prior history ') + 'Chat history');
|
|
762
|
+
console.log(c.muted(' prior usage ') + 'Token usage');
|
|
763
|
+
console.log(c.muted(' prior weather <city> ') + 'Weather');
|
|
764
|
+
console.log('');
|
|
765
|
+
return loop();
|
|
766
|
+
|
|
767
|
+
default:
|
|
768
|
+
console.log(c.err(` Unknown command: ${cmd}\n`));
|
|
769
|
+
return loop();
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// ── Send message ────────────────────────────────────────
|
|
774
|
+
msgCount++;
|
|
775
|
+
console.log('');
|
|
776
|
+
|
|
777
|
+
if (agentMode) {
|
|
778
|
+
// ── Agent mode: full tool loop ──────────────────────
|
|
779
|
+
let responseText = '';
|
|
780
|
+
let responseStarted = false;
|
|
781
|
+
let _progressStarted = false;
|
|
782
|
+
const _thinkStart = Date.now();
|
|
783
|
+
|
|
784
|
+
spinStart('thinking…');
|
|
785
|
+
|
|
786
|
+
try {
|
|
787
|
+
await api.agentChat(
|
|
788
|
+
[...chatHistory, { role: 'user', content: input }],
|
|
789
|
+
{ model: currentModel, uncensored, cwd: process.cwd(), projectContext },
|
|
790
|
+
ev => {
|
|
791
|
+
switch (ev.type) {
|
|
792
|
+
|
|
793
|
+
case 'thinking':
|
|
794
|
+
spinStart('thinking…');
|
|
795
|
+
break;
|
|
796
|
+
|
|
797
|
+
case 'tool_start':
|
|
798
|
+
spinStop();
|
|
799
|
+
_progressStarted = false;
|
|
800
|
+
renderToolStart(ev.name, ev.args);
|
|
801
|
+
spinStart('working…');
|
|
802
|
+
break;
|
|
803
|
+
|
|
804
|
+
case 'tool_progress': {
|
|
805
|
+
if (!_progressStarted) {
|
|
806
|
+
spinStop();
|
|
807
|
+
process.stdout.write('\n');
|
|
808
|
+
_progressStarted = true;
|
|
809
|
+
} else {
|
|
810
|
+
process.stdout.clearLine(0);
|
|
811
|
+
process.stdout.cursorTo(0);
|
|
812
|
+
}
|
|
813
|
+
const pct = ev.percent || 0;
|
|
814
|
+
const bar = progressBar(pct, 20);
|
|
815
|
+
process.stdout.write(` ${c.brand('◈')} ${bar} ${c.muted(`${ev.step}/${ev.total}`)} ${c.brand(`${pct}%`)}`);
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
case 'tool_done':
|
|
820
|
+
spinStop();
|
|
821
|
+
renderToolDone(ev.name, ev.summary);
|
|
822
|
+
break;
|
|
823
|
+
|
|
824
|
+
case 'tool_error':
|
|
825
|
+
spinStop();
|
|
826
|
+
renderToolError(ev.name, ev.error);
|
|
827
|
+
break;
|
|
828
|
+
|
|
829
|
+
case 'text': {
|
|
830
|
+
spinStop();
|
|
831
|
+
const rendered = renderMarkdown(ev.content);
|
|
832
|
+
const thinkTime = elapsed(Date.now() - _thinkStart);
|
|
833
|
+
console.log(c.brand(' Prior ') + c.muted(`· ${timeNow()} · ${thinkTime}`));
|
|
834
|
+
console.log('');
|
|
835
|
+
console.log(rendered);
|
|
836
|
+
responseText += ev.content;
|
|
837
|
+
responseStarted = true;
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
case 'done':
|
|
842
|
+
spinStop();
|
|
843
|
+
break;
|
|
844
|
+
|
|
845
|
+
case 'error':
|
|
846
|
+
spinStop();
|
|
847
|
+
process.stdout.write('\n');
|
|
848
|
+
console.error(c.err(` ✗ ${ev.message}`));
|
|
849
|
+
break;
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
);
|
|
853
|
+
} catch (err) {
|
|
854
|
+
spinStop();
|
|
855
|
+
console.error(c.err(` ✗ ${err.message}`));
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// Update history
|
|
859
|
+
chatHistory.push({ role: 'user', content: input });
|
|
860
|
+
if (responseText) chatHistory.push({ role: 'assistant', content: responseText });
|
|
861
|
+
|
|
862
|
+
process.stdout.write('\n');
|
|
863
|
+
|
|
864
|
+
} else {
|
|
865
|
+
// ── Basic mode: direct API, no tools ───────────────
|
|
866
|
+
process.stdout.write(c.brand(' Prior ') + c.muted('· '));
|
|
867
|
+
|
|
868
|
+
try {
|
|
869
|
+
await api.generate(input, { model: currentModel, uncensored }, chunk => {
|
|
870
|
+
process.stdout.write(chunk);
|
|
871
|
+
});
|
|
872
|
+
} catch (err) {
|
|
873
|
+
process.stdout.write('\n');
|
|
874
|
+
console.error(c.err(` ✗ ${err.message}`));
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
process.stdout.write('\n\n');
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
loop();
|
|
881
|
+
});
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
loop();
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// ── LOGIN ──────────────────────────────────────────────────────
|
|
888
|
+
program
|
|
889
|
+
.command('login')
|
|
890
|
+
.description('Log in to your Prior Network account')
|
|
891
|
+
.action(async () => {
|
|
892
|
+
banner();
|
|
893
|
+
console.log(c.bold(' Sign in\n'));
|
|
894
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
895
|
+
const username = await ask(rl, c.muted(' Username : '));
|
|
896
|
+
rl.close();
|
|
897
|
+
const password = await promptPassword(c.muted(' Password : '));
|
|
898
|
+
console.log('');
|
|
899
|
+
process.stdout.write(c.dim(' Authenticating…'));
|
|
900
|
+
try {
|
|
901
|
+
const data = await api.login(username, password);
|
|
902
|
+
saveAuth(data.token, username);
|
|
903
|
+
clearLine();
|
|
904
|
+
console.log(c.ok(' ✓ Logged in as ') + c.bold(username));
|
|
905
|
+
console.log(c.muted('\n Run "prior" to start chatting.\n'));
|
|
906
|
+
} catch (err) {
|
|
907
|
+
clearLine();
|
|
908
|
+
console.error(c.err(` ✗ ${err.message}`));
|
|
909
|
+
process.exit(1);
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
// ── LOGOUT ─────────────────────────────────────────────────────
|
|
914
|
+
program
|
|
915
|
+
.command('logout')
|
|
916
|
+
.description('Log out and clear saved credentials')
|
|
917
|
+
.action(() => {
|
|
918
|
+
clearAuth();
|
|
919
|
+
console.log(c.ok(' ✓ Logged out.'));
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
// ── WHOAMI ─────────────────────────────────────────────────────
|
|
923
|
+
program
|
|
924
|
+
.command('whoami')
|
|
925
|
+
.description('Show currently logged-in user')
|
|
926
|
+
.action(() => {
|
|
927
|
+
const user = getUsername();
|
|
928
|
+
console.log('');
|
|
929
|
+
if (!user) console.log(c.muted(' Not logged in.'));
|
|
930
|
+
else console.log(` ${c.bold(user)}`);
|
|
931
|
+
console.log('');
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
// ── CHAT ───────────────────────────────────────────────────────
|
|
935
|
+
program
|
|
936
|
+
.command('chat', { isDefault: false })
|
|
937
|
+
.description('Open Prior AI chat session (default when no command given)')
|
|
938
|
+
.option('-m, --model <model>', 'Model to use')
|
|
939
|
+
.option('-u, --uncensored', 'Enable uncensored mode')
|
|
940
|
+
.action(opts => startChat(opts));
|
|
941
|
+
|
|
942
|
+
// ── IMAGINE ────────────────────────────────────────────────────
|
|
943
|
+
program
|
|
944
|
+
.command('imagine <prompt>')
|
|
945
|
+
.description('Generate an image with Prior Diffusion')
|
|
946
|
+
.option('-s, --size <WxH>', 'Resolution e.g. 1024x1024', '896x896')
|
|
947
|
+
.option('--steps <n>', 'Diffusion steps', '20')
|
|
948
|
+
.option('--open', 'Open saved image when done')
|
|
949
|
+
.action(async (prompt, opts) => {
|
|
950
|
+
requireAuth();
|
|
951
|
+
const fs = require('fs');
|
|
952
|
+
const [width, height] = (opts.size || '896x896').split('x').map(Number);
|
|
953
|
+
const steps = parseInt(opts.steps) || 20;
|
|
954
|
+
console.log('');
|
|
955
|
+
console.log(c.bold(' Prior Diffusion'));
|
|
956
|
+
console.log(c.muted(' Prompt : ') + c.white(`"${prompt}"`));
|
|
957
|
+
console.log(c.muted(` Size : ${width}×${height} · Steps: ${steps}`));
|
|
958
|
+
console.log('');
|
|
959
|
+
try {
|
|
960
|
+
const { promptId } = await api.generateImage(prompt, { width, height, steps });
|
|
961
|
+
if (!promptId) throw new Error('No promptId returned.');
|
|
962
|
+
let done = false;
|
|
963
|
+
while (!done) {
|
|
964
|
+
await new Promise(r => setTimeout(r, 900));
|
|
965
|
+
try {
|
|
966
|
+
const p = await api.pollImageProgress(promptId);
|
|
967
|
+
clearLine();
|
|
968
|
+
process.stdout.write(` ${progressBar(p.percent ?? 0)} ${String(p.percent ?? 0).padStart(3)}%`);
|
|
969
|
+
if (p.status === 'complete' || p.filename) {
|
|
970
|
+
done = true;
|
|
971
|
+
clearLine();
|
|
972
|
+
const downloadsDir = path.join(os.homedir(), 'Downloads');
|
|
973
|
+
const savePath = path.join(downloadsDir, p.filename);
|
|
974
|
+
process.stdout.write(c.dim(' Downloading…'));
|
|
975
|
+
try {
|
|
976
|
+
const imgRes = await api.downloadImage(p.filename);
|
|
977
|
+
const buf = await imgRes.buffer();
|
|
978
|
+
fs.writeFileSync(savePath, buf);
|
|
979
|
+
clearLine();
|
|
980
|
+
console.log(c.ok(' ✓ Saved ') + c.white(savePath));
|
|
981
|
+
} catch (dlErr) {
|
|
982
|
+
clearLine();
|
|
983
|
+
console.log(c.ok(' ✓ Done!'));
|
|
984
|
+
console.log(c.warn(` ⚠ Could not save: ${dlErr.message}`));
|
|
985
|
+
}
|
|
986
|
+
console.log('');
|
|
987
|
+
if (opts.open) { const open = require('open'); await open(savePath); }
|
|
988
|
+
} else if (p.status === 'error') {
|
|
989
|
+
done = true; clearLine();
|
|
990
|
+
console.error(c.err(' ✗ Generation failed.'));
|
|
991
|
+
}
|
|
992
|
+
} catch { /* keep polling */ }
|
|
993
|
+
}
|
|
994
|
+
} catch (err) {
|
|
995
|
+
console.error(c.err(` ✗ ${err.message}`));
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
// ── MODELS ─────────────────────────────────────────────────────
|
|
1000
|
+
program
|
|
1001
|
+
.command('models')
|
|
1002
|
+
.description('List available AI models')
|
|
1003
|
+
.action(async () => {
|
|
1004
|
+
requireAuth();
|
|
1005
|
+
try {
|
|
1006
|
+
const data = await api.getModels();
|
|
1007
|
+
const models = data.models || data;
|
|
1008
|
+
console.log('');
|
|
1009
|
+
console.log(c.bold(' Available Models\n'));
|
|
1010
|
+
(Array.isArray(models) ? models : []).forEach(m => {
|
|
1011
|
+
const name = typeof m === 'string' ? m : (m.name || m.model || '');
|
|
1012
|
+
console.log(` ${c.brand('▸')} ${name}`);
|
|
1013
|
+
});
|
|
1014
|
+
console.log('');
|
|
1015
|
+
} catch (err) { console.error(c.err(` ✗ ${err.message}`)); }
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
// ── HISTORY ────────────────────────────────────────────────────
|
|
1019
|
+
program
|
|
1020
|
+
.command('history')
|
|
1021
|
+
.description('List recent chat sessions')
|
|
1022
|
+
.option('-n, --limit <n>', 'Number to show', '20')
|
|
1023
|
+
.action(async opts => {
|
|
1024
|
+
requireAuth();
|
|
1025
|
+
try {
|
|
1026
|
+
const data = await api.getChats();
|
|
1027
|
+
const chats = (data.chats || data || []).slice(0, parseInt(opts.limit));
|
|
1028
|
+
console.log('');
|
|
1029
|
+
console.log(c.bold(' Chat History\n'));
|
|
1030
|
+
if (!chats.length) { console.log(c.muted(' No chats yet.')); }
|
|
1031
|
+
else chats.forEach((ch, i) => {
|
|
1032
|
+
const title = (ch.title || 'Untitled').slice(0, 50);
|
|
1033
|
+
const msgs = ch.message_count ? c.muted(` ${ch.message_count} msgs`) : '';
|
|
1034
|
+
const date = ch.updated_at ? c.muted(' · ' + new Date(ch.updated_at).toLocaleDateString()) : '';
|
|
1035
|
+
console.log(` ${c.muted(String(i + 1).padStart(3))} ${c.white(title)}${msgs}${date}`);
|
|
1036
|
+
});
|
|
1037
|
+
console.log('');
|
|
1038
|
+
} catch (err) { console.error(c.err(` ✗ ${err.message}`)); }
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
// ── USAGE ──────────────────────────────────────────────────────
|
|
1042
|
+
program
|
|
1043
|
+
.command('usage')
|
|
1044
|
+
.description('Show token usage for today')
|
|
1045
|
+
.action(async () => {
|
|
1046
|
+
requireAuth();
|
|
1047
|
+
try {
|
|
1048
|
+
const data = await api.getUsage();
|
|
1049
|
+
const used = data.used ?? data.tokens_used ?? data.tokensUsed ?? 0;
|
|
1050
|
+
const limit = data.limit ?? data.token_limit ?? data.dailyLimit ?? null;
|
|
1051
|
+
const pct = limit ? Math.min(100, Math.round((used / limit) * 100)) : null;
|
|
1052
|
+
console.log('');
|
|
1053
|
+
console.log(c.bold(' Token Usage — Today\n'));
|
|
1054
|
+
if (pct !== null) {
|
|
1055
|
+
console.log(` ${progressBar(pct)}`);
|
|
1056
|
+
console.log(` ${c.bold(used.toLocaleString())} / ${limit.toLocaleString()} tokens ${c.muted(`(${pct}%)`)}`);
|
|
1057
|
+
} else {
|
|
1058
|
+
console.log(` ${c.bold(used.toLocaleString())} tokens used today`);
|
|
1059
|
+
}
|
|
1060
|
+
console.log('');
|
|
1061
|
+
} catch (err) { console.error(c.err(` ✗ ${err.message}`)); }
|
|
1062
|
+
});
|
|
1063
|
+
|
|
1064
|
+
// ── WEATHER ────────────────────────────────────────────────────
|
|
1065
|
+
program
|
|
1066
|
+
.command('weather <location>')
|
|
1067
|
+
.description('Get weather for a location')
|
|
1068
|
+
.action(async location => {
|
|
1069
|
+
requireAuth();
|
|
1070
|
+
try {
|
|
1071
|
+
const data = await api.getWeather(location);
|
|
1072
|
+
const pretty = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
|
|
1073
|
+
console.log('');
|
|
1074
|
+
console.log(c.bold(` Weather — ${location}\n`));
|
|
1075
|
+
console.log(' ' + pretty.split('\n').join('\n '));
|
|
1076
|
+
console.log('');
|
|
1077
|
+
} catch (err) { console.error(c.err(` ✗ ${err.message}`)); }
|
|
1078
|
+
});
|
|
1079
|
+
|
|
1080
|
+
// ── Entry point ────────────────────────────────────────────────
|
|
1081
|
+
program
|
|
1082
|
+
.name('prior')
|
|
1083
|
+
.description('Prior Network — AI command-line interface')
|
|
1084
|
+
.version(version, '-v, --version', 'Print version');
|
|
1085
|
+
|
|
1086
|
+
const args = process.argv.slice(2);
|
|
1087
|
+
|
|
1088
|
+
if (args.length === 0 || (args.length === 1 && args[0].startsWith('-') && args[0] !== '--help' && args[0] !== '-h')) {
|
|
1089
|
+
startChat();
|
|
1090
|
+
} else {
|
|
1091
|
+
program.parse(process.argv);
|
|
1092
|
+
}
|