@glasstrace/sdk 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +56 -0
  2. package/dist/{chunk-JZ475QRH.js → chunk-D3QXU2VM.js} +22 -191
  3. package/dist/chunk-D3QXU2VM.js.map +1 -0
  4. package/dist/{chunk-VQDYXXVS.js → chunk-MLRQTCCK.js} +154 -8
  5. package/dist/chunk-MLRQTCCK.js.map +1 -0
  6. package/dist/{chunk-VJQIFY33.js → chunk-YLY7AGLC.js} +7 -4
  7. package/dist/chunk-YLY7AGLC.js.map +1 -0
  8. package/dist/chunk-ZBQQXVHD.js +208 -0
  9. package/dist/chunk-ZBQQXVHD.js.map +1 -0
  10. package/dist/cli/init.cjs +206 -34
  11. package/dist/cli/init.cjs.map +1 -1
  12. package/dist/cli/init.js +65 -8
  13. package/dist/cli/init.js.map +1 -1
  14. package/dist/cli/mcp-add.cjs +45 -25
  15. package/dist/cli/mcp-add.cjs.map +1 -1
  16. package/dist/cli/mcp-add.js +10 -7
  17. package/dist/cli/mcp-add.js.map +1 -1
  18. package/dist/cli/status.cjs +33 -3
  19. package/dist/cli/status.cjs.map +1 -1
  20. package/dist/cli/status.js +12 -3
  21. package/dist/cli/status.js.map +1 -1
  22. package/dist/cli/uninit.cjs +27 -3
  23. package/dist/cli/uninit.cjs.map +1 -1
  24. package/dist/cli/uninit.d.cts +10 -2
  25. package/dist/cli/uninit.d.ts +10 -2
  26. package/dist/cli/uninit.js +2 -1
  27. package/dist/cli/upgrade-instructions.cjs +440 -0
  28. package/dist/cli/upgrade-instructions.cjs.map +1 -0
  29. package/dist/cli/upgrade-instructions.d.cts +48 -0
  30. package/dist/cli/upgrade-instructions.d.ts +48 -0
  31. package/dist/cli/upgrade-instructions.js +80 -0
  32. package/dist/cli/upgrade-instructions.js.map +1 -0
  33. package/dist/index.cjs +229 -60
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.js +2 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/node-entry.cjs +237 -68
  38. package/dist/node-entry.cjs.map +1 -1
  39. package/dist/node-entry.js +2 -1
  40. package/package.json +3 -2
  41. package/dist/chunk-JZ475QRH.js.map +0 -1
  42. package/dist/chunk-VJQIFY33.js.map +0 -1
  43. package/dist/chunk-VQDYXXVS.js.map +0 -1
@@ -12,6 +12,10 @@ import {
12
12
  import {
13
13
  NEXT_CONFIG_NAMES
14
14
  } from "./chunk-NB7GJE4S.js";
15
+ import {
16
+ isEndMarkerLine,
17
+ parseStartMarkerLine
18
+ } from "./chunk-ZBQQXVHD.js";
15
19
 
16
20
  // src/cli/uninit.ts
17
21
  import * as fs2 from "node:fs";
@@ -450,10 +454,9 @@ function removeMarkerSection(content) {
450
454
  let startIdx = -1;
451
455
  let endIdx = -1;
452
456
  for (let i = 0; i < lines.length; i++) {
453
- const trimmed = lines[i].trim();
454
- if (trimmed === "<!-- glasstrace:mcp:start -->" || trimmed === "# glasstrace:mcp:start") {
457
+ if (parseStartMarkerLine(lines[i]) !== null) {
455
458
  startIdx = i;
456
- } else if ((trimmed === "<!-- glasstrace:mcp:end -->" || trimmed === "# glasstrace:mcp:end") && startIdx !== -1) {
459
+ } else if (isEndMarkerLine(lines[i]) && startIdx !== -1) {
457
460
  endIdx = i;
458
461
  break;
459
462
  }
@@ -914,4 +917,4 @@ export {
914
917
  writeShutdownMarker,
915
918
  runUninit
916
919
  };
917
- //# sourceMappingURL=chunk-VJQIFY33.js.map
920
+ //# sourceMappingURL=chunk-YLY7AGLC.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli/uninit.ts","../src/cli/discovery-file.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { NEXT_CONFIG_NAMES } from \"./constants.js\";\nimport { readEnvLocalApiKey, isDevApiKey } from \"../mcp-runtime.js\";\nimport { atomicWriteFileSync } from \"../atomic-write.js\";\nimport {\n removeDiscoveryFile,\n relativeDiscoveryPath,\n} from \"./discovery-file.js\";\nimport {\n isEndMarkerLine,\n parseStartMarkerLine,\n} from \"../agent-detection/inject.js\";\n\n/**\n * Options for the uninit command.\n */\nexport interface UninitOptions {\n projectRoot: string;\n dryRun: boolean;\n /**\n * When true, skip interactive confirmation before destructive actions\n * such as removing a claimed developer API key from `.env.local`\n * (DISC-1247 Scenario 6).\n */\n force?: boolean;\n /**\n * Optional prompt callback; when omitted, uninit uses a TTY-based\n * `readline` prompt in interactive mode and defaults to `false`\n * (abort) when no TTY is attached. Exposed for testing.\n */\n prompt?: (question: string, defaultValue: boolean) => Promise<boolean>;\n}\n\n/**\n * Result of running the uninit command.\n */\nexport interface UninitResult {\n exitCode: number;\n summary: string[];\n warnings: string[];\n errors: string[];\n}\n\n/**\n * MCP config files that init may create.\n * These are JSON files containing `mcpServers.glasstrace`.\n */\nconst MCP_CONFIG_FILES = [\".mcp.json\", \".cursor/mcp.json\", \".gemini/settings.json\"] as const;\n\n/**\n * Agent info files that may contain glasstrace marker sections.\n * Both HTML-style (`<!-- glasstrace:mcp:start -->`) and hash-style\n * (`# glasstrace:mcp:start`) markers are supported.\n */\nconst AGENT_INFO_FILES = [\n \"CLAUDE.md\",\n \"codex.md\",\n \".cursorrules\",\n] as const;\n\n/**\n * Advances past a string literal (double-quoted, single-quoted, or template\n * literal), respecting backslash escapes.\n *\n * Note: Template literals with `${...}` interpolations containing nested\n * backticks are not fully supported — the scanner stops at the first\n * unescaped backtick. This is acceptable because config files (the primary\n * use case for `findMatchingParen`/`findMatchingBrace`) do not use nested\n * template literals.\n *\n * @param text - The source text.\n * @param start - The index of the opening quote character.\n * @param quote - The quote character (`\"`, `'`, or `` ` ``).\n * @returns The index immediately after the closing quote.\n * @internal Exported for unit testing only.\n */\nexport function skipString(text: string, start: number, quote: string): number {\n let i = start + 1;\n while (i < text.length) {\n if (text[i] === \"\\\\\") {\n i += 2;\n continue;\n }\n if (text[i] === quote) {\n return i + 1;\n }\n i++;\n }\n return text.length;\n}\n\n/**\n * Finds the matching closing delimiter for an opening delimiter at the given\n * position, accounting for nesting and skipping delimiters that appear inside\n * string literals (`\"`, `'`, `` ` ``), single-line comments (`//`), and block\n * comments.\n *\n * @param text - The source text to search.\n * @param openPos - The index of the opening delimiter.\n * @param openChar - The opening delimiter character (e.g., `(` or `{`).\n * @param closeChar - The closing delimiter character (e.g., `)` or `}`).\n * @returns The index of the matching closing delimiter, or -1 if not found.\n * @internal Exported for unit testing only.\n */\nexport function findMatchingDelimiter(\n text: string,\n openPos: number,\n openChar: string,\n closeChar: string,\n): number {\n let depth = 0;\n let i = openPos;\n while (i < text.length) {\n const ch = text[i];\n\n // Skip string literals\n if (ch === '\"' || ch === \"'\" || ch === \"`\") {\n i = skipString(text, i, ch);\n continue;\n }\n\n // Skip single-line comments.\n // Note: This may misidentify regex literals containing `//` (e.g.,\n // `/api\\//`). Config files — the primary use case — do not contain\n // regex literals, so this trade-off is acceptable.\n if (ch === \"/\" && text[i + 1] === \"/\") {\n const newline = text.indexOf(\"\\n\", i);\n if (newline === -1) {\n return -1;\n }\n i = newline + 1;\n continue;\n }\n\n // Skip block comments\n if (ch === \"/\" && text[i + 1] === \"*\") {\n const end = text.indexOf(\"*/\", i + 2);\n if (end === -1) {\n return -1;\n }\n i = end + 2;\n continue;\n }\n\n if (ch === openChar) {\n depth++;\n } else if (ch === closeChar) {\n depth--;\n if (depth === 0) {\n return i;\n }\n }\n i++;\n }\n return -1;\n}\n\n/**\n * Finds the matching closing parenthesis for an opening paren at the given\n * position, accounting for nested parentheses and skipping delimiters inside\n * string literals and comments.\n *\n * @param text - The source text to search.\n * @param openPos - The index of the opening `(`.\n * @returns The index of the matching `)`, or -1 if not found.\n * @internal Exported for unit testing only.\n */\nexport function findMatchingParen(text: string, openPos: number): number {\n return findMatchingDelimiter(text, openPos, \"(\", \")\");\n}\n\n/**\n * Removes the `withGlasstraceConfig(...)` wrapper from an ESM default export,\n * restoring the inner expression.\n *\n * Before: `export default withGlasstraceConfig(innerExpr);`\n * After: `export default innerExpr;`\n *\n * @internal Exported for unit testing only.\n */\nexport function unwrapExport(content: string): { content: string; unwrapped: boolean } {\n const pattern = /export\\s+default\\s+withGlasstraceConfig\\s*\\(/;\n const match = pattern.exec(content);\n if (!match) {\n return { content, unwrapped: false };\n }\n\n // Find the opening paren of withGlasstraceConfig(\n const openParenIdx = match.index + match[0].length - 1;\n const closeParenIdx = findMatchingParen(content, openParenIdx);\n if (closeParenIdx === -1) {\n return { content, unwrapped: false };\n }\n\n const innerExpr = content.slice(openParenIdx + 1, closeParenIdx).trim();\n if (innerExpr.length === 0) {\n return { content, unwrapped: false };\n }\n\n // Everything before `export default ...`\n const before = content.slice(0, match.index);\n // Everything after the closing `)` (skip optional semicolon and trailing whitespace)\n const afterClose = content.slice(closeParenIdx + 1);\n const trailing = afterClose.replace(/^;?\\s*/, \"\");\n\n const result = before + `export default ${innerExpr};\\n` + trailing;\n\n return { content: result, unwrapped: true };\n}\n\n/**\n * Removes the `withGlasstraceConfig(...)` wrapper from a CJS module.exports,\n * restoring the inner expression.\n *\n * Before: `module.exports = withGlasstraceConfig(innerExpr);`\n * After: `module.exports = innerExpr;`\n *\n * @internal Exported for unit testing only.\n */\nexport function unwrapCJSExport(content: string): { content: string; unwrapped: boolean } {\n const pattern = /module\\.exports\\s*=\\s*withGlasstraceConfig\\s*\\(/;\n const match = pattern.exec(content);\n if (!match) {\n return { content, unwrapped: false };\n }\n\n const openParenIdx = match.index + match[0].length - 1;\n const closeParenIdx = findMatchingParen(content, openParenIdx);\n if (closeParenIdx === -1) {\n return { content, unwrapped: false };\n }\n\n const innerExpr = content.slice(openParenIdx + 1, closeParenIdx).trim();\n if (innerExpr.length === 0) {\n return { content, unwrapped: false };\n }\n\n const before = content.slice(0, match.index);\n const afterClose = content.slice(closeParenIdx + 1);\n const trailing = afterClose.replace(/^;?\\s*/, \"\");\n\n const result = before + `module.exports = ${innerExpr};\\n` + trailing;\n\n return { content: result, unwrapped: true };\n}\n\n/**\n * Removes the `import { withGlasstraceConfig } from \"@glasstrace/sdk\"` line\n * from file content. If `withGlasstraceConfig` is the only imported specifier,\n * the entire import line is removed. If other specifiers exist, only\n * `withGlasstraceConfig` is removed from the specifier list.\n *\n * @internal Exported for unit testing only.\n */\nexport function removeGlasstraceConfigImport(content: string): string {\n // ESM: import { withGlasstraceConfig } from \"@glasstrace/sdk\"\n const esmSoleImport =\n /import\\s*\\{\\s*withGlasstraceConfig\\s*\\}\\s*from\\s*[\"']@glasstrace\\/sdk[\"']\\s*;?\\s*\\n?/;\n if (esmSoleImport.test(content)) {\n return content.replace(esmSoleImport, \"\");\n }\n\n // ESM with multiple specifiers — remove withGlasstraceConfig from the list\n const esmMultiImport =\n /import\\s*\\{([^}]*)\\}\\s*from\\s*[\"']@glasstrace\\/sdk[\"']/;\n const multiMatch = esmMultiImport.exec(content);\n if (multiMatch) {\n const specifiers = multiMatch[1]\n .split(\",\")\n .map((s) => s.trim())\n .filter((s) => s !== \"\" && s !== \"withGlasstraceConfig\");\n if (specifiers.length === 0) {\n // All specifiers were withGlasstraceConfig — remove entire import\n return content.replace(\n /import\\s*\\{[^}]*\\}\\s*from\\s*[\"']@glasstrace\\/sdk[\"']\\s*;?\\s*\\n?/,\n \"\",\n );\n }\n const newImport = `import { ${specifiers.join(\", \")} } from \"@glasstrace/sdk\"`;\n return content.replace(multiMatch[0], newImport);\n }\n\n // CJS: const { withGlasstraceConfig } = require(\"@glasstrace/sdk\")\n const cjsSoleRequire =\n /const\\s*\\{\\s*withGlasstraceConfig\\s*\\}\\s*=\\s*require\\s*\\(\\s*[\"']@glasstrace\\/sdk[\"']\\s*\\)\\s*;?\\s*\\n?/;\n if (cjsSoleRequire.test(content)) {\n return content.replace(cjsSoleRequire, \"\");\n }\n\n // CJS with multiple specifiers\n const cjsMultiRequire =\n /const\\s*\\{([^}]*)\\}\\s*=\\s*require\\s*\\(\\s*[\"']@glasstrace\\/sdk[\"']\\s*\\)/;\n const cjsMultiMatch = cjsMultiRequire.exec(content);\n if (cjsMultiMatch) {\n const specifiers = cjsMultiMatch[1]\n .split(\",\")\n .map((s) => s.trim())\n .filter((s) => s !== \"\" && s !== \"withGlasstraceConfig\");\n if (specifiers.length === 0) {\n return content.replace(\n /const\\s*\\{[^}]*\\}\\s*=\\s*require\\s*\\(\\s*[\"']@glasstrace\\/sdk[\"']\\s*\\)\\s*;?\\s*\\n?/,\n \"\",\n );\n }\n const newRequire = `const { ${specifiers.join(\", \")} } = require(\"@glasstrace/sdk\")`;\n return content.replace(cjsMultiMatch[0], newRequire);\n }\n\n return content;\n}\n\n/**\n * Removes blank lines that appear consecutively (more than one empty line\n * in a row) at the top of a file, which can occur after removing import lines.\n */\nfunction cleanLeadingBlankLines(content: string): string {\n return content.replace(/^\\n{2,}/, \"\\n\");\n}\n\n/**\n * Determines whether an instrumentation.ts file was created by `glasstrace init`\n * (i.e., contains only the standard template with no user-added code).\n *\n * A file is considered init-created if:\n * - The only import from any package is `@glasstrace/sdk`\n * - The only meaningful statement in `register()` is `registerGlasstrace()`\n * - There are no other top-level statements, exports, or declarations outside\n * the register function (prevents deleting files where users added their own code)\n *\n * @internal Exported for unit testing only.\n */\nexport function isInitCreatedInstrumentation(content: string): boolean {\n const lines = content.split(\"\\n\");\n\n // Check that all imports are from @glasstrace/sdk\n const importLines = lines.filter(\n (l) => /^\\s*import\\s/.test(l) && !l.trim().startsWith(\"//\"),\n );\n const nonGlasstraceImports = importLines.filter(\n (l) => !l.includes(\"@glasstrace/sdk\"),\n );\n if (nonGlasstraceImports.length > 0) {\n return false;\n }\n\n // Check that the register() function body only contains registerGlasstrace()\n // and comments — no other meaningful statements\n const registerFnRegex = /export\\s+(?:async\\s+)?function\\s+register\\s*\\([^)]*\\)\\s*\\{/;\n const match = registerFnRegex.exec(content);\n if (!match) {\n // No register function — not a standard init template\n return false;\n }\n\n // Extract the function body\n const afterBrace = content.slice(match.index + match[0].length);\n const closingBraceIdx = findMatchingBrace(content, match.index + match[0].length - 1);\n if (closingBraceIdx === -1) {\n return false;\n }\n\n const body = afterBrace.slice(0, closingBraceIdx - (match.index + match[0].length));\n const bodyLines = body.split(\"\\n\");\n\n // Filter out comments and blank lines — only meaningful statements remain\n const statements = bodyLines.filter((l) => {\n const trimmed = l.trim();\n return trimmed !== \"\" && !trimmed.startsWith(\"//\");\n });\n\n // The only statement should be registerGlasstrace()\n if (statements.length !== 1) {\n return false;\n }\n if (!/^\\s*registerGlasstrace\\s*\\(\\s*\\)\\s*;?\\s*$/.test(statements[0])) {\n return false;\n }\n\n // Verify no other top-level code exists outside imports and the register function.\n // Extract everything that isn't an import line or inside the register() function.\n const beforeFn = content.slice(0, match.index);\n const afterFn = content.slice(closingBraceIdx + 1);\n\n const topLevelBefore = beforeFn.split(\"\\n\").filter((l) => {\n const trimmed = l.trim();\n return (\n trimmed !== \"\" &&\n !trimmed.startsWith(\"//\") &&\n !trimmed.startsWith(\"import \") &&\n !trimmed.startsWith(\"import{\")\n );\n });\n\n const topLevelAfter = afterFn.split(\"\\n\").filter((l) => {\n const trimmed = l.trim();\n return trimmed !== \"\" && !trimmed.startsWith(\"//\");\n });\n\n return topLevelBefore.length === 0 && topLevelAfter.length === 0;\n}\n\n/**\n * Finds the matching closing brace for an opening brace at the given position,\n * skipping delimiters inside string literals and comments.\n */\nfunction findMatchingBrace(text: string, openPos: number): number {\n return findMatchingDelimiter(text, openPos, \"{\", \"}\");\n}\n\n/**\n * Removes the `registerGlasstrace()` call and its `@glasstrace/sdk` import\n * from an instrumentation.ts file, preserving all other code.\n *\n * @internal Exported for unit testing only.\n */\nexport function removeRegisterGlasstrace(content: string): string {\n let result = content;\n\n // Remove all comment-block + registerGlasstrace() call pairs.\n // The init template creates a multi-line comment block before the call:\n // // Glasstrace must be registered before Prisma instrumentation\n // // to ensure all ORM spans are captured correctly.\n // // If you use @prisma/instrumentation, import it after this call.\n // registerGlasstrace();\n // Use global flag to handle multiple occurrences.\n result = result.replace(\n /[ \\t]*\\/\\/\\s*Glasstrace must be registered[^\\n]*\\n(?:[ \\t]*\\/\\/[^\\n]*\\n)*[ \\t]*registerGlasstrace\\s*\\(\\s*\\)\\s*;?\\s*\\n?/g,\n \"\",\n );\n\n // Remove any remaining standalone registerGlasstrace() calls (global)\n result = result.replace(\n /[ \\t]*registerGlasstrace\\s*\\(\\s*\\)\\s*;?\\s*\\n?/g,\n \"\",\n );\n\n // Remove the import line for registerGlasstrace from @glasstrace/sdk\n // If it's the sole import, remove the whole line\n const soleImportPattern =\n /import\\s*\\{\\s*registerGlasstrace\\s*\\}\\s*from\\s*[\"']@glasstrace\\/sdk[\"']\\s*;?\\s*\\n?/;\n if (soleImportPattern.test(result)) {\n result = result.replace(soleImportPattern, \"\");\n } else {\n // Multiple specifiers — remove only registerGlasstrace\n const multiImportPattern =\n /import\\s*\\{([^}]*)\\}\\s*from\\s*[\"']@glasstrace\\/sdk[\"']/;\n const multiMatch = multiImportPattern.exec(result);\n if (multiMatch) {\n const specifiers = multiMatch[1]\n .split(\",\")\n .map((s) => s.trim())\n .filter((s) => s !== \"\" && s !== \"registerGlasstrace\");\n if (specifiers.length === 0) {\n result = result.replace(\n /import\\s*\\{[^}]*\\}\\s*from\\s*[\"']@glasstrace\\/sdk[\"']\\s*;?\\s*\\n?/,\n \"\",\n );\n } else {\n const newImport = `import { ${specifiers.join(\", \")} } from \"@glasstrace/sdk\"`;\n result = result.replace(multiMatch[0], newImport);\n }\n }\n }\n\n return cleanLeadingBlankLines(result);\n}\n\n/**\n * Removes content between glasstrace marker comments from a file.\n * Supports both legacy unstamped markers (pre-SDK-050) and SDK-050+\n * stamped markers (`<!-- glasstrace:mcp:start v=<sdkVersion> -->` /\n * `# glasstrace:mcp:start v=<sdkVersion>`) for both HTML and hash\n * conventions, by deferring marker recognition to the shared parser\n * in `agent-detection/inject.ts`.\n *\n * Anchoring matches `findMarkerBoundaries` in inject.ts: when multiple\n * start markers appear before the first end marker (e.g. a quoted\n * example marker line followed by the real managed block), the\n * removal window anchors to the MOST RECENT start preceding the end.\n *\n * @internal Exported for unit testing only.\n */\nexport function removeMarkerSection(content: string): { content: string; removed: boolean } {\n const lines = content.split(\"\\n\");\n let startIdx = -1;\n let endIdx = -1;\n\n for (let i = 0; i < lines.length; i++) {\n if (parseStartMarkerLine(lines[i]) !== null) {\n // Track the most recent start so a quoted/example marker\n // earlier in the file does not capture the removal window.\n startIdx = i;\n } else if (isEndMarkerLine(lines[i]) && startIdx !== -1) {\n endIdx = i;\n break;\n }\n }\n\n if (startIdx === -1 || endIdx === -1) {\n return { content, removed: false };\n }\n\n const before = lines.slice(0, startIdx);\n const after = lines.slice(endIdx + 1);\n\n // Remove trailing blank line that may have preceded the marker block\n while (before.length > 0 && before[before.length - 1].trim() === \"\") {\n before.pop();\n }\n\n const result = [...before, ...after].join(\"\\n\");\n // Ensure file ends with newline if it has content\n const trimmedResult = result.trimEnd();\n return {\n content: trimmedResult.length > 0 ? trimmedResult + \"\\n\" : \"\",\n removed: true,\n };\n}\n\n/**\n * Removes the `glasstrace` key from an MCP config JSON file's `mcpServers`\n * object. Only deletes the file when `mcpServers` is the sole top-level key\n * and `glasstrace` is the only server entry. When other top-level keys exist\n * (e.g., `$schema`, metadata), the `mcpServers` key is removed (if empty)\n * and the file is preserved.\n *\n * @returns `\"removed-key\"` if the key was removed (other data remains),\n * `\"deleted\"` if the file should be deleted (no other data),\n * or `\"skipped\"` if no glasstrace config was found.\n * @internal Exported for unit testing only.\n */\nexport function processJsonMcpConfig(content: string): {\n action: \"removed-key\" | \"deleted\" | \"skipped\";\n content?: string;\n} {\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(content) as Record<string, unknown>;\n } catch {\n return { action: \"skipped\" };\n }\n\n const mcpServers = parsed[\"mcpServers\"] as Record<string, unknown> | undefined;\n if (!mcpServers || typeof mcpServers !== \"object\" || !(\"glasstrace\" in mcpServers)) {\n return { action: \"skipped\" };\n }\n\n const remainingServers = Object.keys(mcpServers).filter((k) => k !== \"glasstrace\");\n const otherTopLevelKeys = Object.keys(parsed).filter((k) => k !== \"mcpServers\");\n\n if (remainingServers.length === 0 && otherTopLevelKeys.length === 0) {\n // mcpServers.glasstrace is the only data in the file — safe to delete\n return { action: \"deleted\" };\n }\n\n // Remove the glasstrace key, keep other servers\n const { glasstrace: _, ...rest } = mcpServers;\n // Suppress unused variable lint — the destructuring intentionally discards glasstrace\n void _;\n\n if (remainingServers.length > 0) {\n // Other servers remain — keep mcpServers with glasstrace removed\n parsed[\"mcpServers\"] = rest;\n } else {\n // No servers remain but other top-level keys exist — remove mcpServers entirely\n delete parsed[\"mcpServers\"];\n }\n\n return { action: \"removed-key\", content: JSON.stringify(parsed, null, 2) + \"\\n\" };\n}\n\n/**\n * Removes the `[mcp_servers.glasstrace]` section from a TOML config file.\n * Since TOML parsing without a dependency is complex, this uses a line-based\n * approach that handles the standard format written by init.\n *\n * @returns `\"removed-section\"` if the glasstrace section was removed,\n * `\"deleted\"` if the entire file should be deleted (only contained\n * glasstrace config), or `\"skipped\"` if no glasstrace config found.\n * @internal Exported for unit testing only.\n */\nexport function processTomlMcpConfig(content: string): {\n action: \"removed-section\" | \"deleted\" | \"skipped\";\n content?: string;\n} {\n if (!content.includes(\"[mcp_servers.glasstrace]\")) {\n return { action: \"skipped\" };\n }\n\n const lines = content.split(\"\\n\");\n const startIdx = lines.findIndex(\n (l) => l.trim() === \"[mcp_servers.glasstrace]\",\n );\n if (startIdx === -1) {\n return { action: \"skipped\" };\n }\n\n // Find the end of the glasstrace section: next section header or end of file\n let endIdx = lines.length;\n for (let i = startIdx + 1; i < lines.length; i++) {\n if (/^\\s*\\[/.test(lines[i])) {\n endIdx = i;\n break;\n }\n }\n\n // Remove the section and any trailing blank lines\n const before = lines.slice(0, startIdx);\n const after = lines.slice(endIdx);\n\n // Trim trailing blank lines from the before section\n while (before.length > 0 && before[before.length - 1].trim() === \"\") {\n before.pop();\n }\n\n const result = [...before, ...after].join(\"\\n\").trimEnd();\n\n // Check if there are any remaining sections\n if (result.trim().length === 0) {\n return { action: \"deleted\" };\n }\n\n return { action: \"removed-section\", content: result + \"\\n\" };\n}\n\n/**\n * Writes the `.glasstrace/shutdown-requested` marker file atomically so\n * that a running SDK heartbeat tick (or equivalent lifecycle hook) can\n * detect that uninit has been invoked and trigger shutdown (DISC-1247\n * Scenario 1).\n *\n * Uses write-temp + rename semantics so a mid-write crash cannot leave\n * a truncated marker that the running process might misread.\n *\n * Best-effort: if `.glasstrace/` does not exist or the write fails, the\n * marker is silently skipped — uninit's cleanup is not blocked by a\n * missing running process.\n *\n * @internal Exported for unit testing only.\n */\nexport function writeShutdownMarker(projectRoot: string): boolean {\n const dirPath = path.join(projectRoot, \".glasstrace\");\n if (!fs.existsSync(dirPath)) {\n // No .glasstrace/ directory means no running SDK state is tracked —\n // nothing to signal. The filesystem removal step will handle any\n // stray artifacts.\n return false;\n }\n const markerPath = path.join(dirPath, \"shutdown-requested\");\n const body = JSON.stringify({ requestedAt: new Date().toISOString() });\n try {\n // Atomic write per SDK 2.0 §4.3: tmp + fsync(tmp) + rename +\n // fsync(parent). The helper handles tmp-file cleanup on failure\n // and swallows directory-fsync errors on platforms that do not\n // support it (e.g., Windows).\n atomicWriteFileSync(markerPath, body, { encoding: \"utf-8\", mode: 0o600 });\n return true;\n } catch {\n // Marker write was best-effort to begin with; swallow errors so\n // uninit itself never fails because of a signal-side-channel\n // write.\n return false;\n }\n}\n\n/**\n * Simple TTY prompt used when `UninitOptions.prompt` is not provided.\n * Returns `defaultValue` when stdin is not a TTY.\n */\nasync function defaultPrompt(question: string, defaultValue: boolean): Promise<boolean> {\n if (!process.stdin.isTTY) return defaultValue;\n const readline = await import(\"node:readline\");\n const rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout,\n });\n return new Promise<boolean>((resolve) => {\n const suffix = defaultValue ? \" [Y/n] \" : \" [y/N] \";\n rl.question(question + suffix, (answer) => {\n rl.close();\n const trimmed = answer.trim().toLowerCase();\n if (trimmed === \"\") {\n resolve(defaultValue);\n return;\n }\n resolve(trimmed === \"y\" || trimmed === \"yes\");\n });\n });\n}\n\n/**\n * Reverses every step of `glasstrace init`, cleanly removing all SDK artifacts\n * from a project.\n *\n * Steps (in order):\n * 1. Write `.glasstrace/shutdown-requested` marker so a running SDK can\n * drain and exit cleanly (DISC-1247 Scenario 1)\n * 2. Unwrap `withGlasstraceConfig` from next.config\n * 3. Remove `registerGlasstrace` from instrumentation.ts (or delete if init-created)\n * 4. Remove `.glasstrace/` directory\n * 4a. Remove `<staticRoot>/.well-known/glasstrace.json` (and prune the\n * enclosing `.well-known/` directory when empty)\n * 5. Remove `GLASSTRACE_*` entries from `.env.local` (with dev-key confirmation)\n * 6. Remove `.glasstrace/` from `.gitignore`\n * 7. Remove MCP config entries\n * 8. Remove info sections from agent files\n *\n * @param options - Configuration for the uninit command.\n * @returns A structured result describing what actions were taken.\n */\nexport async function runUninit(options: UninitOptions): Promise<UninitResult> {\n const { projectRoot, dryRun } = options;\n const force = options.force === true;\n const prompt = options.prompt ?? defaultPrompt;\n const summary: string[] = [];\n const warnings: string[] = [];\n const errors: string[] = [];\n const prefix = dryRun ? \"[dry run] \" : \"\";\n\n // Step 0: Signal any running SDK to shut down via a marker file.\n // Placed first so the running process has maximum time to observe\n // the marker while the remaining cleanup steps execute.\n try {\n if (!dryRun) {\n const markerWritten = writeShutdownMarker(projectRoot);\n if (markerWritten) {\n summary.push(\"Wrote .glasstrace/shutdown-requested marker\");\n }\n } else {\n const dirPath = path.join(projectRoot, \".glasstrace\");\n if (fs.existsSync(dirPath)) {\n summary.push(`${prefix}Would write .glasstrace/shutdown-requested marker`);\n }\n }\n } catch (err) {\n // Marker is best-effort; failure is not an error for uninit.\n warnings.push(\n `Shutdown marker write failed: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n // Step 1: Unwrap withGlasstraceConfig from next.config\n try {\n let configHandled = false;\n for (const name of NEXT_CONFIG_NAMES) {\n const configPath = path.join(projectRoot, name);\n if (!fs.existsSync(configPath)) {\n continue;\n }\n\n const content = fs.readFileSync(configPath, \"utf-8\");\n if (!content.includes(\"withGlasstraceConfig\")) {\n continue;\n }\n\n const isESM = name.endsWith(\".ts\") || name.endsWith(\".mjs\");\n const unwrapResult = isESM\n ? unwrapExport(content)\n : unwrapCJSExport(content);\n\n if (unwrapResult.unwrapped) {\n const cleaned = removeGlasstraceConfigImport(unwrapResult.content);\n const final = cleanLeadingBlankLines(cleaned);\n if (!dryRun) {\n fs.writeFileSync(configPath, final, \"utf-8\");\n }\n summary.push(`${prefix}Unwrapped withGlasstraceConfig from ${name}`);\n configHandled = true;\n break;\n } else {\n warnings.push(\n `${name} contains withGlasstraceConfig but could not be automatically unwrapped. ` +\n \"Please remove withGlasstraceConfig() manually.\",\n );\n configHandled = true;\n break;\n }\n }\n if (!configHandled) {\n // No next.config with withGlasstraceConfig found — nothing to do\n }\n } catch (err) {\n errors.push(\n `Failed to process next.config: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n // Step 2: Remove registerGlasstrace from instrumentation.ts\n try {\n const instrPath = path.join(projectRoot, \"instrumentation.ts\");\n if (fs.existsSync(instrPath)) {\n const content = fs.readFileSync(instrPath, \"utf-8\");\n if (content.includes(\"registerGlasstrace\") || content.includes(\"@glasstrace/sdk\")) {\n if (isInitCreatedInstrumentation(content)) {\n if (!dryRun) {\n fs.unlinkSync(instrPath);\n }\n summary.push(`${prefix}Deleted instrumentation.ts (init-created)`);\n } else {\n const cleaned = removeRegisterGlasstrace(content);\n if (cleaned !== content) {\n if (!dryRun) {\n fs.writeFileSync(instrPath, cleaned, \"utf-8\");\n }\n summary.push(\n `${prefix}Removed registerGlasstrace() from instrumentation.ts`,\n );\n }\n }\n }\n }\n } catch (err) {\n errors.push(\n `Failed to process instrumentation.ts: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n // Step 3: Remove .glasstrace/ directory\n try {\n const glasstraceDir = path.join(projectRoot, \".glasstrace\");\n if (fs.existsSync(glasstraceDir)) {\n if (!dryRun) {\n fs.rmSync(glasstraceDir, { recursive: true, force: true });\n }\n summary.push(`${prefix}Removed .glasstrace/ directory`);\n }\n } catch (err) {\n errors.push(\n `Failed to remove .glasstrace/: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n // Step 3a: Remove the static discovery file at\n // `<staticRoot>/.well-known/glasstrace.json` and, when empty, the\n // enclosing `.well-known/` directory. Sibling files (e.g. a user's\n // own `security.txt`) are never touched.\n try {\n if (dryRun) {\n // Dry-run preview: simulate the removal by checking existence only.\n // `removeDiscoveryFile` is a destructive helper, so the preview path\n // replicates the existence check inline rather than invoking it.\n // This keeps dry-run accurate even if the helper is changed later.\n // Mirrors the real sweep by checking BOTH candidate layouts so an\n // orphaned file in the non-inferred directory still shows up in\n // the preview (heuristic-drift scenario from the Codex re-review).\n for (const previewLayout of [\"public\", \"static\"] as const) {\n const relPath = relativeDiscoveryPath(previewLayout);\n const absPath = path.join(projectRoot, relPath);\n if (fs.existsSync(absPath)) {\n summary.push(`${prefix}Would remove ${relPath}`);\n }\n }\n } else {\n const result = removeDiscoveryFile(projectRoot);\n if (result.action === \"removed\") {\n const relPath = relativeDiscoveryPath(result.layout);\n summary.push(`Removed ${relPath}`);\n if (result.directoryRemoved) {\n const dirRel = relPath.replace(/\\/glasstrace\\.json$/, \"/\");\n summary.push(`Removed empty ${dirRel}`);\n }\n } else if (result.action === \"failed\") {\n warnings.push(\n `Failed to remove ${relativeDiscoveryPath(result.layout)}${\n result.error !== undefined ? `: ${result.error}` : \"\"\n }`,\n );\n }\n }\n } catch (err) {\n warnings.push(\n `Failed to remove discovery file: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n // Step 4: Remove GLASSTRACE entries from .env.local\n // DISC-1247 Scenario 6: if the file contains a claimed developer key\n // (`gt_dev_*`), require explicit confirmation before removing it so\n // users don't silently lose authentication state during uninit.\n // `--force` bypasses the prompt.\n try {\n const envPath = path.join(projectRoot, \".env.local\");\n if (fs.existsSync(envPath)) {\n const content = fs.readFileSync(envPath, \"utf-8\");\n const existingKey = readEnvLocalApiKey(content);\n const hasDevKey = isDevApiKey(existingKey);\n\n // Track how the dev-key path is resolved so the summary reflects\n // what actually happened: prompt-confirmed, force-bypassed, or\n // preview-only. Using the literal \"(dev key confirmed)\" for all\n // three paths was misleading (Copilot review).\n let proceed = true;\n let devKeyPath: \"interactive-confirmed\" | \"force-bypass\" | \"dry-run-preview\" | \"none\" = \"none\";\n if (hasDevKey) {\n if (dryRun) {\n devKeyPath = \"dry-run-preview\";\n } else if (force) {\n devKeyPath = \"force-bypass\";\n } else {\n const confirmed = await prompt(\n \".env.local contains a claimed Glasstrace developer API key (gt_dev_...). \" +\n \"Removing it will require you to re-authenticate. Continue?\",\n false,\n );\n proceed = confirmed;\n if (confirmed) devKeyPath = \"interactive-confirmed\";\n }\n }\n\n if (!proceed) {\n warnings.push(\n \"Preserved GLASSTRACE_API_KEY in .env.local (claimed dev key; re-run with --force to remove)\",\n );\n } else {\n const lines = content.split(\"\\n\");\n const filtered = lines.filter((line) => {\n const trimmed = line.trim();\n // Match both commented and uncommented GLASSTRACE_ lines\n return !(\n /^\\s*#?\\s*GLASSTRACE_API_KEY\\s*=/.test(trimmed) ||\n /^\\s*#?\\s*GLASSTRACE_COVERAGE_MAP\\s*=/.test(trimmed)\n );\n });\n\n if (filtered.length !== lines.length) {\n const result = filtered.join(\"\\n\");\n // If the file is now empty (only newlines), don't write it\n if (result.trim().length === 0) {\n if (!dryRun) {\n fs.unlinkSync(envPath);\n }\n summary.push(`${prefix}Deleted .env.local (no remaining entries)`);\n } else {\n if (!dryRun) {\n fs.writeFileSync(envPath, result, \"utf-8\");\n }\n let devKeyAnnotation = \"\";\n if (devKeyPath === \"interactive-confirmed\") {\n devKeyAnnotation = \" (dev key confirmed)\";\n } else if (devKeyPath === \"force-bypass\") {\n devKeyAnnotation = \" (dev key removed via --force)\";\n } else if (devKeyPath === \"dry-run-preview\") {\n devKeyAnnotation =\n \" (dev key would be removed; real run would require confirmation)\";\n }\n summary.push(\n `${prefix}Removed GLASSTRACE entries from .env.local${devKeyAnnotation}`,\n );\n }\n }\n }\n }\n } catch (err) {\n errors.push(\n `Failed to process .env.local: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n // Step 5: Remove .glasstrace/ from .gitignore\n try {\n const gitignorePath = path.join(projectRoot, \".gitignore\");\n if (fs.existsSync(gitignorePath)) {\n const content = fs.readFileSync(gitignorePath, \"utf-8\");\n const lines = content.split(\"\\n\");\n\n // Remove lines that are exactly \".glasstrace/\" or MCP config file entries\n // added by init (e.g., \".mcp.json\", \".cursor/mcp.json\", \".gemini/settings.json\",\n // \".codex/config.toml\")\n const mcpGitignoreEntries = new Set([\n \".glasstrace/\",\n \".mcp.json\",\n \".cursor/mcp.json\",\n \".gemini/settings.json\",\n \".codex/config.toml\",\n ]);\n\n const filtered = lines.filter(\n (line) => !mcpGitignoreEntries.has(line.trim()),\n );\n\n if (filtered.length !== lines.length) {\n const result = filtered.join(\"\\n\");\n if (result.trim().length === 0) {\n if (!dryRun) {\n fs.unlinkSync(gitignorePath);\n }\n summary.push(`${prefix}Deleted .gitignore (no remaining entries)`);\n } else {\n if (!dryRun) {\n fs.writeFileSync(gitignorePath, result, \"utf-8\");\n }\n summary.push(`${prefix}Removed Glasstrace entries from .gitignore`);\n }\n }\n }\n } catch (err) {\n errors.push(\n `Failed to process .gitignore: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n // Step 6: Remove MCP config entries\n try {\n for (const configFile of MCP_CONFIG_FILES) {\n const configPath = path.join(projectRoot, configFile);\n if (!fs.existsSync(configPath)) {\n continue;\n }\n\n const content = fs.readFileSync(configPath, \"utf-8\");\n const result = processJsonMcpConfig(content);\n\n if (result.action === \"deleted\") {\n if (!dryRun) {\n fs.unlinkSync(configPath);\n }\n summary.push(`${prefix}Deleted ${configFile}`);\n } else if (result.action === \"removed-key\" && result.content !== undefined) {\n if (!dryRun) {\n fs.writeFileSync(configPath, result.content, \"utf-8\");\n }\n summary.push(`${prefix}Removed glasstrace from ${configFile}`);\n }\n }\n // Handle Codex TOML config separately\n const codexConfigPath = path.join(projectRoot, \".codex\", \"config.toml\");\n if (fs.existsSync(codexConfigPath)) {\n const content = fs.readFileSync(codexConfigPath, \"utf-8\");\n const tomlResult = processTomlMcpConfig(content);\n\n if (tomlResult.action === \"deleted\") {\n if (!dryRun) {\n fs.unlinkSync(codexConfigPath);\n }\n summary.push(`${prefix}Deleted .codex/config.toml`);\n } else if (tomlResult.action === \"removed-section\" && tomlResult.content !== undefined) {\n if (!dryRun) {\n fs.writeFileSync(codexConfigPath, tomlResult.content, \"utf-8\");\n }\n summary.push(`${prefix}Removed glasstrace from .codex/config.toml`);\n }\n }\n\n // Handle Windsurf global config at ~/.codeium/windsurf/mcp_config.json\n // Only process if the project has Windsurf markers, to avoid touching\n // global config for non-Windsurf projects\n const hasWindsurfMarkers =\n fs.existsSync(path.join(projectRoot, \".windsurfrules\")) ||\n fs.existsSync(path.join(projectRoot, \".windsurf\"));\n if (hasWindsurfMarkers) {\n const windsurfConfigPath = path.join(\n os.homedir(),\n \".codeium\",\n \"windsurf\",\n \"mcp_config.json\",\n );\n if (fs.existsSync(windsurfConfigPath)) {\n const content = fs.readFileSync(windsurfConfigPath, \"utf-8\");\n const windsurfResult = processJsonMcpConfig(content);\n\n // Display the path with ~ for the home directory to keep output\n // readable, but derive it from the actual path for accuracy.\n const home = os.homedir();\n const displayPath = windsurfConfigPath.startsWith(home)\n ? \"~\" + windsurfConfigPath.slice(home.length)\n : windsurfConfigPath;\n\n if (windsurfResult.action === \"deleted\") {\n if (!dryRun) {\n fs.unlinkSync(windsurfConfigPath);\n }\n summary.push(\n `${prefix}Deleted global Windsurf config (${displayPath})`,\n );\n } else if (\n windsurfResult.action === \"removed-key\" &&\n windsurfResult.content !== undefined\n ) {\n if (!dryRun) {\n fs.writeFileSync(windsurfConfigPath, windsurfResult.content, \"utf-8\");\n }\n summary.push(\n `${prefix}Removed glasstrace from global Windsurf config (${displayPath})`,\n );\n }\n }\n }\n } catch (err) {\n errors.push(\n `Failed to process MCP config: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n // Step 7: Remove info sections from agent files\n try {\n for (const infoFile of AGENT_INFO_FILES) {\n const filePath = path.join(projectRoot, infoFile);\n if (!fs.existsSync(filePath)) {\n continue;\n }\n\n const content = fs.readFileSync(filePath, \"utf-8\");\n const result = removeMarkerSection(content);\n\n if (result.removed) {\n if (result.content.trim().length === 0) {\n // File is now empty after removing the marker section —\n // only delete if the file was solely glasstrace content\n if (!dryRun) {\n fs.unlinkSync(filePath);\n }\n summary.push(`${prefix}Deleted ${infoFile} (only contained Glasstrace section)`);\n } else {\n if (!dryRun) {\n fs.writeFileSync(filePath, result.content, \"utf-8\");\n }\n summary.push(`${prefix}Removed Glasstrace section from ${infoFile}`);\n }\n }\n }\n } catch (err) {\n errors.push(\n `Failed to process agent info files: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n\n if (summary.length === 0 && errors.length === 0) {\n summary.push(\"No Glasstrace artifacts found — nothing to do.\");\n }\n\n return { exitCode: errors.length > 0 ? 1 : 0, summary, warnings, errors };\n}\n","import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport { AnonApiKeySchema } from \"@glasstrace/protocol\";\nimport type { AnonApiKey } from \"@glasstrace/protocol\";\nimport {\n atomicWriteFileSyncWithTmp,\n fsyncParentDirSync,\n writeAndFsyncTempSync,\n} from \"../atomic-write.js\";\n\n/**\n * Standardized static discovery-file path, served at\n * `<static-root>/.well-known/glasstrace.json` (per RFC 8615) with\n * MIME type `application/json`.\n *\n * The SDK writes the file to this relative path under the\n * framework-specific static root (`public/` for Next.js, Remix, Astro;\n * `static/` for SvelteKit) and the browser extension fetches it from\n * the same path under the deployed origin.\n *\n * @drift-check RFC 8615 (https://www.rfc-editor.org/rfc/rfc8615) + ../glasstrace-product/docs/component-designs/sdk-architecture.md §7.1 Static discovery file\n */\nexport const WELL_KNOWN_GLASSTRACE_PATH = \".well-known/glasstrace.json\" as const;\n\n/**\n * Current schema version for `.well-known/glasstrace.json`. Consumers\n * (primarily the Glasstrace browser extension) MUST tolerate unknown\n * integers >= 1 per the forward-compatibility rule in the design doc\n * (\"SDK Discovery Endpoint / Static File — Component Design\", §5.3).\n */\nexport const DISCOVERY_FILE_VERSION = 1 as const;\n\n/**\n * Schema of the static discovery file written by `sdk init`.\n *\n * Version 1 defines exactly two required fields: `version` and `key`.\n * Additional fields may appear in later schema versions — consumers MUST\n * ignore unknown fields (forward-compatibility) and MUST reject files\n * whose `key` does not match `^gt_anon_[a-f0-9]{48}$`.\n */\nexport interface DiscoveryFileV1 {\n version: typeof DISCOVERY_FILE_VERSION;\n key: AnonApiKey;\n}\n\n/**\n * Detected framework-specific static root. `public` covers Next.js,\n * Remix, and Astro; `static` covers SvelteKit. No other frameworks\n * differ today per the design doc's §4.3 table.\n */\nexport type StaticRootLayout = \"public\" | \"static\";\n\n/**\n * Result returned by {@link resolveStaticRoot} so callers can report the\n * framework-specific path they targeted (used in init summary lines and\n * rollback output).\n */\nexport interface StaticRootResolution {\n /** Absolute path to the static root directory (may not exist yet). */\n absolutePath: string;\n /** Which layout was chosen. */\n layout: StaticRootLayout;\n}\n\n/**\n * Describes the outcome of a single call to {@link writeDiscoveryFile} so\n * callers can surface an accurate summary line without re-reading the\n * file. Mirrors the DISC-1247 Scenario 2 re-init preservation contract:\n * a valid file whose `key` already matches the on-disk anon key is left\n * alone rather than rewritten.\n */\nexport type WriteDiscoveryAction =\n | \"created\"\n | \"updated-stale\"\n | \"skipped-matches\"\n | \"skipped-foreign\"\n | \"failed\";\n\n/**\n * Structured result from {@link writeDiscoveryFile}.\n */\nexport interface WriteDiscoveryResult {\n action: WriteDiscoveryAction;\n /** Absolute path of the discovery file (whether or not it was written). */\n filePath: string;\n /** Static root that was resolved, useful for `.gitignore` wiring. */\n layout: StaticRootLayout;\n /**\n * When `action === \"failed\"`, a short human-readable reason. Never\n * contains anon key bytes — callers can forward it to logs safely.\n */\n error?: string;\n}\n\n/**\n * Detects the project's framework-specific static root using the ordered\n * check from §4.4 of the design doc:\n *\n * 1. Classify as SvelteKit (→ `static/`) when `package.json` declares\n * `\"type\": \"module\"` AND the project contains `svelte.config.js` (or\n * `svelte.config.ts`) OR `src/app.html`. These signals together are\n * specific enough to avoid false positives on generic ESM projects.\n * 2. Otherwise use `public/` — this covers Next.js, Remix, Astro, and\n * plain Node web apps, which all serve `public/` verbatim.\n *\n * Returns the absolute directory path and the chosen layout. Does NOT\n * create the directory; callers use {@link writeDiscoveryFile}, which\n * creates any missing parents atomically.\n *\n * @internal Exported for unit testing only.\n */\nexport function resolveStaticRoot(projectRoot: string): StaticRootResolution {\n if (isSvelteKitProject(projectRoot)) {\n return {\n absolutePath: path.join(projectRoot, \"static\"),\n layout: \"static\",\n };\n }\n return {\n absolutePath: path.join(projectRoot, \"public\"),\n layout: \"public\",\n };\n}\n\n/**\n * Heuristic for SvelteKit detection. The design doc deliberately scopes\n * the check narrowly so a plain ESM library is never misclassified —\n * `svelte.config.{js,ts}` or `src/app.html` is the SvelteKit fingerprint,\n * and both must coexist with an ESM package.json.\n */\nfunction isSvelteKitProject(projectRoot: string): boolean {\n const pkgPath = path.join(projectRoot, \"package.json\");\n let isEsm = false;\n try {\n const pkgContent = fs.readFileSync(pkgPath, \"utf-8\");\n const parsed = JSON.parse(pkgContent) as { type?: unknown };\n isEsm = parsed.type === \"module\";\n } catch {\n // Missing or malformed package.json — fall through to default layout.\n return false;\n }\n if (!isEsm) return false;\n\n const svelteConfigJs = path.join(projectRoot, \"svelte.config.js\");\n const svelteConfigTs = path.join(projectRoot, \"svelte.config.ts\");\n const appHtml = path.join(projectRoot, \"src\", \"app.html\");\n return (\n fs.existsSync(svelteConfigJs) ||\n fs.existsSync(svelteConfigTs) ||\n fs.existsSync(appHtml)\n );\n}\n\n/**\n * Returns the project-relative path of the discovery file for the given\n * layout, suitable for surfacing in summary lines and `.gitignore` entries.\n */\nexport function relativeDiscoveryPath(layout: StaticRootLayout): string {\n const rootDir = layout === \"static\" ? \"static\" : \"public\";\n return `${rootDir}/${WELL_KNOWN_GLASSTRACE_PATH}`;\n}\n\n/**\n * Parses an existing discovery file and returns its key if the schema is\n * valid, or `null` when the file is missing, unreadable, not JSON, or\n * does not match the version-1 shape. The check is deliberately strict —\n * a corrupt or third-party-authored file is treated as \"no file\" so\n * {@link writeDiscoveryFile} overwrites it with a fresh SDK-managed copy.\n *\n * Extra unknown fields are tolerated (§5.3 forward-compatibility).\n *\n * @internal Exported for unit testing only.\n */\nexport function readExistingDiscoveryFile(\n filePath: string,\n): { key: AnonApiKey; extras: Record<string, unknown> } | null {\n let raw: string;\n try {\n raw = fs.readFileSync(filePath, \"utf-8\");\n } catch {\n return null;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return null;\n }\n\n if (\n parsed === null ||\n typeof parsed !== \"object\" ||\n Array.isArray(parsed)\n ) {\n return null;\n }\n\n const obj = parsed as Record<string, unknown>;\n const versionRaw = obj.version;\n if (\n typeof versionRaw !== \"number\" ||\n !Number.isInteger(versionRaw) ||\n versionRaw < 1\n ) {\n return null;\n }\n\n const keyResult = AnonApiKeySchema.safeParse(obj.key);\n if (!keyResult.success) {\n return null;\n }\n\n // Preserve user-added fields (extras) so re-init round-trips any custom\n // keys the consumer added. `version` and `key` are SDK-managed and\n // excluded from the extras object.\n const extras: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(obj)) {\n if (k === \"version\" || k === \"key\") continue;\n extras[k] = v;\n }\n\n return { key: keyResult.data, extras };\n}\n\n/**\n * Serializes the discovery payload deterministically (pretty-printed JSON\n * with a trailing newline). Deterministic output keeps git diffs clean\n * when the file is checked in and matches the atomic-write contract:\n * byte-identical output on re-init when `extras` is unchanged.\n */\nfunction serializeDiscoveryPayload(\n key: AnonApiKey,\n extras: Record<string, unknown>,\n): string {\n // Key ordering: version, key, then extras in their original insertion\n // order. Preserves DISC-1247 Scenario 2 alignment — a user who added\n // `\"note\": \"…\"` after `\"key\"` sees the same ordering on re-init.\n const payload: Record<string, unknown> = {\n version: DISCOVERY_FILE_VERSION,\n key,\n ...extras,\n };\n return JSON.stringify(payload, null, 2) + \"\\n\";\n}\n\n/**\n * Writes the discovery file at `<staticRoot>/.well-known/glasstrace.json`\n * atomically.\n *\n * Behavior (per design doc §6.1 and §6.5):\n *\n * - When the target file does not exist, creates it with `{ version: 1,\n * key: <anonKey> }` after creating the `.well-known/` directory if\n * missing.\n * - When the target exists AND parses as a valid version-1 payload AND\n * its `key` matches the supplied `anonKey`: preserves the file (and\n * any user-added extra fields) and returns `\"skipped-matches\"`.\n * - When the target exists AND parses valid BUT its `key` does not\n * match: rewrites the file with the fresh key, preserving extras.\n * Returns `\"updated-stale\"`.\n * - When the target exists BUT fails to parse (corrupt, foreign-authored,\n * wrong schema): rewrites with a fresh SDK-managed payload and returns\n * `\"skipped-foreign\"` to signal that user content was not preserved.\n * - On any unexpected I/O error: returns `\"failed\"` with an error string.\n *\n * Uses a sibling temp file + `renameSync` for atomicity so concurrent\n * readers (e.g., a browser extension polling during dev server startup)\n * never observe a half-written file.\n *\n * @param projectRoot - Absolute path to the project root directory.\n * @param anonKey - The anon key currently on disk (see `anon-key.ts`).\n */\nexport function writeDiscoveryFile(\n projectRoot: string,\n anonKey: AnonApiKey,\n): WriteDiscoveryResult {\n const { absolutePath: staticRoot, layout } = resolveStaticRoot(projectRoot);\n const wellKnownDir = path.join(staticRoot, \".well-known\");\n const filePath = path.join(wellKnownDir, \"glasstrace.json\");\n\n let existingAction: WriteDiscoveryAction;\n let extras: Record<string, unknown> = {};\n\n if (fs.existsSync(filePath)) {\n const existing = readExistingDiscoveryFile(filePath);\n if (existing === null) {\n // Unreadable / malformed / non-SDK content — overwrite with a\n // fresh payload so the extension can discover the current key.\n // Extras are NOT preserved because we cannot safely parse them.\n existingAction = \"skipped-foreign\";\n } else if (existing.key === anonKey) {\n // Valid and already matches — leave the file alone (§6.5 step 2).\n return {\n action: \"skipped-matches\",\n filePath,\n layout,\n };\n } else {\n // Valid but stale — replace the key, preserve extras (§6.5 step 3).\n extras = existing.extras;\n existingAction = \"updated-stale\";\n }\n } else {\n existingAction = \"created\";\n }\n\n const tmpPath = `${filePath}.tmp-${process.pid}`;\n // On Windows, `renameSync` fails with EPERM/EEXIST when the\n // destination already exists. Rather than `unlink` the destination\n // first (which would cause data loss if the subsequent rename fails),\n // move the destination to a sibling backup path, commit the rename,\n // and only then delete the backup. If the rename fails, restore the\n // backup so the original file is preserved.\n const needsWindowsReplace =\n process.platform === \"win32\" && fs.existsSync(filePath);\n const backupPath = needsWindowsReplace\n ? `${filePath}.bak-${process.pid}`\n : null;\n\n try {\n fs.mkdirSync(wellKnownDir, { recursive: true });\n const payload = serializeDiscoveryPayload(anonKey, extras);\n\n if (backupPath !== null) {\n // Windows backup-rollback path. Step the SDK 2.0 §4.3 protocol\n // out manually so the backup rename can interleave between the\n // tmp-fsync and the final rename:\n // 1. write tmp + fsync(tmp)\n // 2. rename existing → backup (Windows requires destination\n // to be free)\n // 3. rename tmp → final\n // 4. fsync(parent) so the rename pair is durable\n // Mode is 0o644 for static/discoverable files per spec §4.3.\n writeAndFsyncTempSync(tmpPath, payload, {\n encoding: \"utf-8\",\n mode: 0o644,\n });\n fs.renameSync(filePath, backupPath);\n try {\n fs.renameSync(tmpPath, filePath);\n } catch (renameErr) {\n try {\n fs.renameSync(backupPath, filePath);\n } catch {\n // Restoration failed; nothing more we can do. Surface the\n // original rename error below so the caller sees the cause.\n }\n throw renameErr;\n }\n fsyncParentDirSync(filePath);\n try {\n fs.unlinkSync(backupPath);\n } catch {\n // Backup cleanup is best-effort; a stale `.bak-<pid>` is\n // preferable to a spurious failure after a successful write.\n }\n } else {\n // Non-Windows / no pre-existing target: full helper composes\n // tmp + fsync(tmp) + rename + fsync(parent) atomically.\n atomicWriteFileSyncWithTmp(filePath, tmpPath, payload, {\n encoding: \"utf-8\",\n mode: 0o644,\n });\n }\n\n return { action: existingAction, filePath, layout };\n } catch (err) {\n // Best-effort: remove the temp file if it was created before the\n // failure so a stale `.tmp-<pid>` does not clutter `.well-known/`.\n try {\n if (fs.existsSync(tmpPath)) {\n fs.unlinkSync(tmpPath);\n }\n } catch {\n // Swallow: the write has already failed; do not mask the root cause.\n }\n return {\n action: \"failed\",\n filePath,\n layout,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n}\n\n/**\n * Describes the outcome of {@link removeDiscoveryFile}. `\"removed\"` means\n * the file existed and was deleted; `\"not-found\"` means there was nothing\n * to remove (no error). `\"failed\"` preserves an error string.\n */\nexport type RemoveDiscoveryAction = \"removed\" | \"not-found\" | \"failed\";\n\n/** Structured result from {@link removeDiscoveryFile}. */\nexport interface RemoveDiscoveryResult {\n action: RemoveDiscoveryAction;\n filePath: string;\n layout: StaticRootLayout;\n /** True when the enclosing `.well-known/` directory was removed too. */\n directoryRemoved: boolean;\n error?: string;\n}\n\n/**\n * Removes the discovery file written by {@link writeDiscoveryFile} if\n * present, and removes the enclosing `.well-known/` directory when it\n * becomes empty. Tolerant of missing files, missing directories, and\n * user-owned sibling content inside `.well-known/` (never deletes a\n * sibling file).\n *\n * Checks BOTH `public/.well-known/glasstrace.json` and\n * `static/.well-known/glasstrace.json` rather than only the\n * currently-inferred layout: if layout detection changes between\n * init and uninit (for example, a SvelteKit project has its\n * `package.json` modified so the heuristic no longer matches),\n * the file written under the original layout would otherwise\n * be orphaned.\n *\n * Matches the uninit contract from design doc §6.4.\n *\n * @param projectRoot - Absolute path to the project root directory.\n */\nexport function removeDiscoveryFile(\n projectRoot: string,\n): RemoveDiscoveryResult {\n const { layout: inferredLayout } = resolveStaticRoot(projectRoot);\n\n // Sweep both candidate layouts so an orphaned file in the non-inferred\n // location is still cleaned up. The returned layout describes where\n // a file was actually removed (preferring the inferred layout when a\n // file existed in both, which is not a supported state but is\n // tolerated); when neither layout had a file, the returned layout\n // mirrors the inferred one so callers surface a stable relative path.\n const layouts: StaticRootLayout[] = [\"public\", \"static\"];\n\n interface LayoutOutcome {\n layout: StaticRootLayout;\n filePath: string;\n removed: boolean;\n directoryRemoved: boolean;\n }\n const outcomes: LayoutOutcome[] = [];\n\n for (const layout of layouts) {\n const staticRoot = path.join(projectRoot, layout);\n const wellKnownDir = path.join(staticRoot, \".well-known\");\n const filePath = path.join(wellKnownDir, \"glasstrace.json\");\n\n let removed = false;\n try {\n if (fs.existsSync(filePath)) {\n fs.unlinkSync(filePath);\n removed = true;\n }\n } catch (err) {\n return {\n action: \"failed\",\n filePath,\n layout,\n directoryRemoved: false,\n error: err instanceof Error ? err.message : String(err),\n };\n }\n\n // Only attempt to prune the enclosing `.well-known/` when we actually\n // removed the discovery file from this layout. Pruning unconditionally\n // would delete a user-owned empty directory (that Glasstrace never\n // populated) as a silent side effect of `sdk uninit`.\n let directoryRemoved = false;\n if (removed) {\n try {\n if (fs.existsSync(wellKnownDir)) {\n const entries = fs.readdirSync(wellKnownDir);\n if (entries.length === 0) {\n fs.rmdirSync(wellKnownDir);\n directoryRemoved = true;\n }\n }\n } catch {\n // Best-effort cleanup; never surface as an error to uninit.\n }\n }\n\n outcomes.push({ layout, filePath, removed, directoryRemoved });\n }\n\n // Pick the outcome to report: prefer one where a file was removed. When\n // both layouts had a file (not a supported state, but tolerated),\n // prefer the inferred layout. When neither had a file, report the\n // inferred layout so callers receive a stable relative path.\n const removals = outcomes.filter((o) => o.removed);\n const chosen: LayoutOutcome = (() => {\n if (removals.length === 0) {\n return (\n outcomes.find((o) => o.layout === inferredLayout) ?? outcomes[0]!\n );\n }\n if (removals.length === 1) return removals[0]!;\n return (\n removals.find((o) => o.layout === inferredLayout) ?? removals[0]!\n );\n })();\n\n // Propagate directoryRemoved across both sweeps so the uninit summary\n // reflects every pruned directory even when only one was the primary.\n const anyDirectoryRemoved = outcomes.some((o) => o.directoryRemoved);\n\n return {\n action: removals.length > 0 ? \"removed\" : \"not-found\",\n filePath: chosen.filePath,\n layout: chosen.layout,\n directoryRemoved: chosen.directoryRemoved || anyDirectoryRemoved,\n };\n}\n"],"mappings":"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","names":["fs","path"]}
@@ -0,0 +1,208 @@
1
+ // src/agent-detection/inject.ts
2
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
3
+ import { dirname, isAbsolute, join } from "node:path";
4
+ var HTML_START_RE = /^<!--\s*glasstrace:mcp:start(?:\s+v=([^\s>]+))?\s*-->$/;
5
+ var HTML_END = "<!-- glasstrace:mcp:end -->";
6
+ var HASH_START_RE = /^#\s*glasstrace:mcp:start(?:\s+v=(\S+))?$/;
7
+ var HASH_END = "# glasstrace:mcp:end";
8
+ function parseStartMarkerLine(line) {
9
+ const trimmed = line.trim();
10
+ const html = HTML_START_RE.exec(trimmed);
11
+ if (html !== null) {
12
+ return { kind: "html", stamp: html[1] ?? null };
13
+ }
14
+ const hash = HASH_START_RE.exec(trimmed);
15
+ if (hash !== null) {
16
+ return { kind: "hash", stamp: hash[1] ?? null };
17
+ }
18
+ return null;
19
+ }
20
+ function isEndMarker(line) {
21
+ const trimmed = line.trim();
22
+ return trimmed === HTML_END || trimmed === HASH_END;
23
+ }
24
+ function isEndMarkerLine(line) {
25
+ return isEndMarker(line);
26
+ }
27
+ function isPermissionError(err) {
28
+ const code = err.code;
29
+ return code === "EACCES" || code === "EPERM" || code === "EROFS";
30
+ }
31
+ async function writeMcpConfig(agent, content, projectRoot) {
32
+ if (agent.mcpConfigPath === null) {
33
+ return;
34
+ }
35
+ const configPath = agent.mcpConfigPath;
36
+ const parentDir = dirname(configPath);
37
+ try {
38
+ await mkdir(parentDir, { recursive: true });
39
+ } catch (err) {
40
+ if (isPermissionError(err)) {
41
+ process.stderr.write(
42
+ `Warning: cannot create directory ${parentDir}: permission denied
43
+ `
44
+ );
45
+ return;
46
+ }
47
+ throw err;
48
+ }
49
+ try {
50
+ await writeFile(configPath, content, { mode: 384 });
51
+ } catch (err) {
52
+ if (isPermissionError(err)) {
53
+ process.stderr.write(
54
+ `Warning: cannot write config file ${configPath}: permission denied
55
+ `
56
+ );
57
+ return;
58
+ }
59
+ throw err;
60
+ }
61
+ try {
62
+ await chmod(configPath, 384);
63
+ } catch {
64
+ }
65
+ }
66
+ function findMarkerBoundaries(lines) {
67
+ let startIdx = -1;
68
+ for (let i = 0; i < lines.length; i++) {
69
+ if (parseStartMarkerLine(lines[i]) !== null) {
70
+ startIdx = i;
71
+ } else if (startIdx !== -1 && isEndMarker(lines[i])) {
72
+ return { startIdx, endIdx: i };
73
+ }
74
+ }
75
+ return null;
76
+ }
77
+ async function injectInfoSection(agent, content, projectRoot) {
78
+ if (agent.infoFilePath === null) {
79
+ return;
80
+ }
81
+ if (content === "") {
82
+ return;
83
+ }
84
+ const filePath = agent.infoFilePath;
85
+ let existingContent = null;
86
+ try {
87
+ existingContent = await readFile(filePath, "utf-8");
88
+ } catch (err) {
89
+ const code = err.code;
90
+ if (code !== "ENOENT") {
91
+ if (isPermissionError(err)) {
92
+ process.stderr.write(
93
+ `Warning: cannot read info file ${filePath}: permission denied
94
+ `
95
+ );
96
+ return;
97
+ }
98
+ throw err;
99
+ }
100
+ }
101
+ if (existingContent === null) {
102
+ try {
103
+ await mkdir(dirname(filePath), { recursive: true });
104
+ await writeFile(filePath, content, "utf-8");
105
+ } catch (err) {
106
+ if (isPermissionError(err)) {
107
+ process.stderr.write(
108
+ `Warning: cannot write info file ${filePath}: permission denied
109
+ `
110
+ );
111
+ return;
112
+ }
113
+ throw err;
114
+ }
115
+ return;
116
+ }
117
+ const lines = existingContent.split("\n");
118
+ const boundaries = findMarkerBoundaries(lines);
119
+ let newContent;
120
+ if (boundaries !== null) {
121
+ const before = lines.slice(0, boundaries.startIdx);
122
+ const after = lines.slice(boundaries.endIdx + 1);
123
+ const contentWithoutTrailingNewline = content.endsWith("\n") ? content.slice(0, -1) : content;
124
+ newContent = [...before, contentWithoutTrailingNewline, ...after].join("\n");
125
+ } else {
126
+ const separator = existingContent.endsWith("\n") ? "\n" : "\n\n";
127
+ newContent = existingContent + separator + content;
128
+ }
129
+ try {
130
+ await writeFile(filePath, newContent, "utf-8");
131
+ } catch (err) {
132
+ if (isPermissionError(err)) {
133
+ process.stderr.write(
134
+ `Warning: cannot write info file ${filePath}: permission denied
135
+ `
136
+ );
137
+ return;
138
+ }
139
+ throw err;
140
+ }
141
+ }
142
+ async function hasManagedSection(filePath) {
143
+ let content;
144
+ try {
145
+ content = await readFile(filePath, "utf-8");
146
+ } catch (err) {
147
+ const code = err.code;
148
+ if (code === "ENOENT") return false;
149
+ throw err;
150
+ }
151
+ return findMarkerBoundaries(content.split("\n")) !== null;
152
+ }
153
+ async function updateGitignore(paths, projectRoot) {
154
+ const gitignorePath = join(projectRoot, ".gitignore");
155
+ const relativePaths = paths.filter((p) => !isAbsolute(p));
156
+ if (relativePaths.length === 0) {
157
+ return;
158
+ }
159
+ let existingContent = "";
160
+ try {
161
+ existingContent = await readFile(gitignorePath, "utf-8");
162
+ } catch (err) {
163
+ const code = err.code;
164
+ if (code !== "ENOENT") {
165
+ if (isPermissionError(err)) {
166
+ process.stderr.write(
167
+ `Warning: cannot read .gitignore: permission denied
168
+ `
169
+ );
170
+ return;
171
+ }
172
+ throw err;
173
+ }
174
+ }
175
+ const existingLines = existingContent.split("\n").map((line) => line.trim()).filter((line) => line !== "");
176
+ const existingSet = new Set(existingLines);
177
+ const toAdd = relativePaths.map((p) => p.trim().replace(/\\/g, "/")).filter((p) => p !== "" && !existingSet.has(p));
178
+ if (toAdd.length === 0) {
179
+ return;
180
+ }
181
+ let updatedContent = existingContent;
182
+ if (updatedContent.length > 0 && !updatedContent.endsWith("\n")) {
183
+ updatedContent += "\n";
184
+ }
185
+ updatedContent += toAdd.join("\n") + "\n";
186
+ try {
187
+ await writeFile(gitignorePath, updatedContent, "utf-8");
188
+ } catch (err) {
189
+ if (isPermissionError(err)) {
190
+ process.stderr.write(
191
+ `Warning: cannot write .gitignore: permission denied
192
+ `
193
+ );
194
+ return;
195
+ }
196
+ throw err;
197
+ }
198
+ }
199
+
200
+ export {
201
+ parseStartMarkerLine,
202
+ isEndMarkerLine,
203
+ writeMcpConfig,
204
+ injectInfoSection,
205
+ hasManagedSection,
206
+ updateGitignore
207
+ };
208
+ //# sourceMappingURL=chunk-ZBQQXVHD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/agent-detection/inject.ts"],"sourcesContent":["import { chmod, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, isAbsolute, join } from \"node:path\";\nimport type { DetectedAgent } from \"./detect.js\";\n\n/**\n * HTML start-marker regex used in markdown files (.md). Matches both\n * legacy unstamped markers (pre-SDK-050) and stamped markers (SDK-050+).\n *\n * Two shapes:\n * - Legacy: `<!-- glasstrace:mcp:start -->`\n * - Stamped: `<!-- glasstrace:mcp:start v=1.4.0 -->`\n *\n * The optional `v=<semver>` capture group is the SDK-050 version stamp\n * (DISC-1592 Required Semantics Item 1). Recognising the legacy form is\n * load-bearing for the SDK-050 backward-compatibility constraint: an\n * upgrading user's first re-render must replace the existing block in\n * place rather than appending a duplicate. Subsequent re-renders write\n * the stamped form.\n *\n * The stamp character class\n * `[^\\s>]+` deliberately excludes whitespace and `>` so a hand-edited\n * malformed marker cannot terminate the comment early or smuggle a\n * line break into the file. The end marker (`...mcp:end`) is unstamped.\n */\nconst HTML_START_RE =\n /^<!--\\s*glasstrace:mcp:start(?:\\s+v=([^\\s>]+))?\\s*-->$/;\nconst HTML_END = \"<!-- glasstrace:mcp:end -->\";\n\n/**\n * Hash-prefixed start-marker regex used in plain text files (e.g.\n * `.cursorrules`). Same legacy/stamped shape model as the HTML form,\n * with the constraint that the captured stamp is non-whitespace\n * (`\\S+`) — the line ends at end-of-line, so there is no closing\n * delimiter to escape.\n */\nconst HASH_START_RE = /^#\\s*glasstrace:mcp:start(?:\\s+v=(\\S+))?$/;\nconst HASH_END = \"# glasstrace:mcp:end\";\n\n/**\n * Parsed start marker — its kind (HTML vs hash) and, when present, the\n * `v=<sdkVersion>` stamp. `stamp === null` means the marker matched the\n * legacy unstamped form (pre-SDK-050).\n */\nexport interface ParsedStartMarker {\n kind: \"html\" | \"hash\";\n stamp: string | null;\n}\n\n/**\n * Parses a single line as a Glasstrace start marker.\n *\n * Accepts both legacy unstamped markers (pre-SDK-050) and stamped\n * markers (SDK-050+). Returns `null` if the line is not a start\n * marker. Trims whitespace before matching so leading/trailing spaces\n * in user-edited files do not defeat detection.\n *\n * Exported so the upgrade-notice module (which checks the start\n * marker line directly) can share the regex, keeping a single source\n * of truth for the marker shape.\n */\nexport function parseStartMarkerLine(\n line: string,\n): ParsedStartMarker | null {\n const trimmed = line.trim();\n const html = HTML_START_RE.exec(trimmed);\n if (html !== null) {\n return { kind: \"html\", stamp: html[1] ?? null };\n }\n const hash = HASH_START_RE.exec(trimmed);\n if (hash !== null) {\n return { kind: \"hash\", stamp: hash[1] ?? null };\n }\n return null;\n}\n\nfunction isEndMarker(line: string): boolean {\n const trimmed = line.trim();\n return trimmed === HTML_END || trimmed === HASH_END;\n}\n\n/**\n * Public alias for {@link isEndMarker}, used by the upgrade-notice\n * module to confirm that a stamped start marker has a matching end\n * before classifying the file as having a managed section. Exported\n * only for cross-module reuse within `agent-detection/`; not part of\n * the public SDK surface.\n */\nexport function isEndMarkerLine(line: string): boolean {\n return isEndMarker(line);\n}\n\n/**\n * Determines whether an error is a filesystem permission or read-only error.\n * Covers EACCES (permission denied), EPERM (operation not permitted), and\n * EROFS (read-only filesystem) to handle containerized/mounted environments.\n */\nfunction isPermissionError(err: unknown): boolean {\n const code = (err as NodeJS.ErrnoException).code;\n return code === \"EACCES\" || code === \"EPERM\" || code === \"EROFS\";\n}\n\n/**\n * Writes MCP configuration content to an agent's config file path.\n *\n * Creates parent directories as needed and sets file permissions to 0o600\n * (owner read/write only) since config files may contain auth tokens.\n *\n * Fails gracefully: logs a warning to stderr on permission errors instead\n * of throwing.\n *\n * @param agent - The detected agent whose config path to write to.\n * @param content - The full configuration file content.\n * @param projectRoot - The project root (reserved for future use).\n */\nexport async function writeMcpConfig(\n agent: DetectedAgent,\n content: string,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n projectRoot: string,\n): Promise<void> {\n if (agent.mcpConfigPath === null) {\n return;\n }\n\n const configPath = agent.mcpConfigPath;\n const parentDir = dirname(configPath);\n\n try {\n await mkdir(parentDir, { recursive: true });\n } catch (err: unknown) {\n if (isPermissionError(err)) {\n process.stderr.write(\n `Warning: cannot create directory ${parentDir}: permission denied\\n`,\n );\n return;\n }\n throw err;\n }\n\n try {\n await writeFile(configPath, content, { mode: 0o600 });\n } catch (err: unknown) {\n if (isPermissionError(err)) {\n process.stderr.write(\n `Warning: cannot write config file ${configPath}: permission denied\\n`,\n );\n return;\n }\n throw err;\n }\n\n // Ensure permissions are set even if the file already existed\n // (writeFile mode only applies to newly created files on some platforms)\n try {\n await chmod(configPath, 0o600);\n } catch {\n // Best-effort; the writeFile mode should have handled this\n }\n}\n\n/**\n * Finds existing marker boundaries in file content.\n *\n * Recognises both the legacy unstamped marker form (pre-SDK-050) and\n * the stamped form (SDK-050+) for both HTML-comment and hash-prefix\n * conventions. Returns the start and end line indices, or `null` if no\n * complete marker pair is found. The `v=<sdkVersion>` stamp itself is\n * only inspected by the upgrade-notice module via\n * {@link parseStartMarkerLine}; in-place replacement only needs the\n * line indices.\n *\n * When multiple start markers appear before the first end marker\n * (e.g. a quoted example of the marker shape earlier in the file\n * followed by the real managed block), the boundary anchors to the\n * MOST RECENT start preceding the end. This matches the pre-SDK-050\n * behaviour of `findMarkerBoundaries` and avoids the \"swallow the\n * user's example into the replacement\" failure mode that anchoring\n * to the FIRST start would produce.\n */\nfunction findMarkerBoundaries(\n lines: string[],\n): { startIdx: number; endIdx: number } | null {\n let startIdx = -1;\n\n for (let i = 0; i < lines.length; i++) {\n if (parseStartMarkerLine(lines[i]) !== null) {\n // Track the most recent start so a quoted/example marker earlier\n // in the file does not capture the replacement window.\n startIdx = i;\n } else if (startIdx !== -1 && isEndMarker(lines[i])) {\n return { startIdx, endIdx: i };\n }\n }\n\n return null;\n}\n\n/**\n * Injects an informational section into an agent's instruction file.\n *\n * Uses marker comments to enable idempotent updates:\n * - If the file contains marker pairs, replaces content between them.\n * - If the file exists but has no markers, appends the section.\n * - If the file does not exist, creates it with the section content.\n *\n * The boundary detector recognises both legacy unstamped markers\n * (pre-SDK-050) and stamped markers, so an upgrading user's first\n * re-render replaces the existing block in place rather than\n * appending a duplicate (DISC-1592 / SDK-050 backward-compatibility\n * constraint). Subsequent re-renders write the stamped form.\n *\n * Fails gracefully: logs a warning to stderr on read-only files instead\n * of throwing.\n *\n * @param agent - The detected agent whose info file to update.\n * @param content - The section content (including markers).\n * @param projectRoot - The project root (reserved for future use).\n */\nexport async function injectInfoSection(\n agent: DetectedAgent,\n content: string,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n projectRoot: string,\n): Promise<void> {\n if (agent.infoFilePath === null) {\n return;\n }\n\n // Empty content means nothing to inject (e.g., agents without info sections)\n if (content === \"\") {\n return;\n }\n\n const filePath = agent.infoFilePath;\n\n let existingContent: string | null = null;\n try {\n existingContent = await readFile(filePath, \"utf-8\");\n } catch (err: unknown) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code !== \"ENOENT\") {\n if (isPermissionError(err)) {\n process.stderr.write(\n `Warning: cannot read info file ${filePath}: permission denied\\n`,\n );\n return;\n }\n throw err;\n }\n }\n\n // File does not exist — create with section content\n if (existingContent === null) {\n try {\n await mkdir(dirname(filePath), { recursive: true });\n await writeFile(filePath, content, \"utf-8\");\n } catch (err: unknown) {\n if (isPermissionError(err)) {\n process.stderr.write(\n `Warning: cannot write info file ${filePath}: permission denied\\n`,\n );\n return;\n }\n throw err;\n }\n return;\n }\n\n // File exists — check for markers\n const lines = existingContent.split(\"\\n\");\n const boundaries = findMarkerBoundaries(lines);\n\n let newContent: string;\n if (boundaries !== null) {\n // Replace everything from start marker through end marker (inclusive)\n const before = lines.slice(0, boundaries.startIdx);\n const after = lines.slice(boundaries.endIdx + 1);\n // content already includes markers and trailing newline\n const contentWithoutTrailingNewline = content.endsWith(\"\\n\")\n ? content.slice(0, -1)\n : content;\n newContent = [...before, contentWithoutTrailingNewline, ...after].join(\"\\n\");\n } else {\n // No markers found — append with a blank line separator\n const separator = existingContent.endsWith(\"\\n\") ? \"\\n\" : \"\\n\\n\";\n newContent = existingContent + separator + content;\n }\n\n try {\n await writeFile(filePath, newContent, \"utf-8\");\n } catch (err: unknown) {\n if (isPermissionError(err)) {\n process.stderr.write(\n `Warning: cannot write info file ${filePath}: permission denied\\n`,\n );\n return;\n }\n throw err;\n }\n}\n\n/**\n * Returns true when the file at `filePath` contains a complete\n * Glasstrace managed section (marker pair). Matches both legacy\n * unstamped markers and SDK-050+ stamped markers. Used by the\n * upgrade-instructions CLI to decide which detected agent files\n * actually have a managed section to refresh.\n *\n * Returns `false` for the genuine \"file does not exist\" case\n * (`ENOENT`). Any other read error — permission denied (`EACCES`),\n * not-permitted (`EPERM`), read-only (`EROFS`), I/O error, etc. — is\n * re-thrown so the caller can surface it to the user. Swallowing\n * those would let `upgrade-instructions` report an inaccessible file\n * as \"skipped (no managed section present)\" and silently mask a real\n * permission problem (Codex review on PR #247).\n */\nexport async function hasManagedSection(filePath: string): Promise<boolean> {\n let content: string;\n try {\n content = await readFile(filePath, \"utf-8\");\n } catch (err: unknown) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"ENOENT\") return false;\n throw err;\n }\n return findMarkerBoundaries(content.split(\"\\n\")) !== null;\n}\n\n/**\n * Ensures that the given paths are listed in the project's `.gitignore`.\n *\n * Only adds entries for paths that are not already present. Creates the\n * `.gitignore` file if it does not exist. Skips absolute paths (e.g.,\n * Windsurf's global config) since those are outside the project tree.\n *\n * Fails gracefully: logs a warning to stderr on permission errors.\n *\n * @param paths - Relative paths to ensure are gitignored.\n * @param projectRoot - The project root directory.\n */\nexport async function updateGitignore(\n paths: string[],\n projectRoot: string,\n): Promise<void> {\n const gitignorePath = join(projectRoot, \".gitignore\");\n\n // Filter out absolute paths — they reference locations outside the project\n // Uses isAbsolute() to handle both POSIX and Windows path formats\n const relativePaths = paths.filter((p) => !isAbsolute(p));\n\n if (relativePaths.length === 0) {\n return;\n }\n\n let existingContent = \"\";\n try {\n existingContent = await readFile(gitignorePath, \"utf-8\");\n } catch (err: unknown) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code !== \"ENOENT\") {\n if (isPermissionError(err)) {\n process.stderr.write(\n `Warning: cannot read .gitignore: permission denied\\n`,\n );\n return;\n }\n throw err;\n }\n }\n\n // Parse existing entries, trimming whitespace for comparison\n const existingLines = existingContent\n .split(\"\\n\")\n .map((line) => line.trim())\n .filter((line) => line !== \"\");\n\n const existingSet = new Set(existingLines);\n\n // Normalize entries: trim whitespace, convert backslashes to forward slashes\n // (git ignore patterns use / as separator; backslash is an escape character),\n // drop empties, and deduplicate against existing entries.\n const toAdd = relativePaths\n .map((p) => p.trim().replace(/\\\\/g, \"/\"))\n .filter((p) => p !== \"\" && !existingSet.has(p));\n\n if (toAdd.length === 0) {\n return;\n }\n\n // Ensure file ends with newline before appending\n let updatedContent = existingContent;\n if (updatedContent.length > 0 && !updatedContent.endsWith(\"\\n\")) {\n updatedContent += \"\\n\";\n }\n\n updatedContent += toAdd.join(\"\\n\") + \"\\n\";\n\n try {\n await writeFile(gitignorePath, updatedContent, \"utf-8\");\n } catch (err: unknown) {\n if (isPermissionError(err)) {\n process.stderr.write(\n `Warning: cannot write .gitignore: permission denied\\n`,\n );\n return;\n }\n throw err;\n }\n}\n"],"mappings":"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","names":[]}