prior-cli 1.7.10 → 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 +122 -4
- package/lib/agent.js +16 -1
- package/lib/tools.js +142 -0
- package/package.json +1 -1
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('·
|
|
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('·
|
|
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(
|
|
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:
|
|
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}`);
|