@glasstrace/sdk 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +56 -0
  2. package/dist/{chunk-JZ475QRH.js → chunk-D3QXU2VM.js} +22 -191
  3. package/dist/chunk-D3QXU2VM.js.map +1 -0
  4. package/dist/{chunk-VQDYXXVS.js → chunk-N3XIVM2U.js} +154 -8
  5. package/dist/chunk-N3XIVM2U.js.map +1 -0
  6. package/dist/{chunk-VJQIFY33.js → chunk-YLY7AGLC.js} +7 -4
  7. package/dist/chunk-YLY7AGLC.js.map +1 -0
  8. package/dist/chunk-ZBQQXVHD.js +208 -0
  9. package/dist/chunk-ZBQQXVHD.js.map +1 -0
  10. package/dist/cli/init.cjs +206 -34
  11. package/dist/cli/init.cjs.map +1 -1
  12. package/dist/cli/init.js +65 -8
  13. package/dist/cli/init.js.map +1 -1
  14. package/dist/cli/mcp-add.cjs +45 -25
  15. package/dist/cli/mcp-add.cjs.map +1 -1
  16. package/dist/cli/mcp-add.js +10 -7
  17. package/dist/cli/mcp-add.js.map +1 -1
  18. package/dist/cli/status.cjs +33 -3
  19. package/dist/cli/status.cjs.map +1 -1
  20. package/dist/cli/status.js +12 -3
  21. package/dist/cli/status.js.map +1 -1
  22. package/dist/cli/uninit.cjs +27 -3
  23. package/dist/cli/uninit.cjs.map +1 -1
  24. package/dist/cli/uninit.d.cts +10 -2
  25. package/dist/cli/uninit.d.ts +10 -2
  26. package/dist/cli/uninit.js +2 -1
  27. package/dist/cli/upgrade-instructions.cjs +440 -0
  28. package/dist/cli/upgrade-instructions.cjs.map +1 -0
  29. package/dist/cli/upgrade-instructions.d.cts +48 -0
  30. package/dist/cli/upgrade-instructions.d.ts +48 -0
  31. package/dist/cli/upgrade-instructions.js +80 -0
  32. package/dist/cli/upgrade-instructions.js.map +1 -0
  33. package/dist/index.cjs +229 -60
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.js +2 -1
  36. package/dist/index.js.map +1 -1
  37. package/dist/node-entry.cjs +237 -68
  38. package/dist/node-entry.cjs.map +1 -1
  39. package/dist/node-entry.js +2 -1
  40. package/package.json +1 -1
  41. package/dist/chunk-JZ475QRH.js.map +0 -1
  42. package/dist/chunk-VJQIFY33.js.map +0 -1
  43. package/dist/chunk-VQDYXXVS.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/cli/upgrade-instructions.ts","../../src/mcp-runtime.ts","../../src/agent-detection/detect.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","import type { DetectedAgent } from \"./detect.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 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 serverUrl: 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 ONLY\n * the endpoint URL, tool descriptions, and setup instructions. Auth tokens\n * are NEVER included in this output.\n *\n * The rendered block opens with a cost-aware cross-tool decision paragraph\n * (DISC-1593 / SDK-050) telling the user's AI agent **when** Glasstrace\n * MCP is worth calling at all and **which** tool is the cheapest first\n * call for each symptom class. The start marker carries a `v=<sdkVersion>`\n * stamp (DISC-1592 / SDK-050) so a later `glasstrace upgrade-instructions`\n * run — and the SDK's stale-section warning at init — can detect that\n * the 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.\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 // Cost-aware cross-tool decision paragraph (DISC-1593 / SDK-050\n // Required Semantics §1). Load-bearing semantics:\n // 1. Frame Glasstrace MCP as conditionally worth calling.\n // 2. Name cheapest-orientation routing per symptom class.\n // 3. Restate the no-candidates / no_traces_found \"scoped retrieval\n // result, not absence of the bug\" contract.\n // 4. List the conditions that justify calling Glasstrace MCP at all.\n // Wording aligned with MCP-025's planned `recoveryActions` so the two\n // surfaces do not contradict each other.\n const content = [\n \"\",\n \"## Glasstrace MCP Integration\",\n \"\",\n `Glasstrace is configured as an MCP server at: ${endpoint}`,\n \"\",\n \"Glasstrace MCP is available when runtime evidence would materially reduce uncertainty. Use it when there is a failing request, stack trace, unclear runtime behavior, race/data-flow symptom, side effect, or performance issue that source inspection alone does not explain. For a current error, `get_latest_error` or `get_error_list` is usually the cheapest orientation call. For a known route/procedure with no exact error, use `find_trace_candidates` and follow returned exact `get_trace` or `get_root_cause` arguments only if the candidates look relevant. Do not call trace tools for trivial source-local fixes. Treat **no candidates** or **no_traces_found** as a scoped retrieval result, not proof the bug is absent.\",\n \"\",\n \"Available tools:\",\n \"- `get_latest_error` - Get the most recent error trace from the current session\",\n \"- `find_trace_candidates` - First-contact route/procedure/URL candidate selection when you have a route fragment, tRPC procedure, method, status, or rough recent activity window but not the exact trace ID. Returns candidate traces plus suggested `get_trace` / `get_root_cause` follow-up call arguments. Candidate discovery, not root-cause proof.\",\n \"- `get_error_list` - List recent errors with filtering and pagination\",\n \"- `get_trace` - Get a specific trace by ID or URL pattern\",\n \"- `get_root_cause` - Get the root cause analysis for a specific error trace (requires a `traceId` from `get_latest_error`, `get_error_list`, or `get_trace`)\",\n \"- `get_test_suggestions` - Get test suggestions based on recent errors\",\n \"- `get_session_timeline` - Get the timeline of all traces in the current session\",\n \"\",\n \"To refresh this managed section after a `@glasstrace/sdk` upgrade, run: `npx glasstrace upgrade-instructions`. To reconfigure MCP credentials, run: `npx glasstrace mcp add`.\",\n \"\",\n ].join(\"\\n\");\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"]}
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Options for {@link runUpgradeInstructions}. The CLI entry point in
4
+ * `init.ts` wires `process.cwd()` through `resolveProjectRoot()` so
5
+ * monorepo roots resolve to the active app directory; tests pass an
6
+ * explicit `projectRoot` for isolation.
7
+ */
8
+ interface UpgradeInstructionsOptions {
9
+ projectRoot: string;
10
+ }
11
+ /**
12
+ * Result of running the upgrade-instructions command. Returned to the
13
+ * CLI entry point so it can render output without forcing the core
14
+ * logic to call `process.stderr.write` / `process.exit`.
15
+ */
16
+ interface UpgradeInstructionsResult {
17
+ exitCode: number;
18
+ /**
19
+ * Files whose managed Glasstrace section was refreshed in place.
20
+ * Reported as paths relative to {@link UpgradeInstructionsOptions.projectRoot}
21
+ * so the CLI output stays portable across machines and developer
22
+ * homes; an absolute path is returned only when the detected file
23
+ * lives outside the resolved project root (e.g. Windsurf's global
24
+ * config under `$HOME/.codeium/`), where a relative form would be
25
+ * misleading.
26
+ */
27
+ refreshed: string[];
28
+ /**
29
+ * Files inspected that did not contain a managed section, and were
30
+ * therefore left untouched. Reported so the user can verify the
31
+ * command did not accidentally append a block to a hand-written
32
+ * instruction file. Same path-shape rule as
33
+ * {@link UpgradeInstructionsResult.refreshed}.
34
+ */
35
+ skipped: string[];
36
+ /**
37
+ * Soft warnings (e.g. permission errors handled internally by
38
+ * `injectInfoSection`). One line per issue.
39
+ */
40
+ warnings: string[];
41
+ /**
42
+ * Hard errors that prevented the command from completing.
43
+ */
44
+ errors: string[];
45
+ }
46
+ declare function runUpgradeInstructions(options: UpgradeInstructionsOptions): Promise<UpgradeInstructionsResult>;
47
+
48
+ export { type UpgradeInstructionsOptions, type UpgradeInstructionsResult, runUpgradeInstructions };
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Options for {@link runUpgradeInstructions}. The CLI entry point in
4
+ * `init.ts` wires `process.cwd()` through `resolveProjectRoot()` so
5
+ * monorepo roots resolve to the active app directory; tests pass an
6
+ * explicit `projectRoot` for isolation.
7
+ */
8
+ interface UpgradeInstructionsOptions {
9
+ projectRoot: string;
10
+ }
11
+ /**
12
+ * Result of running the upgrade-instructions command. Returned to the
13
+ * CLI entry point so it can render output without forcing the core
14
+ * logic to call `process.stderr.write` / `process.exit`.
15
+ */
16
+ interface UpgradeInstructionsResult {
17
+ exitCode: number;
18
+ /**
19
+ * Files whose managed Glasstrace section was refreshed in place.
20
+ * Reported as paths relative to {@link UpgradeInstructionsOptions.projectRoot}
21
+ * so the CLI output stays portable across machines and developer
22
+ * homes; an absolute path is returned only when the detected file
23
+ * lives outside the resolved project root (e.g. Windsurf's global
24
+ * config under `$HOME/.codeium/`), where a relative form would be
25
+ * misleading.
26
+ */
27
+ refreshed: string[];
28
+ /**
29
+ * Files inspected that did not contain a managed section, and were
30
+ * therefore left untouched. Reported so the user can verify the
31
+ * command did not accidentally append a block to a hand-written
32
+ * instruction file. Same path-shape rule as
33
+ * {@link UpgradeInstructionsResult.refreshed}.
34
+ */
35
+ skipped: string[];
36
+ /**
37
+ * Soft warnings (e.g. permission errors handled internally by
38
+ * `injectInfoSection`). One line per issue.
39
+ */
40
+ warnings: string[];
41
+ /**
42
+ * Hard errors that prevented the command from completing.
43
+ */
44
+ errors: string[];
45
+ }
46
+ declare function runUpgradeInstructions(options: UpgradeInstructionsOptions): Promise<UpgradeInstructionsResult>;
47
+
48
+ export { type UpgradeInstructionsOptions, type UpgradeInstructionsResult, runUpgradeInstructions };
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ detectAgents,
4
+ generateInfoSection
5
+ } from "../chunk-D3QXU2VM.js";
6
+ import {
7
+ MCP_ENDPOINT
8
+ } from "../chunk-MFYOQOD7.js";
9
+ import "../chunk-4WI7B5FQ.js";
10
+ import {
11
+ hasManagedSection,
12
+ injectInfoSection
13
+ } from "../chunk-ZBQQXVHD.js";
14
+ import "../chunk-NSBPE2FW.js";
15
+
16
+ // src/cli/upgrade-instructions.ts
17
+ import { isAbsolute, relative } from "node:path";
18
+ function formatPathForOutput(filePath, projectRoot) {
19
+ const rel = relative(projectRoot, filePath);
20
+ if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
21
+ return filePath;
22
+ }
23
+ return rel;
24
+ }
25
+ async function runUpgradeInstructions(options) {
26
+ const refreshed = [];
27
+ const skipped = [];
28
+ const warnings = [];
29
+ const errors = [];
30
+ let agents;
31
+ try {
32
+ agents = await detectAgents(options.projectRoot);
33
+ } catch (err) {
34
+ errors.push(
35
+ `Failed to detect agents: ${err instanceof Error ? err.message : String(err)}`
36
+ );
37
+ return { exitCode: 1, refreshed, skipped, warnings, errors };
38
+ }
39
+ const sdkVersion = true ? "1.5.0" : "0.0.0-dev";
40
+ for (const agent of agents) {
41
+ if (agent.infoFilePath === null) {
42
+ continue;
43
+ }
44
+ const displayPath = formatPathForOutput(
45
+ agent.infoFilePath,
46
+ options.projectRoot
47
+ );
48
+ let containsSection;
49
+ try {
50
+ containsSection = await hasManagedSection(agent.infoFilePath);
51
+ } catch (err) {
52
+ warnings.push(
53
+ `Could not inspect ${displayPath}: ${err instanceof Error ? err.message : String(err)}`
54
+ );
55
+ continue;
56
+ }
57
+ if (!containsSection) {
58
+ skipped.push(displayPath);
59
+ continue;
60
+ }
61
+ const content = generateInfoSection(agent, MCP_ENDPOINT, sdkVersion);
62
+ if (content === "") {
63
+ continue;
64
+ }
65
+ try {
66
+ await injectInfoSection(agent, content, options.projectRoot);
67
+ refreshed.push(displayPath);
68
+ } catch (err) {
69
+ errors.push(
70
+ `Failed to refresh ${displayPath}: ${err instanceof Error ? err.message : String(err)}`
71
+ );
72
+ }
73
+ }
74
+ const exitCode = errors.length === 0 ? 0 : 1;
75
+ return { exitCode, refreshed, skipped, warnings, errors };
76
+ }
77
+ export {
78
+ runUpgradeInstructions
79
+ };
80
+ //# sourceMappingURL=upgrade-instructions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/cli/upgrade-instructions.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"],"mappings":";;;;;;;;;;;;;;;;AACA,SAAS,YAAY,gBAAgB;AAyFrC,SAAS,oBAAoB,UAAkB,aAA6B;AAC1E,QAAM,MAAM,SAAS,aAAa,QAAQ;AAC1C,MAAI,QAAQ,MAAM,IAAI,WAAW,IAAI,KAAK,WAAW,GAAG,GAAG;AACzD,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,eAAsB,uBACpB,SACoC;AACpC,QAAM,YAAsB,CAAC;AAC7B,QAAM,UAAoB,CAAC;AAC3B,QAAM,WAAqB,CAAC;AAC5B,QAAM,SAAmB,CAAC;AAE1B,MAAI;AACJ,MAAI;AACF,aAAS,MAAM,aAAa,QAAQ,WAAW;AAAA,EACjD,SAAS,KAAK;AACZ,WAAO;AAAA,MACL,4BAA4B,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,IAC9E;AACA,WAAO,EAAE,UAAU,GAAG,WAAW,SAAS,UAAU,OAAO;AAAA,EAC7D;AAEA,QAAM,aACJ,OAAsC,UAAkB;AAE1D,aAAW,SAAS,QAAQ;AAC1B,QAAI,MAAM,iBAAiB,MAAM;AAG/B;AAAA,IACF;AAEA,UAAM,cAAc;AAAA,MAClB,MAAM;AAAA,MACN,QAAQ;AAAA,IACV;AAEA,QAAI;AACJ,QAAI;AACF,wBAAkB,MAAM,kBAAkB,MAAM,YAAY;AAAA,IAC9D,SAAS,KAAK;AAGZ,eAAS;AAAA,QACP,qBAAqB,WAAW,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,MACvF;AACA;AAAA,IACF;AAEA,QAAI,CAAC,iBAAiB;AAKpB,cAAQ,KAAK,WAAW;AACxB;AAAA,IACF;AAEA,UAAM,UAAU,oBAAoB,OAAO,cAAc,UAAU;AACnE,QAAI,YAAY,IAAI;AAIlB;AAAA,IACF;AAEA,QAAI;AACF,YAAM,kBAAkB,OAAO,SAAS,QAAQ,WAAW;AAC3D,gBAAU,KAAK,WAAW;AAAA,IAC5B,SAAS,KAAK;AACZ,aAAO;AAAA,QACL,qBAAqB,WAAW,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,MACvF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,WAAW,OAAO,WAAW,IAAI,IAAI;AAC3C,SAAO,EAAE,UAAU,WAAW,SAAS,UAAU,OAAO;AAC1D;","names":[]}