prior-cli 1.7.9 → 1.7.11

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
@@ -169,6 +169,16 @@ const TOOL_HINTS = [
169
169
  ],
170
170
  hint: '[TOOL DIRECTIVE: You MUST call ip_lookup]',
171
171
  },
172
+ {
173
+ tool: 'file_edit',
174
+ patterns: [
175
+ /\bedit\b/i, /\bmodify\b/i, /\bchange\b.*\b(file|function|line|code|return|value|variable)\b/i,
176
+ /\brefactor\b/i, /\brename\b.*\b(function|variable|method)\b/i,
177
+ /\bfix\b.*\b(bug|file|code|function|typo)\b/i, /\breplace\b.*\b(in|line|text|code)\b/i,
178
+ /\bupdate\b.*\b(file|function|code|line)\b/i,
179
+ ],
180
+ hint: '[TOOL DIRECTIVE: To change part of an existing file, you MUST use the <edit path="..."> tag with SEARCH/REPLACE markers — do NOT rewrite the whole file with <write>, and never say you cannot edit files]',
181
+ },
172
182
  {
173
183
  tool: 'generate_image',
174
184
  patterns: [
@@ -191,6 +201,35 @@ function injectToolHint(text) {
191
201
  return text;
192
202
  }
193
203
 
204
+ // ── @file context attachment ───────────────────────────────────
205
+ // Expands @path/to/file references in a prompt into inline file context,
206
+ // so the model sees the contents without a separate file_read round-trip.
207
+ const MAX_ATTACH_BYTES = 256 * 1024;
208
+ function expandFileRefs(input, cwd) {
209
+ const refRe = /(?:^|\s)@([^\s]+)/g;
210
+ const attached = [];
211
+ const seen = new Set();
212
+ let m;
213
+ while ((m = refRe.exec(input)) !== null) {
214
+ let ref = m[1].replace(/[.,;:)\]]+$/, ''); // trim trailing punctuation
215
+ if (!ref || seen.has(ref)) continue;
216
+ const resolved = (/^[a-zA-Z]:[/\\]/.test(ref) || path.isAbsolute(ref)) ? ref : path.resolve(cwd, ref);
217
+ try {
218
+ const stat = fs.statSync(resolved);
219
+ if (!stat.isFile() || stat.size > MAX_ATTACH_BYTES) continue;
220
+ const content = fs.readFileSync(resolved, 'utf8');
221
+ if (content.indexOf('\x00') !== -1) continue; // binary
222
+ attached.push({ ref, content, lines: content.split('\n').length });
223
+ seen.add(ref);
224
+ } catch { /* not a readable file — leave the @token as literal text */ }
225
+ }
226
+ if (!attached.length) return { message: input, attached };
227
+ const ctx = attached
228
+ .map(a => `--- Contents of ${a.ref} ---\n${a.content}`)
229
+ .join('\n\n');
230
+ return { message: `${input}\n\n[Attached file context]\n${ctx}`, attached };
231
+ }
232
+
194
233
  function fmtElapsed(ms) {
195
234
  const s = Math.floor(ms / 1000);
196
235
  if (s < 60) return `${s}s`;
@@ -852,7 +891,7 @@ async function startChat(opts = {}) {
852
891
  );
853
892
  console.log(accountBadge);
854
893
  console.log(c.muted(` ${cwdShort}`));
855
- console.log(c.ok(' ◉') + c.muted(' Agent mode ') + c.dim('· file web shell image prior-network'));
894
+ console.log(c.ok(' ◉') + c.muted(' Agent mode ') + c.dim('· read edit search web shell image · @file to attach'));
856
895
 
857
896
  console.log(DIVIDER);
858
897
  console.log(c.muted(' /help /clear /update /compact /timer /save /load /saves /delete /exit'));
@@ -1250,7 +1289,7 @@ async function startChat(opts = {}) {
1250
1289
  banner();
1251
1290
  console.log(DIVIDER);
1252
1291
  console.log(c.brand(' Prior AI') + c.muted(' · ') + c.muted(`@${user}`));
1253
- console.log(c.ok(' ◉') + c.muted(' Agent mode ') + c.dim('· file web shell image prior-network'));
1292
+ console.log(c.ok(' ◉') + c.muted(' Agent mode ') + c.dim('· read edit search web shell image · @file to attach'));
1254
1293
  console.log(DIVIDER);
1255
1294
  console.log('');
1256
1295
  return loop();
@@ -1640,6 +1679,13 @@ Be concise but thorough — this summary replaces the full history to save conte
1640
1679
  console.log(c.brand(' ◈') + c.dim(` ${label} attached`));
1641
1680
  }
1642
1681
 
1682
+ // Expand any @file references into inline context
1683
+ const { message: expandedInput, attached } = expandFileRefs(input, process.cwd());
1684
+ if (attached.length) {
1685
+ console.log(c.brand(' ◈') + c.muted(' attached: ' +
1686
+ attached.map(a => `${a.ref} (${a.lines} line${a.lines !== 1 ? 's' : ''})`).join(', ')));
1687
+ }
1688
+
1643
1689
  let responseText = '';
1644
1690
  let _progressStarted = false;
1645
1691
  const _thinkStart = Date.now();
@@ -1661,7 +1707,7 @@ Be concise but thorough — this summary replaces the full history to save conte
1661
1707
 
1662
1708
  _currentAbortController = new AbortController();
1663
1709
  await runAgent({
1664
- messages: [...chatHistory, { role: 'user', content: injectToolHint(input) }],
1710
+ messages: [...chatHistory, { role: 'user', content: injectToolHint(expandedInput) }],
1665
1711
  model: currentModel,
1666
1712
  cwd: process.cwd(),
1667
1713
  projectContext,
@@ -1761,7 +1807,7 @@ Be concise but thorough — this summary replaces the full history to save conte
1761
1807
  _currentAbortController = null;
1762
1808
  }
1763
1809
 
1764
- chatHistory.push({ role: 'user', content: input });
1810
+ chatHistory.push({ role: 'user', content: expandedInput });
1765
1811
  if (responseText) chatHistory.push({ role: 'assistant', content: responseText });
1766
1812
 
1767
1813
  process.stdout.write('\n');
@@ -1828,6 +1874,78 @@ program
1828
1874
  .option('-m, --model <model>', 'Model to use')
1829
1875
  .action(opts => startChat(opts));
1830
1876
 
1877
+ // ── RUN (one-shot / non-interactive) ───────────────────────────
1878
+ program
1879
+ .command('run [prompt...]')
1880
+ .description('One-shot prompt — prints the answer and exits (scriptable, pipe-able)')
1881
+ .option('-m, --model <model>', 'Model to use')
1882
+ .option('-y, --yes', 'Auto-approve tool actions (run_command, file edits/writes)')
1883
+ .option('-q, --quiet', 'Print only the final answer (suppress tool activity)')
1884
+ .action(async (promptParts, opts) => {
1885
+ requireAuth();
1886
+
1887
+ // Gather prompt from args + piped stdin
1888
+ let prompt = (promptParts || []).join(' ').trim();
1889
+ if (!process.stdin.isTTY) {
1890
+ const piped = await new Promise(res => {
1891
+ let buf = ''; process.stdin.setEncoding('utf8');
1892
+ process.stdin.on('data', d => buf += d);
1893
+ process.stdin.on('end', () => res(buf.trim()));
1894
+ setTimeout(() => res(buf.trim()), 50); // no pipe → don't hang
1895
+ });
1896
+ if (piped) prompt = prompt ? `${prompt}\n\n${piped}` : piped;
1897
+ }
1898
+ if (!prompt) { console.error(c.err(' ✗ No prompt. Usage: prior run "your question" (or pipe via stdin)')); process.exit(1); }
1899
+
1900
+ const cwd = process.cwd();
1901
+ const { message, attached } = expandFileRefs(prompt, cwd);
1902
+ if (attached.length && !opts.quiet) {
1903
+ console.error(c.muted(' ◈ attached: ' + attached.map(a => a.ref).join(', ')));
1904
+ }
1905
+
1906
+ // Load prior.md if present
1907
+ let projectContext = null;
1908
+ try { projectContext = fs.readFileSync(path.join(cwd, 'prior.md'), 'utf8'); } catch {}
1909
+
1910
+ let responseText = '';
1911
+ let hadError = false;
1912
+ try {
1913
+ await runAgent({
1914
+ messages: [{ role: 'user', content: injectToolHint(message) }],
1915
+ model: opts.model || null,
1916
+ cwd,
1917
+ projectContext,
1918
+ confirm: async ({ name }) => {
1919
+ if (opts.yes) return true;
1920
+ console.error(c.warn(` ⚠ Skipping ${name} — re-run with --yes to allow tool actions in one-shot mode.`));
1921
+ return false;
1922
+ },
1923
+ send: ev => {
1924
+ switch (ev.type) {
1925
+ case 'tool_start':
1926
+ if (!opts.quiet) console.error(c.dim(` ◈ ${ev.name}`));
1927
+ break;
1928
+ case 'tool_error':
1929
+ if (!opts.quiet) console.error(c.err(` ✗ ${ev.name}: ${ev.error}`));
1930
+ break;
1931
+ case 'text':
1932
+ if (ev.content) { process.stdout.write(ev.content); responseText += ev.content; }
1933
+ break;
1934
+ case 'error':
1935
+ hadError = true;
1936
+ console.error(c.err(` ✗ ${ev.message}`));
1937
+ break;
1938
+ }
1939
+ },
1940
+ });
1941
+ } catch (err) {
1942
+ console.error(c.err(` ✗ ${err.message}`));
1943
+ process.exit(1);
1944
+ }
1945
+ if (responseText && !responseText.endsWith('\n')) process.stdout.write('\n');
1946
+ process.exit(hadError ? 1 : 0);
1947
+ });
1948
+
1831
1949
  // ── IMAGINE ────────────────────────────────────────────────────
1832
1950
  program
1833
1951
  .command('imagine <prompt>')
package/lib/agent.js CHANGED
@@ -176,6 +176,19 @@ function parseWriteTags(text) {
176
176
  while ((m = docxRe.exec(text)) !== null) {
177
177
  calls.push({ raw: m[0], offset: m.index, name: 'file_write_docx', args: { path: m[1], title: m[2] || undefined, content: m[3] } });
178
178
  }
179
+ // <edit path="..." [all="true"]> <<<<<<< SEARCH … ======= … >>>>>>> REPLACE </edit>
180
+ // Conflict-marker form so multiline code needs no JSON escaping.
181
+ const editRe = /<edit\s+path="([^"]+)"((?:\s+\w+="[^"]*")*)\s*>([\s\S]*?)<\/edit>/g;
182
+ while ((m = editRe.exec(text)) !== null) {
183
+ const body = m[3];
184
+ const split = body.match(/<{3,}\s*SEARCH\s*\r?\n([\s\S]*?)\r?\n={3,}[ \t]*\r?\n([\s\S]*?)\r?\n>{3,}\s*REPLACE/);
185
+ if (!split) continue;
186
+ const all = /\ball="?(true|1|yes)"?/i.test(m[2]);
187
+ calls.push({
188
+ raw: m[0], offset: m.index, name: 'file_edit',
189
+ args: { path: m[1], old_string: split[1], new_string: split[2], replace_all: all || undefined },
190
+ });
191
+ }
179
192
  return calls;
180
193
  }
181
194
 
@@ -212,12 +225,14 @@ function stripToolTags(text) {
212
225
  out = out.replace(new RegExp(`<(?:${namesPattern})[^>]*>[\\s\\S]*?<\\/(?:${namesPattern})>`, 'gi'), '');
213
226
  // Self-closing or unclosed: <tool_name attr="val" /> or <tool_name attr="val">
214
227
  out = out.replace(new RegExp(`<(?:${namesPattern})(?:\\s[^>]*)?\\s*/?>`, 'gi'), '');
228
+ // Tag-form file ops — never surface their bodies as chat text
229
+ out = out.replace(/<(write|append|docx|edit)\s+[^>]*>[\s\S]*?<\/\1>/gi, '');
215
230
  return out.trim();
216
231
  }
217
232
 
218
233
  // ── Main agent loop ───────────────────────────────────────────
219
234
 
220
- const CONFIRM_TOOLS = new Set(['run_command', 'file_delete', 'file_write']);
235
+ const CONFIRM_TOOLS = new Set(['run_command', 'file_delete', 'file_write', 'file_edit']);
221
236
 
222
237
  async function runAgent({ messages, model, uncensored, cwd, projectContext, images, send, confirm, signal }) {
223
238
  const token = getToken();
package/lib/tools.js CHANGED
@@ -47,6 +47,51 @@ function formatSize(bytes) {
47
47
  return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
48
48
  }
49
49
 
50
+ // Directories never worth walking for search/glob
51
+ const SKIP_DIRS = new Set([
52
+ 'node_modules', '.git', '__pycache__', '.next', 'dist', 'build',
53
+ '.venv', 'venv', '.cache', 'coverage', '.nyc_output', 'vendor',
54
+ '.idea', '.vscode', 'bin/obj', 'obj', '.gradle', 'target',
55
+ ]);
56
+
57
+ // Minimal glob → RegExp. Supports ** * ? matched against a
58
+ // forward-slash relative path. Anchored full-string.
59
+ function globToRegExp(glob) {
60
+ let re = '';
61
+ for (let i = 0; i < glob.length; i++) {
62
+ const ch = glob[i];
63
+ if (ch === '*') {
64
+ if (glob[i + 1] === '*') { // ** → any depth (incl. slashes)
65
+ re += '.*';
66
+ i++;
67
+ if (glob[i + 1] === '/') i++; // swallow the slash after **
68
+ } else {
69
+ re += '[^/]*'; // * → within one path segment
70
+ }
71
+ } else if (ch === '?') re += '[^/]';
72
+ else if ('.+^${}()|[]\\'.includes(ch)) re += '\\' + ch;
73
+ else re += ch;
74
+ }
75
+ return new RegExp('^' + re + '$', 'i');
76
+ }
77
+
78
+ // Recursively collect files under `root`, skipping SKIP_DIRS and oversized files.
79
+ function walkFiles(root, onFile, budget = { left: 20000 }) {
80
+ let entries;
81
+ try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return; }
82
+ for (const e of entries) {
83
+ if (budget.left <= 0) return;
84
+ const full = path.join(root, e.name);
85
+ if (e.isDirectory()) {
86
+ if (SKIP_DIRS.has(e.name) || e.name.startsWith('.git')) continue;
87
+ walkFiles(full, onFile, budget);
88
+ } else {
89
+ budget.left--;
90
+ onFile(full);
91
+ }
92
+ }
93
+ }
94
+
50
95
  // ── Tool implementations ──────────────────────────────────────
51
96
 
52
97
  const TOOLS = {
@@ -91,6 +136,103 @@ const TOOLS = {
91
136
  };
92
137
  },
93
138
 
139
+ async file_edit({ path: filePath, old_string, new_string, replace_all }, { cwd }) {
140
+ if (!filePath) throw new Error('"path" is required');
141
+ if (old_string === undefined) throw new Error('"old_string" is required');
142
+ if (new_string === undefined) throw new Error('"new_string" is required');
143
+ if (old_string === new_string) throw new Error('old_string and new_string are identical — nothing to change');
144
+ const resolved = resolvePath(filePath, cwd);
145
+ if (!fs.existsSync(resolved)) throw new Error(`Not found: ${filePath} — use file_write to create a new file`);
146
+ const stat = fs.statSync(resolved);
147
+ if (stat.isDirectory()) throw new Error(`"${filePath}" is a directory`);
148
+ if (stat.size > MAX_FILE_SIZE) throw new Error(`File too large (${formatSize(stat.size)}, max 500KB)`);
149
+
150
+ const content = fs.readFileSync(resolved, 'utf8');
151
+ const first = content.indexOf(old_string);
152
+ if (first === -1) throw new Error(`old_string not found in ${filePath} — it must match the file exactly, including whitespace and indentation`);
153
+
154
+ let updated, count;
155
+ if (replace_all) {
156
+ count = content.split(old_string).length - 1;
157
+ updated = content.split(old_string).join(new_string);
158
+ } else {
159
+ if (content.indexOf(old_string, first + old_string.length) !== -1) {
160
+ throw new Error(`old_string appears multiple times in ${filePath} — add more surrounding context to make it unique, or pass "replace_all": true`);
161
+ }
162
+ count = 1;
163
+ updated = content.slice(0, first) + new_string + content.slice(first + old_string.length);
164
+ }
165
+ fs.writeFileSync(resolved, updated, 'utf8');
166
+ const delta = (new_string.split('\n').length) - (old_string.split('\n').length);
167
+ return {
168
+ output: `Edited ${filePath} — ${count} replacement${count !== 1 ? 's' : ''}${delta ? ` (${delta > 0 ? '+' : ''}${delta} lines)` : ''}`,
169
+ summary: `${count}× in ${path.basename(filePath)}`,
170
+ };
171
+ },
172
+
173
+ async file_search({ pattern, path: searchPath = '.', glob, ignore_case, max_results }, { cwd }) {
174
+ if (!pattern) throw new Error('"pattern" is required');
175
+ let re;
176
+ try { re = new RegExp(pattern, ignore_case ? 'i' : ''); }
177
+ catch (e) { throw new Error(`Invalid regex pattern: ${e.message}`); }
178
+ const root = resolvePath(searchPath, cwd);
179
+ if (!fs.existsSync(root)) throw new Error(`Not found: ${searchPath}`);
180
+ const cap = Math.min(max_results || 100, 300);
181
+ const globRe = glob ? globToRegExp(glob.includes('/') ? glob : '**/' + glob) : null;
182
+ const hits = [];
183
+ let filesMatched = new Set();
184
+
185
+ const scanFile = (full) => {
186
+ if (hits.length >= cap) return;
187
+ const rel = path.relative(cwd, full).replace(/\\/g, '/');
188
+ if (globRe && !globRe.test(rel) && !globRe.test(path.basename(full))) return;
189
+ let stat; try { stat = fs.statSync(full); } catch { return; }
190
+ if (stat.size > MAX_FILE_SIZE) return;
191
+ let text; try { text = fs.readFileSync(full, 'utf8'); } catch { return; }
192
+ if (text.indexOf('\x00') !== -1) return; // skip binary files
193
+ const lines = text.split('\n');
194
+ for (let i = 0; i < lines.length && hits.length < cap; i++) {
195
+ re.lastIndex = 0;
196
+ if (re.test(lines[i])) {
197
+ filesMatched.add(rel);
198
+ hits.push(`${rel}:${i + 1}: ${lines[i].trim().slice(0, 200)}`);
199
+ }
200
+ }
201
+ };
202
+
203
+ if (fs.statSync(root).isDirectory()) walkFiles(root, scanFile);
204
+ else scanFile(root);
205
+
206
+ if (!hits.length) return { output: `No matches for /${pattern}/${glob ? ` in ${glob}` : ''}`, summary: '0 matches' };
207
+ const more = hits.length >= cap ? `\n… (capped at ${cap} matches)` : '';
208
+ return {
209
+ output: hits.join('\n') + more,
210
+ summary: `${hits.length} match${hits.length !== 1 ? 'es' : ''} in ${filesMatched.size} file${filesMatched.size !== 1 ? 's' : ''}`,
211
+ };
212
+ },
213
+
214
+ async file_glob({ pattern, path: searchPath = '.' }, { cwd }) {
215
+ if (!pattern) throw new Error('"pattern" is required');
216
+ const root = resolvePath(searchPath, cwd);
217
+ if (!fs.existsSync(root)) throw new Error(`Not found: ${searchPath}`);
218
+ const globRe = globToRegExp(pattern.includes('/') ? pattern : '**/' + pattern);
219
+ const results = [];
220
+ walkFiles(root, (full) => {
221
+ const rel = path.relative(root, full).replace(/\\/g, '/');
222
+ if (globRe.test(rel) || globRe.test(path.basename(full))) {
223
+ let mtime = 0; try { mtime = fs.statSync(full).mtimeMs; } catch {}
224
+ results.push({ rel: path.relative(cwd, full).replace(/\\/g, '/'), mtime });
225
+ }
226
+ });
227
+ results.sort((a, b) => b.mtime - a.mtime);
228
+ const top = results.slice(0, 200);
229
+ const more = results.length > top.length ? `\n… and ${results.length - top.length} more` : '';
230
+ return {
231
+ output: top.length ? top.map(r => ' ' + r.rel).join('\n') + more : `No files match "${pattern}"`,
232
+ summary: `${results.length} file${results.length !== 1 ? 's' : ''} match "${pattern}"`,
233
+ };
234
+ },
235
+
94
236
  async file_list({ path: dirPath = '.' }, { cwd }) {
95
237
  const resolved = resolvePath(dirPath, cwd);
96
238
  if (!fs.existsSync(resolved)) throw new Error(`Not found: ${dirPath}`);
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "prior-cli",
3
- "version": "1.7.9",
3
+ "version": "1.7.11",
4
4
  "description": "Prior Network AI — command-line interface",
5
5
  "author": "Prior Network",
6
6
  "homepage": "https://priornetwork.com",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "https://github.com/Niceguygamer/prior-cli.git"
9
+ "url": "git+https://github.com/PriorNetwork/prior-cli.git"
10
10
  },
11
11
  "bugs": {
12
- "url": "https://github.com/Niceguygamer/prior-cli/issues"
12
+ "url": "https://github.com/PriorNetwork/prior-cli/issues"
13
13
  },
14
14
  "bin": {
15
15
  "prior": "bin/prior.js"