@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,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UndoEdit tool: revert the most recent Edit or Write on a single file.
|
|
3
|
+
*
|
|
4
|
+
* Uses the per-runtime `EditHistory` stack: every successful Edit /
|
|
5
|
+
* Write push a pre-mutation snapshot, and `UndoEdit` pops the top
|
|
6
|
+
* snapshot to restore the prior state.
|
|
7
|
+
*
|
|
8
|
+
* The undo is guarded: we verify the current on-disk state still
|
|
9
|
+
* matches what we recorded at the moment the mutation completed (via
|
|
10
|
+
* mtime + SHA-256 hash). If anything has diverged since — another
|
|
11
|
+
* Edit/Write that wasn't history-tracked, a formatter, cloud sync —
|
|
12
|
+
* we refuse rather than overwrite unrelated changes.
|
|
13
|
+
*
|
|
14
|
+
* Reference: docs/cortex/tools/undo-edit.md
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import * as crypto from 'node:crypto';
|
|
18
|
+
import * as fs from 'node:fs';
|
|
19
|
+
import * as path from 'node:path';
|
|
20
|
+
import { Type, type Static } from 'typebox';
|
|
21
|
+
import type { EditHistory, EditHistoryEntry } from './shared/edit-history.js';
|
|
22
|
+
import type { FileMutationLock } from './shared/file-mutation-lock.js';
|
|
23
|
+
import type { ReadRegistry } from './shared/read-registry.js';
|
|
24
|
+
import type { ToolContentDetails } from '../types.js';
|
|
25
|
+
import type { CortexToolRuntime } from './runtime.js';
|
|
26
|
+
import { attachRuntimeAwareTool } from './runtime.js';
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Schema
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
export const UndoEditParams = Type.Object({
|
|
33
|
+
file_path: Type.String({
|
|
34
|
+
description: 'Absolute path to the file whose most recent Edit or Write should be reverted.',
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
export type UndoEditParamsType = Static<typeof UndoEditParams>;
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Details
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
export interface UndoEditDetails {
|
|
45
|
+
filePath: string;
|
|
46
|
+
/** The tool whose mutation was reverted (when the undo succeeds). */
|
|
47
|
+
revertedSource?: 'Edit' | 'Write';
|
|
48
|
+
/** True when the undo removed a file that Write had created. */
|
|
49
|
+
deleted?: boolean;
|
|
50
|
+
/** True when the undo restored prior content to an existing file. */
|
|
51
|
+
restored?: boolean;
|
|
52
|
+
/** True when the undo was rejected (no history, stale state, etc.). */
|
|
53
|
+
rejected?: boolean;
|
|
54
|
+
/** Remaining history depth for this file after the operation. */
|
|
55
|
+
remainingDepth?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Config
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
export interface UndoEditToolConfig {
|
|
63
|
+
runtime?: CortexToolRuntime | undefined;
|
|
64
|
+
editHistory?: EditHistory | undefined;
|
|
65
|
+
readRegistry?: ReadRegistry | undefined;
|
|
66
|
+
fileMutationLock?: FileMutationLock | undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Helpers
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
function hashBytes(data: Buffer | string): string {
|
|
74
|
+
return crypto.createHash('sha256').update(data).digest('hex');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function rejection(
|
|
78
|
+
filePath: string,
|
|
79
|
+
text: string,
|
|
80
|
+
remainingDepth: number,
|
|
81
|
+
): ToolContentDetails<UndoEditDetails> {
|
|
82
|
+
return {
|
|
83
|
+
content: [{ type: 'text', text }],
|
|
84
|
+
details: { filePath, rejected: true, remainingDepth },
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Atomically write the restored content to disk. Mirrors the pattern
|
|
90
|
+
* used by Edit and Write so concurrent consumers observe a coherent
|
|
91
|
+
* file at all times.
|
|
92
|
+
*/
|
|
93
|
+
async function atomicWrite(filePath: string, content: string): Promise<void> {
|
|
94
|
+
const tempPath = path.join(
|
|
95
|
+
path.dirname(filePath),
|
|
96
|
+
`.undo-${crypto.randomUUID()}.tmp`,
|
|
97
|
+
);
|
|
98
|
+
try {
|
|
99
|
+
await fs.promises.writeFile(tempPath, content, 'utf8');
|
|
100
|
+
try {
|
|
101
|
+
await fs.promises.rename(tempPath, filePath);
|
|
102
|
+
} catch {
|
|
103
|
+
// Rename may fail on Windows if the target is open. Fall back to a
|
|
104
|
+
// direct write and clean up the temp file.
|
|
105
|
+
await fs.promises.writeFile(filePath, content, 'utf8');
|
|
106
|
+
try { await fs.promises.unlink(tempPath); } catch { /* ignore */ }
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
try { await fs.promises.unlink(tempPath); } catch { /* ignore */ }
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Tool factory
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
export function createUndoEditTool(config: UndoEditToolConfig): {
|
|
119
|
+
name: string;
|
|
120
|
+
description: string;
|
|
121
|
+
parameters: typeof UndoEditParams;
|
|
122
|
+
execute: (params: UndoEditParamsType) => Promise<ToolContentDetails<UndoEditDetails>>;
|
|
123
|
+
} {
|
|
124
|
+
const editHistory = config.runtime?.editHistory ?? config.editHistory;
|
|
125
|
+
if (!editHistory) {
|
|
126
|
+
throw new Error('createUndoEditTool requires either runtime or editHistory');
|
|
127
|
+
}
|
|
128
|
+
const readRegistry = config.runtime?.readRegistry ?? config.readRegistry;
|
|
129
|
+
const fileMutationLock = config.runtime?.fileMutationLock ?? config.fileMutationLock;
|
|
130
|
+
|
|
131
|
+
const tool = {
|
|
132
|
+
name: 'UndoEdit',
|
|
133
|
+
description:
|
|
134
|
+
'Revert the most recent Edit or Write on a single file, returning it to the state it had before the last mutation. ' +
|
|
135
|
+
'Only reverts the most recent mutation; call repeatedly to roll back multiple steps (bounded history). ' +
|
|
136
|
+
'Refuses if the file has been modified externally since the mutation was recorded.',
|
|
137
|
+
parameters: UndoEditParams,
|
|
138
|
+
|
|
139
|
+
async execute(
|
|
140
|
+
params: UndoEditParamsType,
|
|
141
|
+
): Promise<ToolContentDetails<UndoEditDetails>> {
|
|
142
|
+
const filePath = path.resolve(params.file_path);
|
|
143
|
+
|
|
144
|
+
const release = fileMutationLock ? await fileMutationLock.acquire(filePath) : undefined;
|
|
145
|
+
try {
|
|
146
|
+
// Peek-then-pop pattern isn't expressible directly since `pop`
|
|
147
|
+
// returns the entry. We pop immediately and, on abort, push it
|
|
148
|
+
// back so the stack isn't silently truncated by a rejected
|
|
149
|
+
// undo.
|
|
150
|
+
const entry = editHistory.pop(filePath);
|
|
151
|
+
if (!entry) {
|
|
152
|
+
return rejection(
|
|
153
|
+
filePath,
|
|
154
|
+
`No recorded Edit or Write to undo for ${filePath}.`,
|
|
155
|
+
0,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const restoreEntry = (): void => editHistory.record(filePath, entry);
|
|
160
|
+
const remainingAfter = editHistory.depth(filePath);
|
|
161
|
+
|
|
162
|
+
// Verify current on-disk state matches what we recorded when
|
|
163
|
+
// the mutation completed. Any mismatch means the file drifted
|
|
164
|
+
// and undoing would stomp unrelated changes.
|
|
165
|
+
const verification = await verifyPostMutationState(filePath, entry);
|
|
166
|
+
if (verification.kind === 'drift') {
|
|
167
|
+
restoreEntry();
|
|
168
|
+
return rejection(
|
|
169
|
+
filePath,
|
|
170
|
+
`Cannot undo: ${verification.reason} Read the file to resync, then issue an explicit Edit or Write.`,
|
|
171
|
+
remainingAfter + 1,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Apply the revert.
|
|
176
|
+
if (entry.originalContent === null) {
|
|
177
|
+
// Write created the file; undo = delete.
|
|
178
|
+
try {
|
|
179
|
+
await fs.promises.unlink(filePath);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
restoreEntry();
|
|
182
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
183
|
+
return rejection(
|
|
184
|
+
filePath,
|
|
185
|
+
`Cannot undo: failed to delete ${filePath}: ${msg}`,
|
|
186
|
+
remainingAfter + 1,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
readRegistry?.invalidate(filePath);
|
|
190
|
+
return {
|
|
191
|
+
content: [{
|
|
192
|
+
type: 'text',
|
|
193
|
+
text: `Undid ${entry.source} of ${filePath} (file deleted; was created by the prior ${entry.source}).`,
|
|
194
|
+
}],
|
|
195
|
+
details: {
|
|
196
|
+
filePath,
|
|
197
|
+
revertedSource: entry.source,
|
|
198
|
+
deleted: true,
|
|
199
|
+
rejected: false,
|
|
200
|
+
remainingDepth: remainingAfter,
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Existing file — restore prior content.
|
|
206
|
+
try {
|
|
207
|
+
await atomicWrite(filePath, entry.originalContent);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
restoreEntry();
|
|
210
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
211
|
+
return rejection(
|
|
212
|
+
filePath,
|
|
213
|
+
`Cannot undo: failed to restore ${filePath}: ${msg}`,
|
|
214
|
+
remainingAfter + 1,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Refresh read state: after a successful undo, the agent's
|
|
219
|
+
// knowledge of the file is the restored content.
|
|
220
|
+
try {
|
|
221
|
+
const postStat = await fs.promises.stat(filePath);
|
|
222
|
+
const postHash = hashBytes(entry.originalContent);
|
|
223
|
+
readRegistry?.markRead(filePath, {
|
|
224
|
+
timestamp: postStat.mtimeMs,
|
|
225
|
+
contentHash: postHash,
|
|
226
|
+
});
|
|
227
|
+
} catch {
|
|
228
|
+
readRegistry?.invalidate(filePath);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
content: [{
|
|
233
|
+
type: 'text',
|
|
234
|
+
text: `Undid ${entry.source} of ${filePath} (restored ${entry.originalContent.length} character${entry.originalContent.length === 1 ? '' : 's'} of prior content).`,
|
|
235
|
+
}],
|
|
236
|
+
details: {
|
|
237
|
+
filePath,
|
|
238
|
+
revertedSource: entry.source,
|
|
239
|
+
restored: true,
|
|
240
|
+
rejected: false,
|
|
241
|
+
remainingDepth: remainingAfter,
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
} finally {
|
|
245
|
+
release?.();
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
return attachRuntimeAwareTool(tool, {
|
|
251
|
+
toolKind: 'UndoEdit',
|
|
252
|
+
cloneForRuntime: (runtime) => createUndoEditTool({
|
|
253
|
+
...config,
|
|
254
|
+
runtime,
|
|
255
|
+
editHistory: runtime.editHistory,
|
|
256
|
+
readRegistry: runtime.readRegistry,
|
|
257
|
+
fileMutationLock: runtime.fileMutationLock,
|
|
258
|
+
}),
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// Drift detection
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
type VerificationResult = { kind: 'ok' } | { kind: 'drift'; reason: string };
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Confirm that the file on disk still carries the exact post-mutation
|
|
270
|
+
* fingerprint we recorded. Two axes:
|
|
271
|
+
* - existence: the file must be present iff we expected it to be
|
|
272
|
+
* - identity: bytes must hash to the recorded value
|
|
273
|
+
* mtime is checked first as a cheap short-circuit; on mtime mismatch
|
|
274
|
+
* the hash check is authoritative (formatters / cloud sync can touch
|
|
275
|
+
* mtime without changing bytes).
|
|
276
|
+
*/
|
|
277
|
+
async function verifyPostMutationState(
|
|
278
|
+
filePath: string,
|
|
279
|
+
entry: EditHistoryEntry,
|
|
280
|
+
): Promise<VerificationResult> {
|
|
281
|
+
let stat: fs.Stats | undefined;
|
|
282
|
+
try {
|
|
283
|
+
stat = await fs.promises.stat(filePath);
|
|
284
|
+
} catch (err) {
|
|
285
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
286
|
+
if (code === 'ENOENT') {
|
|
287
|
+
return {
|
|
288
|
+
kind: 'drift',
|
|
289
|
+
reason: 'the file has been deleted since the recorded mutation.',
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
kind: 'drift',
|
|
294
|
+
reason: `the file is inaccessible (${code ?? 'unknown error'}).`,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Fast path: mtime matches exactly. (We use equality here because
|
|
299
|
+
// we're comparing against our OWN previously-recorded mtime, not a
|
|
300
|
+
// user-provided read state — quirks around mtime going backwards
|
|
301
|
+
// aren't relevant when both values came from our writes.)
|
|
302
|
+
if (stat.mtimeMs === entry.postMutationMtimeMs) return { kind: 'ok' };
|
|
303
|
+
|
|
304
|
+
// mtime drifted — confirm via hash before rejecting, since formatters
|
|
305
|
+
// and cloud-sync can touch mtime without changing bytes.
|
|
306
|
+
const bytes = await fs.promises.readFile(filePath);
|
|
307
|
+
const currentHash = hashBytes(bytes);
|
|
308
|
+
if (currentHash === entry.postMutationContentHash) return { kind: 'ok' };
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
kind: 'drift',
|
|
312
|
+
reason: 'the file has been modified since the recorded mutation.',
|
|
313
|
+
};
|
|
314
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-memory URL cache with 15-minute TTL for WebFetch tool.
|
|
3
|
+
*
|
|
4
|
+
* Keyed by URL. Cached entries include the converted markdown content
|
|
5
|
+
* so repeated fetches skip both the HTTP request and HTML conversion.
|
|
6
|
+
*
|
|
7
|
+
* Self-cleaning: expired entries are removed on access and periodically.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Types
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
export interface CacheEntry {
|
|
15
|
+
content: string;
|
|
16
|
+
fetchedAt: number;
|
|
17
|
+
statusCode: number;
|
|
18
|
+
finalUrl: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Constants
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const DEFAULT_TTL_MS = 15 * 60 * 1000; // 15 minutes
|
|
26
|
+
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Cache
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
export class WebFetchCache {
|
|
33
|
+
private readonly cache = new Map<string, CacheEntry>();
|
|
34
|
+
private readonly ttlMs: number;
|
|
35
|
+
private cleanupTimer: ReturnType<typeof setInterval> | null = null;
|
|
36
|
+
|
|
37
|
+
constructor(ttlMs: number = DEFAULT_TTL_MS) {
|
|
38
|
+
this.ttlMs = ttlMs;
|
|
39
|
+
// Start periodic cleanup
|
|
40
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), CLEANUP_INTERVAL_MS);
|
|
41
|
+
// Unref the timer so it doesn't prevent process exit
|
|
42
|
+
if (this.cleanupTimer && typeof this.cleanupTimer === 'object' && 'unref' in this.cleanupTimer) {
|
|
43
|
+
this.cleanupTimer.unref();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get a cached entry for a URL, or undefined if not cached or expired.
|
|
49
|
+
*/
|
|
50
|
+
get(url: string): CacheEntry | undefined {
|
|
51
|
+
const entry = this.cache.get(url);
|
|
52
|
+
if (!entry) return undefined;
|
|
53
|
+
|
|
54
|
+
if (Date.now() - entry.fetchedAt > this.ttlMs) {
|
|
55
|
+
this.cache.delete(url);
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return entry;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Store a cache entry for a URL.
|
|
64
|
+
*/
|
|
65
|
+
set(url: string, entry: CacheEntry): void {
|
|
66
|
+
this.cache.set(url, entry);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if a URL has a valid (non-expired) cache entry.
|
|
71
|
+
*/
|
|
72
|
+
has(url: string): boolean {
|
|
73
|
+
return this.get(url) !== undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Remove expired entries.
|
|
78
|
+
*/
|
|
79
|
+
cleanup(): void {
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
for (const [url, entry] of this.cache) {
|
|
82
|
+
if (now - entry.fetchedAt > this.ttlMs) {
|
|
83
|
+
this.cache.delete(url);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Clear all cached entries.
|
|
90
|
+
*/
|
|
91
|
+
clear(): void {
|
|
92
|
+
this.cache.clear();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Stop the cleanup timer. Call on shutdown.
|
|
97
|
+
*/
|
|
98
|
+
destroy(): void {
|
|
99
|
+
if (this.cleanupTimer) {
|
|
100
|
+
clearInterval(this.cleanupTimer);
|
|
101
|
+
this.cleanupTimer = null;
|
|
102
|
+
}
|
|
103
|
+
this.cache.clear();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get the number of cached entries (for diagnostics).
|
|
108
|
+
*/
|
|
109
|
+
get size(): number {
|
|
110
|
+
return this.cache.size;
|
|
111
|
+
}
|
|
112
|
+
}
|