@glasstrace/sdk 1.10.2 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -5
- package/dist/{chunk-QS5RZ2TC.js → chunk-DQFGNX3H.js} +13 -8
- package/dist/{chunk-QS5RZ2TC.js.map → chunk-DQFGNX3H.js.map} +1 -1
- package/dist/{chunk-UMGZJYC4.js → chunk-FQ4SEG6Y.js} +8 -3
- package/dist/chunk-FQ4SEG6Y.js.map +1 -0
- package/dist/{chunk-CIKPFJOM.js → chunk-KOYZJN6G.js} +310 -20
- package/dist/chunk-KOYZJN6G.js.map +1 -0
- package/dist/{chunk-ZBQQXVHD.js → chunk-YIEXKQYP.js} +2 -67
- package/dist/chunk-YIEXKQYP.js.map +1 -0
- package/dist/cli/init.cjs +453 -126
- package/dist/cli/init.cjs.map +1 -1
- package/dist/cli/init.js +29 -16
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/mcp-add.cjs +339 -97
- package/dist/cli/mcp-add.cjs.map +1 -1
- package/dist/cli/mcp-add.js +32 -14
- package/dist/cli/mcp-add.js.map +1 -1
- package/dist/cli/status.cjs +6 -1
- package/dist/cli/status.cjs.map +1 -1
- package/dist/cli/status.js +7 -2
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/uninit.cjs +6 -1
- package/dist/cli/uninit.cjs.map +1 -1
- package/dist/cli/uninit.js +2 -2
- package/dist/cli/upgrade-instructions.cjs +383 -112
- package/dist/cli/upgrade-instructions.cjs.map +1 -1
- package/dist/cli/upgrade-instructions.js +70 -18
- package/dist/cli/upgrade-instructions.js.map +1 -1
- package/dist/index.cjs +11 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/node-entry.cjs +11 -6
- package/dist/node-entry.cjs.map +1 -1
- package/dist/node-entry.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-CIKPFJOM.js.map +0 -1
- package/dist/chunk-UMGZJYC4.js.map +0 -1
- package/dist/chunk-ZBQQXVHD.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/cli/upgrade-instructions.ts","../../src/mcp-runtime.ts","../../src/agent-detection/detect.ts","../../src/agent-detection/agent-instruction-text.ts","../../src/agent-detection/configs.ts","../../src/agent-detection/inject.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { isAbsolute, relative } from \"node:path\";\nimport { MCP_ENDPOINT } from \"../mcp-runtime.js\";\nimport { detectAgents } from \"../agent-detection/detect.js\";\nimport { generateInfoSection } from \"../agent-detection/configs.js\";\nimport {\n hasManagedSection,\n injectInfoSection,\n} from \"../agent-detection/inject.js\";\n\n// Declare the tsup-injected SDK version literal. Replaced at build time\n// via `define` in tsup.config.ts. Falls back to \"0.0.0-dev\" when\n// running tests under vitest (no tsup build step).\ndeclare const __SDK_VERSION__: string;\n\n/**\n * Options for {@link runUpgradeInstructions}. The CLI entry point in\n * `init.ts` wires `process.cwd()` through `resolveProjectRoot()` so\n * monorepo roots resolve to the active app directory; tests pass an\n * explicit `projectRoot` for isolation.\n */\nexport interface UpgradeInstructionsOptions {\n projectRoot: string;\n}\n\n/**\n * Result of running the upgrade-instructions command. Returned to the\n * CLI entry point so it can render output without forcing the core\n * logic to call `process.stderr.write` / `process.exit`.\n */\nexport interface UpgradeInstructionsResult {\n exitCode: number;\n /**\n * Files whose managed Glasstrace section was refreshed in place.\n * Reported as paths relative to {@link UpgradeInstructionsOptions.projectRoot}\n * so the CLI output stays portable across machines and developer\n * homes; an absolute path is returned only when the detected file\n * lives outside the resolved project root (e.g. Windsurf's global\n * config under `$HOME/.codeium/`), where a relative form would be\n * misleading.\n */\n refreshed: string[];\n /**\n * Files inspected that did not contain a managed section, and were\n * therefore left untouched. Reported so the user can verify the\n * command did not accidentally append a block to a hand-written\n * instruction file. Same path-shape rule as\n * {@link UpgradeInstructionsResult.refreshed}.\n */\n skipped: string[];\n /**\n * Soft warnings (e.g. permission errors handled internally by\n * `injectInfoSection`). One line per issue.\n */\n warnings: string[];\n /**\n * Hard errors that prevented the command from completing.\n */\n errors: string[];\n}\n\n/**\n * Refreshes the managed Glasstrace MCP section in every detected agent\n * instruction file in the project (DISC-1592 / SDK-050 §Required\n * Semantics Item 2). Idempotent and safe to re-run; the helper only\n * touches files that already contain a marker pair, so a hand-written\n * `CLAUDE.md` without a Glasstrace block is left alone.\n *\n * Multi-file projects are handled in a single run (DISC-1592 §Multi-file\n * projects): the same `detectAgents()` call that scaffolds files at\n * `init` time enumerates every detected agent, and this function\n * refreshes every file with a managed section in one pass.\n *\n * The replace-in-place behaviour works for both legacy unstamped\n * markers (pre-SDK-050) and SDK-050+ stamped markers — see\n * `findMarkerBoundaries` in `inject.ts`.\n *\n * @param options - Project root to operate on. The CLI entry point\n * resolves monorepo roots before calling this function.\n */\n/**\n * Renders an absolute file path in a form suitable for CLI output:\n * relative to `projectRoot` when the file lives inside the tree, or\n * the original absolute path otherwise. Keeps output portable for\n * normal in-tree files (`CLAUDE.md`, `.cursorrules`, `codex.md`) while\n * preserving full paths for out-of-tree targets like Windsurf's\n * global config (`$HOME/.codeium/windsurf/mcp_config.json`), where a\n * relative form (e.g. `../../../../home/.../mcp_config.json`) would\n * be harder to read than the absolute path.\n */\nfunction formatPathForOutput(filePath: string, projectRoot: string): string {\n const rel = relative(projectRoot, filePath);\n if (rel === \"\" || rel.startsWith(\"..\") || isAbsolute(rel)) {\n return filePath;\n }\n return rel;\n}\n\nexport async function runUpgradeInstructions(\n options: UpgradeInstructionsOptions,\n): Promise<UpgradeInstructionsResult> {\n const refreshed: string[] = [];\n const skipped: string[] = [];\n const warnings: string[] = [];\n const errors: string[] = [];\n\n let agents;\n try {\n agents = await detectAgents(options.projectRoot);\n } catch (err) {\n errors.push(\n `Failed to detect agents: ${err instanceof Error ? err.message : String(err)}`,\n );\n return { exitCode: 1, refreshed, skipped, warnings, errors };\n }\n\n const sdkVersion =\n typeof __SDK_VERSION__ === \"string\" ? __SDK_VERSION__ : \"0.0.0-dev\";\n\n for (const agent of agents) {\n if (agent.infoFilePath === null) {\n // Generic / gemini / windsurf, or detected agent whose info\n // file does not exist on disk — nothing to refresh.\n continue;\n }\n\n const displayPath = formatPathForOutput(\n agent.infoFilePath,\n options.projectRoot,\n );\n\n let containsSection: boolean;\n try {\n containsSection = await hasManagedSection(agent.infoFilePath);\n } catch (err) {\n // hasManagedSection swallows read errors and returns false, so\n // this branch is defensive against a future refactor.\n warnings.push(\n `Could not inspect ${displayPath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n continue;\n }\n\n if (!containsSection) {\n // The agent was detected (marker file present) but the\n // instruction file has no Glasstrace managed section. Refusing\n // to inject prevents `upgrade-instructions` from accidentally\n // adding a Glasstrace block to a project that opted out.\n skipped.push(displayPath);\n continue;\n }\n\n const content = generateInfoSection(agent, MCP_ENDPOINT, sdkVersion);\n if (content === \"\") {\n // Defensive — agents whose `infoFilePath` is non-null currently\n // always render content. Belt-and-braces guard against a future\n // mismatch.\n continue;\n }\n\n try {\n await injectInfoSection(agent, content, options.projectRoot);\n refreshed.push(displayPath);\n } catch (err) {\n errors.push(\n `Failed to refresh ${displayPath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n const exitCode = errors.length === 0 ? 0 : 1;\n return { exitCode, refreshed, skipped, warnings, errors };\n}\n","import { createHash } from \"node:crypto\";\nimport {\n AnonApiKeySchema,\n DevApiKeySchema,\n type AnonApiKey,\n type DevApiKey,\n} from \"@glasstrace/protocol\";\nimport { readAnonKey, readClaimedKey } from \"./anon-key.js\";\nimport { atomicWriteFile } from \"./atomic-write.js\";\n\n/**\n * Glasstrace MCP endpoint embedded in managed MCP configs and used by\n * the runtime claim-refresh path. Lives here (not in `cli/constants.ts`)\n * so the runtime helper can reach it without crossing the runtime/CLI\n * boundary; CLI callers import it directly from this module.\n */\nexport const MCP_ENDPOINT = \"https://api.glasstrace.dev/mcp\";\n\n/**\n * Runtime-safe MCP credential and config utilities.\n *\n * This module is loaded into user processes at SDK boot. It must not\n * import from `cli/*` or `agent-detection/*` so the runtime bundle does\n * not pull in CLI scaffolding or filesystem scanners. The boundary is\n * enforced by an import-graph guard test.\n *\n * Internal: not re-exported via `node-entry.ts` or `index.ts`.\n *\n * @module\n */\n\nlet fsPathCache:\n | { fs: typeof import(\"node:fs/promises\"); path: typeof import(\"node:path\") }\n | null\n | undefined;\n\nasync function loadFsPath(): Promise<\n | { fs: typeof import(\"node:fs/promises\"); path: typeof import(\"node:path\") }\n | null\n> {\n if (fsPathCache !== undefined) return fsPathCache;\n try {\n const [fs, path] = await Promise.all([\n import(\"node:fs/promises\"),\n import(\"node:path\"),\n ]);\n fsPathCache = { fs, path };\n return fsPathCache;\n } catch {\n fsPathCache = null;\n return null;\n }\n}\n\n/**\n * Computes a stable identity fingerprint for deduplication purposes.\n * This is NOT password hashing — the input is an opaque token used as\n * a marker identity, not a credential stored for authentication.\n *\n * @internal Exported for unit testing and for `cli/scaffolder.ts`'s\n * marker writer.\n */\nexport function identityFingerprint(token: string): string {\n return `sha256:${createHash(\"sha256\").update(token).digest(\"hex\")}`;\n}\n\n/**\n * Compares two MCP config strings for strict canonical-JSON equality.\n * Returns `true` when both inputs parse as JSON and produce structurally\n * equal objects after recursive key sorting; falls back to trimmed text\n * comparison for TOML and other non-JSON formats. Returns `false` on\n * parse errors that don't fall through to text comparison.\n *\n * Used to detect manually-edited MCP configs before overwriting them\n * (DISC-1247 Scenario 2c) and as the staleness signal for SDK-managed\n * configs that must be refreshed when the project's effective\n * credential changes.\n *\n * The matcher is intentionally strict: a hand-edited file that drops\n * any field (including `type: \"http\"` on a Claude `.mcp.json`) must\n * NOT be treated as SDK-managed and silently overwritten on re-init.\n * Callers that need backwards-compatible recognition of the legacy\n * generic `.glasstrace/mcp.json` shape (no `type: \"http\"`, written by\n * SDK versions prior to DISC-1572) should use\n * {@link genericMcpConfigOrLegacyShapeMatches} instead.\n *\n * @internal Exported for unit testing only.\n */\nexport function mcpConfigMatches(\n existingContent: string,\n expectedContent: string,\n): boolean {\n const trimmedExpected = expectedContent.trim();\n\n try {\n const existingParsed: unknown = JSON.parse(existingContent);\n const expectedParsed: unknown = JSON.parse(trimmedExpected);\n return (\n JSON.stringify(canonicalize(existingParsed)) ===\n JSON.stringify(canonicalize(expectedParsed))\n );\n } catch {\n // Fall through to text comparison for TOML and other non-JSON formats.\n }\n\n return existingContent.trim() === trimmedExpected;\n}\n\n/**\n * Generic-shape variant of {@link mcpConfigMatches} that also recognizes\n * the legacy `.glasstrace/mcp.json` shape written by SDK versions prior\n * to DISC-1572 (the same generic shape minus `type: \"http\"`).\n *\n * Scoped to callers that operate exclusively on `.glasstrace/mcp.json`\n * (currently {@link refreshMcpConfigAfterClaim}). It must not be used\n * by per-agent init paths: `decideMcpConfigAction` runs against every\n * detected agent's `.mcp.json`, and a Claude file whose `type` field\n * was hand-edited away must NOT be treated as SDK-managed and silently\n * overwritten on re-init. The shared {@link mcpConfigMatches} stays\n * strict for that reason; the legacy fallback lives here.\n *\n * The fallback is bounded: only the specific legacy generic shape\n * (the new shape minus `type: \"http\"`) is accepted; arbitrary field\n * omissions still report a mismatch.\n *\n * @internal Exported for unit testing only.\n */\nexport function genericMcpConfigOrLegacyShapeMatches(\n existingContent: string,\n expectedGenericContent: string,\n): boolean {\n if (mcpConfigMatches(existingContent, expectedGenericContent)) {\n return true;\n }\n\n try {\n const expectedParsed: unknown = JSON.parse(expectedGenericContent.trim());\n const expectedLegacy = stripTypeFromGlasstraceServer(expectedParsed);\n if (expectedLegacy === null) {\n return false;\n }\n const existingParsed: unknown = JSON.parse(existingContent);\n return (\n JSON.stringify(canonicalize(existingParsed)) ===\n JSON.stringify(canonicalize(expectedLegacy))\n );\n } catch {\n return false;\n }\n}\n\n/**\n * Returns a copy of `value` with `type: \"http\"` removed from\n * `mcpServers.glasstrace`, or `null` when `value` is not the new\n * Claude-compatible SDK shape. Used by\n * {@link genericMcpConfigOrLegacyShapeMatches} to recognize legacy\n * on-disk files written by SDK versions prior to DISC-1572.\n *\n * The helper is deliberately narrow: it only strips when `type` is\n * exactly the string `\"http\"`, and only from the `glasstrace` server\n * entry. Any structural mismatch (non-object root, missing\n * `mcpServers`, missing `glasstrace`, or `type !== \"http\"`) yields\n * `null`, leaving the strict canonical comparison's verdict in place.\n */\nfunction stripTypeFromGlasstraceServer(value: unknown): unknown | null {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n return null;\n }\n const root = value as Record<string, unknown>;\n const servers = root[\"mcpServers\"];\n if (\n servers === null ||\n typeof servers !== \"object\" ||\n Array.isArray(servers)\n ) {\n return null;\n }\n const serversObj = servers as Record<string, unknown>;\n const server = serversObj[\"glasstrace\"];\n if (\n server === null ||\n typeof server !== \"object\" ||\n Array.isArray(server)\n ) {\n return null;\n }\n const serverObj = server as Record<string, unknown>;\n if (serverObj[\"type\"] !== \"http\") {\n return null;\n }\n const { type: _omittedType, ...serverWithoutType } = serverObj;\n void _omittedType;\n return {\n ...root,\n mcpServers: {\n ...serversObj,\n glasstrace: serverWithoutType,\n },\n };\n}\n\nfunction canonicalize(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map(canonicalize);\n }\n if (value !== null && typeof value === \"object\") {\n const obj = value as Record<string, unknown>;\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(obj).sort()) {\n sorted[key] = canonicalize(obj[key]);\n }\n return sorted;\n }\n return value;\n}\n\n/**\n * Parses a `.env.local` file's text content for `GLASSTRACE_API_KEY`,\n * returning the last assignment's value. Empty values\n * (`GLASSTRACE_API_KEY=`) and the `your_key_here` placeholder are\n * filtered out. Surrounding single or double quotes are stripped.\n *\n * The resolver validates the returned value against `DevApiKeySchema`\n * before accepting it; this parser is permissive on purpose so that\n * malformed values can be flagged with a `malformed-env-local`\n * warning rather than silently dropped.\n *\n * @internal Exported for unit testing only.\n */\nexport function readEnvLocalApiKey(content: string): string | null {\n let last: string | null = null;\n const regex = /^\\s*GLASSTRACE_API_KEY\\s*=\\s*(.*)$/gm;\n let match: RegExpExecArray | null;\n while ((match = regex.exec(content)) !== null) {\n const raw = match[1].trim();\n if (raw === \"\") continue;\n const unquoted = raw.replace(/^(['\"])(.*)\\1$/, \"$2\");\n if (unquoted === \"\" || unquoted === \"your_key_here\") continue;\n last = unquoted;\n }\n return last;\n}\n\n/**\n * Returns true when the given API key value looks like a claimed\n * developer key (prefix `gt_dev_`). Defensive against leading or\n * trailing whitespace.\n *\n * **This is a prefix-only check, not strict validation.** Use it as a\n * fast path for \"looks like a claimed key, do not overwrite\". The\n * effective-credential resolver validates with\n * `DevApiKeySchema.safeParse` because a `gt_dev_` prefix alone is not\n * sufficient to authenticate against the backend.\n *\n * @internal Exported for unit testing only.\n */\nexport function isDevApiKey(value: string | null | undefined): boolean {\n if (value === null || value === undefined) return false;\n return value.trim().startsWith(\"gt_dev_\");\n}\n\n/**\n * Returns true when the given API key value is a fully-valid anonymous\n * API key (matches `AnonApiKeySchema`). Used by `registerViaCli` as a\n * runtime guard so that a `DevApiKey` cannot be passed via process\n * arguments to vendor MCP CLIs (which would expose it via `ps` on\n * multi-user hosts).\n *\n * @internal Exported for unit testing only.\n */\nexport function isAnonApiKey(value: string | null | undefined): boolean {\n if (value === null || value === undefined) return false;\n return AnonApiKeySchema.safeParse(value).success;\n}\n\n/**\n * The MCP-effective credential, tagged by which on-disk source produced\n * it. `env-local` and `claimed-key` carry a branded `DevApiKey`;\n * `anon` carries a branded `AnonApiKey`. Internal — not re-exported.\n */\nexport type EffectiveMcpCredential =\n | { source: \"env-local\"; key: DevApiKey }\n | { source: \"claimed-key\"; key: DevApiKey }\n | { source: \"anon\"; key: AnonApiKey };\n\n/**\n * Surfaced when the resolver detected a recoverable anomaly the caller\n * should inform the user about without printing key material.\n *\n * - `malformed-env-local`: `.env.local` set `GLASSTRACE_API_KEY` to a\n * value that fails `DevApiKeySchema`. The resolver fell through.\n * - `claimed-key-only`: the effective credential came from\n * `.glasstrace/claimed-key` because `.env.local` had no usable dev\n * key. Suggest the user copy the key into `.env.local`.\n */\nexport type ResolveWarning = \"malformed-env-local\" | \"claimed-key-only\";\n\n/**\n * The resolved credential plus the on-disk anon key (returned\n * separately so the staleness check does not have to re-read the\n * file) and any warnings the caller should surface to the user.\n */\nexport interface ResolveResult {\n effective: EffectiveMcpCredential | null;\n anonKey: AnonApiKey | null;\n warnings: ReadonlyArray<ResolveWarning>;\n}\n\n/**\n * Resolves the MCP-effective credential for a project, in priority\n * order: `.env.local` `GLASSTRACE_API_KEY` (validated as\n * `DevApiKeySchema`) → `.glasstrace/claimed-key` (validated as\n * `DevApiKeySchema`) → `.glasstrace/anon_key` (`AnonApiKey`). Returns\n * `null` for `effective` only when no source produced a usable key.\n *\n * The function is async because it touches the filesystem. It is\n * called only on the post-claim runtime branch and from the CLI\n * commands `glasstrace init` and `glasstrace mcp add`. It is **not**\n * on the steady-state init path.\n */\nexport async function resolveEffectiveMcpCredential(\n projectRoot?: string,\n): Promise<ResolveResult> {\n const root = projectRoot ?? process.cwd();\n const warnings: ResolveWarning[] = [];\n\n const envLocalKey = await readEnvLocalDevKey(root, warnings);\n const claimedKey = envLocalKey === null ? await readClaimedKey(root) : null;\n const anonKey = await readAnonKey(root);\n\n let effective: EffectiveMcpCredential | null = null;\n if (envLocalKey !== null) {\n effective = { source: \"env-local\", key: envLocalKey };\n } else if (claimedKey !== null) {\n effective = { source: \"claimed-key\", key: claimedKey };\n warnings.push(\"claimed-key-only\");\n } else if (anonKey !== null) {\n effective = { source: \"anon\", key: anonKey };\n }\n\n return { effective, anonKey, warnings };\n}\n\nasync function readEnvLocalDevKey(\n root: string,\n warnings: ResolveWarning[],\n): Promise<DevApiKey | null> {\n const modules = await loadFsPath();\n if (!modules) return null;\n\n const envPath = modules.path.join(root, \".env.local\");\n let content: string;\n try {\n content = await modules.fs.readFile(envPath, \"utf-8\");\n } catch {\n return null;\n }\n\n const raw = readEnvLocalApiKey(content);\n if (raw === null) return null;\n\n const parsed = DevApiKeySchema.safeParse(raw);\n if (!parsed.success) {\n warnings.push(\"malformed-env-local\");\n return null;\n }\n return parsed.data;\n}\n\n/**\n * Source label for the credential a marker file describes.\n *\n * @internal\n */\nexport type MarkerCredentialSource = \"env-local\" | \"claimed-key\" | \"anon\";\n\n/**\n * Descriptor passed to {@link writeMcpMarker} and matched by\n * {@link readMcpMarker}. `credentialHash` is the\n * `identityFingerprint` of the credential actually written into the\n * managed MCP config — never the credential itself.\n *\n * @internal\n */\nexport interface MarkerTarget {\n credentialSource: MarkerCredentialSource;\n credentialHash: string;\n}\n\n/**\n * Normalized state of a `.glasstrace/mcp-connected` marker on disk.\n *\n * - `absent`: no marker file present.\n * - `valid`: a v1 or v2 marker that parsed cleanly. v1 markers are\n * reported as `credentialSource = \"anon\"` with `credentialHash`\n * taken from the legacy `keyHash` field (the v1 schema can only\n * describe an anon credential).\n * - `unknown-version`: the marker has `version > 2`. Treat as\n * not-configured so a future SDK that wrote the marker doesn't\n * block this SDK from refreshing.\n * - `corrupted`: parse failure or schema mismatch. Treat as\n * not-configured.\n *\n * @internal\n */\nexport type MarkerState =\n | { status: \"absent\" }\n | { status: \"valid\"; credentialSource: MarkerCredentialSource; credentialHash: string }\n | { status: \"unknown-version\" }\n | { status: \"corrupted\" };\n\nconst MCP_MARKER_FILE = \"mcp-connected\";\nconst GLASSTRACE_DIR = \".glasstrace\";\n\n/**\n * Reads `.glasstrace/mcp-connected` and returns its normalized state.\n * Used by `mcp add` (marker-mismatch detection) and by\n * {@link writeMcpMarker} (skip-if-match optimization).\n *\n * Reader rules per the design (`SDK-034 D3`):\n * - `version === undefined` → v1: `{ keyHash, configuredAt }`. Mapped\n * to `credentialSource: \"anon\"`, `credentialHash: keyHash`. v1's\n * `keyHash` is itself produced by `identityFingerprint`, so the\n * format matches v2 without conversion.\n * - `version === 2` → v2 reader.\n * - `version > 2` → `unknown-version` (conservative-fail).\n * - Parse failure → `corrupted` (conservative-fail).\n *\n * @internal Exported for unit testing only.\n */\nexport async function readMcpMarker(projectRoot?: string): Promise<MarkerState> {\n const root = projectRoot ?? process.cwd();\n const modules = await loadFsPath();\n if (!modules) return { status: \"absent\" };\n\n const markerPath = modules.path.join(root, GLASSTRACE_DIR, MCP_MARKER_FILE);\n let content: string;\n try {\n content = await modules.fs.readFile(markerPath, \"utf-8\");\n } catch {\n return { status: \"absent\" };\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch {\n return { status: \"corrupted\" };\n }\n\n if (parsed === null || typeof parsed !== \"object\") {\n return { status: \"corrupted\" };\n }\n\n const obj = parsed as Record<string, unknown>;\n const version = obj[\"version\"];\n\n if (version === undefined) {\n // v1: { keyHash, configuredAt }\n const keyHash = obj[\"keyHash\"];\n if (typeof keyHash !== \"string\" || keyHash === \"\") {\n return { status: \"corrupted\" };\n }\n return {\n status: \"valid\",\n credentialSource: \"anon\",\n credentialHash: keyHash,\n };\n }\n\n if (version === 2) {\n const source = obj[\"credentialSource\"];\n const hash = obj[\"credentialHash\"];\n if (\n (source !== \"env-local\" && source !== \"claimed-key\" && source !== \"anon\") ||\n typeof hash !== \"string\" ||\n hash === \"\"\n ) {\n return { status: \"corrupted\" };\n }\n return {\n status: \"valid\",\n credentialSource: source,\n credentialHash: hash,\n };\n }\n\n if (typeof version === \"number\" && version > 2) {\n return { status: \"unknown-version\" };\n }\n\n return { status: \"corrupted\" };\n}\n\n/**\n * Writes a v2 `.glasstrace/mcp-connected` marker. Returns `true` when\n * the marker was created or updated, `false` when an existing marker\n * already records the same `(credentialSource, credentialHash)` pair\n * and was left untouched.\n *\n * Writer always emits v2 with `version: 2`. The legacy `keyHash`\n * field is intentionally omitted from new writes — v1 readers ignore\n * unknown fields and the duplicate would diverge over time. v3+ and\n * corrupted markers are unconditionally overwritten.\n *\n * The directory is created with `0o700` and the file with `0o600`,\n * matching existing scaffolder behavior.\n *\n * @internal Exported for unit testing only.\n */\nexport async function writeMcpMarker(\n projectRoot: string,\n target: MarkerTarget,\n): Promise<boolean> {\n const modules = await loadFsPath();\n if (!modules) return false;\n\n const dirPath = modules.path.join(projectRoot, GLASSTRACE_DIR);\n const markerPath = modules.path.join(dirPath, MCP_MARKER_FILE);\n\n const state = await readMcpMarker(projectRoot);\n if (\n state.status === \"valid\" &&\n state.credentialSource === target.credentialSource &&\n state.credentialHash === target.credentialHash\n ) {\n return false;\n }\n\n await modules.fs.mkdir(dirPath, { recursive: true, mode: 0o700 });\n\n const body = JSON.stringify(\n {\n version: 2,\n credentialSource: target.credentialSource,\n credentialHash: target.credentialHash,\n configuredAt: new Date().toISOString(),\n },\n null,\n 2,\n );\n\n await modules.fs.writeFile(markerPath, body, { mode: 0o600 });\n // writeFile mode only applies on creation on some platforms.\n await modules.fs.chmod(markerPath, 0o600);\n return true;\n}\n\nconst MCP_CONFIG_FILE = \"mcp.json\";\n\n/**\n * The set of outcomes the runtime claim-refresh helper can produce.\n *\n * - `rewrote`: `.glasstrace/mcp.json` matched the SDK-shaped output\n * for the on-disk anon key, was rewritten with the effective\n * credential, and the marker was updated.\n * - `preserved`: `.glasstrace/mcp.json` exists but does not match the\n * SDK-shaped output for the on-disk anon key. The file is left\n * untouched (the user may have hand-edited it). The marker is not\n * touched.\n * - `absent`: `.glasstrace/mcp.json` does not exist (`ENOENT`), or\n * no anon key is on disk so there is nothing to compare against. A\n * project without an anon key never had an SDK-shaped `mcp.json`\n * written by the runtime path, so this branch is a true no-op.\n * - `skipped-anon-source`: the effective credential is `null` or its\n * source is `\"anon\"`. Either way, there is no claim transition to\n * refresh for. Caller should generally gate on\n * `effective.source !== \"anon\"` before invoking the helper; this\n * branch is the runtime-side belt-and-suspenders.\n * - `skipped-not-persisted`: never reached in practice — the caller\n * in `init-client.ts` gates on `writeClaimedKey`'s `persisted` not\n * being `\"none\"`. The variant exists so an exhaustive switch in\n * the caller stays exhaustive if the gate is removed.\n *\n * @internal\n */\nexport type RuntimeRefreshAction =\n | \"rewrote\"\n | \"preserved\"\n | \"absent\"\n | \"skipped-anon-source\"\n | \"skipped-not-persisted\";\n\nlet refreshNudgeEmitted = false;\n\n/**\n * @internal Exported for unit testing only — resets the per-process\n * \"refresh nudge already emitted\" flag.\n */\nexport function __resetRefreshNudgeForTest(): void {\n refreshNudgeEmitted = false;\n}\n\n/**\n * Emits a single redacted stderr line announcing the MCP config\n * refresh. Deduplicated per process via a module-level flag — a\n * second call within the same process is a no-op. Cross-process\n * dedup (the same user running `mcp add` in another terminal moments\n * later) is explicitly out of scope.\n */\nfunction emitRefreshNudge(persistedSource: \"env-local\" | \"claimed-key\"): void {\n if (refreshNudgeEmitted) return;\n refreshNudgeEmitted = true;\n try {\n if (persistedSource === \"claimed-key\") {\n process.stderr.write(\n \"[glasstrace] MCP config refreshed for the new credential. \" +\n \"Copy .glasstrace/claimed-key into .env.local so Codex can pick it up on next restart.\\n\",\n );\n } else {\n process.stderr.write(\n \"[glasstrace] MCP config refreshed for the new credential.\\n\",\n );\n }\n } catch {\n // stderr is best-effort; refresh outcome must not depend on it.\n }\n}\n\n/**\n * Returns the SDK-shaped JSON for `.glasstrace/mcp.json` (the generic\n * MCP config used at runtime). Inlined here — and intentionally not\n * imported from `agent-detection/configs.ts` — because the runtime\n * path must not pull `agent-detection` into the runtime bundle. The\n * shape matches what `generateMcpConfig({ name: \"generic\", ... },\n * endpoint, bearer)` would produce. If the agent-detection version\n * diverges, the staleness check stops detecting SDK-managed configs;\n * a regression test against `generateMcpConfig`'s \"generic\" branch\n * lives in `tests/unit/sdk/mcp-runtime.test.ts`.\n */\nfunction genericMcpConfigContent(endpoint: string, bearer: string): string {\n return JSON.stringify(\n {\n mcpServers: {\n glasstrace: {\n type: \"http\",\n url: endpoint,\n headers: {\n Authorization: `Bearer ${bearer}`,\n },\n },\n },\n },\n null,\n 2,\n );\n}\n\n/**\n * Refreshes `.glasstrace/mcp.json` after a successful account claim\n * transition has persisted a dev/account credential to disk (via\n * `writeClaimedKey`). The file is rewritten only when its content\n * matches the SDK-shaped output for the project's on-disk anon key\n * (canonical-JSON equivalence via `mcpConfigMatches` — whitespace and\n * key order are normalised before comparison). User-edited or\n * third-party `mcp.json` content is preserved.\n *\n * Atomic write protocol: write the replacement to a sibling temp\n * path, set `0o600`, then `rename` into place. This matches the\n * existing pattern at `init-client.ts` for `.glasstrace/config`,\n * `anon-key.ts` for `.glasstrace/anon_key`, and `runtime-state.ts`.\n * The temp must be on the same filesystem as the destination for the\n * `rename` to be atomic.\n *\n * The helper is invoked only on the post-claim runtime branch (see\n * `init-client.ts` `performInit`) and never on the steady-state init\n * path. It must not throw — failures during write/chmod/rename or\n * marker update surface as `\"preserved\"` so the caller's\n * `claimResult` return is preserved. The temp file is best-effort\n * cleaned up on failure to avoid leaving stale `.tmp` siblings on\n * disk.\n *\n * @internal Exported for unit testing only; not re-exported from\n * `node-entry.ts` or `index.ts`.\n */\nexport async function refreshGenericMcpConfigAtRuntime(\n projectRoot: string,\n effective: EffectiveMcpCredential | null,\n anonKeyOnDisk: AnonApiKey | null,\n): Promise<{ action: RuntimeRefreshAction }> {\n if (effective === null || effective.source === \"anon\") {\n return { action: \"skipped-anon-source\" };\n }\n\n // Dev-key-only project (no .glasstrace/anon_key on disk): the\n // staleness check has nothing to compare against. The SDK never\n // wrote mcp.json without an anon key, so there is nothing to\n // refresh.\n if (anonKeyOnDisk === null) {\n return { action: \"absent\" };\n }\n\n const modules = await loadFsPath();\n if (!modules) return { action: \"absent\" };\n\n const dirPath = modules.path.join(projectRoot, GLASSTRACE_DIR);\n const configPath = modules.path.join(dirPath, MCP_CONFIG_FILE);\n\n let existing: string;\n try {\n existing = await modules.fs.readFile(configPath, \"utf-8\");\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"ENOENT\") {\n return { action: \"absent\" };\n }\n return { action: \"preserved\" };\n }\n\n const expectedAnon = genericMcpConfigContent(MCP_ENDPOINT, anonKeyOnDisk);\n if (!genericMcpConfigOrLegacyShapeMatches(existing, expectedAnon)) {\n return { action: \"preserved\" };\n }\n\n // SDK-managed and stale. Replace atomically per SDK 2.0 §4.3:\n // tmp + fsync(tmp) + rename + fsync(parent). Any failure in the\n // helper or marker update path must produce a non-throw outcome\n // so the caller's claimResult return is preserved; the helper\n // best-effort cleans up the .tmp sibling on failure.\n const replacement = genericMcpConfigContent(MCP_ENDPOINT, effective.key);\n try {\n await atomicWriteFile(configPath, replacement, { mode: 0o600 });\n\n await writeMcpMarker(projectRoot, {\n credentialSource: effective.source,\n credentialHash: identityFingerprint(effective.key),\n });\n } catch {\n return { action: \"preserved\" };\n }\n\n emitRefreshNudge(effective.source);\n\n return { action: \"rewrote\" };\n}\n","import { execFile } from \"node:child_process\";\nimport { access, stat } from \"node:fs/promises\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { constants } from \"node:fs\";\n\n/**\n * Describes an AI coding agent detected in a project.\n */\nexport interface DetectedAgent {\n name: \"claude\" | \"codex\" | \"gemini\" | \"cursor\" | \"windsurf\" | \"generic\";\n mcpConfigPath: string | null;\n infoFilePath: string | null;\n cliAvailable: boolean;\n registrationCommand: string | null;\n}\n\ntype AgentName = DetectedAgent[\"name\"];\n\ninterface AgentRule {\n name: AgentName;\n /** Paths relative to a search directory that indicate this agent is present. */\n markers: string[];\n /** Function to compute the MCP config path given the directory where markers were found. */\n mcpConfigPath: (markerDir: string) => string;\n /** Function to compute the info file path, or null. */\n infoFilePath: (markerDir: string) => string | null;\n /** CLI binary name to check in PATH, or null if no CLI exists. */\n cliBinary: string | null;\n /** Registration command template, or null. */\n registrationCommand: string | null;\n}\n\nconst AGENT_RULES: AgentRule[] = [\n {\n name: \"claude\",\n markers: [\".claude\", \"CLAUDE.md\"],\n mcpConfigPath: (dir) => join(dir, \".mcp.json\"),\n infoFilePath: (dir) => join(dir, \"CLAUDE.md\"),\n cliBinary: \"claude\",\n registrationCommand: \"npx glasstrace mcp add --agent claude\",\n },\n {\n name: \"codex\",\n markers: [\"codex.md\", \".codex\"],\n mcpConfigPath: (dir) => join(dir, \".codex\", \"config.toml\"),\n infoFilePath: (dir) => join(dir, \"codex.md\"),\n cliBinary: \"codex\",\n registrationCommand: \"npx glasstrace mcp add --agent codex\",\n },\n {\n name: \"gemini\",\n markers: [\".gemini\"],\n mcpConfigPath: (dir) => join(dir, \".gemini\", \"settings.json\"),\n infoFilePath: () => null,\n cliBinary: \"gemini\",\n registrationCommand: \"npx glasstrace mcp add --agent gemini\",\n },\n {\n name: \"cursor\",\n markers: [\".cursor\", \".cursorrules\"],\n mcpConfigPath: (dir) => join(dir, \".cursor\", \"mcp.json\"),\n infoFilePath: (dir) => join(dir, \".cursorrules\"),\n cliBinary: null,\n registrationCommand: \"npx glasstrace mcp add --agent cursor\",\n },\n {\n name: \"windsurf\",\n markers: [\".windsurfrules\", \".windsurf\"],\n mcpConfigPath: () =>\n join(homedir(), \".codeium\", \"windsurf\", \"mcp_config.json\"),\n infoFilePath: (dir) => join(dir, \".windsurfrules\"),\n cliBinary: null,\n registrationCommand: \"npx glasstrace mcp add --agent windsurf\",\n },\n];\n\n/**\n * Checks whether a path exists and is accessible, following symlinks.\n * Returns false on permission errors or missing paths.\n *\n * @param mode - The access mode to check (defaults to R_OK for marker detection).\n */\nasync function pathExists(\n path: string,\n mode: number = constants.R_OK,\n): Promise<boolean> {\n try {\n await access(path, mode);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Finds the git root directory by walking up from the given path.\n * Returns the starting directory if no `.git` is found.\n */\nasync function findGitRoot(startDir: string): Promise<string> {\n let current = resolve(startDir);\n\n while (true) {\n if (await pathExists(join(current, \".git\"), constants.F_OK)) {\n return current;\n }\n const parent = dirname(current);\n if (parent === current) {\n // Reached filesystem root without finding .git\n break;\n }\n current = parent;\n }\n\n return resolve(startDir);\n}\n\n/**\n * Returns true if a CLI binary is available on PATH.\n * Uses `which` on Unix and `where` on Windows, via execFile (no shell injection).\n */\nfunction isCliAvailable(binary: string): Promise<boolean> {\n return new Promise((resolve) => {\n const command = process.platform === \"win32\" ? \"where\" : \"which\";\n execFile(command, [binary], (error) => {\n resolve(error === null);\n });\n });\n}\n\n/**\n * Detects AI coding agents present in a project by scanning for marker\n * files and directories. Walks up from projectRoot to the git root to\n * support monorepo layouts.\n *\n * Always includes a \"generic\" fallback entry.\n *\n * @param projectRoot - Absolute or relative path to the project directory.\n * @returns Array of detected agents, with generic always last.\n * @throws If projectRoot does not exist or is not a directory.\n */\nexport async function detectAgents(\n projectRoot: string,\n): Promise<DetectedAgent[]> {\n const resolvedRoot = resolve(projectRoot);\n\n // Validate projectRoot exists and is a directory\n let rootStat;\n try {\n rootStat = await stat(resolvedRoot);\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n throw new Error(\n `projectRoot does not exist: ${resolvedRoot}` +\n (code ? ` (${code})` : \"\"),\n );\n }\n\n if (!rootStat.isDirectory()) {\n throw new Error(`projectRoot is not a directory: ${resolvedRoot}`);\n }\n\n const gitRoot = await findGitRoot(resolvedRoot);\n\n // Collect unique directories to search: projectRoot and every ancestor up to gitRoot\n const searchDirs: string[] = [];\n let current = resolvedRoot;\n while (true) {\n searchDirs.push(current);\n if (current === gitRoot) {\n break;\n }\n const parent = dirname(current);\n if (parent === current) {\n break;\n }\n current = parent;\n }\n\n const detected: DetectedAgent[] = [];\n const seenAgents = new Set<AgentName>();\n\n for (const rule of AGENT_RULES) {\n let foundDir: string | null = null;\n\n // Check each search directory for markers\n for (const dir of searchDirs) {\n let markerFound = false;\n for (const marker of rule.markers) {\n if (await pathExists(join(dir, marker))) {\n markerFound = true;\n break;\n }\n }\n if (markerFound) {\n foundDir = dir;\n break;\n }\n }\n\n if (foundDir === null) {\n continue;\n }\n\n if (seenAgents.has(rule.name)) {\n continue;\n }\n seenAgents.add(rule.name);\n\n // Determine info file path — only include if the file actually exists\n let infoFilePath = rule.infoFilePath(foundDir);\n if (infoFilePath !== null && !(await pathExists(infoFilePath))) {\n infoFilePath = null;\n }\n\n const cliAvailable = rule.cliBinary\n ? await isCliAvailable(rule.cliBinary)\n : false;\n\n detected.push({\n name: rule.name,\n mcpConfigPath: rule.mcpConfigPath(foundDir),\n infoFilePath,\n cliAvailable,\n registrationCommand: rule.registrationCommand,\n });\n }\n\n // Always include generic fallback\n detected.push({\n name: \"generic\",\n mcpConfigPath: join(resolvedRoot, \".glasstrace\", \"mcp.json\"),\n infoFilePath: null,\n cliAvailable: false,\n registrationCommand: null,\n });\n\n return detected;\n}\n","/**\n * The text body the SDK injects into a user's agent-instruction file\n * (CLAUDE.md, .cursorrules, codex.md, etc.) between the\n * `<!-- glasstrace:mcp:start v=<sdkVersion> -->` ... `<!-- glasstrace:mcp:end -->`\n * managed-section markers.\n *\n * **Why this lives in its own module:** the text is the contract\n * between the SDK and the user's coding agent at runtime — what the\n * AI reads when it decides whether to call Glasstrace MCP and how to\n * use the returned evidence. It evolves on a different cadence from\n * the surrounding marker / version-stamp / per-agent-format\n * machinery in `configs.ts`. Keeping it in a sibling module means\n * future content edits are a single-file change and don't risk\n * disturbing the `configs.ts` rendering machinery (which carries the\n * SDK-050 / DISC-1592 / DISC-1602 marker contract that has soaked in\n * production and must not regress).\n *\n * **Vocabulary alignment:** every MCP tool name and response-field\n * name in the body below is verified against the current MCP server\n * contract maintained in the private `glasstrace-product` repo\n * (`shared/types/wire-mcp.ts` and `shared/types/agent-evidence.ts`\n * there); the SDK consumes the resulting wire format but does not\n * own the schema source of truth for those tool names and field\n * names. If the server-side MCP contract evolves (renames a field,\n * restructures `suggestedFollowups`, adds new tools, etc.), update\n * this module in lockstep with the protocol change so the\n * agent-instruction text never references fields that don't exist.\n *\n * **Wave 17 follow-up (2026-05-09, post-PR-998):** the\n * vocabulary-mismatch-recovery wave (DISC-1626 + 40 sibling DISCs,\n * shipped via `glasstrace-product` PR #998) added five fields to\n * the no-match envelope on `find_trace_candidates`'s\n * `CandidateDiagnosticSchema` and the sibling-tools'\n * `ToolDiagnosticSchema`: `windowActivity`, `humanReadable`,\n * `diagnosticValue`, `recommendedNextStep`, and `maxUsefulFollowups`.\n * The Workflow §4 below names `closeMatches` /\n * `recentRoutesSample` / `windowActivity` / `humanReadable` /\n * `recoveryActions` / `diagnosticValue` / `recommendedNextStep`\n * because each disambiguates a different reason for an empty\n * result — most notably, `windowActivity` carries the four-way\n * distinguisher between \"wrong vocabulary\", \"no traffic in window\",\n * \"captureConfig-blocked\", and \"no traces ever for this tenant\"\n * (per `wire-mcp.ts` `NoMatchWindowActivitySchema` /\n * DISC-1652 Amendment 1 / DISC-1654). Without `windowActivity`\n * the agent cannot distinguish a vocabulary miss from \"the SDK was\n * never registered for this tenant\" — they look identical at the\n * `closeMatches`-only layer.\n *\n * **Heuristic-first vs tool-first framing:** the body opens with\n * explicit \"Call Glasstrace FIRST when\" / \"SKIP Glasstrace when\"\n * rules so a frontier agent has a cheap pre-tool-call decision\n * heuristic it can apply BEFORE spending tokens on tool\n * consideration. The Workflow section then names\n * `find_trace_candidates` as the entry point and instructs the\n * agent to READ `closeMatches` / `recentRoutesSample` /\n * `recoveryActions` before pivoting to source — that is the\n * load-bearing recovery contract from MCP-025 / MCP-027 (codified\n * in `wire-mcp.ts` `ToolDiagnosticSchema` and `CandidateDiagnosticSchema`)\n * and is the failure mode the prior SDK-050 cost-aware decision\n * paragraph did not surface.\n */\n\n/**\n * The full agent-instruction body, returned as a markdown string.\n *\n * The body has NO endpoint URL inlined — frontier agents reach\n * Glasstrace via the MCP server name `glasstrace` configured in the\n * user's MCP config (`.glasstrace/mcp.json` or per-agent native\n * config), not by reading a URL out of the instruction file. Keeping\n * the URL out of the instruction text avoids drift between the\n * instruction file and the MCP config and keeps the body tight.\n *\n * Returns the body WITHOUT the surrounding markers — the caller\n * (`generateInfoSection` in `configs.ts`) wraps the body with the\n * version-stamped start/end markers per the SDK-050 / DISC-1592\n * marker contract.\n */\nexport function buildAgentInstructionBody(): string {\n return [\n \"\",\n \"## Glasstrace MCP — Runtime Debugging Evidence\",\n \"\",\n \"Glasstrace is connected via MCP (`glasstrace`). It captures runtime traces from this app and exposes them through tools that disambiguate bugs source alone cannot resolve.\",\n \"\",\n \"### Call Glasstrace FIRST when:\",\n \"- A value passed at runtime is suspect: wrong role, locale, timezone, template, tenant id, or any enum/config that branches downstream.\",\n \"- The bug is in a side-effect operation: email sent, calendar event created, webhook dispatched, queued job, `after()` callback.\",\n \"- The user reports a symptom but you do not yet know which code path actually executed.\",\n \"- You need to confirm a route or procedure ran in the relevant time window before doing a source dive.\",\n \"\",\n \"### SKIP Glasstrace when:\",\n \"- The bug is statically obvious from source (N+1 query, missing await, redundant query, type error, missing null check).\",\n \"- The change is a refactor whose correctness does not depend on runtime behavior.\",\n \"- You already have a precise traceId from another source.\",\n \"\",\n \"### Workflow\",\n \"1. Start with `find_trace_candidates`. Pass whatever route or procedure name is natural — the server normalizes vocabulary and, on miss, returns close matches and a sample of routes actually present in the window.\",\n \"2. Take the highest-confidence candidate's `suggestedFollowups` and pass them straight to `get_trace` or `get_root_cause`.\",\n \"3. For side-effect bugs, read `sideEffectSummary` in the `get_trace` / `get_root_cause` response. The allowlisted fields (`templateKey`, `providerOperation`, `role`, `locale`, `timezone`, `status`, `phase`) are the ones that disambiguate payload bugs.\",\n \"4. If a tool returns empty, READ the response's empty-result envelope before pivoting to source — each field disambiguates a different reason for the empty result:\",\n \" - `closeMatches` / `recentRoutesSample` — your filter vocabulary doesn't match server-side names; the server returns the closest known names + a sample of routes actually present.\",\n \" - `windowActivity` — load-bearing four-way distinguisher. `totalTracesInWindow === 0` AND `totalTracesInTenantEver > 0` means \\\"your time window missed the activity\\\"; `totalTracesInTenantEver === 0` means \\\"this tenant has never produced traces\\\" (SDK not registered, or never hit); `captureConfigBlocksRequest === true` means \\\"the SDK's capture config dropped this route\\\"; otherwise the empty result is a vocabulary miss — see `closeMatches`.\",\n \" - `humanReadable` — prose guidance written for the agent.\",\n \" - `recoveryActions` — concrete next-call shapes.\",\n \" - `diagnosticValue` / `recommendedNextStep` — whether to keep searching or stop.\",\n \" Empty results carry `notAbsenceProof: true` — they are never proof the bug did not occur.\",\n \"\",\n \"### Tools\",\n \"- `find_trace_candidates` — discovery, vocabulary-tolerant filter\",\n \"- `get_trace` — exact trace by `traceId`\",\n \"- `get_root_cause` — root-cause analysis for a `traceId`\",\n \"- `get_session_timeline` — events for a session\",\n \"- `get_latest_error` / `get_error_list` — recent server errors\",\n \"\",\n \"Side-effect evidence is allowlisted and compact by design. Fields you don't see may have been omitted by policy, not absent at runtime.\",\n \"\",\n ].join(\"\\n\");\n}\n","import type { DetectedAgent } from \"./detect.js\";\nimport { buildAgentInstructionBody } from \"./agent-instruction-text.js\";\n\n/**\n * Generates the MCP server configuration content for a given agent.\n *\n * The output is the full file content suitable for writing to the agent's\n * MCP config file. The bearer token is intentionally embedded here for\n * agents whose schemas inline the Authorization header — Codex is the\n * exception and uses `bearer_token_env_var` so the actual token never\n * appears in TOML.\n *\n * @param agent - The detected agent to generate config for.\n * @param endpoint - The Glasstrace MCP endpoint URL.\n * @param bearer - The credential to embed in the Authorization header\n * (anon key or dev key, depending on the project's resolved\n * credential source). Empty values throw.\n * @returns The formatted configuration string.\n * @throws If endpoint or bearer is empty.\n */\nexport function generateMcpConfig(\n agent: DetectedAgent,\n endpoint: string,\n bearer: string,\n): string {\n if (!endpoint || endpoint.trim() === \"\") {\n throw new Error(\"endpoint must not be empty\");\n }\n if (!bearer || bearer.trim() === \"\") {\n throw new Error(\"bearer must not be empty\");\n }\n\n switch (agent.name) {\n case \"claude\":\n return JSON.stringify(\n {\n mcpServers: {\n glasstrace: {\n type: \"http\",\n url: endpoint,\n headers: {\n Authorization: `Bearer ${bearer}`,\n },\n },\n },\n },\n null,\n 2,\n );\n\n case \"codex\": {\n // Escape TOML basic string special characters in the endpoint value.\n // TOML requires backslashes, quotes, and control characters to be escaped.\n const safeEndpoint = endpoint\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, \"\\\\n\")\n .replace(/\\r/g, \"\\\\r\")\n .replace(/\\t/g, \"\\\\t\");\n return [\n \"[mcp_servers.glasstrace]\",\n `url = \"${safeEndpoint}\"`,\n `bearer_token_env_var = \"GLASSTRACE_API_KEY\"`,\n \"\",\n ].join(\"\\n\");\n }\n\n case \"gemini\":\n return JSON.stringify(\n {\n mcpServers: {\n glasstrace: {\n httpUrl: endpoint,\n headers: {\n Authorization: `Bearer ${bearer}`,\n },\n },\n },\n },\n null,\n 2,\n );\n\n case \"cursor\":\n return JSON.stringify(\n {\n mcpServers: {\n glasstrace: {\n type: \"http\",\n url: endpoint,\n headers: {\n Authorization: `Bearer ${bearer}`,\n },\n },\n },\n },\n null,\n 2,\n );\n\n case \"windsurf\":\n return JSON.stringify(\n {\n mcpServers: {\n glasstrace: {\n type: \"http\",\n url: endpoint,\n headers: {\n Authorization: `Bearer ${bearer}`,\n },\n },\n },\n },\n null,\n 2,\n );\n\n case \"generic\":\n return JSON.stringify(\n {\n mcpServers: {\n glasstrace: {\n type: \"http\",\n url: endpoint,\n headers: {\n Authorization: `Bearer ${bearer}`,\n },\n },\n },\n },\n null,\n 2,\n );\n\n default: {\n const _exhaustive: never = agent.name;\n throw new Error(`Unknown agent: ${_exhaustive}`);\n }\n }\n}\n\n/**\n * Strict pattern accepted as the value substituted into a `v=<sdkVersion>`\n * marker stamp. Covers the SDK's own published versions\n * (e.g. `1.4.0`, `0.0.0-canary-20260508120000`, `1.4.0+build.42`) without\n * admitting whitespace, angle-brackets, or terminal control sequences\n * that could be smuggled into the agent instruction file via a\n * malformed callsite.\n *\n * The stamp is the SDK semver string and nothing else (DISC-1592 / SDK-050\n * Required Semantics Item 1: \"the stamp encodes only the SDK semver\n * string ... it must not embed user-controlled or environment-derived\n * content\"). Reject anything outside this charset at the render site\n * rather than relying on the upstream `__SDK_VERSION__` define being\n * well-formed.\n */\nconst SDK_VERSION_STAMP_PATTERN = /^[A-Za-z0-9.+-]+$/;\n\n/**\n * Marker pair used to delimit the Glasstrace section in agent info files.\n */\ninterface MarkerPair {\n start: string;\n end: string;\n}\n\nfunction htmlMarkers(sdkVersion: string): MarkerPair {\n return {\n start: `<!-- glasstrace:mcp:start v=${sdkVersion} -->`,\n end: \"<!-- glasstrace:mcp:end -->\",\n };\n}\n\nfunction hashMarkers(sdkVersion: string): MarkerPair {\n return {\n start: `# glasstrace:mcp:start v=${sdkVersion}`,\n end: \"# glasstrace:mcp:end\",\n };\n}\n\n/**\n * Generates informational content for an agent's instruction file.\n *\n * This content is designed to be appended to or inserted into agent-specific\n * instruction files (CLAUDE.md, .cursorrules, codex.md). It contains a\n * tight, agent-facing decision policy + workflow + tool list — no\n * endpoint URL, no auth tokens, no setup instructions (those live in\n * the user's MCP config and the SDK README; the agent reads this file\n * to decide WHEN to call Glasstrace MCP and HOW to use the returned\n * evidence).\n *\n * The rendered block opens with explicit \"Call Glasstrace FIRST when\"\n * / \"SKIP Glasstrace when\" decision rules so a frontier agent has a\n * cheap pre-tool-call heuristic it can apply BEFORE spending tokens\n * on tool consideration. The Workflow section names\n * `find_trace_candidates` as the discovery entry point and instructs\n * the agent to READ `closeMatches` / `recentRoutesSample` /\n * `recoveryActions` before pivoting to source — that is the\n * load-bearing recovery contract from MCP-025 / MCP-027 (codified\n * in the server-side MCP contract maintained in the private\n * `glasstrace-product` repo's `shared/types/wire-mcp.ts` —\n * `ToolDiagnosticSchema` / `CandidateDiagnosticSchema`) and it\n * prevents the bail-to-source failure mode that the prior SDK-050\n * cost-aware decision paragraph did not surface.\n *\n * The body itself lives in a sibling module\n * (`agent-instruction-text.ts`) so future content evolutions are a\n * single-file edit and don't disturb the marker / version-stamp /\n * per-agent-format machinery in this file.\n *\n * The start marker carries a `v=<sdkVersion>` stamp (DISC-1592 /\n * SDK-050) so a later `glasstrace upgrade-instructions` run — and\n * the SDK's stale-section warning at init — can detect that the\n * file was rendered by an older SDK and refresh the block.\n *\n * @param agent - The detected agent to generate info for.\n * @param endpoint - The Glasstrace MCP endpoint URL. (Validated for\n * non-emptiness here for backwards compatibility with the prior\n * SDK-050 contract; not currently inlined in the body — agents\n * reach Glasstrace via the MCP server name `glasstrace` configured\n * separately in `.glasstrace/mcp.json` or per-agent native config.)\n * @param sdkVersion - The SDK semver string to embed in the start marker\n * (e.g. `1.4.0`, `0.0.0-canary-20260508120000`). Must match\n * `[A-Za-z0-9.+\\-]+`; arbitrary or empty values throw.\n * @returns The formatted info section string, or empty string for agents without a supported info file format.\n * @throws If endpoint is empty, or if sdkVersion is empty or contains\n * characters outside the accepted stamp charset.\n */\nexport function generateInfoSection(\n agent: DetectedAgent,\n endpoint: string,\n sdkVersion: string,\n): string {\n if (!endpoint || endpoint.trim() === \"\") {\n throw new Error(\"endpoint must not be empty\");\n }\n if (!sdkVersion || sdkVersion.trim() === \"\") {\n throw new Error(\"sdkVersion must not be empty\");\n }\n if (!SDK_VERSION_STAMP_PATTERN.test(sdkVersion)) {\n throw new Error(\n \"sdkVersion must match [A-Za-z0-9.+\\\\-]+ (semver-shaped, no whitespace, no angle brackets)\",\n );\n }\n\n const content = buildAgentInstructionBody();\n\n switch (agent.name) {\n case \"claude\": {\n const m = htmlMarkers(sdkVersion);\n return `${m.start}\\n${content}${m.end}\\n`;\n }\n\n case \"codex\": {\n const m = htmlMarkers(sdkVersion);\n return `${m.start}\\n${content}${m.end}\\n`;\n }\n\n case \"cursor\": {\n const m = hashMarkers(sdkVersion);\n return `${m.start}\\n${content}${m.end}\\n`;\n }\n\n case \"gemini\":\n case \"windsurf\":\n case \"generic\":\n return \"\";\n\n default: {\n const _exhaustive: never = agent.name;\n throw new Error(`Unknown agent: ${_exhaustive}`);\n }\n }\n}\n","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":["import_node_path","resolve","import_promises","import_node_path"]}
|
|
1
|
+
{"version":3,"sources":["../../src/cli/upgrade-instructions.ts","../../src/mcp-runtime.ts","../../src/agent-detection/detect.ts","../../src/agent-detection/inject.ts","../../src/agent-detection/inject-all-targets.ts","../../src/agent-detection/agent-instruction-text.ts","../../src/agent-detection/configs.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { dirname, isAbsolute, join, relative } from \"node:path\";\nimport { MCP_ENDPOINT } from \"../mcp-runtime.js\";\nimport { detectAgents } from \"../agent-detection/detect.js\";\nimport { hasManagedSection } from \"../agent-detection/inject.js\";\nimport { injectAllTargets } from \"../agent-detection/inject-all-targets.js\";\nimport type { DetectedAgent } from \"../agent-detection/detect.js\";\n\n/**\n * Per-agent legacy destinations that may carry a pre-Wave-18 managed\n * section. Used by the upgrade-instructions opted-in gate to detect\n * users who installed via an older SDK and need their managed section\n * migrated to the Wave 18 canonical destinations.\n */\nfunction legacyDestinationsForAgent(agent: DetectedAgent): string[] {\n // Resolve legacy paths relative to the agent's detected `foundDir`\n // (the directory where `detectAgents` resolved the marker), NOT\n // against the project root. `detectAgents` walks up to the git\n // root for monorepo support, so for a project initialized in\n // `packages/api/` with a legacy `codex.md` at the git root, the\n // detected agent's `infoFilePath` is `<gitRoot>/AGENTS.md` and we\n // need the legacy `codex.md` to resolve under `<gitRoot>`, not\n // under `packages/api/` (Codex P2 review of v5).\n if (agent.infoFilePath === null) {\n return [];\n }\n switch (agent.name) {\n case \"codex\":\n // agent.infoFilePath = <foundDir>/AGENTS.md → foundDir = dirname\n return [join(dirname(agent.infoFilePath), \"codex.md\")];\n case \"cursor\":\n // agent.infoFilePath = <foundDir>/.cursor/rules/glasstrace.mdc\n // → foundDir = dirname(dirname(dirname(infoFilePath)))\n return [\n join(dirname(dirname(dirname(agent.infoFilePath))), \".cursorrules\"),\n ];\n case \"windsurf\":\n // agent.infoFilePath = <foundDir>/.windsurf/rules/glasstrace.md\n // → foundDir = dirname(dirname(dirname(infoFilePath)))\n return [\n join(\n dirname(dirname(dirname(agent.infoFilePath))),\n \".windsurfrules\",\n ),\n ];\n case \"claude\":\n case \"gemini\":\n case \"generic\":\n return [];\n }\n}\n\n/**\n * Returns true if any of the candidate file paths contains a managed\n * section. Walks each candidate via `hasManagedSection`; aggregates\n * the result.\n */\nasync function anyHasManagedSection(paths: string[]): Promise<boolean> {\n for (const p of paths) {\n if (await hasManagedSection(p)) {\n return true;\n }\n }\n return false;\n}\n\n// Declare the tsup-injected SDK version literal. Replaced at build time\n// via `define` in tsup.config.ts. Falls back to \"0.0.0-dev\" when\n// running tests under vitest (no tsup build step).\ndeclare const __SDK_VERSION__: string;\n\n/**\n * Options for {@link runUpgradeInstructions}. The CLI entry point in\n * `init.ts` wires `process.cwd()` through `resolveProjectRoot()` so\n * monorepo roots resolve to the active app directory; tests pass an\n * explicit `projectRoot` for isolation.\n */\nexport interface UpgradeInstructionsOptions {\n projectRoot: string;\n}\n\n/**\n * Result of running the upgrade-instructions command. Returned to the\n * CLI entry point so it can render output without forcing the core\n * logic to call `process.stderr.write` / `process.exit`.\n */\nexport interface UpgradeInstructionsResult {\n exitCode: number;\n /**\n * Files whose managed Glasstrace section was refreshed in place.\n * Reported as paths relative to {@link UpgradeInstructionsOptions.projectRoot}\n * so the CLI output stays portable across machines and developer\n * homes; an absolute path is returned only when the detected file\n * lives outside the resolved project root (e.g. Windsurf's global\n * config under `$HOME/.codeium/`), where a relative form would be\n * misleading.\n */\n refreshed: string[];\n /**\n * Files inspected that did not contain a managed section, and were\n * therefore left untouched. Reported so the user can verify the\n * command did not accidentally append a block to a hand-written\n * instruction file. Same path-shape rule as\n * {@link UpgradeInstructionsResult.refreshed}.\n */\n skipped: string[];\n /**\n * Soft warnings (e.g. permission errors handled internally by\n * `injectInfoSection`). One line per issue.\n */\n warnings: string[];\n /**\n * Hard errors that prevented the command from completing.\n */\n errors: string[];\n}\n\n/**\n * Refreshes the managed Glasstrace MCP section in every detected agent\n * instruction file in the project (DISC-1592 / SDK-050 §Required\n * Semantics Item 2). Idempotent and safe to re-run; the helper only\n * touches files that already contain a marker pair, so a hand-written\n * `CLAUDE.md` without a Glasstrace block is left alone.\n *\n * Multi-file projects are handled in a single run (DISC-1592 §Multi-file\n * projects): the same `detectAgents()` call that scaffolds files at\n * `init` time enumerates every detected agent, and this function\n * refreshes every file with a managed section in one pass.\n *\n * The replace-in-place behaviour works for both legacy unstamped\n * markers (pre-SDK-050) and SDK-050+ stamped markers — see\n * `findMarkerBoundaries` in `inject.ts`.\n *\n * @param options - Project root to operate on. The CLI entry point\n * resolves monorepo roots before calling this function.\n */\n/**\n * Renders an absolute file path in a form suitable for CLI output:\n * relative to `projectRoot` when the file lives inside the tree, or\n * the original absolute path otherwise. Keeps output portable for\n * normal in-tree files (`AGENTS.md`, `CLAUDE.md`, `GEMINI.md`,\n * `.cursor/rules/glasstrace.mdc`, `.windsurf/rules/glasstrace.md`, plus\n * legacy `.cursorrules` / `codex.md` / `.windsurfrules`) while\n * preserving full paths for out-of-tree targets like Windsurf's\n * global config (`$HOME/.codeium/windsurf/mcp_config.json`), where a\n * relative form (e.g. `../../../../home/.../mcp_config.json`) would\n * be harder to read than the absolute path.\n */\nfunction formatPathForOutput(filePath: string, projectRoot: string): string {\n const rel = relative(projectRoot, filePath);\n if (rel === \"\" || rel.startsWith(\"..\") || isAbsolute(rel)) {\n return filePath;\n }\n return rel;\n}\n\nexport async function runUpgradeInstructions(\n options: UpgradeInstructionsOptions,\n): Promise<UpgradeInstructionsResult> {\n const refreshed: string[] = [];\n const skipped: string[] = [];\n const warnings: string[] = [];\n const errors: string[] = [];\n\n let agents;\n try {\n agents = await detectAgents(options.projectRoot);\n } catch (err) {\n errors.push(\n `Failed to detect agents: ${err instanceof Error ? err.message : String(err)}`,\n );\n return { exitCode: 1, refreshed, skipped, warnings, errors };\n }\n\n const sdkVersion =\n typeof __SDK_VERSION__ === \"string\" ? __SDK_VERSION__ : \"0.0.0-dev\";\n\n // Track opted-in agents by identity (the agent reference itself),\n // NOT by display path. Multiple agents (notably codex + generic)\n // share the same canonical destination `AGENTS.md`; if we filtered\n // by path-collision against `skipped`, a generic skip would\n // incorrectly remove an opted-in codex from the refresh set\n // (Codex P1 review of PR #274). Identity-based tracking avoids\n // the collision.\n const optedInAgents: DetectedAgent[] = [];\n for (const agent of agents) {\n if (agent.infoFilePath === null) {\n // Detected agent with no canonical infoFilePath — nothing to\n // refresh. (Pre-Wave-18 this branch covered Gemini / Windsurf /\n // generic which had `infoFilePath: null`; Wave 18 wires all six\n // agents to a non-null canonical destination, so in practice\n // this guard is now defensive.)\n continue;\n }\n\n const displayPath = formatPathForOutput(\n agent.infoFilePath,\n options.projectRoot,\n );\n\n // Wave 18: refresh-gate semantics broadened.\n //\n // The pre-Wave-18 logic refused to inject when the canonical\n // `agent.infoFilePath` had no managed section (preserving opt-out\n // for users who deleted CLAUDE.md content). After Wave 18 the\n // canonical destinations changed (Codex `codex.md` → `AGENTS.md`;\n // Cursor `.cursorrules` → `.cursor/rules/glasstrace.mdc`; etc.),\n // so the new canonical file usually does NOT have a managed\n // section yet for legacy users — but the LEGACY file does, and\n // those users intend to migrate. Check both: the canonical 2026\n // destination AND the agent's known legacy destinations. If\n // either has a managed section the user has opted in; refresh\n // proceeds. If neither has one, the user opted out; skip.\n const legacyDestinations = legacyDestinationsForAgent(agent);\n let optedIn: boolean;\n try {\n optedIn = await anyHasManagedSection([\n agent.infoFilePath,\n ...legacyDestinations,\n ]);\n } catch (err) {\n warnings.push(\n `Could not inspect ${displayPath}: ${err instanceof Error ? err.message : String(err)}`,\n );\n continue;\n }\n\n if (!optedIn) {\n // No managed section in any known destination — user opted out\n // (or never installed). Refusing to inject prevents\n // `upgrade-instructions` from adding a Glasstrace block to a\n // project that doesn't want one.\n skipped.push(displayPath);\n continue;\n }\n\n optedInAgents.push(agent);\n }\n\n // Wave 18: refresh via `injectAllTargets` (multi-target dispatcher).\n // Surviving agents pass through this single hoisted dispatch which\n // writes to canonical 2026 destinations + AGENTS.md companion +\n // Cursor `.cursorrules` transitional fallback, deduplicating\n // AGENTS.md across agents.\n if (optedInAgents.length > 0) {\n try {\n await injectAllTargets(\n optedInAgents,\n MCP_ENDPOINT,\n sdkVersion,\n options.projectRoot,\n );\n for (const a of optedInAgents) {\n if (a.infoFilePath !== null) {\n refreshed.push(formatPathForOutput(a.infoFilePath, options.projectRoot));\n }\n }\n } catch (err) {\n errors.push(\n `Failed to refresh agent-instruction files: ${err instanceof Error ? err.message : String(err)}`,\n );\n }\n }\n\n // Codex P3 review of v4: dedupe `skipped` against `refreshed`. In\n // a legacy Codex migration, codex (canonical destination AGENTS.md)\n // and generic (also canonical destination AGENTS.md) share the same\n // path; codex's legacy `codex.md` carries the managed section so\n // codex is opted-in and refreshes AGENTS.md, while generic has no\n // managed section anywhere and is recorded in `skipped`. Without\n // this dedup the SAME path \"AGENTS.md\" appears in both `refreshed`\n // and `skipped`, contradicting itself for users and automation\n // parsing the lists. Refresh wins (the file was actually written).\n const refreshedSet = new Set(refreshed);\n const dedupedSkipped = skipped.filter((p) => !refreshedSet.has(p));\n\n const exitCode = errors.length === 0 ? 0 : 1;\n return {\n exitCode,\n refreshed,\n skipped: dedupedSkipped,\n warnings,\n errors,\n };\n}\n","import { createHash } from \"node:crypto\";\nimport {\n AnonApiKeySchema,\n DevApiKeySchema,\n type AnonApiKey,\n type DevApiKey,\n} from \"@glasstrace/protocol\";\nimport { readAnonKey, readClaimedKey } from \"./anon-key.js\";\nimport { atomicWriteFile } from \"./atomic-write.js\";\n\n/**\n * Glasstrace MCP endpoint embedded in managed MCP configs and used by\n * the runtime claim-refresh path. Lives here (not in `cli/constants.ts`)\n * so the runtime helper can reach it without crossing the runtime/CLI\n * boundary; CLI callers import it directly from this module.\n */\nexport const MCP_ENDPOINT = \"https://api.glasstrace.dev/mcp\";\n\n/**\n * Runtime-safe MCP credential and config utilities.\n *\n * This module is loaded into user processes at SDK boot. It must not\n * import from `cli/*` or `agent-detection/*` so the runtime bundle does\n * not pull in CLI scaffolding or filesystem scanners. The boundary is\n * enforced by an import-graph guard test.\n *\n * Internal: not re-exported via `node-entry.ts` or `index.ts`.\n *\n * @module\n */\n\nlet fsPathCache:\n | { fs: typeof import(\"node:fs/promises\"); path: typeof import(\"node:path\") }\n | null\n | undefined;\n\nasync function loadFsPath(): Promise<\n | { fs: typeof import(\"node:fs/promises\"); path: typeof import(\"node:path\") }\n | null\n> {\n if (fsPathCache !== undefined) return fsPathCache;\n try {\n const [fs, path] = await Promise.all([\n import(\"node:fs/promises\"),\n import(\"node:path\"),\n ]);\n fsPathCache = { fs, path };\n return fsPathCache;\n } catch {\n fsPathCache = null;\n return null;\n }\n}\n\n/**\n * Computes a stable identity fingerprint for deduplication purposes.\n * This is NOT password hashing — the input is an opaque token used as\n * a marker identity, not a credential stored for authentication.\n *\n * @internal Exported for unit testing and for `cli/scaffolder.ts`'s\n * marker writer.\n */\nexport function identityFingerprint(token: string): string {\n return `sha256:${createHash(\"sha256\").update(token).digest(\"hex\")}`;\n}\n\n/**\n * Compares two MCP config strings for strict canonical-JSON equality.\n * Returns `true` when both inputs parse as JSON and produce structurally\n * equal objects after recursive key sorting; falls back to trimmed text\n * comparison for TOML and other non-JSON formats. Returns `false` on\n * parse errors that don't fall through to text comparison.\n *\n * Used to detect manually-edited MCP configs before overwriting them\n * (DISC-1247 Scenario 2c) and as the staleness signal for SDK-managed\n * configs that must be refreshed when the project's effective\n * credential changes.\n *\n * The matcher is intentionally strict: a hand-edited file that drops\n * any field (including `type: \"http\"` on a Claude `.mcp.json`) must\n * NOT be treated as SDK-managed and silently overwritten on re-init.\n * Callers that need backwards-compatible recognition of the legacy\n * generic `.glasstrace/mcp.json` shape (no `type: \"http\"`, written by\n * SDK versions prior to DISC-1572) should use\n * {@link genericMcpConfigOrLegacyShapeMatches} instead.\n *\n * @internal Exported for unit testing only.\n */\nexport function mcpConfigMatches(\n existingContent: string,\n expectedContent: string,\n): boolean {\n const trimmedExpected = expectedContent.trim();\n\n try {\n const existingParsed: unknown = JSON.parse(existingContent);\n const expectedParsed: unknown = JSON.parse(trimmedExpected);\n return (\n JSON.stringify(canonicalize(existingParsed)) ===\n JSON.stringify(canonicalize(expectedParsed))\n );\n } catch {\n // Fall through to text comparison for TOML and other non-JSON formats.\n }\n\n return existingContent.trim() === trimmedExpected;\n}\n\n/**\n * Generic-shape variant of {@link mcpConfigMatches} that also recognizes\n * the legacy `.glasstrace/mcp.json` shape written by SDK versions prior\n * to DISC-1572 (the same generic shape minus `type: \"http\"`).\n *\n * Scoped to callers that operate exclusively on `.glasstrace/mcp.json`\n * (currently {@link refreshMcpConfigAfterClaim}). It must not be used\n * by per-agent init paths: `decideMcpConfigAction` runs against every\n * detected agent's `.mcp.json`, and a Claude file whose `type` field\n * was hand-edited away must NOT be treated as SDK-managed and silently\n * overwritten on re-init. The shared {@link mcpConfigMatches} stays\n * strict for that reason; the legacy fallback lives here.\n *\n * The fallback is bounded: only the specific legacy generic shape\n * (the new shape minus `type: \"http\"`) is accepted; arbitrary field\n * omissions still report a mismatch.\n *\n * @internal Exported for unit testing only.\n */\nexport function genericMcpConfigOrLegacyShapeMatches(\n existingContent: string,\n expectedGenericContent: string,\n): boolean {\n if (mcpConfigMatches(existingContent, expectedGenericContent)) {\n return true;\n }\n\n try {\n const expectedParsed: unknown = JSON.parse(expectedGenericContent.trim());\n const expectedLegacy = stripTypeFromGlasstraceServer(expectedParsed);\n if (expectedLegacy === null) {\n return false;\n }\n const existingParsed: unknown = JSON.parse(existingContent);\n return (\n JSON.stringify(canonicalize(existingParsed)) ===\n JSON.stringify(canonicalize(expectedLegacy))\n );\n } catch {\n return false;\n }\n}\n\n/**\n * Returns a copy of `value` with `type: \"http\"` removed from\n * `mcpServers.glasstrace`, or `null` when `value` is not the new\n * Claude-compatible SDK shape. Used by\n * {@link genericMcpConfigOrLegacyShapeMatches} to recognize legacy\n * on-disk files written by SDK versions prior to DISC-1572.\n *\n * The helper is deliberately narrow: it only strips when `type` is\n * exactly the string `\"http\"`, and only from the `glasstrace` server\n * entry. Any structural mismatch (non-object root, missing\n * `mcpServers`, missing `glasstrace`, or `type !== \"http\"`) yields\n * `null`, leaving the strict canonical comparison's verdict in place.\n */\nfunction stripTypeFromGlasstraceServer(value: unknown): unknown | null {\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) {\n return null;\n }\n const root = value as Record<string, unknown>;\n const servers = root[\"mcpServers\"];\n if (\n servers === null ||\n typeof servers !== \"object\" ||\n Array.isArray(servers)\n ) {\n return null;\n }\n const serversObj = servers as Record<string, unknown>;\n const server = serversObj[\"glasstrace\"];\n if (\n server === null ||\n typeof server !== \"object\" ||\n Array.isArray(server)\n ) {\n return null;\n }\n const serverObj = server as Record<string, unknown>;\n if (serverObj[\"type\"] !== \"http\") {\n return null;\n }\n const { type: _omittedType, ...serverWithoutType } = serverObj;\n void _omittedType;\n return {\n ...root,\n mcpServers: {\n ...serversObj,\n glasstrace: serverWithoutType,\n },\n };\n}\n\nfunction canonicalize(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map(canonicalize);\n }\n if (value !== null && typeof value === \"object\") {\n const obj = value as Record<string, unknown>;\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(obj).sort()) {\n sorted[key] = canonicalize(obj[key]);\n }\n return sorted;\n }\n return value;\n}\n\n/**\n * Parses a `.env.local` file's text content for `GLASSTRACE_API_KEY`,\n * returning the last assignment's value. Empty values\n * (`GLASSTRACE_API_KEY=`) and the `your_key_here` placeholder are\n * filtered out. Surrounding single or double quotes are stripped.\n *\n * The resolver validates the returned value against `DevApiKeySchema`\n * before accepting it; this parser is permissive on purpose so that\n * malformed values can be flagged with a `malformed-env-local`\n * warning rather than silently dropped.\n *\n * @internal Exported for unit testing only.\n */\nexport function readEnvLocalApiKey(content: string): string | null {\n let last: string | null = null;\n const regex = /^\\s*GLASSTRACE_API_KEY\\s*=\\s*(.*)$/gm;\n let match: RegExpExecArray | null;\n while ((match = regex.exec(content)) !== null) {\n const raw = match[1].trim();\n if (raw === \"\") continue;\n const unquoted = raw.replace(/^(['\"])(.*)\\1$/, \"$2\");\n if (unquoted === \"\" || unquoted === \"your_key_here\") continue;\n last = unquoted;\n }\n return last;\n}\n\n/**\n * Returns true when the given API key value looks like a claimed\n * developer key (prefix `gt_dev_`). Defensive against leading or\n * trailing whitespace.\n *\n * **This is a prefix-only check, not strict validation.** Use it as a\n * fast path for \"looks like a claimed key, do not overwrite\". The\n * effective-credential resolver validates with\n * `DevApiKeySchema.safeParse` because a `gt_dev_` prefix alone is not\n * sufficient to authenticate against the backend.\n *\n * @internal Exported for unit testing only.\n */\nexport function isDevApiKey(value: string | null | undefined): boolean {\n if (value === null || value === undefined) return false;\n return value.trim().startsWith(\"gt_dev_\");\n}\n\n/**\n * Returns true when the given API key value is a fully-valid anonymous\n * API key (matches `AnonApiKeySchema`). Used by `registerViaCli` as a\n * runtime guard so that a `DevApiKey` cannot be passed via process\n * arguments to vendor MCP CLIs (which would expose it via `ps` on\n * multi-user hosts).\n *\n * @internal Exported for unit testing only.\n */\nexport function isAnonApiKey(value: string | null | undefined): boolean {\n if (value === null || value === undefined) return false;\n return AnonApiKeySchema.safeParse(value).success;\n}\n\n/**\n * The MCP-effective credential, tagged by which on-disk source produced\n * it. `env-local` and `claimed-key` carry a branded `DevApiKey`;\n * `anon` carries a branded `AnonApiKey`. Internal — not re-exported.\n */\nexport type EffectiveMcpCredential =\n | { source: \"env-local\"; key: DevApiKey }\n | { source: \"claimed-key\"; key: DevApiKey }\n | { source: \"anon\"; key: AnonApiKey };\n\n/**\n * Surfaced when the resolver detected a recoverable anomaly the caller\n * should inform the user about without printing key material.\n *\n * - `malformed-env-local`: `.env.local` set `GLASSTRACE_API_KEY` to a\n * value that fails `DevApiKeySchema`. The resolver fell through.\n * - `claimed-key-only`: the effective credential came from\n * `.glasstrace/claimed-key` because `.env.local` had no usable dev\n * key. Suggest the user copy the key into `.env.local`.\n */\nexport type ResolveWarning = \"malformed-env-local\" | \"claimed-key-only\";\n\n/**\n * The resolved credential plus the on-disk anon key (returned\n * separately so the staleness check does not have to re-read the\n * file) and any warnings the caller should surface to the user.\n */\nexport interface ResolveResult {\n effective: EffectiveMcpCredential | null;\n anonKey: AnonApiKey | null;\n warnings: ReadonlyArray<ResolveWarning>;\n}\n\n/**\n * Resolves the MCP-effective credential for a project, in priority\n * order: `.env.local` `GLASSTRACE_API_KEY` (validated as\n * `DevApiKeySchema`) → `.glasstrace/claimed-key` (validated as\n * `DevApiKeySchema`) → `.glasstrace/anon_key` (`AnonApiKey`). Returns\n * `null` for `effective` only when no source produced a usable key.\n *\n * The function is async because it touches the filesystem. It is\n * called only on the post-claim runtime branch and from the CLI\n * commands `glasstrace init` and `glasstrace mcp add`. It is **not**\n * on the steady-state init path.\n */\nexport async function resolveEffectiveMcpCredential(\n projectRoot?: string,\n): Promise<ResolveResult> {\n const root = projectRoot ?? process.cwd();\n const warnings: ResolveWarning[] = [];\n\n const envLocalKey = await readEnvLocalDevKey(root, warnings);\n const claimedKey = envLocalKey === null ? await readClaimedKey(root) : null;\n const anonKey = await readAnonKey(root);\n\n let effective: EffectiveMcpCredential | null = null;\n if (envLocalKey !== null) {\n effective = { source: \"env-local\", key: envLocalKey };\n } else if (claimedKey !== null) {\n effective = { source: \"claimed-key\", key: claimedKey };\n warnings.push(\"claimed-key-only\");\n } else if (anonKey !== null) {\n effective = { source: \"anon\", key: anonKey };\n }\n\n return { effective, anonKey, warnings };\n}\n\nasync function readEnvLocalDevKey(\n root: string,\n warnings: ResolveWarning[],\n): Promise<DevApiKey | null> {\n const modules = await loadFsPath();\n if (!modules) return null;\n\n const envPath = modules.path.join(root, \".env.local\");\n let content: string;\n try {\n content = await modules.fs.readFile(envPath, \"utf-8\");\n } catch {\n return null;\n }\n\n const raw = readEnvLocalApiKey(content);\n if (raw === null) return null;\n\n const parsed = DevApiKeySchema.safeParse(raw);\n if (!parsed.success) {\n warnings.push(\"malformed-env-local\");\n return null;\n }\n return parsed.data;\n}\n\n/**\n * Source label for the credential a marker file describes.\n *\n * @internal\n */\nexport type MarkerCredentialSource = \"env-local\" | \"claimed-key\" | \"anon\";\n\n/**\n * Descriptor passed to {@link writeMcpMarker} and matched by\n * {@link readMcpMarker}. `credentialHash` is the\n * `identityFingerprint` of the credential actually written into the\n * managed MCP config — never the credential itself.\n *\n * @internal\n */\nexport interface MarkerTarget {\n credentialSource: MarkerCredentialSource;\n credentialHash: string;\n}\n\n/**\n * Normalized state of a `.glasstrace/mcp-connected` marker on disk.\n *\n * - `absent`: no marker file present.\n * - `valid`: a v1 or v2 marker that parsed cleanly. v1 markers are\n * reported as `credentialSource = \"anon\"` with `credentialHash`\n * taken from the legacy `keyHash` field (the v1 schema can only\n * describe an anon credential).\n * - `unknown-version`: the marker has `version > 2`. Treat as\n * not-configured so a future SDK that wrote the marker doesn't\n * block this SDK from refreshing.\n * - `corrupted`: parse failure or schema mismatch. Treat as\n * not-configured.\n *\n * @internal\n */\nexport type MarkerState =\n | { status: \"absent\" }\n | { status: \"valid\"; credentialSource: MarkerCredentialSource; credentialHash: string }\n | { status: \"unknown-version\" }\n | { status: \"corrupted\" };\n\nconst MCP_MARKER_FILE = \"mcp-connected\";\nconst GLASSTRACE_DIR = \".glasstrace\";\n\n/**\n * Reads `.glasstrace/mcp-connected` and returns its normalized state.\n * Used by `mcp add` (marker-mismatch detection) and by\n * {@link writeMcpMarker} (skip-if-match optimization).\n *\n * Reader rules per the design (`SDK-034 D3`):\n * - `version === undefined` → v1: `{ keyHash, configuredAt }`. Mapped\n * to `credentialSource: \"anon\"`, `credentialHash: keyHash`. v1's\n * `keyHash` is itself produced by `identityFingerprint`, so the\n * format matches v2 without conversion.\n * - `version === 2` → v2 reader.\n * - `version > 2` → `unknown-version` (conservative-fail).\n * - Parse failure → `corrupted` (conservative-fail).\n *\n * @internal Exported for unit testing only.\n */\nexport async function readMcpMarker(projectRoot?: string): Promise<MarkerState> {\n const root = projectRoot ?? process.cwd();\n const modules = await loadFsPath();\n if (!modules) return { status: \"absent\" };\n\n const markerPath = modules.path.join(root, GLASSTRACE_DIR, MCP_MARKER_FILE);\n let content: string;\n try {\n content = await modules.fs.readFile(markerPath, \"utf-8\");\n } catch {\n return { status: \"absent\" };\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch {\n return { status: \"corrupted\" };\n }\n\n if (parsed === null || typeof parsed !== \"object\") {\n return { status: \"corrupted\" };\n }\n\n const obj = parsed as Record<string, unknown>;\n const version = obj[\"version\"];\n\n if (version === undefined) {\n // v1: { keyHash, configuredAt }\n const keyHash = obj[\"keyHash\"];\n if (typeof keyHash !== \"string\" || keyHash === \"\") {\n return { status: \"corrupted\" };\n }\n return {\n status: \"valid\",\n credentialSource: \"anon\",\n credentialHash: keyHash,\n };\n }\n\n if (version === 2) {\n const source = obj[\"credentialSource\"];\n const hash = obj[\"credentialHash\"];\n if (\n (source !== \"env-local\" && source !== \"claimed-key\" && source !== \"anon\") ||\n typeof hash !== \"string\" ||\n hash === \"\"\n ) {\n return { status: \"corrupted\" };\n }\n return {\n status: \"valid\",\n credentialSource: source,\n credentialHash: hash,\n };\n }\n\n if (typeof version === \"number\" && version > 2) {\n return { status: \"unknown-version\" };\n }\n\n return { status: \"corrupted\" };\n}\n\n/**\n * Writes a v2 `.glasstrace/mcp-connected` marker. Returns `true` when\n * the marker was created or updated, `false` when an existing marker\n * already records the same `(credentialSource, credentialHash)` pair\n * and was left untouched.\n *\n * Writer always emits v2 with `version: 2`. The legacy `keyHash`\n * field is intentionally omitted from new writes — v1 readers ignore\n * unknown fields and the duplicate would diverge over time. v3+ and\n * corrupted markers are unconditionally overwritten.\n *\n * The directory is created with `0o700` and the file with `0o600`,\n * matching existing scaffolder behavior.\n *\n * @internal Exported for unit testing only.\n */\nexport async function writeMcpMarker(\n projectRoot: string,\n target: MarkerTarget,\n): Promise<boolean> {\n const modules = await loadFsPath();\n if (!modules) return false;\n\n const dirPath = modules.path.join(projectRoot, GLASSTRACE_DIR);\n const markerPath = modules.path.join(dirPath, MCP_MARKER_FILE);\n\n const state = await readMcpMarker(projectRoot);\n if (\n state.status === \"valid\" &&\n state.credentialSource === target.credentialSource &&\n state.credentialHash === target.credentialHash\n ) {\n return false;\n }\n\n await modules.fs.mkdir(dirPath, { recursive: true, mode: 0o700 });\n\n const body = JSON.stringify(\n {\n version: 2,\n credentialSource: target.credentialSource,\n credentialHash: target.credentialHash,\n configuredAt: new Date().toISOString(),\n },\n null,\n 2,\n );\n\n await modules.fs.writeFile(markerPath, body, { mode: 0o600 });\n // writeFile mode only applies on creation on some platforms.\n await modules.fs.chmod(markerPath, 0o600);\n return true;\n}\n\nconst MCP_CONFIG_FILE = \"mcp.json\";\n\n/**\n * The set of outcomes the runtime claim-refresh helper can produce.\n *\n * - `rewrote`: `.glasstrace/mcp.json` matched the SDK-shaped output\n * for the on-disk anon key, was rewritten with the effective\n * credential, and the marker was updated.\n * - `preserved`: `.glasstrace/mcp.json` exists but does not match the\n * SDK-shaped output for the on-disk anon key. The file is left\n * untouched (the user may have hand-edited it). The marker is not\n * touched.\n * - `absent`: `.glasstrace/mcp.json` does not exist (`ENOENT`), or\n * no anon key is on disk so there is nothing to compare against. A\n * project without an anon key never had an SDK-shaped `mcp.json`\n * written by the runtime path, so this branch is a true no-op.\n * - `skipped-anon-source`: the effective credential is `null` or its\n * source is `\"anon\"`. Either way, there is no claim transition to\n * refresh for. Caller should generally gate on\n * `effective.source !== \"anon\"` before invoking the helper; this\n * branch is the runtime-side belt-and-suspenders.\n * - `skipped-not-persisted`: never reached in practice — the caller\n * in `init-client.ts` gates on `writeClaimedKey`'s `persisted` not\n * being `\"none\"`. The variant exists so an exhaustive switch in\n * the caller stays exhaustive if the gate is removed.\n *\n * @internal\n */\nexport type RuntimeRefreshAction =\n | \"rewrote\"\n | \"preserved\"\n | \"absent\"\n | \"skipped-anon-source\"\n | \"skipped-not-persisted\";\n\nlet refreshNudgeEmitted = false;\n\n/**\n * @internal Exported for unit testing only — resets the per-process\n * \"refresh nudge already emitted\" flag.\n */\nexport function __resetRefreshNudgeForTest(): void {\n refreshNudgeEmitted = false;\n}\n\n/**\n * Emits a single redacted stderr line announcing the MCP config\n * refresh. Deduplicated per process via a module-level flag — a\n * second call within the same process is a no-op. Cross-process\n * dedup (the same user running `mcp add` in another terminal moments\n * later) is explicitly out of scope.\n */\nfunction emitRefreshNudge(persistedSource: \"env-local\" | \"claimed-key\"): void {\n if (refreshNudgeEmitted) return;\n refreshNudgeEmitted = true;\n try {\n if (persistedSource === \"claimed-key\") {\n process.stderr.write(\n \"[glasstrace] MCP config refreshed for the new credential. \" +\n \"Copy .glasstrace/claimed-key into .env.local so Codex can pick it up on next restart.\\n\",\n );\n } else {\n process.stderr.write(\n \"[glasstrace] MCP config refreshed for the new credential.\\n\",\n );\n }\n } catch {\n // stderr is best-effort; refresh outcome must not depend on it.\n }\n}\n\n/**\n * Returns the SDK-shaped JSON for `.glasstrace/mcp.json` (the generic\n * MCP config used at runtime). Inlined here — and intentionally not\n * imported from `agent-detection/configs.ts` — because the runtime\n * path must not pull `agent-detection` into the runtime bundle. The\n * shape matches what `generateMcpConfig({ name: \"generic\", ... },\n * endpoint, bearer)` would produce. If the agent-detection version\n * diverges, the staleness check stops detecting SDK-managed configs;\n * a regression test against `generateMcpConfig`'s \"generic\" branch\n * lives in `tests/unit/sdk/mcp-runtime.test.ts`.\n */\nfunction genericMcpConfigContent(endpoint: string, bearer: string): string {\n return JSON.stringify(\n {\n mcpServers: {\n glasstrace: {\n type: \"http\",\n url: endpoint,\n headers: {\n Authorization: `Bearer ${bearer}`,\n },\n },\n },\n },\n null,\n 2,\n );\n}\n\n/**\n * Refreshes `.glasstrace/mcp.json` after a successful account claim\n * transition has persisted a dev/account credential to disk (via\n * `writeClaimedKey`). The file is rewritten only when its content\n * matches the SDK-shaped output for the project's on-disk anon key\n * (canonical-JSON equivalence via `mcpConfigMatches` — whitespace and\n * key order are normalised before comparison). User-edited or\n * third-party `mcp.json` content is preserved.\n *\n * Atomic write protocol: write the replacement to a sibling temp\n * path, set `0o600`, then `rename` into place. This matches the\n * existing pattern at `init-client.ts` for `.glasstrace/config`,\n * `anon-key.ts` for `.glasstrace/anon_key`, and `runtime-state.ts`.\n * The temp must be on the same filesystem as the destination for the\n * `rename` to be atomic.\n *\n * The helper is invoked only on the post-claim runtime branch (see\n * `init-client.ts` `performInit`) and never on the steady-state init\n * path. It must not throw — failures during write/chmod/rename or\n * marker update surface as `\"preserved\"` so the caller's\n * `claimResult` return is preserved. The temp file is best-effort\n * cleaned up on failure to avoid leaving stale `.tmp` siblings on\n * disk.\n *\n * @internal Exported for unit testing only; not re-exported from\n * `node-entry.ts` or `index.ts`.\n */\nexport async function refreshGenericMcpConfigAtRuntime(\n projectRoot: string,\n effective: EffectiveMcpCredential | null,\n anonKeyOnDisk: AnonApiKey | null,\n): Promise<{ action: RuntimeRefreshAction }> {\n if (effective === null || effective.source === \"anon\") {\n return { action: \"skipped-anon-source\" };\n }\n\n // Dev-key-only project (no .glasstrace/anon_key on disk): the\n // staleness check has nothing to compare against. The SDK never\n // wrote mcp.json without an anon key, so there is nothing to\n // refresh.\n if (anonKeyOnDisk === null) {\n return { action: \"absent\" };\n }\n\n const modules = await loadFsPath();\n if (!modules) return { action: \"absent\" };\n\n const dirPath = modules.path.join(projectRoot, GLASSTRACE_DIR);\n const configPath = modules.path.join(dirPath, MCP_CONFIG_FILE);\n\n let existing: string;\n try {\n existing = await modules.fs.readFile(configPath, \"utf-8\");\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"ENOENT\") {\n return { action: \"absent\" };\n }\n return { action: \"preserved\" };\n }\n\n const expectedAnon = genericMcpConfigContent(MCP_ENDPOINT, anonKeyOnDisk);\n if (!genericMcpConfigOrLegacyShapeMatches(existing, expectedAnon)) {\n return { action: \"preserved\" };\n }\n\n // SDK-managed and stale. Replace atomically per SDK 2.0 §4.3:\n // tmp + fsync(tmp) + rename + fsync(parent). Any failure in the\n // helper or marker update path must produce a non-throw outcome\n // so the caller's claimResult return is preserved; the helper\n // best-effort cleans up the .tmp sibling on failure.\n const replacement = genericMcpConfigContent(MCP_ENDPOINT, effective.key);\n try {\n await atomicWriteFile(configPath, replacement, { mode: 0o600 });\n\n await writeMcpMarker(projectRoot, {\n credentialSource: effective.source,\n credentialHash: identityFingerprint(effective.key),\n });\n } catch {\n return { action: \"preserved\" };\n }\n\n emitRefreshNudge(effective.source);\n\n return { action: \"rewrote\" };\n}\n","import { execFile } from \"node:child_process\";\nimport { access, stat } from \"node:fs/promises\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { homedir } from \"node:os\";\nimport { constants } from \"node:fs\";\n\n/**\n * Describes an AI coding agent detected in a project.\n */\nexport interface DetectedAgent {\n name: \"claude\" | \"codex\" | \"gemini\" | \"cursor\" | \"windsurf\" | \"generic\";\n mcpConfigPath: string | null;\n infoFilePath: string | null;\n cliAvailable: boolean;\n registrationCommand: string | null;\n}\n\ntype AgentName = DetectedAgent[\"name\"];\n\ninterface AgentRule {\n name: AgentName;\n /** Paths relative to a search directory that indicate this agent is present. */\n markers: string[];\n /** Function to compute the MCP config path given the directory where markers were found. */\n mcpConfigPath: (markerDir: string) => string;\n /** Function to compute the info file path, or null. */\n infoFilePath: (markerDir: string) => string | null;\n /** CLI binary name to check in PATH, or null if no CLI exists. */\n cliBinary: string | null;\n /** Registration command template, or null. */\n registrationCommand: string | null;\n}\n\nconst AGENT_RULES: AgentRule[] = [\n {\n name: \"claude\",\n markers: [\".claude\", \"CLAUDE.md\"],\n mcpConfigPath: (dir) => join(dir, \".mcp.json\"),\n infoFilePath: (dir) => join(dir, \"CLAUDE.md\"),\n cliBinary: \"claude\",\n registrationCommand: \"npx glasstrace mcp add --agent claude\",\n },\n {\n name: \"codex\",\n // Codex 2026 default discovery is `AGENTS.override.md` → `AGENTS.md` →\n // opt-in `project_doc_fallback_filenames`; `codex.md` is NOT in the\n // default fallback list. Detection requires Codex-specific markers\n // (`codex.md` legacy, `.codex/` config dir) — `AGENTS.md` is NOT\n // included as a marker because the SDK now writes `AGENTS.md`\n // broadly via the multi-target dispatcher's companion writes; if\n // `AGENTS.md` were a Codex marker, every project with the SDK's\n // own companion AGENTS.md would re-classify as Codex on subsequent\n // detect runs and trigger unintended `.codex/config.toml` writes\n // (Codex P1 + Copilot P1 review of Wave 18 PR #274). The canonical\n // write destination remains AGENTS.md regardless of which marker\n // classified the project.\n markers: [\"codex.md\", \".codex\"],\n mcpConfigPath: (dir) => join(dir, \".codex\", \"config.toml\"),\n infoFilePath: (dir) => join(dir, \"AGENTS.md\"),\n cliBinary: \"codex\",\n registrationCommand: \"npx glasstrace mcp add --agent codex\",\n },\n {\n name: \"gemini\",\n markers: [\".gemini\", \"GEMINI.md\"],\n mcpConfigPath: (dir) => join(dir, \".gemini\", \"settings.json\"),\n infoFilePath: (dir) => join(dir, \"GEMINI.md\"),\n cliBinary: \"gemini\",\n registrationCommand: \"npx glasstrace mcp add --agent gemini\",\n },\n {\n name: \"cursor\",\n // `.cursor/rules/*.mdc` is the current canonical format per Cursor's\n // 2026 docs. `.cursorrules` (single file) is supported-but-deprecated\n // and stays as a transitional fallback that the multi-target write\n // helper writes unconditionally alongside the .mdc canonical.\n markers: [\".cursor\", \".cursorrules\"],\n mcpConfigPath: (dir) => join(dir, \".cursor\", \"mcp.json\"),\n infoFilePath: (dir) => join(dir, \".cursor\", \"rules\", \"glasstrace.mdc\"),\n cliBinary: null,\n registrationCommand: \"npx glasstrace mcp add --agent cursor\",\n },\n {\n name: \"windsurf\",\n // Windsurf's current canonical workspace-rules format is\n // `.windsurf/rules/*.md`. AGENTS.md is a parallel cross-tool\n // mechanism Windsurf also reads BUT is NOT included as a Windsurf\n // detection marker — the SDK writes `AGENTS.md` broadly via the\n // multi-target dispatcher's companion writes, so treating\n // `AGENTS.md` as a Windsurf marker would re-classify every\n // SDK-managed project as Windsurf and cause `glasstrace uninit`\n // to mutate the global `~/.codeium/windsurf/mcp_config.json` for\n // non-Windsurf projects (Codex P1 + Copilot P1 review of Wave 18\n // PR #274). The single-file `.windsurfrules` is the deprecated\n // legacy form — recognized as a marker so legacy projects classify\n // correctly, but the SDK no longer writes to it.\n markers: [\".windsurf\", \".windsurfrules\"],\n mcpConfigPath: () =>\n join(homedir(), \".codeium\", \"windsurf\", \"mcp_config.json\"),\n infoFilePath: (dir) => join(dir, \".windsurf\", \"rules\", \"glasstrace.md\"),\n cliBinary: null,\n registrationCommand: \"npx glasstrace mcp add --agent windsurf\",\n },\n];\n\n/**\n * Checks whether a path exists and is accessible, following symlinks.\n * Returns false on permission errors or missing paths.\n *\n * @param mode - The access mode to check (defaults to R_OK for marker detection).\n */\nasync function pathExists(\n path: string,\n mode: number = constants.R_OK,\n): Promise<boolean> {\n try {\n await access(path, mode);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Finds the git root directory by walking up from the given path.\n * Returns the starting directory if no `.git` is found.\n */\nasync function findGitRoot(startDir: string): Promise<string> {\n let current = resolve(startDir);\n\n while (true) {\n if (await pathExists(join(current, \".git\"), constants.F_OK)) {\n return current;\n }\n const parent = dirname(current);\n if (parent === current) {\n // Reached filesystem root without finding .git\n break;\n }\n current = parent;\n }\n\n return resolve(startDir);\n}\n\n/**\n * Returns true if a CLI binary is available on PATH.\n * Uses `which` on Unix and `where` on Windows, via execFile (no shell injection).\n */\nfunction isCliAvailable(binary: string): Promise<boolean> {\n return new Promise((resolve) => {\n const command = process.platform === \"win32\" ? \"where\" : \"which\";\n execFile(command, [binary], (error) => {\n resolve(error === null);\n });\n });\n}\n\n/**\n * Detects AI coding agents present in a project by scanning for marker\n * files and directories. Walks up from projectRoot to the git root to\n * support monorepo layouts.\n *\n * Always includes a \"generic\" fallback entry.\n *\n * @param projectRoot - Absolute or relative path to the project directory.\n * @returns Array of detected agents, with generic always last.\n * @throws If projectRoot does not exist or is not a directory.\n */\nexport async function detectAgents(\n projectRoot: string,\n): Promise<DetectedAgent[]> {\n const resolvedRoot = resolve(projectRoot);\n\n // Validate projectRoot exists and is a directory\n let rootStat;\n try {\n rootStat = await stat(resolvedRoot);\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n throw new Error(\n `projectRoot does not exist: ${resolvedRoot}` +\n (code ? ` (${code})` : \"\"),\n );\n }\n\n if (!rootStat.isDirectory()) {\n throw new Error(`projectRoot is not a directory: ${resolvedRoot}`);\n }\n\n const gitRoot = await findGitRoot(resolvedRoot);\n\n // Collect unique directories to search: projectRoot and every ancestor up to gitRoot\n const searchDirs: string[] = [];\n let current = resolvedRoot;\n while (true) {\n searchDirs.push(current);\n if (current === gitRoot) {\n break;\n }\n const parent = dirname(current);\n if (parent === current) {\n break;\n }\n current = parent;\n }\n\n const detected: DetectedAgent[] = [];\n const seenAgents = new Set<AgentName>();\n\n for (const rule of AGENT_RULES) {\n let foundDir: string | null = null;\n\n // Check each search directory for markers\n for (const dir of searchDirs) {\n let markerFound = false;\n for (const marker of rule.markers) {\n if (await pathExists(join(dir, marker))) {\n markerFound = true;\n break;\n }\n }\n if (markerFound) {\n foundDir = dir;\n break;\n }\n }\n\n if (foundDir === null) {\n continue;\n }\n\n if (seenAgents.has(rule.name)) {\n continue;\n }\n seenAgents.add(rule.name);\n\n // Determine info file path. Wave 18: dropped the prior path-exists\n // gate that nulled out infoFilePath when the file didn't pre-exist —\n // the DISC-1592 / DISC-1602 marker contract makes file creation safe\n // (idempotent in-place replacement on re-runs), and `inject.ts`'s\n // existing create-or-replace logic already handles missing-file\n // creation under the same marker contract. The gate was a safety\n // guard added BEFORE the marker contract existed; with the contract\n // soaked in production it became a gratuitous skip that left new\n // installs with no managed section.\n const infoFilePath = rule.infoFilePath(foundDir);\n\n const cliAvailable = rule.cliBinary\n ? await isCliAvailable(rule.cliBinary)\n : false;\n\n detected.push({\n name: rule.name,\n mcpConfigPath: rule.mcpConfigPath(foundDir),\n infoFilePath,\n cliAvailable,\n registrationCommand: rule.registrationCommand,\n });\n }\n\n // Always include generic fallback. Wave 18: write AGENTS.md as the\n // universal cross-tool fallback (per the agents.md spec governed by\n // the Agentic AI Foundation under the Linux Foundation; adopted by\n // Cursor, Codex, Claude Code, Copilot, Devin, Windsurf, and Gemini\n // CLI). Generic-detected projects (no per-agent markers) previously\n // got NO instruction injection at all.\n detected.push({\n name: \"generic\",\n mcpConfigPath: join(resolvedRoot, \".glasstrace\", \"mcp.json\"),\n infoFilePath: join(resolvedRoot, \"AGENTS.md\"),\n cliAvailable: false,\n registrationCommand: null,\n });\n\n return detected;\n}\n","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 */\nexport function 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","import { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport type { DetectedAgent } from \"./detect.js\";\nimport {\n generateInfoSection,\n generateInfoSectionForCursorMdc,\n generateInfoSectionForCursorrulesLegacy,\n} from \"./configs.js\";\nimport { findMarkerBoundaries } from \"./inject.js\";\n\n/**\n * Wave 18 multi-target write dispatcher.\n *\n * Per DISC-1782 (P1 design_correction, 2026-05-09): the SDK's\n * agent-instruction injection writes the Glasstrace MCP managed\n * section to deprecated/wrong/missing filenames for nearly every\n * supported agent except Claude Code, and never writes to the\n * cross-tool `AGENTS.md` standard governed by the Agentic AI\n * Foundation under the Linux Foundation. Wave 18 corrects this by\n * routing every detected agent through this multi-target helper,\n * which writes to:\n *\n * - Claude Code: CLAUDE.md (primary) + AGENTS.md (companion)\n * - Codex CLI: AGENTS.md (sole — `codex.md` retired)\n * - Gemini CLI: GEMINI.md (primary) + AGENTS.md (companion)\n * - Cursor: .cursor/rules/glasstrace.mdc (canonical)\n * + .cursorrules (transitional fallback, unconditional)\n * + AGENTS.md (companion)\n * - Windsurf: .windsurf/rules/glasstrace.md (workspace-rules)\n * + AGENTS.md (companion — Windsurf reads both)\n * - Generic: AGENTS.md (sole, universal cross-tool fallback)\n *\n * AGENTS.md is deduplicated across multi-agent detection: a project\n * with both `.claude/` and `.cursor/` markers will detect TWO agents\n * (Claude Code + Cursor) but produce ONE AGENTS.md write, not two.\n *\n * **Failure semantics: fail-loud-per-target, non-atomic, all error\n * classes.** If write-to-target-N fails for any reason — permission\n * denied (EACCES/EPERM), read-only filesystem (EROFS), disk full\n * (ENOSPC), path too long (ENAMETOOLONG, common on Windows with\n * deeply nested project paths), I/O error, etc. — log a per-target\n * stderr warning naming the target path and the error kind, and\n * CONTINUE to the remaining targets. This is the broadened\n * fail-loud policy from the wave's 350-pass adversarial review\n * (finding 350-O8) — the prior `isPermissionError` path covered only\n * permission-class errors. Atomic rollback across targets is\n * explicitly OUT OF SCOPE for Wave 18 (track via closeout-gate).\n *\n * **Silent on success.** Successful writes produce no stdout/stderr\n * output. Only failures emit warnings. The SDK runs at user-runtime\n * load and verbose per-write logging would constitute log spam\n * across the user base.\n *\n * **Marker contract preserved.** All targets use the SDK-050 /\n * DISC-1592 / DISC-1602 marker contract for idempotent in-place\n * replacement on re-runs. Markdown-family destinations (CLAUDE.md,\n * AGENTS.md, GEMINI.md, .windsurf/rules/glasstrace.md, the body of\n * .cursor/rules/glasstrace.mdc) use HTML comment markers; the legacy\n * .cursorrules destination uses hash-prefix markers preserved from\n * the SDK-050 contract for backward-compat with already-rendered\n * managed sections.\n *\n * @param agents - All detected agents (typically the result of\n * `detectAgents()`). The helper iterates each, dispatches to its\n * per-agent target set, and dedupes AGENTS.md across the iteration.\n * @param endpoint - The Glasstrace MCP endpoint URL (currently\n * validated for non-emptiness; not inlined in the body).\n * @param sdkVersion - The SDK semver string for the marker stamp.\n * @param projectRoot - The project root, used to compute companion\n * AGENTS.md path when the per-agent rule didn't already point at it.\n */\nexport async function injectAllTargets(\n agents: DetectedAgent[],\n endpoint: string,\n sdkVersion: string,\n projectRoot: string,\n): Promise<void> {\n // Track AGENTS.md paths we've already written to so multi-agent\n // detection doesn't write the same file twice. Keyed by absolute\n // path string (already resolved by `detect.ts`).\n const writtenAgentsMd = new Set<string>();\n\n for (const agent of agents) {\n const targets = computeTargets(agent, projectRoot);\n\n for (const target of targets) {\n // Skip a duplicate AGENTS.md if another agent in the same\n // detection already wrote to it.\n if (target.isAgentsMdCompanion) {\n if (writtenAgentsMd.has(target.path)) {\n continue;\n }\n writtenAgentsMd.add(target.path);\n }\n\n // For cursor-mdc, emit the YAML frontmatter ONLY when creating\n // the file from scratch — appending it verbatim to an existing\n // mdc that has no markers would produce a duplicate `---` block\n // mid-file and corrupt the rule shape (Codex P2 review of v4).\n // The marker contract anchors on the managed-section markers\n // alone; user-customized frontmatter above the managed section\n // is preserved across re-renders. The managed section itself\n // (markers + body, no frontmatter) is identical across all\n // three file states (create / append / in-place replace), so\n // the frontmatter is a \"create-only prefix\" handed to the write\n // helper separately.\n let createContent: string;\n let managedSectionOnly: string;\n if (target.kind === \"cursor-mdc\") {\n createContent = generateInfoSectionForCursorMdc(endpoint, sdkVersion);\n // Same body but without the frontmatter wrapper — used when\n // appending to an existing mdc that already has its own\n // frontmatter.\n managedSectionOnly = generateInfoSection(agent, endpoint, sdkVersion);\n } else if (target.kind === \"cursorrules-legacy\") {\n createContent = generateInfoSectionForCursorrulesLegacy(\n endpoint,\n sdkVersion,\n );\n managedSectionOnly = createContent;\n } else {\n createContent = generateInfoSection(agent, endpoint, sdkVersion);\n managedSectionOnly = createContent;\n }\n\n if (managedSectionOnly === \"\") continue;\n\n await writeManagedSectionToTarget(\n target.path,\n createContent,\n managedSectionOnly,\n );\n }\n }\n}\n\ninterface WriteTarget {\n path: string;\n /**\n * Discriminator the dispatcher uses to pick the right rendering:\n * - \"primary\": render via `generateInfoSection(agent, ...)` using\n * the agent's existing per-agent format dispatch.\n * - \"agents-md-companion\": same content as primary (htmlMarkers)\n * but written to a different file; rendered via the agent's own\n * dispatch (the configs.ts switch routes Markdown-family agents\n * to htmlMarkers, which is correct for AGENTS.md too).\n * - \"cursor-mdc\": render via `generateInfoSectionForCursorMdc` to\n * prepend YAML frontmatter.\n * - \"cursorrules-legacy\": render via\n * `generateInfoSectionForCursorrulesLegacy` with hash-prefix\n * markers preserved from the SDK-050 contract.\n */\n kind: \"primary\" | \"agents-md-companion\" | \"cursor-mdc\" | \"cursorrules-legacy\";\n isAgentsMdCompanion: boolean;\n}\n\n/**\n * Derive the agent's `foundDir` (the directory `detectAgents` resolved\n * the marker in, after walking up to the git root for monorepo\n * support) from `agent.infoFilePath`. The companion AGENTS.md write\n * AND the Cursor `.cursorrules` transitional fallback MUST resolve\n * against this foundDir, not against `projectRoot` — for monorepos\n * where the SDK is initialized in a subdirectory but the agent\n * markers live at the git root, `projectRoot` is the wrong parent\n * (Codex P2 review of v6).\n *\n * Returns null when `agent.infoFilePath` is null (defensive — Wave 18\n * AGENT_RULES wires every agent to a non-null `infoFilePath`, so in\n * practice this never returns null).\n */\nfunction foundDirFromAgent(agent: DetectedAgent): string | null {\n if (agent.infoFilePath === null) return null;\n switch (agent.name) {\n case \"claude\":\n case \"codex\":\n case \"gemini\":\n case \"generic\":\n // infoFilePath is `<foundDir>/<canonical-filename>` — one\n // dirname call gets to foundDir.\n return dirname(agent.infoFilePath);\n case \"cursor\":\n // infoFilePath is `<foundDir>/.cursor/rules/glasstrace.mdc` —\n // three dirname calls strip `.cursor/rules/glasstrace.mdc`.\n return dirname(dirname(dirname(agent.infoFilePath)));\n case \"windsurf\":\n // infoFilePath is `<foundDir>/.windsurf/rules/glasstrace.md` —\n // three dirname calls strip `.windsurf/rules/glasstrace.md`.\n return dirname(dirname(dirname(agent.infoFilePath)));\n }\n}\n\nfunction computeTargets(\n agent: DetectedAgent,\n projectRoot: string,\n): WriteTarget[] {\n const targets: WriteTarget[] = [];\n\n // Resolve the companion AGENTS.md (and Cursor `.cursorrules`\n // transitional fallback) against the agent's foundDir — the\n // directory `detectAgents` walked up to. Falls back to projectRoot\n // only when foundDir cannot be derived (defensive; should not\n // happen in Wave 18). Codex P2 review of v6.\n const foundDir = foundDirFromAgent(agent) ?? projectRoot;\n\n switch (agent.name) {\n case \"claude\": {\n // Primary: CLAUDE.md (per-agent canonical for Claude Code).\n // Companion: AGENTS.md (cross-tool universal write).\n if (agent.infoFilePath) {\n targets.push({\n path: agent.infoFilePath,\n kind: \"primary\",\n isAgentsMdCompanion: false,\n });\n }\n targets.push({\n path: join(foundDir, \"AGENTS.md\"),\n kind: \"agents-md-companion\",\n isAgentsMdCompanion: true,\n });\n return targets;\n }\n\n case \"codex\": {\n // For Codex, the per-agent canonical IS AGENTS.md (set in\n // detect.ts AGENT_RULES). No separate companion needed —\n // dedup logic ensures we don't double-write.\n if (agent.infoFilePath) {\n targets.push({\n path: agent.infoFilePath,\n kind: \"primary\",\n isAgentsMdCompanion: true,\n });\n }\n return targets;\n }\n\n case \"gemini\": {\n // Primary: GEMINI.md (default Gemini context.fileName).\n // Companion: AGENTS.md (Gemini supports it via opt-in).\n if (agent.infoFilePath) {\n targets.push({\n path: agent.infoFilePath,\n kind: \"primary\",\n isAgentsMdCompanion: false,\n });\n }\n targets.push({\n path: join(foundDir, \"AGENTS.md\"),\n kind: \"agents-md-companion\",\n isAgentsMdCompanion: true,\n });\n return targets;\n }\n\n case \"cursor\": {\n // Primary: .cursor/rules/glasstrace.mdc (canonical 2026 format).\n // Transitional: .cursorrules (legacy, written unconditionally\n // per Codex P2 review of DISC-1782 v3 — mixed-version Cursor\n // scenarios may have Agent mode reading legacy rules\n // inconsistently, so a conditional fallback is too narrow).\n // Companion: AGENTS.md (Cursor reads it as cross-tool standard).\n if (agent.infoFilePath) {\n targets.push({\n path: agent.infoFilePath,\n kind: \"cursor-mdc\",\n isAgentsMdCompanion: false,\n });\n // .cursorrules sits at foundDir — same parent as the\n // .cursor/rules/ subtree.\n targets.push({\n path: join(foundDir, \".cursorrules\"),\n kind: \"cursorrules-legacy\",\n isAgentsMdCompanion: false,\n });\n }\n targets.push({\n path: join(foundDir, \"AGENTS.md\"),\n kind: \"agents-md-companion\",\n isAgentsMdCompanion: true,\n });\n return targets;\n }\n\n case \"windsurf\": {\n // Primary: .windsurf/rules/glasstrace.md (active workspace-rules).\n // Companion: AGENTS.md (cross-tool parallel mechanism Windsurf\n // also reads — NOT a replacement for workspace-rules, per\n // windsurf.com/university docs).\n if (agent.infoFilePath) {\n targets.push({\n path: agent.infoFilePath,\n kind: \"primary\",\n isAgentsMdCompanion: false,\n });\n }\n targets.push({\n path: join(foundDir, \"AGENTS.md\"),\n kind: \"agents-md-companion\",\n isAgentsMdCompanion: true,\n });\n return targets;\n }\n\n case \"generic\": {\n // For generic, the infoFilePath set in detect.ts IS AGENTS.md.\n // Mark as companion so multi-agent dedup applies.\n if (agent.infoFilePath) {\n targets.push({\n path: agent.infoFilePath,\n kind: \"primary\",\n isAgentsMdCompanion: true,\n });\n }\n return targets;\n }\n\n default: {\n const _exhaustive: never = agent.name;\n throw new Error(`Unknown agent: ${_exhaustive}`);\n }\n }\n}\n\n/**\n * Write the managed section to a single target file with broadened\n * fail-loud-per-target semantics.\n *\n * Mirrors the create-or-replace logic in `inject.ts`'s\n * `injectInfoSection` but catches ALL write errors (not just\n * `EACCES`/`EPERM`/`EROFS`) and logs a per-error-class qualifier so\n * the user can distinguish permission failures from disk-full / path-\n * too-long / I/O failures.\n *\n * Accepts two content strings to handle the cursor-mdc case cleanly\n * (Codex P2 review of v4):\n *\n * - `createContent` — used ONLY when the target file does not\n * already exist. For cursor-mdc this includes the YAML\n * frontmatter wrapper (`--- ... ---`) ABOVE the managed section.\n * - `managedSectionOnly` — used when the target file already\n * exists, regardless of whether it carries existing markers.\n * For cursor-mdc this is the managed section without the\n * frontmatter, so appending it to an existing `.mdc` (which\n * already has its own frontmatter) does NOT produce a duplicate\n * `--- ... ---` block mid-file.\n *\n * For non-mdc targets the two strings are identical (the body\n * IS the full content).\n */\nasync function writeManagedSectionToTarget(\n filePath: string,\n createContent: string,\n managedSectionOnly: string,\n): Promise<void> {\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 emitTargetWarning(filePath, \"read\", err);\n return;\n }\n }\n\n // File does not exist — create with the full content (frontmatter\n // wrapper for cursor-mdc, or the managed section alone for other\n // targets).\n if (existingContent === null) {\n try {\n await mkdir(dirname(filePath), { recursive: true });\n await writeFile(filePath, createContent, \"utf-8\");\n } catch (err: unknown) {\n emitTargetWarning(filePath, \"write\", err);\n return;\n }\n return;\n }\n\n // File exists — check for markers. For both branches below we use\n // `managedSectionOnly` (NOT `createContent`) so we don't inject a\n // second YAML frontmatter block into a cursor-mdc file that\n // already has one. The user's existing frontmatter is preserved.\n const lines = existingContent.split(\"\\n\");\n const boundaries = findMarkerBoundaries(lines);\n\n let newContent: string;\n if (boundaries !== null) {\n const before = lines.slice(0, boundaries.startIdx);\n const after = lines.slice(boundaries.endIdx + 1);\n const contentWithoutTrailingNewline = managedSectionOnly.endsWith(\"\\n\")\n ? managedSectionOnly.slice(0, -1)\n : managedSectionOnly;\n newContent = [...before, contentWithoutTrailingNewline, ...after].join(\n \"\\n\",\n );\n } else {\n const separator = existingContent.endsWith(\"\\n\") ? \"\\n\" : \"\\n\\n\";\n newContent = existingContent + separator + managedSectionOnly;\n }\n\n try {\n await writeFile(filePath, newContent, \"utf-8\");\n } catch (err: unknown) {\n emitTargetWarning(filePath, \"write\", err);\n }\n}\n\n/**\n * Emit a per-target stderr warning with an error-class qualifier so\n * users can distinguish failure modes (permission vs disk-full vs\n * path-too-long etc.). No-op when stderr is unavailable (e.g., a\n * non-Node runtime, though the SDK is Node-only).\n */\nfunction emitTargetWarning(\n filePath: string,\n op: \"read\" | \"write\",\n err: unknown,\n): void {\n const code = (err as NodeJS.ErrnoException).code;\n let qualifier: string;\n switch (code) {\n case \"EACCES\":\n case \"EPERM\":\n qualifier = \"permission denied\";\n break;\n case \"EROFS\":\n qualifier = \"filesystem read-only\";\n break;\n case \"ENOSPC\":\n qualifier = \"disk full\";\n break;\n case \"ENAMETOOLONG\":\n qualifier = \"path too long\";\n break;\n case \"ENOTDIR\":\n qualifier = \"not a directory\";\n break;\n case \"EISDIR\":\n qualifier = \"is a directory\";\n break;\n default:\n qualifier = \"I/O error\";\n break;\n }\n try {\n process.stderr.write(\n `Warning: cannot ${op} info file ${filePath}: ${qualifier}\\n`,\n );\n } catch {\n // stderr unavailable — silently swallow per the SDK-050\n // never-throw-from-instrumentation invariant.\n }\n}\n","/**\n * The text body the SDK injects into a user's agent-instruction file\n * (AGENTS.md, CLAUDE.md, GEMINI.md, .cursor/rules/glasstrace.mdc,\n * .windsurf/rules/glasstrace.md, plus legacy .cursorrules — Wave 18\n * expanded the canonical set per DISC-1782) between the\n * `<!-- glasstrace:mcp:start v=<sdkVersion> -->` ... `<!-- glasstrace:mcp:end -->`\n * managed-section markers.\n *\n * **Why this lives in its own module:** the text is the contract\n * between the SDK and the user's coding agent at runtime — what the\n * AI reads when it decides whether to call Glasstrace MCP and how to\n * use the returned evidence. It evolves on a different cadence from\n * the surrounding marker / version-stamp / per-agent-format\n * machinery in `configs.ts`. Keeping it in a sibling module means\n * future content edits are a single-file change and don't risk\n * disturbing the `configs.ts` rendering machinery (which carries the\n * SDK-050 / DISC-1592 / DISC-1602 marker contract that has soaked in\n * production and must not regress).\n *\n * **Vocabulary alignment:** every MCP tool name and response-field\n * name in the body below is verified against the current MCP server\n * contract maintained in the private `glasstrace-product` repo\n * (`shared/types/wire-mcp.ts` and `shared/types/agent-evidence.ts`\n * there); the SDK consumes the resulting wire format but does not\n * own the schema source of truth for those tool names and field\n * names. If the server-side MCP contract evolves (renames a field,\n * restructures `suggestedFollowups`, adds new tools, etc.), update\n * this module in lockstep with the protocol change so the\n * agent-instruction text never references fields that don't exist.\n *\n * **Wave 17 follow-up (2026-05-09, post-PR-998):** the\n * vocabulary-mismatch-recovery wave (DISC-1626 + 40 sibling DISCs,\n * shipped via `glasstrace-product` PR #998) added five fields to\n * the no-match envelope on `find_trace_candidates`'s\n * `CandidateDiagnosticSchema` and the sibling-tools'\n * `ToolDiagnosticSchema`: `windowActivity`, `humanReadable`,\n * `diagnosticValue`, `recommendedNextStep`, and `maxUsefulFollowups`.\n * The Workflow §4 below names `closeMatches` /\n * `recentRoutesSample` / `windowActivity` / `humanReadable` /\n * `recoveryActions` / `diagnosticValue` / `recommendedNextStep`\n * because each disambiguates a different reason for an empty\n * result — most notably, `windowActivity` carries the four-way\n * distinguisher between \"wrong vocabulary\", \"no traffic in window\",\n * \"captureConfig-blocked\", and \"no traces ever for this tenant\"\n * (per `wire-mcp.ts` `NoMatchWindowActivitySchema` /\n * DISC-1652 Amendment 1 / DISC-1654). Without `windowActivity`\n * the agent cannot distinguish a vocabulary miss from \"the SDK was\n * never registered for this tenant\" — they look identical at the\n * `closeMatches`-only layer.\n *\n * **Heuristic-first vs tool-first framing:** the body opens with\n * explicit \"Call Glasstrace FIRST when\" / \"SKIP Glasstrace when\"\n * rules so a frontier agent has a cheap pre-tool-call decision\n * heuristic it can apply BEFORE spending tokens on tool\n * consideration. The Workflow section then names\n * `find_trace_candidates` as the entry point and instructs the\n * agent to READ `closeMatches` / `recentRoutesSample` /\n * `recoveryActions` before pivoting to source — that is the\n * load-bearing recovery contract from MCP-025 / MCP-027 (codified\n * in `wire-mcp.ts` `ToolDiagnosticSchema` and `CandidateDiagnosticSchema`)\n * and is the failure mode the prior SDK-050 cost-aware decision\n * paragraph did not surface.\n */\n\n/**\n * The full agent-instruction body, returned as a markdown string.\n *\n * The body has NO endpoint URL inlined — frontier agents reach\n * Glasstrace via the MCP server name `glasstrace` configured in the\n * user's MCP config (`.glasstrace/mcp.json` or per-agent native\n * config), not by reading a URL out of the instruction file. Keeping\n * the URL out of the instruction text avoids drift between the\n * instruction file and the MCP config and keeps the body tight.\n *\n * Returns the body WITHOUT the surrounding markers — the caller\n * (`generateInfoSection` in `configs.ts`) wraps the body with the\n * version-stamped start/end markers per the SDK-050 / DISC-1592\n * marker contract.\n */\nexport function buildAgentInstructionBody(): string {\n return [\n \"\",\n \"## Glasstrace MCP — Runtime Debugging Evidence\",\n \"\",\n \"Glasstrace is connected via MCP (`glasstrace`). It captures runtime traces from this app and exposes them through tools that disambiguate bugs source alone cannot resolve.\",\n \"\",\n \"### Call Glasstrace FIRST when:\",\n \"- A value passed at runtime is suspect: wrong role, locale, timezone, template, tenant id, or any enum/config that branches downstream.\",\n \"- The bug is in a side-effect operation: email sent, calendar event created, webhook dispatched, queued job, `after()` callback.\",\n \"- The user reports a symptom but you do not yet know which code path actually executed.\",\n \"- You need to confirm a route or procedure ran in the relevant time window before doing a source dive.\",\n \"\",\n \"### SKIP Glasstrace when:\",\n \"- The bug is statically obvious from source (N+1 query, missing await, redundant query, type error, missing null check).\",\n \"- The change is a refactor whose correctness does not depend on runtime behavior.\",\n \"- You already have a precise traceId from another source.\",\n \"\",\n \"### Workflow\",\n \"1. Start with `find_trace_candidates`. Pass whatever route or procedure name is natural — the server normalizes vocabulary and, on miss, returns close matches and a sample of routes actually present in the window.\",\n \"2. Take the highest-confidence candidate's `suggestedFollowups` and pass them straight to `get_trace` or `get_root_cause`.\",\n \"3. For side-effect bugs, read `sideEffectSummary` in the `get_trace` / `get_root_cause` response. The allowlisted fields (`templateKey`, `providerOperation`, `role`, `locale`, `timezone`, `status`, `phase`) are the ones that disambiguate payload bugs.\",\n \"4. If a tool returns empty, READ the response's empty-result envelope before pivoting to source — each field disambiguates a different reason for the empty result:\",\n \" - `closeMatches` / `recentRoutesSample` — your filter vocabulary doesn't match server-side names; the server returns the closest known names + a sample of routes actually present.\",\n \" - `windowActivity` — load-bearing four-way distinguisher. `totalTracesInWindow === 0` AND `totalTracesInTenantEver > 0` means \\\"your time window missed the activity\\\"; `totalTracesInTenantEver === 0` means \\\"this tenant has never produced traces\\\" (SDK not registered, or never hit); `captureConfigBlocksRequest === true` means \\\"the SDK's capture config dropped this route\\\"; otherwise the empty result is a vocabulary miss — see `closeMatches`.\",\n \" - `humanReadable` — prose guidance written for the agent.\",\n \" - `recoveryActions` — concrete next-call shapes.\",\n \" - `diagnosticValue` / `recommendedNextStep` — whether to keep searching or stop.\",\n \" Empty results carry `notAbsenceProof: true` — they are never proof the bug did not occur.\",\n \"\",\n \"### Tools\",\n \"- `find_trace_candidates` — discovery, vocabulary-tolerant filter\",\n \"- `get_trace` — exact trace by `traceId`\",\n \"- `get_root_cause` — root-cause analysis for a `traceId`\",\n \"- `get_session_timeline` — events for a session\",\n \"- `get_latest_error` / `get_error_list` — recent server errors\",\n \"\",\n \"Side-effect evidence is allowlisted and compact by design. Fields you don't see may have been omitted by policy, not absent at runtime.\",\n \"\",\n ].join(\"\\n\");\n}\n","import type { DetectedAgent } from \"./detect.js\";\nimport { buildAgentInstructionBody } from \"./agent-instruction-text.js\";\n\n/**\n * Generates the MCP server configuration content for a given agent.\n *\n * The output is the full file content suitable for writing to the agent's\n * MCP config file. The bearer token is intentionally embedded here for\n * agents whose schemas inline the Authorization header — Codex is the\n * exception and uses `bearer_token_env_var` so the actual token never\n * appears in TOML.\n *\n * @param agent - The detected agent to generate config for.\n * @param endpoint - The Glasstrace MCP endpoint URL.\n * @param bearer - The credential to embed in the Authorization header\n * (anon key or dev key, depending on the project's resolved\n * credential source). Empty values throw.\n * @returns The formatted configuration string.\n * @throws If endpoint or bearer is empty.\n */\nexport function generateMcpConfig(\n agent: DetectedAgent,\n endpoint: string,\n bearer: string,\n): string {\n if (!endpoint || endpoint.trim() === \"\") {\n throw new Error(\"endpoint must not be empty\");\n }\n if (!bearer || bearer.trim() === \"\") {\n throw new Error(\"bearer must not be empty\");\n }\n\n switch (agent.name) {\n case \"claude\":\n return JSON.stringify(\n {\n mcpServers: {\n glasstrace: {\n type: \"http\",\n url: endpoint,\n headers: {\n Authorization: `Bearer ${bearer}`,\n },\n },\n },\n },\n null,\n 2,\n );\n\n case \"codex\": {\n // Escape TOML basic string special characters in the endpoint value.\n // TOML requires backslashes, quotes, and control characters to be escaped.\n const safeEndpoint = endpoint\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\"/g, '\\\\\"')\n .replace(/\\n/g, \"\\\\n\")\n .replace(/\\r/g, \"\\\\r\")\n .replace(/\\t/g, \"\\\\t\");\n return [\n \"[mcp_servers.glasstrace]\",\n `url = \"${safeEndpoint}\"`,\n `bearer_token_env_var = \"GLASSTRACE_API_KEY\"`,\n \"\",\n ].join(\"\\n\");\n }\n\n case \"gemini\":\n return JSON.stringify(\n {\n mcpServers: {\n glasstrace: {\n httpUrl: endpoint,\n headers: {\n Authorization: `Bearer ${bearer}`,\n },\n },\n },\n },\n null,\n 2,\n );\n\n case \"cursor\":\n return JSON.stringify(\n {\n mcpServers: {\n glasstrace: {\n type: \"http\",\n url: endpoint,\n headers: {\n Authorization: `Bearer ${bearer}`,\n },\n },\n },\n },\n null,\n 2,\n );\n\n case \"windsurf\":\n return JSON.stringify(\n {\n mcpServers: {\n glasstrace: {\n type: \"http\",\n url: endpoint,\n headers: {\n Authorization: `Bearer ${bearer}`,\n },\n },\n },\n },\n null,\n 2,\n );\n\n case \"generic\":\n return JSON.stringify(\n {\n mcpServers: {\n glasstrace: {\n type: \"http\",\n url: endpoint,\n headers: {\n Authorization: `Bearer ${bearer}`,\n },\n },\n },\n },\n null,\n 2,\n );\n\n default: {\n const _exhaustive: never = agent.name;\n throw new Error(`Unknown agent: ${_exhaustive}`);\n }\n }\n}\n\n/**\n * Strict pattern accepted as the value substituted into a `v=<sdkVersion>`\n * marker stamp. Covers the SDK's own published versions\n * (e.g. `1.4.0`, `0.0.0-canary-20260508120000`, `1.4.0+build.42`) without\n * admitting whitespace, angle-brackets, or terminal control sequences\n * that could be smuggled into the agent instruction file via a\n * malformed callsite.\n *\n * The stamp is the SDK semver string and nothing else (DISC-1592 / SDK-050\n * Required Semantics Item 1: \"the stamp encodes only the SDK semver\n * string ... it must not embed user-controlled or environment-derived\n * content\"). Reject anything outside this charset at the render site\n * rather than relying on the upstream `__SDK_VERSION__` define being\n * well-formed.\n */\nconst SDK_VERSION_STAMP_PATTERN = /^[A-Za-z0-9.+-]+$/;\n\n/**\n * Marker pair used to delimit the Glasstrace section in agent info files.\n */\ninterface MarkerPair {\n start: string;\n end: string;\n}\n\nfunction htmlMarkers(sdkVersion: string): MarkerPair {\n return {\n start: `<!-- glasstrace:mcp:start v=${sdkVersion} -->`,\n end: \"<!-- glasstrace:mcp:end -->\",\n };\n}\n\nfunction hashMarkers(sdkVersion: string): MarkerPair {\n return {\n start: `# glasstrace:mcp:start v=${sdkVersion}`,\n end: \"# glasstrace:mcp:end\",\n };\n}\n\n/**\n * Generates informational content for an agent's instruction file.\n *\n * This content is designed to be appended to or inserted into agent-specific\n * instruction files. Wave 18 expanded the canonical set to follow the\n * 2026 cross-tool standard governed by the Agentic AI Foundation under\n * the Linux Foundation: `AGENTS.md` is the universal write target,\n * with per-agent canonical files written alongside it where the agent\n * has a documented primary file (Claude Code → CLAUDE.md, Gemini CLI\n * → GEMINI.md, Cursor → `.cursor/rules/glasstrace.mdc` canonical +\n * `.cursorrules` transitional fallback). Codex / Windsurf / generic\n * resolve to AGENTS.md alone (Codex retired `codex.md`; Windsurf\n * supports both AGENTS.md and `.windsurf/rules/glasstrace.md`). The\n * managed section's content is identical across destinations — only\n * the marker shape differs (HTML comments for Markdown / `.md` /\n * `.mdc` targets; `.cursorrules` legacy uses hash-prefix markers\n * preserved from the SDK-050 contract for backward-compat with\n * already-rendered managed sections). It contains a\n * tight, agent-facing decision policy + workflow + tool list — no\n * endpoint URL, no auth tokens, no setup instructions (those live in\n * the user's MCP config and the SDK README; the agent reads this file\n * to decide WHEN to call Glasstrace MCP and HOW to use the returned\n * evidence).\n *\n * The rendered block opens with explicit \"Call Glasstrace FIRST when\"\n * / \"SKIP Glasstrace when\" decision rules so a frontier agent has a\n * cheap pre-tool-call heuristic it can apply BEFORE spending tokens\n * on tool consideration. The Workflow section names\n * `find_trace_candidates` as the discovery entry point and instructs\n * the agent to READ `closeMatches` / `recentRoutesSample` /\n * `recoveryActions` before pivoting to source — that is the\n * load-bearing recovery contract from MCP-025 / MCP-027 (codified\n * in the server-side MCP contract maintained in the private\n * `glasstrace-product` repo's `shared/types/wire-mcp.ts` —\n * `ToolDiagnosticSchema` / `CandidateDiagnosticSchema`) and it\n * prevents the bail-to-source failure mode that the prior SDK-050\n * cost-aware decision paragraph did not surface.\n *\n * The body itself lives in a sibling module\n * (`agent-instruction-text.ts`) so future content evolutions are a\n * single-file edit and don't disturb the marker / version-stamp /\n * per-agent-format machinery in this file.\n *\n * The start marker carries a `v=<sdkVersion>` stamp (DISC-1592 /\n * SDK-050) so a later `glasstrace upgrade-instructions` run — and\n * the SDK's stale-section warning at init — can detect that the\n * file was rendered by an older SDK and refresh the block.\n *\n * @param agent - The detected agent to generate info for.\n * @param endpoint - The Glasstrace MCP endpoint URL. (Validated for\n * non-emptiness here for backwards compatibility with the prior\n * SDK-050 contract; not currently inlined in the body — agents\n * reach Glasstrace via the MCP server name `glasstrace` configured\n * separately in `.glasstrace/mcp.json` or per-agent native config.)\n * @param sdkVersion - The SDK semver string to embed in the start marker\n * (e.g. `1.4.0`, `0.0.0-canary-20260508120000`). Must match\n * `[A-Za-z0-9.+\\-]+`; arbitrary or empty values throw.\n * @returns The formatted info section string, or empty string for agents without a supported info file format.\n * @throws If endpoint is empty, or if sdkVersion is empty or contains\n * characters outside the accepted stamp charset.\n */\nexport function generateInfoSection(\n agent: DetectedAgent,\n endpoint: string,\n sdkVersion: string,\n): string {\n if (!endpoint || endpoint.trim() === \"\") {\n throw new Error(\"endpoint must not be empty\");\n }\n if (!sdkVersion || sdkVersion.trim() === \"\") {\n throw new Error(\"sdkVersion must not be empty\");\n }\n if (!SDK_VERSION_STAMP_PATTERN.test(sdkVersion)) {\n throw new Error(\n \"sdkVersion must match [A-Za-z0-9.+\\\\-]+ (semver-shaped, no whitespace, no angle brackets)\",\n );\n }\n\n const content = buildAgentInstructionBody();\n\n switch (agent.name) {\n case \"claude\":\n case \"codex\":\n case \"gemini\":\n case \"windsurf\":\n case \"generic\": {\n // All Markdown-family targets (CLAUDE.md, AGENTS.md, GEMINI.md,\n // .windsurf/rules/glasstrace.md) use the HTML comment marker\n // shape that has soaked in production via SDK-050 / DISC-1592 /\n // DISC-1602 since Wave 17 main / `@glasstrace/sdk@1.10.x`.\n const m = htmlMarkers(sdkVersion);\n return `${m.start}\\n${content}${m.end}\\n`;\n }\n\n case \"cursor\": {\n // Wave 18 routes Cursor to the canonical `.cursor/rules/\n // glasstrace.mdc` destination. `.mdc` is Markdown body + YAML\n // frontmatter delimited by `---` lines; the SDK's marker\n // contract carries through unchanged via HTML comments. The\n // legacy `.cursorrules` write (still produced by the\n // multi-target write helper as a transitional fallback) is\n // rendered via {@link generateInfoSectionForCursorrulesLegacy}\n // using hash-prefix markers preserved from SDK-050 for\n // backward-compat with already-rendered managed sections.\n const m = htmlMarkers(sdkVersion);\n return `${m.start}\\n${content}${m.end}\\n`;\n }\n\n default: {\n const _exhaustive: never = agent.name;\n throw new Error(`Unknown agent: ${_exhaustive}`);\n }\n }\n}\n\n/**\n * Renders the managed section for Cursor's legacy `.cursorrules` file\n * (transitional fallback companion to the canonical\n * `.cursor/rules/glasstrace.mdc` write).\n *\n * Uses hash-prefix markers preserved from the SDK-050 contract — pre-\n * Wave-18 SDK versions rendered `.cursorrules` with hash markers, so\n * the legacy file's already-rendered managed sections need to be\n * recognized and idempotently replaced by the new SDK. Switching to\n * HTML markers on `.cursorrules` would break in-place replacement for\n * existing users (the new SDK wouldn't find the old marker pair) and\n * append a duplicate section.\n *\n * Wave 18 writes `.cursorrules` UNCONDITIONALLY alongside the\n * `.cursor/rules/glasstrace.mdc` canonical (per Codex P2 review of\n * DISC-1782 v3 — mixed-version Cursor scenarios may have Agent mode\n * reading legacy rules inconsistently across versions, so a\n * conditional fallback is too narrow).\n */\nexport function generateInfoSectionForCursorrulesLegacy(\n endpoint: string,\n sdkVersion: string,\n): string {\n if (!endpoint || endpoint.trim() === \"\") {\n throw new Error(\"endpoint must not be empty\");\n }\n if (!sdkVersion || sdkVersion.trim() === \"\") {\n throw new Error(\"sdkVersion must not be empty\");\n }\n if (!SDK_VERSION_STAMP_PATTERN.test(sdkVersion)) {\n throw new Error(\n \"sdkVersion must match [A-Za-z0-9.+\\\\-]+ (semver-shaped, no whitespace, no angle brackets)\",\n );\n }\n\n const content = buildAgentInstructionBody();\n const m = hashMarkers(sdkVersion);\n return `${m.start}\\n${content}${m.end}\\n`;\n}\n\n/**\n * Renders the managed section for Cursor's `.cursor/rules/\n * glasstrace.mdc` canonical destination.\n *\n * `.mdc` is Cursor's Markdown-extension format with YAML frontmatter\n * delimited by `---` lines (per cursor.com/docs/rules — frontmatter\n * supports `alwaysApply`, `globs`, `description`). The Glasstrace\n * managed section uses `alwaysApply: true` because it's a global\n * agent instruction the user's coding agent should consult on every\n * debugging task. The SDK's idempotent-replacement logic anchors on\n * the markers and does NOT touch the frontmatter — user\n * customizations to the frontmatter survive `upgrade-instructions`.\n *\n * Recon caveat (Wave 18 impl-time, 2026-05-10): Cursor's official\n * docs do not address whether `.mdc` parser preserves HTML comments.\n * This implementation defaults to HTML comment markers (consistent\n * with the SDK-050 contract for `CLAUDE.md` and other Markdown\n * targets which have soaked in production). If a Cursor version\n * strips HTML comments from `.mdc` body content, the marker contract\n * breaks; track via the wave's closeout-gate items.\n */\nexport function generateInfoSectionForCursorMdc(\n endpoint: string,\n sdkVersion: string,\n): string {\n if (!endpoint || endpoint.trim() === \"\") {\n throw new Error(\"endpoint must not be empty\");\n }\n if (!sdkVersion || sdkVersion.trim() === \"\") {\n throw new Error(\"sdkVersion must not be empty\");\n }\n if (!SDK_VERSION_STAMP_PATTERN.test(sdkVersion)) {\n throw new Error(\n \"sdkVersion must match [A-Za-z0-9.+\\\\-]+ (semver-shaped, no whitespace, no angle brackets)\",\n );\n }\n\n const content = buildAgentInstructionBody();\n const m = htmlMarkers(sdkVersion);\n // YAML frontmatter goes ABOVE the managed section. The marker\n // contract anchors on the `<!-- glasstrace:mcp:start -->` ...\n // `<!-- glasstrace:mcp:end -->` markers; the frontmatter sits above\n // the section and is preserved across re-renders.\n return [\n \"---\",\n \"description: Glasstrace MCP runtime debugging tools — runtime evidence the agent reads when source alone cannot resolve a bug\",\n \"alwaysApply: true\",\n \"---\",\n \"\",\n `${m.start}\\n${content}${m.end}\\n`,\n ].join(\"\\n\");\n}\n"],"mappings":"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","names":["import_node_path","resolve","import_promises","import_promises","import_node_path"]}
|
|
@@ -1,20 +1,51 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
detectAgents,
|
|
4
|
-
|
|
5
|
-
} from "../chunk-
|
|
4
|
+
injectAllTargets
|
|
5
|
+
} from "../chunk-KOYZJN6G.js";
|
|
6
6
|
import {
|
|
7
7
|
MCP_ENDPOINT
|
|
8
8
|
} from "../chunk-RL43PU2X.js";
|
|
9
9
|
import "../chunk-6RKS3DNA.js";
|
|
10
10
|
import {
|
|
11
|
-
hasManagedSection
|
|
12
|
-
|
|
13
|
-
} from "../chunk-ZBQQXVHD.js";
|
|
11
|
+
hasManagedSection
|
|
12
|
+
} from "../chunk-YIEXKQYP.js";
|
|
14
13
|
import "../chunk-NSBPE2FW.js";
|
|
15
14
|
|
|
16
15
|
// src/cli/upgrade-instructions.ts
|
|
17
|
-
import { isAbsolute, relative } from "node:path";
|
|
16
|
+
import { dirname, isAbsolute, join, relative } from "node:path";
|
|
17
|
+
function legacyDestinationsForAgent(agent) {
|
|
18
|
+
if (agent.infoFilePath === null) {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
switch (agent.name) {
|
|
22
|
+
case "codex":
|
|
23
|
+
return [join(dirname(agent.infoFilePath), "codex.md")];
|
|
24
|
+
case "cursor":
|
|
25
|
+
return [
|
|
26
|
+
join(dirname(dirname(dirname(agent.infoFilePath))), ".cursorrules")
|
|
27
|
+
];
|
|
28
|
+
case "windsurf":
|
|
29
|
+
return [
|
|
30
|
+
join(
|
|
31
|
+
dirname(dirname(dirname(agent.infoFilePath))),
|
|
32
|
+
".windsurfrules"
|
|
33
|
+
)
|
|
34
|
+
];
|
|
35
|
+
case "claude":
|
|
36
|
+
case "gemini":
|
|
37
|
+
case "generic":
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function anyHasManagedSection(paths) {
|
|
42
|
+
for (const p of paths) {
|
|
43
|
+
if (await hasManagedSection(p)) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
18
49
|
function formatPathForOutput(filePath, projectRoot) {
|
|
19
50
|
const rel = relative(projectRoot, filePath);
|
|
20
51
|
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
|
|
@@ -36,7 +67,8 @@ async function runUpgradeInstructions(options) {
|
|
|
36
67
|
);
|
|
37
68
|
return { exitCode: 1, refreshed, skipped, warnings, errors };
|
|
38
69
|
}
|
|
39
|
-
const sdkVersion = true ? "1.
|
|
70
|
+
const sdkVersion = true ? "1.11.0" : "0.0.0-dev";
|
|
71
|
+
const optedInAgents = [];
|
|
40
72
|
for (const agent of agents) {
|
|
41
73
|
if (agent.infoFilePath === null) {
|
|
42
74
|
continue;
|
|
@@ -45,34 +77,54 @@ async function runUpgradeInstructions(options) {
|
|
|
45
77
|
agent.infoFilePath,
|
|
46
78
|
options.projectRoot
|
|
47
79
|
);
|
|
48
|
-
|
|
80
|
+
const legacyDestinations = legacyDestinationsForAgent(agent);
|
|
81
|
+
let optedIn;
|
|
49
82
|
try {
|
|
50
|
-
|
|
83
|
+
optedIn = await anyHasManagedSection([
|
|
84
|
+
agent.infoFilePath,
|
|
85
|
+
...legacyDestinations
|
|
86
|
+
]);
|
|
51
87
|
} catch (err) {
|
|
52
88
|
warnings.push(
|
|
53
89
|
`Could not inspect ${displayPath}: ${err instanceof Error ? err.message : String(err)}`
|
|
54
90
|
);
|
|
55
91
|
continue;
|
|
56
92
|
}
|
|
57
|
-
if (!
|
|
93
|
+
if (!optedIn) {
|
|
58
94
|
skipped.push(displayPath);
|
|
59
95
|
continue;
|
|
60
96
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
97
|
+
optedInAgents.push(agent);
|
|
98
|
+
}
|
|
99
|
+
if (optedInAgents.length > 0) {
|
|
65
100
|
try {
|
|
66
|
-
await
|
|
67
|
-
|
|
101
|
+
await injectAllTargets(
|
|
102
|
+
optedInAgents,
|
|
103
|
+
MCP_ENDPOINT,
|
|
104
|
+
sdkVersion,
|
|
105
|
+
options.projectRoot
|
|
106
|
+
);
|
|
107
|
+
for (const a of optedInAgents) {
|
|
108
|
+
if (a.infoFilePath !== null) {
|
|
109
|
+
refreshed.push(formatPathForOutput(a.infoFilePath, options.projectRoot));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
68
112
|
} catch (err) {
|
|
69
113
|
errors.push(
|
|
70
|
-
`Failed to refresh
|
|
114
|
+
`Failed to refresh agent-instruction files: ${err instanceof Error ? err.message : String(err)}`
|
|
71
115
|
);
|
|
72
116
|
}
|
|
73
117
|
}
|
|
118
|
+
const refreshedSet = new Set(refreshed);
|
|
119
|
+
const dedupedSkipped = skipped.filter((p) => !refreshedSet.has(p));
|
|
74
120
|
const exitCode = errors.length === 0 ? 0 : 1;
|
|
75
|
-
return {
|
|
121
|
+
return {
|
|
122
|
+
exitCode,
|
|
123
|
+
refreshed,
|
|
124
|
+
skipped: dedupedSkipped,
|
|
125
|
+
warnings,
|
|
126
|
+
errors
|
|
127
|
+
};
|
|
76
128
|
}
|
|
77
129
|
export {
|
|
78
130
|
runUpgradeInstructions
|