@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.
Files changed (293) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/dist/budget-guard.d.ts +75 -0
  4. package/dist/budget-guard.d.ts.map +1 -0
  5. package/dist/budget-guard.js +142 -0
  6. package/dist/budget-guard.js.map +1 -0
  7. package/dist/compaction/compaction.d.ts +99 -0
  8. package/dist/compaction/compaction.d.ts.map +1 -0
  9. package/dist/compaction/compaction.js +302 -0
  10. package/dist/compaction/compaction.js.map +1 -0
  11. package/dist/compaction/failsafe.d.ts +57 -0
  12. package/dist/compaction/failsafe.d.ts.map +1 -0
  13. package/dist/compaction/failsafe.js +135 -0
  14. package/dist/compaction/failsafe.js.map +1 -0
  15. package/dist/compaction/index.d.ts +381 -0
  16. package/dist/compaction/index.d.ts.map +1 -0
  17. package/dist/compaction/index.js +979 -0
  18. package/dist/compaction/index.js.map +1 -0
  19. package/dist/compaction/microcompaction.d.ts +219 -0
  20. package/dist/compaction/microcompaction.d.ts.map +1 -0
  21. package/dist/compaction/microcompaction.js +536 -0
  22. package/dist/compaction/microcompaction.js.map +1 -0
  23. package/dist/compaction/observational/buffering.d.ts +225 -0
  24. package/dist/compaction/observational/buffering.d.ts.map +1 -0
  25. package/dist/compaction/observational/buffering.js +354 -0
  26. package/dist/compaction/observational/buffering.js.map +1 -0
  27. package/dist/compaction/observational/constants.d.ts +70 -0
  28. package/dist/compaction/observational/constants.d.ts.map +1 -0
  29. package/dist/compaction/observational/constants.js +507 -0
  30. package/dist/compaction/observational/constants.js.map +1 -0
  31. package/dist/compaction/observational/index.d.ts +219 -0
  32. package/dist/compaction/observational/index.d.ts.map +1 -0
  33. package/dist/compaction/observational/index.js +641 -0
  34. package/dist/compaction/observational/index.js.map +1 -0
  35. package/dist/compaction/observational/observer.d.ts +97 -0
  36. package/dist/compaction/observational/observer.d.ts.map +1 -0
  37. package/dist/compaction/observational/observer.js +424 -0
  38. package/dist/compaction/observational/observer.js.map +1 -0
  39. package/dist/compaction/observational/recall-tool.d.ts +27 -0
  40. package/dist/compaction/observational/recall-tool.d.ts.map +1 -0
  41. package/dist/compaction/observational/recall-tool.js +93 -0
  42. package/dist/compaction/observational/recall-tool.js.map +1 -0
  43. package/dist/compaction/observational/reflector.d.ts +94 -0
  44. package/dist/compaction/observational/reflector.d.ts.map +1 -0
  45. package/dist/compaction/observational/reflector.js +167 -0
  46. package/dist/compaction/observational/reflector.js.map +1 -0
  47. package/dist/compaction/observational/types.d.ts +271 -0
  48. package/dist/compaction/observational/types.d.ts.map +1 -0
  49. package/dist/compaction/observational/types.js +15 -0
  50. package/dist/compaction/observational/types.js.map +1 -0
  51. package/dist/context-manager.d.ts +134 -0
  52. package/dist/context-manager.d.ts.map +1 -0
  53. package/dist/context-manager.js +170 -0
  54. package/dist/context-manager.js.map +1 -0
  55. package/dist/cortex-agent.d.ts +1020 -0
  56. package/dist/cortex-agent.d.ts.map +1 -0
  57. package/dist/cortex-agent.js +3589 -0
  58. package/dist/cortex-agent.js.map +1 -0
  59. package/dist/error-classifier.d.ts +48 -0
  60. package/dist/error-classifier.d.ts.map +1 -0
  61. package/dist/error-classifier.js +152 -0
  62. package/dist/error-classifier.js.map +1 -0
  63. package/dist/event-bridge.d.ts +166 -0
  64. package/dist/event-bridge.d.ts.map +1 -0
  65. package/dist/event-bridge.js +381 -0
  66. package/dist/event-bridge.js.map +1 -0
  67. package/dist/index.d.ts +55 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +57 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/mcp-client.d.ts +119 -0
  72. package/dist/mcp-client.d.ts.map +1 -0
  73. package/dist/mcp-client.js +474 -0
  74. package/dist/mcp-client.js.map +1 -0
  75. package/dist/model-wrapper.d.ts +58 -0
  76. package/dist/model-wrapper.d.ts.map +1 -0
  77. package/dist/model-wrapper.js +86 -0
  78. package/dist/model-wrapper.js.map +1 -0
  79. package/dist/noop-logger.d.ts +4 -0
  80. package/dist/noop-logger.d.ts.map +1 -0
  81. package/dist/noop-logger.js +8 -0
  82. package/dist/noop-logger.js.map +1 -0
  83. package/dist/prompt-diagnostics.d.ts +47 -0
  84. package/dist/prompt-diagnostics.d.ts.map +1 -0
  85. package/dist/prompt-diagnostics.js +230 -0
  86. package/dist/prompt-diagnostics.js.map +1 -0
  87. package/dist/provider-manager.d.ts +224 -0
  88. package/dist/provider-manager.d.ts.map +1 -0
  89. package/dist/provider-manager.js +563 -0
  90. package/dist/provider-manager.js.map +1 -0
  91. package/dist/provider-registry.d.ts +115 -0
  92. package/dist/provider-registry.d.ts.map +1 -0
  93. package/dist/provider-registry.js +305 -0
  94. package/dist/provider-registry.js.map +1 -0
  95. package/dist/schema-converter.d.ts +20 -0
  96. package/dist/schema-converter.d.ts.map +1 -0
  97. package/dist/schema-converter.js +48 -0
  98. package/dist/schema-converter.js.map +1 -0
  99. package/dist/skill-preprocessor.d.ts +46 -0
  100. package/dist/skill-preprocessor.d.ts.map +1 -0
  101. package/dist/skill-preprocessor.js +237 -0
  102. package/dist/skill-preprocessor.js.map +1 -0
  103. package/dist/skill-registry.d.ts +107 -0
  104. package/dist/skill-registry.d.ts.map +1 -0
  105. package/dist/skill-registry.js +330 -0
  106. package/dist/skill-registry.js.map +1 -0
  107. package/dist/skill-tool.d.ts +54 -0
  108. package/dist/skill-tool.d.ts.map +1 -0
  109. package/dist/skill-tool.js +88 -0
  110. package/dist/skill-tool.js.map +1 -0
  111. package/dist/sub-agent-manager.d.ts +90 -0
  112. package/dist/sub-agent-manager.d.ts.map +1 -0
  113. package/dist/sub-agent-manager.js +192 -0
  114. package/dist/sub-agent-manager.js.map +1 -0
  115. package/dist/token-estimator.d.ts +23 -0
  116. package/dist/token-estimator.d.ts.map +1 -0
  117. package/dist/token-estimator.js +27 -0
  118. package/dist/token-estimator.js.map +1 -0
  119. package/dist/tool-contract.d.ts +68 -0
  120. package/dist/tool-contract.d.ts.map +1 -0
  121. package/dist/tool-contract.js +35 -0
  122. package/dist/tool-contract.js.map +1 -0
  123. package/dist/tool-result-persistence.d.ts +89 -0
  124. package/dist/tool-result-persistence.d.ts.map +1 -0
  125. package/dist/tool-result-persistence.js +152 -0
  126. package/dist/tool-result-persistence.js.map +1 -0
  127. package/dist/tools/bash/index.d.ts +71 -0
  128. package/dist/tools/bash/index.d.ts.map +1 -0
  129. package/dist/tools/bash/index.js +485 -0
  130. package/dist/tools/bash/index.js.map +1 -0
  131. package/dist/tools/bash/interactive.d.ts +47 -0
  132. package/dist/tools/bash/interactive.d.ts.map +1 -0
  133. package/dist/tools/bash/interactive.js +262 -0
  134. package/dist/tools/bash/interactive.js.map +1 -0
  135. package/dist/tools/bash/safety.d.ts +149 -0
  136. package/dist/tools/bash/safety.d.ts.map +1 -0
  137. package/dist/tools/bash/safety.js +1116 -0
  138. package/dist/tools/bash/safety.js.map +1 -0
  139. package/dist/tools/edit.d.ts +57 -0
  140. package/dist/tools/edit.d.ts.map +1 -0
  141. package/dist/tools/edit.js +310 -0
  142. package/dist/tools/edit.js.map +1 -0
  143. package/dist/tools/glob.d.ts +34 -0
  144. package/dist/tools/glob.d.ts.map +1 -0
  145. package/dist/tools/glob.js +268 -0
  146. package/dist/tools/glob.js.map +1 -0
  147. package/dist/tools/grep.d.ts +53 -0
  148. package/dist/tools/grep.d.ts.map +1 -0
  149. package/dist/tools/grep.js +673 -0
  150. package/dist/tools/grep.js.map +1 -0
  151. package/dist/tools/index.d.ts +62 -0
  152. package/dist/tools/index.d.ts.map +1 -0
  153. package/dist/tools/index.js +52 -0
  154. package/dist/tools/index.js.map +1 -0
  155. package/dist/tools/read.d.ts +43 -0
  156. package/dist/tools/read.d.ts.map +1 -0
  157. package/dist/tools/read.js +459 -0
  158. package/dist/tools/read.js.map +1 -0
  159. package/dist/tools/runtime.d.ts +62 -0
  160. package/dist/tools/runtime.d.ts.map +1 -0
  161. package/dist/tools/runtime.js +116 -0
  162. package/dist/tools/runtime.js.map +1 -0
  163. package/dist/tools/shared/cwd-tracker.d.ts +32 -0
  164. package/dist/tools/shared/cwd-tracker.d.ts.map +1 -0
  165. package/dist/tools/shared/cwd-tracker.js +44 -0
  166. package/dist/tools/shared/cwd-tracker.js.map +1 -0
  167. package/dist/tools/shared/edit-history.d.ts +55 -0
  168. package/dist/tools/shared/edit-history.d.ts.map +1 -0
  169. package/dist/tools/shared/edit-history.js +72 -0
  170. package/dist/tools/shared/edit-history.js.map +1 -0
  171. package/dist/tools/shared/edit-matcher.d.ts +83 -0
  172. package/dist/tools/shared/edit-matcher.d.ts.map +1 -0
  173. package/dist/tools/shared/edit-matcher.js +359 -0
  174. package/dist/tools/shared/edit-matcher.js.map +1 -0
  175. package/dist/tools/shared/file-mutation-lock.d.ts +22 -0
  176. package/dist/tools/shared/file-mutation-lock.d.ts.map +1 -0
  177. package/dist/tools/shared/file-mutation-lock.js +35 -0
  178. package/dist/tools/shared/file-mutation-lock.js.map +1 -0
  179. package/dist/tools/shared/gitignore.d.ts +17 -0
  180. package/dist/tools/shared/gitignore.d.ts.map +1 -0
  181. package/dist/tools/shared/gitignore.js +59 -0
  182. package/dist/tools/shared/gitignore.js.map +1 -0
  183. package/dist/tools/shared/pdf-extractor.d.ts +96 -0
  184. package/dist/tools/shared/pdf-extractor.d.ts.map +1 -0
  185. package/dist/tools/shared/pdf-extractor.js +196 -0
  186. package/dist/tools/shared/pdf-extractor.js.map +1 -0
  187. package/dist/tools/shared/read-registry.d.ts +66 -0
  188. package/dist/tools/shared/read-registry.d.ts.map +1 -0
  189. package/dist/tools/shared/read-registry.js +65 -0
  190. package/dist/tools/shared/read-registry.js.map +1 -0
  191. package/dist/tools/shared/safe-env.d.ts +18 -0
  192. package/dist/tools/shared/safe-env.d.ts.map +1 -0
  193. package/dist/tools/shared/safe-env.js +70 -0
  194. package/dist/tools/shared/safe-env.js.map +1 -0
  195. package/dist/tools/sub-agent.d.ts +91 -0
  196. package/dist/tools/sub-agent.d.ts.map +1 -0
  197. package/dist/tools/sub-agent.js +89 -0
  198. package/dist/tools/sub-agent.js.map +1 -0
  199. package/dist/tools/task-output.d.ts +38 -0
  200. package/dist/tools/task-output.d.ts.map +1 -0
  201. package/dist/tools/task-output.js +186 -0
  202. package/dist/tools/task-output.js.map +1 -0
  203. package/dist/tools/tool-search/index.d.ts +40 -0
  204. package/dist/tools/tool-search/index.d.ts.map +1 -0
  205. package/dist/tools/tool-search/index.js +110 -0
  206. package/dist/tools/tool-search/index.js.map +1 -0
  207. package/dist/tools/tool-search/registry.d.ts +82 -0
  208. package/dist/tools/tool-search/registry.d.ts.map +1 -0
  209. package/dist/tools/tool-search/registry.js +238 -0
  210. package/dist/tools/tool-search/registry.js.map +1 -0
  211. package/dist/tools/undo-edit.d.ts +51 -0
  212. package/dist/tools/undo-edit.d.ts.map +1 -0
  213. package/dist/tools/undo-edit.js +231 -0
  214. package/dist/tools/undo-edit.js.map +1 -0
  215. package/dist/tools/web-fetch/cache.d.ts +49 -0
  216. package/dist/tools/web-fetch/cache.d.ts.map +1 -0
  217. package/dist/tools/web-fetch/cache.js +89 -0
  218. package/dist/tools/web-fetch/cache.js.map +1 -0
  219. package/dist/tools/web-fetch/index.d.ts +53 -0
  220. package/dist/tools/web-fetch/index.d.ts.map +1 -0
  221. package/dist/tools/web-fetch/index.js +513 -0
  222. package/dist/tools/web-fetch/index.js.map +1 -0
  223. package/dist/tools/write.d.ts +59 -0
  224. package/dist/tools/write.d.ts.map +1 -0
  225. package/dist/tools/write.js +316 -0
  226. package/dist/tools/write.js.map +1 -0
  227. package/dist/types.d.ts +881 -0
  228. package/dist/types.d.ts.map +1 -0
  229. package/dist/types.js +16 -0
  230. package/dist/types.js.map +1 -0
  231. package/dist/working-tags.d.ts +44 -0
  232. package/dist/working-tags.d.ts.map +1 -0
  233. package/dist/working-tags.js +103 -0
  234. package/dist/working-tags.js.map +1 -0
  235. package/package.json +87 -0
  236. package/src/budget-guard.ts +170 -0
  237. package/src/compaction/compaction.ts +386 -0
  238. package/src/compaction/failsafe.ts +185 -0
  239. package/src/compaction/index.ts +1199 -0
  240. package/src/compaction/microcompaction.ts +709 -0
  241. package/src/compaction/observational/buffering.ts +430 -0
  242. package/src/compaction/observational/constants.ts +532 -0
  243. package/src/compaction/observational/index.ts +837 -0
  244. package/src/compaction/observational/observer.ts +510 -0
  245. package/src/compaction/observational/recall-tool.ts +130 -0
  246. package/src/compaction/observational/reflector.ts +221 -0
  247. package/src/compaction/observational/types.ts +343 -0
  248. package/src/context-manager.ts +237 -0
  249. package/src/cortex-agent.ts +4297 -0
  250. package/src/error-classifier.ts +199 -0
  251. package/src/event-bridge.ts +508 -0
  252. package/src/index.ts +292 -0
  253. package/src/mcp-client.ts +582 -0
  254. package/src/model-wrapper.ts +128 -0
  255. package/src/noop-logger.ts +9 -0
  256. package/src/prompt-diagnostics.ts +296 -0
  257. package/src/provider-manager.ts +823 -0
  258. package/src/provider-registry.ts +386 -0
  259. package/src/schema-converter.ts +51 -0
  260. package/src/skill-preprocessor.ts +314 -0
  261. package/src/skill-registry.ts +378 -0
  262. package/src/skill-tool.ts +130 -0
  263. package/src/sub-agent-manager.ts +236 -0
  264. package/src/token-estimator.ts +26 -0
  265. package/src/tool-contract.ts +113 -0
  266. package/src/tool-result-persistence.ts +197 -0
  267. package/src/tools/bash/index.ts +633 -0
  268. package/src/tools/bash/interactive.ts +302 -0
  269. package/src/tools/bash/safety.ts +1297 -0
  270. package/src/tools/edit.ts +422 -0
  271. package/src/tools/glob.ts +330 -0
  272. package/src/tools/grep.ts +819 -0
  273. package/src/tools/index.ts +110 -0
  274. package/src/tools/read.ts +580 -0
  275. package/src/tools/runtime.ts +173 -0
  276. package/src/tools/shared/cwd-tracker.ts +50 -0
  277. package/src/tools/shared/edit-history.ts +96 -0
  278. package/src/tools/shared/edit-matcher.ts +457 -0
  279. package/src/tools/shared/file-mutation-lock.ts +40 -0
  280. package/src/tools/shared/gitignore.ts +61 -0
  281. package/src/tools/shared/pdf-extractor.ts +290 -0
  282. package/src/tools/shared/read-registry.ts +93 -0
  283. package/src/tools/shared/safe-env.ts +82 -0
  284. package/src/tools/sub-agent.ts +171 -0
  285. package/src/tools/task-output.ts +236 -0
  286. package/src/tools/tool-search/index.ts +167 -0
  287. package/src/tools/tool-search/registry.ts +278 -0
  288. package/src/tools/undo-edit.ts +314 -0
  289. package/src/tools/web-fetch/cache.ts +112 -0
  290. package/src/tools/web-fetch/index.ts +604 -0
  291. package/src/tools/write.ts +385 -0
  292. package/src/types.ts +1057 -0
  293. package/src/working-tags.ts +118 -0
@@ -0,0 +1,385 @@
1
+ /**
2
+ * Write tool: create a new file or overwrite an existing file.
3
+ *
4
+ * Enforces the read-before-write contract via ReadRegistry.
5
+ * Performs atomic writes (write to temp, then rename) to prevent
6
+ * partial writes on crash. Creates parent directories as needed.
7
+ *
8
+ * Reference: docs/cortex/tools/write.md
9
+ */
10
+
11
+ import * as fs from 'node:fs';
12
+ import * as path from 'node:path';
13
+ import * as crypto from 'node:crypto';
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 type { CortexToolRuntime } from './runtime.js';
20
+ import { attachRuntimeAwareTool } from './runtime.js';
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Schema
24
+ // ---------------------------------------------------------------------------
25
+
26
+ export const WriteParams = Type.Object({
27
+ file_path: Type.String({ description: 'Absolute path to the file to write (must be absolute, not relative)' }),
28
+ content: Type.String({ description: 'The full content to write to the file' }),
29
+ });
30
+
31
+ export type WriteParamsType = Static<typeof WriteParams>;
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Details type
35
+ // ---------------------------------------------------------------------------
36
+
37
+ export interface DiffHunk {
38
+ oldStart: number;
39
+ oldLines: number;
40
+ newStart: number;
41
+ newLines: number;
42
+ lines: string[];
43
+ }
44
+
45
+ export interface WriteDetails {
46
+ filePath: string;
47
+ isCreate: boolean;
48
+ bytesWritten: number;
49
+ diff: DiffHunk[] | null;
50
+ originalContent: string | null;
51
+ }
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Config
55
+ // ---------------------------------------------------------------------------
56
+
57
+ export interface WriteToolConfig {
58
+ runtime?: CortexToolRuntime | undefined;
59
+ readRegistry?: ReadRegistry | undefined;
60
+ fileMutationLock?: FileMutationLock | undefined;
61
+ /**
62
+ * Undo stack. When provided, every successful write pushes a
63
+ * pre-mutation snapshot (or `null` when the file did not exist) so
64
+ * `UndoEdit` can restore the prior state.
65
+ */
66
+ editHistory?: EditHistory | undefined;
67
+ }
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // Helpers
71
+ // ---------------------------------------------------------------------------
72
+
73
+ /**
74
+ * Compute a safe line-level diff between two strings.
75
+ *
76
+ * This intentionally emits at most one changed hunk. The UI only needs a
77
+ * compact, deterministic summary of the mutation, not a minimal diff.
78
+ */
79
+ export function computeDiff(oldContent: string, newContent: string): DiffHunk[] {
80
+ const oldLines = oldContent.split('\n');
81
+ const newLines = newContent.split('\n');
82
+
83
+ let prefix = 0;
84
+ while (
85
+ prefix < oldLines.length &&
86
+ prefix < newLines.length &&
87
+ oldLines[prefix] === newLines[prefix]
88
+ ) {
89
+ prefix += 1;
90
+ }
91
+
92
+ if (prefix === oldLines.length && prefix === newLines.length) {
93
+ return [];
94
+ }
95
+
96
+ let oldSuffix = oldLines.length - 1;
97
+ let newSuffix = newLines.length - 1;
98
+ while (
99
+ oldSuffix >= prefix &&
100
+ newSuffix >= prefix &&
101
+ oldLines[oldSuffix] === newLines[newSuffix]
102
+ ) {
103
+ oldSuffix -= 1;
104
+ newSuffix -= 1;
105
+ }
106
+
107
+ const removedLines = oldSuffix >= prefix
108
+ ? oldLines.slice(prefix, oldSuffix + 1).map((line) => `-${line}`)
109
+ : [];
110
+ const addedLines = newSuffix >= prefix
111
+ ? newLines.slice(prefix, newSuffix + 1).map((line) => `+${line}`)
112
+ : [];
113
+
114
+ return [{
115
+ oldStart: prefix + 1,
116
+ oldLines: removedLines.length,
117
+ newStart: prefix + 1,
118
+ newLines: addedLines.length,
119
+ lines: [...removedLines, ...addedLines],
120
+ }];
121
+ }
122
+
123
+ // ---------------------------------------------------------------------------
124
+ // Tool factory
125
+ // ---------------------------------------------------------------------------
126
+
127
+ export function createWriteTool(config: WriteToolConfig): {
128
+ name: string;
129
+ description: string;
130
+ parameters: typeof WriteParams;
131
+ execute: (params: WriteParamsType) => Promise<ToolContentDetails<WriteDetails>>;
132
+ } {
133
+ const readRegistry = config.runtime?.readRegistry ?? config.readRegistry;
134
+ const editHistory = config.runtime?.editHistory ?? config.editHistory;
135
+ if (!readRegistry) {
136
+ throw new Error('createWriteTool requires either runtime or readRegistry');
137
+ }
138
+ const fileMutationLock = config.runtime?.fileMutationLock ?? config.fileMutationLock;
139
+
140
+ const tool = {
141
+ name: 'Write',
142
+ description:
143
+ 'Create a new file or overwrite an existing file on the local filesystem. ' +
144
+ 'If the file already exists, you MUST Read it before using this tool. The write will be rejected if an existing file has not been read first.',
145
+ parameters: WriteParams,
146
+
147
+ async execute(params: WriteParamsType): Promise<ToolContentDetails<WriteDetails>> {
148
+ const filePath = path.resolve(params.file_path);
149
+ const newContent = params.content;
150
+
151
+ // Check if file exists (before acquiring lock)
152
+ let fileExists = false;
153
+ try {
154
+ const stat = await fs.promises.stat(filePath);
155
+ fileExists = stat.isFile();
156
+ } catch {
157
+ // File does not exist - that's fine for creation
158
+ }
159
+
160
+ // Acquire per-file mutation lock (serializes concurrent same-file writes)
161
+ const release = fileMutationLock ? await fileMutationLock.acquire(filePath) : undefined;
162
+ try {
163
+ let originalContent: string | null = null;
164
+
165
+ // Re-check existence after acquiring lock (another tool may have created/deleted it)
166
+ try {
167
+ const stat = await fs.promises.stat(filePath);
168
+ fileExists = stat.isFile();
169
+ } catch {
170
+ fileExists = false;
171
+ }
172
+
173
+ // Enforce read-before-write for existing files
174
+ if (fileExists && !readRegistry.hasBeenRead(filePath)) {
175
+ return {
176
+ content: [{ type: 'text', text: 'You must Read this file before overwriting it.' }],
177
+ details: {
178
+ filePath,
179
+ isCreate: false,
180
+ bytesWritten: 0,
181
+ diff: null,
182
+ originalContent: null,
183
+ },
184
+ };
185
+ }
186
+
187
+ // Mtime freshness check: reject if file changed on disk since last Read.
188
+ // Using strict greater-than (not !==) to tolerate Windows/cloud-sync
189
+ // quirks where mtime can go backwards without a real modification.
190
+ // When mtime does indicate a change, fall back to a content-hash
191
+ // comparison (only possible for full reads) so formatter-style
192
+ // touches that don't change bytes still allow the write.
193
+ let originalBuffer: Buffer | undefined;
194
+ if (fileExists) {
195
+ const readState = readRegistry.getState(filePath);
196
+ if (readState) {
197
+ const currentStat = await fs.promises.stat(filePath);
198
+ if (currentStat.mtimeMs > readState.timestamp) {
199
+ let contentUnchanged = false;
200
+ if (readState.contentHash) {
201
+ try {
202
+ originalBuffer = await fs.promises.readFile(filePath);
203
+ const currentHash = crypto.createHash('sha256')
204
+ .update(originalBuffer).digest('hex');
205
+ contentUnchanged = currentHash === readState.contentHash;
206
+ } catch {
207
+ // If we can't read, fall through to the rejection path.
208
+ }
209
+ }
210
+ if (!contentUnchanged) {
211
+ readRegistry.invalidate(filePath);
212
+ return {
213
+ content: [{
214
+ type: 'text',
215
+ text: 'File was modified since last Read. Read the file again before overwriting it.',
216
+ }],
217
+ details: {
218
+ filePath,
219
+ isCreate: false,
220
+ bytesWritten: 0,
221
+ diff: null,
222
+ originalContent: null,
223
+ },
224
+ };
225
+ }
226
+ }
227
+ }
228
+ }
229
+
230
+ // Read original content for diff (existing files only). Reuse the
231
+ // buffer from the content-hash check if we already loaded it.
232
+ if (fileExists) {
233
+ try {
234
+ originalContent = originalBuffer
235
+ ? originalBuffer.toString('utf8')
236
+ : await fs.promises.readFile(filePath, 'utf8');
237
+ } catch {
238
+ // If we can't read for diff, continue without it
239
+ }
240
+ }
241
+
242
+ // Create parent directories
243
+ const parentDir = path.dirname(filePath);
244
+ try {
245
+ await fs.promises.mkdir(parentDir, { recursive: true });
246
+ } catch (err: unknown) {
247
+ const code = (err as NodeJS.ErrnoException).code;
248
+ if (code === 'EACCES') {
249
+ return {
250
+ content: [{ type: 'text', text: `Cannot create directory: ${parentDir}` }],
251
+ details: {
252
+ filePath,
253
+ isCreate: !fileExists,
254
+ bytesWritten: 0,
255
+ diff: null,
256
+ originalContent,
257
+ },
258
+ };
259
+ }
260
+ throw err;
261
+ }
262
+
263
+ // Atomic write: write to temp file, then rename
264
+ const tempPath = path.join(parentDir, `.write-${crypto.randomUUID()}.tmp`);
265
+ try {
266
+ await fs.promises.writeFile(tempPath, newContent, 'utf8');
267
+ try {
268
+ await fs.promises.rename(tempPath, filePath);
269
+ } catch {
270
+ // Rename may fail on Windows if target is open. Fall back to direct write.
271
+ await fs.promises.writeFile(filePath, newContent, 'utf8');
272
+ // Clean up temp file
273
+ try {
274
+ await fs.promises.unlink(tempPath);
275
+ } catch {
276
+ // Ignore cleanup errors
277
+ }
278
+ }
279
+ } catch (err: unknown) {
280
+ // Clean up temp file on error
281
+ try {
282
+ await fs.promises.unlink(tempPath);
283
+ } catch {
284
+ // Ignore cleanup errors
285
+ }
286
+
287
+ const code = (err as NodeJS.ErrnoException).code;
288
+ if (code === 'EACCES') {
289
+ return {
290
+ content: [{ type: 'text', text: `Permission denied: ${filePath}` }],
291
+ details: {
292
+ filePath,
293
+ isCreate: !fileExists,
294
+ bytesWritten: 0,
295
+ diff: null,
296
+ originalContent,
297
+ },
298
+ };
299
+ }
300
+ if (code === 'ENOSPC') {
301
+ return {
302
+ content: [{ type: 'text', text: `Disk full. Cannot write to: ${filePath}` }],
303
+ details: {
304
+ filePath,
305
+ isCreate: !fileExists,
306
+ bytesWritten: 0,
307
+ diff: null,
308
+ originalContent,
309
+ },
310
+ };
311
+ }
312
+ if (code === 'ENAMETOOLONG') {
313
+ return {
314
+ content: [{ type: 'text', text: `Path exceeds system limit: ${filePath}` }],
315
+ details: {
316
+ filePath,
317
+ isCreate: !fileExists,
318
+ bytesWritten: 0,
319
+ diff: null,
320
+ originalContent,
321
+ },
322
+ };
323
+ }
324
+ throw err;
325
+ }
326
+
327
+ // Refresh read state: the agent's own write is authoritative knowledge
328
+ // of current file contents, so subsequent mutations don't require a re-read.
329
+ // We record the new mtime and a content hash of what we just wrote so
330
+ // external modifications still trigger the freshness check above.
331
+ // Also capture an EditHistory snapshot (when enabled) so UndoEdit
332
+ // can restore the prior state — including the "file didn't exist"
333
+ // case, where undo means deleting what Write just created.
334
+ try {
335
+ const postStat = await fs.promises.stat(filePath);
336
+ const postHash = crypto.createHash('sha256')
337
+ .update(newContent, 'utf8').digest('hex');
338
+ readRegistry.markRead(filePath, {
339
+ timestamp: postStat.mtimeMs,
340
+ contentHash: postHash,
341
+ });
342
+ editHistory?.record(filePath, {
343
+ originalContent,
344
+ postMutationMtimeMs: postStat.mtimeMs,
345
+ postMutationContentHash: postHash,
346
+ source: 'Write',
347
+ });
348
+ } catch {
349
+ readRegistry.invalidate(filePath);
350
+ }
351
+
352
+ const bytesWritten = Buffer.byteLength(newContent, 'utf8');
353
+ const isCreate = !fileExists;
354
+
355
+ // Compute diff for updates
356
+ const diff = originalContent !== null ? computeDiff(originalContent, newContent) : null;
357
+
358
+ const verb = isCreate ? 'Created' : 'Updated';
359
+ return {
360
+ content: [{ type: 'text', text: `${verb} ${filePath} (${bytesWritten} bytes)` }],
361
+ details: {
362
+ filePath,
363
+ isCreate,
364
+ bytesWritten,
365
+ diff,
366
+ originalContent,
367
+ },
368
+ };
369
+ } finally {
370
+ release?.();
371
+ }
372
+ },
373
+ };
374
+
375
+ return attachRuntimeAwareTool(tool, {
376
+ toolKind: 'Write',
377
+ cloneForRuntime: (runtime) => createWriteTool({
378
+ ...config,
379
+ runtime,
380
+ readRegistry: runtime.readRegistry,
381
+ fileMutationLock: runtime.fileMutationLock,
382
+ editHistory: runtime.editHistory,
383
+ }),
384
+ });
385
+ }