codemini-cli 0.3.4 → 0.3.6
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/README.md +20 -18
- package/package.json +6 -6
- package/souls/anime.md +12 -9
- package/souls/caveman.md +6 -6
- package/souls/ceo.md +10 -9
- package/souls/default.md +1 -1
- package/souls/pirate.md +6 -6
- package/souls/playful.md +7 -7
- package/souls/professional.md +1 -1
- package/src/cli.js +3 -1
- package/src/commands/run.js +229 -16
- package/src/core/agent-loop.js +167 -49
- package/src/core/ast.js +40 -0
- package/src/core/chat-runtime.js +720 -126
- package/src/core/command-policy.js +56 -0
- package/src/core/config-store.js +0 -3
- package/src/core/crypto-utils.js +6 -2
- package/src/core/memory-store.js +3 -3
- package/src/core/project-index.js +4 -18
- package/src/core/provider/anthropic.js +15 -2
- package/src/core/provider/anthropic.sdk-backup.js +439 -0
- package/src/core/provider/openai-compatible.js +93 -11
- package/src/core/provider/openai-compatible.sdk-backup.js +412 -0
- package/src/core/session-store.js +90 -25
- package/src/core/shell-profile.js +26 -6
- package/src/core/string-utils.js +37 -0
- package/src/core/tools.js +216 -405
- package/src/tui/chat-app.js +490 -146
- package/src/tui/tool-activity/presenters/files.js +2 -2
- package/src/tui/tool-narration.js +0 -3
- package/src/tui/tool-narration/presenters/patch.js +0 -3
package/src/core/tools.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
|
-
import fsSync from 'node:fs';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { spawn } from 'node:child_process';
|
|
5
4
|
import net from 'node:net';
|
|
5
|
+
import { escapeRegex, normalizePath } from './string-utils.js';
|
|
6
6
|
import {
|
|
7
7
|
classifyCommandIntent,
|
|
8
8
|
hasReadyOutput,
|
|
@@ -13,22 +13,24 @@ import {
|
|
|
13
13
|
terminateChild
|
|
14
14
|
} from './shell.js';
|
|
15
15
|
import { evaluateCommandPolicy } from './command-policy.js';
|
|
16
|
-
import { queryAst, readAstNode, resolveAstTarget } from './ast.js';
|
|
16
|
+
import { findEnclosingSymbol, queryAst, readAstNode, resolveAstTarget } from './ast.js';
|
|
17
17
|
import { initializeProjectIndex, queryProjectIndex, refreshIndexedFile } from './project-index.js';
|
|
18
18
|
import { checkReadDedup } from './agent-loop.js';
|
|
19
19
|
import { TOOL_SKIP_DIRS as SKIP_DIRS, TEXT_EXTENSIONS, CODE_WRITE_GUARD_EXTENSIONS, LANGUAGE_FILE_TYPES } from './constants.js';
|
|
20
|
-
import { sha256Prefixed as sha256,
|
|
20
|
+
import { sha256Prefixed as sha256, sha256 as sha256Hash } from './crypto-utils.js';
|
|
21
21
|
import { forgetMemory, listMemories, rememberMemory, searchMemories } from './memory-store.js';
|
|
22
22
|
import { normalizeTodos } from './todo-state.js';
|
|
23
23
|
const BACKGROUND_TASK_RECENT_OUTPUT_LIMIT = 80;
|
|
24
24
|
const BACKGROUND_TASK_POLL_MS = 150;
|
|
25
|
+
const MAX_AST_ENCLOSING_BYTES = 300_000;
|
|
26
|
+
const MAX_AST_ENCLOSING_LINES = 5_000;
|
|
25
27
|
const backgroundTaskRegistry = new Map();
|
|
26
28
|
let backgroundTaskCounter = 0;
|
|
27
29
|
let backgroundTaskLogCursorCounter = 0;
|
|
28
30
|
|
|
29
|
-
function realpathIfExists(targetPath) {
|
|
31
|
+
async function realpathIfExists(targetPath) {
|
|
30
32
|
try {
|
|
31
|
-
return
|
|
33
|
+
return await fs.realpath(targetPath);
|
|
32
34
|
} catch (error) {
|
|
33
35
|
if (error?.code === 'ENOENT') return null;
|
|
34
36
|
throw error;
|
|
@@ -40,11 +42,11 @@ function isWithinResolvedRoot(resolvedRoot, candidatePath) {
|
|
|
40
42
|
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
|
41
43
|
}
|
|
42
44
|
|
|
43
|
-
function resolveInWorkspace(root, targetPath = '.') {
|
|
45
|
+
async function resolveInWorkspace(root, targetPath = '.') {
|
|
44
46
|
const absRoot = path.resolve(root);
|
|
45
|
-
const realRoot =
|
|
47
|
+
const realRoot = await fs.realpath(absRoot);
|
|
46
48
|
const absTarget = path.resolve(absRoot, targetPath);
|
|
47
|
-
const realTarget = realpathIfExists(absTarget);
|
|
49
|
+
const realTarget = await realpathIfExists(absTarget);
|
|
48
50
|
if (realTarget) {
|
|
49
51
|
if (!isWithinResolvedRoot(realRoot, realTarget)) {
|
|
50
52
|
throw new Error(`Path escapes workspace: ${targetPath}`);
|
|
@@ -53,13 +55,13 @@ function resolveInWorkspace(root, targetPath = '.') {
|
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
let probe = path.dirname(absTarget);
|
|
56
|
-
while (!realpathIfExists(probe)) {
|
|
58
|
+
while (!(await realpathIfExists(probe))) {
|
|
57
59
|
const parent = path.dirname(probe);
|
|
58
60
|
if (parent === probe) break;
|
|
59
61
|
probe = parent;
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
const resolvedProbe = realpathIfExists(probe);
|
|
64
|
+
const resolvedProbe = await realpathIfExists(probe);
|
|
63
65
|
if (!resolvedProbe) {
|
|
64
66
|
throw new Error(`Path escapes workspace: ${targetPath}`);
|
|
65
67
|
}
|
|
@@ -71,12 +73,12 @@ function resolveInWorkspace(root, targetPath = '.') {
|
|
|
71
73
|
return resolvedTarget;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
function getBackgroundTasksDir(root) {
|
|
75
|
-
return path.join(resolveInWorkspace(root, '.codemini'), 'tasks');
|
|
76
|
+
async function getBackgroundTasksDir(root) {
|
|
77
|
+
return path.join(await resolveInWorkspace(root, '.codemini'), 'tasks');
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
function toWorkspaceRelative(root, absPath) {
|
|
79
|
-
return path.relative(path.resolve(root), absPath)
|
|
81
|
+
return normalizePath(path.relative(path.resolve(root), absPath));
|
|
80
82
|
}
|
|
81
83
|
|
|
82
84
|
function trimLinePreview(line, maxLen = 180) {
|
|
@@ -85,10 +87,6 @@ function trimLinePreview(line, maxLen = 180) {
|
|
|
85
87
|
return `${text.slice(0, maxLen - 3)}...`;
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
function escapeRegex(value) {
|
|
89
|
-
return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
90
|
-
}
|
|
91
|
-
|
|
92
90
|
function splitLines(text) {
|
|
93
91
|
return String(text || '').split('\n');
|
|
94
92
|
}
|
|
@@ -297,7 +295,7 @@ async function mapLimit(items, limit, worker) {
|
|
|
297
295
|
const WALKER_CONCURRENCY = 8;
|
|
298
296
|
|
|
299
297
|
async function walkTextFiles(root, startPath = '.', fileTypes = []) {
|
|
300
|
-
const abs = resolveInWorkspace(root, startPath);
|
|
298
|
+
const abs = await resolveInWorkspace(root, startPath);
|
|
301
299
|
const allowedExts = new Set((Array.isArray(fileTypes) ? fileTypes : []).map((item) => `.${String(item || '').replace(/^\./, '')}`));
|
|
302
300
|
|
|
303
301
|
async function visit(current) {
|
|
@@ -318,7 +316,7 @@ async function walkTextFiles(root, startPath = '.', fileTypes = []) {
|
|
|
318
316
|
}
|
|
319
317
|
|
|
320
318
|
async function walkWorkspaceEntries(root, startPath = '.', { includeHidden = false } = {}) {
|
|
321
|
-
const abs = resolveInWorkspace(root, startPath);
|
|
319
|
+
const abs = await resolveInWorkspace(root, startPath);
|
|
322
320
|
|
|
323
321
|
async function visit(current) {
|
|
324
322
|
const stat = await fs.stat(current);
|
|
@@ -369,41 +367,6 @@ function globToRegex(pattern) {
|
|
|
369
367
|
return new RegExp(`^${regexBody}$`);
|
|
370
368
|
}
|
|
371
369
|
|
|
372
|
-
function getLineColumnForMatch(line, query, caseSensitive = false) {
|
|
373
|
-
const haystack = caseSensitive ? line : line.toLowerCase();
|
|
374
|
-
const needle = caseSensitive ? query : query.toLowerCase();
|
|
375
|
-
const index = haystack.indexOf(needle);
|
|
376
|
-
return index === -1 ? 1 : index + 1;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function classifyMatch(preview, query) {
|
|
380
|
-
const line = String(preview || '');
|
|
381
|
-
const escaped = escapeRegex(query);
|
|
382
|
-
const normalized = line.toLowerCase();
|
|
383
|
-
const queryLower = String(query || '').toLowerCase();
|
|
384
|
-
const definitionLeadPatterns = [
|
|
385
|
-
/^\s*(?:export\s+)?(?:async\s+)?function\b/i,
|
|
386
|
-
/^\s*(?:export\s+)?class\b/i,
|
|
387
|
-
/^\s*(?:export\s+)?(?:const|let|var)\b/i,
|
|
388
|
-
/^\s*(?:export\s+)?(?:interface|type|enum)\b/i,
|
|
389
|
-
/^\s*def\b/i,
|
|
390
|
-
/^\s*(?:public|private|protected)\s+[A-Za-z0-9_<>,[\]\s?]+\s+[A-Za-z0-9_$]+\s*\(/i
|
|
391
|
-
];
|
|
392
|
-
if (definitionLeadPatterns.some((pattern) => pattern.test(line)) && normalized.includes(queryLower)) {
|
|
393
|
-
return 'definition';
|
|
394
|
-
}
|
|
395
|
-
if (new RegExp(String.raw`\b${escaped}\s*\(`, 'i').test(line)) return 'reference';
|
|
396
|
-
return 'text';
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function matchSpecificity(preview, query) {
|
|
400
|
-
const line = String(preview || '');
|
|
401
|
-
const escaped = escapeRegex(query);
|
|
402
|
-
if (new RegExp(String.raw`\b${escaped}\b`, 'i').test(line)) return 0;
|
|
403
|
-
if (line.toLowerCase().includes(String(query || '').toLowerCase())) return 1;
|
|
404
|
-
return 2;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
370
|
function findSymbolDefinition(lines, symbol) {
|
|
408
371
|
const escaped = String(symbol || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
409
372
|
const patterns = [
|
|
@@ -555,7 +518,7 @@ function extractDirectCalls(lines, symbol, maxItems = 3, excludeRange = null) {
|
|
|
555
518
|
if (excludeRange && i + 1 >= excludeRange.startLine && i + 1 <= excludeRange.endLine) continue;
|
|
556
519
|
const line = String(lines[i] || '');
|
|
557
520
|
if (!new RegExp(String.raw`\b${escaped}\s*\(`).test(line)) continue;
|
|
558
|
-
const blockLine =
|
|
521
|
+
const blockLine = findEnclosingSymbolLine(lines, i + 1);
|
|
559
522
|
const owner = blockLine ? trimLinePreview(lines[blockLine - 1], 220) : trimLinePreview(line, 220);
|
|
560
523
|
const ownerName = blockLine ? extractSymbolName(lines[blockLine - 1]) : '';
|
|
561
524
|
if (ownerName === symbol) continue;
|
|
@@ -578,7 +541,7 @@ function extractSymbolName(line) {
|
|
|
578
541
|
return match?.[1] || '';
|
|
579
542
|
}
|
|
580
543
|
|
|
581
|
-
function
|
|
544
|
+
function findEnclosingSymbolLine(lines, anchorLine) {
|
|
582
545
|
for (let i = Math.max(0, anchorLine - 1); i >= 0; i -= 1) {
|
|
583
546
|
const name = extractSymbolName(lines[i]);
|
|
584
547
|
if (name) return i + 1;
|
|
@@ -620,131 +583,8 @@ function buildUnifiedDiff(oldContent, newContent, filePath = 'file') {
|
|
|
620
583
|
return body.join('\n');
|
|
621
584
|
}
|
|
622
585
|
|
|
623
|
-
function parseUnifiedPatch(patchText) {
|
|
624
|
-
const lines = splitLines(String(patchText || ''));
|
|
625
|
-
const files = [];
|
|
626
|
-
let current = null;
|
|
627
|
-
|
|
628
|
-
const pushCurrent = () => {
|
|
629
|
-
if (current) files.push(current);
|
|
630
|
-
};
|
|
631
|
-
|
|
632
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
633
|
-
const line = lines[i];
|
|
634
|
-
if (line.startsWith('--- ')) {
|
|
635
|
-
pushCurrent();
|
|
636
|
-
current = {
|
|
637
|
-
oldPath: line.slice(4).trim(),
|
|
638
|
-
newPath: '',
|
|
639
|
-
hunks: []
|
|
640
|
-
};
|
|
641
|
-
continue;
|
|
642
|
-
}
|
|
643
|
-
if (!current) continue;
|
|
644
|
-
if (line.startsWith('+++ ')) {
|
|
645
|
-
current.newPath = line.slice(4).trim();
|
|
646
|
-
continue;
|
|
647
|
-
}
|
|
648
|
-
if (line.startsWith('@@ ')) {
|
|
649
|
-
const match = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
|
|
650
|
-
if (!match) {
|
|
651
|
-
throw new Error(`invalid patch hunk header: ${line}`);
|
|
652
|
-
}
|
|
653
|
-
const hunk = {
|
|
654
|
-
oldStart: Number(match[1]),
|
|
655
|
-
oldCount: Number(match[2] || '1'),
|
|
656
|
-
newStart: Number(match[3]),
|
|
657
|
-
newCount: Number(match[4] || '1'),
|
|
658
|
-
lines: []
|
|
659
|
-
};
|
|
660
|
-
i += 1;
|
|
661
|
-
while (i < lines.length) {
|
|
662
|
-
const hunkLine = lines[i];
|
|
663
|
-
if (hunkLine.startsWith('@@ ') || hunkLine.startsWith('--- ')) {
|
|
664
|
-
i -= 1;
|
|
665
|
-
break;
|
|
666
|
-
}
|
|
667
|
-
if (hunkLine.startsWith('\')) {
|
|
668
|
-
i += 1;
|
|
669
|
-
continue;
|
|
670
|
-
}
|
|
671
|
-
if (hunkLine === '') {
|
|
672
|
-
hunk.lines.push(' ');
|
|
673
|
-
i += 1;
|
|
674
|
-
continue;
|
|
675
|
-
}
|
|
676
|
-
if (!/^[ +\-]/.test(hunkLine)) {
|
|
677
|
-
hunk.lines.push(` ${hunkLine}`);
|
|
678
|
-
i += 1;
|
|
679
|
-
continue;
|
|
680
|
-
}
|
|
681
|
-
if (!/^[ +\-]/.test(hunkLine)) {
|
|
682
|
-
throw new Error(`invalid patch line: ${hunkLine}`);
|
|
683
|
-
}
|
|
684
|
-
hunk.lines.push(hunkLine);
|
|
685
|
-
i += 1;
|
|
686
|
-
}
|
|
687
|
-
current.hunks.push(hunk);
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
pushCurrent();
|
|
692
|
-
return files.filter((file) => file.oldPath || file.newPath);
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
function applyHunkToLines(lines, hunk) {
|
|
696
|
-
const oldChunk = [];
|
|
697
|
-
const newChunk = [];
|
|
698
|
-
for (const line of hunk.lines) {
|
|
699
|
-
if (line.startsWith(' ')) {
|
|
700
|
-
const text = line.slice(1);
|
|
701
|
-
oldChunk.push(text);
|
|
702
|
-
newChunk.push(text);
|
|
703
|
-
continue;
|
|
704
|
-
}
|
|
705
|
-
if (line.startsWith('-')) {
|
|
706
|
-
oldChunk.push(line.slice(1));
|
|
707
|
-
continue;
|
|
708
|
-
}
|
|
709
|
-
if (line.startsWith('+')) {
|
|
710
|
-
newChunk.push(line.slice(1));
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
if (oldChunk.length === 0) {
|
|
715
|
-
const insertAt = Math.max(0, Number(hunk.oldStart || 1) - 1);
|
|
716
|
-
return [...lines.slice(0, insertAt), ...newChunk, ...lines.slice(insertAt)];
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
const lastStart = Math.max(0, lines.length - oldChunk.length);
|
|
720
|
-
const matches = [];
|
|
721
|
-
for (let start = 0; start <= lastStart; start += 1) {
|
|
722
|
-
let ok = true;
|
|
723
|
-
for (let offset = 0; offset < oldChunk.length; offset += 1) {
|
|
724
|
-
if (lines[start + offset] !== oldChunk[offset]) {
|
|
725
|
-
ok = false;
|
|
726
|
-
break;
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
if (ok) {
|
|
730
|
-
matches.push(start);
|
|
731
|
-
if (matches.length > 1) break;
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
if (matches.length === 0) {
|
|
736
|
-
throw new Error('patch hunk context not found');
|
|
737
|
-
}
|
|
738
|
-
if (matches.length > 1) {
|
|
739
|
-
throw new Error('patch hunk context not unique');
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
const start = matches[0];
|
|
743
|
-
return [...lines.slice(0, start), ...newChunk, ...lines.slice(start + oldChunk.length)];
|
|
744
|
-
}
|
|
745
|
-
|
|
746
586
|
async function getFileState(root, relativePath) {
|
|
747
|
-
const target = resolveInWorkspace(root, relativePath);
|
|
587
|
+
const target = await resolveInWorkspace(root, relativePath);
|
|
748
588
|
const stat = await fs.stat(target);
|
|
749
589
|
const content = await fs.readFile(target, 'utf8');
|
|
750
590
|
return {
|
|
@@ -757,7 +597,7 @@ async function getFileState(root, relativePath) {
|
|
|
757
597
|
|
|
758
598
|
async function readFile(root, args) {
|
|
759
599
|
const normalizedArgs = normalizeReadArgs(args);
|
|
760
|
-
const target = resolveInWorkspace(root, normalizedArgs?.path);
|
|
600
|
+
const target = await resolveInWorkspace(root, normalizedArgs?.path);
|
|
761
601
|
const stat = await fs.stat(target);
|
|
762
602
|
const text = await fs.readFile(target, 'utf8');
|
|
763
603
|
const lines = splitLines(text);
|
|
@@ -777,7 +617,7 @@ async function readFile(root, args) {
|
|
|
777
617
|
endLine = Math.max(startLine, Math.min(endLine, totalLines));
|
|
778
618
|
|
|
779
619
|
const tokenSeed = `${normalizedArgs?.path}|${stat.size}|${stat.mtimeMs}|${startLine}|${endLine}`;
|
|
780
|
-
const readToken =
|
|
620
|
+
const readToken = sha256Hash(tokenSeed).slice(0, 16);
|
|
781
621
|
|
|
782
622
|
if (wantsMetadataOnly) {
|
|
783
623
|
return {
|
|
@@ -820,6 +660,11 @@ async function readFile(root, args) {
|
|
|
820
660
|
};
|
|
821
661
|
}
|
|
822
662
|
|
|
663
|
+
// Resolve enclosing structural symbol via Tree-sitter (best-effort, skipped for large files)
|
|
664
|
+
const shouldResolveEnclosing = text.length <= MAX_AST_ENCLOSING_BYTES && totalLines <= MAX_AST_ENCLOSING_LINES;
|
|
665
|
+
const anchorLine = Math.floor((startLine + endLine) / 2);
|
|
666
|
+
const enclosing = shouldResolveEnclosing ? await findEnclosingSymbol(text, normalizedArgs?.path, anchorLine) : null;
|
|
667
|
+
|
|
823
668
|
return {
|
|
824
669
|
path: normalizedArgs?.path,
|
|
825
670
|
phase: 'content',
|
|
@@ -827,7 +672,8 @@ async function readFile(root, args) {
|
|
|
827
672
|
end_line: endLine,
|
|
828
673
|
total_lines: totalLines,
|
|
829
674
|
truncated,
|
|
830
|
-
content
|
|
675
|
+
content,
|
|
676
|
+
...(enclosing ? { enclosing_symbol: enclosing.name, enclosing_kind: enclosing.kind, enclosing_line: enclosing.start_line } : {})
|
|
831
677
|
};
|
|
832
678
|
}
|
|
833
679
|
|
|
@@ -840,7 +686,7 @@ async function writeFile(root, args) {
|
|
|
840
686
|
if (rawPath === '.' || rawPath === './') {
|
|
841
687
|
throw new Error('write requires a file path, not the workspace root');
|
|
842
688
|
}
|
|
843
|
-
const target = resolveInWorkspace(root, rawPath);
|
|
689
|
+
const target = await resolveInWorkspace(root, rawPath);
|
|
844
690
|
try {
|
|
845
691
|
const stat = await fs.stat(target);
|
|
846
692
|
if (stat.isDirectory()) {
|
|
@@ -889,6 +735,63 @@ async function writeFile(root, args) {
|
|
|
889
735
|
};
|
|
890
736
|
}
|
|
891
737
|
|
|
738
|
+
async function prepareDeleteTarget(root, args) {
|
|
739
|
+
const normalizedArgs = normalizePathArgs(args, ['file', 'file_path', 'target', 'directory', 'dir']);
|
|
740
|
+
const rawPath = String(normalizedArgs?.path || '').trim();
|
|
741
|
+
if (!rawPath) {
|
|
742
|
+
throw new Error('delete requires a file or directory path');
|
|
743
|
+
}
|
|
744
|
+
const absRoot = path.resolve(root);
|
|
745
|
+
const realRoot = await fs.realpath(absRoot);
|
|
746
|
+
const originalTarget = path.resolve(absRoot, rawPath);
|
|
747
|
+
if (originalTarget === absRoot) {
|
|
748
|
+
throw new Error('delete requires a path inside the workspace, not the workspace root');
|
|
749
|
+
}
|
|
750
|
+
const resolvedTarget = await resolveInWorkspace(root, rawPath);
|
|
751
|
+
if (resolvedTarget === realRoot) {
|
|
752
|
+
throw new Error('delete requires a path inside the workspace, not the workspace root');
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
let rawStat;
|
|
756
|
+
let stat;
|
|
757
|
+
try {
|
|
758
|
+
rawStat = await fs.lstat(originalTarget);
|
|
759
|
+
} catch (error) {
|
|
760
|
+
if (error?.code === 'ENOENT') {
|
|
761
|
+
throw new Error(`delete target not found: ${rawPath}`);
|
|
762
|
+
}
|
|
763
|
+
throw error;
|
|
764
|
+
}
|
|
765
|
+
try {
|
|
766
|
+
stat = await fs.stat(resolvedTarget);
|
|
767
|
+
} catch (error) {
|
|
768
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const type = stat?.isDirectory?.() ? 'directory' : rawStat.isDirectory() ? 'directory' : 'file';
|
|
772
|
+
const pathInWorkspace = toWorkspaceRelative(root, originalTarget);
|
|
773
|
+
return {
|
|
774
|
+
originalTarget,
|
|
775
|
+
resolvedTarget,
|
|
776
|
+
path: pathInWorkspace,
|
|
777
|
+
name: path.basename(pathInWorkspace),
|
|
778
|
+
type
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async function deletePath(root, args) {
|
|
783
|
+
const target = await prepareDeleteTarget(root, args);
|
|
784
|
+
await fs.rm(target.originalTarget, { recursive: true, force: false });
|
|
785
|
+
|
|
786
|
+
return {
|
|
787
|
+
ok: true,
|
|
788
|
+
path: target.path,
|
|
789
|
+
name: target.name,
|
|
790
|
+
type: target.type,
|
|
791
|
+
deleted: true
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
|
|
892
795
|
async function runCommand(root, config, args) {
|
|
893
796
|
const command = args?.command || '';
|
|
894
797
|
if (!command.trim()) {
|
|
@@ -1086,7 +989,7 @@ async function startBackgroundTask(root, config, args) {
|
|
|
1086
989
|
const successMatchers = normalizeSuccessMatchers(args?.success_matchers || args?.successMatchers);
|
|
1087
990
|
const portProbe = Number(args?.port_probe || args?.portProbe || 0) || 0;
|
|
1088
991
|
const httpProbe = normalizeHttpProbe(args?.http_probe || args?.httpProbe);
|
|
1089
|
-
const outputDir = getBackgroundTasksDir(root);
|
|
992
|
+
const outputDir = await getBackgroundTasksDir(root);
|
|
1090
993
|
await fs.mkdir(outputDir, { recursive: true });
|
|
1091
994
|
const outputFileAbs = path.join(outputDir, `${taskId}.log`);
|
|
1092
995
|
await fs.writeFile(outputFileAbs, '', 'utf8');
|
|
@@ -1229,68 +1132,6 @@ async function stopBackgroundTask(_root, args) {
|
|
|
1229
1132
|
return { ...snapshotBackgroundTask(task), stopped: true };
|
|
1230
1133
|
}
|
|
1231
1134
|
|
|
1232
|
-
async function searchCode(root, args) {
|
|
1233
|
-
const query = String(args?.query || args?.symbol || '').trim();
|
|
1234
|
-
if (!query) throw new Error('search_code requires query');
|
|
1235
|
-
const maxResults = Math.max(1, Math.min(50, Number(args?.max_results || 12)));
|
|
1236
|
-
const caseSensitive = Boolean(args?.case_sensitive);
|
|
1237
|
-
const files = await walkTextFiles(root, args?.path || '.', normalizeFileTypes(args));
|
|
1238
|
-
const matches = [];
|
|
1239
|
-
|
|
1240
|
-
for (const filePath of files) {
|
|
1241
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
1242
|
-
const lines = splitLines(content);
|
|
1243
|
-
for (let idx = 0; idx < lines.length; idx += 1) {
|
|
1244
|
-
const line = lines[idx];
|
|
1245
|
-
const haystack = caseSensitive ? line : line.toLowerCase();
|
|
1246
|
-
const needle = caseSensitive ? query : query.toLowerCase();
|
|
1247
|
-
if (!haystack.includes(needle)) continue;
|
|
1248
|
-
matches.push({
|
|
1249
|
-
file: toWorkspaceRelative(root, filePath),
|
|
1250
|
-
line: idx + 1,
|
|
1251
|
-
column: getLineColumnForMatch(line, query, caseSensitive),
|
|
1252
|
-
preview: trimLinePreview(line),
|
|
1253
|
-
kind: classifyMatch(line, query),
|
|
1254
|
-
symbolHint: query
|
|
1255
|
-
});
|
|
1256
|
-
if (matches.length >= maxResults) {
|
|
1257
|
-
matches.sort((left, right) => {
|
|
1258
|
-
const kindRank = { definition: 0, reference: 1, text: 2 };
|
|
1259
|
-
const specificity = matchSpecificity(left.preview, query) - matchSpecificity(right.preview, query);
|
|
1260
|
-
if (specificity !== 0) return specificity;
|
|
1261
|
-
if (kindRank[left.kind] !== kindRank[right.kind]) return kindRank[left.kind] - kindRank[right.kind];
|
|
1262
|
-
return left.file.localeCompare(right.file) || left.line - right.line;
|
|
1263
|
-
});
|
|
1264
|
-
return {
|
|
1265
|
-
query,
|
|
1266
|
-
matches,
|
|
1267
|
-
definitions: matches.filter((item) => item.kind === 'definition'),
|
|
1268
|
-
references: matches.filter((item) => item.kind === 'reference'),
|
|
1269
|
-
text_matches: matches.filter((item) => item.kind === 'text'),
|
|
1270
|
-
truncated: true
|
|
1271
|
-
};
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
matches.sort((left, right) => {
|
|
1277
|
-
const kindRank = { definition: 0, reference: 1, text: 2 };
|
|
1278
|
-
const specificity = matchSpecificity(left.preview, query) - matchSpecificity(right.preview, query);
|
|
1279
|
-
if (specificity !== 0) return specificity;
|
|
1280
|
-
if (kindRank[left.kind] !== kindRank[right.kind]) return kindRank[left.kind] - kindRank[right.kind];
|
|
1281
|
-
return left.file.localeCompare(right.file) || left.line - right.line;
|
|
1282
|
-
});
|
|
1283
|
-
|
|
1284
|
-
return {
|
|
1285
|
-
query,
|
|
1286
|
-
matches,
|
|
1287
|
-
definitions: matches.filter((item) => item.kind === 'definition'),
|
|
1288
|
-
references: matches.filter((item) => item.kind === 'reference'),
|
|
1289
|
-
text_matches: matches.filter((item) => item.kind === 'text'),
|
|
1290
|
-
truncated: false
|
|
1291
|
-
};
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
1135
|
async function grep(root, args) {
|
|
1295
1136
|
const normalizedArgs = normalizePatternArgs(args, ['query', 'symbol', 'q'], ['directory', 'dir', 'cwd']);
|
|
1296
1137
|
const pattern = String(normalizedArgs?.pattern || '').trim();
|
|
@@ -1349,7 +1190,7 @@ async function glob(root, args) {
|
|
|
1349
1190
|
async function list(root, args) {
|
|
1350
1191
|
const normalizedArgs = normalizePathArgs(args, ['dir', 'directory', 'target']);
|
|
1351
1192
|
const relativePath = String(normalizedArgs?.path || '.').trim() || '.';
|
|
1352
|
-
const target = resolveInWorkspace(root, relativePath);
|
|
1193
|
+
const target = await resolveInWorkspace(root, relativePath);
|
|
1353
1194
|
const entries = await fs.readdir(target, { withFileTypes: true });
|
|
1354
1195
|
const includeHidden = Boolean(normalizedArgs?.include_hidden);
|
|
1355
1196
|
const items = entries
|
|
@@ -1529,70 +1370,6 @@ async function insertRelative(root, args, mode) {
|
|
|
1529
1370
|
return editResult(relativePath, mode, state.content, afterContent, changedLine);
|
|
1530
1371
|
}
|
|
1531
1372
|
|
|
1532
|
-
async function generateDiff(root, args) {
|
|
1533
|
-
const relativePath = String(args?.path || '').trim();
|
|
1534
|
-
if (!relativePath) throw new Error('generate_diff requires path');
|
|
1535
|
-
const state = await getFileState(root, relativePath);
|
|
1536
|
-
const newContent = String(args?.new_content || '');
|
|
1537
|
-
return {
|
|
1538
|
-
path: relativePath,
|
|
1539
|
-
old_hash: sha256(state.content),
|
|
1540
|
-
new_hash: sha256(newContent),
|
|
1541
|
-
diff: buildUnifiedDiff(state.content, newContent, relativePath)
|
|
1542
|
-
};
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
async function applyPatch(root, args) {
|
|
1546
|
-
const patchText = String(args?.patch || args?.content || '').trim();
|
|
1547
|
-
if (!patchText) throw new Error('patch requires patch content');
|
|
1548
|
-
const files = parseUnifiedPatch(patchText);
|
|
1549
|
-
if (files.length === 0) throw new Error('patch contains no file changes');
|
|
1550
|
-
|
|
1551
|
-
const results = [];
|
|
1552
|
-
for (const fileChange of files) {
|
|
1553
|
-
const newPath = String(fileChange.newPath || '').trim();
|
|
1554
|
-
const oldPath = String(fileChange.oldPath || '').trim();
|
|
1555
|
-
const targetPath = newPath && newPath !== '/dev/null' ? newPath : oldPath;
|
|
1556
|
-
if (!targetPath || targetPath === '/dev/null') {
|
|
1557
|
-
throw new Error('patch requires a target file path');
|
|
1558
|
-
}
|
|
1559
|
-
const absTarget = resolveInWorkspace(root, targetPath);
|
|
1560
|
-
let beforeContent = '';
|
|
1561
|
-
let beforeLines = [];
|
|
1562
|
-
try {
|
|
1563
|
-
beforeContent = await fs.readFile(absTarget, 'utf8');
|
|
1564
|
-
beforeLines = splitLines(beforeContent);
|
|
1565
|
-
} catch (error) {
|
|
1566
|
-
if (!(error && error.code === 'ENOENT')) throw error;
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
let nextLines = beforeLines;
|
|
1570
|
-
for (const hunk of fileChange.hunks) {
|
|
1571
|
-
nextLines = applyHunkToLines(nextLines, hunk);
|
|
1572
|
-
}
|
|
1573
|
-
const afterContent = nextLines.join('\n');
|
|
1574
|
-
|
|
1575
|
-
if (newPath === '/dev/null') {
|
|
1576
|
-
await fs.rm(absTarget, { force: true });
|
|
1577
|
-
results.push({
|
|
1578
|
-
path: targetPath,
|
|
1579
|
-
action: 'delete',
|
|
1580
|
-
changed_line: 1,
|
|
1581
|
-
diff_preview: `deleted ${targetPath}`,
|
|
1582
|
-
diff: buildUnifiedDiff(beforeContent, '', targetPath),
|
|
1583
|
-
new_hash: sha256('')
|
|
1584
|
-
});
|
|
1585
|
-
continue;
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
await fs.mkdir(path.dirname(absTarget), { recursive: true });
|
|
1589
|
-
await fs.writeFile(absTarget, afterContent, 'utf8');
|
|
1590
|
-
results.push(editResult(targetPath, beforeContent ? 'patch' : 'create', beforeContent, afterContent, 1));
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1593
|
-
return results.length === 1 ? results[0] : { ok: true, files: results };
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
1373
|
async function openTarget(root, args) {
|
|
1597
1374
|
const file = String(args?.file || args?.path || '').trim();
|
|
1598
1375
|
if (!file) throw new Error('open_target requires file');
|
|
@@ -1843,7 +1620,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1843
1620
|
function: {
|
|
1844
1621
|
name: 'read',
|
|
1845
1622
|
description:
|
|
1846
|
-
'Inspect
|
|
1623
|
+
'Inspect code or text files. Use read(path) for normal file or line-window reads, read(ast_target=...) for a node-scoped AST read, and read(path, query=..., capture_name=...) to run an inline Tree-sitter query before returning the first matched node. Prefer the AST forms when targeting a function, class, or method and you want tighter context. Demo-style aliases like file_path, offset, and limit are accepted. Use metadata_only=true only when you want file metadata without content. Do not use run with cat, head, or tail for file reads.',
|
|
1847
1624
|
parameters: {
|
|
1848
1625
|
type: 'object',
|
|
1849
1626
|
properties: {
|
|
@@ -1856,7 +1633,11 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1856
1633
|
max_chars: { type: 'number', description: 'Max chars to return' },
|
|
1857
1634
|
include_content: { type: 'boolean', description: 'Legacy compatibility flag. Content is returned by default.' },
|
|
1858
1635
|
read_token: { type: 'string', description: 'Legacy compatibility token. No longer required for content reads.' },
|
|
1859
|
-
metadata_only: { type: 'boolean', description: 'Set true to return metadata without content.' }
|
|
1636
|
+
metadata_only: { type: 'boolean', description: 'Set true to return metadata without content.' },
|
|
1637
|
+
ast_target: { type: 'object', description: 'AST target from ast_query or a prior AST selection. When provided, read returns that node instead of a line window.' },
|
|
1638
|
+
query: { type: 'string', description: 'Optional Tree-sitter query to run inline before reading the first matched AST node. Use with path for one-shot function/class/method reads.' },
|
|
1639
|
+
capture_name: { type: 'string', description: 'Optional capture name to select when query is provided.' },
|
|
1640
|
+
language: { type: 'string', description: 'Optional Tree-sitter language override for AST reads or inline queries.' }
|
|
1860
1641
|
},
|
|
1861
1642
|
required: []
|
|
1862
1643
|
}
|
|
@@ -1900,6 +1681,26 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1900
1681
|
}
|
|
1901
1682
|
}
|
|
1902
1683
|
},
|
|
1684
|
+
{
|
|
1685
|
+
type: 'function',
|
|
1686
|
+
function: {
|
|
1687
|
+
name: 'glob',
|
|
1688
|
+
description:
|
|
1689
|
+
'Find files by glob pattern. Use this when you already know a filename pattern such as src/**/*.ts. Aliases like query and directory are accepted.',
|
|
1690
|
+
parameters: {
|
|
1691
|
+
type: 'object',
|
|
1692
|
+
properties: {
|
|
1693
|
+
pattern: { type: 'string', description: 'Glob pattern' },
|
|
1694
|
+
path: { type: 'string', description: 'Directory to search' },
|
|
1695
|
+
query: { type: 'string', description: 'Alias for pattern' },
|
|
1696
|
+
directory: { type: 'string', description: 'Alias for path' },
|
|
1697
|
+
include_hidden: { type: 'boolean', description: 'Include dotfiles' },
|
|
1698
|
+
max_results: { type: 'number', description: 'Max results' }
|
|
1699
|
+
},
|
|
1700
|
+
required: ['pattern']
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
},
|
|
1903
1704
|
{
|
|
1904
1705
|
type: 'function',
|
|
1905
1706
|
function: {
|
|
@@ -1923,7 +1724,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1923
1724
|
function: {
|
|
1924
1725
|
name: 'edit',
|
|
1925
1726
|
description:
|
|
1926
|
-
'Edit existing files. Prefer one of these shapes: 1) {file, old_text, new_text} for exact text replacement, 2) {file, symbol, edit:{kind:"replace_block", new_content:"..."}} for block replacement, 3) {file, anchor_text, position:"before"|"after", content:"..."} for inserts. Demo-style aliases {file_path, old_string, new_string} are also accepted. Read first unless the exact target is already known. Prefer this over write for existing code changes.',
|
|
1727
|
+
'Edit existing files. Prefer one of these shapes: 1) {file, old_text, new_text} for exact text replacement, 2) {file, symbol, edit:{kind:"replace_block", new_content:"..."}} for block replacement, 3) {file, anchor_text, position:"before"|"after", content:"..."} for inserts. Demo-style aliases {file_path, old_string, new_string} are also accepted. Read first unless the exact target is already known, and prefer read(ast_target=...) or read(path, query=...) before symbol- or block-level edits when you want tighter context. Prefer this over write for existing code changes.',
|
|
1927
1728
|
parameters: {
|
|
1928
1729
|
type: 'object',
|
|
1929
1730
|
properties: {
|
|
@@ -1971,6 +1772,25 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1971
1772
|
}
|
|
1972
1773
|
}
|
|
1973
1774
|
},
|
|
1775
|
+
{
|
|
1776
|
+
type: 'function',
|
|
1777
|
+
function: {
|
|
1778
|
+
name: 'delete',
|
|
1779
|
+
description:
|
|
1780
|
+
'Delete a file or directory inside the workspace. Use path, file, or file_path to point at the target. Missing targets fail. Workspace escape attempts are rejected.',
|
|
1781
|
+
parameters: {
|
|
1782
|
+
type: 'object',
|
|
1783
|
+
properties: {
|
|
1784
|
+
path: { type: 'string', description: 'File or directory path to delete' },
|
|
1785
|
+
file: { type: 'string', description: 'Alias for path' },
|
|
1786
|
+
file_path: { type: 'string', description: 'Alias for path' },
|
|
1787
|
+
directory: { type: 'string', description: 'Alias for path' },
|
|
1788
|
+
dir: { type: 'string', description: 'Alias for path' }
|
|
1789
|
+
},
|
|
1790
|
+
required: ['path']
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
},
|
|
1974
1794
|
{
|
|
1975
1795
|
type: 'function',
|
|
1976
1796
|
function: {
|
|
@@ -2053,7 +1873,7 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2053
1873
|
function: {
|
|
2054
1874
|
name: 'ast_query',
|
|
2055
1875
|
description:
|
|
2056
|
-
'Run a Tree-sitter query on a code file and return ast_target objects. Use this when you
|
|
1876
|
+
'Run a Tree-sitter query on a code file and return ast_target objects. Use this for advanced AST workflows such as multi-match selection, explicit node caching, or when you plan to reuse ast_target across follow-up reads or edits. For a common one-shot function, class, or method read, prefer read(path, query=...) or read(ast_target=...).',
|
|
2057
1877
|
parameters: {
|
|
2058
1878
|
type: 'object',
|
|
2059
1879
|
properties: {
|
|
@@ -2067,32 +1887,12 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2067
1887
|
}
|
|
2068
1888
|
}
|
|
2069
1889
|
},
|
|
2070
|
-
glob: {
|
|
2071
|
-
type: 'function',
|
|
2072
|
-
function: {
|
|
2073
|
-
name: 'glob',
|
|
2074
|
-
description:
|
|
2075
|
-
'Find files by glob pattern. Use this when you already know a filename pattern such as src/**/*.ts. Aliases like query and directory are accepted.',
|
|
2076
|
-
parameters: {
|
|
2077
|
-
type: 'object',
|
|
2078
|
-
properties: {
|
|
2079
|
-
pattern: { type: 'string', description: 'Glob pattern' },
|
|
2080
|
-
path: { type: 'string', description: 'Directory to search' },
|
|
2081
|
-
query: { type: 'string', description: 'Alias for pattern' },
|
|
2082
|
-
directory: { type: 'string', description: 'Alias for path' },
|
|
2083
|
-
include_hidden: { type: 'boolean', description: 'Include dotfiles' },
|
|
2084
|
-
max_results: { type: 'number', description: 'Max results' }
|
|
2085
|
-
},
|
|
2086
|
-
required: ['pattern']
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
},
|
|
2090
1890
|
read_ast_node: {
|
|
2091
1891
|
type: 'function',
|
|
2092
1892
|
function: {
|
|
2093
1893
|
name: 'read_ast_node',
|
|
2094
1894
|
description:
|
|
2095
|
-
'Read a previously selected AST node with compact structural context. Use this after ast_query before a scoped structural edit.',
|
|
1895
|
+
'Read a previously selected AST node with compact structural context. Use this after ast_query when you want an explicit follow-up read of a cached node before a scoped structural edit. For common one-shot AST reads, prefer read(ast_target=...) or read(path, query=...).',
|
|
2096
1896
|
parameters: {
|
|
2097
1897
|
type: 'object',
|
|
2098
1898
|
properties: {
|
|
@@ -2104,36 +1904,6 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2104
1904
|
}
|
|
2105
1905
|
}
|
|
2106
1906
|
},
|
|
2107
|
-
generate_diff: {
|
|
2108
|
-
type: 'function',
|
|
2109
|
-
function: {
|
|
2110
|
-
name: 'generate_diff',
|
|
2111
|
-
description: 'Generate a unified diff for proposed content. Use this when you want to preview or prepare a patch before applying it.',
|
|
2112
|
-
parameters: {
|
|
2113
|
-
type: 'object',
|
|
2114
|
-
properties: {
|
|
2115
|
-
path: { type: 'string' },
|
|
2116
|
-
new_content: { type: 'string' }
|
|
2117
|
-
},
|
|
2118
|
-
required: ['path', 'new_content']
|
|
2119
|
-
}
|
|
2120
|
-
}
|
|
2121
|
-
},
|
|
2122
|
-
patch: {
|
|
2123
|
-
type: 'function',
|
|
2124
|
-
function: {
|
|
2125
|
-
name: 'patch',
|
|
2126
|
-
description: 'Apply one or more unified diff hunks to workspace files. Use this for prepared unified diffs instead of ad-hoc shell patching.',
|
|
2127
|
-
parameters: {
|
|
2128
|
-
type: 'object',
|
|
2129
|
-
properties: {
|
|
2130
|
-
patch: { type: 'string' },
|
|
2131
|
-
content: { type: 'string' }
|
|
2132
|
-
},
|
|
2133
|
-
required: ['patch']
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
},
|
|
2137
1907
|
remember_user: {
|
|
2138
1908
|
type: 'function',
|
|
2139
1909
|
function: {
|
|
@@ -2274,19 +2044,63 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2274
2044
|
const definitions = [...primaryDefinitions];
|
|
2275
2045
|
|
|
2276
2046
|
const handlers = {
|
|
2277
|
-
read: (args) =>
|
|
2278
|
-
|
|
2047
|
+
read: async (args) => {
|
|
2048
|
+
const inlineQuery = String(args?.query || '').trim();
|
|
2049
|
+
const directAstTarget = args?.ast_target;
|
|
2050
|
+
|
|
2051
|
+
if (directAstTarget) {
|
|
2052
|
+
const result = await readAstNode(workspaceRoot, {
|
|
2053
|
+
...args,
|
|
2054
|
+
path: args?.path || directAstTarget?.path,
|
|
2055
|
+
ast_target: directAstTarget
|
|
2056
|
+
});
|
|
2057
|
+
if (directAstTarget?.path) rememberAstSelection(directAstTarget.path, directAstTarget);
|
|
2058
|
+
const readPath = String(result?.path || directAstTarget?.path || '').trim();
|
|
2059
|
+
if (readPath) lastReadPath = readPath;
|
|
2060
|
+
return result;
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
if (inlineQuery) {
|
|
2064
|
+
const queryResult = await queryAst(workspaceRoot, args);
|
|
2065
|
+
const firstTarget = queryResult?.matches?.[0]?.ast_target;
|
|
2066
|
+
if (!firstTarget) {
|
|
2067
|
+
return {
|
|
2068
|
+
path: String(args?.path || '').trim(),
|
|
2069
|
+
language: queryResult?.language,
|
|
2070
|
+
query: inlineQuery,
|
|
2071
|
+
capture_name: String(args?.capture_name || '').trim() || undefined,
|
|
2072
|
+
matches: 0,
|
|
2073
|
+
content: ''
|
|
2074
|
+
};
|
|
2075
|
+
}
|
|
2076
|
+
rememberAstSelection(firstTarget.path, firstTarget);
|
|
2077
|
+
const result = await readAstNode(workspaceRoot, {
|
|
2078
|
+
...args,
|
|
2079
|
+
path: firstTarget.path,
|
|
2080
|
+
ast_target: firstTarget
|
|
2081
|
+
});
|
|
2082
|
+
const readPath = String(result?.path || firstTarget?.path || '').trim();
|
|
2083
|
+
if (readPath) lastReadPath = readPath;
|
|
2084
|
+
return {
|
|
2085
|
+
...result,
|
|
2086
|
+
query: inlineQuery,
|
|
2087
|
+
capture_name: String(args?.capture_name || '').trim() || undefined,
|
|
2088
|
+
matches: queryResult.matches.length
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
const result = await readFile(workspaceRoot, {
|
|
2279
2093
|
...args,
|
|
2280
2094
|
default_lines: config.context?.read_file_default_lines ?? 220,
|
|
2281
2095
|
max_chars:
|
|
2282
2096
|
typeof args?.max_chars === 'number'
|
|
2283
2097
|
? args.max_chars
|
|
2284
2098
|
: config.context?.read_file_max_chars ?? 24000
|
|
2285
|
-
})
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2099
|
+
});
|
|
2100
|
+
const readPath = String(result?.path || args?.path || '').trim();
|
|
2101
|
+
if (readPath) lastReadPath = readPath;
|
|
2102
|
+
return result;
|
|
2103
|
+
},
|
|
2290
2104
|
query_project_index: async (args) => {
|
|
2291
2105
|
await ensureProjectIndex();
|
|
2292
2106
|
return queryProjectIndex(workspaceRoot, args);
|
|
@@ -2317,24 +2131,27 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2317
2131
|
if (result?.path) await refreshProjectFile(result.path);
|
|
2318
2132
|
return result;
|
|
2319
2133
|
},
|
|
2320
|
-
|
|
2321
|
-
patch: async (args) => {
|
|
2134
|
+
write: async (args) => {
|
|
2322
2135
|
await ensureProjectIndex();
|
|
2323
|
-
const result = await
|
|
2136
|
+
const result = await writeFile(workspaceRoot, args);
|
|
2324
2137
|
if (result?.path) await refreshProjectFile(result.path);
|
|
2325
|
-
if (Array.isArray(result?.files)) {
|
|
2326
|
-
for (const item of result.files) {
|
|
2327
|
-
if (item?.path) await refreshProjectFile(item.path);
|
|
2328
|
-
}
|
|
2329
|
-
}
|
|
2330
2138
|
return result;
|
|
2331
2139
|
},
|
|
2332
|
-
|
|
2140
|
+
delete: Object.assign(async (args) => {
|
|
2333
2141
|
await ensureProjectIndex();
|
|
2334
|
-
const result = await
|
|
2142
|
+
const result = await deletePath(workspaceRoot, args);
|
|
2335
2143
|
if (result?.path) await refreshProjectFile(result.path);
|
|
2336
2144
|
return result;
|
|
2337
|
-
},
|
|
2145
|
+
}, {
|
|
2146
|
+
prepareApproval: async (args) => {
|
|
2147
|
+
const target = await prepareDeleteTarget(workspaceRoot, args);
|
|
2148
|
+
return {
|
|
2149
|
+
path: target.path,
|
|
2150
|
+
name: target.name,
|
|
2151
|
+
type: target.type
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
}),
|
|
2338
2155
|
update_todos: async (args = {}) => {
|
|
2339
2156
|
const oldTodos = normalizeTodos(typeof getTodos === 'function' ? getTodos() : []);
|
|
2340
2157
|
const nextTodos = normalizeTodos(args?.todos);
|
|
@@ -2427,13 +2244,18 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2427
2244
|
read(result) {
|
|
2428
2245
|
if (typeof result === 'string') return result;
|
|
2429
2246
|
if (!result || typeof result !== 'object') return String(result);
|
|
2247
|
+
if (result.node && typeof result.content === 'string') {
|
|
2248
|
+
const header = `[AST: ${result.path || '?'} ${result.node.node_type || 'node'} ${result.node.start_line || '?'}-${result.node.end_line || '?'}${result.matches ? `, matches ${result.matches}` : ''}]`;
|
|
2249
|
+
return `${header}\n${result.content}`;
|
|
2250
|
+
}
|
|
2430
2251
|
// Phase 1 metadata: small, return as-is
|
|
2431
2252
|
if (result.phase === 'metadata') {
|
|
2432
2253
|
return JSON.stringify(result);
|
|
2433
2254
|
}
|
|
2434
2255
|
// Phase 2 content: structured header + head/tail content
|
|
2435
2256
|
if (result.phase === 'content') {
|
|
2436
|
-
const
|
|
2257
|
+
const enclosing = result.enclosing_symbol ? `, inside ${result.enclosing_kind || 'symbol'} ${result.enclosing_symbol}` : '';
|
|
2258
|
+
const header = `[File: ${result.path}, lines ${result.start_line || 1}-${result.end_line || '?'}${result.total_lines ? ` of ${result.total_lines}` : ''}${result.truncated ? ', truncated' : ''}${enclosing}]`;
|
|
2437
2259
|
const content = result.content || '';
|
|
2438
2260
|
if (typeof content !== 'string' || content.length <= 3000) {
|
|
2439
2261
|
return `${header}\n${content}`;
|
|
@@ -2545,6 +2367,14 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2545
2367
|
return summary;
|
|
2546
2368
|
},
|
|
2547
2369
|
|
|
2370
|
+
delete(result) {
|
|
2371
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
2372
|
+
if (result.ok === false) return JSON.stringify(result);
|
|
2373
|
+
const kind = result.type || 'item';
|
|
2374
|
+
const target = result.path || '';
|
|
2375
|
+
return `[delete: ${kind}] deleted ${target}`;
|
|
2376
|
+
},
|
|
2377
|
+
|
|
2548
2378
|
run(result) {
|
|
2549
2379
|
if (!result || typeof result !== 'object') return String(result);
|
|
2550
2380
|
if (result.background) {
|
|
@@ -2598,25 +2428,6 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2598
2428
|
return `removed ${Number(result?.removed || 0)} memory item(s)`;
|
|
2599
2429
|
},
|
|
2600
2430
|
|
|
2601
|
-
generate_diff(result) {
|
|
2602
|
-
if (!result || typeof result !== 'object') return String(result);
|
|
2603
|
-
const p = result.path || '';
|
|
2604
|
-
const diff = result.diff || '';
|
|
2605
|
-
if (diff.length <= 2000) return `${p ? `[${p}]\n` : ''}${diff}`;
|
|
2606
|
-
return `${p ? `[${p}]\n` : ''}${diff.slice(0, 1997)}...\n[diff truncated: ${diff.length} chars total]`;
|
|
2607
|
-
},
|
|
2608
|
-
|
|
2609
|
-
patch(result) {
|
|
2610
|
-
if (!result || typeof result !== 'object') return String(result);
|
|
2611
|
-
if (Array.isArray(result.files)) {
|
|
2612
|
-
const names = result.files.slice(0, 10).map((f) => typeof f === 'string' ? f : f.path || '?');
|
|
2613
|
-
return `patched ${result.files.length} file(s): ${names.join(', ')}${result.files.length > 10 ? ` ... +${result.files.length - 10} more` : ''}`;
|
|
2614
|
-
}
|
|
2615
|
-
const p = result.path || '';
|
|
2616
|
-
const line = result.changed_line || 0;
|
|
2617
|
-
return `patched ${p}${line > 0 ? ` @L${line}` : ''}${result.ok === false ? ` [FAILED: ${result.error || ''}]` : ''}`;
|
|
2618
|
-
},
|
|
2619
|
-
|
|
2620
2431
|
ast_query(result) {
|
|
2621
2432
|
if (!result || typeof result !== 'object') return String(result);
|
|
2622
2433
|
if (!Array.isArray(result.matches)) return JSON.stringify(result);
|