@glasstrace/sdk 0.15.1 → 0.16.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 +84 -1
- package/dist/adapters/drizzle.js +2 -5
- package/dist/adapters/drizzle.js.map +1 -1
- package/dist/{chunk-PD2SKFQQ.js → chunk-55FBXXER.js} +4 -8
- package/dist/{chunk-PD2SKFQQ.js.map → chunk-55FBXXER.js.map} +1 -1
- package/dist/chunk-5C2TJFLB.js +851 -0
- package/dist/chunk-5C2TJFLB.js.map +1 -0
- package/dist/{chunk-YMEXDDTA.js → chunk-7JBKXSBU.js} +3 -99
- package/dist/chunk-7JBKXSBU.js.map +1 -0
- package/dist/{chunk-2LDBR3F3.js → chunk-BANTDXUT.js} +15 -74
- package/dist/chunk-BANTDXUT.js.map +1 -0
- package/dist/{chunk-ZNOD6FC7.js → chunk-CTJI2YKA.js} +8 -15
- package/dist/{chunk-ZNOD6FC7.js.map → chunk-CTJI2YKA.js.map} +1 -1
- package/dist/{chunk-WK7MPK2T.js → chunk-DQ25VOKK.js} +1 -89
- package/dist/chunk-DQ25VOKK.js.map +1 -0
- package/dist/{chunk-BL3YDC6V.js → chunk-DXRZKKSO.js} +1 -6
- package/dist/{chunk-BL3YDC6V.js.map → chunk-DXRZKKSO.js.map} +1 -1
- package/dist/{chunk-BGZ7J74D.js → chunk-NSBPE2FW.js} +2 -16
- package/dist/{chunk-A2AZL6MZ.js → chunk-O63DJKIJ.js} +169 -18
- package/dist/chunk-O63DJKIJ.js.map +1 -0
- package/dist/{chunk-ECEN724Y.js → chunk-TM5NKZTO.js} +4 -8
- package/dist/{chunk-ECEN724Y.js.map → chunk-TM5NKZTO.js.map} +1 -1
- package/dist/chunk-VUZCLMIX.js +57 -0
- package/dist/chunk-VUZCLMIX.js.map +1 -0
- package/dist/{chunk-OSXIUKD5.js → chunk-WZXVS2EO.js} +1 -6
- package/dist/{chunk-OSXIUKD5.js.map → chunk-WZXVS2EO.js.map} +1 -1
- package/dist/{chunk-ROFOJQWN.js → chunk-XNDHQN4S.js} +7 -11
- package/dist/{chunk-ROFOJQWN.js.map → chunk-XNDHQN4S.js.map} +1 -1
- package/dist/cli/init.cjs +673 -161
- package/dist/cli/init.cjs.map +1 -1
- package/dist/cli/init.d.cts +54 -1
- package/dist/cli/init.d.ts +54 -1
- package/dist/cli/init.js +146 -37
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/mcp-add.cjs +16 -16
- package/dist/cli/mcp-add.cjs.map +1 -1
- package/dist/cli/mcp-add.js +10 -13
- package/dist/cli/mcp-add.js.map +1 -1
- package/dist/cli/status.cjs +2 -2
- package/dist/cli/status.js +4 -7
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/uninit.cjs +56 -59
- package/dist/cli/uninit.cjs.map +1 -1
- package/dist/cli/uninit.js +4 -4
- package/dist/cli/validate.cjs +2 -2
- package/dist/cli/validate.js +3 -6
- package/dist/cli/validate.js.map +1 -1
- package/dist/{esm-MDK7CZID.js → esm-KBPHCVB4.js} +3 -3
- package/dist/{getMachineId-bsd-4NIRBWME.js → getMachineId-bsd-345PYXFX.js} +4 -7
- package/dist/{getMachineId-bsd-4NIRBWME.js.map → getMachineId-bsd-345PYXFX.js.map} +1 -1
- package/dist/{getMachineId-darwin-2XNOCCJQ.js → getMachineId-darwin-5L2D25AD.js} +4 -7
- package/dist/{getMachineId-darwin-2XNOCCJQ.js.map → getMachineId-darwin-5L2D25AD.js.map} +1 -1
- package/dist/{getMachineId-linux-V6YSQEY7.js → getMachineId-linux-KJR4P5HN.js} +3 -6
- package/dist/{getMachineId-linux-V6YSQEY7.js.map → getMachineId-linux-KJR4P5HN.js.map} +1 -1
- package/dist/{getMachineId-unsupported-4FKBJNVO.js → getMachineId-unsupported-NDNXDYDY.js} +3 -6
- package/dist/{getMachineId-unsupported-4FKBJNVO.js.map → getMachineId-unsupported-NDNXDYDY.js.map} +1 -1
- package/dist/{getMachineId-win-WLRZBKVG.js → getMachineId-win-T7PJNJXG.js} +4 -7
- package/dist/{getMachineId-win-WLRZBKVG.js.map → getMachineId-win-T7PJNJXG.js.map} +1 -1
- package/dist/index.cjs +449 -461
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -3
- package/dist/index.d.ts +38 -3
- package/dist/index.js +195 -701
- package/dist/index.js.map +1 -1
- package/dist/{monorepo-YILKGQXQ.js → monorepo-N5Z63XP7.js} +4 -4
- package/dist/{source-map-uploader-3GWUQDTS.js → source-map-uploader-MUZPI2S5.js} +5 -4
- package/dist/source-map-uploader-MUZPI2S5.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-2LDBR3F3.js.map +0 -1
- package/dist/chunk-A2AZL6MZ.js.map +0 -1
- package/dist/chunk-BGZ7J74D.js.map +0 -1
- package/dist/chunk-UPS5BGER.js +0 -182
- package/dist/chunk-UPS5BGER.js.map +0 -1
- package/dist/chunk-WK7MPK2T.js.map +0 -1
- package/dist/chunk-YMEXDDTA.js.map +0 -1
- /package/dist/{esm-MDK7CZID.js.map → chunk-NSBPE2FW.js.map} +0 -0
- /package/dist/{monorepo-YILKGQXQ.js.map → esm-KBPHCVB4.js.map} +0 -0
- /package/dist/{source-map-uploader-3GWUQDTS.js.map → monorepo-N5Z63XP7.js.map} +0 -0
|
@@ -1,76 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isProductionDisabled,
|
|
3
|
+
resolveConfig
|
|
4
|
+
} from "./chunk-VUZCLMIX.js";
|
|
1
5
|
import {
|
|
2
6
|
PresignedUploadResponseSchema,
|
|
3
7
|
SourceMapManifestResponseSchema,
|
|
4
8
|
SourceMapUploadResponseSchema
|
|
5
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-7JBKXSBU.js";
|
|
6
10
|
import {
|
|
7
|
-
__require
|
|
8
|
-
|
|
9
|
-
} from "./chunk-BGZ7J74D.js";
|
|
11
|
+
__require
|
|
12
|
+
} from "./chunk-NSBPE2FW.js";
|
|
10
13
|
|
|
11
14
|
// src/source-map-uploader.ts
|
|
12
|
-
|
|
13
|
-
import * as
|
|
14
|
-
import * as
|
|
15
|
-
import
|
|
16
|
-
import { execFileSync } from "child_process";
|
|
17
|
-
|
|
18
|
-
// src/console-capture.ts
|
|
19
|
-
init_esm_shims();
|
|
20
|
-
|
|
21
|
-
// src/nudge/error-nudge.ts
|
|
22
|
-
init_esm_shims();
|
|
23
|
-
|
|
24
|
-
// src/env-detection.ts
|
|
25
|
-
init_esm_shims();
|
|
26
|
-
var DEFAULT_ENDPOINT = "https://api.glasstrace.dev";
|
|
27
|
-
function readEnvVars() {
|
|
28
|
-
return {
|
|
29
|
-
GLASSTRACE_API_KEY: process.env.GLASSTRACE_API_KEY?.trim() || void 0,
|
|
30
|
-
GLASSTRACE_FORCE_ENABLE: process.env.GLASSTRACE_FORCE_ENABLE,
|
|
31
|
-
GLASSTRACE_ENV: process.env.GLASSTRACE_ENV,
|
|
32
|
-
GLASSTRACE_COVERAGE_MAP: process.env.GLASSTRACE_COVERAGE_MAP,
|
|
33
|
-
NODE_ENV: process.env.NODE_ENV,
|
|
34
|
-
VERCEL_ENV: process.env.VERCEL_ENV
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
function resolveConfig(options) {
|
|
38
|
-
const env = readEnvVars();
|
|
39
|
-
return {
|
|
40
|
-
apiKey: options?.apiKey ?? env.GLASSTRACE_API_KEY,
|
|
41
|
-
endpoint: options?.endpoint ?? DEFAULT_ENDPOINT,
|
|
42
|
-
forceEnable: options?.forceEnable ?? env.GLASSTRACE_FORCE_ENABLE === "true",
|
|
43
|
-
verbose: options?.verbose ?? false,
|
|
44
|
-
environment: env.GLASSTRACE_ENV,
|
|
45
|
-
coverageMapEnabled: env.GLASSTRACE_COVERAGE_MAP === "true",
|
|
46
|
-
nodeEnv: env.NODE_ENV,
|
|
47
|
-
vercelEnv: env.VERCEL_ENV
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
function isProductionDisabled(config) {
|
|
51
|
-
if (config.forceEnable) {
|
|
52
|
-
return false;
|
|
53
|
-
}
|
|
54
|
-
if (config.nodeEnv === "production") {
|
|
55
|
-
return true;
|
|
56
|
-
}
|
|
57
|
-
if (config.vercelEnv === "production") {
|
|
58
|
-
return true;
|
|
59
|
-
}
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
function isAnonymousMode(config) {
|
|
63
|
-
if (config.apiKey === void 0) {
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
if (config.apiKey.trim() === "") {
|
|
67
|
-
return true;
|
|
68
|
-
}
|
|
69
|
-
if (config.apiKey.startsWith("gt_anon_")) {
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
15
|
+
import * as fs from "node:fs/promises";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import * as crypto from "node:crypto";
|
|
18
|
+
import { execFileSync } from "node:child_process";
|
|
74
19
|
|
|
75
20
|
// src/nudge/error-nudge.ts
|
|
76
21
|
var hasFired = false;
|
|
@@ -79,8 +24,8 @@ function sanitize(input) {
|
|
|
79
24
|
}
|
|
80
25
|
function markerFileExists() {
|
|
81
26
|
try {
|
|
82
|
-
const fs2 = __require("fs");
|
|
83
|
-
const path2 = __require("path");
|
|
27
|
+
const fs2 = __require("node:fs");
|
|
28
|
+
const path2 = __require("node:path");
|
|
84
29
|
const markerPath = path2.join(process.cwd(), ".glasstrace", "mcp-connected");
|
|
85
30
|
return fs2.existsSync(markerPath);
|
|
86
31
|
} catch {
|
|
@@ -133,7 +78,7 @@ function isSdkMessage(args) {
|
|
|
133
78
|
async function installConsoleCapture() {
|
|
134
79
|
if (installed) return;
|
|
135
80
|
try {
|
|
136
|
-
otelApi = await import("./esm-
|
|
81
|
+
otelApi = await import("./esm-KBPHCVB4.js");
|
|
137
82
|
} catch {
|
|
138
83
|
otelApi = null;
|
|
139
84
|
}
|
|
@@ -486,10 +431,6 @@ async function uploadSourceMapsAuto(apiKey, endpoint, buildHash, maps, options)
|
|
|
486
431
|
}
|
|
487
432
|
|
|
488
433
|
export {
|
|
489
|
-
readEnvVars,
|
|
490
|
-
resolveConfig,
|
|
491
|
-
isProductionDisabled,
|
|
492
|
-
isAnonymousMode,
|
|
493
434
|
maybeShowMcpNudge,
|
|
494
435
|
installConsoleCapture,
|
|
495
436
|
sdkLog,
|
|
@@ -506,4 +447,4 @@ export {
|
|
|
506
447
|
uploadSourceMapsPresigned,
|
|
507
448
|
uploadSourceMapsAuto
|
|
508
449
|
};
|
|
509
|
-
//# sourceMappingURL=chunk-
|
|
450
|
+
//# sourceMappingURL=chunk-BANTDXUT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/source-map-uploader.ts","../src/nudge/error-nudge.ts","../src/console-capture.ts"],"sourcesContent":["import * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport * as crypto from \"node:crypto\";\nimport { execFileSync } from \"node:child_process\";\nimport { sdkLog } from \"./console-capture.js\";\nimport {\n SourceMapUploadResponseSchema,\n type SourceMapUploadResponse,\n PresignedUploadResponseSchema,\n type PresignedUploadResponse,\n SourceMapManifestResponseSchema,\n type SourceMapManifestResponse,\n} from \"@glasstrace/protocol\";\n\nexport interface SourceMapEntry {\n filePath: string;\n content: string;\n}\n\n/**\n * Metadata for a discovered source map file, without its content loaded.\n * Used by the streaming upload flow to defer file reads until upload time.\n */\nexport interface SourceMapFileInfo {\n /** Relative path to the compiled JS file (`.map` extension stripped). */\n filePath: string;\n /** Absolute path on disk for reading the file content on demand. */\n absolutePath: string;\n /** File size in bytes. */\n sizeBytes: number;\n}\n\n/** Threshold (50 MB) above which a single source map triggers a warning. */\nconst LARGE_FILE_WARNING_BYTES = 50 * 1024 * 1024;\n\n/**\n * Recursively discovers all `.map` files in the given build directory.\n * Returns metadata only — file content is NOT read into memory.\n */\nexport async function discoverSourceMapFiles(\n buildDir: string,\n): Promise<SourceMapFileInfo[]> {\n // Resolve to absolute so absolutePath in results is always absolute,\n // even when buildDir is relative (e.g. \".next\").\n const resolvedDir = path.resolve(buildDir);\n const results: SourceMapFileInfo[] = [];\n\n try {\n await walkDirMetadata(resolvedDir, resolvedDir, results);\n } catch {\n // Directory doesn't exist or is unreadable — return empty\n return [];\n }\n\n // Warn about oversized source map files\n for (const file of results) {\n if (file.sizeBytes >= LARGE_FILE_WARNING_BYTES) {\n const sizeMB = (file.sizeBytes / (1024 * 1024)).toFixed(1);\n sdkLog(\n \"warn\",\n `[glasstrace] Large source map detected: ${file.filePath} (${sizeMB}MB). Consider enabling source map compression.`,\n );\n }\n }\n\n return results;\n}\n\nasync function walkDirMetadata(\n baseDir: string,\n currentDir: string,\n results: SourceMapFileInfo[],\n): Promise<void> {\n let entries: import(\"node:fs\").Dirent[];\n try {\n entries = await fs.readdir(currentDir, { withFileTypes: true });\n } catch {\n return;\n }\n\n for (const entry of entries) {\n const fullPath = path.join(currentDir, entry.name);\n\n if (entry.isDirectory()) {\n await walkDirMetadata(baseDir, fullPath, results);\n } else if (entry.isFile() && entry.name.endsWith(\".map\")) {\n try {\n const stat = await fs.stat(fullPath);\n const relativePath = path.relative(baseDir, fullPath).replace(/\\\\/g, \"/\");\n // Strip the trailing .map extension so the key matches the compiled\n // JS path that the runtime uses for stack-frame lookups (e.g.\n // \"static/chunks/main.js\" instead of \"static/chunks/main.js.map\").\n const compiledPath = relativePath.replace(/\\.map$/, \"\");\n results.push({\n filePath: compiledPath,\n absolutePath: fullPath,\n sizeBytes: stat.size,\n });\n } catch {\n // Skip unreadable files\n }\n }\n }\n}\n\n/**\n * Reads the content of a single source map file from disk.\n */\nasync function readSourceMapContent(absolutePath: string): Promise<string> {\n return fs.readFile(absolutePath, \"utf-8\");\n}\n\n/**\n * Recursively finds all .map files in the given build directory.\n * Returns relative paths and file contents.\n *\n * @deprecated Prefer {@link discoverSourceMapFiles} to avoid loading all\n * source maps into memory simultaneously.\n */\nexport async function collectSourceMaps(\n buildDir: string,\n): Promise<SourceMapEntry[]> {\n const fileInfos = await discoverSourceMapFiles(buildDir);\n const results: SourceMapEntry[] = [];\n\n for (const info of fileInfos) {\n try {\n const content = await readSourceMapContent(info.absolutePath);\n results.push({ filePath: info.filePath, content });\n } catch {\n // Skip unreadable files — consistent with previous behavior\n }\n }\n\n return results;\n}\n\n/**\n * Computes a build hash for source map uploads.\n *\n * First tries `git rev-parse HEAD` to get the git commit SHA.\n * On failure, falls back to a deterministic content hash:\n * sort source map file paths alphabetically, concatenate each as\n * `{relativePath}\\n{fileLength}\\n{fileContent}`, then SHA-256 the result.\n *\n * Accepts either `SourceMapEntry[]` (legacy, in-memory) or\n * `SourceMapFileInfo[]` (streaming, reads on demand).\n */\nexport async function computeBuildHash(\n maps?: SourceMapEntry[] | SourceMapFileInfo[],\n): Promise<string> {\n // Try git first\n try {\n const sha = execFileSync(\"git\", [\"rev-parse\", \"HEAD\"], { encoding: \"utf-8\" }).trim();\n if (sha) {\n return sha;\n }\n } catch {\n // Git not available, fall through to content hash\n }\n\n // Fallback: content-based hash\n const sortedMaps = [...(maps ?? [])].sort((a, b) =>\n a.filePath.localeCompare(b.filePath),\n );\n\n const hash = crypto.createHash(\"sha256\");\n\n for (const m of sortedMaps) {\n let content: string;\n if (\"content\" in m) {\n content = m.content;\n } else {\n try {\n content = await readSourceMapContent(m.absolutePath);\n } catch {\n // Skip unreadable files — consistent with collectSourceMaps behavior\n continue;\n }\n }\n hash.update(`${m.filePath}\\n${content.length}\\n${content}`);\n }\n\n return hash.digest(\"hex\");\n}\n\n/**\n * Uploads source maps to the ingestion API.\n *\n * POSTs to `{endpoint}/v1/source-maps` with the API key, build hash,\n * and file entries. Validates the response against SourceMapUploadResponseSchema.\n *\n * Accepts either `SourceMapEntry[]` (legacy, in-memory) or\n * `SourceMapFileInfo[]` (deferred reads). With `SourceMapFileInfo[]`,\n * file content is read at upload time rather than at discovery time.\n * Note: the legacy endpoint sends all files in a single JSON body, so\n * peak memory is similar — the benefit is deferring reads past discovery.\n */\nexport async function uploadSourceMaps(\n apiKey: string,\n endpoint: string,\n buildHash: string,\n maps: SourceMapEntry[] | SourceMapFileInfo[],\n): Promise<SourceMapUploadResponse> {\n // Read files on demand — for SourceMapEntry[], content is already available;\n // for SourceMapFileInfo[], each file is read individually to limit memory.\n // Individual reads are guarded so one transient failure (deleted file,\n // permission change) does not abort the entire upload.\n const files: Array<{ filePath: string; sourceMap: string }> = [];\n for (const m of maps) {\n try {\n const content = \"content\" in m\n ? m.content\n : await readSourceMapContent(m.absolutePath);\n files.push({ filePath: m.filePath, sourceMap: content });\n } catch {\n sdkLog(\"warn\", `[glasstrace] Skipping unreadable source map: ${m.filePath}`);\n }\n }\n\n const body = {\n buildHash,\n files,\n };\n\n const baseUrl = stripTrailingSlashes(endpoint);\n const response = await fetch(`${baseUrl}/v1/source-maps`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n // Consume the response body to release the connection back to the pool.\n // Without this, the underlying TCP socket stays allocated until GC, which\n // causes connection pool exhaustion under sustained error conditions.\n // Wrapped in try-catch so a stream error doesn't mask the HTTP status error.\n try { await response.text(); } catch { /* body drain is best-effort */ }\n throw new Error(\n `Source map upload failed: ${String(response.status)} ${response.statusText}`,\n );\n }\n\n const json: unknown = await response.json();\n return SourceMapUploadResponseSchema.parse(json);\n}\n\n// ---------------------------------------------------------------------------\n// Presigned source map upload (3-phase flow for large builds)\n// ---------------------------------------------------------------------------\n\n/** Builds at or above this byte size route to the presigned upload flow. */\nexport const PRESIGNED_THRESHOLD_BYTES = 4_500_000;\n\n/** Signature for the blob upload function, injectable for testing. */\nexport type BlobUploader = (\n clientToken: string,\n pathname: string,\n content: string,\n) => Promise<{ url: string; size: number }>;\n\n/**\n * Strips trailing slashes from a URL string.\n * Uses an iterative approach to avoid regex (CodeQL js/polynomial-redos).\n */\nfunction stripTrailingSlashes(url: string): string {\n let result = url;\n while (result.endsWith(\"/\")) {\n result = result.slice(0, -1);\n }\n return result;\n}\n\n/**\n * Phase 1: Request presigned upload tokens from the ingestion API.\n *\n * POSTs to `{endpoint}/v1/source-maps/presign` with the build hash and\n * file metadata. Returns presigned tokens for each file that the client\n * uses to upload directly to blob storage.\n */\nexport async function requestPresignedTokens(\n apiKey: string,\n endpoint: string,\n buildHash: string,\n files: Array<{ filePath: string; sizeBytes: number }>,\n): Promise<PresignedUploadResponse> {\n const baseUrl = stripTrailingSlashes(endpoint);\n const response = await fetch(`${baseUrl}/v1/source-maps/presign`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify({ buildHash, files }),\n });\n\n if (!response.ok) {\n try { await response.text(); } catch { /* body drain is best-effort */ }\n throw new Error(\n `Presigned token request failed: ${String(response.status)} ${response.statusText}`,\n );\n }\n\n const json: unknown = await response.json();\n return PresignedUploadResponseSchema.parse(json);\n}\n\n/** Shape of the subset of `@vercel/blob/client` the SDK consumes. */\ntype BlobClientModule = {\n put: (\n pathname: string,\n body: Blob,\n options: { access: string; token: string },\n ) => Promise<{ url: string }>;\n};\n\n/**\n * Loads `@vercel/blob/client` at runtime using the `Function()`-based\n * dynamic-import evasion trick.\n *\n * The indirection hides the specifier from static bundler analysis — webpack,\n * tsup, esbuild, and rollup all resolve literal `await import(\"...\")` targets\n * at build time and would raise `Module not found` for consumers without\n * `@vercel/blob` installed (it is an optional peer dependency). See DISC-1255.\n *\n * **CSP note:** `Function()` is semantically equivalent to `eval()` and will\n * trigger `unsafe-eval` CSP violations in restricted environments. Source-map\n * upload only runs in Node.js at build time, so CSP does not apply here. The\n * same caveat documented on `tryImport` in `otel-config.ts` applies if this\n * code is ever re-used in a browser-equivalent runtime.\n *\n * Exported for internal test injection via {@link _setBlobClientLoaderForTesting}\n * only; not part of the public API.\n */\nasync function defaultBlobClientLoader(): Promise<BlobClientModule> {\n const dynamicImport = Function(\"id\", \"return import(id)\") as (\n id: string,\n ) => Promise<BlobClientModule>;\n return dynamicImport(\"@vercel/blob/client\");\n}\n\nlet _blobClientLoader: () => Promise<BlobClientModule> = defaultBlobClientLoader;\n\n/**\n * Replaces the blob client loader. For test use only.\n *\n * The production loader uses a `Function()`-based dynamic import to evade\n * static bundler analysis (DISC-1255), which in turn bypasses Vitest's\n * module-mock interceptor. Tests that need to stub `@vercel/blob/client`\n * call this helper to install a fake loader, then restore the default\n * with {@link _resetBlobClientLoaderForTesting}.\n *\n * @internal\n */\nexport function _setBlobClientLoaderForTesting(\n loader: () => Promise<BlobClientModule>,\n): void {\n _blobClientLoader = loader;\n}\n\n/**\n * Restores the default `@vercel/blob/client` loader. For test use only.\n *\n * @internal\n */\nexport function _resetBlobClientLoaderForTesting(): void {\n _blobClientLoader = defaultBlobClientLoader;\n}\n\n/**\n * Phase 2: Upload a single source map to blob storage using a presigned token.\n *\n * Dynamically imports `@vercel/blob/client` via {@link defaultBlobClientLoader}\n * to avoid bundling the optional peer dependency. Throws a descriptive error\n * if the package is not installed.\n */\nexport async function uploadToBlob(\n clientToken: string,\n pathname: string,\n content: string,\n): Promise<{ url: string; size: number }> {\n let mod: BlobClientModule;\n try {\n mod = await _blobClientLoader();\n } catch (err) {\n // Distinguish \"not installed\" from other import errors\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"ERR_MODULE_NOT_FOUND\" || code === \"MODULE_NOT_FOUND\") {\n throw new Error(\n \"Presigned upload requires @vercel/blob. Install it: npm install @vercel/blob\",\n );\n }\n throw err;\n }\n\n const result = await mod.put(pathname, new Blob([content]), {\n access: \"public\",\n token: clientToken,\n });\n\n return { url: result.url, size: Buffer.byteLength(content, \"utf-8\") };\n}\n\n/**\n * Phase 3: Submit the upload manifest to finalize a presigned upload.\n *\n * POSTs to `{endpoint}/v1/source-maps/manifest` with the upload ID,\n * build hash, and blob URLs for each uploaded file. The backend activates\n * the source maps for stack trace resolution.\n */\nexport async function submitManifest(\n apiKey: string,\n endpoint: string,\n uploadId: string,\n buildHash: string,\n files: Array<{ filePath: string; sizeBytes: number; blobUrl: string }>,\n): Promise<SourceMapManifestResponse> {\n const baseUrl = stripTrailingSlashes(endpoint);\n const response = await fetch(`${baseUrl}/v1/source-maps/manifest`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${apiKey}`,\n },\n body: JSON.stringify({ uploadId, buildHash, files }),\n });\n\n if (!response.ok) {\n try { await response.text(); } catch { /* body drain is best-effort */ }\n throw new Error(\n `Source map manifest submission failed: ${String(response.status)} ${response.statusText}`,\n );\n }\n\n const json: unknown = await response.json();\n return SourceMapManifestResponseSchema.parse(json);\n}\n\n/**\n * Orchestrates the 3-phase presigned upload flow.\n *\n * 1. Requests presigned tokens for all source map files\n * 2. Uploads each file to blob storage with a concurrency limit of 5,\n * reading file content from disk just before each upload\n * 3. Submits the manifest to finalize the upload\n *\n * Accepts either `SourceMapEntry[]` (legacy, in-memory) or\n * `SourceMapFileInfo[]` (streaming, reads on demand).\n *\n * Accepts an optional `blobUploader` for test injection; defaults to\n * {@link uploadToBlob}.\n */\nexport async function uploadSourceMapsPresigned(\n apiKey: string,\n endpoint: string,\n buildHash: string,\n maps: SourceMapEntry[] | SourceMapFileInfo[],\n blobUploader: BlobUploader = uploadToBlob,\n): Promise<SourceMapManifestResponse> {\n if (maps.length === 0) {\n throw new Error(\"No source maps to upload\");\n }\n\n // Determine file metadata for the presign request\n const fileMetadata = maps.map((m) => ({\n filePath: m.filePath,\n sizeBytes: \"content\" in m\n ? Buffer.byteLength(m.content, \"utf-8\")\n : m.sizeBytes,\n }));\n\n // Phase 1: request presigned tokens\n const presigned = await requestPresignedTokens(\n apiKey, endpoint, buildHash, fileMetadata,\n );\n\n // Build a lookup map for O(1) access by filePath\n const mapsByPath = new Map(maps.map((m) => [m.filePath, m]));\n\n if (mapsByPath.size !== maps.length) {\n throw new Error(\"Duplicate filePath entries in source maps\");\n }\n\n // Validate all tokens have matching entries before starting any uploads.\n for (const token of presigned.files) {\n if (!mapsByPath.has(token.filePath)) {\n throw new Error(\n `Presigned token for \"${token.filePath}\" has no matching source map entry`,\n );\n }\n }\n\n // Phase 2: upload to blob storage in chunks of CONCURRENCY.\n // File content is read from disk just before each upload to avoid\n // holding all source maps in memory simultaneously.\n const CONCURRENCY = 5;\n const uploadResults: Array<{ filePath: string; sizeBytes: number; blobUrl: string }> = [];\n\n for (let i = 0; i < presigned.files.length; i += CONCURRENCY) {\n const chunk = presigned.files.slice(i, i + CONCURRENCY);\n const chunkResults = await Promise.all(\n chunk.map(async (token) => {\n const entry = mapsByPath.get(token.filePath)!;\n let content: string;\n if (\"content\" in entry) {\n content = entry.content;\n } else {\n try {\n content = await readSourceMapContent(entry.absolutePath);\n } catch {\n sdkLog(\"warn\", `[glasstrace] Skipping unreadable source map: ${token.filePath}`);\n return null;\n }\n }\n const result = await blobUploader(token.clientToken, token.pathname, content);\n return {\n filePath: token.filePath,\n sizeBytes: result.size,\n blobUrl: result.url,\n };\n }),\n );\n for (const r of chunkResults) {\n if (r !== null) {\n uploadResults.push(r);\n }\n }\n }\n\n // Phase 3: submit manifest\n return submitManifest(apiKey, endpoint, presigned.uploadId, buildHash, uploadResults);\n}\n\n/**\n * Options for {@link uploadSourceMapsAuto}, primarily used for test injection.\n */\nexport interface AutoUploadOptions {\n /** Override blob availability check (for testing). */\n checkBlobAvailable?: () => Promise<boolean>;\n /** Override blob uploader (for testing). */\n blobUploader?: BlobUploader;\n}\n\n/**\n * Automatically routes source map uploads based on total build size.\n *\n * - Below {@link PRESIGNED_THRESHOLD_BYTES}: uses the legacy single-request\n * {@link uploadSourceMaps} endpoint.\n * - At or above the threshold: checks if `@vercel/blob` is available and\n * uses the presigned 3-phase flow. Falls back to legacy with a warning\n * if the package is not installed.\n *\n * Accepts either `SourceMapEntry[]` (legacy, in-memory) or\n * `SourceMapFileInfo[]` (streaming, reads on demand).\n */\nexport async function uploadSourceMapsAuto(\n apiKey: string,\n endpoint: string,\n buildHash: string,\n maps: SourceMapEntry[] | SourceMapFileInfo[],\n options?: AutoUploadOptions,\n): Promise<SourceMapUploadResponse | SourceMapManifestResponse> {\n if (maps.length === 0) {\n throw new Error(\"No source maps to upload\");\n }\n\n const totalBytes = maps.reduce(\n (sum, m) => {\n const bytes = \"content\" in m\n ? Buffer.byteLength(m.content, \"utf-8\")\n : m.sizeBytes;\n return sum + bytes;\n },\n 0,\n );\n\n if (totalBytes < PRESIGNED_THRESHOLD_BYTES) {\n return uploadSourceMaps(apiKey, endpoint, buildHash, maps);\n }\n\n // Check if @vercel/blob is available. Uses the shared blob client loader\n // (which goes through the `Function()` evasion trick) so webpack / tsup /\n // esbuild do not resolve `@vercel/blob/client` at build time (DISC-1255).\n const checkAvailable = options?.checkBlobAvailable ?? (async () => {\n try {\n await _blobClientLoader();\n return true;\n } catch {\n return false;\n }\n });\n\n const blobAvailable = await checkAvailable();\n\n if (blobAvailable) {\n return uploadSourceMapsPresigned(\n apiKey, endpoint, buildHash, maps, options?.blobUploader,\n );\n }\n\n // Fall back to legacy upload with a warning\n sdkLog(\"warn\",\n `[glasstrace] Build exceeds 4.5MB (${String(totalBytes)} bytes). Install @vercel/blob for ` +\n `presigned uploads to avoid serverless body size limits. Falling back to legacy upload.`\n );\n\n return uploadSourceMaps(apiKey, endpoint, buildHash, maps);\n}\n","import { resolveConfig, isProductionDisabled } from \"../env-detection.js\";\n\n/**\n * Module-level flag ensuring the nudge fires at most once per process.\n */\nlet hasFired = false;\n\n/**\n * Strips control characters (except space) from a string to prevent\n * terminal escape sequence injection via error summaries written to stderr.\n */\nfunction sanitize(input: string): string {\n // eslint-disable-next-line no-control-regex\n return input.replace(/[\\x00-\\x1f\\x7f]/g, \"\");\n}\n\n/**\n * Checks whether the MCP marker file exists using synchronous filesystem\n * APIs. Returns `false` when `node:fs` or `node:path` cannot be resolved\n * (non-Node environments) or on any I/O error.\n */\nfunction markerFileExists(): boolean {\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const fs = require(\"node:fs\") as typeof import(\"node:fs\");\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const path = require(\"node:path\") as typeof import(\"node:path\");\n const markerPath = path.join(process.cwd(), \".glasstrace\", \"mcp-connected\");\n return fs.existsSync(markerPath);\n } catch {\n // node:fs/node:path unavailable, permission denied, ENOENT from\n // cwd(), or other error — treat as not connected\n return false;\n }\n}\n\n/**\n * Shows a one-time stderr nudge when the SDK captures its first error\n * and the MCP connection marker file is absent.\n *\n * The nudge is suppressed when:\n * - It has already fired in this process\n * - The `.glasstrace/mcp-connected` marker file exists at the project root\n * - The environment is detected as production (and force-enable is off)\n *\n * Uses `process.stderr.write()` instead of `console.error()` to avoid\n * being captured by OpenTelemetry console instrumentation.\n */\nexport function maybeShowMcpNudge(errorSummary: string): void {\n if (hasFired) {\n return;\n }\n\n // Production check — suppress silently, but remember the decision\n // so subsequent calls fast-exit via hasFired without re-running I/O.\n const config = resolveConfig();\n if (isProductionDisabled(config)) {\n hasFired = true;\n return;\n }\n\n // Check for MCP connection marker file.\n if (markerFileExists()) {\n hasFired = true;\n return;\n }\n\n // Fire the nudge exactly once\n hasFired = true;\n\n const safe = sanitize(errorSummary);\n process.stderr.write(\n `[glasstrace] Error captured: ${safe}\\n` +\n ` Debug with AI: ask your agent \"What's the latest Glasstrace error?\"\\n` +\n ` Not connected? Run: npx glasstrace mcp add\\n`,\n );\n}\n","/**\n * Console error/warn capture module.\n *\n * When enabled, monkey-patches `console.error` and `console.warn` to record\n * their output as OTel span events on the currently active span. SDK-internal\n * log messages (prefixed with \"[glasstrace]\") are never captured.\n */\n\nimport { maybeShowMcpNudge } from \"./nudge/error-nudge.js\";\n\n/**\n * Module-level flag to suppress capture of SDK-internal log messages.\n * Set to `true` before calling `console.warn`/`console.error` from SDK code,\n * then reset to `false` immediately after.\n */\nexport let isGlasstraceLog = false;\n\n/** Saved reference to the original `console.error`. */\nlet originalError: typeof console.error | null = null;\n\n/** Saved reference to the original `console.warn`. */\nlet originalWarn: typeof console.warn | null = null;\n\n/** Whether the console capture is currently installed. */\nlet installed = false;\n\n/** Cached OTel API module reference, resolved at install time. */\nlet otelApi: typeof import(\"@opentelemetry/api\") | null = null;\n\n/**\n * Formats console arguments into a single string for span event attributes.\n * Uses best-effort serialization: strings pass through, Errors preserve their\n * stack trace, and other values are JSON-stringified with a String() fallback.\n */\nfunction formatArgs(args: unknown[]): string {\n return args\n .map((arg) => {\n if (typeof arg === \"string\") return arg;\n if (arg instanceof Error) return arg.stack ?? arg.message;\n try {\n return JSON.stringify(arg);\n } catch {\n return String(arg);\n }\n })\n .join(\" \");\n}\n\n/**\n * Returns `true` if the first argument is a string starting with \"[glasstrace]\".\n * Used to skip capture of SDK-internal log messages without requiring every\n * call site to set the `isGlasstraceLog` flag.\n */\nfunction isSdkMessage(args: unknown[]): boolean {\n return typeof args[0] === \"string\" && args[0].startsWith(\"[glasstrace]\");\n}\n\n/**\n * Installs console capture by replacing `console.error` and `console.warn`\n * with wrappers that record span events on the active OTel span.\n *\n * Must be called after OTel is configured so the API module is available.\n * Safe to call multiple times; subsequent calls are no-ops.\n */\nexport async function installConsoleCapture(): Promise<void> {\n if (installed) return;\n\n // Resolve OTel API at install time via dynamic import so that:\n // 1. tsup inlines @opentelemetry/api into the bundle (it's in noExternal)\n // 2. vitest's vi.doMock can intercept this import for testing\n try {\n otelApi = await import(\"@opentelemetry/api\");\n } catch {\n otelApi = null;\n }\n\n originalError = console.error;\n originalWarn = console.warn;\n installed = true;\n\n console.error = (...args: unknown[]) => {\n // Always call the original first to preserve developer experience\n originalError!.apply(console, args);\n\n // Skip SDK-internal messages and flagged messages\n if (isGlasstraceLog || isSdkMessage(args)) return;\n\n if (otelApi) {\n const span = otelApi.trace.getSpan(otelApi.context.active());\n if (span) {\n const formattedMessage = formatArgs(args);\n span.addEvent(\"console.error\", {\n \"console.message\": formattedMessage,\n });\n // Show one-time MCP connection nudge on first captured error\n try {\n maybeShowMcpNudge(formattedMessage);\n } catch {\n // Never allow the monkey-patched console.error wrapper to throw\n }\n }\n }\n };\n\n console.warn = (...args: unknown[]) => {\n originalWarn!.apply(console, args);\n\n if (isGlasstraceLog || isSdkMessage(args)) return;\n\n if (otelApi) {\n const span = otelApi.trace.getSpan(otelApi.context.active());\n if (span) {\n span.addEvent(\"console.warn\", {\n \"console.message\": formatArgs(args),\n });\n }\n }\n };\n}\n\n/**\n * Restores the original `console.error` and `console.warn` methods.\n * Primarily intended for use in tests.\n */\nexport function uninstallConsoleCapture(): void {\n if (!installed) return;\n\n if (originalError) console.error = originalError;\n if (originalWarn) console.warn = originalWarn;\n\n originalError = null;\n originalWarn = null;\n otelApi = null;\n installed = false;\n}\n\n/**\n * Logs a message from SDK-internal code without triggering console capture.\n *\n * Use this helper in new SDK code instead of bare `console.warn(...)` calls\n * to prevent SDK log messages from being recorded as user-facing span events.\n *\n * @param level - The console log level to use.\n * @param message - The message to log.\n */\nexport function sdkLog(level: \"warn\" | \"info\" | \"error\", message: string): void {\n isGlasstraceLog = true;\n try {\n console[level](message);\n } finally {\n isGlasstraceLog = false;\n }\n}\n"],"mappings":"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","names":["fs","path","stat"]}
|
|
@@ -1,14 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
init_esm_shims
|
|
3
|
-
} from "./chunk-BGZ7J74D.js";
|
|
4
|
-
|
|
5
1
|
// src/agent-detection/detect.ts
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import { constants } from "fs";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { access, stat } from "node:fs/promises";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import { constants } from "node:fs";
|
|
12
7
|
var AGENT_RULES = [
|
|
13
8
|
{
|
|
14
9
|
name: "claude",
|
|
@@ -157,7 +152,6 @@ async function detectAgents(projectRoot) {
|
|
|
157
152
|
}
|
|
158
153
|
|
|
159
154
|
// src/agent-detection/configs.ts
|
|
160
|
-
init_esm_shims();
|
|
161
155
|
function generateMcpConfig(agent, endpoint, anonKey) {
|
|
162
156
|
if (!endpoint || endpoint.trim() === "") {
|
|
163
157
|
throw new Error("endpoint must not be empty");
|
|
@@ -320,9 +314,8 @@ ${content}${m.end}
|
|
|
320
314
|
}
|
|
321
315
|
|
|
322
316
|
// src/agent-detection/inject.ts
|
|
323
|
-
|
|
324
|
-
import {
|
|
325
|
-
import { dirname as dirname2, isAbsolute, join as join2 } from "path";
|
|
317
|
+
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
318
|
+
import { dirname as dirname2, isAbsolute, join as join2 } from "node:path";
|
|
326
319
|
var HTML_START = "<!-- glasstrace:mcp:start -->";
|
|
327
320
|
var HTML_END = "<!-- glasstrace:mcp:end -->";
|
|
328
321
|
var HASH_START = "# glasstrace:mcp:start";
|
|
@@ -505,4 +498,4 @@ export {
|
|
|
505
498
|
injectInfoSection,
|
|
506
499
|
updateGitignore
|
|
507
500
|
};
|
|
508
|
-
//# sourceMappingURL=chunk-
|
|
501
|
+
//# sourceMappingURL=chunk-CTJI2YKA.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/agent-detection/detect.ts","../src/agent-detection/configs.ts","../src/agent-detection/inject.ts"],"sourcesContent":["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. Auth tokens are intentionally included here because\n * MCP config files are local-only and required for server authentication.\n *\n * @param agent - The detected agent to generate config for.\n * @param endpoint - The Glasstrace MCP endpoint URL.\n * @param anonKey - The anonymous API key for authentication.\n * @returns The formatted configuration string.\n * @throws If endpoint or anonKey is empty.\n */\nexport function generateMcpConfig(\n agent: DetectedAgent,\n endpoint: string,\n anonKey: string,\n): string {\n if (!endpoint || endpoint.trim() === \"\") {\n throw new Error(\"endpoint must not be empty\");\n }\n if (!anonKey || anonKey.trim() === \"\") {\n throw new Error(\"anonKey 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 ${anonKey}`,\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 ${anonKey}`,\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 ${anonKey}`,\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 ${anonKey}`,\n },\n },\n },\n },\n null,\n 2,\n );\n\n case \"generic\":\n return JSON.stringify(\n {\n mcpServers: {\n glasstrace: {\n url: endpoint,\n headers: {\n Authorization: `Bearer ${anonKey}`,\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 * 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(): MarkerPair {\n return {\n start: \"<!-- glasstrace:mcp:start -->\",\n end: \"<!-- glasstrace:mcp:end -->\",\n };\n}\n\nfunction hashMarkers(): MarkerPair {\n return {\n start: \"# glasstrace:mcp:start\",\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 * @param agent - The detected agent to generate info for.\n * @param endpoint - The Glasstrace MCP endpoint URL.\n * @returns The formatted info section string, or empty string for agents without a supported info file format.\n * @throws If endpoint is empty.\n */\nexport function generateInfoSection(\n agent: DetectedAgent,\n endpoint: string,\n): string {\n if (!endpoint || endpoint.trim() === \"\") {\n throw new Error(\"endpoint must not be empty\");\n }\n\n const content = [\n \"\",\n \"## Glasstrace MCP Integration\",\n \"\",\n `Glasstrace is configured as an MCP server at: ${endpoint}`,\n \"\",\n \"Available tools:\",\n \"- `get_latest_error` - Get the most recent error trace from the current session\",\n \"- `get_trace` - Get a specific trace by ID or URL pattern\",\n \"- `get_root_cause` - Get the full span tree and root cause analysis for an error\",\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 reconfigure, run: `npx glasstrace mcp add`\",\n \"\",\n ].join(\"\\n\");\n\n switch (agent.name) {\n case \"claude\": {\n const m = htmlMarkers();\n return `${m.start}\\n${content}${m.end}\\n`;\n }\n\n case \"codex\": {\n const m = htmlMarkers();\n return `${m.start}\\n${content}${m.end}\\n`;\n }\n\n case \"cursor\": {\n const m = hashMarkers();\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/** HTML comment markers used in markdown files (.md). */\nconst HTML_START = \"<!-- glasstrace:mcp:start -->\";\nconst HTML_END = \"<!-- glasstrace:mcp:end -->\";\n\n/** Hash-prefixed markers used in plain text files (.cursorrules). */\nconst HASH_START = \"# glasstrace:mcp:start\";\nconst HASH_END = \"# glasstrace:mcp:end\";\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 * Searches for both HTML comment and hash-prefixed marker formats,\n * since an existing file might use either convention.\n *\n * @returns The start and end indices (line-level) and the matched markers,\n * or null if no complete marker pair is found.\n */\nfunction findMarkerBoundaries(\n lines: string[],\n): { startIdx: number; endIdx: number } | null {\n let startIdx = -1;\n let endIdx = -1;\n\n for (let i = 0; i < lines.length; i++) {\n const trimmed = lines[i].trim();\n if (trimmed === HTML_START || trimmed === HASH_START) {\n startIdx = i;\n } else if (trimmed === HTML_END || trimmed === HASH_END) {\n if (startIdx !== -1) {\n endIdx = i;\n break;\n }\n }\n }\n\n if (startIdx === -1 || endIdx === -1) {\n return null;\n }\n\n return { startIdx, endIdx };\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 * 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 * 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":";;;;;AAAA;AAAA,SAAS,gBAAgB;AACzB,SAAS,QAAQ,YAAY;AAC7B,SAAS,SAAS,MAAM,eAAe;AACvC,SAAS,eAAe;AACxB,SAAS,iBAAiB;AA6B1B,IAAM,cAA2B;AAAA,EAC/B;AAAA,IACE,MAAM;AAAA,IACN,SAAS,CAAC,WAAW,WAAW;AAAA,IAChC,eAAe,CAAC,QAAQ,KAAK,KAAK,WAAW;AAAA,IAC7C,cAAc,CAAC,QAAQ,KAAK,KAAK,WAAW;AAAA,IAC5C,WAAW;AAAA,IACX,qBAAqB;AAAA,EACvB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,SAAS,CAAC,YAAY,QAAQ;AAAA,IAC9B,eAAe,CAAC,QAAQ,KAAK,KAAK,UAAU,aAAa;AAAA,IACzD,cAAc,CAAC,QAAQ,KAAK,KAAK,UAAU;AAAA,IAC3C,WAAW;AAAA,IACX,qBAAqB;AAAA,EACvB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,SAAS,CAAC,SAAS;AAAA,IACnB,eAAe,CAAC,QAAQ,KAAK,KAAK,WAAW,eAAe;AAAA,IAC5D,cAAc,MAAM;AAAA,IACpB,WAAW;AAAA,IACX,qBAAqB;AAAA,EACvB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,SAAS,CAAC,WAAW,cAAc;AAAA,IACnC,eAAe,CAAC,QAAQ,KAAK,KAAK,WAAW,UAAU;AAAA,IACvD,cAAc,CAAC,QAAQ,KAAK,KAAK,cAAc;AAAA,IAC/C,WAAW;AAAA,IACX,qBAAqB;AAAA,EACvB;AAAA,EACA;AAAA,IACE,MAAM;AAAA,IACN,SAAS,CAAC,kBAAkB,WAAW;AAAA,IACvC,eAAe,MACb,KAAK,QAAQ,GAAG,YAAY,YAAY,iBAAiB;AAAA,IAC3D,cAAc,CAAC,QAAQ,KAAK,KAAK,gBAAgB;AAAA,IACjD,WAAW;AAAA,IACX,qBAAqB;AAAA,EACvB;AACF;AAQA,eAAe,WACb,MACA,OAAe,UAAU,MACP;AAClB,MAAI;AACF,UAAM,OAAO,MAAM,IAAI;AACvB,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,eAAe,YAAY,UAAmC;AAC5D,MAAI,UAAU,QAAQ,QAAQ;AAE9B,SAAO,MAAM;AACX,QAAI,MAAM,WAAW,KAAK,SAAS,MAAM,GAAG,UAAU,IAAI,GAAG;AAC3D,aAAO;AAAA,IACT;AACA,UAAM,SAAS,QAAQ,OAAO;AAC9B,QAAI,WAAW,SAAS;AAEtB;AAAA,IACF;AACA,cAAU;AAAA,EACZ;AAEA,SAAO,QAAQ,QAAQ;AACzB;AAMA,SAAS,eAAe,QAAkC;AACxD,SAAO,IAAI,QAAQ,CAACA,aAAY;AAC9B,UAAM,UAAU,QAAQ,aAAa,UAAU,UAAU;AACzD,aAAS,SAAS,CAAC,MAAM,GAAG,CAAC,UAAU;AACrC,MAAAA,SAAQ,UAAU,IAAI;AAAA,IACxB,CAAC;AAAA,EACH,CAAC;AACH;AAaA,eAAsB,aACpB,aAC0B;AAC1B,QAAM,eAAe,QAAQ,WAAW;AAGxC,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,KAAK,YAAY;AAAA,EACpC,SAAS,KAAK;AACZ,UAAM,OAAQ,IAA8B;AAC5C,UAAM,IAAI;AAAA,MACR,+BAA+B,YAAY,MACxC,OAAO,KAAK,IAAI,MAAM;AAAA,IAC3B;AAAA,EACF;AAEA,MAAI,CAAC,SAAS,YAAY,GAAG;AAC3B,UAAM,IAAI,MAAM,mCAAmC,YAAY,EAAE;AAAA,EACnE;AAEA,QAAM,UAAU,MAAM,YAAY,YAAY;AAG9C,QAAM,aAAuB,CAAC;AAC9B,MAAI,UAAU;AACd,SAAO,MAAM;AACX,eAAW,KAAK,OAAO;AACvB,QAAI,YAAY,SAAS;AACvB;AAAA,IACF;AACA,UAAM,SAAS,QAAQ,OAAO;AAC9B,QAAI,WAAW,SAAS;AACtB;AAAA,IACF;AACA,cAAU;AAAA,EACZ;AAEA,QAAM,WAA4B,CAAC;AACnC,QAAM,aAAa,oBAAI,IAAe;AAEtC,aAAW,QAAQ,aAAa;AAC9B,QAAI,WAA0B;AAG9B,eAAW,OAAO,YAAY;AAC5B,UAAI,cAAc;AAClB,iBAAW,UAAU,KAAK,SAAS;AACjC,YAAI,MAAM,WAAW,KAAK,KAAK,MAAM,CAAC,GAAG;AACvC,wBAAc;AACd;AAAA,QACF;AAAA,MACF;AACA,UAAI,aAAa;AACf,mBAAW;AACX;AAAA,MACF;AAAA,IACF;AAEA,QAAI,aAAa,MAAM;AACrB;AAAA,IACF;AAEA,QAAI,WAAW,IAAI,KAAK,IAAI,GAAG;AAC7B;AAAA,IACF;AACA,eAAW,IAAI,KAAK,IAAI;AAGxB,QAAI,eAAe,KAAK,aAAa,QAAQ;AAC7C,QAAI,iBAAiB,QAAQ,CAAE,MAAM,WAAW,YAAY,GAAI;AAC9D,qBAAe;AAAA,IACjB;AAEA,UAAM,eAAe,KAAK,YACtB,MAAM,eAAe,KAAK,SAAS,IACnC;AAEJ,aAAS,KAAK;AAAA,MACZ,MAAM,KAAK;AAAA,MACX,eAAe,KAAK,cAAc,QAAQ;AAAA,MAC1C;AAAA,MACA;AAAA,MACA,qBAAqB,KAAK;AAAA,IAC5B,CAAC;AAAA,EACH;AAGA,WAAS,KAAK;AAAA,IACZ,MAAM;AAAA,IACN,eAAe,KAAK,cAAc,eAAe,UAAU;AAAA,IAC3D,cAAc;AAAA,IACd,cAAc;AAAA,IACd,qBAAqB;AAAA,EACvB,CAAC;AAED,SAAO;AACT;;;AC9OA;AAeO,SAAS,kBACd,OACA,UACA,SACQ;AACR,MAAI,CAAC,YAAY,SAAS,KAAK,MAAM,IAAI;AACvC,UAAM,IAAI,MAAM,4BAA4B;AAAA,EAC9C;AACA,MAAI,CAAC,WAAW,QAAQ,KAAK,MAAM,IAAI;AACrC,UAAM,IAAI,MAAM,2BAA2B;AAAA,EAC7C;AAEA,UAAQ,MAAM,MAAM;AAAA,IAClB,KAAK;AACH,aAAO,KAAK;AAAA,QACV;AAAA,UACE,YAAY;AAAA,YACV,YAAY;AAAA,cACV,MAAM;AAAA,cACN,KAAK;AAAA,cACL,SAAS;AAAA,gBACP,eAAe,UAAU,OAAO;AAAA,cAClC;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IAEF,KAAK,SAAS;AAGZ,YAAM,eAAe,SAClB,QAAQ,OAAO,MAAM,EACrB,QAAQ,MAAM,KAAK,EACnB,QAAQ,OAAO,KAAK,EACpB,QAAQ,OAAO,KAAK,EACpB,QAAQ,OAAO,KAAK;AACvB,aAAO;AAAA,QACL;AAAA,QACA,UAAU,YAAY;AAAA,QACtB;AAAA,QACA;AAAA,MACF,EAAE,KAAK,IAAI;AAAA,IACb;AAAA,IAEA,KAAK;AACH,aAAO,KAAK;AAAA,QACV;AAAA,UACE,YAAY;AAAA,YACV,YAAY;AAAA,cACV,SAAS;AAAA,cACT,SAAS;AAAA,gBACP,eAAe,UAAU,OAAO;AAAA,cAClC;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IAEF,KAAK;AACH,aAAO,KAAK;AAAA,QACV;AAAA,UACE,YAAY;AAAA,YACV,YAAY;AAAA,cACV,KAAK;AAAA,cACL,SAAS;AAAA,gBACP,eAAe,UAAU,OAAO;AAAA,cAClC;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IAEF,KAAK;AACH,aAAO,KAAK;AAAA,QACV;AAAA,UACE,YAAY;AAAA,YACV,YAAY;AAAA,cACV,WAAW;AAAA,cACX,SAAS;AAAA,gBACP,eAAe,UAAU,OAAO;AAAA,cAClC;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IAEF,KAAK;AACH,aAAO,KAAK;AAAA,QACV;AAAA,UACE,YAAY;AAAA,YACV,YAAY;AAAA,cACV,KAAK;AAAA,cACL,SAAS;AAAA,gBACP,eAAe,UAAU,OAAO;AAAA,cAClC;AAAA,YACF;AAAA,UACF;AAAA,QACF;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IAEF,SAAS;AACP,YAAM,cAAqB,MAAM;AACjC,YAAM,IAAI,MAAM,kBAAkB,WAAW,EAAE;AAAA,IACjD;AAAA,EACF;AACF;AAUA,SAAS,cAA0B;AACjC,SAAO;AAAA,IACL,OAAO;AAAA,IACP,KAAK;AAAA,EACP;AACF;AAEA,SAAS,cAA0B;AACjC,SAAO;AAAA,IACL,OAAO;AAAA,IACP,KAAK;AAAA,EACP;AACF;AAeO,SAAS,oBACd,OACA,UACQ;AACR,MAAI,CAAC,YAAY,SAAS,KAAK,MAAM,IAAI;AACvC,UAAM,IAAI,MAAM,4BAA4B;AAAA,EAC9C;AAEA,QAAM,UAAU;AAAA,IACd;AAAA,IACA;AAAA,IACA;AAAA,IACA,iDAAiD,QAAQ;AAAA,IACzD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AAEX,UAAQ,MAAM,MAAM;AAAA,IAClB,KAAK,UAAU;AACb,YAAM,IAAI,YAAY;AACtB,aAAO,GAAG,EAAE,KAAK;AAAA,EAAK,OAAO,GAAG,EAAE,GAAG;AAAA;AAAA,IACvC;AAAA,IAEA,KAAK,SAAS;AACZ,YAAM,IAAI,YAAY;AACtB,aAAO,GAAG,EAAE,KAAK;AAAA,EAAK,OAAO,GAAG,EAAE,GAAG;AAAA;AAAA,IACvC;AAAA,IAEA,KAAK,UAAU;AACb,YAAM,IAAI,YAAY;AACtB,aAAO,GAAG,EAAE,KAAK;AAAA,EAAK,OAAO,GAAG,EAAE,GAAG;AAAA;AAAA,IACvC;AAAA,IAEA,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IAET,SAAS;AACP,YAAM,cAAqB,MAAM;AACjC,YAAM,IAAI,MAAM,kBAAkB,WAAW,EAAE;AAAA,IACjD;AAAA,EACF;AACF;;;AC3NA;AAAA,SAAS,OAAO,OAAO,UAAU,iBAAiB;AAClD,SAAS,WAAAC,UAAS,YAAY,QAAAC,aAAY;AAI1C,IAAM,aAAa;AACnB,IAAM,WAAW;AAGjB,IAAM,aAAa;AACnB,IAAM,WAAW;AAOjB,SAAS,kBAAkB,KAAuB;AAChD,QAAM,OAAQ,IAA8B;AAC5C,SAAO,SAAS,YAAY,SAAS,WAAW,SAAS;AAC3D;AAeA,eAAsB,eACpB,OACA,SAEA,aACe;AACf,MAAI,MAAM,kBAAkB,MAAM;AAChC;AAAA,EACF;AAEA,QAAM,aAAa,MAAM;AACzB,QAAM,YAAYD,SAAQ,UAAU;AAEpC,MAAI;AACF,UAAM,MAAM,WAAW,EAAE,WAAW,KAAK,CAAC;AAAA,EAC5C,SAAS,KAAc;AACrB,QAAI,kBAAkB,GAAG,GAAG;AAC1B,cAAQ,OAAO;AAAA,QACb,oCAAoC,SAAS;AAAA;AAAA,MAC/C;AACA;AAAA,IACF;AACA,UAAM;AAAA,EACR;AAEA,MAAI;AACF,UAAM,UAAU,YAAY,SAAS,EAAE,MAAM,IAAM,CAAC;AAAA,EACtD,SAAS,KAAc;AACrB,QAAI,kBAAkB,GAAG,GAAG;AAC1B,cAAQ,OAAO;AAAA,QACb,qCAAqC,UAAU;AAAA;AAAA,MACjD;AACA;AAAA,IACF;AACA,UAAM;AAAA,EACR;AAIA,MAAI;AACF,UAAM,MAAM,YAAY,GAAK;AAAA,EAC/B,QAAQ;AAAA,EAER;AACF;AAWA,SAAS,qBACP,OAC6C;AAC7C,MAAI,WAAW;AACf,MAAI,SAAS;AAEb,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAM,UAAU,MAAM,CAAC,EAAE,KAAK;AAC9B,QAAI,YAAY,cAAc,YAAY,YAAY;AACpD,iBAAW;AAAA,IACb,WAAW,YAAY,YAAY,YAAY,UAAU;AACvD,UAAI,aAAa,IAAI;AACnB,iBAAS;AACT;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,MAAI,aAAa,MAAM,WAAW,IAAI;AACpC,WAAO;AAAA,EACT;AAEA,SAAO,EAAE,UAAU,OAAO;AAC5B;AAiBA,eAAsB,kBACpB,OACA,SAEA,aACe;AACf,MAAI,MAAM,iBAAiB,MAAM;AAC/B;AAAA,EACF;AAGA,MAAI,YAAY,IAAI;AAClB;AAAA,EACF;AAEA,QAAM,WAAW,MAAM;AAEvB,MAAI,kBAAiC;AACrC,MAAI;AACF,sBAAkB,MAAM,SAAS,UAAU,OAAO;AAAA,EACpD,SAAS,KAAc;AACrB,UAAM,OAAQ,IAA8B;AAC5C,QAAI,SAAS,UAAU;AACrB,UAAI,kBAAkB,GAAG,GAAG;AAC1B,gBAAQ,OAAO;AAAA,UACb,kCAAkC,QAAQ;AAAA;AAAA,QAC5C;AACA;AAAA,MACF;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAGA,MAAI,oBAAoB,MAAM;AAC5B,QAAI;AACF,YAAM,MAAMA,SAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;AAClD,YAAM,UAAU,UAAU,SAAS,OAAO;AAAA,IAC5C,SAAS,KAAc;AACrB,UAAI,kBAAkB,GAAG,GAAG;AAC1B,gBAAQ,OAAO;AAAA,UACb,mCAAmC,QAAQ;AAAA;AAAA,QAC7C;AACA;AAAA,MACF;AACA,YAAM;AAAA,IACR;AACA;AAAA,EACF;AAGA,QAAM,QAAQ,gBAAgB,MAAM,IAAI;AACxC,QAAM,aAAa,qBAAqB,KAAK;AAE7C,MAAI;AACJ,MAAI,eAAe,MAAM;AAEvB,UAAM,SAAS,MAAM,MAAM,GAAG,WAAW,QAAQ;AACjD,UAAM,QAAQ,MAAM,MAAM,WAAW,SAAS,CAAC;AAE/C,UAAM,gCAAgC,QAAQ,SAAS,IAAI,IACvD,QAAQ,MAAM,GAAG,EAAE,IACnB;AACJ,iBAAa,CAAC,GAAG,QAAQ,+BAA+B,GAAG,KAAK,EAAE,KAAK,IAAI;AAAA,EAC7E,OAAO;AAEL,UAAM,YAAY,gBAAgB,SAAS,IAAI,IAAI,OAAO;AAC1D,iBAAa,kBAAkB,YAAY;AAAA,EAC7C;AAEA,MAAI;AACF,UAAM,UAAU,UAAU,YAAY,OAAO;AAAA,EAC/C,SAAS,KAAc;AACrB,QAAI,kBAAkB,GAAG,GAAG;AAC1B,cAAQ,OAAO;AAAA,QACb,mCAAmC,QAAQ;AAAA;AAAA,MAC7C;AACA;AAAA,IACF;AACA,UAAM;AAAA,EACR;AACF;AAcA,eAAsB,gBACpB,OACA,aACe;AACf,QAAM,gBAAgBC,MAAK,aAAa,YAAY;AAIpD,QAAM,gBAAgB,MAAM,OAAO,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;AAExD,MAAI,cAAc,WAAW,GAAG;AAC9B;AAAA,EACF;AAEA,MAAI,kBAAkB;AACtB,MAAI;AACF,sBAAkB,MAAM,SAAS,eAAe,OAAO;AAAA,EACzD,SAAS,KAAc;AACrB,UAAM,OAAQ,IAA8B;AAC5C,QAAI,SAAS,UAAU;AACrB,UAAI,kBAAkB,GAAG,GAAG;AAC1B,gBAAQ,OAAO;AAAA,UACb;AAAA;AAAA,QACF;AACA;AAAA,MACF;AACA,YAAM;AAAA,IACR;AAAA,EACF;AAGA,QAAM,gBAAgB,gBACnB,MAAM,IAAI,EACV,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,EACzB,OAAO,CAAC,SAAS,SAAS,EAAE;AAE/B,QAAM,cAAc,IAAI,IAAI,aAAa;AAKzC,QAAM,QAAQ,cACX,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,QAAQ,OAAO,GAAG,CAAC,EACvC,OAAO,CAAC,MAAM,MAAM,MAAM,CAAC,YAAY,IAAI,CAAC,CAAC;AAEhD,MAAI,MAAM,WAAW,GAAG;AACtB;AAAA,EACF;AAGA,MAAI,iBAAiB;AACrB,MAAI,eAAe,SAAS,KAAK,CAAC,eAAe,SAAS,IAAI,GAAG;AAC/D,sBAAkB;AAAA,EACpB;AAEA,oBAAkB,MAAM,KAAK,IAAI,IAAI;AAErC,MAAI;AACF,UAAM,UAAU,eAAe,gBAAgB,OAAO;AAAA,EACxD,SAAS,KAAc;AACrB,QAAI,kBAAkB,GAAG,GAAG;AAC1B,cAAQ,OAAO;AAAA,QACb;AAAA;AAAA,MACF;AACA;AAAA,IACF;AACA,UAAM;AAAA,EACR;AACF;","names":["resolve","dirname","join"]}
|
|
1
|
+
{"version":3,"sources":["../src/agent-detection/detect.ts","../src/agent-detection/configs.ts","../src/agent-detection/inject.ts"],"sourcesContent":["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. Auth tokens are intentionally included here because\n * MCP config files are local-only and required for server authentication.\n *\n * @param agent - The detected agent to generate config for.\n * @param endpoint - The Glasstrace MCP endpoint URL.\n * @param anonKey - The anonymous API key for authentication.\n * @returns The formatted configuration string.\n * @throws If endpoint or anonKey is empty.\n */\nexport function generateMcpConfig(\n agent: DetectedAgent,\n endpoint: string,\n anonKey: string,\n): string {\n if (!endpoint || endpoint.trim() === \"\") {\n throw new Error(\"endpoint must not be empty\");\n }\n if (!anonKey || anonKey.trim() === \"\") {\n throw new Error(\"anonKey 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 ${anonKey}`,\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 ${anonKey}`,\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 ${anonKey}`,\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 ${anonKey}`,\n },\n },\n },\n },\n null,\n 2,\n );\n\n case \"generic\":\n return JSON.stringify(\n {\n mcpServers: {\n glasstrace: {\n url: endpoint,\n headers: {\n Authorization: `Bearer ${anonKey}`,\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 * 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(): MarkerPair {\n return {\n start: \"<!-- glasstrace:mcp:start -->\",\n end: \"<!-- glasstrace:mcp:end -->\",\n };\n}\n\nfunction hashMarkers(): MarkerPair {\n return {\n start: \"# glasstrace:mcp:start\",\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 * @param agent - The detected agent to generate info for.\n * @param endpoint - The Glasstrace MCP endpoint URL.\n * @returns The formatted info section string, or empty string for agents without a supported info file format.\n * @throws If endpoint is empty.\n */\nexport function generateInfoSection(\n agent: DetectedAgent,\n endpoint: string,\n): string {\n if (!endpoint || endpoint.trim() === \"\") {\n throw new Error(\"endpoint must not be empty\");\n }\n\n const content = [\n \"\",\n \"## Glasstrace MCP Integration\",\n \"\",\n `Glasstrace is configured as an MCP server at: ${endpoint}`,\n \"\",\n \"Available tools:\",\n \"- `get_latest_error` - Get the most recent error trace from the current session\",\n \"- `get_trace` - Get a specific trace by ID or URL pattern\",\n \"- `get_root_cause` - Get the full span tree and root cause analysis for an error\",\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 reconfigure, run: `npx glasstrace mcp add`\",\n \"\",\n ].join(\"\\n\");\n\n switch (agent.name) {\n case \"claude\": {\n const m = htmlMarkers();\n return `${m.start}\\n${content}${m.end}\\n`;\n }\n\n case \"codex\": {\n const m = htmlMarkers();\n return `${m.start}\\n${content}${m.end}\\n`;\n }\n\n case \"cursor\": {\n const m = hashMarkers();\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/** HTML comment markers used in markdown files (.md). */\nconst HTML_START = \"<!-- glasstrace:mcp:start -->\";\nconst HTML_END = \"<!-- glasstrace:mcp:end -->\";\n\n/** Hash-prefixed markers used in plain text files (.cursorrules). */\nconst HASH_START = \"# glasstrace:mcp:start\";\nconst HASH_END = \"# glasstrace:mcp:end\";\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 * Searches for both HTML comment and hash-prefixed marker formats,\n * since an existing file might use either convention.\n *\n * @returns The start and end indices (line-level) and the matched markers,\n * or null if no complete marker pair is found.\n */\nfunction findMarkerBoundaries(\n lines: string[],\n): { startIdx: number; endIdx: number } | null {\n let startIdx = -1;\n let endIdx = -1;\n\n for (let i = 0; i < lines.length; i++) {\n const trimmed = lines[i].trim();\n if (trimmed === HTML_START || trimmed === HASH_START) {\n startIdx = i;\n } else if (trimmed === HTML_END || trimmed === HASH_END) {\n if (startIdx !== -1) {\n endIdx = i;\n break;\n }\n }\n }\n\n if (startIdx === -1 || endIdx === -1) {\n return null;\n }\n\n return { startIdx, endIdx };\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 * 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 * 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":["resolve","dirname","join"]}
|