UNPKG

pi-mcp-extension

Version:

Full-featured, lightweight MCP extension for the Pi coding agent: stdio, SSE & Streamable HTTP transports, OAuth 2.1 (PKCE + DCR), lazy/eager lifecycles — minimal dependencies, zero bloat.

130 lines (116 loc) • 4.86 kB
/** * Pure JSON-with-comments (JSONC) text utilities. This module has no `src/` * dependencies of its own and is safe for `scripts/` and `tests/` to import: it * does not load `zod`, the MCP error types, or the OAuth/callback modules, so * non-runtime consumers stay free of runtime MCP imports. * * The only behaviour this module performs is `stripJsonComments` — a single-pass * character scan that removes `//` line comments and `/* ... *\/` block comments * while leaving the rest of the document byte-equivalent. Comments inside * double-quoted strings are preserved verbatim, so a JSON value such as * `"https://example.com/foo"` is never accidentally truncated. Trailing commas * (the other common JSONC extension) are **not** handled here — none of the * files this helper feeds today uses them, and an explicit `JSON.parse` failure * on a misplaced comma is a more honest signal than silent acceptance. */ interface ConsumedChunk { /** Text to append to the output for the span that was consumed. */ chunk: string; /** Index of the first character after the consumed span. */ nextIndex: number; } /** * Copy a double-quoted string literal verbatim, starting at its opening quote. * Backslash escapes are skipped in pairs so an escaped `"` never closes the string. */ function consumeDoubleQuotedString(text: string, start: number): ConsumedChunk { let index = start + 1; while (index < text.length) { const ch = text[index]!; if (ch === '"') { return { chunk: text.slice(start, index + 1), nextIndex: index + 1 }; } index += ch === "\\" ? 2 : 1; } // WHY: An unterminated string literal is copied verbatim; JSON.parse then reports // the syntax error instead of this helper silently dropping the rest of the file. return { chunk: text.slice(start), nextIndex: text.length }; } /** * Advance past a `//` line comment, stopping before `\n` (or before `\r` in `\r\n`) * so the line break itself is copied through to the output. */ function skipLineComment(text: string, start: number): number { let index = start + 2; while (index < text.length) { if (text[index] === "\n") break; if (text[index] === "\r" && text[index + 1] === "\n") break; index += 1; } return index; } /** * Replace every code unit of a block comment with a space, keeping `\n` and `\r` * so line numbers in subsequent `JSON.parse` errors stay accurate. */ function maskBlockComment(comment: string): string { return comment.replace(/[^\r\n]/g, " "); } /** * Consume a `/* ... *\/` block comment, starting at its opening `/*`. * Unterminated comments are returned intact. */ function consumeBlockComment(text: string, start: number): ConsumedChunk { const closeIdx = text.indexOf("*/", start + 2); if (closeIdx === -1) { // WHY: Leave unterminated block comments intact so JSON.parse fails with a // SyntaxError rather than silently truncating the rest of the file. return { chunk: text.slice(start), nextIndex: text.length }; } return { chunk: maskBlockComment(text.slice(start, closeIdx + 2)), nextIndex: closeIdx + 2, }; } /** * Consume the next token of the document: a double-quoted string, a line comment * (dropped, text preserved), a block comment (masked), or a single literal character. * Keeping the dispatch in one named helper keeps the scan loop flat. */ function consumeNextChunk(text: string, index: number): ConsumedChunk { const ch = text[index]!; if (ch === '"') { return consumeDoubleQuotedString(text, index); } if (ch === "/" && text[index + 1] === "/") { return { chunk: "", nextIndex: skipLineComment(text, index) }; } if (ch === "/" && text[index + 1] === "*") { return consumeBlockComment(text, index); } return { chunk: ch, nextIndex: index + 1 }; } /** * Strip `//` line comments and `/* ... *\/` block comments outside of double-quoted * JSON strings in a single character-by-character pass. * * Preserves `\n` and `\r` characters so line numbers in `JSON.parse` error messages * remain accurate. Unterminated `/*` block comments are left intact so `JSON.parse` * fails with a syntax error rather than silently truncating the document. * * WHY: this module exists so `scripts/ruleset-audit.ts` and `tests/repo-policy.test.ts` * can read `.github/rulesets/*.json` with the same JSONC tolerance the runtime uses * for `mcp.json`. A future maintainer can add `// WHY: …` style comments to the * ruleset files (mirroring the convention used throughout `src/`) without breaking the * ruleset audit or policy tests. */ export function stripJsonComments(text: string): string { let out = ""; let index = 0; while (index < text.length) { const consumed = consumeNextChunk(text, index); out += consumed.chunk; index = consumed.nextIndex; } return out; }