@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,422 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit tool: make precise string replacements in existing files.
|
|
3
|
+
*
|
|
4
|
+
* Supports exact string matching with a uniqueness constraint
|
|
5
|
+
* (when replaceAll is false). Enforces read-before-edit via ReadRegistry.
|
|
6
|
+
* Handles line ending normalization for cross-platform compatibility.
|
|
7
|
+
*
|
|
8
|
+
* Reference: docs/cortex/tools/edit.md
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as crypto from 'node:crypto';
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import { Type, type Static } from 'typebox';
|
|
15
|
+
import type { EditHistory } from './shared/edit-history.js';
|
|
16
|
+
import type { FileMutationLock } from './shared/file-mutation-lock.js';
|
|
17
|
+
import type { ReadRegistry } from './shared/read-registry.js';
|
|
18
|
+
import type { ToolContentDetails } from '../types.js';
|
|
19
|
+
import { computeDiff, type DiffHunk } from './write.js';
|
|
20
|
+
import type { CortexToolRuntime } from './runtime.js';
|
|
21
|
+
import { attachRuntimeAwareTool } from './runtime.js';
|
|
22
|
+
import {
|
|
23
|
+
findMatch,
|
|
24
|
+
findNearestMatch,
|
|
25
|
+
reindentReplacement,
|
|
26
|
+
type MatchResult,
|
|
27
|
+
} from './shared/edit-matcher.js';
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Schema
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
export const EditParams = Type.Object({
|
|
34
|
+
file_path: Type.String({ description: 'Absolute path to the file to edit' }),
|
|
35
|
+
old_string: Type.String({ description: 'The exact text to find and replace' }),
|
|
36
|
+
new_string: Type.String({ description: 'The replacement text (must differ from old_string)' }),
|
|
37
|
+
replace_all: Type.Optional(
|
|
38
|
+
Type.Boolean({
|
|
39
|
+
description: 'Replace all occurrences. Default: false (replace first unique match).',
|
|
40
|
+
default: false,
|
|
41
|
+
}),
|
|
42
|
+
),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export type EditParamsType = Static<typeof EditParams>;
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Details type
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
export interface EditDetails {
|
|
52
|
+
filePath: string;
|
|
53
|
+
oldString: string;
|
|
54
|
+
newString: string;
|
|
55
|
+
replacementCount: number;
|
|
56
|
+
replaceAll: boolean;
|
|
57
|
+
diff: DiffHunk[];
|
|
58
|
+
originalContent: string;
|
|
59
|
+
/**
|
|
60
|
+
* Which matcher tier resolved the edit. Useful for consumers that want
|
|
61
|
+
* to surface "we applied a fuzzy match" in the UI. Absent when no edit
|
|
62
|
+
* was performed (errors, identical strings, etc.).
|
|
63
|
+
*/
|
|
64
|
+
matchTier?: 'exact' | 'line-trimmed' | 'indentation-flexible';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Config
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
export interface EditToolConfig {
|
|
72
|
+
runtime?: CortexToolRuntime | undefined;
|
|
73
|
+
readRegistry?: ReadRegistry | undefined;
|
|
74
|
+
fileMutationLock?: FileMutationLock | undefined;
|
|
75
|
+
/**
|
|
76
|
+
* Undo stack. When provided, every successful edit pushes a
|
|
77
|
+
* pre-mutation snapshot so `UndoEdit` can restore the prior state.
|
|
78
|
+
* Optional — tests and embedded consumers that don't expose undo
|
|
79
|
+
* may omit it; the tool degrades gracefully to current behavior.
|
|
80
|
+
*/
|
|
81
|
+
editHistory?: EditHistory | undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Helpers
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
type AppliedTier = 'exact' | 'line-trimmed' | 'indentation-flexible';
|
|
89
|
+
|
|
90
|
+
interface AppliedReplacement {
|
|
91
|
+
newContent: string;
|
|
92
|
+
replacementCount: number;
|
|
93
|
+
tier: AppliedTier;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Given a successful match (not `none` and not `ambiguous`), produce the
|
|
98
|
+
* rebuilt file content along with the replacement count and the tier
|
|
99
|
+
* that resolved the edit. Caller is responsible for having already
|
|
100
|
+
* rejected `none`, `ambiguous`, and the tier-1 `count>1 && !replaceAll`
|
|
101
|
+
* case. Returns null when `match` is one of those guarded states, which
|
|
102
|
+
* is a programming error at the call site.
|
|
103
|
+
*/
|
|
104
|
+
function applyReplacement(
|
|
105
|
+
match: MatchResult,
|
|
106
|
+
normalizedContent: string,
|
|
107
|
+
normalizedOldString: string,
|
|
108
|
+
normalizedNewString: string,
|
|
109
|
+
replaceAll: boolean,
|
|
110
|
+
): AppliedReplacement | null {
|
|
111
|
+
if (match.kind === 'exact') {
|
|
112
|
+
if (replaceAll) {
|
|
113
|
+
return {
|
|
114
|
+
newContent: normalizedContent
|
|
115
|
+
.split(normalizedOldString)
|
|
116
|
+
.join(normalizedNewString),
|
|
117
|
+
replacementCount: match.count,
|
|
118
|
+
tier: 'exact',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
newContent:
|
|
123
|
+
normalizedContent.slice(0, match.startIndex) +
|
|
124
|
+
normalizedNewString +
|
|
125
|
+
normalizedContent.slice(match.startIndex + match.matchedLength),
|
|
126
|
+
replacementCount: 1,
|
|
127
|
+
tier: 'exact',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (match.kind === 'line-trimmed') {
|
|
131
|
+
return {
|
|
132
|
+
newContent:
|
|
133
|
+
normalizedContent.slice(0, match.startIndex) +
|
|
134
|
+
normalizedNewString +
|
|
135
|
+
normalizedContent.slice(match.startIndex + match.matchedLength),
|
|
136
|
+
replacementCount: 1,
|
|
137
|
+
tier: 'line-trimmed',
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (match.kind === 'indentation-flexible') {
|
|
141
|
+
const reindented = reindentReplacement(
|
|
142
|
+
normalizedNewString,
|
|
143
|
+
match.needleIndent,
|
|
144
|
+
match.haystackIndent,
|
|
145
|
+
);
|
|
146
|
+
return {
|
|
147
|
+
newContent:
|
|
148
|
+
normalizedContent.slice(0, match.startIndex) +
|
|
149
|
+
reindented +
|
|
150
|
+
normalizedContent.slice(match.startIndex + match.matchedLength),
|
|
151
|
+
replacementCount: 1,
|
|
152
|
+
tier: 'indentation-flexible',
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
// Tool factory
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
export function createEditTool(config: EditToolConfig): {
|
|
163
|
+
name: string;
|
|
164
|
+
description: string;
|
|
165
|
+
parameters: typeof EditParams;
|
|
166
|
+
execute: (params: EditParamsType) => Promise<ToolContentDetails<EditDetails>>;
|
|
167
|
+
} {
|
|
168
|
+
const readRegistry = config.runtime?.readRegistry ?? config.readRegistry;
|
|
169
|
+
if (!readRegistry) {
|
|
170
|
+
throw new Error('createEditTool requires either runtime or readRegistry');
|
|
171
|
+
}
|
|
172
|
+
const fileMutationLock = config.runtime?.fileMutationLock ?? config.fileMutationLock;
|
|
173
|
+
const editHistory = config.runtime?.editHistory ?? config.editHistory;
|
|
174
|
+
|
|
175
|
+
/** Build a no-op result for early returns. */
|
|
176
|
+
function noChange(
|
|
177
|
+
filePath: string, oldString: string, newString: string,
|
|
178
|
+
replaceAll: boolean, text: string, originalContent = '',
|
|
179
|
+
): ToolContentDetails<EditDetails> {
|
|
180
|
+
return {
|
|
181
|
+
content: [{ type: 'text', text }],
|
|
182
|
+
details: { filePath, oldString, newString, replacementCount: 0, replaceAll, diff: [], originalContent },
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const tool = {
|
|
187
|
+
name: 'Edit',
|
|
188
|
+
description:
|
|
189
|
+
'Make precise string replacements in an existing file. ' +
|
|
190
|
+
'You MUST Read the file before using this tool. The edit will be rejected if the file has not been read first.',
|
|
191
|
+
parameters: EditParams,
|
|
192
|
+
|
|
193
|
+
async execute(params: EditParamsType): Promise<ToolContentDetails<EditDetails>> {
|
|
194
|
+
const filePath = path.resolve(params.file_path);
|
|
195
|
+
const oldString = params.old_string;
|
|
196
|
+
const newString = params.new_string;
|
|
197
|
+
const replaceAll = params.replace_all ?? false;
|
|
198
|
+
|
|
199
|
+
// Check identical strings (no lock needed)
|
|
200
|
+
if (oldString === newString) {
|
|
201
|
+
return noChange(filePath, oldString, newString, replaceAll,
|
|
202
|
+
'old_string and new_string are identical. No change needed.');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Check file exists (no lock needed)
|
|
206
|
+
let stat: fs.Stats;
|
|
207
|
+
try {
|
|
208
|
+
stat = await fs.promises.stat(filePath);
|
|
209
|
+
} catch (err: unknown) {
|
|
210
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
211
|
+
if (code === 'ENOENT') {
|
|
212
|
+
return noChange(filePath, oldString, newString, replaceAll,
|
|
213
|
+
`File does not exist: ${filePath}`);
|
|
214
|
+
}
|
|
215
|
+
if (code === 'EACCES') {
|
|
216
|
+
return noChange(filePath, oldString, newString, replaceAll,
|
|
217
|
+
`Permission denied: ${filePath}`);
|
|
218
|
+
}
|
|
219
|
+
throw err;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Acquire per-file mutation lock (serializes concurrent same-file edits)
|
|
223
|
+
const release = fileMutationLock ? await fileMutationLock.acquire(filePath) : undefined;
|
|
224
|
+
try {
|
|
225
|
+
// Enforce read-before-edit
|
|
226
|
+
if (!readRegistry.hasBeenRead(filePath)) {
|
|
227
|
+
return noChange(filePath, oldString, newString, replaceAll,
|
|
228
|
+
'You must Read this file before editing it.');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Mtime freshness check: reject if file changed since last Read.
|
|
232
|
+
// Using strict greater-than (not !==) to tolerate Windows/cloud-sync
|
|
233
|
+
// quirks where mtime can go backwards without a real modification.
|
|
234
|
+
// When mtime does indicate a change, fall back to a content-hash
|
|
235
|
+
// comparison (only possible for full reads) so formatter-style
|
|
236
|
+
// touches that don't change bytes still allow the edit.
|
|
237
|
+
const readState = readRegistry.getState(filePath);
|
|
238
|
+
let originalBuffer: Buffer | undefined;
|
|
239
|
+
if (readState) {
|
|
240
|
+
const currentStat = await fs.promises.stat(filePath);
|
|
241
|
+
if (currentStat.mtimeMs > readState.timestamp) {
|
|
242
|
+
let contentUnchanged = false;
|
|
243
|
+
if (readState.contentHash) {
|
|
244
|
+
originalBuffer = await fs.promises.readFile(filePath);
|
|
245
|
+
const currentHash = crypto.createHash('sha256')
|
|
246
|
+
.update(originalBuffer).digest('hex');
|
|
247
|
+
contentUnchanged = currentHash === readState.contentHash;
|
|
248
|
+
}
|
|
249
|
+
if (!contentUnchanged) {
|
|
250
|
+
readRegistry.invalidate(filePath);
|
|
251
|
+
return noChange(filePath, oldString, newString, replaceAll,
|
|
252
|
+
'File was modified since last Read. Read the file again before editing.');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Read the file content (reusing the buffer if we already loaded it
|
|
258
|
+
// for the content-hash fallback).
|
|
259
|
+
const originalContent = originalBuffer
|
|
260
|
+
? originalBuffer.toString('utf8')
|
|
261
|
+
: await fs.promises.readFile(filePath, 'utf8');
|
|
262
|
+
|
|
263
|
+
// Normalize line endings for matching: \r\n -> \n
|
|
264
|
+
// We'll do matching on normalized content but track whether the
|
|
265
|
+
// original had \r\n so we can preserve the original style.
|
|
266
|
+
const hadCRLF = originalContent.includes('\r\n');
|
|
267
|
+
const normalizedContent = originalContent.replace(/\r\n/g, '\n');
|
|
268
|
+
const normalizedOldString = oldString.replace(/\r\n/g, '\n');
|
|
269
|
+
const normalizedNewString = newString.replace(/\r\n/g, '\n');
|
|
270
|
+
|
|
271
|
+
// Resolve the match via the tiered cascade (see edit-matcher.ts):
|
|
272
|
+
// tier 1: exact — substring indexOf
|
|
273
|
+
// tier 2: line-trimmed — tolerates trailing whitespace
|
|
274
|
+
// tier 3: indentation-flexible — tolerates leading indent delta
|
|
275
|
+
// replace_all semantics apply only to tier 1; tier 2 and tier 3
|
|
276
|
+
// always resolve to a single replacement (ambiguity there rejects).
|
|
277
|
+
const match = findMatch(normalizedContent, normalizedOldString);
|
|
278
|
+
|
|
279
|
+
if (match.kind === 'none') {
|
|
280
|
+
const hint = findNearestMatch(normalizedContent, normalizedOldString);
|
|
281
|
+
const text = hint
|
|
282
|
+
? `The specified text was not found in the file.\n\nNearest match in ${path.basename(filePath)}:\n${hint.snippet}`
|
|
283
|
+
: 'The specified text was not found in the file.';
|
|
284
|
+
return noChange(
|
|
285
|
+
filePath, oldString, newString, replaceAll, text, originalContent,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (match.kind === 'ambiguous') {
|
|
290
|
+
const tolerance =
|
|
291
|
+
match.tier === 'line-trimmed'
|
|
292
|
+
? 'trailing-whitespace tolerance'
|
|
293
|
+
: 'indentation tolerance';
|
|
294
|
+
const lines = match.matchLines.join(', ');
|
|
295
|
+
const suffix = match.count > match.matchLines.length ? ' (first 3 shown)' : '';
|
|
296
|
+
return {
|
|
297
|
+
content: [{
|
|
298
|
+
type: 'text',
|
|
299
|
+
text:
|
|
300
|
+
`Found ${match.count} possible matches on lines ${lines}${suffix} via ${tolerance}. ` +
|
|
301
|
+
'No exact match exists. Tighten old_string to uniquely identify the edit location.',
|
|
302
|
+
}],
|
|
303
|
+
details: {
|
|
304
|
+
filePath, oldString, newString,
|
|
305
|
+
replacementCount: 0, replaceAll, diff: [], originalContent,
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (match.kind === 'exact' && !replaceAll && match.count > 1) {
|
|
311
|
+
const lines = match.matchLines.join(', ');
|
|
312
|
+
const suffix = match.count > match.matchLines.length ? ' (first 3 shown)' : '';
|
|
313
|
+
return {
|
|
314
|
+
content: [{
|
|
315
|
+
type: 'text',
|
|
316
|
+
text:
|
|
317
|
+
`Found ${match.count} exact matches on lines ${lines}${suffix}. ` +
|
|
318
|
+
'Provide more surrounding context to uniquely identify the edit location, or pass replace_all: true.',
|
|
319
|
+
}],
|
|
320
|
+
details: {
|
|
321
|
+
filePath, oldString, newString,
|
|
322
|
+
replacementCount: 0, replaceAll, diff: [], originalContent,
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const applied = applyReplacement(
|
|
328
|
+
match, normalizedContent, normalizedOldString, normalizedNewString, replaceAll,
|
|
329
|
+
);
|
|
330
|
+
if (!applied) {
|
|
331
|
+
// Unreachable: above guards cover 'none' and 'ambiguous'. Treat
|
|
332
|
+
// as a programming error rather than silently succeeding.
|
|
333
|
+
throw new Error(`Unexpected match kind: ${match.kind}`);
|
|
334
|
+
}
|
|
335
|
+
const newNormalizedContent = applied.newContent;
|
|
336
|
+
const replacementCount = applied.replacementCount;
|
|
337
|
+
const matchTier = applied.tier;
|
|
338
|
+
|
|
339
|
+
// Restore original line ending style if it was CRLF
|
|
340
|
+
const finalContent = hadCRLF
|
|
341
|
+
? newNormalizedContent.replace(/\n/g, '\r\n')
|
|
342
|
+
: newNormalizedContent;
|
|
343
|
+
|
|
344
|
+
// Compute diff
|
|
345
|
+
const diff = computeDiff(originalContent, finalContent);
|
|
346
|
+
|
|
347
|
+
// Atomic write: write to temp file, then rename
|
|
348
|
+
const tempPath = path.join(path.dirname(filePath), `.edit-${crypto.randomUUID()}.tmp`);
|
|
349
|
+
try {
|
|
350
|
+
await fs.promises.writeFile(tempPath, finalContent, 'utf8');
|
|
351
|
+
try {
|
|
352
|
+
await fs.promises.rename(tempPath, filePath);
|
|
353
|
+
} catch {
|
|
354
|
+
// Rename may fail on Windows if target is open. Fall back to direct write.
|
|
355
|
+
await fs.promises.writeFile(filePath, finalContent, 'utf8');
|
|
356
|
+
try { await fs.promises.unlink(tempPath); } catch { /* ignore */ }
|
|
357
|
+
}
|
|
358
|
+
} catch (writeErr) {
|
|
359
|
+
try { await fs.promises.unlink(tempPath); } catch { /* ignore */ }
|
|
360
|
+
throw writeErr;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Refresh read state: the agent's own edit is authoritative knowledge
|
|
364
|
+
// of current file contents, so subsequent edits don't require a re-read.
|
|
365
|
+
// We record the new mtime and a content hash of what we just wrote so
|
|
366
|
+
// external modifications still trigger the freshness check above.
|
|
367
|
+
// Also capture an EditHistory snapshot (when enabled) so UndoEdit
|
|
368
|
+
// can restore the prior contents while being able to detect
|
|
369
|
+
// post-edit external modifications.
|
|
370
|
+
try {
|
|
371
|
+
const postStat = await fs.promises.stat(filePath);
|
|
372
|
+
const postHash = crypto.createHash('sha256')
|
|
373
|
+
.update(finalContent, 'utf8').digest('hex');
|
|
374
|
+
readRegistry.markRead(filePath, {
|
|
375
|
+
timestamp: postStat.mtimeMs,
|
|
376
|
+
contentHash: postHash,
|
|
377
|
+
});
|
|
378
|
+
editHistory?.record(filePath, {
|
|
379
|
+
originalContent,
|
|
380
|
+
postMutationMtimeMs: postStat.mtimeMs,
|
|
381
|
+
postMutationContentHash: postHash,
|
|
382
|
+
source: 'Edit',
|
|
383
|
+
});
|
|
384
|
+
} catch {
|
|
385
|
+
readRegistry.invalidate(filePath);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const plural = replacementCount === 1 ? 'replacement' : 'replacements';
|
|
389
|
+
const tierSuffix =
|
|
390
|
+
matchTier === 'line-trimmed'
|
|
391
|
+
? ' (matched after trailing-whitespace tolerance)'
|
|
392
|
+
: matchTier === 'indentation-flexible'
|
|
393
|
+
? ' (matched after indentation tolerance)'
|
|
394
|
+
: '';
|
|
395
|
+
return {
|
|
396
|
+
content: [{
|
|
397
|
+
type: 'text',
|
|
398
|
+
text: `Made ${replacementCount} ${plural} in ${filePath}${tierSuffix}`,
|
|
399
|
+
}],
|
|
400
|
+
details: {
|
|
401
|
+
filePath, oldString, newString,
|
|
402
|
+
replacementCount, replaceAll, diff, originalContent,
|
|
403
|
+
matchTier,
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
} finally {
|
|
407
|
+
release?.();
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
return attachRuntimeAwareTool(tool, {
|
|
413
|
+
toolKind: 'Edit',
|
|
414
|
+
cloneForRuntime: (runtime) => createEditTool({
|
|
415
|
+
...config,
|
|
416
|
+
runtime,
|
|
417
|
+
readRegistry: runtime.readRegistry,
|
|
418
|
+
fileMutationLock: runtime.fileMutationLock,
|
|
419
|
+
editHistory: runtime.editHistory,
|
|
420
|
+
}),
|
|
421
|
+
});
|
|
422
|
+
}
|