@animus-labs/cortex 0.2.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/LICENSE +21 -0
- package/README.md +73 -0
- package/dist/budget-guard.d.ts +75 -0
- package/dist/budget-guard.d.ts.map +1 -0
- package/dist/budget-guard.js +142 -0
- package/dist/budget-guard.js.map +1 -0
- package/dist/compaction/compaction.d.ts +99 -0
- package/dist/compaction/compaction.d.ts.map +1 -0
- package/dist/compaction/compaction.js +302 -0
- package/dist/compaction/compaction.js.map +1 -0
- package/dist/compaction/failsafe.d.ts +57 -0
- package/dist/compaction/failsafe.d.ts.map +1 -0
- package/dist/compaction/failsafe.js +135 -0
- package/dist/compaction/failsafe.js.map +1 -0
- package/dist/compaction/index.d.ts +381 -0
- package/dist/compaction/index.d.ts.map +1 -0
- package/dist/compaction/index.js +979 -0
- package/dist/compaction/index.js.map +1 -0
- package/dist/compaction/microcompaction.d.ts +219 -0
- package/dist/compaction/microcompaction.d.ts.map +1 -0
- package/dist/compaction/microcompaction.js +536 -0
- package/dist/compaction/microcompaction.js.map +1 -0
- package/dist/compaction/observational/buffering.d.ts +225 -0
- package/dist/compaction/observational/buffering.d.ts.map +1 -0
- package/dist/compaction/observational/buffering.js +354 -0
- package/dist/compaction/observational/buffering.js.map +1 -0
- package/dist/compaction/observational/constants.d.ts +70 -0
- package/dist/compaction/observational/constants.d.ts.map +1 -0
- package/dist/compaction/observational/constants.js +507 -0
- package/dist/compaction/observational/constants.js.map +1 -0
- package/dist/compaction/observational/index.d.ts +219 -0
- package/dist/compaction/observational/index.d.ts.map +1 -0
- package/dist/compaction/observational/index.js +641 -0
- package/dist/compaction/observational/index.js.map +1 -0
- package/dist/compaction/observational/observer.d.ts +97 -0
- package/dist/compaction/observational/observer.d.ts.map +1 -0
- package/dist/compaction/observational/observer.js +424 -0
- package/dist/compaction/observational/observer.js.map +1 -0
- package/dist/compaction/observational/recall-tool.d.ts +27 -0
- package/dist/compaction/observational/recall-tool.d.ts.map +1 -0
- package/dist/compaction/observational/recall-tool.js +93 -0
- package/dist/compaction/observational/recall-tool.js.map +1 -0
- package/dist/compaction/observational/reflector.d.ts +94 -0
- package/dist/compaction/observational/reflector.d.ts.map +1 -0
- package/dist/compaction/observational/reflector.js +167 -0
- package/dist/compaction/observational/reflector.js.map +1 -0
- package/dist/compaction/observational/types.d.ts +271 -0
- package/dist/compaction/observational/types.d.ts.map +1 -0
- package/dist/compaction/observational/types.js +15 -0
- package/dist/compaction/observational/types.js.map +1 -0
- package/dist/context-manager.d.ts +134 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +170 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/cortex-agent.d.ts +1020 -0
- package/dist/cortex-agent.d.ts.map +1 -0
- package/dist/cortex-agent.js +3589 -0
- package/dist/cortex-agent.js.map +1 -0
- package/dist/error-classifier.d.ts +48 -0
- package/dist/error-classifier.d.ts.map +1 -0
- package/dist/error-classifier.js +152 -0
- package/dist/error-classifier.js.map +1 -0
- package/dist/event-bridge.d.ts +166 -0
- package/dist/event-bridge.d.ts.map +1 -0
- package/dist/event-bridge.js +381 -0
- package/dist/event-bridge.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +57 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-client.d.ts +119 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +474 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/model-wrapper.d.ts +58 -0
- package/dist/model-wrapper.d.ts.map +1 -0
- package/dist/model-wrapper.js +86 -0
- package/dist/model-wrapper.js.map +1 -0
- package/dist/noop-logger.d.ts +4 -0
- package/dist/noop-logger.d.ts.map +1 -0
- package/dist/noop-logger.js +8 -0
- package/dist/noop-logger.js.map +1 -0
- package/dist/prompt-diagnostics.d.ts +47 -0
- package/dist/prompt-diagnostics.d.ts.map +1 -0
- package/dist/prompt-diagnostics.js +230 -0
- package/dist/prompt-diagnostics.js.map +1 -0
- package/dist/provider-manager.d.ts +224 -0
- package/dist/provider-manager.d.ts.map +1 -0
- package/dist/provider-manager.js +563 -0
- package/dist/provider-manager.js.map +1 -0
- package/dist/provider-registry.d.ts +115 -0
- package/dist/provider-registry.d.ts.map +1 -0
- package/dist/provider-registry.js +305 -0
- package/dist/provider-registry.js.map +1 -0
- package/dist/schema-converter.d.ts +20 -0
- package/dist/schema-converter.d.ts.map +1 -0
- package/dist/schema-converter.js +48 -0
- package/dist/schema-converter.js.map +1 -0
- package/dist/skill-preprocessor.d.ts +46 -0
- package/dist/skill-preprocessor.d.ts.map +1 -0
- package/dist/skill-preprocessor.js +237 -0
- package/dist/skill-preprocessor.js.map +1 -0
- package/dist/skill-registry.d.ts +107 -0
- package/dist/skill-registry.d.ts.map +1 -0
- package/dist/skill-registry.js +330 -0
- package/dist/skill-registry.js.map +1 -0
- package/dist/skill-tool.d.ts +54 -0
- package/dist/skill-tool.d.ts.map +1 -0
- package/dist/skill-tool.js +88 -0
- package/dist/skill-tool.js.map +1 -0
- package/dist/sub-agent-manager.d.ts +90 -0
- package/dist/sub-agent-manager.d.ts.map +1 -0
- package/dist/sub-agent-manager.js +192 -0
- package/dist/sub-agent-manager.js.map +1 -0
- package/dist/token-estimator.d.ts +23 -0
- package/dist/token-estimator.d.ts.map +1 -0
- package/dist/token-estimator.js +27 -0
- package/dist/token-estimator.js.map +1 -0
- package/dist/tool-contract.d.ts +68 -0
- package/dist/tool-contract.d.ts.map +1 -0
- package/dist/tool-contract.js +35 -0
- package/dist/tool-contract.js.map +1 -0
- package/dist/tool-result-persistence.d.ts +89 -0
- package/dist/tool-result-persistence.d.ts.map +1 -0
- package/dist/tool-result-persistence.js +152 -0
- package/dist/tool-result-persistence.js.map +1 -0
- package/dist/tools/bash/index.d.ts +71 -0
- package/dist/tools/bash/index.d.ts.map +1 -0
- package/dist/tools/bash/index.js +485 -0
- package/dist/tools/bash/index.js.map +1 -0
- package/dist/tools/bash/interactive.d.ts +47 -0
- package/dist/tools/bash/interactive.d.ts.map +1 -0
- package/dist/tools/bash/interactive.js +262 -0
- package/dist/tools/bash/interactive.js.map +1 -0
- package/dist/tools/bash/safety.d.ts +149 -0
- package/dist/tools/bash/safety.d.ts.map +1 -0
- package/dist/tools/bash/safety.js +1116 -0
- package/dist/tools/bash/safety.js.map +1 -0
- package/dist/tools/edit.d.ts +57 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +310 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/glob.d.ts +34 -0
- package/dist/tools/glob.d.ts.map +1 -0
- package/dist/tools/glob.js +268 -0
- package/dist/tools/glob.js.map +1 -0
- package/dist/tools/grep.d.ts +53 -0
- package/dist/tools/grep.d.ts.map +1 -0
- package/dist/tools/grep.js +673 -0
- package/dist/tools/grep.js.map +1 -0
- package/dist/tools/index.d.ts +62 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +52 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/read.d.ts +43 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +459 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/runtime.d.ts +62 -0
- package/dist/tools/runtime.d.ts.map +1 -0
- package/dist/tools/runtime.js +116 -0
- package/dist/tools/runtime.js.map +1 -0
- package/dist/tools/shared/cwd-tracker.d.ts +32 -0
- package/dist/tools/shared/cwd-tracker.d.ts.map +1 -0
- package/dist/tools/shared/cwd-tracker.js +44 -0
- package/dist/tools/shared/cwd-tracker.js.map +1 -0
- package/dist/tools/shared/edit-history.d.ts +55 -0
- package/dist/tools/shared/edit-history.d.ts.map +1 -0
- package/dist/tools/shared/edit-history.js +72 -0
- package/dist/tools/shared/edit-history.js.map +1 -0
- package/dist/tools/shared/edit-matcher.d.ts +83 -0
- package/dist/tools/shared/edit-matcher.d.ts.map +1 -0
- package/dist/tools/shared/edit-matcher.js +359 -0
- package/dist/tools/shared/edit-matcher.js.map +1 -0
- package/dist/tools/shared/file-mutation-lock.d.ts +22 -0
- package/dist/tools/shared/file-mutation-lock.d.ts.map +1 -0
- package/dist/tools/shared/file-mutation-lock.js +35 -0
- package/dist/tools/shared/file-mutation-lock.js.map +1 -0
- package/dist/tools/shared/gitignore.d.ts +17 -0
- package/dist/tools/shared/gitignore.d.ts.map +1 -0
- package/dist/tools/shared/gitignore.js +59 -0
- package/dist/tools/shared/gitignore.js.map +1 -0
- package/dist/tools/shared/pdf-extractor.d.ts +96 -0
- package/dist/tools/shared/pdf-extractor.d.ts.map +1 -0
- package/dist/tools/shared/pdf-extractor.js +196 -0
- package/dist/tools/shared/pdf-extractor.js.map +1 -0
- package/dist/tools/shared/read-registry.d.ts +66 -0
- package/dist/tools/shared/read-registry.d.ts.map +1 -0
- package/dist/tools/shared/read-registry.js +65 -0
- package/dist/tools/shared/read-registry.js.map +1 -0
- package/dist/tools/shared/safe-env.d.ts +18 -0
- package/dist/tools/shared/safe-env.d.ts.map +1 -0
- package/dist/tools/shared/safe-env.js +70 -0
- package/dist/tools/shared/safe-env.js.map +1 -0
- package/dist/tools/sub-agent.d.ts +91 -0
- package/dist/tools/sub-agent.d.ts.map +1 -0
- package/dist/tools/sub-agent.js +89 -0
- package/dist/tools/sub-agent.js.map +1 -0
- package/dist/tools/task-output.d.ts +38 -0
- package/dist/tools/task-output.d.ts.map +1 -0
- package/dist/tools/task-output.js +186 -0
- package/dist/tools/task-output.js.map +1 -0
- package/dist/tools/tool-search/index.d.ts +40 -0
- package/dist/tools/tool-search/index.d.ts.map +1 -0
- package/dist/tools/tool-search/index.js +110 -0
- package/dist/tools/tool-search/index.js.map +1 -0
- package/dist/tools/tool-search/registry.d.ts +82 -0
- package/dist/tools/tool-search/registry.d.ts.map +1 -0
- package/dist/tools/tool-search/registry.js +238 -0
- package/dist/tools/tool-search/registry.js.map +1 -0
- package/dist/tools/undo-edit.d.ts +51 -0
- package/dist/tools/undo-edit.d.ts.map +1 -0
- package/dist/tools/undo-edit.js +231 -0
- package/dist/tools/undo-edit.js.map +1 -0
- package/dist/tools/web-fetch/cache.d.ts +49 -0
- package/dist/tools/web-fetch/cache.d.ts.map +1 -0
- package/dist/tools/web-fetch/cache.js +89 -0
- package/dist/tools/web-fetch/cache.js.map +1 -0
- package/dist/tools/web-fetch/index.d.ts +53 -0
- package/dist/tools/web-fetch/index.d.ts.map +1 -0
- package/dist/tools/web-fetch/index.js +513 -0
- package/dist/tools/web-fetch/index.js.map +1 -0
- package/dist/tools/write.d.ts +59 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +316 -0
- package/dist/tools/write.js.map +1 -0
- package/dist/types.d.ts +881 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/working-tags.d.ts +44 -0
- package/dist/working-tags.d.ts.map +1 -0
- package/dist/working-tags.js +103 -0
- package/dist/working-tags.js.map +1 -0
- package/package.json +87 -0
- package/src/budget-guard.ts +170 -0
- package/src/compaction/compaction.ts +386 -0
- package/src/compaction/failsafe.ts +185 -0
- package/src/compaction/index.ts +1199 -0
- package/src/compaction/microcompaction.ts +709 -0
- package/src/compaction/observational/buffering.ts +430 -0
- package/src/compaction/observational/constants.ts +532 -0
- package/src/compaction/observational/index.ts +837 -0
- package/src/compaction/observational/observer.ts +510 -0
- package/src/compaction/observational/recall-tool.ts +130 -0
- package/src/compaction/observational/reflector.ts +221 -0
- package/src/compaction/observational/types.ts +343 -0
- package/src/context-manager.ts +237 -0
- package/src/cortex-agent.ts +4297 -0
- package/src/error-classifier.ts +199 -0
- package/src/event-bridge.ts +508 -0
- package/src/index.ts +292 -0
- package/src/mcp-client.ts +582 -0
- package/src/model-wrapper.ts +128 -0
- package/src/noop-logger.ts +9 -0
- package/src/prompt-diagnostics.ts +296 -0
- package/src/provider-manager.ts +823 -0
- package/src/provider-registry.ts +386 -0
- package/src/schema-converter.ts +51 -0
- package/src/skill-preprocessor.ts +314 -0
- package/src/skill-registry.ts +378 -0
- package/src/skill-tool.ts +130 -0
- package/src/sub-agent-manager.ts +236 -0
- package/src/token-estimator.ts +26 -0
- package/src/tool-contract.ts +113 -0
- package/src/tool-result-persistence.ts +197 -0
- package/src/tools/bash/index.ts +633 -0
- package/src/tools/bash/interactive.ts +302 -0
- package/src/tools/bash/safety.ts +1297 -0
- package/src/tools/edit.ts +422 -0
- package/src/tools/glob.ts +330 -0
- package/src/tools/grep.ts +819 -0
- package/src/tools/index.ts +110 -0
- package/src/tools/read.ts +580 -0
- package/src/tools/runtime.ts +173 -0
- package/src/tools/shared/cwd-tracker.ts +50 -0
- package/src/tools/shared/edit-history.ts +96 -0
- package/src/tools/shared/edit-matcher.ts +457 -0
- package/src/tools/shared/file-mutation-lock.ts +40 -0
- package/src/tools/shared/gitignore.ts +61 -0
- package/src/tools/shared/pdf-extractor.ts +290 -0
- package/src/tools/shared/read-registry.ts +93 -0
- package/src/tools/shared/safe-env.ts +82 -0
- package/src/tools/sub-agent.ts +171 -0
- package/src/tools/task-output.ts +236 -0
- package/src/tools/tool-search/index.ts +167 -0
- package/src/tools/tool-search/registry.ts +278 -0
- package/src/tools/undo-edit.ts +314 -0
- package/src/tools/web-fetch/cache.ts +112 -0
- package/src/tools/web-fetch/index.ts +604 -0
- package/src/tools/write.ts +385 -0
- package/src/types.ts +1057 -0
- package/src/working-tags.ts +118 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit matcher: resolve an Edit tool `old_string` against a file's content
|
|
3
|
+
* via a tiered cascade.
|
|
4
|
+
*
|
|
5
|
+
* Tiers (short-circuit on the first that yields any match):
|
|
6
|
+
* 1. exact — raw indexOf, caller handles multi-match based on replace_all
|
|
7
|
+
* 2. line-trimmed — per-line trailing-whitespace tolerance; must be unique
|
|
8
|
+
* 3. indentation-flexible — strips common leading indent on both sides,
|
|
9
|
+
* also tolerates trailing whitespace; must be unique
|
|
10
|
+
*
|
|
11
|
+
* Also exports `findNearestMatch` for "did you mean...?" error hints when
|
|
12
|
+
* no tier matches, and `reindentReplacement` for producing a replacement
|
|
13
|
+
* string that respects the haystack's indentation when tier 3 matched.
|
|
14
|
+
*
|
|
15
|
+
* This module is intentionally pure and I/O-free so the cascade can be
|
|
16
|
+
* exhaustively unit-tested without the filesystem.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Result type
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
export type MatchResult =
|
|
24
|
+
| {
|
|
25
|
+
kind: 'exact';
|
|
26
|
+
/** Char offset in haystack where the first match begins. */
|
|
27
|
+
startIndex: number;
|
|
28
|
+
/** Length of the matched span (equals needle.length). */
|
|
29
|
+
matchedLength: number;
|
|
30
|
+
/** Total number of exact occurrences in haystack. */
|
|
31
|
+
count: number;
|
|
32
|
+
/**
|
|
33
|
+
* 1-based line numbers where each match begins. Capped at the first
|
|
34
|
+
* 3 entries (diagnostic use only; full count is in `count`).
|
|
35
|
+
*/
|
|
36
|
+
matchLines: number[];
|
|
37
|
+
}
|
|
38
|
+
| {
|
|
39
|
+
kind: 'line-trimmed';
|
|
40
|
+
startIndex: number;
|
|
41
|
+
matchedLength: number;
|
|
42
|
+
}
|
|
43
|
+
| {
|
|
44
|
+
kind: 'indentation-flexible';
|
|
45
|
+
startIndex: number;
|
|
46
|
+
matchedLength: number;
|
|
47
|
+
/** Common leading indent of the needle's non-empty lines. */
|
|
48
|
+
needleIndent: string;
|
|
49
|
+
/** Common leading indent of the matched haystack window. */
|
|
50
|
+
haystackIndent: string;
|
|
51
|
+
}
|
|
52
|
+
| {
|
|
53
|
+
kind: 'ambiguous';
|
|
54
|
+
tier: 'line-trimmed' | 'indentation-flexible';
|
|
55
|
+
count: number;
|
|
56
|
+
/** 1-based line numbers, first 3. */
|
|
57
|
+
matchLines: number[];
|
|
58
|
+
}
|
|
59
|
+
| { kind: 'none' };
|
|
60
|
+
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Public API
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Run the tiered matcher. Both arguments should already be line-ending
|
|
67
|
+
* normalized (CRLF -> LF) by the caller.
|
|
68
|
+
*/
|
|
69
|
+
export function findMatch(haystack: string, needle: string): MatchResult {
|
|
70
|
+
// Note: we intentionally do NOT bail on needle.length > haystack.length.
|
|
71
|
+
// Tier 3 tolerates over-indented needles, so a longer-than-haystack needle
|
|
72
|
+
// can still produce a valid match after common-indent stripping.
|
|
73
|
+
if (needle.length === 0) return { kind: 'none' };
|
|
74
|
+
|
|
75
|
+
const exact = findExact(haystack, needle);
|
|
76
|
+
if (exact.kind === 'exact') return exact;
|
|
77
|
+
|
|
78
|
+
const trimmed = findLineTrimmed(haystack, needle);
|
|
79
|
+
if (trimmed.kind !== 'none') return trimmed;
|
|
80
|
+
|
|
81
|
+
return findIndentationFlexible(haystack, needle);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Re-indent a replacement string so it fits the haystack's indentation
|
|
86
|
+
* when tier 3 (indentation-flexible) matched.
|
|
87
|
+
*
|
|
88
|
+
* Strips up to `needleIndent` leading whitespace from each non-empty line
|
|
89
|
+
* of `newString`, then prepends `haystackIndent`. Empty / whitespace-only
|
|
90
|
+
* lines pass through untouched.
|
|
91
|
+
*/
|
|
92
|
+
export function reindentReplacement(
|
|
93
|
+
newString: string,
|
|
94
|
+
needleIndent: string,
|
|
95
|
+
haystackIndent: string,
|
|
96
|
+
): string {
|
|
97
|
+
if (needleIndent === haystackIndent) return newString;
|
|
98
|
+
return newString
|
|
99
|
+
.split('\n')
|
|
100
|
+
.map((line) => {
|
|
101
|
+
if (line.trim() === '') return line;
|
|
102
|
+
const stripped = line.startsWith(needleIndent)
|
|
103
|
+
? line.slice(needleIndent.length)
|
|
104
|
+
: line;
|
|
105
|
+
return haystackIndent + stripped;
|
|
106
|
+
})
|
|
107
|
+
.join('\n');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Nearest-match hint
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
|
|
114
|
+
export interface NearestMatch {
|
|
115
|
+
/** 1-based line number of the best candidate. */
|
|
116
|
+
bestLine: number;
|
|
117
|
+
/** Similarity ratio in [0, 1]. */
|
|
118
|
+
bestRatio: number;
|
|
119
|
+
/**
|
|
120
|
+
* Pre-formatted multi-line snippet with line numbers and an arrow on
|
|
121
|
+
* the best candidate. Suitable for direct inclusion in an error message.
|
|
122
|
+
*/
|
|
123
|
+
snippet: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Hard scan limit on large files; Levenshtein is O(m*n) per line. */
|
|
127
|
+
const NEAREST_MATCH_SCAN_CAP = 2000;
|
|
128
|
+
const NEAREST_MATCH_MIN_RATIO = 0.5;
|
|
129
|
+
const NEAREST_MATCH_CONTEXT = 3;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Find the closest line in `haystack` to the first non-empty line of
|
|
133
|
+
* `needle`, returning a diff-style snippet with ±3 lines of context.
|
|
134
|
+
* Returns undefined when no reasonable candidate (ratio < 0.5) exists.
|
|
135
|
+
*/
|
|
136
|
+
export function findNearestMatch(
|
|
137
|
+
haystack: string,
|
|
138
|
+
needle: string,
|
|
139
|
+
): NearestMatch | undefined {
|
|
140
|
+
const needleLines = needle.split('\n');
|
|
141
|
+
const firstNonEmpty = needleLines.find((line) => line.trim() !== '');
|
|
142
|
+
if (firstNonEmpty === undefined) return undefined;
|
|
143
|
+
|
|
144
|
+
const haystackLines = haystack.split('\n');
|
|
145
|
+
const scanLimit = Math.min(haystackLines.length, NEAREST_MATCH_SCAN_CAP);
|
|
146
|
+
const needleTrim = firstNonEmpty.trim();
|
|
147
|
+
|
|
148
|
+
let bestIdx = -1;
|
|
149
|
+
let bestRatio = 0;
|
|
150
|
+
|
|
151
|
+
for (let i = 0; i < scanLimit; i++) {
|
|
152
|
+
const line = haystackLines[i]!;
|
|
153
|
+
const lineTrim = line.trim();
|
|
154
|
+
if (lineTrim === '') continue;
|
|
155
|
+
const ratio = levenshteinRatio(needleTrim, lineTrim);
|
|
156
|
+
if (ratio > bestRatio) {
|
|
157
|
+
bestRatio = ratio;
|
|
158
|
+
bestIdx = i;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (bestIdx === -1 || bestRatio < NEAREST_MATCH_MIN_RATIO) return undefined;
|
|
163
|
+
|
|
164
|
+
const start = Math.max(0, bestIdx - NEAREST_MATCH_CONTEXT);
|
|
165
|
+
const end = Math.min(haystackLines.length - 1, bestIdx + NEAREST_MATCH_CONTEXT);
|
|
166
|
+
const gutterWidth = String(end + 1).length;
|
|
167
|
+
|
|
168
|
+
const rendered: string[] = [];
|
|
169
|
+
for (let i = start; i <= end; i++) {
|
|
170
|
+
const num = String(i + 1).padStart(gutterWidth);
|
|
171
|
+
const marker = i === bestIdx ? ' <- nearest' : '';
|
|
172
|
+
rendered.push(` ${num} | ${haystackLines[i]}${marker}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
bestLine: bestIdx + 1,
|
|
177
|
+
bestRatio,
|
|
178
|
+
snippet: rendered.join('\n'),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Tier 1: exact
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
function findExact(
|
|
187
|
+
haystack: string,
|
|
188
|
+
needle: string,
|
|
189
|
+
): Extract<MatchResult, { kind: 'exact' }> | { kind: 'none' } {
|
|
190
|
+
const uniqueLines: number[] = [];
|
|
191
|
+
let count = 0;
|
|
192
|
+
let firstStart = -1;
|
|
193
|
+
let pos = 0;
|
|
194
|
+
while (true) {
|
|
195
|
+
const idx = haystack.indexOf(needle, pos);
|
|
196
|
+
if (idx === -1) break;
|
|
197
|
+
count++;
|
|
198
|
+
if (count === 1) firstStart = idx;
|
|
199
|
+
// Keep up to 3 DISTINCT line numbers. Multiple hits on the same line
|
|
200
|
+
// (e.g. replace_all of a short variable) should show one line, not
|
|
201
|
+
// the same number repeated.
|
|
202
|
+
if (uniqueLines.length < 3) {
|
|
203
|
+
const line = charIndexToLine(haystack, idx);
|
|
204
|
+
if (!uniqueLines.includes(line)) uniqueLines.push(line);
|
|
205
|
+
}
|
|
206
|
+
pos = idx + needle.length;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (count === 0) return { kind: 'none' };
|
|
210
|
+
return {
|
|
211
|
+
kind: 'exact',
|
|
212
|
+
startIndex: firstStart,
|
|
213
|
+
matchedLength: needle.length,
|
|
214
|
+
count,
|
|
215
|
+
matchLines: uniqueLines,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// Tier 2: line-trimmed (tolerates per-line trailing whitespace)
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
function findLineTrimmed(
|
|
224
|
+
haystack: string,
|
|
225
|
+
needle: string,
|
|
226
|
+
): MatchResult {
|
|
227
|
+
const needleLines = needle.split('\n');
|
|
228
|
+
const haystackLines = haystack.split('\n');
|
|
229
|
+
if (needleLines.length > haystackLines.length) return { kind: 'none' };
|
|
230
|
+
|
|
231
|
+
const needleTrimmed = needleLines.map(trimEnd);
|
|
232
|
+
|
|
233
|
+
// Short-circuit when tier 2 would degenerate to tier 1: if trimming
|
|
234
|
+
// changes nothing on either side, tier 1 has already run and either
|
|
235
|
+
// found a match or not — re-running here would just rediscover the
|
|
236
|
+
// same result.
|
|
237
|
+
const needleEqual = arraysEqual(needleLines, needleTrimmed);
|
|
238
|
+
if (needleEqual && !haystackHasTrailingWhitespace(haystackLines)) {
|
|
239
|
+
return { kind: 'none' };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const windowLen = needleLines.length;
|
|
243
|
+
const matches: number[] = [];
|
|
244
|
+
|
|
245
|
+
for (let i = 0; i + windowLen <= haystackLines.length; i++) {
|
|
246
|
+
let ok = true;
|
|
247
|
+
for (let j = 0; j < windowLen; j++) {
|
|
248
|
+
if (trimEnd(haystackLines[i + j]!) !== needleTrimmed[j]) {
|
|
249
|
+
ok = false;
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (ok) matches.push(i);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (matches.length === 0) return { kind: 'none' };
|
|
257
|
+
if (matches.length > 1) {
|
|
258
|
+
return {
|
|
259
|
+
kind: 'ambiguous',
|
|
260
|
+
tier: 'line-trimmed',
|
|
261
|
+
count: matches.length,
|
|
262
|
+
matchLines: matches.slice(0, 3).map((i) => i + 1),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const span = linesToSpan(haystackLines, matches[0]!, windowLen);
|
|
267
|
+
return {
|
|
268
|
+
kind: 'line-trimmed',
|
|
269
|
+
startIndex: span.startIndex,
|
|
270
|
+
matchedLength: span.matchedLength,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// Tier 3: indentation-flexible (common-indent strip + trailing trim)
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
function findIndentationFlexible(
|
|
279
|
+
haystack: string,
|
|
280
|
+
needle: string,
|
|
281
|
+
): MatchResult {
|
|
282
|
+
const needleLines = needle.split('\n');
|
|
283
|
+
const haystackLines = haystack.split('\n');
|
|
284
|
+
if (needleLines.length > haystackLines.length) return { kind: 'none' };
|
|
285
|
+
|
|
286
|
+
const needleIndent = commonLeadingIndent(needleLines);
|
|
287
|
+
const needleCanonical = needleLines.map((line) =>
|
|
288
|
+
trimEnd(stripLeading(line, needleIndent)),
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// If the needle has no stripped content (e.g. entirely blank lines),
|
|
292
|
+
// fall out — we'd match noise.
|
|
293
|
+
if (needleCanonical.every((line) => line === '')) return { kind: 'none' };
|
|
294
|
+
|
|
295
|
+
const windowLen = needleLines.length;
|
|
296
|
+
const matches: Array<{ startLine: number; haystackIndent: string }> = [];
|
|
297
|
+
|
|
298
|
+
for (let i = 0; i + windowLen <= haystackLines.length; i++) {
|
|
299
|
+
const window = haystackLines.slice(i, i + windowLen);
|
|
300
|
+
const windowIndent = commonLeadingIndent(window);
|
|
301
|
+
let ok = true;
|
|
302
|
+
for (let j = 0; j < windowLen; j++) {
|
|
303
|
+
const canonical = trimEnd(stripLeading(window[j]!, windowIndent));
|
|
304
|
+
if (canonical !== needleCanonical[j]) {
|
|
305
|
+
ok = false;
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (ok) matches.push({ startLine: i, haystackIndent: windowIndent });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (matches.length === 0) return { kind: 'none' };
|
|
313
|
+
if (matches.length > 1) {
|
|
314
|
+
return {
|
|
315
|
+
kind: 'ambiguous',
|
|
316
|
+
tier: 'indentation-flexible',
|
|
317
|
+
count: matches.length,
|
|
318
|
+
matchLines: matches.slice(0, 3).map((m) => m.startLine + 1),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const m = matches[0]!;
|
|
323
|
+
const span = linesToSpan(haystackLines, m.startLine, windowLen);
|
|
324
|
+
|
|
325
|
+
// Guard: if the indents are identical, tier 2 would have caught this
|
|
326
|
+
// (or tier 1 already did). Promote to 'none' so we don't mask a true
|
|
327
|
+
// no-match with a spurious tier 3 hit.
|
|
328
|
+
if (needleIndent === m.haystackIndent) {
|
|
329
|
+
return { kind: 'none' };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
kind: 'indentation-flexible',
|
|
334
|
+
startIndex: span.startIndex,
|
|
335
|
+
matchedLength: span.matchedLength,
|
|
336
|
+
needleIndent,
|
|
337
|
+
haystackIndent: m.haystackIndent,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
// Utilities
|
|
343
|
+
// ---------------------------------------------------------------------------
|
|
344
|
+
|
|
345
|
+
function trimEnd(s: string): string {
|
|
346
|
+
return s.replace(/[ \t]+$/u, '');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function stripLeading(line: string, indent: string): string {
|
|
350
|
+
return line.startsWith(indent) ? line.slice(indent.length) : line;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function haystackHasTrailingWhitespace(lines: string[]): boolean {
|
|
354
|
+
for (const line of lines) {
|
|
355
|
+
if (line.length > 0 && /[ \t]$/u.test(line)) return true;
|
|
356
|
+
}
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function arraysEqual(a: string[], b: string[]): boolean {
|
|
361
|
+
if (a.length !== b.length) return false;
|
|
362
|
+
for (let i = 0; i < a.length; i++) {
|
|
363
|
+
if (a[i] !== b[i]) return false;
|
|
364
|
+
}
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Longest common leading whitespace prefix across non-empty lines.
|
|
370
|
+
* Lines that are empty or whitespace-only are skipped so a blank line
|
|
371
|
+
* inside a block doesn't collapse the shared indent to "".
|
|
372
|
+
*/
|
|
373
|
+
function commonLeadingIndent(lines: string[]): string {
|
|
374
|
+
let common: string | undefined;
|
|
375
|
+
for (const line of lines) {
|
|
376
|
+
if (line.trim() === '') continue;
|
|
377
|
+
const m = /^[ \t]*/u.exec(line);
|
|
378
|
+
const leading = m ? m[0] : '';
|
|
379
|
+
if (common === undefined) {
|
|
380
|
+
common = leading;
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
let i = 0;
|
|
384
|
+
const max = Math.min(common.length, leading.length);
|
|
385
|
+
while (i < max && common[i] === leading[i]) i++;
|
|
386
|
+
common = common.slice(0, i);
|
|
387
|
+
if (common.length === 0) return '';
|
|
388
|
+
}
|
|
389
|
+
return common ?? '';
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Convert a (startLine, windowLen) line-index span into a (startIndex,
|
|
394
|
+
* matchedLength) character span in the original haystack. Assumes the
|
|
395
|
+
* haystack was split by '\n' — inner newlines are counted, but no
|
|
396
|
+
* trailing newline is included in `matchedLength`.
|
|
397
|
+
*/
|
|
398
|
+
function linesToSpan(
|
|
399
|
+
lines: string[],
|
|
400
|
+
startLine: number,
|
|
401
|
+
windowLen: number,
|
|
402
|
+
): { startIndex: number; matchedLength: number } {
|
|
403
|
+
let startIndex = 0;
|
|
404
|
+
for (let i = 0; i < startLine; i++) {
|
|
405
|
+
startIndex += lines[i]!.length + 1;
|
|
406
|
+
}
|
|
407
|
+
let matchedLength = 0;
|
|
408
|
+
for (let i = 0; i < windowLen; i++) {
|
|
409
|
+
matchedLength += lines[startLine + i]!.length;
|
|
410
|
+
if (i < windowLen - 1) matchedLength += 1;
|
|
411
|
+
}
|
|
412
|
+
return { startIndex, matchedLength };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Count 1-based line number of a character offset in haystack. */
|
|
416
|
+
function charIndexToLine(haystack: string, charIndex: number): number {
|
|
417
|
+
let line = 1;
|
|
418
|
+
for (let i = 0; i < charIndex && i < haystack.length; i++) {
|
|
419
|
+
if (haystack.charCodeAt(i) === 10 /* \n */) line++;
|
|
420
|
+
}
|
|
421
|
+
return line;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ---------------------------------------------------------------------------
|
|
425
|
+
// Levenshtein (for findNearestMatch only)
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
function levenshteinRatio(a: string, b: string): number {
|
|
429
|
+
if (a.length === 0 && b.length === 0) return 1;
|
|
430
|
+
const max = Math.max(a.length, b.length);
|
|
431
|
+
if (max === 0) return 1;
|
|
432
|
+
const dist = levenshteinDistance(a, b);
|
|
433
|
+
return 1 - dist / max;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function levenshteinDistance(a: string, b: string): number {
|
|
437
|
+
const m = a.length;
|
|
438
|
+
const n = b.length;
|
|
439
|
+
if (m === 0) return n;
|
|
440
|
+
if (n === 0) return m;
|
|
441
|
+
let prev = new Array<number>(n + 1);
|
|
442
|
+
let curr = new Array<number>(n + 1);
|
|
443
|
+
for (let j = 0; j <= n; j++) prev[j] = j;
|
|
444
|
+
for (let i = 1; i <= m; i++) {
|
|
445
|
+
curr[0] = i;
|
|
446
|
+
for (let j = 1; j <= n; j++) {
|
|
447
|
+
const cost = a.charCodeAt(i - 1) === b.charCodeAt(j - 1) ? 0 : 1;
|
|
448
|
+
curr[j] = Math.min(
|
|
449
|
+
curr[j - 1]! + 1,
|
|
450
|
+
prev[j]! + 1,
|
|
451
|
+
prev[j - 1]! + cost,
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
[prev, curr] = [curr, prev];
|
|
455
|
+
}
|
|
456
|
+
return prev[n]!;
|
|
457
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-file async mutation lock.
|
|
3
|
+
*
|
|
4
|
+
* Serializes concurrent mutations (Edit, Write) that target the same file
|
|
5
|
+
* while allowing mutations on different files to proceed in parallel.
|
|
6
|
+
*
|
|
7
|
+
* Uses promise chaining rather than OS mutexes: each `acquire()` appends
|
|
8
|
+
* a new link to the per-path chain. The caller awaits the previous link
|
|
9
|
+
* (serialization) and receives a `release` callback that resolves the
|
|
10
|
+
* current link so the next waiter can proceed.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
|
|
15
|
+
export class FileMutationLock {
|
|
16
|
+
private chains = new Map<string, Promise<void>>();
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Acquire exclusive mutation access for a file path.
|
|
20
|
+
* Returns a release function that MUST be called when done (use try/finally).
|
|
21
|
+
*/
|
|
22
|
+
async acquire(filePath: string): Promise<() => void> {
|
|
23
|
+
const key = path.resolve(filePath);
|
|
24
|
+
const previous = this.chains.get(key) ?? Promise.resolve();
|
|
25
|
+
|
|
26
|
+
let release!: () => void;
|
|
27
|
+
const current = new Promise<void>(resolve => {
|
|
28
|
+
release = resolve;
|
|
29
|
+
});
|
|
30
|
+
this.chains.set(key, current);
|
|
31
|
+
|
|
32
|
+
await previous;
|
|
33
|
+
return release;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Clear all chains. Safe to call between agentic loops. */
|
|
37
|
+
clear(): void {
|
|
38
|
+
this.chains.clear();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared .gitignore reading utility.
|
|
3
|
+
*
|
|
4
|
+
* Walks up the directory tree collecting .gitignore patterns.
|
|
5
|
+
* Used by both Glob and Grep tools to respect .gitignore rules.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from 'node:fs';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default ignore patterns used when no .gitignore is present,
|
|
13
|
+
* or merged with .gitignore patterns.
|
|
14
|
+
*/
|
|
15
|
+
export const DEFAULT_IGNORE_PATTERNS = [
|
|
16
|
+
'node_modules',
|
|
17
|
+
'.git',
|
|
18
|
+
'dist',
|
|
19
|
+
'build',
|
|
20
|
+
'__pycache__',
|
|
21
|
+
'.DS_Store',
|
|
22
|
+
'.next',
|
|
23
|
+
'.nuxt',
|
|
24
|
+
'coverage',
|
|
25
|
+
'.cache',
|
|
26
|
+
'.parcel-cache',
|
|
27
|
+
'.vite',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read and parse .gitignore files by walking up from `dir` to the filesystem root.
|
|
32
|
+
* Returns an array of gitignore patterns (comments and empty lines stripped).
|
|
33
|
+
*/
|
|
34
|
+
export async function readGitignorePatterns(dir: string): Promise<string[]> {
|
|
35
|
+
const patterns: string[] = [];
|
|
36
|
+
|
|
37
|
+
let current = dir;
|
|
38
|
+
const visited = new Set<string>();
|
|
39
|
+
while (!visited.has(current)) {
|
|
40
|
+
visited.add(current);
|
|
41
|
+
const gitignorePath = path.join(current, '.gitignore');
|
|
42
|
+
try {
|
|
43
|
+
const content = await fs.promises.readFile(gitignorePath, 'utf8');
|
|
44
|
+
const lines = content.split('\n');
|
|
45
|
+
for (const line of lines) {
|
|
46
|
+
const trimmed = line.trim();
|
|
47
|
+
// Skip comments and empty lines
|
|
48
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
49
|
+
patterns.push(trimmed);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// No .gitignore at this level, continue
|
|
54
|
+
}
|
|
55
|
+
const parent = path.dirname(current);
|
|
56
|
+
if (parent === current) break;
|
|
57
|
+
current = parent;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return patterns;
|
|
61
|
+
}
|