@wrongstack/tools 0.313.0 → 0.316.1

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 (182) hide show
  1. package/dist/_edit-match.d.ts +3 -2
  2. package/dist/_output-spool.d.ts +4 -3
  3. package/dist/_shell-pick.d.ts +2 -1
  4. package/dist/_spawn-stream.d.ts +2 -1
  5. package/dist/_syntax-check.d.ts +2 -1
  6. package/dist/audit.js +10 -4055
  7. package/dist/bash-kill-guard.d.ts +3 -6
  8. package/dist/bash.js +9 -2308
  9. package/dist/batch-tool-use.js +2 -187
  10. package/dist/browser/index.js +25 -1339
  11. package/dist/browser/network-guard-proxy.d.ts +2 -1
  12. package/dist/builtin.js +69 -33949
  13. package/dist/chunk-22B7GE5T.js +260 -0
  14. package/dist/chunk-2VSZC6QM.js +113 -0
  15. package/dist/chunk-3PVYQFRQ.js +472 -0
  16. package/dist/chunk-42BHI7TN.js +337 -0
  17. package/dist/chunk-4BHUSMNL.js +104 -0
  18. package/dist/chunk-4I4LWFOO.js +431 -0
  19. package/dist/chunk-4UESMQDP.js +122 -0
  20. package/dist/chunk-553IQUMO.js +478 -0
  21. package/dist/chunk-5H4GSJAI.js +223 -0
  22. package/dist/chunk-6RNIFLG4.js +305 -0
  23. package/dist/chunk-7EY5DCRO.js +434 -0
  24. package/dist/chunk-A6BRWIXN.js +317 -0
  25. package/dist/chunk-AHHDWNXC.js +200 -0
  26. package/dist/chunk-BSLVR56S.js +199 -0
  27. package/dist/chunk-BTSFTGIL.js +7 -0
  28. package/dist/chunk-D33S4RQG.js +222 -0
  29. package/dist/chunk-DA4KNE2H.js +4412 -0
  30. package/dist/chunk-DR7JOQTC.js +520 -0
  31. package/dist/chunk-EEOFQDT6.js +898 -0
  32. package/dist/chunk-EJWSCQ6D.js +137 -0
  33. package/dist/chunk-EMRGN4KM.js +170 -0
  34. package/dist/chunk-FQZOHLG3.js +89 -0
  35. package/dist/chunk-GOIZNNOA.js +221 -0
  36. package/dist/chunk-HFXSNKCV.js +196 -0
  37. package/dist/chunk-HYX2TZ7O.js +1272 -0
  38. package/dist/chunk-I64BS3VC.js +15 -0
  39. package/dist/chunk-IVWV2UM6.js +955 -0
  40. package/dist/chunk-IXMNTNW5.js +238 -0
  41. package/dist/chunk-IYK7XGDZ.js +892 -0
  42. package/dist/chunk-J5SKD6LN.js +235 -0
  43. package/dist/chunk-JNNVRHMV.js +2111 -0
  44. package/dist/chunk-JOHXDF3D.js +295 -0
  45. package/dist/chunk-JRJJX3L5.js +424 -0
  46. package/dist/chunk-K3WNSHUF.js +1371 -0
  47. package/dist/chunk-L25CVUVR.js +2830 -0
  48. package/dist/chunk-LIJQMYYX.js +59 -0
  49. package/dist/chunk-LWKUTMD5.js +197 -0
  50. package/dist/chunk-MQX2KXVW.js +54 -0
  51. package/dist/chunk-MZZN7VCD.js +359 -0
  52. package/dist/chunk-NBDZMMG2.js +169 -0
  53. package/dist/chunk-NKJW5GFO.js +366 -0
  54. package/dist/chunk-NKNOBUBU.js +522 -0
  55. package/dist/chunk-O4MBV4AX.js +134 -0
  56. package/dist/chunk-OUBDL3WH.js +3515 -0
  57. package/dist/chunk-PQKK5JH7.js +101 -0
  58. package/dist/chunk-PY2IOS7C.js +1000 -0
  59. package/dist/chunk-Q5JPPCZC.js +12 -0
  60. package/dist/chunk-QBV725YG.js +128 -0
  61. package/dist/chunk-QZQSPUBF.js +315 -0
  62. package/dist/chunk-RGISBZBD.js +231 -0
  63. package/dist/chunk-RJZQWEMS.js +84 -0
  64. package/dist/chunk-RXISBFR5.js +315 -0
  65. package/dist/chunk-S5CCLZL3.js +99 -0
  66. package/dist/chunk-SDLVH5RZ.js +178 -0
  67. package/dist/chunk-T2F4S7U4.js +1723 -0
  68. package/dist/chunk-TEBQ4VPZ.js +163 -0
  69. package/dist/chunk-U4D5S3VQ.js +21 -0
  70. package/dist/chunk-U4VAATKP.js +193 -0
  71. package/dist/chunk-UCYQTZ7F.js +113 -0
  72. package/dist/chunk-UKU2ZX4C.js +203 -0
  73. package/dist/chunk-UNSKG6DC.js +159 -0
  74. package/dist/chunk-V43EQ6VR.js +68 -0
  75. package/dist/chunk-VB3MJGCV.js +610 -0
  76. package/dist/chunk-VFKPWSOX.js +154 -0
  77. package/dist/chunk-VJNHFZ7J.js +190 -0
  78. package/dist/chunk-VXECAJJ7.js +1091 -0
  79. package/dist/chunk-WHIIIREL.js +223 -0
  80. package/dist/chunk-Z6DAKQ3U.js +1 -0
  81. package/dist/chunk-ZJQ4IGXK.js +474 -0
  82. package/dist/chunk-ZLTFLC25.js +385 -0
  83. package/dist/circuit-breaker.js +3 -191
  84. package/dist/codebase-index/ast-invariant-engine.d.ts +5 -4
  85. package/dist/codebase-index/binary-frame.d.ts +0 -2
  86. package/dist/codebase-index/bm25.d.ts +1 -1
  87. package/dist/codebase-index/circuit-breaker.d.ts +2 -1
  88. package/dist/codebase-index/codebase-ast-replace-tool.d.ts +3 -2
  89. package/dist/codebase-index/codebase-impact-analysis-tool.d.ts +4 -3
  90. package/dist/codebase-index/codebase-invariant-check-tool.d.ts +3 -2
  91. package/dist/codebase-index/codebase-repo-map-tool.d.ts +3 -2
  92. package/dist/codebase-index/codebase-skeleton-tool.d.ts +3 -2
  93. package/dist/codebase-index/codebase-targeted-test-tool.d.ts +3 -2
  94. package/dist/codebase-index/content-hash.d.ts +0 -2
  95. package/dist/codebase-index/import-extractor.d.ts +0 -13
  96. package/dist/codebase-index/index-service.d.ts +1 -1
  97. package/dist/codebase-index/index.js +84 -13688
  98. package/dist/codebase-index/languages.d.ts +2 -1
  99. package/dist/codebase-index/module-roots.d.ts +3 -2
  100. package/dist/codebase-index/parser-batch.d.ts +0 -5
  101. package/dist/codebase-index/parser-dispatch.d.ts +2 -1
  102. package/dist/codebase-index/parser-output.d.ts +4 -3
  103. package/dist/codebase-index/parser-worker-pool.d.ts +0 -2
  104. package/dist/codebase-index/parser-worker-script.d.ts +1 -16
  105. package/dist/codebase-index/parser-worker-script.js +8 -3995
  106. package/dist/codebase-index/project-server-client-state.d.ts +2 -2
  107. package/dist/codebase-index/project-server-endpoint.d.ts +0 -15
  108. package/dist/codebase-index/project-server.js +60 -8548
  109. package/dist/codebase-index/tree-sitter/visitor.d.ts +2 -1
  110. package/dist/codebase-index/tree-sitter-parser.d.ts +0 -2
  111. package/dist/codebase-index/ts-parser.d.ts +1 -1
  112. package/dist/codebase-index/vector-search.d.ts +2 -1
  113. package/dist/codebase-index/wal-maintenance.d.ts +3 -2
  114. package/dist/codebase-index/worker.js +20 -8416
  115. package/dist/codebase-index/writer-admin.d.ts +0 -1
  116. package/dist/codebase-index/writer-search.d.ts +0 -9
  117. package/dist/design.js +2 -299
  118. package/dist/diff.js +4 -256
  119. package/dist/document.js +4 -222
  120. package/dist/e2e.js +5 -476
  121. package/dist/edit.js +14 -10200
  122. package/dist/exec.js +16 -3131
  123. package/dist/fetch.js +8 -398
  124. package/dist/format.js +10 -3750
  125. package/dist/generic-parser-VUXDIAOZ.js +13 -0
  126. package/dist/git.js +4 -380
  127. package/dist/glob.js +6 -280
  128. package/dist/go-parser-JXYKJHRC.js +469 -0
  129. package/dist/grep.js +7 -604
  130. package/dist/index.js +318 -35387
  131. package/dist/install.js +10 -4091
  132. package/dist/json-parser-IBHZ4BZC.js +213 -0
  133. package/dist/json.js +4 -665
  134. package/dist/kanban-contract-actions.d.ts +1 -1
  135. package/dist/kanban-detail-actions.d.ts +2 -1
  136. package/dist/kanban-presence.d.ts +2 -1
  137. package/dist/kanban-serializer.d.ts +0 -8
  138. package/dist/kanban-split-task-handler.d.ts +2 -2
  139. package/dist/kanban-tool-types.d.ts +2 -1
  140. package/dist/kanban.js +3 -2096
  141. package/dist/languages/diagnostics.d.ts +2 -1
  142. package/dist/languages/index.js +23 -4664
  143. package/dist/languages/legacy-bridge.d.ts +3 -2
  144. package/dist/languages/types.d.ts +2 -1
  145. package/dist/legacy-bridge-7M6QLUQN.js +18 -0
  146. package/dist/lint.js +10 -3706
  147. package/dist/logs.js +4 -287
  148. package/dist/memory.js +6 -215
  149. package/dist/mode.js +3 -99
  150. package/dist/outdated.js +4 -5170
  151. package/dist/pack.js +65 -33897
  152. package/dist/parser-dispatch-QH4XPGG6.js +14 -0
  153. package/dist/patch.js +13 -9935
  154. package/dist/plan.js +5 -3286
  155. package/dist/process-registry.js +8 -651
  156. package/dist/ps-slash.js +12 -1439
  157. package/dist/pwsh.d.ts +0 -1
  158. package/dist/py-parser-IS4HKYMF.js +16 -0
  159. package/dist/read.js +13 -9816
  160. package/dist/replace.js +15 -10014
  161. package/dist/rs-parser-XHUUO2KZ.js +81 -0
  162. package/dist/scaffold.js +4 -253
  163. package/dist/search.js +5 -585
  164. package/dist/session-kanban-sync.d.ts +0 -6
  165. package/dist/session-kanban.js +30 -972
  166. package/dist/skill.js +2 -215
  167. package/dist/spawn-background.d.ts +1 -1
  168. package/dist/task.js +5 -3410
  169. package/dist/test.js +10 -3773
  170. package/dist/todo.js +4 -2891
  171. package/dist/tool-help.js +3 -123
  172. package/dist/tool-search.js +3 -79
  173. package/dist/tool-tier.js +68 -33984
  174. package/dist/tool-use.js +2 -93
  175. package/dist/tree-sitter-parser-AIERW3YI.js +15 -0
  176. package/dist/tree.js +4 -288
  177. package/dist/ts-parser-RCTYKSVI.js +232 -0
  178. package/dist/typecheck.js +10 -3753
  179. package/dist/win32.js +6 -60
  180. package/dist/write.js +14 -9882
  181. package/dist/yaml-parser-4SAXV3PL.js +157 -0
  182. package/package.json +5 -5
@@ -0,0 +1,223 @@
1
+ // src/memory.ts
2
+ import { ToolValidationError } from "@wrongstack/core/types";
3
+ function rememberTool(memory) {
4
+ return {
5
+ name: "remember",
6
+ category: "Session",
7
+ description: "Persist facts, conventions, decisions, and preferences into long-term memory. Memories survive restarts and are scored for relevance in future sessions.",
8
+ usageHint: "Persist facts, conventions, decisions, and preferences into long-term memory.\n\nWHEN TO USE:\n- Project conventions discovered during a task (build tool, lint rules, code style)\n- Architecture decisions made (chose X over Y, decided to use pattern Z)\n- User preferences expressed (prefers short names, always uses pnpm)\n- Anti-patterns identified (never do X, avoid pattern Y)\n- File/location references useful across sessions\n\nWHEN NOT TO USE:\n- Temporary task state or progress \u2192 use `todo`\n- One-off debugging notes\n- Information already obvious from the codebase\n\nAlways include `type` and `priority`. Use 1-3 `tags` for grouping.\nBetter to remember a fact now than rediscover it next session.",
9
+ permission: "confirm",
10
+ mutating: true,
11
+ timeoutMs: 2e3,
12
+ capabilities: ["memory.write"],
13
+ icon: "settings",
14
+ inputSchema: {
15
+ type: "object",
16
+ properties: {
17
+ text: {
18
+ type: "string",
19
+ description: "The fact or note to remember. Keep it concise and factual."
20
+ },
21
+ scope: {
22
+ type: "string",
23
+ enum: ["project-agents", "project-memory", "user-memory"],
24
+ description: "Where to store it: project-memory (shared), user-memory (personal), or project-agents."
25
+ },
26
+ type: {
27
+ type: "string",
28
+ enum: ["fact", "decision", "convention", "preference", "reference", "anti_pattern"],
29
+ description: "Category for filtering and relevance scoring."
30
+ },
31
+ tags: {
32
+ type: "array",
33
+ items: { type: "string" },
34
+ description: "Hashtag-style tags for grouping and search."
35
+ },
36
+ priority: {
37
+ type: "string",
38
+ enum: ["critical", "high", "medium", "low"],
39
+ description: "Priority level. Critical = always injected into context."
40
+ }
41
+ },
42
+ required: ["text"]
43
+ },
44
+ async execute(input) {
45
+ if (!input?.text) {
46
+ throw new ToolValidationError({
47
+ message: "remember: text is required",
48
+ field: "text"
49
+ });
50
+ }
51
+ const scope = input.scope ?? "project-memory";
52
+ await memory.remember(input.text, scope, {
53
+ type: input.type,
54
+ tags: input.tags,
55
+ priority: input.priority
56
+ });
57
+ return { ok: true, scope };
58
+ }
59
+ };
60
+ }
61
+ function forgetTool(memory) {
62
+ return {
63
+ name: "forget",
64
+ category: "Session",
65
+ description: "Remove memory entries that contain the given substring (case-insensitive). Use with caution. Pass `dry_run: true` to preview the matching entries (capped at 20) without deleting anything.",
66
+ usageHint: "This permanently deletes matching memories in the chosen scope.\n- Provide a reasonably specific `query` to avoid deleting unrelated memories.\n- Always double-check before calling with broad queries \u2014 `dry_run: true` previews the matches without deleting.\n- Use `remember` + `forget` together to maintain clean long-term memory.",
67
+ permission: "confirm",
68
+ // WS-046: gives permission decisions something to key on — the substring
69
+ // being forgotten.
70
+ subjectKey: "query",
71
+ mutating: true,
72
+ timeoutMs: 2e3,
73
+ capabilities: ["memory.delete"],
74
+ icon: "settings",
75
+ inputSchema: {
76
+ type: "object",
77
+ properties: {
78
+ query: { type: "string" },
79
+ scope: { type: "string", enum: ["project-agents", "project-memory", "user-memory"] },
80
+ dry_run: {
81
+ type: "boolean",
82
+ description: "When true, return the matched entries (capped at 20) WITHOUT deleting them. Default false."
83
+ }
84
+ },
85
+ required: ["query"]
86
+ },
87
+ async execute(input) {
88
+ if (!input?.query) {
89
+ throw new ToolValidationError({
90
+ message: "forget: query is required",
91
+ field: "query"
92
+ });
93
+ }
94
+ const scope = input.scope ?? "project-memory";
95
+ if (input.dry_run) {
96
+ const entries = await memory.list(scope);
97
+ const needle = input.query.toLowerCase();
98
+ const matching = entries.filter((entry) => entry.text.toLowerCase().includes(needle));
99
+ return {
100
+ removed: 0,
101
+ scope,
102
+ dryRun: true,
103
+ matched: matching.length,
104
+ matches: matching.slice(0, 20).map((entry) => entry.text)
105
+ };
106
+ }
107
+ const removed = await memory.forget(input.query, scope);
108
+ return { removed, scope };
109
+ }
110
+ };
111
+ }
112
+ function searchMemoryTool(memory) {
113
+ return {
114
+ name: "search_memory",
115
+ category: "Session",
116
+ description: "Search memory entries by content. With the default backend this does substring matching; semantic/graph backends use embedding similarity or graph traversal.",
117
+ usageHint: "Search long-term memory for relevant facts, conventions, or decisions.\n- Returns results ordered by relevance (newest-first for default, similarity for semantic).\n- Use before starting a task to recall project conventions and past decisions.\n- `limit` caps results (default 5, max 20).",
118
+ permission: "auto",
119
+ mutating: false,
120
+ timeoutMs: 2e3,
121
+ capabilities: ["memory.read"],
122
+ inputSchema: {
123
+ type: "object",
124
+ properties: {
125
+ query: {
126
+ type: "string",
127
+ description: "Search query \u2014 words or phrase to find in memory."
128
+ },
129
+ scope: {
130
+ type: "string",
131
+ enum: ["project-agents", "project-memory", "user-memory"],
132
+ description: "Which scope to search. Defaults to project-memory."
133
+ },
134
+ limit: {
135
+ type: "number",
136
+ description: "Maximum results to return (default 5, max 20)."
137
+ }
138
+ },
139
+ required: ["query"]
140
+ },
141
+ async execute(input) {
142
+ if (!input?.query) {
143
+ throw new ToolValidationError({
144
+ message: "search_memory: query is required",
145
+ field: "query"
146
+ });
147
+ }
148
+ const scope = input.scope ?? "project-memory";
149
+ const limit = Math.min(input.limit ?? 5, 20);
150
+ const entries = await memory.search(input.query, scope, limit);
151
+ return {
152
+ results: entries.map((e) => ({
153
+ text: e.text,
154
+ ts: e.ts,
155
+ scope: e.scope,
156
+ type: e.type,
157
+ tags: e.tags,
158
+ priority: e.priority
159
+ }))
160
+ };
161
+ }
162
+ };
163
+ }
164
+ function relatedMemoryTool(memory) {
165
+ return {
166
+ name: "find_related_memories",
167
+ category: "Session",
168
+ description: "Find memories related to the given text via graph traversal. Only available with graph backends; falls back to content search with file backends.",
169
+ usageHint: "Discover memories connected to a topic through co-occurrence or similarity edges.\n- Useful for exploring what else the project knows about a given concept.\n- Falls back to content search when no graph backend is configured.\n- `limit` caps results (default 5, max 20).",
170
+ permission: "auto",
171
+ mutating: false,
172
+ timeoutMs: 2e3,
173
+ capabilities: ["memory.read"],
174
+ inputSchema: {
175
+ type: "object",
176
+ properties: {
177
+ text: {
178
+ type: "string",
179
+ description: "Text to find related memories for."
180
+ },
181
+ scope: {
182
+ type: "string",
183
+ enum: ["project-agents", "project-memory", "user-memory"],
184
+ description: "Which scope to search. Defaults to project-memory."
185
+ },
186
+ limit: {
187
+ type: "number",
188
+ description: "Maximum results to return (default 5, max 20)."
189
+ }
190
+ },
191
+ required: ["text"]
192
+ },
193
+ async execute(input) {
194
+ if (!input?.text) {
195
+ throw new ToolValidationError({
196
+ message: "find_related_memories: text is required",
197
+ field: "text"
198
+ });
199
+ }
200
+ const scope = input.scope ?? "project-memory";
201
+ const limit = Math.min(input.limit ?? 5, 20);
202
+ const entries = memory.findRelated ? await memory.findRelated(input.text, scope, limit) : await memory.search(input.text, scope, limit);
203
+ return {
204
+ results: entries.map((e) => ({
205
+ text: e.text,
206
+ ts: e.ts,
207
+ scope: e.scope,
208
+ type: e.type,
209
+ tags: e.tags,
210
+ priority: e.priority
211
+ }))
212
+ };
213
+ }
214
+ };
215
+ }
216
+
217
+ export {
218
+ rememberTool,
219
+ forgetTool,
220
+ searchMemoryTool,
221
+ relatedMemoryTool
222
+ };
223
+ //# sourceMappingURL=chunk-WHIIIREL.js.map
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=chunk-Z6DAKQ3U.js.map
@@ -0,0 +1,474 @@
1
+ import {
2
+ checkSyntax
3
+ } from "./chunk-FQZOHLG3.js";
4
+ import {
5
+ enqueueReindex
6
+ } from "./chunk-HYX2TZ7O.js";
7
+ import {
8
+ safeResolveReal,
9
+ sha256hex,
10
+ truncateDiffPayload
11
+ } from "./chunk-D33S4RQG.js";
12
+
13
+ // src/edit.ts
14
+ import * as fs from "node:fs/promises";
15
+ import {
16
+ atomicWrite,
17
+ detectNewlineStyle,
18
+ normalizeToLf,
19
+ toStyle,
20
+ unifiedDiff
21
+ } from "@wrongstack/core/utils";
22
+ import { ToolValidationError } from "@wrongstack/core/types";
23
+
24
+ // src/_edit-match.ts
25
+ var TIER_LABEL = {
26
+ exact: "exact match",
27
+ "trailing-whitespace": "whitespace-normalized match (trailing whitespace ignored)",
28
+ "whitespace-normalized": "whitespace-normalized match (indentation ignored, replacement re-indented)",
29
+ fuzzy: "fuzzy match (block anchors exact, interior \u226590% similar)"
30
+ };
31
+ var TIER_CONFIDENCE = {
32
+ exact: "exact",
33
+ "trailing-whitespace": "high",
34
+ "whitespace-normalized": "medium",
35
+ fuzzy: "low"
36
+ };
37
+ var MIN_NORMALIZED_NEEDLE_CHARS = 16;
38
+ var FUZZY_MIN_SIMILARITY = 0.9;
39
+ var FUZZY_AMBIGUITY_MARGIN = 0.05;
40
+ var FUZZY_MAX_INTERIOR_CHARS = 2e3;
41
+ function findLadderMatches(fileLf, oldLf) {
42
+ const exact = [];
43
+ let idx = fileLf.indexOf(oldLf);
44
+ while (idx !== -1) {
45
+ exact.push({ start: idx, end: idx + oldLf.length, startLine: lineAt(fileLf, idx) });
46
+ idx = fileLf.indexOf(oldLf, idx + 1);
47
+ }
48
+ if (exact.length > 0) return { tier: "exact", matches: exact };
49
+ const fileLines = fileLf.split("\n");
50
+ const needleLines = oldLf.split("\n");
51
+ if (needleLines.length > fileLines.length) return void 0;
52
+ const offsets = lineOffsets(fileLines);
53
+ const fileTrimEnd = fileLines.map((l) => l.trimEnd());
54
+ const needleTrimEnd = needleLines.map((l) => l.trimEnd());
55
+ const trailing = windowScan(fileTrimEnd, needleTrimEnd, offsets, fileLines, (a, b) => a === b);
56
+ if (trailing.length > 0) return { tier: "trailing-whitespace", matches: trailing };
57
+ const normalizedLen = needleTrimEnd.reduce((n, l) => n + l.trimStart().length, 0);
58
+ if (normalizedLen < MIN_NORMALIZED_NEEDLE_CHARS) return void 0;
59
+ const fileTrimmed = fileTrimEnd.map((l) => l.trimStart());
60
+ const needleTrimmed = needleTrimEnd.map((l) => l.trimStart());
61
+ const normalized = windowScan(fileTrimmed, needleTrimmed, offsets, fileLines, (a, b) => a === b);
62
+ if (normalized.length > 0) return { tier: "whitespace-normalized", matches: normalized };
63
+ return fuzzyScan(fileLines, needleLines, offsets);
64
+ }
65
+ function lineAt(text, pos) {
66
+ if (pos < 512) {
67
+ let line2 = 1;
68
+ for (let i = 0; i < pos; i++) {
69
+ if (text.charCodeAt(i) === 10) line2++;
70
+ }
71
+ return line2;
72
+ }
73
+ let line = 1;
74
+ let search = 0;
75
+ while (true) {
76
+ const idx = text.indexOf("\n", search);
77
+ if (idx === -1 || idx >= pos) return line;
78
+ search = idx + 1;
79
+ line++;
80
+ }
81
+ }
82
+ function lineOffsets(lines) {
83
+ const out = new Array(lines.length);
84
+ let pos = 0;
85
+ for (let i = 0; i < lines.length; i++) {
86
+ out[i] = pos;
87
+ pos += lines[i].length + 1;
88
+ }
89
+ return out;
90
+ }
91
+ function windowToMatch(fileLines, offsets, start, windowLen) {
92
+ const lastLine = start + windowLen - 1;
93
+ return {
94
+ start: offsets[start],
95
+ end: offsets[lastLine] + fileLines[lastLine].length,
96
+ startLine: start + 1
97
+ };
98
+ }
99
+ function windowScan(comparisonLines, needleLines, offsets, originalLines, eq) {
100
+ const n = needleLines.length;
101
+ const out = [];
102
+ for (let i = 0; i + n <= comparisonLines.length; i++) {
103
+ let all = true;
104
+ for (let j = 0; j < n; j++) {
105
+ if (!eq(comparisonLines[i + j], needleLines[j])) {
106
+ all = false;
107
+ break;
108
+ }
109
+ }
110
+ if (all) {
111
+ out.push(windowToMatch(originalLines, offsets, i, n));
112
+ i += n - 1;
113
+ }
114
+ }
115
+ return out;
116
+ }
117
+ function fuzzyScan(fileLines, needleLines, offsets) {
118
+ const n = needleLines.length;
119
+ if (n < 3) return void 0;
120
+ const firstNeedle = needleLines[0].trim();
121
+ const lastNeedle = needleLines[n - 1].trim();
122
+ const needleInterior = needleLines.slice(1, -1).map((l) => l.trim()).join("\n");
123
+ if (needleInterior.length > FUZZY_MAX_INTERIOR_CHARS) return void 0;
124
+ const candidates = [];
125
+ for (let i = 0; i + n <= fileLines.length; i++) {
126
+ if (fileLines[i].trim() !== firstNeedle) continue;
127
+ if (fileLines[i + n - 1].trim() !== lastNeedle) continue;
128
+ const windowInterior = fileLines.slice(i + 1, i + n - 1).map((l) => l.trim()).join("\n");
129
+ if (windowInterior.length > FUZZY_MAX_INTERIOR_CHARS) continue;
130
+ const score = similarity(needleInterior, windowInterior);
131
+ if (score >= FUZZY_MIN_SIMILARITY) {
132
+ candidates.push({ match: windowToMatch(fileLines, offsets, i, n), score });
133
+ }
134
+ }
135
+ if (candidates.length === 0) return void 0;
136
+ candidates.sort((a, b) => b.score - a.score);
137
+ const best = candidates[0];
138
+ const runnerUp = candidates[1];
139
+ if (runnerUp && best.score - runnerUp.score < FUZZY_AMBIGUITY_MARGIN) {
140
+ return {
141
+ tier: "fuzzy",
142
+ matches: candidates.map((c) => c.match),
143
+ score: best.score,
144
+ ambiguous: true
145
+ };
146
+ }
147
+ return { tier: "fuzzy", matches: [best.match], score: best.score };
148
+ }
149
+ function similarity(a, b) {
150
+ if (a === b) return 1;
151
+ const max = Math.max(a.length, b.length);
152
+ if (max === 0) return 1;
153
+ if (Math.abs(a.length - b.length) / max > 1 - FUZZY_MIN_SIMILARITY + 0.05) {
154
+ return 1 - Math.abs(a.length - b.length) / max;
155
+ }
156
+ return 1 - levenshtein(a, b) / max;
157
+ }
158
+ function levenshtein(a, b) {
159
+ if (a.length === 0) return b.length;
160
+ if (b.length === 0) return a.length;
161
+ let prev = new Array(b.length + 1);
162
+ let cur = new Array(b.length + 1);
163
+ for (let j = 0; j <= b.length; j++) prev[j] = j;
164
+ for (let i = 1; i <= a.length; i++) {
165
+ cur[0] = i;
166
+ const ca = a.charCodeAt(i - 1);
167
+ for (let j = 1; j <= b.length; j++) {
168
+ const cost = ca === b.charCodeAt(j - 1) ? 0 : 1;
169
+ cur[j] = Math.min(
170
+ prev[j] + 1,
171
+ cur[j - 1] + 1,
172
+ prev[j - 1] + cost
173
+ );
174
+ }
175
+ [prev, cur] = [cur, prev];
176
+ }
177
+ return prev[b.length];
178
+ }
179
+ function firstLineIndent(text) {
180
+ for (const line of text.split("\n")) {
181
+ if (line.trim() === "") continue;
182
+ return /^[ \t]*/.exec(line)[0];
183
+ }
184
+ return "";
185
+ }
186
+ function adjustIndent(newLf, fromIndent, toIndent) {
187
+ if (fromIndent === toIndent) return { text: newLf, adjusted: false };
188
+ const lines = newLf.split("\n");
189
+ if (toIndent.startsWith(fromIndent)) {
190
+ const extra = toIndent.slice(fromIndent.length);
191
+ return {
192
+ text: lines.map((l) => l.trim() === "" ? l : extra + l).join("\n"),
193
+ adjusted: true
194
+ };
195
+ }
196
+ if (fromIndent.startsWith(toIndent)) {
197
+ const remove = fromIndent.slice(toIndent.length);
198
+ return {
199
+ text: lines.map((l) => l.startsWith(remove) ? l.slice(remove.length) : l).join("\n"),
200
+ adjusted: true
201
+ };
202
+ }
203
+ return { text: newLf, adjusted: false };
204
+ }
205
+ function nearestMatchHint(fileLf, oldLf) {
206
+ const fileLines = fileLf.split("\n");
207
+ const needleLines = oldLf.split("\n");
208
+ if (needleLines.length > fileLines.length) return void 0;
209
+ const needleTrimmed = needleLines.map((l) => l.trim());
210
+ let bestScore = 0;
211
+ let bestStart = -1;
212
+ for (let i = 0; i + needleLines.length <= fileLines.length; i++) {
213
+ let sum = 0;
214
+ for (let j = 0; j < needleLines.length; j++) {
215
+ sum += prefixSimilarity(needleTrimmed[j], fileLines[i + j].trim());
216
+ }
217
+ const score = sum / needleLines.length;
218
+ if (score > bestScore) {
219
+ bestScore = score;
220
+ bestStart = i;
221
+ }
222
+ }
223
+ if (bestStart === -1 || bestScore < 0.5) return void 0;
224
+ const snippet = fileLines.slice(bestStart, bestStart + Math.min(3, needleLines.length)).map((l) => l.length > 120 ? `${l.slice(0, 120)}\u2026` : l).join("\n");
225
+ return { line: bestStart + 1, snippet };
226
+ }
227
+ function prefixSimilarity(a, b) {
228
+ if (a === b) return 1;
229
+ const max = Math.max(a.length, b.length);
230
+ if (max === 0) return 1;
231
+ let p = 0;
232
+ const lim = Math.min(a.length, b.length);
233
+ while (p < lim && a.charCodeAt(p) === b.charCodeAt(p)) p++;
234
+ return p / max;
235
+ }
236
+
237
+ // src/edit.ts
238
+ var MAX_DIFF_BYTES = 262144;
239
+ var editTool = {
240
+ name: "edit",
241
+ category: "Filesystem",
242
+ description: "Perform a precise, surgical text replacement in a file. This is the preferred tool for modifying existing code. It works best after a prior `read`, but can auto-read the current file when the replacement is still unambiguous. Fails safely if the `old_string` appears more than once unless `replace_all` is set.",
243
+ usageHint: "RECOMMENDED WORKFLOW:\n1. Prefer calling `read` on the target file first when planning an edit.\n2. Use a sufficiently unique `old_string` (include surrounding lines/context if needed).\n3. If the string appears multiple times and you want to change all of them, set `replace_all: true`.\n4. `new_string` must be the exact replacement text.\n\nIf no prior read is recorded, the tool auto-reads the current file and only applies the edit after the same ambiguity checks pass.\nIf `old_string` differs from the file only in whitespace (trailing spaces, indentation), a lower-confidence fallback match is applied and reported in `matched_by`/`note` \u2014 always verify the diff when this happens.\nAfter the edit, TS/JS/JSON files are syntax-checked; if `syntax_errors` is present in the output, fix those errors immediately with a follow-up edit.",
244
+ selection: {
245
+ doNotUseWhen: "creating a new file, replacing the whole file, or applying an existing unified diff.",
246
+ useInstead: ["write", "patch"]
247
+ },
248
+ permission: "confirm",
249
+ // WS-046: gives permission decisions something to key on — the file being
250
+ // edited, so trust rules can scope by path.
251
+ subjectKey: "path",
252
+ mutating: true,
253
+ capabilities: ["fs.write"],
254
+ icon: "edit",
255
+ timeoutMs: 5e3,
256
+ maxOutputBytes: 262144,
257
+ inputSchema: {
258
+ type: "object",
259
+ properties: {
260
+ path: {
261
+ type: "string",
262
+ description: "Path to the file to edit \u2014 relative to the project root, or absolute inside it."
263
+ },
264
+ old_string: {
265
+ type: "string",
266
+ description: "The exact text to replace, including whitespace and indentation. Must be unique in the file unless `replace_all` is set \u2014 add surrounding lines to disambiguate."
267
+ },
268
+ new_string: {
269
+ type: "string",
270
+ description: "The exact replacement text (may be empty to delete `old_string`)."
271
+ },
272
+ replace_all: {
273
+ type: "boolean",
274
+ description: "Replace every occurrence instead of requiring a unique match. Only allowed when `old_string` matches exactly (or up to trailing whitespace) \u2014 fuzzy matches stay single-target."
275
+ }
276
+ },
277
+ required: ["path", "old_string", "new_string"]
278
+ },
279
+ async execute(input, ctx, opts) {
280
+ if (!input?.path) {
281
+ throw new ToolValidationError({ message: "edit: path is required", field: "path" });
282
+ }
283
+ if (input.old_string === void 0) {
284
+ throw new ToolValidationError({
285
+ message: "edit: old_string is required",
286
+ field: "old_string"
287
+ });
288
+ }
289
+ if (input.new_string === void 0) {
290
+ throw new ToolValidationError({
291
+ message: "edit: new_string is required",
292
+ field: "new_string"
293
+ });
294
+ }
295
+ if (input.old_string === "") {
296
+ throw new ToolValidationError({
297
+ message: "edit: old_string cannot be empty",
298
+ field: "old_string"
299
+ });
300
+ }
301
+ const absPath = await safeResolveReal(input.path, ctx);
302
+ const stat2 = await fs.stat(absPath).catch((err) => {
303
+ if (err.code === "ENOENT") {
304
+ throw new ToolValidationError({
305
+ message: `edit: file "${input.path}" does not exist. Use \`write\` instead.`,
306
+ field: "path",
307
+ context: { exists: false }
308
+ });
309
+ }
310
+ throw err;
311
+ });
312
+ if (!stat2.isFile()) {
313
+ throw new ToolValidationError({
314
+ message: `edit: "${input.path}" is not a regular file`,
315
+ field: "path"
316
+ });
317
+ }
318
+ const autoRead = !ctx.hasRead(absPath);
319
+ const original = await fs.readFile(absPath, "utf8");
320
+ const updated = await fs.stat(absPath);
321
+ const mtimeTolerance = process.platform === "win32" ? 2e3 : 1;
322
+ const originalHash = sha256hex(original);
323
+ const lastReadHash = ctx.lastReadHash?.(absPath);
324
+ if (lastReadHash !== void 0) {
325
+ if (lastReadHash !== originalHash) {
326
+ throw new ToolValidationError({
327
+ message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
328
+ field: "path",
329
+ context: { reason: "external_modification" }
330
+ });
331
+ }
332
+ } else {
333
+ const lastReadMtime = ctx.lastReadMtime(absPath);
334
+ if (lastReadMtime !== void 0 && updated.mtimeMs > lastReadMtime + mtimeTolerance) {
335
+ throw new ToolValidationError({
336
+ message: `edit: file "${input.path}" was modified externally. Re-read it first.`,
337
+ field: "path",
338
+ context: { reason: "external_modification" }
339
+ });
340
+ }
341
+ }
342
+ if (autoRead && updated.mtimeMs > stat2.mtimeMs + mtimeTolerance) {
343
+ throw new ToolValidationError({
344
+ message: `edit: file "${input.path}" changed while being auto-read. Retry the edit.`,
345
+ field: "path",
346
+ context: { reason: "auto_read_race" }
347
+ });
348
+ }
349
+ const autoReadNote = autoRead ? `No prior read was recorded for "${input.path}"; edit auto-read the current file and applied the replacement only after the ambiguity checks passed.` : void 0;
350
+ const style = detectNewlineStyle(original);
351
+ const fileLf = normalizeToLf(original);
352
+ const oldLf = normalizeToLf(input.old_string);
353
+ const newLf = normalizeToLf(input.new_string);
354
+ if (oldLf === newLf) {
355
+ if (!fileLf.includes(oldLf)) {
356
+ throw noMatchError(input.path, fileLf, oldLf);
357
+ }
358
+ if (autoRead) ctx.recordRead(absPath, updated.mtimeMs, "user", originalHash);
359
+ return {
360
+ path: absPath,
361
+ replacements: 0,
362
+ diff: "(no-op: old and new are identical)",
363
+ matched_by: "exact",
364
+ note: autoReadNote
365
+ };
366
+ }
367
+ opts?.signal?.throwIfAborted();
368
+ const ladder = findLadderMatches(fileLf, oldLf);
369
+ if (!ladder) {
370
+ opts?.signal?.throwIfAborted();
371
+ throw noMatchError(input.path, fileLf, oldLf);
372
+ }
373
+ const { tier, matches } = ladder;
374
+ const count = matches.length;
375
+ if (ladder.ambiguous) {
376
+ const lines = matches.map((m) => m.startLine);
377
+ throw new ToolValidationError({
378
+ message: `edit: old_string only matched fuzzily and ${count} candidate blocks scored too close to distinguish (lines: ${lines.join(", ")}) in "${input.path}". Re-read the file and use the exact text of the intended block.`,
379
+ field: "old_string",
380
+ context: { occurrences: count, matchTier: tier }
381
+ });
382
+ }
383
+ if (input.replace_all && tier !== "exact" && tier !== "trailing-whitespace") {
384
+ throw new ToolValidationError({
385
+ message: `edit: old_string only matched via ${TIER_LABEL[tier]} in "${input.path}", but replace_all requires an exact (or trailing-whitespace) match. Re-read the file and use its exact text.`,
386
+ field: "old_string",
387
+ context: { matchTier: tier }
388
+ });
389
+ }
390
+ if (count > 1 && !input.replace_all) {
391
+ const lines = matches.map((m) => m.startLine);
392
+ throw new ToolValidationError({
393
+ message: `edit: old_string matched ${count} times in "${input.path}" (lines: ${lines.join(", ")})${tier === "exact" ? "" : ` via ${TIER_LABEL[tier]}`}. Add more context to make it unique, or set replace_all: true.`,
394
+ field: "old_string",
395
+ context: { occurrences: count, matchTier: tier }
396
+ });
397
+ }
398
+ let newFileLf;
399
+ let tierNote;
400
+ if (tier === "exact") {
401
+ newFileLf = input.replace_all ? fileLf.split(oldLf).join(newLf) : fileLf.replace(oldLf, newLf);
402
+ } else {
403
+ let replacement = newLf;
404
+ let indentSuffix = "";
405
+ if (tier === "whitespace-normalized" || tier === "fuzzy") {
406
+ const first = matches[0];
407
+ const matchedText = fileLf.slice(first.start, first.end);
408
+ const adjusted = adjustIndent(newLf, firstLineIndent(oldLf), firstLineIndent(matchedText));
409
+ replacement = adjusted.text;
410
+ if (adjusted.adjusted) indentSuffix = "; replacement re-indented to match the file";
411
+ }
412
+ newFileLf = fileLf;
413
+ const applied = input.replace_all ? matches : matches.slice(0, 1);
414
+ for (let i = applied.length - 1; i >= 0; i--) {
415
+ const m = applied[i];
416
+ newFileLf = newFileLf.slice(0, m.start) + replacement + newFileLf.slice(m.end);
417
+ }
418
+ const scoreSuffix = ladder.score !== void 0 ? `, similarity ${(ladder.score * 100).toFixed(1)}%` : "";
419
+ tierNote = `old_string did not match exactly; applied via ${TIER_LABEL[tier]} at line ${matches[0].startLine} (confidence: ${TIER_CONFIDENCE[tier]}${scoreSuffix}${indentSuffix}). Review the diff to confirm the intended target was edited.`;
420
+ }
421
+ const newFile = toStyle(newFileLf, style);
422
+ opts?.signal?.throwIfAborted();
423
+ await atomicWrite(absPath, newFile, { mode: updated.mode & 511 });
424
+ try {
425
+ enqueueReindex({ projectRoot: ctx.projectRoot, files: [absPath] });
426
+ } catch {
427
+ }
428
+ const written = await fs.stat(absPath);
429
+ ctx.recordRead(absPath, written.mtimeMs, "write", sha256hex(newFile));
430
+ ctx.session?.recordFileChange?.({
431
+ path: absPath,
432
+ action: "modified",
433
+ before: original,
434
+ after: newFile
435
+ });
436
+ opts?.signal?.throwIfAborted();
437
+ const { text: diff, truncated: diffTruncated } = truncateDiffPayload(
438
+ unifiedDiff(original, newFile, {
439
+ fromFile: input.path,
440
+ toFile: input.path
441
+ }),
442
+ MAX_DIFF_BYTES
443
+ );
444
+ const diffNote = diffTruncated ? "Diff truncated to the 256 KiB output budget \u2014 the full edit is on disk." : void 0;
445
+ const syntax = await checkSyntax(absPath, newFile, original).catch(() => void 0);
446
+ let syntaxNote;
447
+ if (syntax && syntax.errors.length > 0) {
448
+ syntaxNote = syntax.preExisting ? `Syntax check: the file still has parse errors (they pre-date this edit) \u2014 see syntax_errors.` : `Syntax check: this edit introduced ${syntax.errors.length} parse error(s) \u2014 fix them now, see syntax_errors.`;
449
+ }
450
+ const notes = [autoReadNote, tierNote, diffNote, syntaxNote].filter(Boolean);
451
+ return {
452
+ path: absPath,
453
+ replacements: input.replace_all ? count : 1,
454
+ diff,
455
+ matched_by: tier,
456
+ syntax_errors: syntax && syntax.errors.length > 0 ? syntax.errors : void 0,
457
+ note: notes.length > 0 ? notes.join("\n") : void 0
458
+ };
459
+ }
460
+ };
461
+ function noMatchError(inputPath, fileLf, oldLf) {
462
+ const hint = nearestMatchHint(fileLf, oldLf);
463
+ return new ToolValidationError({
464
+ message: `edit: no match for old_string in "${inputPath}".${hint ? ` Nearest match near line ${hint.line}:
465
+ ${hint.snippet}
466
+ Compare this against your old_string and retry with the file's actual text.` : ""}`,
467
+ field: "old_string"
468
+ });
469
+ }
470
+
471
+ export {
472
+ editTool
473
+ };
474
+ //# sourceMappingURL=chunk-ZJQ4IGXK.js.map