@calliopelabs/cli 2.0.6 → 2.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/dist/api-server.d.ts +11 -1
- package/dist/api-server.d.ts.map +1 -1
- package/dist/api-server.js +28 -3
- package/dist/api-server.js.map +1 -1
- package/dist/bin.d.ts +2 -1
- package/dist/bin.d.ts.map +1 -1
- package/dist/bin.js +31 -3
- package/dist/bin.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +14 -0
- package/dist/config.js.map +1 -1
- package/dist/headless.d.ts +1 -0
- package/dist/headless.d.ts.map +1 -1
- package/dist/headless.js +9 -1
- package/dist/headless.js.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +397 -13
- package/dist/tools.js.map +1 -1
- package/dist/ui/commands.js +6 -6
- package/dist/ui/commands.js.map +1 -1
- package/dist/ui/status-bar.d.ts +1 -0
- package/dist/ui/status-bar.d.ts.map +1 -1
- package/dist/ui/status-bar.js +11 -1
- package/dist/ui/status-bar.js.map +1 -1
- package/package.json +1 -1
package/dist/tools.js
CHANGED
|
@@ -14,6 +14,7 @@ import { getPluginTools, isPluginTool, executePluginTool } from './plugins.js';
|
|
|
14
14
|
import config from './config.js';
|
|
15
15
|
import { applySkin, applyPalette, listSkins, listPalettes } from './hud/api.js';
|
|
16
16
|
import { listCompanions } from './companions.js';
|
|
17
|
+
import { generateDiff as generateFileDiff } from './diff.js';
|
|
17
18
|
/**
|
|
18
19
|
* Available tools for the agent
|
|
19
20
|
*/
|
|
@@ -197,6 +198,76 @@ export const TOOLS = [
|
|
|
197
198
|
required: ['operation'],
|
|
198
199
|
},
|
|
199
200
|
},
|
|
201
|
+
{
|
|
202
|
+
name: 'edit_file',
|
|
203
|
+
description: 'Edit a file by replacing an exact string. Prefer this over write_file for modifications. Fails if old_string is not found or appears multiple times (use replace_all for intentional multi-replace).',
|
|
204
|
+
parameters: {
|
|
205
|
+
type: 'object',
|
|
206
|
+
properties: {
|
|
207
|
+
path: {
|
|
208
|
+
type: 'string',
|
|
209
|
+
description: 'The path to the file to edit',
|
|
210
|
+
},
|
|
211
|
+
old_string: {
|
|
212
|
+
type: 'string',
|
|
213
|
+
description: 'The exact string to find and replace',
|
|
214
|
+
},
|
|
215
|
+
new_string: {
|
|
216
|
+
type: 'string',
|
|
217
|
+
description: 'The string to replace old_string with',
|
|
218
|
+
},
|
|
219
|
+
replace_all: {
|
|
220
|
+
type: 'boolean',
|
|
221
|
+
description: 'If true, replace all occurrences. If false (default), fails when multiple matches exist.',
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
required: ['path', 'old_string', 'new_string'],
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: 'glob',
|
|
229
|
+
description: 'Find files matching a glob pattern (e.g. **/*.ts, src/**/*.json). Returns paths relative to cwd, sorted.',
|
|
230
|
+
parameters: {
|
|
231
|
+
type: 'object',
|
|
232
|
+
properties: {
|
|
233
|
+
pattern: {
|
|
234
|
+
type: 'string',
|
|
235
|
+
description: 'Glob pattern to match files against (e.g. **/*.ts, src/**/*.json)',
|
|
236
|
+
},
|
|
237
|
+
cwd: {
|
|
238
|
+
type: 'string',
|
|
239
|
+
description: 'Directory to search in (default: current working directory)',
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
required: ['pattern'],
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: 'grep',
|
|
247
|
+
description: 'Search file contents for a pattern. Returns matching lines with file path and line number.',
|
|
248
|
+
parameters: {
|
|
249
|
+
type: 'object',
|
|
250
|
+
properties: {
|
|
251
|
+
pattern: {
|
|
252
|
+
type: 'string',
|
|
253
|
+
description: 'The regex or literal string pattern to search for',
|
|
254
|
+
},
|
|
255
|
+
path: {
|
|
256
|
+
type: 'string',
|
|
257
|
+
description: 'Directory or file to search in (default: ".")',
|
|
258
|
+
},
|
|
259
|
+
glob: {
|
|
260
|
+
type: 'string',
|
|
261
|
+
description: 'Filter files by glob pattern (e.g. "*.ts")',
|
|
262
|
+
},
|
|
263
|
+
case_insensitive: {
|
|
264
|
+
type: 'boolean',
|
|
265
|
+
description: 'If true, perform case-insensitive matching',
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
required: ['pattern'],
|
|
269
|
+
},
|
|
270
|
+
},
|
|
200
271
|
{
|
|
201
272
|
name: 'configure',
|
|
202
273
|
description: `Read, set, or list Calliope configuration options. Use this when the user asks to change settings, switch themes, providers, models, companions, or any preference through natural conversation. Always use action "list" first if you need to show available options.
|
|
@@ -560,6 +631,38 @@ export async function executeTool(toolCall, cwd, timeout = 60000, onOutput) {
|
|
|
560
631
|
result = generateMermaidDiagram(diagramType, args.content, title);
|
|
561
632
|
break;
|
|
562
633
|
}
|
|
634
|
+
case 'edit_file': {
|
|
635
|
+
if (typeof args.path !== 'string') {
|
|
636
|
+
return { toolCallId: id, result: 'Error: path must be a string', isError: true };
|
|
637
|
+
}
|
|
638
|
+
if (typeof args.old_string !== 'string') {
|
|
639
|
+
return { toolCallId: id, result: 'Error: old_string must be a string', isError: true };
|
|
640
|
+
}
|
|
641
|
+
if (typeof args.new_string !== 'string') {
|
|
642
|
+
return { toolCallId: id, result: 'Error: new_string must be a string', isError: true };
|
|
643
|
+
}
|
|
644
|
+
const replaceAll = args.replace_all === true;
|
|
645
|
+
result = await editFile(args.path, args.old_string, args.new_string, replaceAll, cwd);
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
case 'glob': {
|
|
649
|
+
if (typeof args.pattern !== 'string') {
|
|
650
|
+
return { toolCallId: id, result: 'Error: pattern must be a string', isError: true };
|
|
651
|
+
}
|
|
652
|
+
const globCwd = typeof args.cwd === 'string' ? args.cwd : cwd;
|
|
653
|
+
result = await globFiles(args.pattern, globCwd);
|
|
654
|
+
break;
|
|
655
|
+
}
|
|
656
|
+
case 'grep': {
|
|
657
|
+
if (typeof args.pattern !== 'string') {
|
|
658
|
+
return { toolCallId: id, result: 'Error: pattern must be a string', isError: true };
|
|
659
|
+
}
|
|
660
|
+
const grepPath = typeof args.path === 'string' ? args.path : '.';
|
|
661
|
+
const grepGlob = typeof args.glob === 'string' ? args.glob : undefined;
|
|
662
|
+
const caseInsensitive = args.case_insensitive === true;
|
|
663
|
+
result = await grepFiles(args.pattern, grepPath, cwd, grepGlob, caseInsensitive);
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
563
666
|
default:
|
|
564
667
|
return { toolCallId: id, result: `Unknown tool: ${name}`, isError: true };
|
|
565
668
|
}
|
|
@@ -836,7 +939,16 @@ async function readFile(filePath, cwd) {
|
|
|
836
939
|
if (stats.size > 1024 * 1024) {
|
|
837
940
|
throw new Error(`File too large (${Math.round(stats.size / 1024)}KB). Max 1MB.`);
|
|
838
941
|
}
|
|
839
|
-
|
|
942
|
+
const content = fs.readFileSync(absPath, 'utf-8');
|
|
943
|
+
// Inline file preview header (#119)
|
|
944
|
+
const PREVIEW_CAP = 20;
|
|
945
|
+
const allLines = content.split('\n');
|
|
946
|
+
const totalLines = allLines.length;
|
|
947
|
+
const previewLines = allLines.slice(0, PREVIEW_CAP);
|
|
948
|
+
const header = `[file: ${filePath} \u2014 ${totalLines} line${totalLines !== 1 ? 's' : ''}]\n${'─'.repeat(40)}`;
|
|
949
|
+
const previewBody = previewLines.join('\n');
|
|
950
|
+
const footer = totalLines > PREVIEW_CAP ? `\n... (${totalLines - PREVIEW_CAP} more lines)` : '';
|
|
951
|
+
return `${header}\n${previewBody}${footer}\n\n${content}`;
|
|
840
952
|
}
|
|
841
953
|
/**
|
|
842
954
|
* Generate a simple line-diff between old and new content
|
|
@@ -934,24 +1046,42 @@ async function writeFile(filePath, content, cwd) {
|
|
|
934
1046
|
}
|
|
935
1047
|
}
|
|
936
1048
|
fs.writeFileSync(absPath, content);
|
|
937
|
-
// Generate diff output
|
|
1049
|
+
// Generate diff output (#119)
|
|
1050
|
+
const DIFF_CAP = 50;
|
|
1051
|
+
const header = `[wrote: ${filePath}]\n${'─'.repeat(Math.min(filePath.length + 9, 60))}`;
|
|
938
1052
|
if (isNewFile) {
|
|
939
1053
|
const allLines = content.split('\n');
|
|
940
|
-
const
|
|
941
|
-
const
|
|
942
|
-
const
|
|
943
|
-
|
|
944
|
-
const more = allLines.length > 10 ? '\n ... (new file truncated)' : '';
|
|
945
|
-
return `DIFF:NEW_FILE:${absPath}\n⎿ Added ${allLines.length} lines\n${preview}${more}`;
|
|
1054
|
+
const previewLines = allLines.slice(0, DIFF_CAP);
|
|
1055
|
+
const diffLines = previewLines.map(l => `+${l}`);
|
|
1056
|
+
const more = allLines.length > DIFF_CAP ? `\n... (${allLines.length - DIFF_CAP} more lines)` : '';
|
|
1057
|
+
return `${header}\n[new file: ${filePath}]\n--- /dev/null\n+++ b/${filePath}\n${diffLines.join('\n')}${more}`;
|
|
946
1058
|
}
|
|
947
1059
|
else {
|
|
948
|
-
const
|
|
949
|
-
|
|
950
|
-
|
|
1060
|
+
const fileDiff = generateFileDiff(oldContent, content, filePath);
|
|
1061
|
+
const diffParts = [
|
|
1062
|
+
`--- a/${filePath}`,
|
|
1063
|
+
`+++ b/${filePath}`,
|
|
1064
|
+
];
|
|
1065
|
+
const diffLines = fileDiff.lines.filter(l => l.type !== 'header');
|
|
1066
|
+
let lineCount = 0;
|
|
1067
|
+
let truncated = false;
|
|
1068
|
+
for (const line of diffLines) {
|
|
1069
|
+
if (line.type === 'context')
|
|
1070
|
+
continue; // skip context for compact output
|
|
1071
|
+
if (lineCount >= DIFF_CAP) {
|
|
1072
|
+
truncated = true;
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
const prefix = line.type === 'add' ? '+' : '-';
|
|
1076
|
+
diffParts.push(`${prefix}${line.content}`);
|
|
1077
|
+
lineCount++;
|
|
951
1078
|
}
|
|
952
|
-
|
|
953
|
-
|
|
1079
|
+
if (truncated)
|
|
1080
|
+
diffParts.push(`... (diff truncated at ${DIFF_CAP} lines)`);
|
|
1081
|
+
if (lineCount === 0) {
|
|
1082
|
+
return `File unchanged: ${filePath}`;
|
|
954
1083
|
}
|
|
1084
|
+
return `${header}\n${diffParts.join('\n')}`;
|
|
955
1085
|
}
|
|
956
1086
|
}
|
|
957
1087
|
/**
|
|
@@ -1213,4 +1343,258 @@ ${content}
|
|
|
1213
1343
|
\`\`\``;
|
|
1214
1344
|
return `MERMAID_DIAGRAM:\n${diagram}\n\nTo view this diagram, paste the mermaid code into https://mermaid.live or a markdown viewer that supports Mermaid.`;
|
|
1215
1345
|
}
|
|
1346
|
+
/**
|
|
1347
|
+
* Edit a file by replacing an exact string (in-place edit).
|
|
1348
|
+
* Supports single-occurrence enforcement or replace_all mode.
|
|
1349
|
+
*/
|
|
1350
|
+
async function editFile(filePath, oldString, newString, replaceAll, cwd) {
|
|
1351
|
+
const absPath = validatePath(filePath, cwd);
|
|
1352
|
+
if (!fs.existsSync(absPath)) {
|
|
1353
|
+
throw new Error(`File not found: ${absPath}`);
|
|
1354
|
+
}
|
|
1355
|
+
const stats = fs.statSync(absPath);
|
|
1356
|
+
if (stats.isDirectory()) {
|
|
1357
|
+
throw new Error(`Path is a directory: ${absPath}`);
|
|
1358
|
+
}
|
|
1359
|
+
const content = fs.readFileSync(absPath, 'utf-8');
|
|
1360
|
+
// Helper to build a compact edit diff from old_string/new_string (#119)
|
|
1361
|
+
const buildEditDiff = (oldStr, newStr, fPath, count) => {
|
|
1362
|
+
const DIFF_CAP = 50;
|
|
1363
|
+
const label = count === 1 ? '1 occurrence' : `${count} occurrences`;
|
|
1364
|
+
const header = `[edited: ${fPath} — replaced ${label}]\n${'─'.repeat(Math.min(fPath.length + 10, 60))}`;
|
|
1365
|
+
const oldLines = oldStr.split('\n');
|
|
1366
|
+
const newLines = newStr.split('\n');
|
|
1367
|
+
const diffParts = [
|
|
1368
|
+
`--- a/${fPath}`,
|
|
1369
|
+
`+++ b/${fPath}`,
|
|
1370
|
+
];
|
|
1371
|
+
let lineCount = 0;
|
|
1372
|
+
let truncated = false;
|
|
1373
|
+
for (const line of oldLines) {
|
|
1374
|
+
if (lineCount >= DIFF_CAP) {
|
|
1375
|
+
truncated = true;
|
|
1376
|
+
break;
|
|
1377
|
+
}
|
|
1378
|
+
diffParts.push(`-${line}`);
|
|
1379
|
+
lineCount++;
|
|
1380
|
+
}
|
|
1381
|
+
if (!truncated) {
|
|
1382
|
+
for (const line of newLines) {
|
|
1383
|
+
if (lineCount >= DIFF_CAP) {
|
|
1384
|
+
truncated = true;
|
|
1385
|
+
break;
|
|
1386
|
+
}
|
|
1387
|
+
diffParts.push(`+${line}`);
|
|
1388
|
+
lineCount++;
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
if (truncated)
|
|
1392
|
+
diffParts.push(`... (diff truncated at ${DIFF_CAP} lines)`);
|
|
1393
|
+
return `${header}\n${diffParts.join('\n')}`;
|
|
1394
|
+
};
|
|
1395
|
+
if (replaceAll) {
|
|
1396
|
+
const updated = content.replaceAll(oldString, newString);
|
|
1397
|
+
const count = (content.split(oldString).length - 1);
|
|
1398
|
+
if (count === 0) {
|
|
1399
|
+
throw new Error(`old_string not found in file: ${absPath}`);
|
|
1400
|
+
}
|
|
1401
|
+
fs.writeFileSync(absPath, updated);
|
|
1402
|
+
return buildEditDiff(oldString, newString, filePath, count);
|
|
1403
|
+
}
|
|
1404
|
+
// Count occurrences
|
|
1405
|
+
const occurrences = content.split(oldString).length - 1;
|
|
1406
|
+
if (occurrences === 0) {
|
|
1407
|
+
throw new Error(`old_string not found in file: ${absPath}`);
|
|
1408
|
+
}
|
|
1409
|
+
if (occurrences > 1) {
|
|
1410
|
+
throw new Error(`old_string matches ${occurrences} occurrences — use replace_all: true or make it more specific`);
|
|
1411
|
+
}
|
|
1412
|
+
const updated = content.replace(oldString, newString);
|
|
1413
|
+
fs.writeFileSync(absPath, updated);
|
|
1414
|
+
return buildEditDiff(oldString, newString, filePath, 1);
|
|
1415
|
+
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Convert a glob pattern to a RegExp.
|
|
1418
|
+
* Supports: *, **, ?, {a,b} syntax.
|
|
1419
|
+
*/
|
|
1420
|
+
function globToRegex(pattern) {
|
|
1421
|
+
let regexStr = '';
|
|
1422
|
+
let i = 0;
|
|
1423
|
+
while (i < pattern.length) {
|
|
1424
|
+
const ch = pattern[i];
|
|
1425
|
+
if (ch === '*') {
|
|
1426
|
+
if (pattern[i + 1] === '*') {
|
|
1427
|
+
// ** matches any path segment including slashes
|
|
1428
|
+
regexStr += '.*';
|
|
1429
|
+
i += 2;
|
|
1430
|
+
// Consume optional trailing slash
|
|
1431
|
+
if (pattern[i] === '/')
|
|
1432
|
+
i++;
|
|
1433
|
+
}
|
|
1434
|
+
else {
|
|
1435
|
+
// * matches anything except /
|
|
1436
|
+
regexStr += '[^/]*';
|
|
1437
|
+
i++;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
else if (ch === '?') {
|
|
1441
|
+
regexStr += '[^/]';
|
|
1442
|
+
i++;
|
|
1443
|
+
}
|
|
1444
|
+
else if (ch === '{') {
|
|
1445
|
+
// {a,b,c} → (a|b|c)
|
|
1446
|
+
const end = pattern.indexOf('}', i);
|
|
1447
|
+
if (end === -1) {
|
|
1448
|
+
regexStr += '\\{';
|
|
1449
|
+
i++;
|
|
1450
|
+
}
|
|
1451
|
+
else {
|
|
1452
|
+
const options = pattern.slice(i + 1, end).split(',').map(s => s.replace(/[.+^$[\]\\(){}|]/g, '\\$&'));
|
|
1453
|
+
regexStr += `(?:${options.join('|')})`;
|
|
1454
|
+
i = end + 1;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
else if ('.+^$[]\\(){}|'.includes(ch)) {
|
|
1458
|
+
regexStr += '\\' + ch;
|
|
1459
|
+
i++;
|
|
1460
|
+
}
|
|
1461
|
+
else {
|
|
1462
|
+
regexStr += ch;
|
|
1463
|
+
i++;
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
return new RegExp(`^${regexStr}$`);
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Recursively walk a directory and collect all file paths relative to the base.
|
|
1470
|
+
*/
|
|
1471
|
+
function walkDir(dir, base, results) {
|
|
1472
|
+
let entries;
|
|
1473
|
+
try {
|
|
1474
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1475
|
+
}
|
|
1476
|
+
catch {
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
for (const entry of entries) {
|
|
1480
|
+
const fullPath = path.join(dir, entry.name);
|
|
1481
|
+
const relPath = path.relative(base, fullPath);
|
|
1482
|
+
if (entry.isDirectory()) {
|
|
1483
|
+
walkDir(fullPath, base, results);
|
|
1484
|
+
}
|
|
1485
|
+
else {
|
|
1486
|
+
results.push(relPath);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Find files matching a glob pattern.
|
|
1492
|
+
*/
|
|
1493
|
+
async function globFiles(pattern, searchCwd) {
|
|
1494
|
+
const absCwd = path.isAbsolute(searchCwd)
|
|
1495
|
+
? path.resolve(searchCwd)
|
|
1496
|
+
: path.resolve(searchCwd);
|
|
1497
|
+
let exists = false;
|
|
1498
|
+
try {
|
|
1499
|
+
exists = fs.existsSync(absCwd) && fs.statSync(absCwd).isDirectory();
|
|
1500
|
+
}
|
|
1501
|
+
catch {
|
|
1502
|
+
exists = false;
|
|
1503
|
+
}
|
|
1504
|
+
if (!exists) {
|
|
1505
|
+
throw new Error(`Directory not found: ${absCwd}`);
|
|
1506
|
+
}
|
|
1507
|
+
const regex = globToRegex(pattern);
|
|
1508
|
+
const allFiles = [];
|
|
1509
|
+
walkDir(absCwd, absCwd, allFiles);
|
|
1510
|
+
// Normalize to forward slashes for matching (glob convention)
|
|
1511
|
+
const matched = allFiles
|
|
1512
|
+
.filter(f => regex.test(f.replace(/\\/g, '/')))
|
|
1513
|
+
.sort();
|
|
1514
|
+
if (matched.length === 0) {
|
|
1515
|
+
return `No files matched pattern: ${pattern}`;
|
|
1516
|
+
}
|
|
1517
|
+
return matched.join('\n');
|
|
1518
|
+
}
|
|
1519
|
+
/**
|
|
1520
|
+
* Search file contents using a regex pattern (or literal string fallback).
|
|
1521
|
+
*/
|
|
1522
|
+
async function grepFiles(pattern, searchPath, cwd, globPattern, caseInsensitive) {
|
|
1523
|
+
// Resolve search path relative to cwd
|
|
1524
|
+
const absSearchPath = path.isAbsolute(searchPath)
|
|
1525
|
+
? path.resolve(searchPath)
|
|
1526
|
+
: path.resolve(cwd, searchPath);
|
|
1527
|
+
// Validate access
|
|
1528
|
+
try {
|
|
1529
|
+
validatePath(absSearchPath, cwd);
|
|
1530
|
+
}
|
|
1531
|
+
catch {
|
|
1532
|
+
throw new Error(`Access denied: ${searchPath} is outside allowed scope`);
|
|
1533
|
+
}
|
|
1534
|
+
if (!fs.existsSync(absSearchPath)) {
|
|
1535
|
+
throw new Error(`Path not found: ${absSearchPath}`);
|
|
1536
|
+
}
|
|
1537
|
+
// Build the regex, fallback to literal if invalid
|
|
1538
|
+
let regex;
|
|
1539
|
+
try {
|
|
1540
|
+
regex = new RegExp(pattern, caseInsensitive ? 'i' : '');
|
|
1541
|
+
}
|
|
1542
|
+
catch {
|
|
1543
|
+
// Treat as literal string
|
|
1544
|
+
const escaped = pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1545
|
+
regex = new RegExp(escaped, caseInsensitive ? 'i' : '');
|
|
1546
|
+
}
|
|
1547
|
+
// Collect files to search
|
|
1548
|
+
let filesToSearch;
|
|
1549
|
+
const stat = fs.statSync(absSearchPath);
|
|
1550
|
+
if (stat.isFile()) {
|
|
1551
|
+
filesToSearch = [absSearchPath];
|
|
1552
|
+
}
|
|
1553
|
+
else {
|
|
1554
|
+
const all = [];
|
|
1555
|
+
walkDir(absSearchPath, absSearchPath, all);
|
|
1556
|
+
filesToSearch = all.map(f => path.join(absSearchPath, f));
|
|
1557
|
+
}
|
|
1558
|
+
// Apply glob filter if provided
|
|
1559
|
+
if (globPattern) {
|
|
1560
|
+
const globRegex = globToRegex(globPattern);
|
|
1561
|
+
filesToSearch = filesToSearch.filter(f => {
|
|
1562
|
+
const basename = path.basename(f);
|
|
1563
|
+
return globRegex.test(basename) || globRegex.test(f.replace(/\\/g, '/'));
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
const results = [];
|
|
1567
|
+
const MAX_RESULTS = 200;
|
|
1568
|
+
for (const filePath of filesToSearch) {
|
|
1569
|
+
if (results.length >= MAX_RESULTS)
|
|
1570
|
+
break;
|
|
1571
|
+
let content;
|
|
1572
|
+
try {
|
|
1573
|
+
const fileStat = fs.statSync(filePath);
|
|
1574
|
+
if (fileStat.size > 5 * 1024 * 1024)
|
|
1575
|
+
continue; // skip files > 5MB
|
|
1576
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
1577
|
+
}
|
|
1578
|
+
catch {
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1581
|
+
const lines = content.split('\n');
|
|
1582
|
+
const relPath = path.relative(cwd, filePath);
|
|
1583
|
+
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
1584
|
+
if (results.length >= MAX_RESULTS)
|
|
1585
|
+
break;
|
|
1586
|
+
if (regex.test(lines[lineIdx])) {
|
|
1587
|
+
results.push(`${relPath}:${lineIdx + 1}: ${lines[lineIdx]}`);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
if (results.length === 0) {
|
|
1592
|
+
return 'No matches found';
|
|
1593
|
+
}
|
|
1594
|
+
let output = results.join('\n');
|
|
1595
|
+
if (results.length >= MAX_RESULTS) {
|
|
1596
|
+
output += `\n(results truncated at ${MAX_RESULTS} matches)`;
|
|
1597
|
+
}
|
|
1598
|
+
return output;
|
|
1599
|
+
}
|
|
1216
1600
|
//# sourceMappingURL=tools.js.map
|