diffninja 0.1.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 (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +259 -0
  3. package/dist/calltree.d.ts +47 -0
  4. package/dist/calltree.js +296 -0
  5. package/dist/cli.d.ts +57 -0
  6. package/dist/cli.js +340 -0
  7. package/dist/diff.d.ts +7 -0
  8. package/dist/diff.js +114 -0
  9. package/dist/extract.d.ts +26 -0
  10. package/dist/extract.js +152 -0
  11. package/dist/git.d.ts +40 -0
  12. package/dist/git.js +288 -0
  13. package/dist/index.d.ts +9 -0
  14. package/dist/index.js +8 -0
  15. package/dist/infer.d.ts +21 -0
  16. package/dist/infer.js +189 -0
  17. package/dist/languages/bash.d.ts +2 -0
  18. package/dist/languages/bash.js +208 -0
  19. package/dist/languages/c.d.ts +2 -0
  20. package/dist/languages/c.js +218 -0
  21. package/dist/languages/call-syntax.d.ts +125 -0
  22. package/dist/languages/call-syntax.js +997 -0
  23. package/dist/languages/cpp.d.ts +2 -0
  24. package/dist/languages/cpp.js +321 -0
  25. package/dist/languages/csharp.d.ts +2 -0
  26. package/dist/languages/csharp.js +324 -0
  27. package/dist/languages/elixir.d.ts +2 -0
  28. package/dist/languages/elixir.js +331 -0
  29. package/dist/languages/go.d.ts +2 -0
  30. package/dist/languages/go.js +299 -0
  31. package/dist/languages/grammars.d.ts +50 -0
  32. package/dist/languages/grammars.js +351 -0
  33. package/dist/languages/haskell.d.ts +2 -0
  34. package/dist/languages/haskell.js +250 -0
  35. package/dist/languages/java.d.ts +2 -0
  36. package/dist/languages/java.js +351 -0
  37. package/dist/languages/javascript.d.ts +4 -0
  38. package/dist/languages/javascript.js +648 -0
  39. package/dist/languages/kotlin.d.ts +2 -0
  40. package/dist/languages/kotlin.js +368 -0
  41. package/dist/languages/lua.d.ts +2 -0
  42. package/dist/languages/lua.js +212 -0
  43. package/dist/languages/ocaml.d.ts +2 -0
  44. package/dist/languages/ocaml.js +291 -0
  45. package/dist/languages/perl.d.ts +2 -0
  46. package/dist/languages/perl.js +418 -0
  47. package/dist/languages/php.d.ts +2 -0
  48. package/dist/languages/php.js +397 -0
  49. package/dist/languages/python.d.ts +2 -0
  50. package/dist/languages/python.js +376 -0
  51. package/dist/languages/registry.d.ts +7 -0
  52. package/dist/languages/registry.js +69 -0
  53. package/dist/languages/ruby.d.ts +2 -0
  54. package/dist/languages/ruby.js +391 -0
  55. package/dist/languages/rust.d.ts +2 -0
  56. package/dist/languages/rust.js +261 -0
  57. package/dist/languages/scala.d.ts +2 -0
  58. package/dist/languages/scala.js +307 -0
  59. package/dist/languages/solidity.d.ts +2 -0
  60. package/dist/languages/solidity.js +240 -0
  61. package/dist/languages/swift.d.ts +2 -0
  62. package/dist/languages/swift.js +268 -0
  63. package/dist/languages/types.d.ts +36 -0
  64. package/dist/languages/types.js +74 -0
  65. package/dist/languages/typescript-contracts.d.ts +57 -0
  66. package/dist/languages/typescript-contracts.js +528 -0
  67. package/dist/languages/typescript-dispatch.d.ts +68 -0
  68. package/dist/languages/typescript-dispatch.js +710 -0
  69. package/dist/languages/typescript.d.ts +4 -0
  70. package/dist/languages/typescript.js +722 -0
  71. package/dist/languages/zig.d.ts +2 -0
  72. package/dist/languages/zig.js +243 -0
  73. package/dist/loc.d.ts +17 -0
  74. package/dist/loc.js +34 -0
  75. package/dist/reach.d.ts +17 -0
  76. package/dist/reach.js +65 -0
  77. package/dist/render.d.ts +18 -0
  78. package/dist/render.js +83 -0
  79. package/dist/review/brand.d.ts +8 -0
  80. package/dist/review/brand.js +25 -0
  81. package/dist/review/call-context.d.ts +27 -0
  82. package/dist/review/call-context.js +446 -0
  83. package/dist/review/call-flow-html.d.ts +32 -0
  84. package/dist/review/call-flow-html.js +1870 -0
  85. package/dist/review/call-flow-nav.d.ts +151 -0
  86. package/dist/review/call-flow-nav.js +317 -0
  87. package/dist/review/call-flow.d.ts +47 -0
  88. package/dist/review/call-flow.js +229 -0
  89. package/dist/review/change-facts.d.ts +69 -0
  90. package/dist/review/change-facts.js +729 -0
  91. package/dist/review/cli.d.ts +2 -0
  92. package/dist/review/cli.js +50 -0
  93. package/dist/review/connected-analysis.d.ts +100 -0
  94. package/dist/review/connected-analysis.js +163 -0
  95. package/dist/review/connected-html.d.ts +17 -0
  96. package/dist/review/connected-html.js +2853 -0
  97. package/dist/review/connected.d.ts +23 -0
  98. package/dist/review/connected.js +141 -0
  99. package/dist/review/escape-html.d.ts +2 -0
  100. package/dist/review/escape-html.js +9 -0
  101. package/dist/review/evidence-html.d.ts +21 -0
  102. package/dist/review/evidence-html.js +521 -0
  103. package/dist/review/evidence-syntax.d.ts +132 -0
  104. package/dist/review/evidence-syntax.js +478 -0
  105. package/dist/review/evidence-types.d.ts +62 -0
  106. package/dist/review/evidence-types.js +1 -0
  107. package/dist/review/evidence.d.ts +31 -0
  108. package/dist/review/evidence.js +1603 -0
  109. package/dist/review/file-role.d.ts +9 -0
  110. package/dist/review/file-role.js +29 -0
  111. package/dist/review/github.d.ts +204 -0
  112. package/dist/review/github.js +1245 -0
  113. package/dist/review/history.d.ts +101 -0
  114. package/dist/review/history.js +412 -0
  115. package/dist/review/html.d.ts +34 -0
  116. package/dist/review/html.js +1104 -0
  117. package/dist/review/input.d.ts +10 -0
  118. package/dist/review/input.js +113 -0
  119. package/dist/review/intent.d.ts +4 -0
  120. package/dist/review/intent.js +75 -0
  121. package/dist/review/mcp-cli.d.ts +2 -0
  122. package/dist/review/mcp-cli.js +25 -0
  123. package/dist/review/mcp.d.ts +12 -0
  124. package/dist/review/mcp.js +414 -0
  125. package/dist/review/module-resolution.d.ts +2 -0
  126. package/dist/review/module-resolution.js +86 -0
  127. package/dist/review/palette.d.ts +7 -0
  128. package/dist/review/palette.js +104 -0
  129. package/dist/review/pipeline.d.ts +77 -0
  130. package/dist/review/pipeline.js +227 -0
  131. package/dist/review/pr-input.d.ts +19 -0
  132. package/dist/review/pr-input.js +130 -0
  133. package/dist/review/questions.d.ts +201 -0
  134. package/dist/review/questions.js +174 -0
  135. package/dist/review/reference-check.d.ts +7 -0
  136. package/dist/review/reference-check.js +733 -0
  137. package/dist/review/report-pages.d.ts +109 -0
  138. package/dist/review/report-pages.js +328 -0
  139. package/dist/review/service.d.ts +23 -0
  140. package/dist/review/service.js +198 -0
  141. package/dist/review/setup.d.ts +112 -0
  142. package/dist/review/setup.js +549 -0
  143. package/dist/review/source.d.ts +26 -0
  144. package/dist/review/source.js +276 -0
  145. package/dist/review/toml.d.ts +38 -0
  146. package/dist/review/toml.js +565 -0
  147. package/dist/review/types.d.ts +179 -0
  148. package/dist/review/types.js +1 -0
  149. package/dist/run.d.ts +49 -0
  150. package/dist/run.js +311 -0
  151. package/dist/types.d.ts +366 -0
  152. package/dist/types.js +83 -0
  153. package/package.json +88 -0
  154. package/scripts/ensure-native-grammar.mjs +188 -0
@@ -0,0 +1,565 @@
1
+ /**
2
+ * Byte-preserving TOML table edits for agent config files.
3
+ *
4
+ * The scanner walks the document statement by statement — skipping comments
5
+ * and every string form — so a table is addressed by its parsed key path
6
+ * rather than by matching text. Quoted keys, indented headers, and text that
7
+ * only looks like a header (inside a comment or a multiline string) therefore
8
+ * cannot misdirect an edit, and a table's whole subtree (nested tables and
9
+ * arrays of tables) is replaced or removed with it, wherever the document
10
+ * defines it: a header block, dotted keys, or an entry of an inline parent
11
+ * table. `smol-toml` parses the document before and after the edit: malformed
12
+ * input is reported instead of edited, and the result is only handed back when
13
+ * it parses and carries exactly the table the caller asked for.
14
+ */
15
+ import { parse, TomlError } from "smol-toml";
16
+ /**
17
+ * Insert, replace, or extend the table at `target`, keeping every other byte
18
+ * of the document. A header-form table is replaced by a `[target]` block; a
19
+ * table defined with dotted or inline keys is rewritten in place as an inline
20
+ * table so the surrounding table body stays valid.
21
+ */
22
+ export function upsertTomlTable(document, label, target, values) {
23
+ const parsed = parseDocument(document, label, "use");
24
+ const run = findRun(scanToml(document), target, label);
25
+ if (run === undefined && tableAt(parsed, target) !== undefined) {
26
+ throw new Error(`Cannot rewrite ${label}: ${pathText(target)} is defined inside another table. Remove it and re-run.`);
27
+ }
28
+ const section = block(target, values);
29
+ let text;
30
+ if (run === undefined) {
31
+ text = appendBlock(document, section);
32
+ }
33
+ else {
34
+ const replacement = run.statements[0].header ? section : inlineAssignment(document, run, target, values, label);
35
+ text = document.slice(0, run.start) + replacement + document.slice(run.end);
36
+ }
37
+ assertEdited(text, label, target, values);
38
+ return { text, changed: text !== document };
39
+ }
40
+ /**
41
+ * Remove the table at `target` and everything nested under it, wherever the
42
+ * document defines it: a `[target]` block with its subtree, dotted keys, or an
43
+ * entry of an inline parent table. Statements nested under `target` are
44
+ * removed even when unrelated tables sit between them. Every other byte of the
45
+ * document is kept.
46
+ */
47
+ export function removeTomlTable(document, label, target) {
48
+ parseDocument(document, label, "use");
49
+ const statements = scanToml(document);
50
+ const nested = statements.filter((statement) => underPath(statement.path, target));
51
+ let text;
52
+ if (nested.length > 0) {
53
+ text = removeStatements(document, nested);
54
+ }
55
+ else {
56
+ const inline = removeInlineParent(document, statements, target);
57
+ if (inline === undefined)
58
+ return { text: document, changed: false };
59
+ text = inline;
60
+ }
61
+ assertEdited(text, label, target, undefined);
62
+ return { text, changed: text !== document };
63
+ }
64
+ /**
65
+ * Statements in source order. `document` must already parse: the scanner
66
+ * locates spans, it does not validate syntax.
67
+ */
68
+ function scanToml(document) {
69
+ const statements = [];
70
+ let table = [];
71
+ let at = 0;
72
+ while (at < document.length) {
73
+ const char = document[at];
74
+ if (char === "\n" || char === "\r" || char === " " || char === "\t") {
75
+ at++;
76
+ continue;
77
+ }
78
+ if (char === "#") {
79
+ at = skipComment(document, at);
80
+ continue;
81
+ }
82
+ const start = at;
83
+ if (char === "[") {
84
+ const array = document[at + 1] === "[";
85
+ const key = readKeyPath(document, at + (array ? 2 : 1));
86
+ const close = skipSpaces(document, key.end);
87
+ const delimiter = array ? "]]" : "]";
88
+ if (!document.startsWith(delimiter, close))
89
+ throw malformed(document, close, `expected "${delimiter}"`);
90
+ table = key.path;
91
+ at = finishStatement(document, close + delimiter.length);
92
+ statements.push({ path: key.path, header: true, start, end: at, keySpans: key.spans });
93
+ continue;
94
+ }
95
+ const key = readKeyPath(document, at);
96
+ const equals = skipSpaces(document, key.end);
97
+ if (document[equals] !== "=")
98
+ throw malformed(document, equals, 'expected "=" after the key');
99
+ at = finishStatement(document, skipValue(document, skipSpaces(document, equals + 1)));
100
+ statements.push({ path: [...table, ...key.path], header: false, start, end: at, keySpans: key.spans });
101
+ }
102
+ return statements;
103
+ }
104
+ /** Dotted key path of one key, with the spans of its segments. */
105
+ function readKeyPath(document, index) {
106
+ const path = [];
107
+ const spans = [];
108
+ let at = skipSpaces(document, index);
109
+ for (;;) {
110
+ const start = at;
111
+ const quote = document[at];
112
+ if (quote === '"' || quote === "'") {
113
+ const end = readString(document, at);
114
+ path.push(quote === '"' ? decodeBasicString(document.slice(at + 1, end - 1)) : document.slice(at + 1, end - 1));
115
+ at = end;
116
+ }
117
+ else {
118
+ while (at < document.length && /[A-Za-z0-9_-]/.test(document[at]))
119
+ at++;
120
+ if (at === start)
121
+ throw malformed(document, start, "expected a key");
122
+ path.push(document.slice(start, at));
123
+ }
124
+ spans.push([start, at]);
125
+ const dot = skipSpaces(document, at);
126
+ if (document[dot] !== ".")
127
+ return { path, end: at, spans };
128
+ at = skipSpaces(document, dot + 1);
129
+ }
130
+ }
131
+ /** Offset just past the value that starts at `index`. */
132
+ function skipValue(document, index) {
133
+ const char = document[index];
134
+ if (char === '"' || char === "'")
135
+ return readString(document, index);
136
+ if (char === "[" || char === "{")
137
+ return skipContainer(document, index);
138
+ return skipBare(document, index);
139
+ }
140
+ /**
141
+ * Offset just past the bare value — a number, boolean, or date — that starts
142
+ * at `index`. A datetime may separate its date and time with a space, and the
143
+ * time that follows a date is part of the same value.
144
+ */
145
+ function skipBare(document, index) {
146
+ const end = nextBareEnd(document, index);
147
+ if (end === index)
148
+ throw malformed(document, index, "expected a value");
149
+ if (document[end] !== " " || !/^\d{4}-\d{2}-\d{2}$/.test(document.slice(index, end)))
150
+ return end;
151
+ const time = skipSpaces(document, end);
152
+ // A date on its own is a whole value, and a comment is not a time.
153
+ if (!/[0-9]/.test(document[time] ?? ""))
154
+ return end;
155
+ return nextBareEnd(document, time);
156
+ }
157
+ /** Offset of the next character that ends a bare value. */
158
+ function nextBareEnd(document, index) {
159
+ const ends = " \t\n\r#,]}";
160
+ let at = index;
161
+ while (at < document.length && !ends.includes(document[at]))
162
+ at++;
163
+ return at;
164
+ }
165
+ /** Offset just past the array or inline table that starts at `index`. */
166
+ function skipContainer(document, index) {
167
+ let depth = 0;
168
+ let at = index;
169
+ while (at < document.length) {
170
+ const char = document[at];
171
+ if (char === '"' || char === "'") {
172
+ at = readString(document, at);
173
+ continue;
174
+ }
175
+ if (char === "#") {
176
+ at = skipComment(document, at);
177
+ continue;
178
+ }
179
+ if (char === "[" || char === "{")
180
+ depth++;
181
+ else if (char === "]" || char === "}") {
182
+ depth--;
183
+ at++;
184
+ if (depth === 0)
185
+ return at;
186
+ continue;
187
+ }
188
+ at++;
189
+ }
190
+ throw malformed(document, index, "unterminated array or inline table");
191
+ }
192
+ /** Offset just past the string that starts at `index`, which holds a quote. */
193
+ function readString(document, index) {
194
+ const quote = document[index];
195
+ if (document.startsWith(quote.repeat(3), index)) {
196
+ let at = index + 3;
197
+ while (at < document.length) {
198
+ if (quote === '"' && document[at] === "\\") {
199
+ at += 2;
200
+ continue;
201
+ }
202
+ if (document[at] === quote) {
203
+ let run = 0;
204
+ while (document[at + run] === quote)
205
+ run++;
206
+ // A run of three or more quotes closes the string; at most two extra
207
+ // quotes belong to its content.
208
+ if (run >= 3)
209
+ return at + run;
210
+ at += run;
211
+ continue;
212
+ }
213
+ at++;
214
+ }
215
+ throw malformed(document, index, "unterminated multiline string");
216
+ }
217
+ let at = index + 1;
218
+ while (at < document.length && document[at] !== "\n") {
219
+ if (quote === '"' && document[at] === "\\") {
220
+ at += 2;
221
+ continue;
222
+ }
223
+ if (document[at] === quote)
224
+ return at + 1;
225
+ at++;
226
+ }
227
+ throw malformed(document, index, "unterminated string");
228
+ }
229
+ /**
230
+ * Offset just past the end of the line holding the statement, past a trailing
231
+ * comment. Anything else on the line is a syntax error.
232
+ */
233
+ function finishStatement(document, index) {
234
+ let at = skipSpaces(document, index);
235
+ if (document[at] === "#")
236
+ at = skipComment(document, at);
237
+ if (at >= document.length)
238
+ return at;
239
+ if (document[at] === "\n")
240
+ return at + 1;
241
+ if (document[at] === "\r" && document[at + 1] === "\n")
242
+ return at + 2;
243
+ throw malformed(document, at, "unexpected text after the statement");
244
+ }
245
+ /** Offset of the newline that ends the comment starting at `index`. */
246
+ function skipComment(document, index) {
247
+ const newline = document.indexOf("\n", index);
248
+ return newline === -1 ? document.length : newline;
249
+ }
250
+ /**
251
+ * Offset of the next key, comma, or closing bracket after `index`, past the
252
+ * spaces, newlines, and comments between them.
253
+ */
254
+ function skipTrivia(document, index) {
255
+ let at = index;
256
+ for (;;) {
257
+ const char = document[at];
258
+ if (char === " " || char === "\t" || char === "\n" || char === "\r")
259
+ at++;
260
+ else if (char === "#")
261
+ at = skipComment(document, at);
262
+ else
263
+ return at;
264
+ }
265
+ }
266
+ function skipSpaces(document, index) {
267
+ let at = index;
268
+ while (at < document.length && (document[at] === " " || document[at] === "\t"))
269
+ at++;
270
+ return at;
271
+ }
272
+ function decodeBasicString(raw) {
273
+ return raw.replace(/\\(u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8}|.)/gs, (_, escape) => {
274
+ const kind = escape[0];
275
+ if (kind === "u" || kind === "U")
276
+ return String.fromCodePoint(Number.parseInt(escape.slice(1), 16));
277
+ switch (kind) {
278
+ case "b":
279
+ return "\b";
280
+ case "t":
281
+ return "\t";
282
+ case "n":
283
+ return "\n";
284
+ case "f":
285
+ return "\f";
286
+ case "r":
287
+ return "\r";
288
+ default:
289
+ return kind;
290
+ }
291
+ });
292
+ }
293
+ function malformed(document, offset, reason) {
294
+ let line = 1;
295
+ for (let at = 0; at < offset && at < document.length; at++) {
296
+ if (document[at] === "\n")
297
+ line++;
298
+ }
299
+ return new Error(`${reason} (line ${line})`);
300
+ }
301
+ /** True when `path` addresses `prefix` itself or a table nested under it. */
302
+ function underPath(path, prefix) {
303
+ return path.length >= prefix.length && prefix.every((segment, index) => path[index] === segment);
304
+ }
305
+ /**
306
+ * The document without `statements`, each taking one blank line with it, in
307
+ * source order. Statements are cut one by one, so removing a table that the
308
+ * document splits across the file keeps everything between its parts.
309
+ */
310
+ function removeStatements(document, statements) {
311
+ const spans = statements.map((statement) => ({
312
+ start: swallowBlankLine(document, statement.start),
313
+ end: statement.end,
314
+ }));
315
+ const cut = applyCuts(document, spans);
316
+ // Blank lines left at the top of the file are a gap, not content.
317
+ return spans[0].start === 0 ? cut.replace(/^\n+/, "") : cut;
318
+ }
319
+ /** The document without each of `spans`, which must be disjoint and in order. */
320
+ function applyCuts(document, spans) {
321
+ let text = "";
322
+ let at = 0;
323
+ for (const span of spans) {
324
+ text += document.slice(at, span.start);
325
+ at = span.end;
326
+ }
327
+ return text + document.slice(at);
328
+ }
329
+ /** Statements of `target` and its subtree, which must sit next to each other. */
330
+ function findRun(statements, target, label) {
331
+ const matching = statements.filter((statement) => underPath(statement.path, target));
332
+ const first = matching[0];
333
+ const last = matching[matching.length - 1];
334
+ if (first === undefined || last === undefined)
335
+ return undefined;
336
+ if (statements.indexOf(last) - statements.indexOf(first) + 1 !== matching.length) {
337
+ throw new Error(`Cannot rewrite ${label}: ${pathText(target)} is split across the file. Edit it by hand.`);
338
+ }
339
+ return { statements: matching, start: first.start, end: last.end };
340
+ }
341
+ /**
342
+ * Rewrite a table that is defined by dotted keys as one inline-table
343
+ * assignment, under the key text the document already uses for the target.
344
+ */
345
+ function inlineAssignment(document, run, target, values, label) {
346
+ const head = run.statements[0];
347
+ if (run.statements.some((statement) => statement.header)) {
348
+ throw new Error(`Cannot rewrite ${label}: ${pathText(target)} mixes header and dotted keys. Edit it by hand.`);
349
+ }
350
+ // The statement's own key segments sit below its enclosing table headers;
351
+ // the target's last segment ends the key text this rewrite must reuse.
352
+ const key = head.keySpans[target.length - (head.path.length - head.keySpans.length) - 1];
353
+ if (key === undefined) {
354
+ throw new Error(`Cannot rewrite ${label}: ${pathText(target)} is not addressable as a key. Edit it by hand.`);
355
+ }
356
+ return `${document.slice(head.start, key[1])} = { ${assignments(values).join(", ")} }\n`;
357
+ }
358
+ /** `key = value` pairs, rendered in the order the table was built. */
359
+ function assignments(values) {
360
+ return Object.entries(values).map(([key, value]) => `${tomlKey(key)} = ${Array.isArray(value) ? `[${value.map(tomlQuote).join(", ")}]` : tomlQuote(value)}`);
361
+ }
362
+ /**
363
+ * Remove `target` from an inline parent table — `mcp_servers = { diffninja =
364
+ * { … }, other = { … } }` — when the document defines it as an entry of one.
365
+ * Returns `undefined` when no inline table holds it.
366
+ */
367
+ function removeInlineParent(document, statements, target) {
368
+ for (const statement of statements) {
369
+ // Only a statement whose own key path sits above the target can hold it.
370
+ if (statement.header || statement.path.length >= target.length)
371
+ continue;
372
+ if (!underPath(target, statement.path))
373
+ continue;
374
+ const last = statement.keySpans[statement.keySpans.length - 1];
375
+ const value = skipSpaces(document, skipSpaces(document, last[1]) + 1);
376
+ if (document[value] !== "{")
377
+ continue;
378
+ const cuts = removeInlineEntry(document, value, target.slice(statement.path.length));
379
+ if (cuts !== undefined)
380
+ return applyCuts(document, cuts);
381
+ }
382
+ return undefined;
383
+ }
384
+ /**
385
+ * Cuts that remove `rest` from the inline table whose `{` sits at `open`, or
386
+ * `undefined` when no entry of that table holds it. Sibling entries are kept.
387
+ */
388
+ function removeInlineEntry(document, open, rest) {
389
+ const { entries, close } = readInlineEntries(document, open);
390
+ const removals = entries.map((entry, index) => (underPath(entry.path, rest) ? index : -1)).filter((index) => index !== -1);
391
+ if (removals.length > 0)
392
+ return entryCuts({ start: open + 1, end: close - 1 }, entries, removals);
393
+ // No entry is the target itself; a key above it holds the entry to remove.
394
+ for (const entry of entries) {
395
+ if (!underPath(rest, entry.path) || entry.path.length >= rest.length)
396
+ continue;
397
+ if (document[entry.valueStart] !== "{")
398
+ continue;
399
+ const cuts = removeInlineEntry(document, entry.valueStart, rest.slice(entry.path.length));
400
+ if (cuts !== undefined)
401
+ return cuts;
402
+ }
403
+ return undefined;
404
+ }
405
+ /** Entries of the inline table whose `{` sits at `open`, and its `}` offset. */
406
+ function readInlineEntries(document, open) {
407
+ const entries = [];
408
+ const close = skipContainer(document, open);
409
+ let at = skipTrivia(document, open + 1);
410
+ while (at < close - 1) {
411
+ const start = at;
412
+ const key = readKeyPath(document, at);
413
+ const equals = skipSpaces(document, key.end);
414
+ if (document[equals] !== "=")
415
+ throw malformed(document, equals, 'expected "=" after the key');
416
+ const valueStart = skipSpaces(document, equals + 1);
417
+ const valueEnd = skipValue(document, valueStart);
418
+ const after = skipTrivia(document, valueEnd);
419
+ const comma = document[after] === ",";
420
+ entries.push({
421
+ path: key.path,
422
+ start,
423
+ valueStart,
424
+ valueEnd,
425
+ commaEnd: comma ? after + 1 : undefined,
426
+ });
427
+ at = skipTrivia(document, comma ? after + 1 : after);
428
+ }
429
+ return { entries, close };
430
+ }
431
+ /**
432
+ * Cuts that drop the entries at `removals` (indexes into `entries`, ascending)
433
+ * from one inline table, along with the separators they leave behind. `inner`
434
+ * is the text between the table's braces.
435
+ */
436
+ function entryCuts(inner, entries, removals) {
437
+ if (removals.length === entries.length)
438
+ return [inner];
439
+ const cuts = [];
440
+ for (let at = 0; at < removals.length;) {
441
+ const first = removals[at];
442
+ let last = first;
443
+ while (removals[at + 1] === last + 1)
444
+ last = removals[++at];
445
+ const next = entries[last + 1];
446
+ // An entry not at the end takes its comma with it; the last one takes the
447
+ // comma before it instead.
448
+ cuts.push(next === undefined
449
+ ? { start: entries[first - 1].valueEnd, end: entries[last].commaEnd ?? entries[last].valueEnd }
450
+ : { start: entries[first].start, end: next.start });
451
+ at++;
452
+ }
453
+ return cuts;
454
+ }
455
+ /** The `[target]` header and its pairs, ready to insert as a whole table. */
456
+ function block(target, values) {
457
+ return [`[${pathText(target)}]`, ...assignments(values)].join("\n") + "\n";
458
+ }
459
+ function appendBlock(document, section) {
460
+ if (document === "")
461
+ return section;
462
+ return `${document.endsWith("\n") ? document : `${document}\n`}\n${section}`;
463
+ }
464
+ /** Take the blank line before a removed statement with it, keeping the rest. */
465
+ function swallowBlankLine(document, start) {
466
+ return start >= 2 && document[start - 1] === "\n" && document[start - 2] === "\n" ? start - 1 : start;
467
+ }
468
+ /** `mcp_servers.diffninja`-style rendering of a key path. */
469
+ function pathText(path) {
470
+ return path.map(tomlKey).join(".");
471
+ }
472
+ /** A bare key when the segment allows it, else a quoted one. */
473
+ function tomlKey(key) {
474
+ return /^[A-Za-z0-9_-]+$/.test(key) ? key : tomlQuote(key);
475
+ }
476
+ /** Escape for a control character: the short form TOML names, else `\uXXXX`. */
477
+ function controlEscape(code) {
478
+ switch (code) {
479
+ case 0x08:
480
+ return "\\b";
481
+ case 0x09:
482
+ return "\\t";
483
+ case 0x0a:
484
+ return "\\n";
485
+ case 0x0c:
486
+ return "\\f";
487
+ case 0x0d:
488
+ return "\\r";
489
+ default:
490
+ return `\\u${code.toString(16).padStart(4, "0")}`;
491
+ }
492
+ }
493
+ /** A TOML basic string: quotes, backslashes, and control characters escaped. */
494
+ function tomlQuote(value) {
495
+ let body = "";
496
+ for (const char of value) {
497
+ const code = char.codePointAt(0);
498
+ if (char === "\\")
499
+ body += "\\\\";
500
+ else if (char === '"')
501
+ body += '\\"';
502
+ else if (code < 0x20 || code === 0x7f)
503
+ body += controlEscape(code);
504
+ else
505
+ body += char;
506
+ }
507
+ return `"${body}"`;
508
+ }
509
+ /** Reason a parse failed, with smol-toml's line and column when it has them. */
510
+ function describeTomlError(error) {
511
+ if (!(error instanceof TomlError))
512
+ return `: ${error.message}`;
513
+ const reason = error.message.replace(/^Invalid TOML document: /, "").split("\n")[0];
514
+ return ` at line ${error.line}, column ${error.column}: ${reason}`;
515
+ }
516
+ /** Parse a document into tables, arrays, and scalars, or report why not. */
517
+ function parseDocument(document, label, action) {
518
+ try {
519
+ return parse(document, { integersAsBigInt: false });
520
+ }
521
+ catch (error) {
522
+ // SAFETY: catch bindings are unknown; normalize to Error at the boundary.
523
+ const failure = error instanceof Error ? error : new Error(String(error));
524
+ throw new Error(`Cannot ${action} ${label}: not valid TOML${describeTomlError(failure)}`);
525
+ }
526
+ }
527
+ /**
528
+ * Parse the edited document and check the edit landed: the target table must
529
+ * be exactly `values`, or absent when removing. Nothing is written otherwise.
530
+ */
531
+ function assertEdited(document, label, target, values) {
532
+ const parsed = parseDocument(document, label, "write");
533
+ const actual = tableAt(parsed, target);
534
+ if (values === undefined) {
535
+ if (actual !== undefined)
536
+ throw new Error(`Cannot write ${label}: ${pathText(target)} is still defined.`);
537
+ return;
538
+ }
539
+ if (actual === undefined || stable(actual) !== stable(values)) {
540
+ throw new Error(`Cannot write ${label}: the edit does not produce the expected ${pathText(target)} table.`);
541
+ }
542
+ }
543
+ /** Objects and dates are the only TOML values that can hold subtables. */
544
+ function isTomlTable(node) {
545
+ return node instanceof Object && !Array.isArray(node);
546
+ }
547
+ function tableAt(root, path) {
548
+ let current = root;
549
+ for (const segment of path) {
550
+ if (!isTomlTable(current) || !Object.prototype.hasOwnProperty.call(current, segment))
551
+ return undefined;
552
+ current = current[segment];
553
+ }
554
+ return current;
555
+ }
556
+ /** JSON shape of a value, with object keys sorted so order does not matter. */
557
+ function stable(node) {
558
+ if (Array.isArray(node))
559
+ return `[${node.map(stable).join(",")}]`;
560
+ if (isTomlTable(node)) {
561
+ const entries = Object.entries(node).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0));
562
+ return `{${entries.map(([key, item]) => `${JSON.stringify(key)}:${stable(item)}`).join(",")}}`;
563
+ }
564
+ return JSON.stringify(node) ?? "null";
565
+ }