codemini-cli 0.3.5 → 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 +5 -2
- package/src/cli.js +3 -1
- package/src/commands/run.js +229 -16
- package/src/core/agent-loop.js +159 -47
- package/src/core/ast.js +40 -0
- package/src/core/chat-runtime.js +712 -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/openai-compatible.js +15 -2
- package/src/core/session-store.js +82 -25
- package/src/core/shell-profile.js +17 -1
- package/src/core/string-utils.js +37 -0
- package/src/core/tools.js +152 -393
- package/src/tui/chat-app.js +461 -147
- 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');
|
|
@@ -1904,6 +1681,26 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1904
1681
|
}
|
|
1905
1682
|
}
|
|
1906
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
|
+
},
|
|
1907
1704
|
{
|
|
1908
1705
|
type: 'function',
|
|
1909
1706
|
function: {
|
|
@@ -1975,6 +1772,25 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1975
1772
|
}
|
|
1976
1773
|
}
|
|
1977
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
|
+
},
|
|
1978
1794
|
{
|
|
1979
1795
|
type: 'function',
|
|
1980
1796
|
function: {
|
|
@@ -2071,26 +1887,6 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2071
1887
|
}
|
|
2072
1888
|
}
|
|
2073
1889
|
},
|
|
2074
|
-
glob: {
|
|
2075
|
-
type: 'function',
|
|
2076
|
-
function: {
|
|
2077
|
-
name: 'glob',
|
|
2078
|
-
description:
|
|
2079
|
-
'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.',
|
|
2080
|
-
parameters: {
|
|
2081
|
-
type: 'object',
|
|
2082
|
-
properties: {
|
|
2083
|
-
pattern: { type: 'string', description: 'Glob pattern' },
|
|
2084
|
-
path: { type: 'string', description: 'Directory to search' },
|
|
2085
|
-
query: { type: 'string', description: 'Alias for pattern' },
|
|
2086
|
-
directory: { type: 'string', description: 'Alias for path' },
|
|
2087
|
-
include_hidden: { type: 'boolean', description: 'Include dotfiles' },
|
|
2088
|
-
max_results: { type: 'number', description: 'Max results' }
|
|
2089
|
-
},
|
|
2090
|
-
required: ['pattern']
|
|
2091
|
-
}
|
|
2092
|
-
}
|
|
2093
|
-
},
|
|
2094
1890
|
read_ast_node: {
|
|
2095
1891
|
type: 'function',
|
|
2096
1892
|
function: {
|
|
@@ -2108,36 +1904,6 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2108
1904
|
}
|
|
2109
1905
|
}
|
|
2110
1906
|
},
|
|
2111
|
-
generate_diff: {
|
|
2112
|
-
type: 'function',
|
|
2113
|
-
function: {
|
|
2114
|
-
name: 'generate_diff',
|
|
2115
|
-
description: 'Generate a unified diff for proposed content. Use this when you want to preview or prepare a patch before applying it.',
|
|
2116
|
-
parameters: {
|
|
2117
|
-
type: 'object',
|
|
2118
|
-
properties: {
|
|
2119
|
-
path: { type: 'string' },
|
|
2120
|
-
new_content: { type: 'string' }
|
|
2121
|
-
},
|
|
2122
|
-
required: ['path', 'new_content']
|
|
2123
|
-
}
|
|
2124
|
-
}
|
|
2125
|
-
},
|
|
2126
|
-
patch: {
|
|
2127
|
-
type: 'function',
|
|
2128
|
-
function: {
|
|
2129
|
-
name: 'patch',
|
|
2130
|
-
description: 'Apply one or more unified diff hunks to workspace files. Use this for prepared unified diffs instead of ad-hoc shell patching.',
|
|
2131
|
-
parameters: {
|
|
2132
|
-
type: 'object',
|
|
2133
|
-
properties: {
|
|
2134
|
-
patch: { type: 'string' },
|
|
2135
|
-
content: { type: 'string' }
|
|
2136
|
-
},
|
|
2137
|
-
required: ['patch']
|
|
2138
|
-
}
|
|
2139
|
-
}
|
|
2140
|
-
},
|
|
2141
1907
|
remember_user: {
|
|
2142
1908
|
type: 'function',
|
|
2143
1909
|
function: {
|
|
@@ -2365,24 +2131,27 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2365
2131
|
if (result?.path) await refreshProjectFile(result.path);
|
|
2366
2132
|
return result;
|
|
2367
2133
|
},
|
|
2368
|
-
|
|
2369
|
-
patch: async (args) => {
|
|
2134
|
+
write: async (args) => {
|
|
2370
2135
|
await ensureProjectIndex();
|
|
2371
|
-
const result = await
|
|
2136
|
+
const result = await writeFile(workspaceRoot, args);
|
|
2372
2137
|
if (result?.path) await refreshProjectFile(result.path);
|
|
2373
|
-
if (Array.isArray(result?.files)) {
|
|
2374
|
-
for (const item of result.files) {
|
|
2375
|
-
if (item?.path) await refreshProjectFile(item.path);
|
|
2376
|
-
}
|
|
2377
|
-
}
|
|
2378
2138
|
return result;
|
|
2379
2139
|
},
|
|
2380
|
-
|
|
2140
|
+
delete: Object.assign(async (args) => {
|
|
2381
2141
|
await ensureProjectIndex();
|
|
2382
|
-
const result = await
|
|
2142
|
+
const result = await deletePath(workspaceRoot, args);
|
|
2383
2143
|
if (result?.path) await refreshProjectFile(result.path);
|
|
2384
2144
|
return result;
|
|
2385
|
-
},
|
|
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
|
+
}),
|
|
2386
2155
|
update_todos: async (args = {}) => {
|
|
2387
2156
|
const oldTodos = normalizeTodos(typeof getTodos === 'function' ? getTodos() : []);
|
|
2388
2157
|
const nextTodos = normalizeTodos(args?.todos);
|
|
@@ -2485,7 +2254,8 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2485
2254
|
}
|
|
2486
2255
|
// Phase 2 content: structured header + head/tail content
|
|
2487
2256
|
if (result.phase === 'content') {
|
|
2488
|
-
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}]`;
|
|
2489
2259
|
const content = result.content || '';
|
|
2490
2260
|
if (typeof content !== 'string' || content.length <= 3000) {
|
|
2491
2261
|
return `${header}\n${content}`;
|
|
@@ -2597,6 +2367,14 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2597
2367
|
return summary;
|
|
2598
2368
|
},
|
|
2599
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
|
+
|
|
2600
2378
|
run(result) {
|
|
2601
2379
|
if (!result || typeof result !== 'object') return String(result);
|
|
2602
2380
|
if (result.background) {
|
|
@@ -2650,25 +2428,6 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2650
2428
|
return `removed ${Number(result?.removed || 0)} memory item(s)`;
|
|
2651
2429
|
},
|
|
2652
2430
|
|
|
2653
|
-
generate_diff(result) {
|
|
2654
|
-
if (!result || typeof result !== 'object') return String(result);
|
|
2655
|
-
const p = result.path || '';
|
|
2656
|
-
const diff = result.diff || '';
|
|
2657
|
-
if (diff.length <= 2000) return `${p ? `[${p}]\n` : ''}${diff}`;
|
|
2658
|
-
return `${p ? `[${p}]\n` : ''}${diff.slice(0, 1997)}...\n[diff truncated: ${diff.length} chars total]`;
|
|
2659
|
-
},
|
|
2660
|
-
|
|
2661
|
-
patch(result) {
|
|
2662
|
-
if (!result || typeof result !== 'object') return String(result);
|
|
2663
|
-
if (Array.isArray(result.files)) {
|
|
2664
|
-
const names = result.files.slice(0, 10).map((f) => typeof f === 'string' ? f : f.path || '?');
|
|
2665
|
-
return `patched ${result.files.length} file(s): ${names.join(', ')}${result.files.length > 10 ? ` ... +${result.files.length - 10} more` : ''}`;
|
|
2666
|
-
}
|
|
2667
|
-
const p = result.path || '';
|
|
2668
|
-
const line = result.changed_line || 0;
|
|
2669
|
-
return `patched ${p}${line > 0 ? ` @L${line}` : ''}${result.ok === false ? ` [FAILED: ${result.error || ''}]` : ''}`;
|
|
2670
|
-
},
|
|
2671
|
-
|
|
2672
2431
|
ast_query(result) {
|
|
2673
2432
|
if (!result || typeof result !== 'object') return String(result);
|
|
2674
2433
|
if (!Array.isArray(result.matches)) return JSON.stringify(result);
|