prior-cli 1.0.0 → 1.1.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 +26 -51
- package/lib/agent.js +202 -0
- package/lib/api.js +17 -62
- package/lib/systemPrompt.js +81 -0
- package/lib/tools.js +313 -0
- package/package.json +2 -1
package/bin/prior.js
CHANGED
|
@@ -12,6 +12,7 @@ const { version } = require('../package.json');
|
|
|
12
12
|
const api = require('../lib/api');
|
|
13
13
|
const { renderMarkdown } = require('../lib/render');
|
|
14
14
|
const { getToken, getUsername, saveAuth, clearAuth } = require('../lib/config');
|
|
15
|
+
const { runAgent } = require('../lib/agent');
|
|
15
16
|
|
|
16
17
|
// ── Theme ──────────────────────────────────────────────────────
|
|
17
18
|
const THEME = '#9CE2D4';
|
|
@@ -297,7 +298,7 @@ async function loginViaBrowser() {
|
|
|
297
298
|
|
|
298
299
|
// Long-poll the CLI backend until browser completes login (3 min timeout handled server-side)
|
|
299
300
|
const fetch = require('node-fetch');
|
|
300
|
-
const res = await fetch(`
|
|
301
|
+
const res = await fetch(`https://prior.ngrok.app/cli-backend/wait?state=${state}`, {
|
|
301
302
|
timeout: 185000,
|
|
302
303
|
});
|
|
303
304
|
|
|
@@ -365,13 +366,6 @@ async function startChat(opts = {}) {
|
|
|
365
366
|
|
|
366
367
|
const user = getUsername();
|
|
367
368
|
|
|
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
369
|
console.clear();
|
|
376
370
|
banner();
|
|
377
371
|
console.log(DIVIDER);
|
|
@@ -386,12 +380,7 @@ async function startChat(opts = {}) {
|
|
|
386
380
|
c.muted(` · ${modelLabel}`)
|
|
387
381
|
);
|
|
388
382
|
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
|
-
}
|
|
383
|
+
console.log(c.ok(' ◉') + c.muted(' Agent mode ') + c.dim('· file web shell image prior-network'));
|
|
395
384
|
|
|
396
385
|
if (opts.uncensored) console.log(c.warn(' ⚠ Uncensored mode active'));
|
|
397
386
|
console.log(DIVIDER);
|
|
@@ -595,7 +584,6 @@ async function startChat(opts = {}) {
|
|
|
595
584
|
console.log(` ${c.brand('▸')} ${c.bold(name.padEnd(16))} ${c.muted(desc)}`);
|
|
596
585
|
});
|
|
597
586
|
console.log('');
|
|
598
|
-
if (!agentMode) console.log(c.warn(' ⚠ Backend offline — start prior-cli-backend to enable\n'));
|
|
599
587
|
return loop();
|
|
600
588
|
|
|
601
589
|
case '/learn': {
|
|
@@ -677,10 +665,13 @@ Keep it under 350 words. Write prior.md now.`;
|
|
|
677
665
|
let learnTextBuffer = '';
|
|
678
666
|
|
|
679
667
|
try {
|
|
680
|
-
await
|
|
681
|
-
[{ role: 'user', content: learnPrompt }],
|
|
682
|
-
|
|
683
|
-
|
|
668
|
+
await runAgent({
|
|
669
|
+
messages: [{ role: 'user', content: learnPrompt }],
|
|
670
|
+
model: currentModel,
|
|
671
|
+
uncensored,
|
|
672
|
+
cwd: process.cwd(),
|
|
673
|
+
projectContext: null,
|
|
674
|
+
send: ev => {
|
|
684
675
|
switch (ev.type) {
|
|
685
676
|
case 'thinking': spinStart('writing…'); break;
|
|
686
677
|
case 'tool_start':
|
|
@@ -697,8 +688,8 @@ Keep it under 350 words. Write prior.md now.`;
|
|
|
697
688
|
case 'done': spinStop(); break;
|
|
698
689
|
case 'error': spinStop(); console.error(c.err(` ✗ ${ev.message}`)); break;
|
|
699
690
|
}
|
|
700
|
-
}
|
|
701
|
-
);
|
|
691
|
+
},
|
|
692
|
+
});
|
|
702
693
|
|
|
703
694
|
// Text fallback: if model returned markdown content but didn't use write tag
|
|
704
695
|
if (!fs.existsSync(priorMdPath) && learnTextBuffer.length > 80) {
|
|
@@ -774,20 +765,21 @@ Keep it under 350 words. Write prior.md now.`;
|
|
|
774
765
|
msgCount++;
|
|
775
766
|
console.log('');
|
|
776
767
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
let
|
|
780
|
-
|
|
781
|
-
let _progressStarted = false;
|
|
782
|
-
const _thinkStart = Date.now();
|
|
768
|
+
{
|
|
769
|
+
let responseText = '';
|
|
770
|
+
let _progressStarted = false;
|
|
771
|
+
const _thinkStart = Date.now();
|
|
783
772
|
|
|
784
773
|
spinStart('thinking…');
|
|
785
774
|
|
|
786
775
|
try {
|
|
787
|
-
await
|
|
788
|
-
[...chatHistory, { role: 'user', content: input }],
|
|
789
|
-
|
|
790
|
-
|
|
776
|
+
await runAgent({
|
|
777
|
+
messages: [...chatHistory, { role: 'user', content: input }],
|
|
778
|
+
model: currentModel,
|
|
779
|
+
uncensored,
|
|
780
|
+
cwd: process.cwd(),
|
|
781
|
+
projectContext,
|
|
782
|
+
send: ev => {
|
|
791
783
|
switch (ev.type) {
|
|
792
784
|
|
|
793
785
|
case 'thinking':
|
|
@@ -828,13 +820,12 @@ Keep it under 350 words. Write prior.md now.`;
|
|
|
828
820
|
|
|
829
821
|
case 'text': {
|
|
830
822
|
spinStop();
|
|
831
|
-
const rendered
|
|
823
|
+
const rendered = renderMarkdown(ev.content);
|
|
832
824
|
const thinkTime = elapsed(Date.now() - _thinkStart);
|
|
833
825
|
console.log(c.brand(' Prior ') + c.muted(`· ${timeNow()} · ${thinkTime}`));
|
|
834
826
|
console.log('');
|
|
835
827
|
console.log(rendered);
|
|
836
828
|
responseText += ev.content;
|
|
837
|
-
responseStarted = true;
|
|
838
829
|
break;
|
|
839
830
|
}
|
|
840
831
|
|
|
@@ -848,33 +839,17 @@ Keep it under 350 words. Write prior.md now.`;
|
|
|
848
839
|
console.error(c.err(` ✗ ${ev.message}`));
|
|
849
840
|
break;
|
|
850
841
|
}
|
|
851
|
-
}
|
|
852
|
-
);
|
|
842
|
+
},
|
|
843
|
+
});
|
|
853
844
|
} catch (err) {
|
|
854
845
|
spinStop();
|
|
855
846
|
console.error(c.err(` ✗ ${err.message}`));
|
|
856
847
|
}
|
|
857
848
|
|
|
858
|
-
// Update history
|
|
859
849
|
chatHistory.push({ role: 'user', content: input });
|
|
860
850
|
if (responseText) chatHistory.push({ role: 'assistant', content: responseText });
|
|
861
851
|
|
|
862
852
|
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
853
|
}
|
|
879
854
|
|
|
880
855
|
loop();
|
package/lib/agent.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fetch = require('node-fetch');
|
|
4
|
+
const { executeTool, TOOL_NAMES } = require('./tools');
|
|
5
|
+
const { buildSystemPrompt } = require('./systemPrompt');
|
|
6
|
+
const { getToken, getUsername } = require('./config');
|
|
7
|
+
|
|
8
|
+
const CLI_BASE = 'https://prior.ngrok.app/cli-backend';
|
|
9
|
+
const PRIOR_BASE = 'https://prior.ngrok.app';
|
|
10
|
+
const MAX_ITER = 14;
|
|
11
|
+
|
|
12
|
+
// ── Single inference call (server just runs Ollama + returns) ─
|
|
13
|
+
|
|
14
|
+
async function infer(messages, model, token) {
|
|
15
|
+
const res = await fetch(`${CLI_BASE}/api/infer`, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: { 'Content-Type': 'application/json' },
|
|
18
|
+
body: JSON.stringify({ messages, model, token }),
|
|
19
|
+
timeout: 120000,
|
|
20
|
+
});
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
const err = await res.json().catch(() => ({}));
|
|
23
|
+
throw new Error(err.error || `Server error: HTTP ${res.status}`);
|
|
24
|
+
}
|
|
25
|
+
return await res.json(); // { content, promptTokens, completionTokens }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ── Token usage tracking ──────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
async function trackTokenUsage(token, promptTokens, completionTokens) {
|
|
31
|
+
if (!token || (!promptTokens && !completionTokens)) return;
|
|
32
|
+
try {
|
|
33
|
+
await fetch(`${PRIOR_BASE}/prior/api/user/token-usage`, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
36
|
+
body: JSON.stringify({ promptTokens, completionTokens }),
|
|
37
|
+
timeout: 5000,
|
|
38
|
+
});
|
|
39
|
+
} catch { /* non-fatal */ }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Tool call parsers (mirror server-side logic) ──────────────
|
|
43
|
+
|
|
44
|
+
function fixJsonLiterals(str) {
|
|
45
|
+
let result = '';
|
|
46
|
+
let inString = false;
|
|
47
|
+
let escaped = false;
|
|
48
|
+
for (let i = 0; i < str.length; i++) {
|
|
49
|
+
const ch = str[i];
|
|
50
|
+
if (escaped) { result += ch; escaped = false; continue; }
|
|
51
|
+
if (ch === '\\' && inString) { result += ch; escaped = true; continue; }
|
|
52
|
+
if (ch === '"') { inString = !inString; result += ch; continue; }
|
|
53
|
+
if (inString) {
|
|
54
|
+
if (ch === '\n') { result += '\\n'; continue; }
|
|
55
|
+
else if (ch === '\r') { result += '\\r'; continue; }
|
|
56
|
+
else if (ch === '\t') { result += '\\t'; continue; }
|
|
57
|
+
}
|
|
58
|
+
result += ch;
|
|
59
|
+
}
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseToolCalls(text) {
|
|
64
|
+
const calls = [];
|
|
65
|
+
|
|
66
|
+
// Primary: <tool>{"name":"X","args":{...}}</tool>
|
|
67
|
+
const re = /<tool>([\s\S]*?)<\/tool>/g;
|
|
68
|
+
let m;
|
|
69
|
+
while ((m = re.exec(text)) !== null) {
|
|
70
|
+
try {
|
|
71
|
+
const fixed = fixJsonLiterals(m[1].trim());
|
|
72
|
+
const parsed = JSON.parse(fixed);
|
|
73
|
+
if (parsed && typeof parsed.name === 'string') {
|
|
74
|
+
const { name, args, ...rest } = parsed;
|
|
75
|
+
calls.push({ raw: m[0], offset: m.index, name, args: args || (Object.keys(rest).length > 0 ? rest : {}) });
|
|
76
|
+
}
|
|
77
|
+
} catch { /* skip */ }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fallback: <tool_name>{...}</tool_name>
|
|
81
|
+
for (const name of TOOL_NAMES) {
|
|
82
|
+
const fbRe = new RegExp(`<${name}>([\\s\\S]*?)<\\/${name}>`, 'g');
|
|
83
|
+
let fm;
|
|
84
|
+
while ((fm = fbRe.exec(text)) !== null) {
|
|
85
|
+
const alreadyCaptured = calls.some(c => fm.index >= c.offset && fm.index < c.offset + c.raw.length);
|
|
86
|
+
if (alreadyCaptured) continue;
|
|
87
|
+
try {
|
|
88
|
+
const fixed = fixJsonLiterals(fm[1].trim());
|
|
89
|
+
const parsed = JSON.parse(fixed);
|
|
90
|
+
const { args, ...rest } = parsed || {};
|
|
91
|
+
calls.push({ raw: fm[0], offset: fm.index, name, args: args || (parsed && Object.keys(rest).length > 0 ? rest : {}) });
|
|
92
|
+
} catch { /* skip */ }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return calls;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function parseWriteTags(text) {
|
|
100
|
+
const calls = [];
|
|
101
|
+
for (const { tag, name } of [
|
|
102
|
+
{ tag: 'write', name: 'file_write' },
|
|
103
|
+
{ tag: 'append', name: 'file_append' },
|
|
104
|
+
]) {
|
|
105
|
+
const re = new RegExp(`<${tag}\\s+path="([^"]+)"[^>]*>([\\s\\S]*?)<\\/${tag}>`, 'g');
|
|
106
|
+
let m;
|
|
107
|
+
while ((m = re.exec(text)) !== null) {
|
|
108
|
+
calls.push({ raw: m[0], offset: m.index, name, args: { path: m[1], content: m[2] } });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// <docx path="..." title="...">content</docx>
|
|
112
|
+
const docxRe = /<docx\s+path="([^"]+)"(?:\s+title="([^"]*)")?[^>]*>([\s\S]*?)<\/docx>/g;
|
|
113
|
+
let m;
|
|
114
|
+
while ((m = docxRe.exec(text)) !== null) {
|
|
115
|
+
calls.push({ raw: m[0], offset: m.index, name: 'file_write_docx', args: { path: m[1], title: m[2] || undefined, content: m[3] } });
|
|
116
|
+
}
|
|
117
|
+
return calls;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function stripThink(text) {
|
|
121
|
+
return text.replace(/<think>[\s\S]*?<\/think>/gi, '').trim();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Main agent loop ───────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
async function runAgent({ messages, model, uncensored, cwd, projectContext, send }) {
|
|
127
|
+
const token = getToken();
|
|
128
|
+
const username = getUsername() || 'user';
|
|
129
|
+
const sysPrompt = buildSystemPrompt(username, cwd, uncensored, projectContext);
|
|
130
|
+
|
|
131
|
+
const history = [
|
|
132
|
+
{ role: 'system', content: sysPrompt },
|
|
133
|
+
...messages,
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
let totalPromptTokens = 0;
|
|
137
|
+
let totalCompletionTokens = 0;
|
|
138
|
+
|
|
139
|
+
for (let iter = 0; iter < MAX_ITER; iter++) {
|
|
140
|
+
|
|
141
|
+
send({ type: 'thinking' });
|
|
142
|
+
|
|
143
|
+
let result;
|
|
144
|
+
try {
|
|
145
|
+
result = await infer(history, model || 'qwen3.5:4b', token);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
await trackTokenUsage(token, totalPromptTokens, totalCompletionTokens);
|
|
148
|
+
send({ type: 'error', message: err.message });
|
|
149
|
+
send({ type: 'done' });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
totalPromptTokens += result.promptTokens || 0;
|
|
154
|
+
totalCompletionTokens += result.completionTokens || 0;
|
|
155
|
+
|
|
156
|
+
const raw = result.content;
|
|
157
|
+
const cleaned = stripThink(raw)
|
|
158
|
+
.replace(/<tool_result[\s\S]*?<\/tool_result>/gi, '')
|
|
159
|
+
.trim();
|
|
160
|
+
|
|
161
|
+
const calls = [
|
|
162
|
+
...parseToolCalls(cleaned),
|
|
163
|
+
...parseWriteTags(cleaned),
|
|
164
|
+
].sort((a, b) => a.offset - b.offset);
|
|
165
|
+
|
|
166
|
+
// ── No tool calls → final answer ──────────────────────────
|
|
167
|
+
if (calls.length === 0) {
|
|
168
|
+
await trackTokenUsage(token, totalPromptTokens, totalCompletionTokens);
|
|
169
|
+
send({ type: 'text', content: cleaned });
|
|
170
|
+
send({ type: 'done' });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ── Text before first tool call ───────────────────────────
|
|
175
|
+
const textBefore = cleaned.slice(0, calls[0].offset).trim();
|
|
176
|
+
if (textBefore) send({ type: 'text', content: textBefore });
|
|
177
|
+
|
|
178
|
+
history.push({ role: 'assistant', content: raw });
|
|
179
|
+
|
|
180
|
+
// ── Execute each tool locally ─────────────────────────────
|
|
181
|
+
const resultParts = [];
|
|
182
|
+
for (const call of calls) {
|
|
183
|
+
send({ type: 'tool_start', name: call.name, args: call.args });
|
|
184
|
+
try {
|
|
185
|
+
const toolResult = await executeTool(call.name, call.args, { cwd, token, send });
|
|
186
|
+
send({ type: 'tool_done', name: call.name, summary: toolResult.summary });
|
|
187
|
+
resultParts.push(`<tool_result name="${call.name}">\n${toolResult.output}\n</tool_result>`);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
send({ type: 'tool_error', name: call.name, error: err.message });
|
|
190
|
+
resultParts.push(`<tool_result name="${call.name}">\nERROR: ${err.message}\n</tool_result>`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
history.push({ role: 'user', content: resultParts.join('\n\n') });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
await trackTokenUsage(token, totalPromptTokens, totalCompletionTokens);
|
|
198
|
+
send({ type: 'error', message: 'Reached maximum tool iterations.' });
|
|
199
|
+
send({ type: 'done' });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = { runAgent };
|
package/lib/api.js
CHANGED
|
@@ -6,6 +6,22 @@ const { getToken } = require('./config');
|
|
|
6
6
|
const BASE = 'https://prior.ngrok.app';
|
|
7
7
|
const CLI_BASE = 'https://prior.ngrok.app/cli-backend';
|
|
8
8
|
|
|
9
|
+
// ── Infer — single AI call, returns { content, promptTokens, completionTokens }
|
|
10
|
+
|
|
11
|
+
async function infer(messages, model, token) {
|
|
12
|
+
const res = await fetch(`${CLI_BASE}/api/infer`, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: { 'Content-Type': 'application/json' },
|
|
15
|
+
body: JSON.stringify({ messages, model, token }),
|
|
16
|
+
timeout: 120000,
|
|
17
|
+
});
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
const err = await res.json().catch(() => ({}));
|
|
20
|
+
throw new Error(err.error || `Server error: HTTP ${res.status}`);
|
|
21
|
+
}
|
|
22
|
+
return await res.json();
|
|
23
|
+
}
|
|
24
|
+
|
|
9
25
|
function authHeaders(extra = {}) {
|
|
10
26
|
const token = getToken();
|
|
11
27
|
const h = { 'Content-Type': 'application/json', ...extra };
|
|
@@ -26,66 +42,6 @@ async function login(username, password) {
|
|
|
26
42
|
return data;
|
|
27
43
|
}
|
|
28
44
|
|
|
29
|
-
// ── CLI Backend ───────────────────────────────────────────────
|
|
30
|
-
|
|
31
|
-
async function checkBackend() {
|
|
32
|
-
try {
|
|
33
|
-
const res = await fetch(`${CLI_BASE}/health`, { timeout: 3000 });
|
|
34
|
-
if (!res.ok) return null;
|
|
35
|
-
return await res.json();
|
|
36
|
-
} catch {
|
|
37
|
-
return null;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Agent chat — SSE stream. Calls onEvent(event) for each SSE event.
|
|
42
|
-
// Events: { type: 'thinking' | 'tool_start' | 'tool_done' | 'tool_error' |
|
|
43
|
-
// 'response_start' | 'response_pause' | 'text' | 'error' | 'done' }
|
|
44
|
-
async function agentChat(messages, opts = {}, onEvent) {
|
|
45
|
-
const token = getToken();
|
|
46
|
-
const res = await fetch(`${CLI_BASE}/api/chat`, {
|
|
47
|
-
method: 'POST',
|
|
48
|
-
headers: { 'Content-Type': 'application/json' },
|
|
49
|
-
body: JSON.stringify({
|
|
50
|
-
messages,
|
|
51
|
-
model: opts.model || undefined,
|
|
52
|
-
uncensored: opts.uncensored || false,
|
|
53
|
-
cwd: opts.cwd || process.cwd(),
|
|
54
|
-
projectContext: opts.projectContext || undefined,
|
|
55
|
-
token,
|
|
56
|
-
}),
|
|
57
|
-
timeout: 300000,
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
if (!res.ok) {
|
|
61
|
-
const err = await res.json().catch(() => ({}));
|
|
62
|
-
throw new Error(err.error || `HTTP ${res.status}`);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return new Promise((resolve, reject) => {
|
|
66
|
-
let buf = '';
|
|
67
|
-
|
|
68
|
-
res.body.on('data', chunk => {
|
|
69
|
-
buf += chunk.toString();
|
|
70
|
-
const lines = buf.split('\n');
|
|
71
|
-
buf = lines.pop();
|
|
72
|
-
|
|
73
|
-
for (const line of lines) {
|
|
74
|
-
if (!line.startsWith('data: ')) continue;
|
|
75
|
-
const raw = line.slice(6).trim();
|
|
76
|
-
if (!raw) continue;
|
|
77
|
-
try {
|
|
78
|
-
const event = JSON.parse(raw);
|
|
79
|
-
onEvent(event);
|
|
80
|
-
if (event.type === 'done' || event.type === 'error') resolve();
|
|
81
|
-
} catch { /* skip malformed SSE */ }
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
res.body.on('end', resolve);
|
|
86
|
-
res.body.on('error', reject);
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
45
|
|
|
90
46
|
// ── Direct AI (fallback, no agent) ───────────────────────────
|
|
91
47
|
|
|
@@ -236,8 +192,7 @@ async function search(query) {
|
|
|
236
192
|
}
|
|
237
193
|
|
|
238
194
|
module.exports = {
|
|
239
|
-
login,
|
|
240
|
-
checkBackend, agentChat,
|
|
195
|
+
login, infer,
|
|
241
196
|
generate,
|
|
242
197
|
generateImage, pollImageProgress, downloadImage,
|
|
243
198
|
getModels, getChats, getUsage, getWeather, search,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function buildSystemPrompt(username, cwd, uncensored, projectContext) {
|
|
4
|
+
const date = new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
|
5
|
+
|
|
6
|
+
return `You are Prior, an AI assistant created by Niceguygamer (Euwin). Be direct, confident, and helpful. Never say "according to my database" or "based on my training data."
|
|
7
|
+
|
|
8
|
+
IMPORTANT: Do NOT assume the person talking to you is Euwin. Euwin is the creator/developer — the user may be anyone.
|
|
9
|
+
|
|
10
|
+
PERSONALITY: Dry wit, genuine character. One short off-script remark when it genuinely fits — don't force it.
|
|
11
|
+
|
|
12
|
+
Origins: Started as a school bot for the BEN friend group. Originally a Jollibee prank bot.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
Signed-in user: @${username} | CWD: ${cwd} | ${date}
|
|
17
|
+
|
|
18
|
+
## Tools
|
|
19
|
+
|
|
20
|
+
CRITICAL: When you need to use a tool, you MUST output the tool call immediately. Do NOT say "I'll write that" or "Writing that now..." — just call the tool directly. Describing an action instead of doing it is wrong.
|
|
21
|
+
|
|
22
|
+
Correct tool call format — output this exactly, on its own line:
|
|
23
|
+
<tool>{"name":"TOOL_NAME","args":{"key":"value"}}</tool>
|
|
24
|
+
|
|
25
|
+
You will then receive:
|
|
26
|
+
<tool_result name="TOOL_NAME">result</tool_result>
|
|
27
|
+
|
|
28
|
+
### Writing / Appending Files (ALWAYS use this format — never put file content in JSON)
|
|
29
|
+
<write path="relative/or/C:/absolute/path.ext">
|
|
30
|
+
file content goes here — no escaping needed
|
|
31
|
+
</write>
|
|
32
|
+
|
|
33
|
+
<append path="path/to/file.txt">
|
|
34
|
+
content to add
|
|
35
|
+
</append>
|
|
36
|
+
|
|
37
|
+
### Other Tools (JSON format)
|
|
38
|
+
<tool>{"name":"TOOL_NAME","args":{"key":"value"}}</tool>
|
|
39
|
+
|
|
40
|
+
Available tools:
|
|
41
|
+
- file_read args: {"path":"string"}
|
|
42
|
+
- file_list args: {"path":"string"}
|
|
43
|
+
- file_delete args: {"path":"string"}
|
|
44
|
+
- file_write_docx — Word document. Use this tag format (NEVER put content in JSON):
|
|
45
|
+
<docx path="C:/path/to/file.docx" title="Optional Title">
|
|
46
|
+
# Heading 1
|
|
47
|
+
## Heading 2
|
|
48
|
+
Normal paragraph with **bold** and *italic* supported.
|
|
49
|
+
</docx>
|
|
50
|
+
- web_search args: {"query":"string"}
|
|
51
|
+
- url_fetch args: {"url":"string"}
|
|
52
|
+
- run_command args: {"command":"string"}
|
|
53
|
+
- clipboard_read args: {}
|
|
54
|
+
- clipboard_write args: {"text":"string"}
|
|
55
|
+
- generate_image args: {"prompt":"string","width":896,"height":896,"steps":20}
|
|
56
|
+
- prior_feed args: {}
|
|
57
|
+
- prior_profile args: {}
|
|
58
|
+
|
|
59
|
+
### Image generation notes
|
|
60
|
+
- generate_image queues and polls automatically — it can take 1-3 minutes, this is normal
|
|
61
|
+
- Always confirm where the image was saved after it completes
|
|
62
|
+
- If user says "generate X" or "create an image of X", use generate_image immediately
|
|
63
|
+
- Image saves to the current working directory by default; user can ask to move it after
|
|
64
|
+
- When calling generate_image, extract only the core subject/description the user gave. Strip conversational filler ("can you", "please", "I want", "make me", "draw me", etc.). Do NOT add style tags, quality boosters, or extra descriptors like "high detail", "cinematic", "4k", "in motion", "sleek", etc. Pass only what the user described.
|
|
65
|
+
Examples:
|
|
66
|
+
"generate car" → prompt: "car"
|
|
67
|
+
"can you make me a sunset" → prompt: "sunset"
|
|
68
|
+
"draw a cat sitting on a roof" → prompt: "cat sitting on a roof"
|
|
69
|
+
"I want to see a dragon flying" → prompt: "dragon flying"
|
|
70
|
+
|
|
71
|
+
${projectContext ? `### Project Context (prior.md)\n${projectContext}\n` : ''}### Rules
|
|
72
|
+
- ACT with tools, don't narrate. If asked to create a file — create it immediately.
|
|
73
|
+
- If the user asks for a Word document (.docx), use the <docx> tag format above — NEVER use file_write for .docx.
|
|
74
|
+
- For file_write / file_append ALWAYS use the <write>/<append> tags above — NEVER put file content inside JSON.
|
|
75
|
+
- Paths relative to CWD unless absolute (e.g. C:/Users/ADMIN/Downloads/file.py)
|
|
76
|
+
- After tool results, give a short confirmation
|
|
77
|
+
- Never output <tool_result> yourself — that comes from the system
|
|
78
|
+
${uncensored ? '- Uncensored mode active\n' : ''}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { buildSystemPrompt };
|
package/lib/tools.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { exec } = require('child_process');
|
|
6
|
+
const fetch = require('node-fetch');
|
|
7
|
+
|
|
8
|
+
const CLI_BASE = 'https://prior.ngrok.app/cli-backend';
|
|
9
|
+
const PRIOR_BASE = 'https://prior.ngrok.app';
|
|
10
|
+
const MAX_FILE_SIZE = 500 * 1024; // 500 KB
|
|
11
|
+
|
|
12
|
+
const BLOCKED_PATTERNS = [
|
|
13
|
+
'rm -rf /',
|
|
14
|
+
'del /s /q c:\\',
|
|
15
|
+
'format c:',
|
|
16
|
+
'fdisk',
|
|
17
|
+
':(){:|:&};:',
|
|
18
|
+
'dd if=',
|
|
19
|
+
'mkfs',
|
|
20
|
+
'bcdedit /delete',
|
|
21
|
+
'reg delete hklm',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function resolvePath(inputPath, cwd) {
|
|
25
|
+
if (!inputPath) return cwd;
|
|
26
|
+
if (/^[a-zA-Z]:[/\\]/.test(inputPath) || path.isAbsolute(inputPath)) return inputPath;
|
|
27
|
+
return path.resolve(cwd, inputPath);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function execAsync(command, opts = {}) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const lower = command.toLowerCase();
|
|
33
|
+
for (const pattern of BLOCKED_PATTERNS) {
|
|
34
|
+
if (lower.includes(pattern)) return reject(new Error(`Blocked: command contains "${pattern}"`));
|
|
35
|
+
}
|
|
36
|
+
exec(command, { timeout: 20000, maxBuffer: 4 * 1024 * 1024, ...opts }, (err, stdout, stderr) => {
|
|
37
|
+
if (err && !stdout && !stderr) return reject(new Error(err.message));
|
|
38
|
+
resolve({ stdout: (stdout || '').trim(), stderr: (stderr || '').trim() });
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatSize(bytes) {
|
|
44
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
45
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
46
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Tool implementations ──────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
const TOOLS = {
|
|
52
|
+
|
|
53
|
+
async file_read({ path: filePath }, { cwd }) {
|
|
54
|
+
if (!filePath) throw new Error('"path" is required');
|
|
55
|
+
const resolved = resolvePath(filePath, cwd);
|
|
56
|
+
if (!fs.existsSync(resolved)) throw new Error(`Not found: ${filePath}`);
|
|
57
|
+
const stat = fs.statSync(resolved);
|
|
58
|
+
if (stat.isDirectory()) throw new Error(`"${filePath}" is a directory — use file_list`);
|
|
59
|
+
if (stat.size > MAX_FILE_SIZE) throw new Error(`File too large (${formatSize(stat.size)}, max 500KB)`);
|
|
60
|
+
const content = fs.readFileSync(resolved, 'utf8');
|
|
61
|
+
return {
|
|
62
|
+
output: content,
|
|
63
|
+
summary: `${content.split('\n').length} lines · ${formatSize(stat.size)}`,
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
async file_write({ path: filePath, content }, { cwd }) {
|
|
68
|
+
if (!filePath) throw new Error('"path" is required');
|
|
69
|
+
if (content === undefined) throw new Error('"content" is required');
|
|
70
|
+
const resolved = resolvePath(filePath, cwd);
|
|
71
|
+
const dir = path.dirname(resolved);
|
|
72
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
73
|
+
fs.writeFileSync(resolved, content, 'utf8');
|
|
74
|
+
const bytes = Buffer.byteLength(content, 'utf8');
|
|
75
|
+
return {
|
|
76
|
+
output: `Written: ${filePath} (${formatSize(bytes)})`,
|
|
77
|
+
summary: `${formatSize(bytes)} → ${path.basename(filePath)}`,
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
async file_append({ path: filePath, content }, { cwd }) {
|
|
82
|
+
if (!filePath) throw new Error('"path" is required');
|
|
83
|
+
if (content === undefined) throw new Error('"content" is required');
|
|
84
|
+
const resolved = resolvePath(filePath, cwd);
|
|
85
|
+
fs.appendFileSync(resolved, content, 'utf8');
|
|
86
|
+
const bytes = Buffer.byteLength(content, 'utf8');
|
|
87
|
+
return {
|
|
88
|
+
output: `Appended ${formatSize(bytes)} to ${filePath}`,
|
|
89
|
+
summary: `+${formatSize(bytes)} to ${path.basename(filePath)}`,
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
async file_list({ path: dirPath = '.' }, { cwd }) {
|
|
94
|
+
const resolved = resolvePath(dirPath, cwd);
|
|
95
|
+
if (!fs.existsSync(resolved)) throw new Error(`Not found: ${dirPath}`);
|
|
96
|
+
if (!fs.statSync(resolved).isDirectory()) throw new Error(`Not a directory: ${dirPath}`);
|
|
97
|
+
const entries = fs.readdirSync(resolved, { withFileTypes: true });
|
|
98
|
+
const lines = entries.map(e => {
|
|
99
|
+
if (e.isDirectory()) return ` 📁 ${e.name}/`;
|
|
100
|
+
try {
|
|
101
|
+
const size = fs.statSync(path.join(resolved, e.name)).size;
|
|
102
|
+
return ` 📄 ${e.name} ${formatSize(size)}`;
|
|
103
|
+
} catch {
|
|
104
|
+
return ` 📄 ${e.name}`;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
output: lines.join('\n') || '(empty directory)',
|
|
109
|
+
summary: `${entries.length} items in ${dirPath}`,
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
async file_delete({ path: filePath }, { cwd }) {
|
|
114
|
+
if (!filePath) throw new Error('"path" is required');
|
|
115
|
+
const resolved = resolvePath(filePath, cwd);
|
|
116
|
+
if (!fs.existsSync(resolved)) throw new Error(`Not found: ${filePath}`);
|
|
117
|
+
if (fs.statSync(resolved).isDirectory()) throw new Error('Use run_command to remove directories');
|
|
118
|
+
fs.unlinkSync(resolved);
|
|
119
|
+
return {
|
|
120
|
+
output: `Deleted: ${filePath}`,
|
|
121
|
+
summary: path.basename(filePath),
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
async file_write_docx({ path: filePath, content, title }, { cwd }) {
|
|
126
|
+
if (!filePath) throw new Error('"path" is required');
|
|
127
|
+
if (content === undefined) throw new Error('"content" is required');
|
|
128
|
+
const { Document, Packer, Paragraph, TextRun, HeadingLevel } = require('docx');
|
|
129
|
+
const resolved = resolvePath(filePath, cwd);
|
|
130
|
+
const dir = path.dirname(resolved);
|
|
131
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
132
|
+
|
|
133
|
+
const lines = content.split('\n');
|
|
134
|
+
const children = [];
|
|
135
|
+
if (title) children.push(new Paragraph({ text: title, heading: HeadingLevel.TITLE }));
|
|
136
|
+
|
|
137
|
+
for (const line of lines) {
|
|
138
|
+
if (line.startsWith('# ')) children.push(new Paragraph({ text: line.slice(2), heading: HeadingLevel.HEADING_1 }));
|
|
139
|
+
else if (line.startsWith('## ')) children.push(new Paragraph({ text: line.slice(3), heading: HeadingLevel.HEADING_2 }));
|
|
140
|
+
else if (line.startsWith('### ')) children.push(new Paragraph({ text: line.slice(4), heading: HeadingLevel.HEADING_3 }));
|
|
141
|
+
else if (line.trim() === '') children.push(new Paragraph({ text: '' }));
|
|
142
|
+
else {
|
|
143
|
+
const runs = [];
|
|
144
|
+
const parts = line.split(/(\*\*[^*]+\*\*|\*[^*]+\*)/g);
|
|
145
|
+
for (const part of parts) {
|
|
146
|
+
if (part.startsWith('**') && part.endsWith('**')) runs.push(new TextRun({ text: part.slice(2, -2), bold: true }));
|
|
147
|
+
else if (part.startsWith('*') && part.endsWith('*')) runs.push(new TextRun({ text: part.slice(1, -1), italics: true }));
|
|
148
|
+
else if (part) runs.push(new TextRun({ text: part }));
|
|
149
|
+
}
|
|
150
|
+
children.push(new Paragraph({ children: runs }));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const doc = new Document({ sections: [{ children }] });
|
|
155
|
+
const buffer = await Packer.toBuffer(doc);
|
|
156
|
+
fs.writeFileSync(resolved, buffer);
|
|
157
|
+
return {
|
|
158
|
+
output: `Word document saved: ${filePath} (${formatSize(buffer.length)})`,
|
|
159
|
+
summary: `${formatSize(buffer.length)} → ${path.basename(filePath)}`,
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
async web_search({ query }, { token }) {
|
|
164
|
+
if (!query) throw new Error('"query" is required');
|
|
165
|
+
const res = await fetch(`${CLI_BASE}/api/web-search`, {
|
|
166
|
+
method: 'POST',
|
|
167
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
168
|
+
body: JSON.stringify({ query }),
|
|
169
|
+
timeout: 15000,
|
|
170
|
+
});
|
|
171
|
+
if (!res.ok) throw new Error(`Web search error: HTTP ${res.status}`);
|
|
172
|
+
const data = await res.json();
|
|
173
|
+
if (!data.items || !data.items.length) return { output: 'No results found.', summary: '0 results' };
|
|
174
|
+
const results = data.items.map(item => {
|
|
175
|
+
const parts = [`**${item.title}**`, item.link];
|
|
176
|
+
if (item.snippet) parts.push(item.snippet);
|
|
177
|
+
return parts.join('\n');
|
|
178
|
+
});
|
|
179
|
+
return {
|
|
180
|
+
output: results.join('\n\n'),
|
|
181
|
+
summary: `${results.length} results for "${query}"`,
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
async url_fetch({ url }, { token }) {
|
|
186
|
+
if (!url) throw new Error('"url" is required');
|
|
187
|
+
const res = await fetch(`${CLI_BASE}/api/url-fetch`, {
|
|
188
|
+
method: 'POST',
|
|
189
|
+
headers: {
|
|
190
|
+
'Content-Type': 'application/json',
|
|
191
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
192
|
+
},
|
|
193
|
+
body: JSON.stringify({ url }),
|
|
194
|
+
timeout: 30000,
|
|
195
|
+
});
|
|
196
|
+
if (!res.ok) {
|
|
197
|
+
const err = await res.json().catch(() => ({}));
|
|
198
|
+
throw new Error(err.error || `HTTP ${res.status}`);
|
|
199
|
+
}
|
|
200
|
+
const data = await res.json();
|
|
201
|
+
const hostname = (() => { try { return new URL(url).hostname; } catch { return url; } })();
|
|
202
|
+
const content = data.content || data.description || '(no readable content)';
|
|
203
|
+
const titleStr = data.title ? `# ${data.title}\n\n` : '';
|
|
204
|
+
return {
|
|
205
|
+
output: (titleStr + content).slice(0, 8000),
|
|
206
|
+
summary: `${content.length} chars from ${hostname}`,
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
async clipboard_read() {
|
|
211
|
+
const { stdout } = await execAsync('powershell -command "Get-Clipboard"');
|
|
212
|
+
return {
|
|
213
|
+
output: stdout || '(clipboard is empty)',
|
|
214
|
+
summary: `${stdout.length} chars from clipboard`,
|
|
215
|
+
};
|
|
216
|
+
},
|
|
217
|
+
|
|
218
|
+
async clipboard_write({ text }) {
|
|
219
|
+
if (!text && text !== '') throw new Error('"text" is required');
|
|
220
|
+
const escaped = text.replace(/'/g, "''");
|
|
221
|
+
await execAsync(`powershell -command "Set-Clipboard -Value '${escaped}'"`);
|
|
222
|
+
return {
|
|
223
|
+
output: `Copied to clipboard (${text.length} chars)`,
|
|
224
|
+
summary: `${text.length} chars copied`,
|
|
225
|
+
};
|
|
226
|
+
},
|
|
227
|
+
|
|
228
|
+
async generate_image({ prompt, width, height, steps }, { cwd, token }) {
|
|
229
|
+
if (!prompt) throw new Error('"prompt" is required');
|
|
230
|
+
// Server handles queuing, polling, and watermarking — returns base64
|
|
231
|
+
const res = await fetch(`${CLI_BASE}/api/generate-image`, {
|
|
232
|
+
method: 'POST',
|
|
233
|
+
headers: {
|
|
234
|
+
'Content-Type': 'application/json',
|
|
235
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
236
|
+
},
|
|
237
|
+
body: JSON.stringify({ prompt, width: width || 896, height: height || 896, steps: steps || 20 }),
|
|
238
|
+
timeout: 360000,
|
|
239
|
+
});
|
|
240
|
+
if (!res.ok) {
|
|
241
|
+
const err = await res.json().catch(() => ({}));
|
|
242
|
+
throw new Error(err.error || err.message || `HTTP ${res.status}`);
|
|
243
|
+
}
|
|
244
|
+
const data = await res.json();
|
|
245
|
+
if (!data.filename || !data.data) throw new Error('Invalid response from image generation service');
|
|
246
|
+
const savePath = path.join(cwd, data.filename);
|
|
247
|
+
fs.writeFileSync(savePath, Buffer.from(data.data, 'base64'));
|
|
248
|
+
return {
|
|
249
|
+
output: `Image saved to: ${savePath}`,
|
|
250
|
+
summary: savePath,
|
|
251
|
+
};
|
|
252
|
+
},
|
|
253
|
+
|
|
254
|
+
async run_command({ command }, { cwd }) {
|
|
255
|
+
if (!command) throw new Error('"command" is required');
|
|
256
|
+
const { stdout, stderr } = await execAsync(`cd /d "${cwd}" && ${command}`);
|
|
257
|
+
const output = [stdout, stderr].filter(Boolean).join('\n') || '(no output)';
|
|
258
|
+
return {
|
|
259
|
+
output,
|
|
260
|
+
summary: command.length > 60 ? command.slice(0, 57) + '…' : command,
|
|
261
|
+
};
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
async prior_feed({}, { token }) {
|
|
265
|
+
const res = await fetch(`${PRIOR_BASE}/network/api/posts`, {
|
|
266
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
267
|
+
timeout: 8000,
|
|
268
|
+
});
|
|
269
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
270
|
+
const data = await res.json();
|
|
271
|
+
const posts = (data.posts || data || []).slice(0, 8);
|
|
272
|
+
if (!posts.length) return { output: 'No posts found.', summary: '0 posts' };
|
|
273
|
+
const lines = posts.map(p => {
|
|
274
|
+
const date = p.created_at ? new Date(p.created_at).toLocaleDateString() : '';
|
|
275
|
+
return `@${p.username || '?'} ${date}\n${(p.content || '').slice(0, 200)}`;
|
|
276
|
+
});
|
|
277
|
+
return {
|
|
278
|
+
output: lines.join('\n\n─────────────────────\n\n'),
|
|
279
|
+
summary: `${posts.length} posts`,
|
|
280
|
+
};
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
async prior_profile({}, { token }) {
|
|
284
|
+
const res = await fetch(`${PRIOR_BASE}/network/api/profile`, {
|
|
285
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
286
|
+
timeout: 8000,
|
|
287
|
+
});
|
|
288
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
289
|
+
const data = await res.json();
|
|
290
|
+
const u = data.user || data;
|
|
291
|
+
return {
|
|
292
|
+
output: [
|
|
293
|
+
`Username : @${u.username || '?'}`,
|
|
294
|
+
`Display Name : ${u.display_name || u.username || '?'}`,
|
|
295
|
+
`Bio : ${u.bio || '(none)'}`,
|
|
296
|
+
`Posts : ${u.post_count ?? 0}`,
|
|
297
|
+
`Friends : ${u.friend_count ?? 0}`,
|
|
298
|
+
`Joined : ${u.created_at ? new Date(u.created_at).toLocaleDateString() : 'unknown'}`,
|
|
299
|
+
].join('\n'),
|
|
300
|
+
summary: `@${u.username || '?'}`,
|
|
301
|
+
};
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
async function executeTool(name, args, context) {
|
|
306
|
+
const fn = TOOLS[name];
|
|
307
|
+
if (!fn) throw new Error(`Unknown tool: "${name}"`);
|
|
308
|
+
return await fn(args || {}, context);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const TOOL_NAMES = Object.keys(TOOLS);
|
|
312
|
+
|
|
313
|
+
module.exports = { executeTool, TOOL_NAMES };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prior-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Prior Network AI — command-line interface",
|
|
5
5
|
"bin": {
|
|
6
6
|
"prior": "bin/prior.js"
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"chalk": "^4.1.2",
|
|
13
13
|
"commander": "^11.1.0",
|
|
14
|
+
"docx": "^9.6.1",
|
|
14
15
|
"node-fetch": "^2.7.0",
|
|
15
16
|
"open": "^8.4.2"
|
|
16
17
|
},
|