@cdoing/core 0.1.0
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/agents/coordinator.d.ts +114 -0
- package/dist/agents/coordinator.d.ts.map +1 -0
- package/dist/agents/coordinator.js +158 -0
- package/dist/agents/coordinator.js.map +1 -0
- package/dist/context-providers/clipboard.d.ts +13 -0
- package/dist/context-providers/clipboard.d.ts.map +1 -0
- package/dist/context-providers/clipboard.js +53 -0
- package/dist/context-providers/clipboard.js.map +1 -0
- package/dist/context-providers/codebase.d.ts +46 -0
- package/dist/context-providers/codebase.d.ts.map +1 -0
- package/dist/context-providers/codebase.js +273 -0
- package/dist/context-providers/codebase.js.map +1 -0
- package/dist/context-providers/diff.d.ts +18 -0
- package/dist/context-providers/diff.d.ts.map +1 -0
- package/dist/context-providers/diff.js +63 -0
- package/dist/context-providers/diff.js.map +1 -0
- package/dist/context-providers/docs.d.ts +21 -0
- package/dist/context-providers/docs.d.ts.map +1 -0
- package/dist/context-providers/docs.js +180 -0
- package/dist/context-providers/docs.js.map +1 -0
- package/dist/context-providers/file-include.d.ts +13 -0
- package/dist/context-providers/file-include.d.ts.map +1 -0
- package/dist/context-providers/file-include.js +82 -0
- package/dist/context-providers/file-include.js.map +1 -0
- package/dist/context-providers/folder.d.ts +19 -0
- package/dist/context-providers/folder.d.ts.map +1 -0
- package/dist/context-providers/folder.js +130 -0
- package/dist/context-providers/folder.js.map +1 -0
- package/dist/context-providers/git.d.ts +19 -0
- package/dist/context-providers/git.d.ts.map +1 -0
- package/dist/context-providers/git.js +74 -0
- package/dist/context-providers/git.js.map +1 -0
- package/dist/context-providers/index.d.ts +26 -0
- package/dist/context-providers/index.d.ts.map +1 -0
- package/dist/context-providers/index.js +37 -0
- package/dist/context-providers/index.js.map +1 -0
- package/dist/context-providers/open-files.d.ts +25 -0
- package/dist/context-providers/open-files.d.ts.map +1 -0
- package/dist/context-providers/open-files.js +134 -0
- package/dist/context-providers/open-files.js.map +1 -0
- package/dist/context-providers/problems.d.ts +24 -0
- package/dist/context-providers/problems.d.ts.map +1 -0
- package/dist/context-providers/problems.js +97 -0
- package/dist/context-providers/problems.js.map +1 -0
- package/dist/context-providers/registry.d.ts +61 -0
- package/dist/context-providers/registry.d.ts.map +1 -0
- package/dist/context-providers/registry.js +92 -0
- package/dist/context-providers/registry.js.map +1 -0
- package/dist/context-providers/terminal.d.ts +25 -0
- package/dist/context-providers/terminal.d.ts.map +1 -0
- package/dist/context-providers/terminal.js +55 -0
- package/dist/context-providers/terminal.js.map +1 -0
- package/dist/context-providers/tree.d.ts +29 -0
- package/dist/context-providers/tree.d.ts.map +1 -0
- package/dist/context-providers/tree.js +172 -0
- package/dist/context-providers/tree.js.map +1 -0
- package/dist/context-providers/types.d.ts +72 -0
- package/dist/context-providers/types.d.ts.map +1 -0
- package/dist/context-providers/types.js +10 -0
- package/dist/context-providers/types.js.map +1 -0
- package/dist/context-providers/url.d.ts +27 -0
- package/dist/context-providers/url.d.ts.map +1 -0
- package/dist/context-providers/url.js +131 -0
- package/dist/context-providers/url.js.map +1 -0
- package/dist/effort/index.d.ts +78 -0
- package/dist/effort/index.d.ts.map +1 -0
- package/dist/effort/index.js +146 -0
- package/dist/effort/index.js.map +1 -0
- package/dist/hooks/index.d.ts +47 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +151 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +152 -0
- package/dist/index.js.map +1 -0
- package/dist/indexing/chunker.d.ts +25 -0
- package/dist/indexing/chunker.d.ts.map +1 -0
- package/dist/indexing/chunker.js +217 -0
- package/dist/indexing/chunker.js.map +1 -0
- package/dist/indexing/database.d.ts +49 -0
- package/dist/indexing/database.d.ts.map +1 -0
- package/dist/indexing/database.js +287 -0
- package/dist/indexing/database.js.map +1 -0
- package/dist/indexing/index.d.ts +9 -0
- package/dist/indexing/index.d.ts.map +1 -0
- package/dist/indexing/index.js +13 -0
- package/dist/indexing/index.js.map +1 -0
- package/dist/indexing/indexer.d.ts +63 -0
- package/dist/indexing/indexer.d.ts.map +1 -0
- package/dist/indexing/indexer.js +352 -0
- package/dist/indexing/indexer.js.map +1 -0
- package/dist/indexing/recent-edits-cache.d.ts +77 -0
- package/dist/indexing/recent-edits-cache.d.ts.map +1 -0
- package/dist/indexing/recent-edits-cache.js +123 -0
- package/dist/indexing/recent-edits-cache.js.map +1 -0
- package/dist/indexing/types.d.ts +39 -0
- package/dist/indexing/types.d.ts.map +1 -0
- package/dist/indexing/types.js +6 -0
- package/dist/indexing/types.js.map +1 -0
- package/dist/mcp/index.d.ts +33 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +37 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/manager.d.ts +123 -0
- package/dist/mcp/manager.d.ts.map +1 -0
- package/dist/mcp/manager.js +331 -0
- package/dist/mcp/manager.js.map +1 -0
- package/dist/oauth.d.ts +33 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +312 -0
- package/dist/oauth.js.map +1 -0
- package/dist/permissions/index.d.ts +216 -0
- package/dist/permissions/index.d.ts.map +1 -0
- package/dist/permissions/index.js +938 -0
- package/dist/permissions/index.js.map +1 -0
- package/dist/plan/index.d.ts +20 -0
- package/dist/plan/index.d.ts.map +1 -0
- package/dist/plan/index.js +24 -0
- package/dist/plan/index.js.map +1 -0
- package/dist/plan/manager.d.ts +101 -0
- package/dist/plan/manager.d.ts.map +1 -0
- package/dist/plan/manager.js +170 -0
- package/dist/plan/manager.js.map +1 -0
- package/dist/rules/index.d.ts +28 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +31 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/manager.d.ts +77 -0
- package/dist/rules/manager.d.ts.map +1 -0
- package/dist/rules/manager.js +279 -0
- package/dist/rules/manager.js.map +1 -0
- package/dist/rules/types.d.ts +34 -0
- package/dist/rules/types.d.ts.map +1 -0
- package/dist/rules/types.js +9 -0
- package/dist/rules/types.js.map +1 -0
- package/dist/sandbox/filesystem.d.ts +20 -0
- package/dist/sandbox/filesystem.d.ts.map +1 -0
- package/dist/sandbox/filesystem.js +141 -0
- package/dist/sandbox/filesystem.js.map +1 -0
- package/dist/sandbox/index.d.ts +4 -0
- package/dist/sandbox/index.d.ts.map +1 -0
- package/dist/sandbox/index.js +8 -0
- package/dist/sandbox/index.js.map +1 -0
- package/dist/sandbox/manager.d.ts +47 -0
- package/dist/sandbox/manager.d.ts.map +1 -0
- package/dist/sandbox/manager.js +220 -0
- package/dist/sandbox/manager.js.map +1 -0
- package/dist/sandbox/network.d.ts +14 -0
- package/dist/sandbox/network.d.ts.map +1 -0
- package/dist/sandbox/network.js +87 -0
- package/dist/sandbox/network.js.map +1 -0
- package/dist/sandbox/types.d.ts +42 -0
- package/dist/sandbox/types.d.ts.map +1 -0
- package/dist/sandbox/types.js +25 -0
- package/dist/sandbox/types.js.map +1 -0
- package/dist/tools/ast-edit.d.ts +57 -0
- package/dist/tools/ast-edit.d.ts.map +1 -0
- package/dist/tools/ast-edit.js +443 -0
- package/dist/tools/ast-edit.js.map +1 -0
- package/dist/tools/code-verify.d.ts +8 -0
- package/dist/tools/code-verify.d.ts.map +1 -0
- package/dist/tools/code-verify.js +159 -0
- package/dist/tools/code-verify.js.map +1 -0
- package/dist/tools/codebase-search.d.ts +17 -0
- package/dist/tools/codebase-search.d.ts.map +1 -0
- package/dist/tools/codebase-search.js +104 -0
- package/dist/tools/codebase-search.js.map +1 -0
- package/dist/tools/file-delete.d.ts +26 -0
- package/dist/tools/file-delete.d.ts.map +1 -0
- package/dist/tools/file-delete.js +179 -0
- package/dist/tools/file-delete.js.map +1 -0
- package/dist/tools/file-edit.d.ts +10 -0
- package/dist/tools/file-edit.d.ts.map +1 -0
- package/dist/tools/file-edit.js +138 -0
- package/dist/tools/file-edit.js.map +1 -0
- package/dist/tools/file-read.d.ts +12 -0
- package/dist/tools/file-read.d.ts.map +1 -0
- package/dist/tools/file-read.js +211 -0
- package/dist/tools/file-read.js.map +1 -0
- package/dist/tools/file-run.d.ts +10 -0
- package/dist/tools/file-run.d.ts.map +1 -0
- package/dist/tools/file-run.js +179 -0
- package/dist/tools/file-run.js.map +1 -0
- package/dist/tools/file-write.d.ts +10 -0
- package/dist/tools/file-write.d.ts.map +1 -0
- package/dist/tools/file-write.js +134 -0
- package/dist/tools/file-write.js.map +1 -0
- package/dist/tools/glob-search.d.ts +8 -0
- package/dist/tools/glob-search.d.ts.map +1 -0
- package/dist/tools/glob-search.js +108 -0
- package/dist/tools/glob-search.js.map +1 -0
- package/dist/tools/grep-search.d.ts +8 -0
- package/dist/tools/grep-search.d.ts.map +1 -0
- package/dist/tools/grep-search.js +139 -0
- package/dist/tools/grep-search.js.map +1 -0
- package/dist/tools/list-dir.d.ts +16 -0
- package/dist/tools/list-dir.d.ts.map +1 -0
- package/dist/tools/list-dir.js +183 -0
- package/dist/tools/list-dir.js.map +1 -0
- package/dist/tools/multi-edit.d.ts +16 -0
- package/dist/tools/multi-edit.d.ts.map +1 -0
- package/dist/tools/multi-edit.js +163 -0
- package/dist/tools/multi-edit.js.map +1 -0
- package/dist/tools/notebook-edit.d.ts +31 -0
- package/dist/tools/notebook-edit.d.ts.map +1 -0
- package/dist/tools/notebook-edit.js +321 -0
- package/dist/tools/notebook-edit.js.map +1 -0
- package/dist/tools/registry.d.ts +16 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +41 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/shell-exec.d.ts +12 -0
- package/dist/tools/shell-exec.d.ts.map +1 -0
- package/dist/tools/shell-exec.js +261 -0
- package/dist/tools/shell-exec.js.map +1 -0
- package/dist/tools/sub-agent-manager.d.ts +57 -0
- package/dist/tools/sub-agent-manager.d.ts.map +1 -0
- package/dist/tools/sub-agent-manager.js +153 -0
- package/dist/tools/sub-agent-manager.js.map +1 -0
- package/dist/tools/sub-agent-status.d.ts +12 -0
- package/dist/tools/sub-agent-status.d.ts.map +1 -0
- package/dist/tools/sub-agent-status.js +59 -0
- package/dist/tools/sub-agent-status.js.map +1 -0
- package/dist/tools/sub-agent-terminate.d.ts +12 -0
- package/dist/tools/sub-agent-terminate.d.ts.map +1 -0
- package/dist/tools/sub-agent-terminate.js +55 -0
- package/dist/tools/sub-agent-terminate.js.map +1 -0
- package/dist/tools/sub-agent.d.ts +34 -0
- package/dist/tools/sub-agent.d.ts.map +1 -0
- package/dist/tools/sub-agent.js +140 -0
- package/dist/tools/sub-agent.js.map +1 -0
- package/dist/tools/system-info.d.ts +24 -0
- package/dist/tools/system-info.d.ts.map +1 -0
- package/dist/tools/system-info.js +220 -0
- package/dist/tools/system-info.js.map +1 -0
- package/dist/tools/todo.d.ts +16 -0
- package/dist/tools/todo.d.ts.map +1 -0
- package/dist/tools/todo.js +144 -0
- package/dist/tools/todo.js.map +1 -0
- package/dist/tools/types.d.ts +20 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +3 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/view-diff.d.ts +11 -0
- package/dist/tools/view-diff.d.ts.map +1 -0
- package/dist/tools/view-diff.js +88 -0
- package/dist/tools/view-diff.js.map +1 -0
- package/dist/tools/view-repo-map.d.ts +18 -0
- package/dist/tools/view-repo-map.d.ts.map +1 -0
- package/dist/tools/view-repo-map.js +245 -0
- package/dist/tools/view-repo-map.js.map +1 -0
- package/dist/tools/web-fetch.d.ts +13 -0
- package/dist/tools/web-fetch.d.ts.map +1 -0
- package/dist/tools/web-fetch.js +106 -0
- package/dist/tools/web-fetch.js.map +1 -0
- package/dist/tools/web-search.d.ts +10 -0
- package/dist/tools/web-search.d.ts.map +1 -0
- package/dist/tools/web-search.js +106 -0
- package/dist/tools/web-search.js.map +1 -0
- package/dist/utils/gitignore.d.ts +10 -0
- package/dist/utils/gitignore.d.ts.map +1 -0
- package/dist/utils/gitignore.js +104 -0
- package/dist/utils/gitignore.js.map +1 -0
- package/dist/utils/lazy-apply.d.ts +45 -0
- package/dist/utils/lazy-apply.d.ts.map +1 -0
- package/dist/utils/lazy-apply.js +164 -0
- package/dist/utils/lazy-apply.js.map +1 -0
- package/dist/utils/memory.d.ts +36 -0
- package/dist/utils/memory.d.ts.map +1 -0
- package/dist/utils/memory.js +136 -0
- package/dist/utils/memory.js.map +1 -0
- package/dist/utils/path-matching.d.ts +24 -0
- package/dist/utils/path-matching.d.ts.map +1 -0
- package/dist/utils/path-matching.js +116 -0
- package/dist/utils/path-matching.js.map +1 -0
- package/dist/utils/path-safety.d.ts +13 -0
- package/dist/utils/path-safety.d.ts.map +1 -0
- package/dist/utils/path-safety.js +54 -0
- package/dist/utils/path-safety.js.map +1 -0
- package/dist/utils/project-config.d.ts +18 -0
- package/dist/utils/project-config.d.ts.map +1 -0
- package/dist/utils/project-config.js +76 -0
- package/dist/utils/project-config.js.map +1 -0
- package/dist/utils/search-match.d.ts +63 -0
- package/dist/utils/search-match.d.ts.map +1 -0
- package/dist/utils/search-match.js +426 -0
- package/dist/utils/search-match.js.map +1 -0
- package/dist/utils/shell-paths.d.ts +17 -0
- package/dist/utils/shell-paths.d.ts.map +1 -0
- package/dist/utils/shell-paths.js +107 -0
- package/dist/utils/shell-paths.js.map +1 -0
- package/dist/utils/streaming-diff.d.ts +45 -0
- package/dist/utils/streaming-diff.d.ts.map +1 -0
- package/dist/utils/streaming-diff.js +230 -0
- package/dist/utils/streaming-diff.js.map +1 -0
- package/dist/utils/todo.d.ts +47 -0
- package/dist/utils/todo.d.ts.map +1 -0
- package/dist/utils/todo.js +102 -0
- package/dist/utils/todo.js.map +1 -0
- package/package.json +23 -0
- package/src/agents/coordinator.ts +240 -0
- package/src/context-providers/clipboard.ts +48 -0
- package/src/context-providers/codebase.ts +274 -0
- package/src/context-providers/diff.ts +66 -0
- package/src/context-providers/docs.ts +160 -0
- package/src/context-providers/file-include.ts +54 -0
- package/src/context-providers/folder.ts +106 -0
- package/src/context-providers/git.ts +72 -0
- package/src/context-providers/index.ts +26 -0
- package/src/context-providers/open-files.ts +113 -0
- package/src/context-providers/problems.ts +100 -0
- package/src/context-providers/registry.ts +99 -0
- package/src/context-providers/terminal.ts +58 -0
- package/src/context-providers/tree.ts +161 -0
- package/src/context-providers/types.ts +84 -0
- package/src/context-providers/url.ts +138 -0
- package/src/effort/index.ts +177 -0
- package/src/hooks/index.ts +148 -0
- package/src/index.ts +114 -0
- package/src/indexing/README.md +267 -0
- package/src/indexing/chunker.ts +206 -0
- package/src/indexing/database.ts +299 -0
- package/src/indexing/index.ts +15 -0
- package/src/indexing/indexer.ts +383 -0
- package/src/indexing/recent-edits-cache.ts +150 -0
- package/src/indexing/types.ts +44 -0
- package/src/mcp/index.ts +33 -0
- package/src/mcp/manager.ts +385 -0
- package/src/oauth.ts +330 -0
- package/src/permissions/index.ts +1011 -0
- package/src/plan/index.ts +20 -0
- package/src/plan/manager.ts +233 -0
- package/src/rules/index.ts +28 -0
- package/src/rules/manager.ts +276 -0
- package/src/rules/types.ts +40 -0
- package/src/sandbox/filesystem.ts +135 -0
- package/src/sandbox/index.ts +9 -0
- package/src/sandbox/manager.ts +213 -0
- package/src/sandbox/network.ts +101 -0
- package/src/sandbox/types.ts +63 -0
- package/src/tools/ast-edit.ts +493 -0
- package/src/tools/code-verify.ts +143 -0
- package/src/tools/codebase-search.ts +117 -0
- package/src/tools/file-delete.ts +155 -0
- package/src/tools/file-edit.ts +115 -0
- package/src/tools/file-read.ts +195 -0
- package/src/tools/file-run.ts +158 -0
- package/src/tools/file-write.ts +104 -0
- package/src/tools/glob-search.ts +80 -0
- package/src/tools/grep-search.ts +120 -0
- package/src/tools/list-dir.ts +172 -0
- package/src/tools/multi-edit.ts +138 -0
- package/src/tools/notebook-edit.ts +342 -0
- package/src/tools/registry.ts +43 -0
- package/src/tools/shell-exec.ts +251 -0
- package/src/tools/sub-agent-manager.ts +183 -0
- package/src/tools/sub-agent-status.ts +67 -0
- package/src/tools/sub-agent-terminate.ts +62 -0
- package/src/tools/sub-agent.ts +162 -0
- package/src/tools/system-info.ts +248 -0
- package/src/tools/todo.ts +149 -0
- package/src/tools/types.ts +21 -0
- package/src/tools/view-diff.ts +99 -0
- package/src/tools/view-repo-map.ts +249 -0
- package/src/tools/web-fetch.ts +118 -0
- package/src/tools/web-search.ts +129 -0
- package/src/utils/gitignore.ts +73 -0
- package/src/utils/lazy-apply.ts +189 -0
- package/src/utils/memory.ts +124 -0
- package/src/utils/path-matching.ts +84 -0
- package/src/utils/path-safety.ts +19 -0
- package/src/utils/project-config.ts +41 -0
- package/src/utils/search-match.ts +495 -0
- package/src/utils/shell-paths.ts +79 -0
- package/src/utils/streaming-diff.ts +260 -0
- package/src/utils/todo.ts +115 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Search Match — multi-strategy string matching for file editing.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by Continue's cascading match strategies:
|
|
5
|
+
* 1. Exact match
|
|
6
|
+
* 2. Trimmed match (ignore leading/trailing whitespace)
|
|
7
|
+
* 3. Case-insensitive match
|
|
8
|
+
* 4. Whitespace-ignored match (strips all whitespace, maps back to original positions)
|
|
9
|
+
*
|
|
10
|
+
* This makes the edit tool far more resilient to LLM formatting differences.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export interface SearchMatchResult {
|
|
14
|
+
startIndex: number;
|
|
15
|
+
endIndex: number;
|
|
16
|
+
strategyName: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type MatchStrategy = (fileContent: string, searchContent: string) => SearchMatchResult | null;
|
|
20
|
+
|
|
21
|
+
/** 1. Exact string match */
|
|
22
|
+
function exactMatch(fileContent: string, searchContent: string): SearchMatchResult | null {
|
|
23
|
+
const idx = fileContent.indexOf(searchContent);
|
|
24
|
+
if (idx !== -1) {
|
|
25
|
+
return { startIndex: idx, endIndex: idx + searchContent.length, strategyName: "exact" };
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 2. Trimmed match — ignore leading/trailing whitespace */
|
|
31
|
+
function trimmedMatch(fileContent: string, searchContent: string): SearchMatchResult | null {
|
|
32
|
+
const trimmed = searchContent.trim();
|
|
33
|
+
if (!trimmed) return null;
|
|
34
|
+
if (trimmed === searchContent) return null;
|
|
35
|
+
const idx = fileContent.indexOf(trimmed);
|
|
36
|
+
if (idx !== -1) {
|
|
37
|
+
return { startIndex: idx, endIndex: idx + trimmed.length, strategyName: "trimmed" };
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 3. Case-insensitive match */
|
|
43
|
+
function caseInsensitiveMatch(fileContent: string, searchContent: string): SearchMatchResult | null {
|
|
44
|
+
const idx = fileContent.toLowerCase().indexOf(searchContent.toLowerCase());
|
|
45
|
+
if (idx !== -1) {
|
|
46
|
+
return { startIndex: idx, endIndex: idx + searchContent.length, strategyName: "caseInsensitive" };
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 4. Whitespace-ignored match — strips all whitespace, maps positions back */
|
|
52
|
+
function whitespaceIgnoredMatch(fileContent: string, searchContent: string): SearchMatchResult | null {
|
|
53
|
+
const strippedFile = fileContent.replace(/\s/g, "");
|
|
54
|
+
const strippedSearch = searchContent.replace(/\s/g, "");
|
|
55
|
+
|
|
56
|
+
if (!strippedSearch) return null;
|
|
57
|
+
|
|
58
|
+
const strippedIdx = strippedFile.indexOf(strippedSearch);
|
|
59
|
+
if (strippedIdx === -1) return null;
|
|
60
|
+
|
|
61
|
+
// Map stripped position back to original
|
|
62
|
+
let originalStart = -1;
|
|
63
|
+
let nonWsCount = 0;
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < fileContent.length; i++) {
|
|
66
|
+
if (!/\s/.test(fileContent[i])) {
|
|
67
|
+
if (nonWsCount === strippedIdx) {
|
|
68
|
+
originalStart = i;
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
nonWsCount++;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (originalStart === -1) return null;
|
|
76
|
+
|
|
77
|
+
// Find end position
|
|
78
|
+
let originalEnd = originalStart;
|
|
79
|
+
let matchedChars = 0;
|
|
80
|
+
|
|
81
|
+
for (let i = originalStart; i < fileContent.length; i++) {
|
|
82
|
+
if (!/\s/.test(fileContent[i])) {
|
|
83
|
+
matchedChars++;
|
|
84
|
+
if (matchedChars === strippedSearch.length) {
|
|
85
|
+
originalEnd = i + 1;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
originalEnd = i + 1;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { startIndex: originalStart, endIndex: originalEnd, strategyName: "whitespaceIgnored" };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 5. Jaro-Winkler fuzzy match — line-based sliding window with 90%+ threshold */
|
|
96
|
+
function fuzzyMatch(fileContent: string, searchContent: string): SearchMatchResult | null {
|
|
97
|
+
const searchBlock = searchContent.trim();
|
|
98
|
+
if (searchBlock.length < 10) return null; // Too short for meaningful fuzzy match
|
|
99
|
+
|
|
100
|
+
const searchLines = searchBlock.split("\n");
|
|
101
|
+
const fileLines = fileContent.split("\n");
|
|
102
|
+
if (searchLines.length > fileLines.length) return null;
|
|
103
|
+
|
|
104
|
+
let bestMatch: SearchMatchResult | null = null;
|
|
105
|
+
let bestSimilarity = 0;
|
|
106
|
+
const threshold = 0.9;
|
|
107
|
+
|
|
108
|
+
// Sliding window of searchLines.length over fileLines
|
|
109
|
+
for (let i = 0; i <= fileLines.length - searchLines.length; i++) {
|
|
110
|
+
const candidate = fileLines.slice(i, i + searchLines.length).join("\n").trim();
|
|
111
|
+
if (candidate.length < 5) continue;
|
|
112
|
+
|
|
113
|
+
const similarity = jaroWinklerSimilarity(searchBlock, candidate);
|
|
114
|
+
if (similarity >= threshold && similarity > bestSimilarity) {
|
|
115
|
+
const before = fileLines.slice(0, i).join("\n");
|
|
116
|
+
const startIndex = before.length + (i > 0 ? 1 : 0);
|
|
117
|
+
bestMatch = {
|
|
118
|
+
startIndex,
|
|
119
|
+
endIndex: startIndex + candidate.length,
|
|
120
|
+
strategyName: "fuzzy",
|
|
121
|
+
};
|
|
122
|
+
bestSimilarity = similarity;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return bestMatch;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function jaroSimilarity(s1: string, s2: string): number {
|
|
130
|
+
if (s1 === s2) return 1.0;
|
|
131
|
+
if (!s1.length || !s2.length) return 0.0;
|
|
132
|
+
|
|
133
|
+
const matchDist = Math.floor(Math.max(s1.length, s2.length) / 2) - 1;
|
|
134
|
+
if (matchDist < 0) return 0.0;
|
|
135
|
+
|
|
136
|
+
const s1m = new Array(s1.length).fill(false);
|
|
137
|
+
const s2m = new Array(s2.length).fill(false);
|
|
138
|
+
let matches = 0;
|
|
139
|
+
let transpositions = 0;
|
|
140
|
+
|
|
141
|
+
for (let i = 0; i < s1.length; i++) {
|
|
142
|
+
const lo = Math.max(0, i - matchDist);
|
|
143
|
+
const hi = Math.min(i + matchDist + 1, s2.length);
|
|
144
|
+
for (let j = lo; j < hi; j++) {
|
|
145
|
+
if (s2m[j] || s1[i] !== s2[j]) continue;
|
|
146
|
+
s1m[i] = s2m[j] = true;
|
|
147
|
+
matches++;
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!matches) return 0.0;
|
|
153
|
+
|
|
154
|
+
let k = 0;
|
|
155
|
+
for (let i = 0; i < s1.length; i++) {
|
|
156
|
+
if (!s1m[i]) continue;
|
|
157
|
+
while (!s2m[k]) k++;
|
|
158
|
+
if (s1[i] !== s2[k]) transpositions++;
|
|
159
|
+
k++;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return (matches / s1.length + matches / s2.length + (matches - transpositions / 2) / matches) / 3;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function jaroWinklerSimilarity(s1: string, s2: string): number {
|
|
166
|
+
const jaro = jaroSimilarity(s1, s2);
|
|
167
|
+
if (jaro < 0.7) return jaro;
|
|
168
|
+
let prefix = 0;
|
|
169
|
+
const max = Math.min(4, Math.min(s1.length, s2.length));
|
|
170
|
+
for (let i = 0; i < max; i++) {
|
|
171
|
+
if (s1[i] === s2[i]) prefix++;
|
|
172
|
+
else break;
|
|
173
|
+
}
|
|
174
|
+
return jaro + prefix * 0.1 * (1 - jaro);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const strategies: MatchStrategy[] = [
|
|
178
|
+
exactMatch,
|
|
179
|
+
trimmedMatch,
|
|
180
|
+
caseInsensitiveMatch,
|
|
181
|
+
whitespaceIgnoredMatch,
|
|
182
|
+
fuzzyMatch,
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Find a match for searchContent in fileContent using cascading strategies.
|
|
187
|
+
* Returns null if no strategy matches.
|
|
188
|
+
*/
|
|
189
|
+
export function findSearchMatch(fileContent: string, searchContent: string): SearchMatchResult | null {
|
|
190
|
+
if (!searchContent.trim()) {
|
|
191
|
+
return { startIndex: 0, endIndex: 0, strategyName: "empty" };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const strategy of strategies) {
|
|
195
|
+
const result = strategy(fileContent, searchContent);
|
|
196
|
+
if (result) return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Find all matches for searchContent in fileContent.
|
|
204
|
+
*/
|
|
205
|
+
export function findAllSearchMatches(fileContent: string, searchContent: string): SearchMatchResult[] {
|
|
206
|
+
if (!searchContent.trim()) {
|
|
207
|
+
return [{ startIndex: 0, endIndex: 0, strategyName: "empty" }];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const matches: SearchMatchResult[] = [];
|
|
211
|
+
let remaining = fileContent;
|
|
212
|
+
let offset = 0;
|
|
213
|
+
|
|
214
|
+
while (remaining.length > 0) {
|
|
215
|
+
const match = findSearchMatch(remaining, searchContent);
|
|
216
|
+
if (!match) break;
|
|
217
|
+
|
|
218
|
+
const adjusted: SearchMatchResult = {
|
|
219
|
+
startIndex: match.startIndex + offset,
|
|
220
|
+
endIndex: match.endIndex + offset,
|
|
221
|
+
strategyName: match.strategyName,
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
// Prevent infinite loops
|
|
225
|
+
if (matches.length > 0 && adjusted.startIndex <= matches[matches.length - 1].startIndex) break;
|
|
226
|
+
|
|
227
|
+
matches.push(adjusted);
|
|
228
|
+
offset = adjusted.endIndex;
|
|
229
|
+
remaining = fileContent.slice(offset);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return matches;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Execute a single find-and-replace on content.
|
|
237
|
+
* Uses multi-strategy matching. Returns the new content.
|
|
238
|
+
* Throws if match not found or multiple matches without replace_all.
|
|
239
|
+
*
|
|
240
|
+
* When a non-exact strategy is used, indentation is automatically
|
|
241
|
+
* adjusted so that new_string matches the file's original indentation
|
|
242
|
+
* rather than whatever the LLM happened to produce.
|
|
243
|
+
*/
|
|
244
|
+
export function executeFindAndReplace(
|
|
245
|
+
content: string,
|
|
246
|
+
oldString: string,
|
|
247
|
+
newString: string,
|
|
248
|
+
replaceAll: boolean,
|
|
249
|
+
): { result: string; count: number; strategy: string } {
|
|
250
|
+
if (oldString === newString) {
|
|
251
|
+
throw new Error("old_string and new_string are identical — no change needed");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const matches = findAllSearchMatches(content, oldString);
|
|
255
|
+
|
|
256
|
+
if (matches.length === 0) {
|
|
257
|
+
throw new Error(`old_string not found in file (tried exact, trimmed, case-insensitive, and whitespace-ignored matching)`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!replaceAll && matches.length > 1) {
|
|
261
|
+
throw new Error(`Multiple matches found (${matches.length}). Use replace_all or add more surrounding context to make old_string unique.`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const toReplace = replaceAll ? matches : [matches[0]];
|
|
265
|
+
const strategy = matches[0].strategyName;
|
|
266
|
+
|
|
267
|
+
// Apply replacements in reverse order to preserve positions
|
|
268
|
+
let result = content;
|
|
269
|
+
for (let i = toReplace.length - 1; i >= 0; i--) {
|
|
270
|
+
const m = toReplace[i];
|
|
271
|
+
const adjustedNewString = normalizeIndentation(content, m.startIndex, oldString, newString, strategy === "exact");
|
|
272
|
+
result = result.substring(0, m.startIndex) + adjustedNewString + result.substring(m.endIndex);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return { result, count: toReplace.length, strategy };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Normalize indentation of new_string so it matches the file's context.
|
|
280
|
+
*
|
|
281
|
+
* The key idea: preserve the RELATIVE indentation within new_string,
|
|
282
|
+
* but set the BASE indentation to match the target (the file's actual
|
|
283
|
+
* indentation at the match point).
|
|
284
|
+
*
|
|
285
|
+
* Example:
|
|
286
|
+
* File: " update: (id, data) => {\n return id;\n }" (base: 2 spaces)
|
|
287
|
+
* new_string: " modifyItem: (id, data) => {\n return id;\n }" (base: 6 spaces)
|
|
288
|
+
* Result: " modifyItem: (id, data) => {\n return id;\n }" (base: 2 spaces, relative preserved)
|
|
289
|
+
*/
|
|
290
|
+
function normalizeIndentation(
|
|
291
|
+
fileContent: string,
|
|
292
|
+
matchStartIndex: number,
|
|
293
|
+
_oldString: string,
|
|
294
|
+
newString: string,
|
|
295
|
+
_isExact: boolean,
|
|
296
|
+
): string {
|
|
297
|
+
// Get the full line indentation at the match point in the file
|
|
298
|
+
const fileLineIndent = getLineIndent(fileContent, matchStartIndex);
|
|
299
|
+
|
|
300
|
+
// How much of that indent is BEFORE matchStartIndex?
|
|
301
|
+
// (already in the file, outside the matched range — it stays in the output)
|
|
302
|
+
//
|
|
303
|
+
// For exact match: match includes the indent → preMatchIndent = ""
|
|
304
|
+
// For trimmed match: match starts after indent → preMatchIndent = fileLineIndent
|
|
305
|
+
const lineStartPos = matchStartIndex - fileLineIndent.length;
|
|
306
|
+
const preMatchIndent = fileContent.substring(lineStartPos, matchStartIndex);
|
|
307
|
+
|
|
308
|
+
// The first line of the replacement needs LESS indent because preMatchIndent
|
|
309
|
+
// is already in the file before the insertion point.
|
|
310
|
+
// Subsequent lines start on fresh lines (after \n) — they need the full indent.
|
|
311
|
+
const firstLineTargetIndent = fileLineIndent.substring(preMatchIndent.length);
|
|
312
|
+
|
|
313
|
+
// What base indentation does new_string currently have?
|
|
314
|
+
const newBaseIndent = getLeadingWhitespace(newString);
|
|
315
|
+
|
|
316
|
+
// If first-line target already matches new_string's base, no adjustment needed
|
|
317
|
+
if (firstLineTargetIndent === newBaseIndent) return newString;
|
|
318
|
+
|
|
319
|
+
// Re-indent every line: strip newBaseIndent, prepend the correct target indent.
|
|
320
|
+
const lines = newString.split("\n");
|
|
321
|
+
const adjusted = lines.map((line, i) => {
|
|
322
|
+
const lineIndent = getLeadingWhitespace(line);
|
|
323
|
+
const content = line.substring(lineIndent.length);
|
|
324
|
+
|
|
325
|
+
// Empty/whitespace-only lines: keep as-is
|
|
326
|
+
if (!content) return line;
|
|
327
|
+
|
|
328
|
+
// Compute relative indent (how much deeper than new_string's base)
|
|
329
|
+
let relativeIndent = "";
|
|
330
|
+
if (lineIndent.length >= newBaseIndent.length && lineIndent.startsWith(newBaseIndent)) {
|
|
331
|
+
relativeIndent = lineIndent.substring(newBaseIndent.length);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// First line: use firstLineTargetIndent (accounts for pre-match whitespace already in file)
|
|
335
|
+
// Subsequent lines: use full fileLineIndent (they start on fresh lines after \n)
|
|
336
|
+
const baseIndent = i === 0 ? firstLineTargetIndent : fileLineIndent;
|
|
337
|
+
|
|
338
|
+
return baseIndent + relativeIndent + content;
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
return adjusted.join("\n");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Get the indentation (leading whitespace) at a position in the file */
|
|
345
|
+
function getLineIndent(content: string, position: number): string {
|
|
346
|
+
// Walk backwards to find the start of the line
|
|
347
|
+
let lineStart = position;
|
|
348
|
+
while (lineStart > 0 && content[lineStart - 1] !== "\n") {
|
|
349
|
+
lineStart--;
|
|
350
|
+
}
|
|
351
|
+
// Extract leading whitespace from line start to the first non-whitespace
|
|
352
|
+
let indent = "";
|
|
353
|
+
for (let i = lineStart; i < content.length && (content[i] === " " || content[i] === "\t"); i++) {
|
|
354
|
+
indent += content[i];
|
|
355
|
+
}
|
|
356
|
+
return indent;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Get leading whitespace of a string's first line */
|
|
360
|
+
function getLeadingWhitespace(text: string): string {
|
|
361
|
+
const match = text.match(/^([ \t]*)/);
|
|
362
|
+
return match ? match[1] : "";
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Execute multiple sequential find-and-replace operations.
|
|
368
|
+
* Each edit operates on the result of the previous one.
|
|
369
|
+
* Atomic: if any edit fails, none are applied (throws).
|
|
370
|
+
*/
|
|
371
|
+
export function executeMultiFindAndReplace(
|
|
372
|
+
content: string,
|
|
373
|
+
edits: Array<{ old_string: string; new_string: string; replace_all?: boolean }>,
|
|
374
|
+
): { result: string; totalCount: number } {
|
|
375
|
+
let current = content;
|
|
376
|
+
let totalCount = 0;
|
|
377
|
+
|
|
378
|
+
for (let i = 0; i < edits.length; i++) {
|
|
379
|
+
const edit = edits[i];
|
|
380
|
+
try {
|
|
381
|
+
const { result, count } = executeFindAndReplace(
|
|
382
|
+
current,
|
|
383
|
+
edit.old_string,
|
|
384
|
+
edit.new_string,
|
|
385
|
+
edit.replace_all ?? false,
|
|
386
|
+
);
|
|
387
|
+
current = result;
|
|
388
|
+
totalCount += count;
|
|
389
|
+
} catch (err) {
|
|
390
|
+
throw new Error(`Edit ${i + 1}/${edits.length} failed: ${(err as Error).message}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return { result: current, totalCount };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ── Unified Diff Application ────────────────────────────────────────────────
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Check if a string looks like a unified diff format.
|
|
401
|
+
*/
|
|
402
|
+
export function isUnifiedDiff(text: string): boolean {
|
|
403
|
+
return /^@@\s+-\d+(?:,\d+)?\s+\+\d+(?:,\d+)?\s+@@/m.test(text);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Apply a unified diff to file content.
|
|
408
|
+
* Supports standard @@ -n,m +n,m @@ hunk headers.
|
|
409
|
+
* Tolerates minor whitespace differences in context lines.
|
|
410
|
+
*/
|
|
411
|
+
export function applyUnifiedDiff(originalContent: string, diff: string): string {
|
|
412
|
+
const lines = diff.split("\n");
|
|
413
|
+
const sourceLines = originalContent.split("\n");
|
|
414
|
+
const resultLines = [...sourceLines];
|
|
415
|
+
|
|
416
|
+
// Parse hunks
|
|
417
|
+
const hunks: Array<{
|
|
418
|
+
oldStart: number;
|
|
419
|
+
oldCount: number;
|
|
420
|
+
newStart: number;
|
|
421
|
+
newCount: number;
|
|
422
|
+
lines: Array<{ type: "context" | "add" | "remove"; text: string }>;
|
|
423
|
+
}> = [];
|
|
424
|
+
|
|
425
|
+
let currentHunk: typeof hunks[0] | null = null;
|
|
426
|
+
|
|
427
|
+
for (const line of lines) {
|
|
428
|
+
// Skip file headers
|
|
429
|
+
if (line.startsWith("---") || line.startsWith("+++")) continue;
|
|
430
|
+
|
|
431
|
+
// Parse hunk header
|
|
432
|
+
const hunkMatch = line.match(/^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?\s+@@/);
|
|
433
|
+
if (hunkMatch) {
|
|
434
|
+
if (currentHunk) hunks.push(currentHunk);
|
|
435
|
+
currentHunk = {
|
|
436
|
+
oldStart: parseInt(hunkMatch[1], 10),
|
|
437
|
+
oldCount: parseInt(hunkMatch[2] ?? "1", 10),
|
|
438
|
+
newStart: parseInt(hunkMatch[3], 10),
|
|
439
|
+
newCount: parseInt(hunkMatch[4] ?? "1", 10),
|
|
440
|
+
lines: [],
|
|
441
|
+
};
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (!currentHunk) continue;
|
|
446
|
+
|
|
447
|
+
if (line.startsWith("+")) {
|
|
448
|
+
currentHunk.lines.push({ type: "add", text: line.substring(1) });
|
|
449
|
+
} else if (line.startsWith("-")) {
|
|
450
|
+
currentHunk.lines.push({ type: "remove", text: line.substring(1) });
|
|
451
|
+
} else if (line.startsWith(" ") || line === "") {
|
|
452
|
+
currentHunk.lines.push({ type: "context", text: line.startsWith(" ") ? line.substring(1) : line });
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (currentHunk) hunks.push(currentHunk);
|
|
457
|
+
|
|
458
|
+
if (hunks.length === 0) {
|
|
459
|
+
throw new Error("No valid hunks found in diff");
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// Apply hunks in reverse order to preserve line numbers
|
|
463
|
+
for (let h = hunks.length - 1; h >= 0; h--) {
|
|
464
|
+
const hunk = hunks[h];
|
|
465
|
+
const startLine = hunk.oldStart - 1; // Convert to 0-based
|
|
466
|
+
|
|
467
|
+
// Collect removals and additions
|
|
468
|
+
const toRemove: number[] = [];
|
|
469
|
+
const toAdd: string[] = [];
|
|
470
|
+
let lineIdx = startLine;
|
|
471
|
+
|
|
472
|
+
for (const dl of hunk.lines) {
|
|
473
|
+
if (dl.type === "remove") {
|
|
474
|
+
toRemove.push(lineIdx);
|
|
475
|
+
lineIdx++;
|
|
476
|
+
} else if (dl.type === "add") {
|
|
477
|
+
toAdd.push(dl.text);
|
|
478
|
+
} else {
|
|
479
|
+
// context
|
|
480
|
+
lineIdx++;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Remove lines (reverse order)
|
|
485
|
+
for (let i = toRemove.length - 1; i >= 0; i--) {
|
|
486
|
+
resultLines.splice(toRemove[i], 1);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Insert new lines at the position of first removal (or startLine if no removals)
|
|
490
|
+
const insertAt = toRemove.length > 0 ? toRemove[0] : startLine;
|
|
491
|
+
resultLines.splice(insertAt, 0, ...toAdd);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return resultLines.join("\n");
|
|
495
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell command path extraction — shared between ShellExecTool and PermissionManager.
|
|
3
|
+
*
|
|
4
|
+
* Heuristically extracts file paths from a shell command and classifies them
|
|
5
|
+
* as read / write / delete operations so permission rules can be applied.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
|
|
10
|
+
/** Commands that READ files */
|
|
11
|
+
const READ_COMMANDS = /\b(?:cat|less|more|head|tail|bat|view|open|code|vim|nano|emacs|pg|nl|od|xxd|strings|file|wc|cksum|md5|sha\w+sum|diff|cmp|sort|uniq|awk|sed|grep|rg|ag|fzf)\b/;
|
|
12
|
+
/** Commands that WRITE/MODIFY files */
|
|
13
|
+
const WRITE_COMMANDS = /\b(?:cp|mv|install|patch|chmod|chown|chgrp|touch|truncate|split|csplit|sed\s+-i|perl\s+-[pi])\b/;
|
|
14
|
+
/** Commands that DELETE files */
|
|
15
|
+
const DELETE_COMMANDS = /\b(?:rm|rmdir|del|rd|unlink|shred|trash|git\s+clean)\b/;
|
|
16
|
+
|
|
17
|
+
export interface ExtractedPaths {
|
|
18
|
+
read: string[];
|
|
19
|
+
write: string[];
|
|
20
|
+
delete: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Extract file paths from a shell command and classify by operation type.
|
|
25
|
+
* Best-effort heuristic — compound commands are split on operators and analyzed per segment.
|
|
26
|
+
*/
|
|
27
|
+
export function extractShellPaths(command: string, workingDir: string): ExtractedPaths {
|
|
28
|
+
const result: ExtractedPaths = { read: [], write: [], delete: [] };
|
|
29
|
+
|
|
30
|
+
const segments = command.split(/\s*(?:&&|\|\||;|\|)\s*/);
|
|
31
|
+
|
|
32
|
+
for (const seg of segments) {
|
|
33
|
+
const trimmed = seg.trim();
|
|
34
|
+
if (!trimmed) continue;
|
|
35
|
+
|
|
36
|
+
const parts = trimmed.split(/\s+/).filter((p) => !p.includes("=") || p.startsWith("-"));
|
|
37
|
+
if (parts.length === 0) continue;
|
|
38
|
+
|
|
39
|
+
const args = parts.slice(1).filter((a) => !a.startsWith("-") && a !== "");
|
|
40
|
+
|
|
41
|
+
const resolvePath = (p: string): string => {
|
|
42
|
+
if (p.startsWith("/") || p.startsWith("~")) return p;
|
|
43
|
+
return path.resolve(workingDir, p);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (DELETE_COMMANDS.test(trimmed)) {
|
|
47
|
+
for (const a of args) result.delete.push(resolvePath(a));
|
|
48
|
+
} else if (WRITE_COMMANDS.test(trimmed)) {
|
|
49
|
+
// cp/mv: last arg is destination (write), rest are sources (read)
|
|
50
|
+
if (/\b(?:cp|mv)\b/.test(trimmed) && args.length >= 2) {
|
|
51
|
+
for (let i = 0; i < args.length - 1; i++) result.read.push(resolvePath(args[i]));
|
|
52
|
+
result.write.push(resolvePath(args[args.length - 1]));
|
|
53
|
+
} else {
|
|
54
|
+
for (const a of args) result.write.push(resolvePath(a));
|
|
55
|
+
}
|
|
56
|
+
} else if (READ_COMMANDS.test(trimmed)) {
|
|
57
|
+
for (const a of args) result.read.push(resolvePath(a));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Redirect writes: > file, >> file
|
|
61
|
+
let m;
|
|
62
|
+
const redirectRe = />{1,2}\s*([^\s;|&>]+)/g;
|
|
63
|
+
while ((m = redirectRe.exec(trimmed)) !== null) {
|
|
64
|
+
result.write.push(resolvePath(m[1]));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// tee writes
|
|
68
|
+
const teeRe = /\btee\s+(?:-[a-zA-Z]\s+)*([^\s;|&]+)/g;
|
|
69
|
+
while ((m = teeRe.exec(trimmed)) !== null) {
|
|
70
|
+
result.write.push(resolvePath(m[1]));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
result.read = [...new Set(result.read)];
|
|
75
|
+
result.write = [...new Set(result.write)];
|
|
76
|
+
result.delete = [...new Set(result.delete)];
|
|
77
|
+
|
|
78
|
+
return result;
|
|
79
|
+
}
|