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 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(`http://127.0.0.1:7100/wait?state=${state}`, {
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 api.agentChat(
681
- [{ role: 'user', content: learnPrompt }],
682
- { model: currentModel, uncensored, cwd: process.cwd() },
683
- ev => {
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
- if (agentMode) {
778
- // ── Agent mode: full tool loop ──────────────────────
779
- let responseText = '';
780
- let responseStarted = false;
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 api.agentChat(
788
- [...chatHistory, { role: 'user', content: input }],
789
- { model: currentModel, uncensored, cwd: process.cwd(), projectContext },
790
- ev => {
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 = renderMarkdown(ev.content);
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.0.0",
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
  },