@dahch/muninn 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +260 -17
  2. package/dist/cli.d.ts +1 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +18 -2
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/memory.d.ts.map +1 -1
  7. package/dist/commands/memory.js +52 -3
  8. package/dist/commands/memory.js.map +1 -1
  9. package/dist/commands/setup.d.ts +3 -0
  10. package/dist/commands/setup.d.ts.map +1 -0
  11. package/dist/commands/setup.js +397 -0
  12. package/dist/commands/setup.js.map +1 -0
  13. package/dist/db/client.d.ts +17 -1
  14. package/dist/db/client.d.ts.map +1 -1
  15. package/dist/db/client.js +54 -0
  16. package/dist/db/client.js.map +1 -1
  17. package/dist/db/schema.sql +2 -0
  18. package/dist/mcp/server.d.ts +27 -6
  19. package/dist/mcp/server.d.ts.map +1 -1
  20. package/dist/mcp/server.js +156 -15
  21. package/dist/mcp/server.js.map +1 -1
  22. package/dist/service/memory-service.d.ts +121 -4
  23. package/dist/service/memory-service.d.ts.map +1 -1
  24. package/dist/service/memory-service.js +665 -45
  25. package/dist/service/memory-service.js.map +1 -1
  26. package/dist/setup/index.d.ts +6 -0
  27. package/dist/setup/index.d.ts.map +1 -0
  28. package/dist/setup/index.js +6 -0
  29. package/dist/setup/index.js.map +1 -0
  30. package/dist/setup/json-target.d.ts +30 -0
  31. package/dist/setup/json-target.d.ts.map +1 -0
  32. package/dist/setup/json-target.js +263 -0
  33. package/dist/setup/json-target.js.map +1 -0
  34. package/dist/setup/portable.d.ts +56 -0
  35. package/dist/setup/portable.d.ts.map +1 -0
  36. package/dist/setup/portable.js +365 -0
  37. package/dist/setup/portable.js.map +1 -0
  38. package/dist/setup/registry.d.ts +54 -0
  39. package/dist/setup/registry.d.ts.map +1 -0
  40. package/dist/setup/registry.js +225 -0
  41. package/dist/setup/registry.js.map +1 -0
  42. package/dist/setup/rules-target.d.ts +31 -0
  43. package/dist/setup/rules-target.d.ts.map +1 -0
  44. package/dist/setup/rules-target.js +270 -0
  45. package/dist/setup/rules-target.js.map +1 -0
  46. package/dist/setup/toml-target.d.ts +62 -0
  47. package/dist/setup/toml-target.d.ts.map +1 -0
  48. package/dist/setup/toml-target.js +869 -0
  49. package/dist/setup/toml-target.js.map +1 -0
  50. package/package.json +1 -1
@@ -0,0 +1,869 @@
1
+ /**
2
+ * Codex TOML MCP destination inspection and installation.
3
+ *
4
+ * Iteration 3 adds the Codex destination without a TOML runtime dependency. The
5
+ * parser below validates the complete document before any merge, keeps the
6
+ * parsed tables for classification, and edits only the Muninn table when a write
7
+ * is required. Unrelated tables, keys, comments, and formatting stay intact; a
8
+ * forced replacement may discard comments inside the replaced Muninn region. A
9
+ * correct entry is a byte-for-byte no-op. Writes reuse the same path screen,
10
+ * atomic temporary file, permission handling, and symlink refusal as the JSON
11
+ * destinations.
12
+ */
13
+ import fs from "node:fs";
14
+ import { checkDocPath, lstatOrUndefined } from "../util/path-safety.js";
15
+ import { canonicalAgentEntry } from "./json-target.js";
16
+ import { safeWriteAtomic } from "./portable.js";
17
+ class TomlTable {
18
+ kind = "table";
19
+ entries = new Map();
20
+ explicitHeader = false;
21
+ createdByDottedKey = false;
22
+ }
23
+ class TomlParseError extends Error {
24
+ }
25
+ function isTomlTable(value) {
26
+ return value instanceof TomlTable;
27
+ }
28
+ function isTomlArray(value) {
29
+ return typeof value === "object" && value !== null && value.kind === "array";
30
+ }
31
+ function isStringArray(value) {
32
+ return isTomlArray(value) && value.items.every((item) => typeof item === "string");
33
+ }
34
+ function pathEquals(a, b) {
35
+ return a.length === b.length && a.every((part, index) => part === b[index]);
36
+ }
37
+ function pathStartsWith(path, prefix) {
38
+ return (path.length >= prefix.length &&
39
+ prefix.every((part, index) => path[index] === part));
40
+ }
41
+ function neutralState() {
42
+ return { mode: "none", arrayDepth: 0, inlineDepth: 0 };
43
+ }
44
+ function isNeutral(state) {
45
+ return state.mode === "none" && state.arrayDepth === 0 && state.inlineDepth === 0;
46
+ }
47
+ /**
48
+ * Scan one physical line for TOML string / bracket state and return the part
49
+ * before an unquoted comment marker. The line does not include its newline.
50
+ */
51
+ function scanLine(line, state) {
52
+ let out = "";
53
+ let i = 0;
54
+ while (i < line.length) {
55
+ const ch = line[i];
56
+ if (state.mode === "basic") {
57
+ if (ch === "\\") {
58
+ out += ch;
59
+ if (i + 1 < line.length) {
60
+ out += line[i + 1];
61
+ i += 2;
62
+ }
63
+ else {
64
+ i++;
65
+ }
66
+ continue;
67
+ }
68
+ out += ch;
69
+ if (ch === '"')
70
+ state.mode = "none";
71
+ i++;
72
+ continue;
73
+ }
74
+ if (state.mode === "literal") {
75
+ out += ch;
76
+ if (ch === "'")
77
+ state.mode = "none";
78
+ i++;
79
+ continue;
80
+ }
81
+ if (state.mode === "multi-basic") {
82
+ if (line.startsWith('"""', i)) {
83
+ out += '"""';
84
+ state.mode = "none";
85
+ i += 3;
86
+ continue;
87
+ }
88
+ out += ch;
89
+ if (ch === "\\" && i + 1 < line.length) {
90
+ out += line[i + 1];
91
+ i += 2;
92
+ }
93
+ else {
94
+ i++;
95
+ }
96
+ continue;
97
+ }
98
+ if (state.mode === "multi-literal") {
99
+ if (line.startsWith("'''", i)) {
100
+ out += "'''";
101
+ state.mode = "none";
102
+ i += 3;
103
+ continue;
104
+ }
105
+ out += ch;
106
+ i++;
107
+ continue;
108
+ }
109
+ if (ch === "#")
110
+ break;
111
+ if (ch === '"') {
112
+ if (line.startsWith('"""', i)) {
113
+ out += '"""';
114
+ state.mode = "multi-basic";
115
+ i += 3;
116
+ }
117
+ else {
118
+ out += ch;
119
+ state.mode = "basic";
120
+ i++;
121
+ }
122
+ continue;
123
+ }
124
+ if (ch === "'") {
125
+ if (line.startsWith("'''", i)) {
126
+ out += "'''";
127
+ state.mode = "multi-literal";
128
+ i += 3;
129
+ }
130
+ else {
131
+ out += ch;
132
+ state.mode = "literal";
133
+ i++;
134
+ }
135
+ continue;
136
+ }
137
+ if (ch === "[")
138
+ state.arrayDepth++;
139
+ else if (ch === "]")
140
+ state.arrayDepth--;
141
+ else if (ch === "{")
142
+ state.inlineDepth++;
143
+ else if (ch === "}")
144
+ state.inlineDepth--;
145
+ out += ch;
146
+ i++;
147
+ }
148
+ return out;
149
+ }
150
+ function skipWhitespace(text, index) {
151
+ let i = index;
152
+ while (i < text.length && /[ \t\r\n]/.test(text[i]))
153
+ i++;
154
+ return i;
155
+ }
156
+ function decodeEscape(text, index) {
157
+ const marker = text[index];
158
+ const next = text[index + 1];
159
+ if (marker !== "\\")
160
+ throw new TomlParseError("internal escape parser error");
161
+ if (next === undefined)
162
+ throw new TomlParseError("unterminated escape sequence");
163
+ switch (next) {
164
+ case "b":
165
+ return { value: "\b", next: index + 2 };
166
+ case "t":
167
+ return { value: "\t", next: index + 2 };
168
+ case "n":
169
+ return { value: "\n", next: index + 2 };
170
+ case "f":
171
+ return { value: "\f", next: index + 2 };
172
+ case "r":
173
+ return { value: "\r", next: index + 2 };
174
+ case '"':
175
+ return { value: '"', next: index + 2 };
176
+ case "\\":
177
+ return { value: "\\", next: index + 2 };
178
+ case "u":
179
+ case "U": {
180
+ const length = next === "u" ? 4 : 8;
181
+ const hex = text.slice(index + 2, index + 2 + length);
182
+ if (hex.length !== length || !/^[0-9A-Fa-f]+$/.test(hex)) {
183
+ throw new TomlParseError(`invalid \\${next} escape`);
184
+ }
185
+ return { value: String.fromCodePoint(Number.parseInt(hex, 16)), next: index + 2 + length };
186
+ }
187
+ default:
188
+ throw new TomlParseError(`invalid escape sequence \\${next}`);
189
+ }
190
+ }
191
+ function parseBasicStringAt(text, index) {
192
+ const multiline = text.startsWith('"""', index);
193
+ let i = index + (multiline ? 3 : 1);
194
+ let value = "";
195
+ if (multiline && (text[i] === "\n" || text[i] === "\r")) {
196
+ if (text[i] === "\r" && text[i + 1] === "\n")
197
+ i += 2;
198
+ else
199
+ i++;
200
+ }
201
+ while (i < text.length) {
202
+ if (multiline && text.startsWith('"""', i)) {
203
+ return { value, next: i + 3 };
204
+ }
205
+ const ch = text[i];
206
+ if (!multiline && ch === '"')
207
+ return { value, next: i + 1 };
208
+ if (!multiline && (ch === "\n" || ch === "\r")) {
209
+ throw new TomlParseError("newline in a single-line string");
210
+ }
211
+ if (ch === "\\") {
212
+ const next = text[i + 1];
213
+ if (multiline && (next === "\n" || next === "\r" || next === " " || next === "\t")) {
214
+ i += 1;
215
+ while (i < text.length && /[ \t\r\n]/.test(text[i]))
216
+ i++;
217
+ continue;
218
+ }
219
+ const decoded = decodeEscape(text, i);
220
+ value += decoded.value;
221
+ i = decoded.next;
222
+ continue;
223
+ }
224
+ value += ch;
225
+ i++;
226
+ }
227
+ throw new TomlParseError("unterminated string");
228
+ }
229
+ function parseLiteralStringAt(text, index) {
230
+ const multiline = text.startsWith("'''", index);
231
+ let i = index + (multiline ? 3 : 1);
232
+ let value = "";
233
+ if (multiline && (text[i] === "\n" || text[i] === "\r")) {
234
+ if (text[i] === "\r" && text[i + 1] === "\n")
235
+ i += 2;
236
+ else
237
+ i++;
238
+ }
239
+ while (i < text.length) {
240
+ if (multiline && text.startsWith("'''", i)) {
241
+ return { value, next: i + 3 };
242
+ }
243
+ const ch = text[i];
244
+ if (!multiline && ch === "'")
245
+ return { value, next: i + 1 };
246
+ if (!multiline && (ch === "\n" || ch === "\r")) {
247
+ throw new TomlParseError("newline in a single-line string");
248
+ }
249
+ value += ch;
250
+ i++;
251
+ }
252
+ throw new TomlParseError("unterminated string");
253
+ }
254
+ function parseKeyPathAt(text, start) {
255
+ const path = [];
256
+ let i = start;
257
+ for (;;) {
258
+ i = skipWhitespace(text, i);
259
+ if (i >= text.length)
260
+ break;
261
+ const ch = text[i];
262
+ let part;
263
+ if (ch === '"') {
264
+ const parsed = parseBasicStringAt(text, i);
265
+ part = parsed.value;
266
+ i = parsed.next;
267
+ }
268
+ else if (ch === "'") {
269
+ const parsed = parseLiteralStringAt(text, i);
270
+ part = parsed.value;
271
+ i = parsed.next;
272
+ }
273
+ else {
274
+ const keyStart = i;
275
+ while (i < text.length && /[A-Za-z0-9_-]/.test(text[i]))
276
+ i++;
277
+ if (i === keyStart)
278
+ throw new TomlParseError("invalid key");
279
+ part = text.slice(keyStart, i);
280
+ }
281
+ path.push(part);
282
+ i = skipWhitespace(text, i);
283
+ if (i >= text.length)
284
+ break;
285
+ if (text[i] !== ".")
286
+ break;
287
+ i++;
288
+ }
289
+ if (path.length === 0)
290
+ throw new TomlParseError("empty key");
291
+ return { path, next: i };
292
+ }
293
+ const INTEGER_PATTERN = /^[+-]?(?:0|[1-9](?:_?\d)*)$/;
294
+ const HEX_PATTERN = /^0x[0-9A-Fa-f](?:_?[0-9A-Fa-f])*$/;
295
+ const OCTAL_PATTERN = /^0o[0-7](?:_?[0-7])*$/;
296
+ const BINARY_PATTERN = /^0b[01](?:_?[01])*$/;
297
+ const FLOAT_PATTERN = /^[+-]?(?:(?:0|[1-9](?:_?\d)*)(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?|\.\d(?:_?\d)*(?:[eE][+-]?\d(?:_?\d)*)?)$/;
298
+ const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
299
+ const TIME_PATTERN = /^\d{2}:\d{2}:\d{2}(?:\.\d+)?$/;
300
+ const DATETIME_PATTERN = /^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})?$/;
301
+ function parseBareValue(token) {
302
+ const value = token.trim();
303
+ if (value.length === 0)
304
+ throw new TomlParseError("missing value");
305
+ if (value === "true")
306
+ return true;
307
+ if (value === "false")
308
+ return false;
309
+ if (/^[+-]?(?:inf|nan)$/i.test(value))
310
+ return Number(value.replaceAll("_", ""));
311
+ if (INTEGER_PATTERN.test(value))
312
+ return Number(value.replaceAll("_", ""));
313
+ if (HEX_PATTERN.test(value))
314
+ return Number.parseInt(value.slice(2).replaceAll("_", ""), 16);
315
+ if (OCTAL_PATTERN.test(value))
316
+ return Number.parseInt(value.slice(2).replaceAll("_", ""), 8);
317
+ if (BINARY_PATTERN.test(value))
318
+ return Number.parseInt(value.slice(2).replaceAll("_", ""), 2);
319
+ if (FLOAT_PATTERN.test(value))
320
+ return Number(value.replaceAll("_", ""));
321
+ if (DATE_PATTERN.test(value) || TIME_PATTERN.test(value) || DATETIME_PATTERN.test(value)) {
322
+ return value;
323
+ }
324
+ throw new TomlParseError(`invalid value \`${value}\``);
325
+ }
326
+ function parseValueAt(text, start) {
327
+ let i = skipWhitespace(text, start);
328
+ if (i >= text.length)
329
+ throw new TomlParseError("missing value");
330
+ const ch = text[i];
331
+ if (ch === '"') {
332
+ const parsed = parseBasicStringAt(text, i);
333
+ return { value: parsed.value, next: parsed.next };
334
+ }
335
+ if (ch === "'") {
336
+ const parsed = parseLiteralStringAt(text, i);
337
+ return { value: parsed.value, next: parsed.next };
338
+ }
339
+ if (ch === "[")
340
+ return parseArrayAt(text, i);
341
+ if (ch === "{")
342
+ return parseInlineTableAt(text, i);
343
+ const valueStart = i;
344
+ while (i < text.length && !/[,\]}\r\n]/.test(text[i]))
345
+ i++;
346
+ return { value: parseBareValue(text.slice(valueStart, i)), next: i };
347
+ }
348
+ function parseArrayAt(text, start) {
349
+ let i = start + 1;
350
+ const items = [];
351
+ for (;;) {
352
+ i = skipWhitespace(text, i);
353
+ if (i >= text.length)
354
+ throw new TomlParseError("unterminated array");
355
+ if (text[i] === "]")
356
+ return { value: { kind: "array", items }, next: i + 1 };
357
+ const parsed = parseValueAt(text, i);
358
+ items.push(parsed.value);
359
+ i = skipWhitespace(text, parsed.next);
360
+ if (i >= text.length)
361
+ throw new TomlParseError("unterminated array");
362
+ if (text[i] === ",") {
363
+ i++;
364
+ continue;
365
+ }
366
+ if (text[i] === "]")
367
+ return { value: { kind: "array", items }, next: i + 1 };
368
+ throw new TomlParseError("expected `,` or `]` in array");
369
+ }
370
+ }
371
+ function assignValue(table, path, value) {
372
+ let current = table;
373
+ for (let index = 0; index < path.length - 1; index++) {
374
+ const key = path[index];
375
+ const existing = current.entries.get(key);
376
+ if (existing === undefined) {
377
+ const next = new TomlTable();
378
+ next.createdByDottedKey = true;
379
+ current.entries.set(key, next);
380
+ current = next;
381
+ }
382
+ else if (isTomlTable(existing)) {
383
+ current = existing;
384
+ }
385
+ else if (isTomlArray(existing)) {
386
+ const last = existing.items[existing.items.length - 1];
387
+ if (!isTomlTable(last))
388
+ throw new TomlParseError(`\`${key}\` is not a table`);
389
+ current = last;
390
+ }
391
+ else {
392
+ throw new TomlParseError(`key \`${key}\` is already defined as a value`);
393
+ }
394
+ }
395
+ const finalKey = path[path.length - 1];
396
+ if (current.entries.has(finalKey)) {
397
+ throw new TomlParseError(`duplicate key \`${path.join(".")}\``);
398
+ }
399
+ current.entries.set(finalKey, value);
400
+ }
401
+ function parseInlineTableAt(text, start) {
402
+ const table = new TomlTable();
403
+ let i = start + 1;
404
+ for (;;) {
405
+ i = skipWhitespace(text, i);
406
+ if (i >= text.length)
407
+ throw new TomlParseError("unterminated inline table");
408
+ if (text[i] === "}")
409
+ return { value: table, next: i + 1 };
410
+ const key = parseKeyPathAt(text, i);
411
+ i = skipWhitespace(text, key.next);
412
+ if (text[i] !== "=")
413
+ throw new TomlParseError("expected `=` in inline table");
414
+ i = skipWhitespace(text, i + 1);
415
+ const value = parseValueAt(text, i);
416
+ assignValue(table, key.path, value.value);
417
+ i = skipWhitespace(text, value.next);
418
+ if (i >= text.length)
419
+ throw new TomlParseError("unterminated inline table");
420
+ if (text[i] === ",") {
421
+ i++;
422
+ continue;
423
+ }
424
+ if (text[i] === "}")
425
+ return { value: table, next: i + 1 };
426
+ throw new TomlParseError("expected `,` or `}` in inline table");
427
+ }
428
+ }
429
+ function parseHeaderContent(text) {
430
+ const trimmed = text.trim();
431
+ if (!trimmed.startsWith("["))
432
+ throw new TomlParseError("invalid table header");
433
+ const array = trimmed.startsWith("[[");
434
+ const closing = array ? "]]" : "]";
435
+ if (!trimmed.endsWith(closing))
436
+ throw new TomlParseError("unterminated table header");
437
+ const inner = trimmed.slice(array ? 2 : 1, trimmed.length - closing.length);
438
+ const parsed = parseKeyPathAt(inner, 0);
439
+ if (skipWhitespace(inner, parsed.next) !== inner.length) {
440
+ throw new TomlParseError("unexpected content after table header");
441
+ }
442
+ if (parsed.path.length === 0)
443
+ throw new TomlParseError("empty table header");
444
+ return { path: parsed.path, array };
445
+ }
446
+ function parseKeyValueContent(text) {
447
+ const key = parseKeyPathAt(text, 0);
448
+ let i = skipWhitespace(text, key.next);
449
+ if (text[i] !== "=")
450
+ throw new TomlParseError("expected `=` after key");
451
+ i = skipWhitespace(text, i + 1);
452
+ const value = parseValueAt(text, i);
453
+ if (skipWhitespace(text, value.next) !== text.length) {
454
+ throw new TomlParseError("unexpected content after value");
455
+ }
456
+ return { keyPath: key.path, value: value.value };
457
+ }
458
+ function descendHeader(root, path, array) {
459
+ let current = root;
460
+ for (let index = 0; index < path.length; index++) {
461
+ const key = path[index];
462
+ const last = index === path.length - 1;
463
+ const existing = current.entries.get(key);
464
+ if (last && array) {
465
+ let values;
466
+ if (existing === undefined) {
467
+ values = { kind: "array", items: [] };
468
+ current.entries.set(key, values);
469
+ }
470
+ else if (isTomlArray(existing)) {
471
+ values = existing;
472
+ }
473
+ else {
474
+ throw new TomlParseError(`table \`${path.join(".")}\` conflicts with an existing value`);
475
+ }
476
+ const table = new TomlTable();
477
+ table.explicitHeader = true;
478
+ values.items.push(table);
479
+ return table;
480
+ }
481
+ let next;
482
+ if (existing === undefined) {
483
+ next = new TomlTable();
484
+ current.entries.set(key, next);
485
+ }
486
+ else if (isTomlTable(existing)) {
487
+ next = existing;
488
+ }
489
+ else if (isTomlArray(existing)) {
490
+ const lastItem = existing.items[existing.items.length - 1];
491
+ if (!isTomlTable(lastItem)) {
492
+ throw new TomlParseError(`table \`${path.join(".")}\` conflicts with an existing value`);
493
+ }
494
+ next = lastItem;
495
+ }
496
+ else {
497
+ throw new TomlParseError(`table \`${path.join(".")}\` conflicts with an existing value`);
498
+ }
499
+ if (last) {
500
+ if (next.explicitHeader) {
501
+ throw new TomlParseError(`duplicate table header \`${path.join(".")}\``);
502
+ }
503
+ if (next.createdByDottedKey) {
504
+ throw new TomlParseError(`table \`${path.join(".")}\` is already defined by dotted keys`);
505
+ }
506
+ next.explicitHeader = true;
507
+ }
508
+ current = next;
509
+ }
510
+ return current;
511
+ }
512
+ /** Validate a complete TOML document and retain the structural statement map. */
513
+ export function parseToml(raw) {
514
+ const root = new TomlTable();
515
+ const statements = [];
516
+ let currentTable = root;
517
+ let currentTablePath = [];
518
+ let pending;
519
+ let position = 0;
520
+ while (position < raw.length) {
521
+ const newline = raw.indexOf("\n", position);
522
+ const lineEnd = newline === -1 ? raw.length : newline;
523
+ const line = raw.slice(position, lineEnd);
524
+ const nextPosition = newline === -1 ? raw.length : newline + 1;
525
+ if (pending === undefined) {
526
+ const trimmed = line.trimStart();
527
+ if (trimmed.length > 0 && !trimmed.startsWith("#")) {
528
+ if (trimmed.startsWith("[")) {
529
+ const content = scanLine(line, neutralState());
530
+ const header = parseHeaderContent(content);
531
+ currentTable = descendHeader(root, header.path, header.array);
532
+ currentTablePath = header.path;
533
+ statements.push({
534
+ kind: "header",
535
+ path: header.path,
536
+ array: header.array,
537
+ start: position,
538
+ end: nextPosition,
539
+ });
540
+ }
541
+ else {
542
+ const state = neutralState();
543
+ const content = scanLine(line, state);
544
+ pending = { start: position, content, state };
545
+ if (!isNeutral(state)) {
546
+ position = nextPosition;
547
+ continue;
548
+ }
549
+ const parsed = parseKeyValueContent(pending.content);
550
+ assignValue(currentTable, parsed.keyPath, parsed.value);
551
+ statements.push({
552
+ kind: "keyValue",
553
+ tablePath: currentTablePath,
554
+ keyPath: parsed.keyPath,
555
+ fullPath: [...currentTablePath, ...parsed.keyPath],
556
+ value: parsed.value,
557
+ start: pending.start,
558
+ end: nextPosition,
559
+ });
560
+ pending = undefined;
561
+ }
562
+ }
563
+ }
564
+ else {
565
+ const content = scanLine(line, pending.state);
566
+ pending.content += `\n${content}`;
567
+ if (isNeutral(pending.state)) {
568
+ const parsed = parseKeyValueContent(pending.content);
569
+ assignValue(currentTable, parsed.keyPath, parsed.value);
570
+ statements.push({
571
+ kind: "keyValue",
572
+ tablePath: currentTablePath,
573
+ keyPath: parsed.keyPath,
574
+ fullPath: [...currentTablePath, ...parsed.keyPath],
575
+ value: parsed.value,
576
+ start: pending.start,
577
+ end: nextPosition,
578
+ });
579
+ pending = undefined;
580
+ }
581
+ }
582
+ position = nextPosition;
583
+ }
584
+ if (pending !== undefined)
585
+ throw new TomlParseError("unterminated value");
586
+ return { root, statements };
587
+ }
588
+ function desiredEntry(options) {
589
+ const entry = canonicalAgentEntry(options.target, options.path, options.projectPath);
590
+ const command = entry.command;
591
+ const args = entry.args;
592
+ if (typeof command !== "string" || !Array.isArray(args) || !args.every((arg) => typeof arg === "string")) {
593
+ throw new Error(`cannot build a TOML entry for ${options.target.id}`);
594
+ }
595
+ return { command, args };
596
+ }
597
+ function sameStringArray(value, expected) {
598
+ if (!isStringArray(value))
599
+ return false;
600
+ return (value.items.length === expected.length &&
601
+ value.items.every((item, index) => item === expected[index]));
602
+ }
603
+ function classify(root, desired) {
604
+ const servers = root.entries.get("mcp_servers");
605
+ if (servers === undefined)
606
+ return { state: "absent" };
607
+ if (!isTomlTable(servers)) {
608
+ return { state: "malformed", error: "`mcp_servers` must be a table" };
609
+ }
610
+ const entry = servers.entries.get("muninn");
611
+ if (entry === undefined)
612
+ return { state: "absent" };
613
+ if (!isTomlTable(entry)) {
614
+ return { state: "malformed", error: "`mcp_servers.muninn` must be a table" };
615
+ }
616
+ const command = entry.entries.get("command");
617
+ const args = entry.entries.get("args");
618
+ if (command !== undefined && typeof command !== "string") {
619
+ return { state: "malformed", error: "`mcp_servers.muninn.command` must be a string" };
620
+ }
621
+ if (args !== undefined && !isStringArray(args)) {
622
+ return {
623
+ state: "malformed",
624
+ error: "`mcp_servers.muninn.args` must be an array of strings",
625
+ };
626
+ }
627
+ const canonical = entry.entries.size === 2 &&
628
+ entry.entries.has("command") &&
629
+ entry.entries.has("args") &&
630
+ command === desired.command &&
631
+ sameStringArray(args, desired.args);
632
+ if (canonical)
633
+ return { state: "registered" };
634
+ if (command === "huginn")
635
+ return { state: "legacy" };
636
+ return { state: "conflict" };
637
+ }
638
+ function inspect(options) {
639
+ const desired = desiredEntry(options);
640
+ const label = `${options.target.id} MCP configuration`;
641
+ const errors = [];
642
+ const check = checkDocPath(options.path, {
643
+ label,
644
+ action: "read or write",
645
+ projectPath: options.projectPath,
646
+ });
647
+ if (!check.ok) {
648
+ return {
649
+ target: options.target,
650
+ path: options.path,
651
+ state: "malformed",
652
+ desired,
653
+ error: check.reason,
654
+ errors: [check.reason],
655
+ };
656
+ }
657
+ const stat = lstatOrUndefined(check.path);
658
+ if (stat === undefined) {
659
+ return {
660
+ target: options.target,
661
+ path: check.path,
662
+ state: "absent",
663
+ desired,
664
+ errors,
665
+ };
666
+ }
667
+ if (!stat.isFile()) {
668
+ const error = `${label} ${check.path} is not a regular file`;
669
+ return {
670
+ target: options.target,
671
+ path: check.path,
672
+ state: "malformed",
673
+ desired,
674
+ error,
675
+ errors: [...errors, error],
676
+ };
677
+ }
678
+ let raw;
679
+ try {
680
+ raw = fs.readFileSync(check.path, "utf8");
681
+ }
682
+ catch (err) {
683
+ const error = `Cannot read ${label} ${check.path}: ${err.message}`;
684
+ return {
685
+ target: options.target,
686
+ path: check.path,
687
+ state: "malformed",
688
+ desired,
689
+ error,
690
+ errors: [...errors, error],
691
+ };
692
+ }
693
+ let parsed;
694
+ try {
695
+ parsed = parseToml(raw);
696
+ }
697
+ catch (err) {
698
+ const message = err instanceof Error ? err.message : String(err);
699
+ const error = `Malformed TOML in ${check.path}: ${message}`;
700
+ return {
701
+ target: options.target,
702
+ path: check.path,
703
+ state: "malformed",
704
+ desired,
705
+ raw,
706
+ error,
707
+ errors: [...errors, error],
708
+ };
709
+ }
710
+ const classified = classify(parsed.root, desired);
711
+ if (classified.state === "malformed") {
712
+ const error = `Malformed ${label} ${check.path}: ${classified.error}`;
713
+ return {
714
+ target: options.target,
715
+ path: check.path,
716
+ state: "malformed",
717
+ desired,
718
+ raw,
719
+ parsed,
720
+ error,
721
+ errors: [...errors, error],
722
+ };
723
+ }
724
+ return {
725
+ target: options.target,
726
+ path: check.path,
727
+ state: classified.state,
728
+ desired,
729
+ raw,
730
+ parsed,
731
+ errors,
732
+ };
733
+ }
734
+ function tomlString(value) {
735
+ return JSON.stringify(value);
736
+ }
737
+ function renderMuninnTable(desired) {
738
+ return (`[mcp_servers.muninn]\n` +
739
+ `command = ${tomlString(desired.command)}\n` +
740
+ `args = [${desired.args.map(tomlString).join(", ")}]\n`);
741
+ }
742
+ function appendTable(raw, block) {
743
+ if (raw.length === 0)
744
+ return block;
745
+ let prefix = raw;
746
+ if (!prefix.endsWith("\n"))
747
+ prefix += "\n";
748
+ if (!prefix.endsWith("\n\n"))
749
+ prefix += "\n";
750
+ return prefix + block;
751
+ }
752
+ function mergeToml(raw, parsed, desired) {
753
+ const block = renderMuninnTable(desired);
754
+ if (raw === undefined)
755
+ return block;
756
+ const statements = parsed.statements;
757
+ const explicitHeader = statements.find((statement) => statement.kind === "header" &&
758
+ !statement.array &&
759
+ pathEquals(statement.path, ["mcp_servers", "muninn"]));
760
+ if (explicitHeader !== undefined) {
761
+ const headerIndex = statements.indexOf(explicitHeader);
762
+ const earlierMuninn = statements.slice(0, headerIndex).find((statement) => statement.kind === "keyValue" &&
763
+ pathStartsWith(statement.fullPath, ["mcp_servers", "muninn"]));
764
+ if (earlierMuninn !== undefined) {
765
+ throw new TomlParseError("cannot replace `mcp_servers.muninn` because it is also defined by earlier dotted keys");
766
+ }
767
+ const nextHeader = statements
768
+ .slice(headerIndex + 1)
769
+ .find((statement) => statement.kind === "header");
770
+ const end = nextHeader?.start ?? raw.length;
771
+ const before = raw.slice(0, explicitHeader.start);
772
+ const after = raw.slice(end);
773
+ return before + block + (after.length > 0 ? "\n" + after : "");
774
+ }
775
+ const servers = parsed.root.entries.get("mcp_servers");
776
+ if (isTomlArray(servers)) {
777
+ throw new TomlParseError("cannot extend an array-of-tables `mcp_servers` entry");
778
+ }
779
+ if (isTomlTable(servers) && !servers.explicitHeader) {
780
+ const inline = statements.find((statement) => statement.kind === "keyValue" &&
781
+ pathEquals(statement.fullPath, ["mcp_servers"]));
782
+ if (inline !== undefined) {
783
+ throw new TomlParseError("cannot extend an inline `mcp_servers` table");
784
+ }
785
+ }
786
+ const arrayHeader = statements.find((statement) => statement.kind === "header" &&
787
+ statement.array &&
788
+ pathStartsWith(statement.path, ["mcp_servers", "muninn"]));
789
+ if (arrayHeader !== undefined) {
790
+ throw new TomlParseError("cannot replace an array-of-tables `mcp_servers.muninn` entry");
791
+ }
792
+ const removals = statements.filter((statement) => statement.kind === "keyValue" &&
793
+ pathStartsWith(statement.fullPath, ["mcp_servers", "muninn"]));
794
+ let base = raw;
795
+ for (const statement of [...removals].sort((a, b) => b.start - a.start)) {
796
+ base = base.slice(0, statement.start) + base.slice(statement.end);
797
+ }
798
+ return appendTable(base, block);
799
+ }
800
+ /** Read-only diagnostic used by `muninn setup --status`. */
801
+ export function getTomlTargetStatus(options) {
802
+ const inspected = inspect(options);
803
+ return {
804
+ target: options.target,
805
+ path: inspected.path,
806
+ state: inspected.state,
807
+ changed: false,
808
+ error: inspected.error,
809
+ errors: inspected.errors,
810
+ };
811
+ }
812
+ export function setupTomlTarget(options) {
813
+ const inspected = inspect(options);
814
+ const dryRun = options.dryRun === true;
815
+ const base = {
816
+ target: inspected.target,
817
+ path: inspected.path,
818
+ state: inspected.state,
819
+ dryRun,
820
+ errors: inspected.errors,
821
+ };
822
+ if (inspected.state === "malformed") {
823
+ return { ...base, outcome: "failed", changed: false, error: inspected.error };
824
+ }
825
+ if (inspected.state === "registered") {
826
+ return { ...base, outcome: "present", changed: false };
827
+ }
828
+ if ((inspected.state === "conflict" || inspected.state === "legacy") && options.force !== true) {
829
+ return { ...base, outcome: "skipped", changed: false };
830
+ }
831
+ let content;
832
+ try {
833
+ content = mergeToml(inspected.raw, inspected.parsed, inspected.desired);
834
+ }
835
+ catch (err) {
836
+ const error = `Cannot merge ${inspected.target.id} MCP configuration ${inspected.path}: ${err.message}`;
837
+ return { ...base, outcome: "failed", changed: false, error };
838
+ }
839
+ if (dryRun) {
840
+ return { ...base, outcome: "written", changed: true };
841
+ }
842
+ const check = checkDocPath(inspected.path, {
843
+ label: `${inspected.target.id} MCP configuration`,
844
+ action: "write",
845
+ projectPath: options.projectPath,
846
+ });
847
+ if (!check.ok) {
848
+ return { ...base, outcome: "failed", changed: false, error: check.reason };
849
+ }
850
+ const existing = lstatOrUndefined(check.path);
851
+ if (existing?.isSymbolicLink()) {
852
+ const error = `${inspected.target.id} MCP configuration ${check.path} is a symlink; refusing to write through it`;
853
+ return { ...base, outcome: "failed", changed: false, error };
854
+ }
855
+ if (existing !== undefined && !existing.isFile()) {
856
+ const error = `${inspected.target.id} MCP configuration ${check.path} is not a regular file`;
857
+ return { ...base, outcome: "failed", changed: false, error };
858
+ }
859
+ const mode = existing === undefined ? 0o600 : existing.mode & 0o777;
860
+ try {
861
+ safeWriteAtomic(check.path, content, mode);
862
+ }
863
+ catch (err) {
864
+ const error = `Cannot write ${inspected.target.id} MCP configuration ${check.path}: ${err.message}`;
865
+ return { ...base, outcome: "failed", changed: false, error };
866
+ }
867
+ return { ...base, outcome: "written", changed: true };
868
+ }
869
+ //# sourceMappingURL=toml-target.js.map