context-mode 1.0.161 → 1.0.163

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 (153) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.openclaw-plugin/openclaw.plugin.json +1 -1
  5. package/.openclaw-plugin/package.json +1 -1
  6. package/README.md +142 -28
  7. package/bin/statusline.mjs +24 -4
  8. package/build/adapters/antigravity/index.d.ts +1 -1
  9. package/build/adapters/antigravity-cli/index.d.ts +51 -0
  10. package/build/adapters/antigravity-cli/index.js +341 -0
  11. package/build/adapters/claude-code/hooks.d.ts +1 -0
  12. package/build/adapters/claude-code/hooks.js +3 -0
  13. package/build/adapters/claude-code/index.js +24 -5
  14. package/build/adapters/client-map.js +5 -0
  15. package/build/adapters/codex/hooks.d.ts +5 -1
  16. package/build/adapters/codex/hooks.js +5 -1
  17. package/build/adapters/codex/index.d.ts +9 -1
  18. package/build/adapters/codex/index.js +87 -5
  19. package/build/adapters/copilot-cli/hooks.d.ts +33 -0
  20. package/build/adapters/copilot-cli/hooks.js +64 -0
  21. package/build/adapters/copilot-cli/index.d.ts +48 -0
  22. package/build/adapters/copilot-cli/index.js +341 -0
  23. package/build/adapters/detect.d.ts +1 -1
  24. package/build/adapters/detect.js +71 -3
  25. package/build/adapters/openclaw/mcp-tools.js +1 -1
  26. package/build/adapters/opencode/index.js +31 -17
  27. package/build/adapters/opencode/zod3tov4.js +27 -6
  28. package/build/adapters/pi/extension.d.ts +2 -12
  29. package/build/adapters/pi/extension.js +114 -96
  30. package/build/adapters/types.d.ts +5 -4
  31. package/build/adapters/types.js +4 -3
  32. package/build/cache-heal.d.ts +48 -0
  33. package/build/cache-heal.js +150 -0
  34. package/build/cli.js +37 -97
  35. package/build/executor.d.ts +25 -0
  36. package/build/executor.js +143 -22
  37. package/build/opencode-plugin.js +5 -2
  38. package/build/routing-block.d.ts +8 -0
  39. package/build/routing-block.js +86 -0
  40. package/build/runtime.d.ts +0 -36
  41. package/build/runtime.js +107 -27
  42. package/build/search/flood-guard.d.ts +57 -0
  43. package/build/search/flood-guard.js +80 -0
  44. package/build/security.d.ts +8 -3
  45. package/build/security.js +155 -29
  46. package/build/server.d.ts +14 -0
  47. package/build/server.js +368 -350
  48. package/build/session/analytics.d.ts +8 -8
  49. package/build/session/analytics.js +18 -13
  50. package/build/session/db.d.ts +1 -0
  51. package/build/session/db.js +37 -4
  52. package/build/session/extract.d.ts +46 -0
  53. package/build/session/extract.js +764 -13
  54. package/build/session/project-attribution.js +14 -0
  55. package/build/store.d.ts +1 -1
  56. package/build/store.js +139 -25
  57. package/build/tool-naming.d.ts +4 -0
  58. package/build/tool-naming.js +24 -0
  59. package/build/util/jsonc.d.ts +14 -0
  60. package/build/util/jsonc.js +104 -0
  61. package/cli.bundle.mjs +260 -254
  62. package/configs/antigravity/GEMINI.md +2 -2
  63. package/configs/antigravity-cli/hooks/hooks.json +37 -0
  64. package/configs/antigravity-cli/hooks.json +37 -0
  65. package/configs/antigravity-cli/mcp_config.json +10 -0
  66. package/configs/antigravity-cli/plugin.json +14 -0
  67. package/configs/antigravity-cli/rules/context-mode.md +77 -0
  68. package/configs/antigravity-cli/skills/context-mode/SKILL.md +77 -0
  69. package/configs/claude-code/CLAUDE.md +2 -2
  70. package/configs/codex/AGENTS.md +2 -2
  71. package/configs/copilot-cli/.github/plugin/plugin.json +23 -0
  72. package/configs/copilot-cli/.mcp.json +12 -0
  73. package/configs/copilot-cli/README.md +47 -0
  74. package/configs/copilot-cli/hooks.json +41 -0
  75. package/configs/copilot-cli/skills/context-mode/SKILL.md +38 -0
  76. package/configs/gemini-cli/GEMINI.md +2 -2
  77. package/configs/jetbrains-copilot/copilot-instructions.md +2 -2
  78. package/configs/kilo/AGENTS.md +2 -2
  79. package/configs/kiro/KIRO.md +2 -2
  80. package/configs/omp/SYSTEM.md +2 -2
  81. package/configs/openclaw/AGENTS.md +2 -2
  82. package/configs/opencode/AGENTS.md +2 -2
  83. package/configs/qwen-code/QWEN.md +2 -2
  84. package/configs/vscode-copilot/copilot-instructions.md +2 -2
  85. package/configs/zed/AGENTS.md +2 -2
  86. package/hooks/antigravity-cli/payload.mjs +98 -0
  87. package/hooks/antigravity-cli/posttooluse.mjs +138 -0
  88. package/hooks/antigravity-cli/pretooluse.mjs +78 -0
  89. package/hooks/antigravity-cli/stop.mjs +58 -0
  90. package/hooks/codex/pretooluse.mjs +14 -4
  91. package/hooks/codex/stop.mjs +12 -4
  92. package/hooks/copilot-cli/posttooluse.mjs +79 -0
  93. package/hooks/copilot-cli/precompact.mjs +66 -0
  94. package/hooks/copilot-cli/pretooluse.mjs +41 -0
  95. package/hooks/copilot-cli/sessionstart.mjs +121 -0
  96. package/hooks/copilot-cli/stop.mjs +59 -0
  97. package/hooks/copilot-cli/userpromptsubmit.mjs +77 -0
  98. package/hooks/core/codex-caps.mjs +112 -0
  99. package/hooks/core/formatters.mjs +158 -7
  100. package/hooks/core/mcp-ready.mjs +37 -8
  101. package/hooks/core/routing.mjs +94 -8
  102. package/hooks/core/tool-naming.mjs +3 -0
  103. package/hooks/hooks.json +12 -1
  104. package/hooks/pretooluse.mjs +6 -2
  105. package/hooks/routing-block.mjs +2 -2
  106. package/hooks/security.bundle.mjs +2 -1
  107. package/hooks/session-db.bundle.mjs +11 -7
  108. package/hooks/session-directive.mjs +88 -20
  109. package/hooks/session-extract.bundle.mjs +2 -2
  110. package/hooks/session-helpers.mjs +21 -0
  111. package/hooks/session-loaders.mjs +8 -5
  112. package/hooks/sessionstart.mjs +53 -7
  113. package/hooks/stop.mjs +49 -0
  114. package/hooks/userpromptsubmit.mjs +9 -2
  115. package/openclaw.plugin.json +1 -1
  116. package/package.json +4 -10
  117. package/scripts/install-antigravity-cli-plugin.mjs +141 -0
  118. package/server.bundle.mjs +214 -205
  119. package/skills/ctx-insight/SKILL.md +12 -17
  120. package/build/util/db-lock.d.ts +0 -65
  121. package/build/util/db-lock.js +0 -166
  122. package/insight/index.html +0 -13
  123. package/insight/package.json +0 -55
  124. package/insight/server.mjs +0 -1265
  125. package/insight/src/components/analytics.tsx +0 -112
  126. package/insight/src/components/ui/badge.tsx +0 -52
  127. package/insight/src/components/ui/button.tsx +0 -58
  128. package/insight/src/components/ui/card.tsx +0 -103
  129. package/insight/src/components/ui/chart.tsx +0 -371
  130. package/insight/src/components/ui/collapsible.tsx +0 -19
  131. package/insight/src/components/ui/input.tsx +0 -20
  132. package/insight/src/components/ui/progress.tsx +0 -83
  133. package/insight/src/components/ui/scroll-area.tsx +0 -55
  134. package/insight/src/components/ui/separator.tsx +0 -23
  135. package/insight/src/components/ui/table.tsx +0 -114
  136. package/insight/src/components/ui/tabs.tsx +0 -82
  137. package/insight/src/components/ui/tooltip.tsx +0 -64
  138. package/insight/src/lib/api.ts +0 -144
  139. package/insight/src/lib/utils.ts +0 -6
  140. package/insight/src/main.tsx +0 -22
  141. package/insight/src/routeTree.gen.ts +0 -189
  142. package/insight/src/router.tsx +0 -19
  143. package/insight/src/routes/__root.tsx +0 -55
  144. package/insight/src/routes/enterprise.tsx +0 -316
  145. package/insight/src/routes/index.tsx +0 -1482
  146. package/insight/src/routes/knowledge.tsx +0 -221
  147. package/insight/src/routes/knowledge_.$dbHash.$sourceId.tsx +0 -137
  148. package/insight/src/routes/search.tsx +0 -97
  149. package/insight/src/routes/sessions.tsx +0 -179
  150. package/insight/src/routes/sessions_.$dbHash.$sessionId.tsx +0 -181
  151. package/insight/src/styles.css +0 -104
  152. package/insight/tsconfig.json +0 -29
  153. package/insight/vite.config.ts +0 -19
@@ -194,14 +194,28 @@ export function resolveProjectAttribution(event, context) {
194
194
  export function resolveProjectAttributions(events, context) {
195
195
  const out = [];
196
196
  let lastKnown = context.lastKnownProjectDir ? normalizePath(context.lastKnownProjectDir) : "";
197
+ // v1.0.162 Bug 8 — track whether an in-batch CWD-level event has explicitly
198
+ // re-scoped the project. When extract.ts emits a cwd event for `cd /projB`
199
+ // or `git -C /projB ...`, subsequent path-less events in the same batch
200
+ // (e.g. the git operation event itself) currently fall back to the hook's
201
+ // inputProjectDir, which still points at the session startup cwd. The
202
+ // user's INTENTIONAL scoping should win over the hook's startup cwd —
203
+ // shadow inputProjectDir with the carried-forward lastKnown once a high-
204
+ // confidence cwd event has fired in this batch.
205
+ let inBatchCwdScope = false;
197
206
  for (const ev of events) {
207
+ const effectiveInputCwd = inBatchCwdScope ? lastKnown : context.inputProjectDir;
198
208
  const attribution = resolveProjectAttribution(ev, {
199
209
  ...context,
210
+ inputProjectDir: effectiveInputCwd,
200
211
  lastKnownProjectDir: lastKnown || context.lastKnownProjectDir || null,
201
212
  });
202
213
  out.push(attribution);
203
214
  if (attribution.projectDir && attribution.confidence >= ATTRIBUTION_CONFIDENCE.CARRY_FORWARD_THRESHOLD) {
204
215
  lastKnown = attribution.projectDir;
216
+ if (attribution.confidence >= ATTRIBUTION_CONFIDENCE.CWD_EVENT) {
217
+ inBatchCwdScope = true;
218
+ }
205
219
  }
206
220
  }
207
221
  return out;
package/build/store.d.ts CHANGED
@@ -88,7 +88,7 @@ export declare class ContentStore {
88
88
  indexPlainText(content: string, source: string, linesPerChunk?: number, attribution?: {
89
89
  sessionId?: string;
90
90
  eventId?: string;
91
- }): IndexResult;
91
+ }, maxChunkBytes?: number): IndexResult;
92
92
  /**
93
93
  * Index JSON content by walking the object tree and using key paths
94
94
  * as chunk titles (analogous to heading hierarchy in markdown). Objects
package/build/store.js CHANGED
@@ -108,6 +108,20 @@ function maxEditDistance(wordLength) {
108
108
  // length normalization and produce unwieldy search results. Split at paragraph
109
109
  // boundaries when a chunk exceeds this cap.
110
110
  const MAX_CHUNK_BYTES = 4096;
111
+ // Blank-line sectioning is used only for output that is *naturally* sectioned:
112
+ // at least a few sections, not an unbounded explosion, and no single section so
113
+ // large that the split is clearly not the real structure (those fall back to
114
+ // line-grouping). Sections that pass the heuristic but still exceed
115
+ // MAX_CHUNK_BYTES are sub-split so no persisted chunk breaks the cap.
116
+ const MIN_BLANK_LINE_SECTIONS = 3;
117
+ const MAX_BLANK_LINE_SECTIONS = 200;
118
+ const BLANK_SECTION_STRATEGY_MAX_BYTES = 5000;
119
+ // Number of leading characters of a chunk's first line used as its title.
120
+ const CHUNK_TITLE_MAX_CHARS = 80;
121
+ // When byte-splitting an oversized single line, prefer to break at a whitespace
122
+ // boundary for readability — but only if that boundary is past this fraction of
123
+ // the slice, otherwise we'd waste too much of the byte budget.
124
+ const WHITESPACE_BREAK_RATIO = 0.5;
111
125
  // ─────────────────────────────────────────────────────────
112
126
  // ContentStore
113
127
  // ─────────────────────────────────────────────────────────
@@ -820,11 +834,11 @@ export class ContentStore {
820
834
  * into fixed-size line groups. Unlike markdown indexing, this does not
821
835
  * look for headings — it chunks by line count with overlap.
822
836
  */
823
- indexPlainText(content, source, linesPerChunk = 20, attribution) {
837
+ indexPlainText(content, source, linesPerChunk = 20, attribution, maxChunkBytes = MAX_CHUNK_BYTES) {
824
838
  if (!content || content.trim().length === 0) {
825
839
  return this.#insertChunks([], source, "", undefined, undefined, attribution);
826
840
  }
827
- const chunks = this.#chunkPlainText(content, linesPerChunk);
841
+ const chunks = this.#chunkPlainText(content, linesPerChunk, maxChunkBytes);
828
842
  return withRetry(() => this.#insertChunks(chunks.map((c) => ({ ...c, hasCode: false })), source, content, undefined, undefined, attribution));
829
843
  }
830
844
  // ── Index JSON ──
@@ -837,19 +851,19 @@ export class ContentStore {
837
851
  */
838
852
  indexJSON(content, source, maxChunkBytes = MAX_CHUNK_BYTES, attribution) {
839
853
  if (!content || content.trim().length === 0) {
840
- return this.indexPlainText("", source, undefined, attribution);
854
+ return this.indexPlainText("", source, undefined, attribution, maxChunkBytes);
841
855
  }
842
856
  let parsed;
843
857
  try {
844
858
  parsed = JSON.parse(content);
845
859
  }
846
860
  catch {
847
- return this.indexPlainText(content, source, undefined, attribution);
861
+ return this.indexPlainText(content, source, undefined, attribution, maxChunkBytes);
848
862
  }
849
863
  const chunks = [];
850
864
  this.#walkJSON(parsed, [], chunks, maxChunkBytes);
851
865
  if (chunks.length === 0) {
852
- return this.indexPlainText(content, source, undefined, attribution);
866
+ return this.indexPlainText(content, source, undefined, attribution, maxChunkBytes);
853
867
  }
854
868
  return withRetry(() => this.#insertChunks(chunks, source, content, undefined, undefined, attribution));
855
869
  }
@@ -1448,27 +1462,119 @@ export class ContentStore {
1448
1462
  flush();
1449
1463
  return chunks;
1450
1464
  }
1451
- #chunkPlainText(text, linesPerChunk) {
1465
+ /**
1466
+ * Return the largest prefix of `str` whose UTF-8 byte length does not exceed
1467
+ * `maxBytes`, walking by Unicode code point so multibyte sequences (CJK) and
1468
+ * surrogate pairs (emoji) are never cut mid-character. Guarantees forward
1469
+ * progress: if even the first code point exceeds `maxBytes`, it is still
1470
+ * returned whole (a 1-4 byte overshoot beats an infinite loop).
1471
+ */
1472
+ #byteCappedPrefix(str, maxBytes) {
1473
+ if (Buffer.byteLength(str) <= maxBytes)
1474
+ return str;
1475
+ let prefix = "";
1476
+ let bytes = 0;
1477
+ for (const char of str) {
1478
+ const charBytes = Buffer.byteLength(char);
1479
+ if (bytes + charBytes > maxBytes)
1480
+ break;
1481
+ prefix += char;
1482
+ bytes += charBytes;
1483
+ }
1484
+ // Defensive: a single code point wider than the cap (only possible with a
1485
+ // pathologically small maxBytes) still advances by one character.
1486
+ if (prefix.length === 0)
1487
+ return [...str][0] ?? "";
1488
+ return prefix;
1489
+ }
1490
+ /**
1491
+ * Split a single oversized plain-text chunk into byte-capped sub-chunks
1492
+ * by accumulating lines until the byte count would exceed maxChunkBytes.
1493
+ * Falls back to byte-accurate splitting for extremely long single lines.
1494
+ */
1495
+ #splitOversizedPlainChunk(lines, titlePrefix, maxChunkBytes) {
1496
+ const subChunks = [];
1497
+ let accumulator = [];
1498
+ let partIndex = 1;
1499
+ const flushAccumulator = () => {
1500
+ if (accumulator.length === 0)
1501
+ return;
1502
+ const content = accumulator.join("\n");
1503
+ const partTitle = partIndex === 1 ? titlePrefix : `${titlePrefix} (${partIndex})`;
1504
+ subChunks.push({ title: partTitle, content });
1505
+ partIndex++;
1506
+ accumulator = [];
1507
+ };
1508
+ for (const line of lines) {
1509
+ // If a single line itself exceeds the cap (even as first line),
1510
+ // split it by character before accumulating
1511
+ if (Buffer.byteLength(line) > maxChunkBytes) {
1512
+ flushAccumulator();
1513
+ // Split the long line into byte-capped pieces
1514
+ let remaining = line;
1515
+ let linePart = 1;
1516
+ while (remaining.length > 0) {
1517
+ // Byte-accurate slice: never exceeds the cap, never cuts a multibyte
1518
+ // character (CJK) or surrogate pair (emoji) in half.
1519
+ let slice = this.#byteCappedPrefix(remaining, maxChunkBytes);
1520
+ // Try to break at a whitespace boundary near the end for readability,
1521
+ // but only when text remains after this slice.
1522
+ if (slice.length < remaining.length) {
1523
+ const lastSpace = slice.lastIndexOf(" ");
1524
+ const lastNewline = slice.lastIndexOf("\n");
1525
+ const breakPoint = Math.max(lastSpace, lastNewline);
1526
+ if (breakPoint > slice.length * WHITESPACE_BREAK_RATIO) {
1527
+ slice = slice.slice(0, breakPoint);
1528
+ }
1529
+ }
1530
+ const linePartTitle = partIndex === 1 && linePart === 1
1531
+ ? titlePrefix
1532
+ : `${titlePrefix} (${partIndex}.${linePart})`;
1533
+ subChunks.push({ title: linePartTitle, content: slice });
1534
+ remaining = remaining.slice(slice.length);
1535
+ linePart++;
1536
+ partIndex++;
1537
+ }
1538
+ continue;
1539
+ }
1540
+ const candidate = accumulator.length > 0
1541
+ ? accumulator.join("\n") + "\n" + line
1542
+ : line;
1543
+ // If adding this line would exceed the cap, flush accumulator first
1544
+ if (Buffer.byteLength(candidate) > maxChunkBytes && accumulator.length > 0) {
1545
+ flushAccumulator();
1546
+ }
1547
+ accumulator.push(line);
1548
+ }
1549
+ flushAccumulator();
1550
+ return subChunks;
1551
+ }
1552
+ #chunkPlainText(text, linesPerChunk, maxChunkBytes = MAX_CHUNK_BYTES) {
1452
1553
  // Try blank-line splitting first for naturally-sectioned output
1453
1554
  const sections = text.split(/\n\s*\n/);
1454
- if (sections.length >= 3 &&
1455
- sections.length <= 200 &&
1456
- sections.every((s) => Buffer.byteLength(s) < 5000)) {
1457
- return sections
1458
- .map((section, i) => {
1555
+ if (sections.length >= MIN_BLANK_LINE_SECTIONS &&
1556
+ sections.length <= MAX_BLANK_LINE_SECTIONS &&
1557
+ sections.every((s) => Buffer.byteLength(s) < BLANK_SECTION_STRATEGY_MAX_BYTES)) {
1558
+ return sections.flatMap((section, i) => {
1459
1559
  const trimmed = section.trim();
1460
- const firstLine = trimmed.split("\n")[0].slice(0, 80);
1461
- return {
1462
- title: firstLine || `Section ${i + 1}`,
1463
- content: trimmed,
1464
- };
1465
- })
1466
- .filter((s) => s.content.length > 0);
1560
+ if (trimmed.length === 0)
1561
+ return [];
1562
+ const title = trimmed.split("\n")[0].slice(0, CHUNK_TITLE_MAX_CHARS) || `Section ${i + 1}`;
1563
+ // A section may pass the strategy guard yet still exceed the byte cap
1564
+ // (4097–4999B band): sub-split it so no stored chunk breaks the cap.
1565
+ if (Buffer.byteLength(trimmed) <= maxChunkBytes) {
1566
+ return [{ title, content: trimmed }];
1567
+ }
1568
+ return this.#splitOversizedPlainChunk(trimmed.split("\n"), title, maxChunkBytes);
1569
+ });
1467
1570
  }
1468
1571
  const lines = text.split("\n");
1469
- // Small enough for a single chunk
1572
+ // Small enough for a single chunk — but still enforce byte cap
1470
1573
  if (lines.length <= linesPerChunk) {
1471
- return [{ title: "Output", content: text }];
1574
+ if (Buffer.byteLength(text) <= maxChunkBytes) {
1575
+ return [{ title: "Output", content: text }];
1576
+ }
1577
+ return this.#splitOversizedPlainChunk(lines, "Output", maxChunkBytes);
1472
1578
  }
1473
1579
  // Fixed-size line groups with 2-line overlap
1474
1580
  const chunks = [];
@@ -1480,11 +1586,19 @@ export class ContentStore {
1480
1586
  break;
1481
1587
  const startLine = i + 1;
1482
1588
  const endLine = Math.min(i + slice.length, lines.length);
1483
- const firstLine = slice[0]?.trim().slice(0, 80);
1484
- chunks.push({
1485
- title: firstLine || `Lines ${startLine}-${endLine}`,
1486
- content: slice.join("\n"),
1487
- });
1589
+ const firstLine = slice[0]?.trim().slice(0, CHUNK_TITLE_MAX_CHARS);
1590
+ const joined = slice.join("\n");
1591
+ // Enforce byte cap: sub-split oversized line-group chunks
1592
+ if (Buffer.byteLength(joined) <= maxChunkBytes) {
1593
+ chunks.push({
1594
+ title: firstLine || `Lines ${startLine}-${endLine}`,
1595
+ content: joined,
1596
+ });
1597
+ }
1598
+ else {
1599
+ const subChunks = this.#splitOversizedPlainChunk(slice, firstLine || `Lines ${startLine}-${endLine}`, maxChunkBytes);
1600
+ chunks.push(...subChunks);
1601
+ }
1488
1602
  }
1489
1603
  return chunks;
1490
1604
  }
@@ -0,0 +1,4 @@
1
+ export declare function getToolName(platform: string, bareTool: string): string;
2
+ export type ToolNamer = (bareTool: string) => string;
3
+ export declare function createToolNamer(platform: string): ToolNamer;
4
+ export declare const KNOWN_PLATFORMS: string[];
@@ -0,0 +1,24 @@
1
+ const TOOL_PREFIXES = {
2
+ "claude-code": (tool) => `mcp__plugin_context-mode_context-mode__${tool}`,
3
+ "gemini-cli": (tool) => `mcp__context-mode__${tool}`,
4
+ "antigravity": (tool) => `mcp__context-mode__${tool}`,
5
+ "opencode": (tool) => `context-mode_${tool}`,
6
+ "kilo": (tool) => `context-mode_${tool}`,
7
+ "vscode-copilot": (tool) => `context-mode_${tool}`,
8
+ "jetbrains-copilot": (tool) => `context-mode_${tool}`,
9
+ "kiro": (tool) => `@context-mode/${tool}`,
10
+ "zed": (tool) => `mcp:context-mode:${tool}`,
11
+ "cursor": (tool) => tool,
12
+ "codex": (tool) => tool,
13
+ "openclaw": (tool) => tool,
14
+ "pi": (tool) => tool,
15
+ "qwen-code": (tool) => `mcp__context-mode__${tool}`,
16
+ };
17
+ export function getToolName(platform, bareTool) {
18
+ const fn = TOOL_PREFIXES[platform] || TOOL_PREFIXES["claude-code"];
19
+ return fn(bareTool);
20
+ }
21
+ export function createToolNamer(platform) {
22
+ return (bareTool) => getToolName(platform, bareTool);
23
+ }
24
+ export const KNOWN_PLATFORMS = Object.keys(TOOL_PREFIXES);
@@ -0,0 +1,14 @@
1
+ /**
2
+ * util/jsonc — string-aware JSONC comment + trailing-comma stripping and a
3
+ * tolerant parse. Several agent CLIs ship config files as JSONC (VS Code
4
+ * `mcp.json`, Zed `settings.json`), so a strict `JSON.parse` false-fails on a
5
+ * perfectly valid commented file. Use `parseJsonc` whenever reading a
6
+ * platform config we did not write ourselves.
7
+ */
8
+ /** Strip `//` line + `/* *​/` block comments and trailing commas, string-aware. */
9
+ export declare function stripJsonComments(str: string): string;
10
+ /**
11
+ * Parse JSON or JSONC. Tries strict `JSON.parse` first (fast, exact), then a
12
+ * comment/trailing-comma-stripped parse. Returns `undefined` when both fail.
13
+ */
14
+ export declare function parseJsonc<T = unknown>(raw: string): T | undefined;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * util/jsonc — string-aware JSONC comment + trailing-comma stripping and a
3
+ * tolerant parse. Several agent CLIs ship config files as JSONC (VS Code
4
+ * `mcp.json`, Zed `settings.json`), so a strict `JSON.parse` false-fails on a
5
+ * perfectly valid commented file. Use `parseJsonc` whenever reading a
6
+ * platform config we did not write ourselves.
7
+ */
8
+ /** Strip `//` line + `/* *​/` block comments and trailing commas, string-aware. */
9
+ export function stripJsonComments(str) {
10
+ let out = "";
11
+ let inString = false;
12
+ let escaped = false;
13
+ let inBlockComment = false;
14
+ for (let i = 0; i < str.length; i++) {
15
+ const c = str[i];
16
+ const next = str[i + 1];
17
+ if (inBlockComment) {
18
+ if (c === "*" && next === "/") {
19
+ inBlockComment = false;
20
+ i++;
21
+ }
22
+ continue;
23
+ }
24
+ if (escaped) {
25
+ out += c;
26
+ escaped = false;
27
+ continue;
28
+ }
29
+ if (c === "\\") {
30
+ out += c;
31
+ escaped = inString;
32
+ continue;
33
+ }
34
+ if (c === '"') {
35
+ inString = !inString;
36
+ out += c;
37
+ continue;
38
+ }
39
+ if (!inString && c === "/" && next === "/") {
40
+ while (i < str.length && str[i] !== "\n")
41
+ i++;
42
+ if (i < str.length)
43
+ out += "\n";
44
+ continue;
45
+ }
46
+ if (!inString && c === "/" && next === "*") {
47
+ inBlockComment = true;
48
+ i++;
49
+ continue;
50
+ }
51
+ out += c;
52
+ }
53
+ // Trailing-comma removal, string-aware. The scan above already removed
54
+ // comments, so this second pass over `out` only needs to track string state:
55
+ // a comma is "trailing" when the next significant char is `}` or `]`. Doing
56
+ // it here — instead of a post-hoc trailing-comma regex over the whole string —
57
+ // preserves commas inside string literals (e.g. "[1, ]"), which that regex
58
+ // silently corrupted to "[1 ]". See #787 review.
59
+ let result = "";
60
+ let inStr = false;
61
+ let esc = false;
62
+ for (let i = 0; i < out.length; i++) {
63
+ const c = out[i];
64
+ if (esc) {
65
+ result += c;
66
+ esc = false;
67
+ continue;
68
+ }
69
+ if (c === "\\") {
70
+ result += c;
71
+ esc = inStr;
72
+ continue;
73
+ }
74
+ if (c === '"') {
75
+ inStr = !inStr;
76
+ result += c;
77
+ continue;
78
+ }
79
+ if (!inStr && c === ",") {
80
+ let j = i + 1;
81
+ while (j < out.length && (out[j] === " " || out[j] === "\t" || out[j] === "\r" || out[j] === "\n"))
82
+ j++;
83
+ if (out[j] === "}" || out[j] === "]")
84
+ continue;
85
+ }
86
+ result += c;
87
+ }
88
+ return result;
89
+ }
90
+ /**
91
+ * Parse JSON or JSONC. Tries strict `JSON.parse` first (fast, exact), then a
92
+ * comment/trailing-comma-stripped parse. Returns `undefined` when both fail.
93
+ */
94
+ export function parseJsonc(raw) {
95
+ for (const candidate of [raw, stripJsonComments(raw)]) {
96
+ try {
97
+ return JSON.parse(candidate);
98
+ }
99
+ catch {
100
+ /* try next */
101
+ }
102
+ }
103
+ return undefined;
104
+ }