@namzu/sdk 0.5.0 → 1.0.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 (302) hide show
  1. package/CHANGELOG.md +393 -0
  2. package/dist/advisory/executor.d.ts.map +1 -1
  3. package/dist/advisory/executor.js +9 -2
  4. package/dist/advisory/executor.js.map +1 -1
  5. package/dist/advisory/executor.test.d.ts +2 -1
  6. package/dist/advisory/executor.test.d.ts.map +1 -1
  7. package/dist/advisory/executor.test.js +7 -4
  8. package/dist/advisory/executor.test.js.map +1 -1
  9. package/dist/agents/ReactiveAgent.d.ts.map +1 -1
  10. package/dist/agents/ReactiveAgent.js +2 -0
  11. package/dist/agents/ReactiveAgent.js.map +1 -1
  12. package/dist/agents/SupervisorAgent.d.ts.map +1 -1
  13. package/dist/agents/SupervisorAgent.js +13 -0
  14. package/dist/agents/SupervisorAgent.js.map +1 -1
  15. package/dist/bridge/sse/mapper.test.js +2 -2
  16. package/dist/constants/compaction/index.d.ts.map +1 -1
  17. package/dist/constants/compaction/index.js +8 -3
  18. package/dist/constants/compaction/index.js.map +1 -1
  19. package/dist/constants/sandbox/index.d.ts +21 -0
  20. package/dist/constants/sandbox/index.d.ts.map +1 -1
  21. package/dist/constants/sandbox/index.js +30 -0
  22. package/dist/constants/sandbox/index.js.map +1 -1
  23. package/dist/constants/tools/index.d.ts.map +1 -1
  24. package/dist/constants/tools/index.js +33 -2
  25. package/dist/constants/tools/index.js.map +1 -1
  26. package/dist/manager/run/persistence.d.ts.map +1 -1
  27. package/dist/manager/run/persistence.js +35 -5
  28. package/dist/manager/run/persistence.js.map +1 -1
  29. package/dist/persona/assembler.d.ts +1 -0
  30. package/dist/persona/assembler.d.ts.map +1 -1
  31. package/dist/persona/assembler.js +28 -6
  32. package/dist/persona/assembler.js.map +1 -1
  33. package/dist/provider/collect.test.js +2 -2
  34. package/dist/public-runtime.d.ts +5 -4
  35. package/dist/public-runtime.d.ts.map +1 -1
  36. package/dist/public-runtime.js +5 -4
  37. package/dist/public-runtime.js.map +1 -1
  38. package/dist/public-tools.d.ts +2 -0
  39. package/dist/public-tools.d.ts.map +1 -1
  40. package/dist/public-tools.js +2 -0
  41. package/dist/public-tools.js.map +1 -1
  42. package/dist/public-types.d.ts +3 -0
  43. package/dist/public-types.d.ts.map +1 -1
  44. package/dist/registry/index.d.ts +2 -0
  45. package/dist/registry/index.d.ts.map +1 -1
  46. package/dist/registry/index.js +1 -0
  47. package/dist/registry/index.js.map +1 -1
  48. package/dist/registry/tool/execute.d.ts.map +1 -1
  49. package/dist/registry/tool/execute.js +87 -5
  50. package/dist/registry/tool/execute.js.map +1 -1
  51. package/dist/registry/tool/execute.test.d.ts +4 -2
  52. package/dist/registry/tool/execute.test.d.ts.map +1 -1
  53. package/dist/registry/tool/execute.test.js +112 -3
  54. package/dist/registry/tool/execute.test.js.map +1 -1
  55. package/dist/registry/toolset/catalog.d.ts +42 -0
  56. package/dist/registry/toolset/catalog.d.ts.map +1 -0
  57. package/dist/registry/toolset/catalog.js +217 -0
  58. package/dist/registry/toolset/catalog.js.map +1 -0
  59. package/dist/registry/toolset/catalog.test.d.ts +2 -0
  60. package/dist/registry/toolset/catalog.test.d.ts.map +1 -0
  61. package/dist/registry/toolset/catalog.test.js +85 -0
  62. package/dist/registry/toolset/catalog.test.js.map +1 -0
  63. package/dist/runtime/query/__tests__/deferred-tools.test.d.ts +2 -0
  64. package/dist/runtime/query/__tests__/deferred-tools.test.d.ts.map +1 -0
  65. package/dist/runtime/query/__tests__/deferred-tools.test.js +147 -0
  66. package/dist/runtime/query/__tests__/deferred-tools.test.js.map +1 -0
  67. package/dist/runtime/query/__tests__/executor-concurrency.test.d.ts +2 -0
  68. package/dist/runtime/query/__tests__/executor-concurrency.test.d.ts.map +1 -0
  69. package/dist/runtime/query/__tests__/executor-concurrency.test.js +98 -0
  70. package/dist/runtime/query/__tests__/executor-concurrency.test.js.map +1 -0
  71. package/dist/runtime/query/__tests__/executor-plugin-hooks.test.js +38 -3
  72. package/dist/runtime/query/__tests__/executor-plugin-hooks.test.js.map +1 -1
  73. package/dist/runtime/query/__tests__/prompt.test.js +47 -2
  74. package/dist/runtime/query/__tests__/prompt.test.js.map +1 -1
  75. package/dist/runtime/query/__tests__/stream-recovery.test.d.ts +2 -0
  76. package/dist/runtime/query/__tests__/stream-recovery.test.d.ts.map +1 -0
  77. package/dist/runtime/query/__tests__/stream-recovery.test.js +126 -0
  78. package/dist/runtime/query/__tests__/stream-recovery.test.js.map +1 -0
  79. package/dist/runtime/query/continuation.d.ts +16 -0
  80. package/dist/runtime/query/continuation.d.ts.map +1 -0
  81. package/dist/runtime/query/continuation.js +16 -0
  82. package/dist/runtime/query/continuation.js.map +1 -0
  83. package/dist/runtime/query/executor.d.ts +3 -0
  84. package/dist/runtime/query/executor.d.ts.map +1 -1
  85. package/dist/runtime/query/executor.js +71 -3
  86. package/dist/runtime/query/executor.js.map +1 -1
  87. package/dist/runtime/query/index.d.ts.map +1 -1
  88. package/dist/runtime/query/index.js +19 -3
  89. package/dist/runtime/query/index.js.map +1 -1
  90. package/dist/runtime/query/iteration/index.d.ts +22 -0
  91. package/dist/runtime/query/iteration/index.d.ts.map +1 -1
  92. package/dist/runtime/query/iteration/index.js +227 -60
  93. package/dist/runtime/query/iteration/index.js.map +1 -1
  94. package/dist/runtime/query/iteration/phases/context.d.ts +10 -0
  95. package/dist/runtime/query/iteration/phases/context.d.ts.map +1 -1
  96. package/dist/runtime/query/iteration/phases/context.js.map +1 -1
  97. package/dist/runtime/query/prompt.d.ts.map +1 -1
  98. package/dist/runtime/query/prompt.js +21 -1
  99. package/dist/runtime/query/prompt.js.map +1 -1
  100. package/dist/runtime/query/tooling.d.ts +1 -0
  101. package/dist/runtime/query/tooling.d.ts.map +1 -1
  102. package/dist/runtime/query/tooling.js +1 -0
  103. package/dist/runtime/query/tooling.js.map +1 -1
  104. package/dist/sandbox/provider/local.d.ts.map +1 -1
  105. package/dist/sandbox/provider/local.js +32 -1
  106. package/dist/sandbox/provider/local.js.map +1 -1
  107. package/dist/session/workspace/__tests__/shared-run.test.d.ts +2 -0
  108. package/dist/session/workspace/__tests__/shared-run.test.d.ts.map +1 -0
  109. package/dist/session/workspace/__tests__/shared-run.test.js +147 -0
  110. package/dist/session/workspace/__tests__/shared-run.test.js.map +1 -0
  111. package/dist/session/workspace/index.d.ts +2 -0
  112. package/dist/session/workspace/index.d.ts.map +1 -1
  113. package/dist/session/workspace/index.js +1 -0
  114. package/dist/session/workspace/index.js.map +1 -1
  115. package/dist/session/workspace/shared-run.d.ts +81 -0
  116. package/dist/session/workspace/shared-run.d.ts.map +1 -0
  117. package/dist/session/workspace/shared-run.js +251 -0
  118. package/dist/session/workspace/shared-run.js.map +1 -0
  119. package/dist/skills/loader.d.ts.map +1 -1
  120. package/dist/skills/loader.js +36 -6
  121. package/dist/skills/loader.js.map +1 -1
  122. package/dist/skills/loader.test.d.ts +2 -0
  123. package/dist/skills/loader.test.d.ts.map +1 -0
  124. package/dist/skills/loader.test.js +65 -0
  125. package/dist/skills/loader.test.js.map +1 -0
  126. package/dist/streaming/coalesce.test.js +1 -1
  127. package/dist/tools/builtins/__tests__/edit.test.d.ts +2 -0
  128. package/dist/tools/builtins/__tests__/edit.test.d.ts.map +1 -0
  129. package/dist/tools/builtins/__tests__/edit.test.js +38 -0
  130. package/dist/tools/builtins/__tests__/edit.test.js.map +1 -0
  131. package/dist/tools/builtins/__tests__/payload-budget.test.d.ts +2 -0
  132. package/dist/tools/builtins/__tests__/payload-budget.test.d.ts.map +1 -0
  133. package/dist/tools/builtins/__tests__/payload-budget.test.js +22 -0
  134. package/dist/tools/builtins/__tests__/payload-budget.test.js.map +1 -0
  135. package/dist/tools/builtins/__tests__/read-file.test.d.ts +2 -0
  136. package/dist/tools/builtins/__tests__/read-file.test.d.ts.map +1 -0
  137. package/dist/tools/builtins/__tests__/read-file.test.js +24 -0
  138. package/dist/tools/builtins/__tests__/read-file.test.js.map +1 -0
  139. package/dist/tools/builtins/__tests__/verify-outputs.test.d.ts +2 -0
  140. package/dist/tools/builtins/__tests__/verify-outputs.test.d.ts.map +1 -0
  141. package/dist/tools/builtins/__tests__/verify-outputs.test.js +52 -0
  142. package/dist/tools/builtins/__tests__/verify-outputs.test.js.map +1 -0
  143. package/dist/tools/builtins/__tests__/write-file.test.d.ts +2 -0
  144. package/dist/tools/builtins/__tests__/write-file.test.d.ts.map +1 -0
  145. package/dist/tools/builtins/__tests__/write-file.test.js +74 -0
  146. package/dist/tools/builtins/__tests__/write-file.test.js.map +1 -0
  147. package/dist/tools/builtins/bash.d.ts.map +1 -1
  148. package/dist/tools/builtins/bash.js +40 -7
  149. package/dist/tools/builtins/bash.js.map +1 -1
  150. package/dist/tools/builtins/edit.d.ts +5 -2
  151. package/dist/tools/builtins/edit.d.ts.map +1 -1
  152. package/dist/tools/builtins/edit.js +114 -18
  153. package/dist/tools/builtins/edit.js.map +1 -1
  154. package/dist/tools/builtins/index.d.ts +1 -0
  155. package/dist/tools/builtins/index.d.ts.map +1 -1
  156. package/dist/tools/builtins/index.js +13 -13
  157. package/dist/tools/builtins/index.js.map +1 -1
  158. package/dist/tools/builtins/read-file.d.ts +1 -0
  159. package/dist/tools/builtins/read-file.d.ts.map +1 -1
  160. package/dist/tools/builtins/read-file.js +23 -8
  161. package/dist/tools/builtins/read-file.js.map +1 -1
  162. package/dist/tools/builtins/search-tools.d.ts.map +1 -1
  163. package/dist/tools/builtins/search-tools.js +4 -1
  164. package/dist/tools/builtins/search-tools.js.map +1 -1
  165. package/dist/tools/builtins/verify-outputs.d.ts +5 -0
  166. package/dist/tools/builtins/verify-outputs.d.ts.map +1 -0
  167. package/dist/tools/builtins/verify-outputs.js +103 -0
  168. package/dist/tools/builtins/verify-outputs.js.map +1 -0
  169. package/dist/tools/builtins/write-file.d.ts +3 -2
  170. package/dist/tools/builtins/write-file.d.ts.map +1 -1
  171. package/dist/tools/builtins/write-file.js +72 -12
  172. package/dist/tools/builtins/write-file.js.map +1 -1
  173. package/dist/tools/coordinator/__tests__/agent.test.d.ts +15 -0
  174. package/dist/tools/coordinator/__tests__/agent.test.d.ts.map +1 -0
  175. package/dist/tools/coordinator/__tests__/agent.test.js +142 -0
  176. package/dist/tools/coordinator/__tests__/agent.test.js.map +1 -0
  177. package/dist/tools/coordinator/__tests__/task-list.test.d.ts +13 -0
  178. package/dist/tools/coordinator/__tests__/task-list.test.d.ts.map +1 -0
  179. package/dist/tools/coordinator/__tests__/task-list.test.js +162 -0
  180. package/dist/tools/coordinator/__tests__/task-list.test.js.map +1 -0
  181. package/dist/tools/coordinator/agent.d.ts +34 -0
  182. package/dist/tools/coordinator/agent.d.ts.map +1 -0
  183. package/dist/tools/coordinator/agent.js +107 -0
  184. package/dist/tools/coordinator/agent.js.map +1 -0
  185. package/dist/tools/coordinator/index.d.ts +7 -0
  186. package/dist/tools/coordinator/index.d.ts.map +1 -1
  187. package/dist/tools/coordinator/index.js +111 -21
  188. package/dist/tools/coordinator/index.js.map +1 -1
  189. package/dist/types/agent/base.d.ts +8 -0
  190. package/dist/types/agent/base.d.ts.map +1 -1
  191. package/dist/types/agent/reactive.d.ts +23 -0
  192. package/dist/types/agent/reactive.d.ts.map +1 -1
  193. package/dist/types/agent/supervisor.d.ts +41 -0
  194. package/dist/types/agent/supervisor.d.ts.map +1 -1
  195. package/dist/types/message/index.d.ts +22 -1
  196. package/dist/types/message/index.d.ts.map +1 -1
  197. package/dist/types/message/index.js +7 -2
  198. package/dist/types/message/index.js.map +1 -1
  199. package/dist/types/provider/chat.d.ts +2 -9
  200. package/dist/types/provider/chat.d.ts.map +1 -1
  201. package/dist/types/run/events.d.ts +6 -0
  202. package/dist/types/run/events.d.ts.map +1 -1
  203. package/dist/types/run/events.js.map +1 -1
  204. package/dist/types/sandbox/index.d.ts +193 -0
  205. package/dist/types/sandbox/index.d.ts.map +1 -1
  206. package/dist/types/sandbox/index.js.map +1 -1
  207. package/dist/types/skills/index.d.ts +2 -0
  208. package/dist/types/skills/index.d.ts.map +1 -1
  209. package/dist/types/tool/index.d.ts +22 -0
  210. package/dist/types/tool/index.d.ts.map +1 -1
  211. package/dist/types/toolset/index.d.ts +71 -0
  212. package/dist/types/toolset/index.d.ts.map +1 -0
  213. package/dist/types/toolset/index.js +2 -0
  214. package/dist/types/toolset/index.js.map +1 -0
  215. package/dist/types/workspace/index.d.ts +1 -0
  216. package/dist/types/workspace/index.d.ts.map +1 -1
  217. package/dist/types/workspace/shared-run.d.ts +61 -0
  218. package/dist/types/workspace/shared-run.d.ts.map +1 -0
  219. package/dist/types/workspace/shared-run.js +2 -0
  220. package/dist/types/workspace/shared-run.js.map +1 -0
  221. package/dist/verification/index.d.ts +1 -0
  222. package/dist/verification/index.d.ts.map +1 -1
  223. package/dist/verification/index.js +1 -0
  224. package/dist/verification/index.js.map +1 -1
  225. package/dist/verification/presets.d.ts +53 -0
  226. package/dist/verification/presets.d.ts.map +1 -0
  227. package/dist/verification/presets.js +70 -0
  228. package/dist/verification/presets.js.map +1 -0
  229. package/dist/verification/presets.test.d.ts +16 -0
  230. package/dist/verification/presets.test.d.ts.map +1 -0
  231. package/dist/verification/presets.test.js +79 -0
  232. package/dist/verification/presets.test.js.map +1 -0
  233. package/package.json +3 -2
  234. package/src/advisory/executor.test.ts +7 -4
  235. package/src/advisory/executor.ts +11 -2
  236. package/src/agents/ReactiveAgent.ts +2 -0
  237. package/src/agents/SupervisorAgent.ts +13 -0
  238. package/src/bridge/sse/mapper.test.ts +2 -2
  239. package/src/constants/compaction/index.ts +8 -3
  240. package/src/constants/sandbox/index.ts +37 -0
  241. package/src/constants/tools/index.ts +33 -2
  242. package/src/manager/run/persistence.ts +34 -6
  243. package/src/persona/assembler.ts +31 -8
  244. package/src/provider/collect.test.ts +2 -2
  245. package/src/public-runtime.ts +14 -1
  246. package/src/public-tools.ts +2 -0
  247. package/src/public-types.ts +7 -0
  248. package/src/registry/index.ts +7 -0
  249. package/src/registry/tool/execute.test.ts +132 -3
  250. package/src/registry/tool/execute.ts +94 -9
  251. package/src/registry/toolset/catalog.test.ts +97 -0
  252. package/src/registry/toolset/catalog.ts +283 -0
  253. package/src/runtime/query/__tests__/deferred-tools.test.ts +183 -0
  254. package/src/runtime/query/__tests__/executor-concurrency.test.ts +122 -0
  255. package/src/runtime/query/__tests__/executor-plugin-hooks.test.ts +48 -3
  256. package/src/runtime/query/__tests__/prompt.test.ts +51 -2
  257. package/src/runtime/query/__tests__/stream-recovery.test.ts +156 -0
  258. package/src/runtime/query/continuation.ts +16 -0
  259. package/src/runtime/query/executor.ts +82 -13
  260. package/src/runtime/query/index.ts +24 -3
  261. package/src/runtime/query/iteration/index.ts +263 -68
  262. package/src/runtime/query/iteration/phases/context.ts +10 -0
  263. package/src/runtime/query/prompt.ts +17 -1
  264. package/src/runtime/query/tooling.ts +2 -0
  265. package/src/sandbox/provider/local.ts +33 -0
  266. package/src/session/workspace/__tests__/shared-run.test.ts +181 -0
  267. package/src/session/workspace/index.ts +6 -0
  268. package/src/session/workspace/shared-run.ts +316 -0
  269. package/src/skills/loader.test.ts +89 -0
  270. package/src/skills/loader.ts +37 -6
  271. package/src/streaming/coalesce.test.ts +1 -1
  272. package/src/tools/builtins/__tests__/edit.test.ts +57 -0
  273. package/src/tools/builtins/__tests__/payload-budget.test.ts +29 -0
  274. package/src/tools/builtins/__tests__/read-file.test.ts +31 -0
  275. package/src/tools/builtins/__tests__/verify-outputs.test.ts +71 -0
  276. package/src/tools/builtins/__tests__/write-file.test.ts +97 -0
  277. package/src/tools/builtins/bash.ts +48 -7
  278. package/src/tools/builtins/edit.ts +162 -27
  279. package/src/tools/builtins/index.ts +13 -13
  280. package/src/tools/builtins/read-file.ts +31 -8
  281. package/src/tools/builtins/search-tools.ts +5 -1
  282. package/src/tools/builtins/verify-outputs.ts +126 -0
  283. package/src/tools/builtins/write-file.ts +83 -14
  284. package/src/tools/coordinator/__tests__/agent.test.ts +172 -0
  285. package/src/tools/coordinator/__tests__/task-list.test.ts +182 -0
  286. package/src/tools/coordinator/agent.ts +157 -0
  287. package/src/tools/coordinator/index.ts +128 -22
  288. package/src/types/agent/base.ts +8 -0
  289. package/src/types/agent/reactive.ts +25 -0
  290. package/src/types/agent/supervisor.ts +45 -0
  291. package/src/types/message/index.ts +32 -2
  292. package/src/types/provider/chat.ts +2 -9
  293. package/src/types/run/events.ts +6 -0
  294. package/src/types/sandbox/index.ts +219 -0
  295. package/src/types/skills/index.ts +4 -0
  296. package/src/types/tool/index.ts +24 -0
  297. package/src/types/toolset/index.ts +86 -0
  298. package/src/types/workspace/index.ts +9 -0
  299. package/src/types/workspace/shared-run.ts +65 -0
  300. package/src/verification/index.ts +1 -0
  301. package/src/verification/presets.test.ts +112 -0
  302. package/src/verification/presets.ts +72 -0
@@ -0,0 +1,57 @@
1
+ import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { describe, expect, it } from 'vitest'
5
+ import type { ToolContext } from '../../../types/tool/index.js'
6
+ import { EditTool } from '../edit.js'
7
+
8
+ function makeContext(workingDirectory: string): ToolContext {
9
+ return {
10
+ runId: 'run_test' as ToolContext['runId'],
11
+ workingDirectory,
12
+ abortSignal: new AbortController().signal,
13
+ env: {},
14
+ log: () => {},
15
+ }
16
+ }
17
+
18
+ describe('EditTool', () => {
19
+ it('accepts oldStr/newStr aliases for string replacement', async () => {
20
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
21
+ writeFileSync(join(dir, 'doc.md'), 'alpha\nbeta\n')
22
+
23
+ const result = await EditTool.execute(
24
+ { path: 'doc.md', oldStr: 'beta', newStr: 'gamma', replace_all: false },
25
+ makeContext(dir),
26
+ )
27
+
28
+ expect(result.success).toBe(true)
29
+ expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('alpha\ngamma\n')
30
+ })
31
+
32
+ it('inserts content after a 1-indexed line number', async () => {
33
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
34
+ writeFileSync(join(dir, 'doc.md'), 'alpha\nbeta\n')
35
+
36
+ const result = await EditTool.execute(
37
+ { path: 'doc.md', insertLine: 1, newStr: 'inserted', replace_all: false },
38
+ makeContext(dir),
39
+ )
40
+
41
+ expect(result.success).toBe(true)
42
+ expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('alpha\ninserted\nbeta\n')
43
+ })
44
+
45
+ it('inserts content at the end with insertLine=end', async () => {
46
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-edit-'))
47
+ writeFileSync(join(dir, 'doc.md'), 'alpha\n')
48
+
49
+ const result = await EditTool.execute(
50
+ { path: 'doc.md', insertLine: 'end', newStr: 'omega', replace_all: false },
51
+ makeContext(dir),
52
+ )
53
+
54
+ expect(result.success).toBe(true)
55
+ expect(readFileSync(join(dir, 'doc.md'), 'utf-8')).toBe('alpha\nomega\n')
56
+ })
57
+ })
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { EditTool } from '../edit.js'
3
+ import { getBuiltinTools } from '../index.js'
4
+ import { WriteFileTool } from '../write-file.js'
5
+
6
+ describe('filesystem tool payload budgeting', () => {
7
+ it('does not hard-fail oversized write/edit payloads at schema validation', () => {
8
+ const oversized = 'x'.repeat(12_500)
9
+
10
+ expect(() =>
11
+ WriteFileTool.inputSchema.parse({ path: 'outputs/long.md', content: oversized }),
12
+ ).not.toThrow()
13
+ expect(() =>
14
+ EditTool.inputSchema.parse({
15
+ path: 'outputs/long.md',
16
+ oldStr: '{{SECTION}}',
17
+ newStr: oversized,
18
+ replace_all: false,
19
+ }),
20
+ ).not.toThrow()
21
+ })
22
+
23
+ it('keeps append out of the default builtin toolset', () => {
24
+ const names = getBuiltinTools().map((tool) => tool.name)
25
+
26
+ expect(names).toContain('edit')
27
+ expect(names).not.toContain('append')
28
+ })
29
+ })
@@ -0,0 +1,31 @@
1
+ import { mkdtempSync, writeFileSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { describe, expect, it } from 'vitest'
5
+ import type { ToolContext } from '../../../types/tool/index.js'
6
+ import { ReadFileTool } from '../read-file.js'
7
+
8
+ function makeContext(workingDirectory: string): ToolContext {
9
+ return {
10
+ runId: 'run_test' as ToolContext['runId'],
11
+ workingDirectory,
12
+ abortSignal: new AbortController().signal,
13
+ env: {},
14
+ log: () => {},
15
+ }
16
+ }
17
+
18
+ describe('ReadFileTool', () => {
19
+ it('accepts readRange as a 1-indexed inclusive line range', async () => {
20
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-read-'))
21
+ writeFileSync(join(dir, 'doc.md'), ['one', 'two', 'three', 'four'].join('\n'))
22
+
23
+ const result = await ReadFileTool.execute(
24
+ { path: 'doc.md', readRange: [2, 3] },
25
+ makeContext(dir),
26
+ )
27
+
28
+ expect(result.success).toBe(true)
29
+ expect(result.output).toBe('2\ttwo\n3\tthree')
30
+ })
31
+ })
@@ -0,0 +1,71 @@
1
+ import { mkdtempSync, writeFileSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { describe, expect, it } from 'vitest'
5
+ import type { ToolContext } from '../../../types/tool/index.js'
6
+ import { VerifyOutputsTool } from '../verify-outputs.js'
7
+
8
+ function makeContext(workingDirectory: string): ToolContext {
9
+ return {
10
+ runId: 'run_test' as ToolContext['runId'],
11
+ workingDirectory,
12
+ abortSignal: new AbortController().signal,
13
+ env: {},
14
+ log: () => {},
15
+ }
16
+ }
17
+
18
+ describe('VerifyOutputsTool', () => {
19
+ it('reports OK for every existing non-empty file', async () => {
20
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-verify-'))
21
+ writeFileSync(join(dir, 'a.md'), 'hello world')
22
+ writeFileSync(join(dir, 'b.txt'), 'x'.repeat(500))
23
+
24
+ const result = await VerifyOutputsTool.execute({ paths: ['a.md', 'b.txt'] }, makeContext(dir))
25
+
26
+ expect(result.success).toBe(true)
27
+ expect(result.output).toMatch(/2\/2 passed/)
28
+ expect(result.output).toMatch(/OK {3}a\.md/)
29
+ expect(result.output).toMatch(/OK {3}b\.txt/)
30
+ })
31
+
32
+ it('flags missing files as FAIL with summary', async () => {
33
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-verify-'))
34
+ writeFileSync(join(dir, 'present.md'), 'data')
35
+
36
+ const result = await VerifyOutputsTool.execute(
37
+ { paths: ['present.md', 'missing.md', 'also-missing.md'] },
38
+ makeContext(dir),
39
+ )
40
+
41
+ expect(result.success).toBe(false)
42
+ expect(result.error).toMatch(/2 of 3 expected outputs failed/)
43
+ expect(result.output).toMatch(/1\/3 passed/)
44
+ expect(result.output).toMatch(/FAIL missing\.md — missing/)
45
+ })
46
+
47
+ it('treats files smaller than min_bytes as FAIL', async () => {
48
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-verify-'))
49
+ writeFileSync(join(dir, 'tiny.md'), 'x') // 1 byte
50
+ writeFileSync(join(dir, 'big.md'), 'y'.repeat(2000))
51
+
52
+ const result = await VerifyOutputsTool.execute(
53
+ { paths: ['tiny.md', 'big.md'], min_bytes: 1000 },
54
+ makeContext(dir),
55
+ )
56
+
57
+ expect(result.success).toBe(false)
58
+ expect(result.output).toMatch(/FAIL tiny\.md — size 1B < min 1000B/)
59
+ expect(result.output).toMatch(/OK {3}big\.md/)
60
+ })
61
+
62
+ it('rejects directories that match a path', async () => {
63
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-verify-'))
64
+ const result = await VerifyOutputsTool.execute(
65
+ { paths: ['.'] }, // working directory itself
66
+ makeContext(dir),
67
+ )
68
+ expect(result.success).toBe(false)
69
+ expect(result.output).toMatch(/FAIL \. — not a regular file/)
70
+ })
71
+ })
@@ -0,0 +1,97 @@
1
+ import { mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { tmpdir } from 'node:os'
3
+ import { join } from 'node:path'
4
+ import { describe, expect, it } from 'vitest'
5
+ import type { FileReadTracker, ToolContext } from '../../../types/tool/index.js'
6
+ import { WriteFileTool } from '../write-file.js'
7
+
8
+ function makeTracker(): FileReadTracker & { keys(): string[] } {
9
+ const set = new Set<string>()
10
+ return {
11
+ recordRead: (k) => {
12
+ set.add(k)
13
+ },
14
+ hasRead: (k) => set.has(k),
15
+ keys: () => Array.from(set),
16
+ }
17
+ }
18
+
19
+ function makeContext(workingDirectory: string, tracker?: FileReadTracker): ToolContext {
20
+ return {
21
+ runId: 'run_test' as ToolContext['runId'],
22
+ workingDirectory,
23
+ abortSignal: new AbortController().signal,
24
+ env: {},
25
+ log: () => {},
26
+ fileReadTracker: tracker,
27
+ }
28
+ }
29
+
30
+ describe('WriteFileTool — read-before-overwrite invariant', () => {
31
+ it('writes a new file without requiring a prior read', async () => {
32
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-write-'))
33
+ const tracker = makeTracker()
34
+ const ctx = makeContext(dir, tracker)
35
+
36
+ const result = await WriteFileTool.execute({ path: 'fresh.txt', content: 'hello' }, ctx)
37
+
38
+ expect(result.success).toBe(true)
39
+ expect(readFileSync(join(dir, 'fresh.txt'), 'utf-8')).toBe('hello')
40
+ expect(tracker.hasRead(join(dir, 'fresh.txt'))).toBe(true)
41
+ })
42
+
43
+ it('accepts newStr as the canonical create/write content alias', async () => {
44
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-write-'))
45
+ const tracker = makeTracker()
46
+ const ctx = makeContext(dir, tracker)
47
+
48
+ const result = await WriteFileTool.execute({ path: 'fresh.txt', newStr: 'hello' }, ctx)
49
+
50
+ expect(result.success).toBe(true)
51
+ expect(readFileSync(join(dir, 'fresh.txt'), 'utf-8')).toBe('hello')
52
+ })
53
+
54
+ it('refuses to overwrite an existing file the agent has not read', async () => {
55
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-write-'))
56
+ writeFileSync(join(dir, 'pre-existing.txt'), 'original')
57
+ const tracker = makeTracker()
58
+ const ctx = makeContext(dir, tracker)
59
+
60
+ const result = await WriteFileTool.execute(
61
+ { path: 'pre-existing.txt', content: 'replaced' },
62
+ ctx,
63
+ )
64
+
65
+ expect(result.success).toBe(false)
66
+ expect(result.error).toMatch(/already exists.*read/i)
67
+ expect(readFileSync(join(dir, 'pre-existing.txt'), 'utf-8')).toBe('original')
68
+ })
69
+
70
+ it('allows overwrite once the file has been read in the same context', async () => {
71
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-write-'))
72
+ const filePath = join(dir, 'pre-existing.txt')
73
+ writeFileSync(filePath, 'original')
74
+ const tracker = makeTracker()
75
+ tracker.recordRead(filePath)
76
+ const ctx = makeContext(dir, tracker)
77
+
78
+ const result = await WriteFileTool.execute(
79
+ { path: 'pre-existing.txt', content: 'replaced' },
80
+ ctx,
81
+ )
82
+
83
+ expect(result.success).toBe(true)
84
+ expect(readFileSync(filePath, 'utf-8')).toBe('replaced')
85
+ })
86
+
87
+ it('falls back to legacy behaviour when no fileReadTracker is provided (back-compat)', async () => {
88
+ const dir = mkdtempSync(join(tmpdir(), 'namzu-write-'))
89
+ writeFileSync(join(dir, 'legacy.txt'), 'before')
90
+ const ctx = makeContext(dir)
91
+
92
+ const result = await WriteFileTool.execute({ path: 'legacy.txt', content: 'after' }, ctx)
93
+
94
+ expect(result.success).toBe(true)
95
+ expect(readFileSync(join(dir, 'legacy.txt'), 'utf-8')).toBe('after')
96
+ })
97
+ })
@@ -5,12 +5,31 @@ import { DANGEROUS_PATTERNS } from '../../constants/tools/index.js'
5
5
  import { defineTool } from '../defineTool.js'
6
6
 
7
7
  const execAsync = promisify(exec)
8
+ // Namzu owns its own bash timeout knob — `NAMZU_BASH_TIMEOUT_MS`.
9
+ // The Vandal fallback (`VANDAL_NAMZU_TIMEOUT_MS`) lived here as a
10
+ // historical bridge while Namzu was carved out of the Vandal repo,
11
+ // but Namzu shouldn't read a consumer's env name. Consumers can
12
+ // still alias their own var to `NAMZU_BASH_TIMEOUT_MS` at deploy
13
+ // time if they want a unified knob.
14
+ const DEFAULT_BASH_TIMEOUT_MS = readPositiveIntEnv('NAMZU_BASH_TIMEOUT_MS', 60 * 60 * 1000)
15
+ const DEFAULT_BASH_MAX_BUFFER_BYTES = readPositiveIntEnv(
16
+ 'NAMZU_BASH_MAX_BUFFER_BYTES',
17
+ 100 * 1024 * 1024,
18
+ )
8
19
 
9
20
  const inputSchema = z.object({
10
- command: z.string().describe('The bash command to execute'),
21
+ command: z
22
+ .string()
23
+ .min(1)
24
+ .describe(
25
+ 'The bash command to execute. Required, non-empty. Single command per call (use `&&` / `;` chaining for compound commands). Avoid heredocs that span more than a few hundred bytes — large content should be created with `write`, then extended with `edit` insertLine: "end", not piped into bash.',
26
+ ),
11
27
  timeout: z
12
- .preprocess((v) => (typeof v === 'string' ? Number(v) : v), z.number().default(30_000))
13
- .describe('Command timeout in milliseconds. Default: 30000'),
28
+ .preprocess(
29
+ (v) => (typeof v === 'string' ? Number(v) : v),
30
+ z.number().default(DEFAULT_BASH_TIMEOUT_MS),
31
+ )
32
+ .describe(`Command timeout in milliseconds. Default: ${DEFAULT_BASH_TIMEOUT_MS}`),
14
33
  })
15
34
 
16
35
  type BashInput = z.infer<typeof inputSchema>
@@ -22,7 +41,7 @@ function isDangerousCommand(command: string): boolean {
22
41
  export const BashTool = defineTool({
23
42
  name: 'bash',
24
43
  description:
25
- 'Executes a bash command and returns stdout/stderr output. Command timeout is configurable.',
44
+ 'Executes a bash command and returns stdout/stderr output. Command timeout is configurable. The `command` parameter is required — never call this tool with empty arguments. For very long content (e.g. building a large file), prefer `write` for the opening and `edit` with insertLine: "end" for follow-up chunks over a heredoc to avoid hitting the output token limit mid-stream.',
26
45
  inputSchema,
27
46
  category: 'shell',
28
47
  permissions: ['shell_execute'],
@@ -39,11 +58,26 @@ export const BashTool = defineTool({
39
58
  }
40
59
  }
41
60
 
42
- // Sandbox-aware: route through sandbox.exec() when available
61
+ // Sandbox-aware: route through sandbox.exec() when available.
62
+ //
63
+ // `context.workingDirectory` is the HOST-side workspace path the
64
+ // SDK consumer chose for the run (Vandal: `/var/lib/vandal/sessions/<task>`),
65
+ // which is meaningless inside the sandbox container. Forwarding
66
+ // it as `cwd` would either land on a path that doesn't exist
67
+ // (and the worker would `mkdir -p` it inside the container,
68
+ // silently divorcing the model's filesystem view from where its
69
+ // deliverables actually need to land) or, in the case of the
70
+ // `container:docker` worker, fail the workspace-confinement
71
+ // guard outright. The right behaviour is to let the worker
72
+ // fall through to its own default (`NAMZU_SANDBOX_WORKSPACE`
73
+ // → the per-task mount root the host configured at provider
74
+ // construction time). Tools that need a sub-cwd inside the
75
+ // sandbox can be added later as an explicit
76
+ // `SandboxExecOptions.workspaceRelativeCwd` field; the bash
77
+ // builtin doesn't have that requirement today.
43
78
  if (context.sandbox) {
44
79
  const result = await context.sandbox.exec('/bin/sh', ['-c', input.command], {
45
80
  timeout: input.timeout,
46
- cwd: context.workingDirectory,
47
81
  env: context.env,
48
82
  })
49
83
 
@@ -74,7 +108,7 @@ export const BashTool = defineTool({
74
108
  cwd: context.workingDirectory,
75
109
  timeout: input.timeout,
76
110
  env: { ...process.env, ...context.env },
77
- maxBuffer: 1024 * 1024 * 10,
111
+ maxBuffer: DEFAULT_BASH_MAX_BUFFER_BYTES,
78
112
  })
79
113
 
80
114
  const output = [stdout ? `STDOUT:\n${stdout}` : '', stderr ? `STDERR:\n${stderr}` : '']
@@ -88,3 +122,10 @@ export const BashTool = defineTool({
88
122
  }
89
123
  },
90
124
  })
125
+
126
+ function readPositiveIntEnv(key: string, fallback: number): number {
127
+ const value = process.env[key]?.trim()
128
+ if (!value) return fallback
129
+ const parsed = Number(value)
130
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback
131
+ }
@@ -3,24 +3,73 @@ import { resolve } from 'node:path'
3
3
  import { z } from 'zod'
4
4
  import { defineTool } from '../defineTool.js'
5
5
 
6
- const inputSchema = z.object({
7
- path: z.string().describe('Path to the file to edit'),
8
- old_string: z
9
- .string()
10
- .describe('The exact string to find and replace. Must be unique in the file.'),
11
- new_string: z.string().describe('The replacement string'),
12
- replace_all: z
13
- .boolean()
14
- .default(false)
15
- .describe('Replace all occurrences instead of just the first unique match'),
16
- })
6
+ const inputSchema = z
7
+ .object({
8
+ path: z.string().describe('Path to the file to edit'),
9
+ old_string: z
10
+ .string()
11
+ .optional()
12
+ .describe('The exact string to find and replace. Must be unique in the file.'),
13
+ oldStr: z
14
+ .string()
15
+ .optional()
16
+ .describe(
17
+ 'Alias for old_string. Used by hosts that expose text replacement as oldStr/newStr.',
18
+ ),
19
+ new_string: z
20
+ .string()
21
+ .optional()
22
+ .describe(
23
+ 'The replacement string. Self-budget this payload under 12000 characters before calling.',
24
+ ),
25
+ newStr: z
26
+ .string()
27
+ .optional()
28
+ .describe(
29
+ 'Alias for new_string. Also used as inserted content when insertLine is provided. Self-budget this payload under 12000 characters before calling.',
30
+ ),
31
+ insertLine: z
32
+ .union([z.coerce.number().int().min(0), z.string().min(1)])
33
+ .optional()
34
+ .describe(
35
+ 'Optional line insertion target. Inserts the replacement after this 1-indexed line; 0 inserts before the first line; "end" appends to the file.',
36
+ ),
37
+ replace_all: z
38
+ .boolean()
39
+ .default(false)
40
+ .describe('Replace all occurrences instead of just the first unique match'),
41
+ })
42
+ .refine((value) => typeof value.new_string === 'string' || typeof value.newStr === 'string', {
43
+ message: 'Either new_string or newStr is required.',
44
+ })
45
+ .refine(
46
+ (value) =>
47
+ value.insertLine !== undefined ||
48
+ typeof value.old_string === 'string' ||
49
+ typeof value.oldStr === 'string',
50
+ { message: 'Either old_string/oldStr or insertLine is required.' },
51
+ )
17
52
 
18
53
  type EditInput = z.infer<typeof inputSchema>
19
54
 
55
+ type NormalizedEditInput =
56
+ | {
57
+ operation: 'replace'
58
+ oldString: string
59
+ newString: string
60
+ replace_all: boolean
61
+ }
62
+ | {
63
+ operation: 'insert'
64
+ insertLine: number | 'end'
65
+ newString: string
66
+ replace_all: boolean
67
+ }
68
+
20
69
  export const EditTool = defineTool({
21
70
  name: 'edit',
22
71
  description:
23
- 'Makes targeted edits to a file using exact string find-and-replace. The old_string must be unique in the file unless replace_all is true. Preserves file formatting and indentation.',
72
+ 'Makes targeted edits to a file using exact string find-and-replace or line insertion. THIS IS THE PREFERRED WAY TO MODIFY AN EXISTING FILE — never reach for `write` to change a file that already exists, because `write` overwrites the whole body and discards earlier work on partial failure. `edit` keeps the rest of the file byte-for-byte intact and is recoverable: if a single edit fails (old_string/oldStr ambiguous, broader restructuring needed), follow up with another `edit` instead of re-emitting the entire file via `write`. The old_string/oldStr must be unique in the file unless replace_all is true. For insertions, pass insertLine plus new_string/newStr; use insertLine: "end" to extend a file at the end. Self-budget new_string/newStr under 12000 characters before emitting the tool call; use repeated bounded edits for long sections. Preserves file formatting and indentation.',
24
73
  inputSchema,
25
74
  category: 'filesystem',
26
75
  permissions: ['file_write'],
@@ -29,11 +78,18 @@ export const EditTool = defineTool({
29
78
  concurrencySafe: false,
30
79
 
31
80
  async execute(input: EditInput, context) {
32
- if (input.old_string === input.new_string) {
81
+ const normalized = normalizeEditInput(input)
82
+ if (!normalized.success) {
83
+ return { success: false, output: '', error: normalized.error }
84
+ }
85
+ if (
86
+ normalized.operation.operation === 'replace' &&
87
+ normalized.operation.oldString === normalized.operation.newString
88
+ ) {
33
89
  return {
34
90
  success: false,
35
91
  output: '',
36
- error: 'old_string and new_string are identical — no change needed',
92
+ error: 'old_string/oldStr and new_string/newStr are identical — no change needed',
37
93
  }
38
94
  }
39
95
 
@@ -42,7 +98,7 @@ export const EditTool = defineTool({
42
98
  const buffer = await context.sandbox.readFile(input.path)
43
99
  const content = buffer.toString('utf-8')
44
100
 
45
- const result = applyEdit(content, input)
101
+ const result = applyEdit(content, normalized.operation)
46
102
  if (!result.success) {
47
103
  return { success: false, output: '', error: result.error }
48
104
  }
@@ -58,7 +114,7 @@ export const EditTool = defineTool({
58
114
  const filePath = resolve(context.workingDirectory, input.path)
59
115
  const content = await readFile(filePath, 'utf-8')
60
116
 
61
- const result = applyEdit(content, input)
117
+ const result = applyEdit(content, normalized.operation)
62
118
  if (!result.success) {
63
119
  return { success: false, output: '', error: result.error }
64
120
  }
@@ -72,38 +128,94 @@ export const EditTool = defineTool({
72
128
  },
73
129
  })
74
130
 
131
+ function normalizeEditInput(
132
+ input: EditInput,
133
+ ): { success: true; operation: NormalizedEditInput } | { success: false; error: string } {
134
+ const newString = input.new_string ?? input.newStr
135
+ if (typeof newString !== 'string') {
136
+ return { success: false, error: 'Either new_string or newStr is required.' }
137
+ }
138
+
139
+ if (input.insertLine !== undefined) {
140
+ const insertLine = normalizeInsertLine(input.insertLine)
141
+ if (!insertLine.success) return insertLine
142
+ return {
143
+ success: true,
144
+ operation: {
145
+ operation: 'insert',
146
+ insertLine: insertLine.value,
147
+ newString,
148
+ replace_all: input.replace_all,
149
+ },
150
+ }
151
+ }
152
+
153
+ const oldString = input.old_string ?? input.oldStr
154
+ if (typeof oldString !== 'string') {
155
+ return { success: false, error: 'Either old_string/oldStr or insertLine is required.' }
156
+ }
157
+ return {
158
+ success: true,
159
+ operation: {
160
+ operation: 'replace',
161
+ oldString,
162
+ newString,
163
+ replace_all: input.replace_all,
164
+ },
165
+ }
166
+ }
167
+
168
+ function normalizeInsertLine(
169
+ value: string | number,
170
+ ): { success: true; value: number | 'end' } | { success: false; error: string } {
171
+ if (typeof value === 'string') {
172
+ if (value.trim().toLowerCase() === 'end') return { success: true, value: 'end' }
173
+ const parsed = Number(value)
174
+ if (Number.isInteger(parsed) && parsed >= 0) return { success: true, value: parsed }
175
+ return {
176
+ success: false,
177
+ error: 'insertLine must be a non-negative line number or "end".',
178
+ }
179
+ }
180
+ return { success: true, value }
181
+ }
182
+
75
183
  function applyEdit(
76
184
  content: string,
77
- input: EditInput,
185
+ input: NormalizedEditInput,
78
186
  ): { success: true; content: string; replacements: number } | { success: false; error: string } {
79
- if (!content.includes(input.old_string)) {
187
+ if (input.operation === 'insert') {
188
+ return applyLineInsert(content, input)
189
+ }
190
+
191
+ if (!content.includes(input.oldString)) {
80
192
  return {
81
193
  success: false,
82
194
  error:
83
- 'old_string not found in file. Make sure the string matches exactly, including whitespace and indentation.',
195
+ 'old_string/oldStr not found in file. Make sure the string matches exactly, including whitespace and indentation.',
84
196
  }
85
197
  }
86
198
 
87
199
  if (input.replace_all) {
88
- const parts = content.split(input.old_string)
200
+ const parts = content.split(input.oldString)
89
201
  const replacements = parts.length - 1
90
202
  return {
91
203
  success: true,
92
- content: parts.join(input.new_string),
204
+ content: parts.join(input.newString),
93
205
  replacements,
94
206
  }
95
207
  }
96
208
 
97
- // Uniqueness check: old_string must appear exactly once
98
- const firstIndex = content.indexOf(input.old_string)
99
- const secondIndex = content.indexOf(input.old_string, firstIndex + 1)
209
+ // Uniqueness check: old_string/oldStr must appear exactly once
210
+ const firstIndex = content.indexOf(input.oldString)
211
+ const secondIndex = content.indexOf(input.oldString, firstIndex + 1)
100
212
 
101
213
  if (secondIndex !== -1) {
102
214
  const lineNumber = content.slice(0, firstIndex).split('\n').length
103
215
  const secondLine = content.slice(0, secondIndex).split('\n').length
104
216
  return {
105
217
  success: false,
106
- error: `old_string is not unique — found at lines ${lineNumber} and ${secondLine}. Provide more surrounding context to make it unique, or use replace_all: true.`,
218
+ error: `old_string/oldStr is not unique — found at lines ${lineNumber} and ${secondLine}. Provide more surrounding context to make it unique, or use replace_all: true.`,
107
219
  }
108
220
  }
109
221
 
@@ -111,8 +223,31 @@ function applyEdit(
111
223
  success: true,
112
224
  content:
113
225
  content.slice(0, firstIndex) +
114
- input.new_string +
115
- content.slice(firstIndex + input.old_string.length),
226
+ input.newString +
227
+ content.slice(firstIndex + input.oldString.length),
228
+ replacements: 1,
229
+ }
230
+ }
231
+
232
+ function applyLineInsert(
233
+ content: string,
234
+ input: Extract<NormalizedEditInput, { operation: 'insert' }>,
235
+ ): { success: true; content: string; replacements: number } {
236
+ const hasTrailingNewline = content.endsWith('\n')
237
+ const lines = content.split('\n')
238
+ if (hasTrailingNewline) lines.pop()
239
+
240
+ const line =
241
+ input.insertLine === 'end'
242
+ ? lines.length
243
+ : Math.min(Math.max(input.insertLine, 0), lines.length)
244
+ const inserted = input.newString.endsWith('\n')
245
+ ? input.newString.slice(0, -1).split('\n')
246
+ : input.newString.split('\n')
247
+ lines.splice(line, 0, ...inserted)
248
+ return {
249
+ success: true,
250
+ content: `${lines.join('\n')}${hasTrailingNewline ? '\n' : ''}`,
116
251
  replacements: 1,
117
252
  }
118
253
  }