prior-cli 1.7.10 → 1.7.12

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,59 @@ 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
+
233
+ // Live completions for an @file token being typed. `partial` is the text
234
+ // after @ (e.g. "lib/to"). Returns up to `limit` matches, dirs first.
235
+ const REF_SKIP_DIRS = new Set(['node_modules', '.git', '.next', 'dist', 'build', '.cache', 'coverage', 'vendor']);
236
+ function fileRefSuggestions(partial, cwd, limit = 6) {
237
+ const slash = Math.max(partial.lastIndexOf('/'), partial.lastIndexOf('\\'));
238
+ const dirPart = slash >= 0 ? partial.slice(0, slash) : '';
239
+ const prefix = (slash >= 0 ? partial.slice(slash + 1) : partial).toLowerCase();
240
+ const baseDir = dirPart
241
+ ? (path.isAbsolute(dirPart) ? dirPart : path.resolve(cwd, dirPart))
242
+ : cwd;
243
+ let entries;
244
+ try { entries = fs.readdirSync(baseDir, { withFileTypes: true }); } catch { return []; }
245
+ const out = [];
246
+ for (const e of entries) {
247
+ if (e.name.startsWith('.') && !prefix.startsWith('.')) continue; // hide dotfiles unless asked
248
+ if (e.isDirectory() && REF_SKIP_DIRS.has(e.name)) continue;
249
+ if (!e.name.toLowerCase().startsWith(prefix)) continue;
250
+ const ref = (dirPart ? dirPart.replace(/\\/g, '/') + '/' : '') + e.name;
251
+ out.push({ ref, isDir: e.isDirectory() });
252
+ }
253
+ out.sort((a, b) => (a.isDir === b.isDir ? a.ref.localeCompare(b.ref) : a.isDir ? -1 : 1));
254
+ return out.slice(0, limit);
255
+ }
256
+
194
257
  function fmtElapsed(ms) {
195
258
  const s = Math.floor(ms / 1000);
196
259
  if (s < 60) return `${s}s`;
@@ -824,6 +887,13 @@ async function startChat(opts = {}) {
824
887
  terminal: true,
825
888
  historySize: 100,
826
889
  completer: line => {
890
+ // @file completion — complete the @token at the end of the line
891
+ const at = line.match(/(?:^|\s)@([^\s]*)$/);
892
+ if (at) {
893
+ const matches = fileRefSuggestions(at[1], process.cwd(), 20)
894
+ .map(s => '@' + (s.isDir ? s.ref + '/' : s.ref));
895
+ return [matches, '@' + at[1]];
896
+ }
827
897
  const cmds = ['/help', '/clear', '/censored', '/uncensored', '/login', '/logout', '/exit'];
828
898
  if (!line.startsWith('/')) return [[], line];
829
899
  const hits = cmds.filter(cmd => cmd.startsWith(line));
@@ -852,7 +922,7 @@ async function startChat(opts = {}) {
852
922
  );
853
923
  console.log(accountBadge);
854
924
  console.log(c.muted(` ${cwdShort}`));
855
- console.log(c.ok(' ◉') + c.muted(' Agent mode ') + c.dim('· file web shell image prior-network'));
925
+ console.log(c.ok(' ◉') + c.muted(' Agent mode ') + c.dim('· read edit search web shell image · @file to attach'));
856
926
 
857
927
  console.log(DIVIDER);
858
928
  console.log(c.muted(' /help /clear /update /compact /timer /save /load /saves /delete /exit'));
@@ -933,6 +1003,22 @@ async function startChat(opts = {}) {
933
1003
  }
934
1004
  }
935
1005
 
1006
+ // @file completions — when the token at the end of the line starts with @
1007
+ const atMatch = (line || '').match(/(?:^|\s)@([^\s]*)$/);
1008
+ if (atMatch) {
1009
+ const sugg = fileRefSuggestions(atMatch[1], process.cwd());
1010
+ for (const s of sugg) {
1011
+ const label = s.isDir ? s.ref + '/' : s.ref;
1012
+ const icon = s.isDir ? '📁' : '📄';
1013
+ process.stdout.write(`\x1b[B\r\x1b[2K ${c.brand('@')}${c.white(label.padEnd(30))}${c.dim(icon)}`);
1014
+ rows++;
1015
+ }
1016
+ if (sugg.length) {
1017
+ process.stdout.write(`\x1b[B\r\x1b[2K ${c.dim('tab to complete · attaches file contents as context')}`);
1018
+ rows++;
1019
+ }
1020
+ }
1021
+
936
1022
  process.stdout.write('\x1b[u');
937
1023
  _subRowCount = rows;
938
1024
  }
@@ -1250,7 +1336,7 @@ async function startChat(opts = {}) {
1250
1336
  banner();
1251
1337
  console.log(DIVIDER);
1252
1338
  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'));
1339
+ console.log(c.ok(' ◉') + c.muted(' Agent mode ') + c.dim('· read edit search web shell image · @file to attach'));
1254
1340
  console.log(DIVIDER);
1255
1341
  console.log('');
1256
1342
  return loop();
@@ -1640,6 +1726,13 @@ Be concise but thorough — this summary replaces the full history to save conte
1640
1726
  console.log(c.brand(' ◈') + c.dim(` ${label} attached`));
1641
1727
  }
1642
1728
 
1729
+ // Expand any @file references into inline context
1730
+ const { message: expandedInput, attached } = expandFileRefs(input, process.cwd());
1731
+ if (attached.length) {
1732
+ console.log(c.brand(' ◈') + c.muted(' attached: ' +
1733
+ attached.map(a => `${a.ref} (${a.lines} line${a.lines !== 1 ? 's' : ''})`).join(', ')));
1734
+ }
1735
+
1643
1736
  let responseText = '';
1644
1737
  let _progressStarted = false;
1645
1738
  const _thinkStart = Date.now();
@@ -1661,7 +1754,7 @@ Be concise but thorough — this summary replaces the full history to save conte
1661
1754
 
1662
1755
  _currentAbortController = new AbortController();
1663
1756
  await runAgent({
1664
- messages: [...chatHistory, { role: 'user', content: injectToolHint(input) }],
1757
+ messages: [...chatHistory, { role: 'user', content: injectToolHint(expandedInput) }],
1665
1758
  model: currentModel,
1666
1759
  cwd: process.cwd(),
1667
1760
  projectContext,
@@ -1761,7 +1854,7 @@ Be concise but thorough — this summary replaces the full history to save conte
1761
1854
  _currentAbortController = null;
1762
1855
  }
1763
1856
 
1764
- chatHistory.push({ role: 'user', content: input });
1857
+ chatHistory.push({ role: 'user', content: expandedInput });
1765
1858
  if (responseText) chatHistory.push({ role: 'assistant', content: responseText });
1766
1859
 
1767
1860
  process.stdout.write('\n');
@@ -1828,6 +1921,78 @@ program
1828
1921
  .option('-m, --model <model>', 'Model to use')
1829
1922
  .action(opts => startChat(opts));
1830
1923
 
1924
+ // ── RUN (one-shot / non-interactive) ───────────────────────────
1925
+ program
1926
+ .command('run [prompt...]')
1927
+ .description('One-shot prompt — prints the answer and exits (scriptable, pipe-able)')
1928
+ .option('-m, --model <model>', 'Model to use')
1929
+ .option('-y, --yes', 'Auto-approve tool actions (run_command, file edits/writes)')
1930
+ .option('-q, --quiet', 'Print only the final answer (suppress tool activity)')
1931
+ .action(async (promptParts, opts) => {
1932
+ requireAuth();
1933
+
1934
+ // Gather prompt from args + piped stdin
1935
+ let prompt = (promptParts || []).join(' ').trim();
1936
+ if (!process.stdin.isTTY) {
1937
+ const piped = await new Promise(res => {
1938
+ let buf = ''; process.stdin.setEncoding('utf8');
1939
+ process.stdin.on('data', d => buf += d);
1940
+ process.stdin.on('end', () => res(buf.trim()));
1941
+ setTimeout(() => res(buf.trim()), 50); // no pipe → don't hang
1942
+ });
1943
+ if (piped) prompt = prompt ? `${prompt}\n\n${piped}` : piped;
1944
+ }
1945
+ if (!prompt) { console.error(c.err(' ✗ No prompt. Usage: prior run "your question" (or pipe via stdin)')); process.exit(1); }
1946
+
1947
+ const cwd = process.cwd();
1948
+ const { message, attached } = expandFileRefs(prompt, cwd);
1949
+ if (attached.length && !opts.quiet) {
1950
+ console.error(c.muted(' ◈ attached: ' + attached.map(a => a.ref).join(', ')));
1951
+ }
1952
+
1953
+ // Load prior.md if present
1954
+ let projectContext = null;
1955
+ try { projectContext = fs.readFileSync(path.join(cwd, 'prior.md'), 'utf8'); } catch {}
1956
+
1957
+ let responseText = '';
1958
+ let hadError = false;
1959
+ try {
1960
+ await runAgent({
1961
+ messages: [{ role: 'user', content: injectToolHint(message) }],
1962
+ model: opts.model || null,
1963
+ cwd,
1964
+ projectContext,
1965
+ confirm: async ({ name }) => {
1966
+ if (opts.yes) return true;
1967
+ console.error(c.warn(` ⚠ Skipping ${name} — re-run with --yes to allow tool actions in one-shot mode.`));
1968
+ return false;
1969
+ },
1970
+ send: ev => {
1971
+ switch (ev.type) {
1972
+ case 'tool_start':
1973
+ if (!opts.quiet) console.error(c.dim(` ◈ ${ev.name}`));
1974
+ break;
1975
+ case 'tool_error':
1976
+ if (!opts.quiet) console.error(c.err(` ✗ ${ev.name}: ${ev.error}`));
1977
+ break;
1978
+ case 'text':
1979
+ if (ev.content) { process.stdout.write(ev.content); responseText += ev.content; }
1980
+ break;
1981
+ case 'error':
1982
+ hadError = true;
1983
+ console.error(c.err(` ✗ ${ev.message}`));
1984
+ break;
1985
+ }
1986
+ },
1987
+ });
1988
+ } catch (err) {
1989
+ console.error(c.err(` ✗ ${err.message}`));
1990
+ process.exit(1);
1991
+ }
1992
+ if (responseText && !responseText.endsWith('\n')) process.stdout.write('\n');
1993
+ process.exit(hadError ? 1 : 0);
1994
+ });
1995
+
1831
1996
  // ── IMAGINE ────────────────────────────────────────────────────
1832
1997
  program
1833
1998
  .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,6 +1,6 @@
1
1
  {
2
2
  "name": "prior-cli",
3
- "version": "1.7.10",
3
+ "version": "1.7.12",
4
4
  "description": "Prior Network AI — command-line interface",
5
5
  "author": "Prior Network",
6
6
  "homepage": "https://priornetwork.com",