@glasstrace/sdk 0.2.3 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-CUFIV225.js → chunk-EC5IINUT.js} +86 -201
- package/dist/chunk-EC5IINUT.js.map +1 -0
- package/dist/chunk-LAMTBURS.js +178 -0
- package/dist/chunk-LAMTBURS.js.map +1 -0
- package/dist/chunk-STECO33B.js +675 -0
- package/dist/chunk-STECO33B.js.map +1 -0
- package/dist/cli/init.cjs +12476 -10881
- package/dist/cli/init.cjs.map +1 -1
- package/dist/cli/init.js +193 -184
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/mcp-add.cjs +14651 -0
- package/dist/cli/mcp-add.cjs.map +1 -0
- package/dist/cli/mcp-add.d.cts +46 -0
- package/dist/cli/mcp-add.d.ts +46 -0
- package/dist/cli/mcp-add.js +243 -0
- package/dist/cli/mcp-add.js.map +1 -0
- package/dist/index.cjs +54 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +59 -67
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-CUFIV225.js.map +0 -1
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
// src/cli/scaffolder.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
function identityFingerprint(token) {
|
|
6
|
+
return `sha256:${createHash("sha256").update(token).digest("hex")}`;
|
|
7
|
+
}
|
|
8
|
+
var NEXT_CONFIG_NAMES = ["next.config.ts", "next.config.js", "next.config.mjs"];
|
|
9
|
+
async function scaffoldInstrumentation(projectRoot, force) {
|
|
10
|
+
const filePath = path.join(projectRoot, "instrumentation.ts");
|
|
11
|
+
if (fs.existsSync(filePath) && !force) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const content = `import { registerGlasstrace } from "@glasstrace/sdk";
|
|
15
|
+
|
|
16
|
+
export async function register() {
|
|
17
|
+
// Glasstrace must be registered before Prisma instrumentation
|
|
18
|
+
// to ensure all ORM spans are captured correctly.
|
|
19
|
+
// If you use @prisma/instrumentation, import it after this call.
|
|
20
|
+
registerGlasstrace();
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
async function scaffoldNextConfig(projectRoot) {
|
|
27
|
+
let configPath;
|
|
28
|
+
let configName;
|
|
29
|
+
for (const name of NEXT_CONFIG_NAMES) {
|
|
30
|
+
const candidate = path.join(projectRoot, name);
|
|
31
|
+
if (fs.existsSync(candidate)) {
|
|
32
|
+
configPath = candidate;
|
|
33
|
+
configName = name;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (configPath === void 0 || configName === void 0) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const existing = fs.readFileSync(configPath, "utf-8");
|
|
41
|
+
if (existing.includes("withGlasstraceConfig")) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const isESM = configName.endsWith(".ts") || configName.endsWith(".mjs");
|
|
45
|
+
if (isESM) {
|
|
46
|
+
const importLine = 'import { withGlasstraceConfig } from "@glasstrace/sdk";\n';
|
|
47
|
+
const wrapResult2 = wrapExport(existing);
|
|
48
|
+
if (!wrapResult2.wrapped) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const modified2 = importLine + "\n" + wrapResult2.content;
|
|
52
|
+
fs.writeFileSync(configPath, modified2, "utf-8");
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
const requireLine = 'const { withGlasstraceConfig } = require("@glasstrace/sdk");\n';
|
|
56
|
+
const wrapResult = wrapCJSExport(existing);
|
|
57
|
+
if (!wrapResult.wrapped) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const modified = requireLine + "\n" + wrapResult.content;
|
|
61
|
+
fs.writeFileSync(configPath, modified, "utf-8");
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
function wrapExport(content) {
|
|
65
|
+
const marker = "export default";
|
|
66
|
+
const idx = content.lastIndexOf(marker);
|
|
67
|
+
if (idx === -1) {
|
|
68
|
+
return { content, wrapped: false };
|
|
69
|
+
}
|
|
70
|
+
const preamble = content.slice(0, idx);
|
|
71
|
+
const exprRaw = content.slice(idx + marker.length);
|
|
72
|
+
const expr = exprRaw.trim().replace(/;?\s*$/, "");
|
|
73
|
+
if (expr.length === 0) {
|
|
74
|
+
return { content, wrapped: false };
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
content: preamble + `export default withGlasstraceConfig(${expr});
|
|
78
|
+
`,
|
|
79
|
+
wrapped: true
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function wrapCJSExport(content) {
|
|
83
|
+
const cjsMarker = "module.exports";
|
|
84
|
+
const cjsIdx = content.lastIndexOf(cjsMarker);
|
|
85
|
+
if (cjsIdx === -1) {
|
|
86
|
+
return { content, wrapped: false };
|
|
87
|
+
}
|
|
88
|
+
const preamble = content.slice(0, cjsIdx);
|
|
89
|
+
const afterMarker = content.slice(cjsIdx + cjsMarker.length);
|
|
90
|
+
const eqMatch = /^\s*=\s*/.exec(afterMarker);
|
|
91
|
+
if (!eqMatch) {
|
|
92
|
+
return { content, wrapped: false };
|
|
93
|
+
}
|
|
94
|
+
const exprRaw = afterMarker.slice(eqMatch[0].length);
|
|
95
|
+
const expr = exprRaw.trim().replace(/;?\s*$/, "");
|
|
96
|
+
if (expr.length === 0) {
|
|
97
|
+
return { content, wrapped: false };
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
content: preamble + `module.exports = withGlasstraceConfig(${expr});
|
|
101
|
+
`,
|
|
102
|
+
wrapped: true
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async function scaffoldEnvLocal(projectRoot) {
|
|
106
|
+
const filePath = path.join(projectRoot, ".env.local");
|
|
107
|
+
if (fs.existsSync(filePath)) {
|
|
108
|
+
const existing = fs.readFileSync(filePath, "utf-8");
|
|
109
|
+
if (/^\s*#?\s*GLASSTRACE_API_KEY\s*=/m.test(existing)) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
const separator = existing.endsWith("\n") ? "" : "\n";
|
|
113
|
+
fs.writeFileSync(filePath, existing + separator + "# GLASSTRACE_API_KEY=your_key_here\n", "utf-8");
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
fs.writeFileSync(filePath, "# GLASSTRACE_API_KEY=your_key_here\n", "utf-8");
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
async function addCoverageMapEnv(projectRoot) {
|
|
120
|
+
const filePath = path.join(projectRoot, ".env.local");
|
|
121
|
+
if (!fs.existsSync(filePath)) {
|
|
122
|
+
fs.writeFileSync(filePath, "GLASSTRACE_COVERAGE_MAP=true\n", "utf-8");
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
const existing = fs.readFileSync(filePath, "utf-8");
|
|
126
|
+
const keyRegex = /^(\s*GLASSTRACE_COVERAGE_MAP\s*=\s*)(.*)$/m;
|
|
127
|
+
const keyMatch = keyRegex.exec(existing);
|
|
128
|
+
if (keyMatch) {
|
|
129
|
+
const currentValue = keyMatch[2].trim();
|
|
130
|
+
if (currentValue === "true") {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
const updated = existing.replace(keyRegex, `${keyMatch[1]}true`);
|
|
134
|
+
fs.writeFileSync(filePath, updated, "utf-8");
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
const separator = existing.endsWith("\n") ? "" : "\n";
|
|
138
|
+
fs.writeFileSync(filePath, existing + separator + "GLASSTRACE_COVERAGE_MAP=true\n", "utf-8");
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
async function scaffoldGitignore(projectRoot) {
|
|
142
|
+
const filePath = path.join(projectRoot, ".gitignore");
|
|
143
|
+
if (fs.existsSync(filePath)) {
|
|
144
|
+
const existing = fs.readFileSync(filePath, "utf-8");
|
|
145
|
+
const lines = existing.split("\n").map((l) => l.trim());
|
|
146
|
+
if (lines.includes(".glasstrace/")) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
const separator = existing.endsWith("\n") ? "" : "\n";
|
|
150
|
+
fs.writeFileSync(filePath, existing + separator + ".glasstrace/\n", "utf-8");
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
fs.writeFileSync(filePath, ".glasstrace/\n", "utf-8");
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
async function scaffoldMcpMarker(projectRoot, anonKey) {
|
|
157
|
+
const dirPath = path.join(projectRoot, ".glasstrace");
|
|
158
|
+
const markerPath = path.join(dirPath, "mcp-connected");
|
|
159
|
+
const keyHash = identityFingerprint(anonKey);
|
|
160
|
+
if (fs.existsSync(markerPath)) {
|
|
161
|
+
try {
|
|
162
|
+
const existing = JSON.parse(fs.readFileSync(markerPath, "utf-8"));
|
|
163
|
+
if (existing.keyHash === keyHash) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
fs.mkdirSync(dirPath, { recursive: true, mode: 448 });
|
|
170
|
+
const marker = JSON.stringify(
|
|
171
|
+
{ keyHash, configuredAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
172
|
+
null,
|
|
173
|
+
2
|
|
174
|
+
);
|
|
175
|
+
fs.writeFileSync(markerPath, marker, { mode: 384 });
|
|
176
|
+
fs.chmodSync(markerPath, 384);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// src/agent-detection/detect.ts
|
|
181
|
+
import { execFile } from "child_process";
|
|
182
|
+
import { access, stat } from "fs/promises";
|
|
183
|
+
import { dirname, join as join2, resolve } from "path";
|
|
184
|
+
import { homedir } from "os";
|
|
185
|
+
import { constants } from "fs";
|
|
186
|
+
var AGENT_RULES = [
|
|
187
|
+
{
|
|
188
|
+
name: "claude",
|
|
189
|
+
markers: [".claude", "CLAUDE.md"],
|
|
190
|
+
mcpConfigPath: (dir) => join2(dir, ".mcp.json"),
|
|
191
|
+
infoFilePath: (dir) => join2(dir, "CLAUDE.md"),
|
|
192
|
+
cliBinary: "claude",
|
|
193
|
+
registrationCommand: "npx glasstrace mcp add --agent claude"
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: "codex",
|
|
197
|
+
markers: ["codex.md", ".codex"],
|
|
198
|
+
mcpConfigPath: (dir) => join2(dir, ".codex", "config.toml"),
|
|
199
|
+
infoFilePath: (dir) => join2(dir, "codex.md"),
|
|
200
|
+
cliBinary: "codex",
|
|
201
|
+
registrationCommand: "npx glasstrace mcp add --agent codex"
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: "gemini",
|
|
205
|
+
markers: [".gemini"],
|
|
206
|
+
mcpConfigPath: (dir) => join2(dir, ".gemini", "settings.json"),
|
|
207
|
+
infoFilePath: () => null,
|
|
208
|
+
cliBinary: "gemini",
|
|
209
|
+
registrationCommand: "npx glasstrace mcp add --agent gemini"
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
name: "cursor",
|
|
213
|
+
markers: [".cursor", ".cursorrules"],
|
|
214
|
+
mcpConfigPath: (dir) => join2(dir, ".cursor", "mcp.json"),
|
|
215
|
+
infoFilePath: (dir) => join2(dir, ".cursorrules"),
|
|
216
|
+
cliBinary: null,
|
|
217
|
+
registrationCommand: "npx glasstrace mcp add --agent cursor"
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
name: "windsurf",
|
|
221
|
+
markers: [".windsurfrules", ".windsurf"],
|
|
222
|
+
mcpConfigPath: () => join2(homedir(), ".codeium", "windsurf", "mcp_config.json"),
|
|
223
|
+
infoFilePath: (dir) => join2(dir, ".windsurfrules"),
|
|
224
|
+
cliBinary: null,
|
|
225
|
+
registrationCommand: "npx glasstrace mcp add --agent windsurf"
|
|
226
|
+
}
|
|
227
|
+
];
|
|
228
|
+
async function pathExists(path2, mode = constants.R_OK) {
|
|
229
|
+
try {
|
|
230
|
+
await access(path2, mode);
|
|
231
|
+
return true;
|
|
232
|
+
} catch {
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
async function findGitRoot(startDir) {
|
|
237
|
+
let current = resolve(startDir);
|
|
238
|
+
while (true) {
|
|
239
|
+
if (await pathExists(join2(current, ".git"), constants.F_OK)) {
|
|
240
|
+
return current;
|
|
241
|
+
}
|
|
242
|
+
const parent = dirname(current);
|
|
243
|
+
if (parent === current) {
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
current = parent;
|
|
247
|
+
}
|
|
248
|
+
return resolve(startDir);
|
|
249
|
+
}
|
|
250
|
+
function isCliAvailable(binary) {
|
|
251
|
+
return new Promise((resolve2) => {
|
|
252
|
+
const command = process.platform === "win32" ? "where" : "which";
|
|
253
|
+
execFile(command, [binary], (error) => {
|
|
254
|
+
resolve2(error === null);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
async function detectAgents(projectRoot) {
|
|
259
|
+
const resolvedRoot = resolve(projectRoot);
|
|
260
|
+
let rootStat;
|
|
261
|
+
try {
|
|
262
|
+
rootStat = await stat(resolvedRoot);
|
|
263
|
+
} catch (err) {
|
|
264
|
+
const code = err.code;
|
|
265
|
+
throw new Error(
|
|
266
|
+
`projectRoot does not exist: ${resolvedRoot}` + (code ? ` (${code})` : "")
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (!rootStat.isDirectory()) {
|
|
270
|
+
throw new Error(`projectRoot is not a directory: ${resolvedRoot}`);
|
|
271
|
+
}
|
|
272
|
+
const gitRoot = await findGitRoot(resolvedRoot);
|
|
273
|
+
const searchDirs = [];
|
|
274
|
+
let current = resolvedRoot;
|
|
275
|
+
while (true) {
|
|
276
|
+
searchDirs.push(current);
|
|
277
|
+
if (current === gitRoot) {
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
const parent = dirname(current);
|
|
281
|
+
if (parent === current) {
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
current = parent;
|
|
285
|
+
}
|
|
286
|
+
const detected = [];
|
|
287
|
+
const seenAgents = /* @__PURE__ */ new Set();
|
|
288
|
+
for (const rule of AGENT_RULES) {
|
|
289
|
+
let foundDir = null;
|
|
290
|
+
for (const dir of searchDirs) {
|
|
291
|
+
let markerFound = false;
|
|
292
|
+
for (const marker of rule.markers) {
|
|
293
|
+
if (await pathExists(join2(dir, marker))) {
|
|
294
|
+
markerFound = true;
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (markerFound) {
|
|
299
|
+
foundDir = dir;
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (foundDir === null) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (seenAgents.has(rule.name)) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
seenAgents.add(rule.name);
|
|
310
|
+
let infoFilePath = rule.infoFilePath(foundDir);
|
|
311
|
+
if (infoFilePath !== null && !await pathExists(infoFilePath)) {
|
|
312
|
+
infoFilePath = null;
|
|
313
|
+
}
|
|
314
|
+
const cliAvailable = rule.cliBinary ? await isCliAvailable(rule.cliBinary) : false;
|
|
315
|
+
detected.push({
|
|
316
|
+
name: rule.name,
|
|
317
|
+
mcpConfigPath: rule.mcpConfigPath(foundDir),
|
|
318
|
+
infoFilePath,
|
|
319
|
+
cliAvailable,
|
|
320
|
+
registrationCommand: rule.registrationCommand
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
detected.push({
|
|
324
|
+
name: "generic",
|
|
325
|
+
mcpConfigPath: join2(resolvedRoot, ".glasstrace", "mcp.json"),
|
|
326
|
+
infoFilePath: null,
|
|
327
|
+
cliAvailable: false,
|
|
328
|
+
registrationCommand: null
|
|
329
|
+
});
|
|
330
|
+
return detected;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/agent-detection/configs.ts
|
|
334
|
+
function generateMcpConfig(agent, endpoint, anonKey) {
|
|
335
|
+
if (!endpoint || endpoint.trim() === "") {
|
|
336
|
+
throw new Error("endpoint must not be empty");
|
|
337
|
+
}
|
|
338
|
+
if (!anonKey || anonKey.trim() === "") {
|
|
339
|
+
throw new Error("anonKey must not be empty");
|
|
340
|
+
}
|
|
341
|
+
switch (agent.name) {
|
|
342
|
+
case "claude":
|
|
343
|
+
return JSON.stringify(
|
|
344
|
+
{
|
|
345
|
+
mcpServers: {
|
|
346
|
+
glasstrace: {
|
|
347
|
+
type: "http",
|
|
348
|
+
url: endpoint,
|
|
349
|
+
headers: {
|
|
350
|
+
Authorization: `Bearer ${anonKey}`
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
null,
|
|
356
|
+
2
|
|
357
|
+
);
|
|
358
|
+
case "codex": {
|
|
359
|
+
const safeEndpoint = endpoint.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
360
|
+
return [
|
|
361
|
+
"[mcp_servers.glasstrace]",
|
|
362
|
+
`url = "${safeEndpoint}"`,
|
|
363
|
+
`bearer_token_env_var = "GLASSTRACE_API_KEY"`,
|
|
364
|
+
""
|
|
365
|
+
].join("\n");
|
|
366
|
+
}
|
|
367
|
+
case "gemini":
|
|
368
|
+
return JSON.stringify(
|
|
369
|
+
{
|
|
370
|
+
mcpServers: {
|
|
371
|
+
glasstrace: {
|
|
372
|
+
httpUrl: endpoint,
|
|
373
|
+
headers: {
|
|
374
|
+
Authorization: `Bearer ${anonKey}`
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
},
|
|
379
|
+
null,
|
|
380
|
+
2
|
|
381
|
+
);
|
|
382
|
+
case "cursor":
|
|
383
|
+
return JSON.stringify(
|
|
384
|
+
{
|
|
385
|
+
mcpServers: {
|
|
386
|
+
glasstrace: {
|
|
387
|
+
url: endpoint,
|
|
388
|
+
headers: {
|
|
389
|
+
Authorization: `Bearer ${anonKey}`
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
null,
|
|
395
|
+
2
|
|
396
|
+
);
|
|
397
|
+
case "windsurf":
|
|
398
|
+
return JSON.stringify(
|
|
399
|
+
{
|
|
400
|
+
mcpServers: {
|
|
401
|
+
glasstrace: {
|
|
402
|
+
serverUrl: endpoint,
|
|
403
|
+
headers: {
|
|
404
|
+
Authorization: `Bearer ${anonKey}`
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
null,
|
|
410
|
+
2
|
|
411
|
+
);
|
|
412
|
+
case "generic":
|
|
413
|
+
return JSON.stringify(
|
|
414
|
+
{
|
|
415
|
+
mcpServers: {
|
|
416
|
+
glasstrace: {
|
|
417
|
+
url: endpoint,
|
|
418
|
+
headers: {
|
|
419
|
+
Authorization: `Bearer ${anonKey}`
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
null,
|
|
425
|
+
2
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function htmlMarkers() {
|
|
430
|
+
return {
|
|
431
|
+
start: "<!-- glasstrace:mcp:start -->",
|
|
432
|
+
end: "<!-- glasstrace:mcp:end -->"
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function hashMarkers() {
|
|
436
|
+
return {
|
|
437
|
+
start: "# glasstrace:mcp:start",
|
|
438
|
+
end: "# glasstrace:mcp:end"
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function generateInfoSection(agent, endpoint) {
|
|
442
|
+
if (!endpoint || endpoint.trim() === "") {
|
|
443
|
+
throw new Error("endpoint must not be empty");
|
|
444
|
+
}
|
|
445
|
+
const content = [
|
|
446
|
+
"",
|
|
447
|
+
"## Glasstrace MCP Integration",
|
|
448
|
+
"",
|
|
449
|
+
`Glasstrace is configured as an MCP server at: ${endpoint}`,
|
|
450
|
+
"",
|
|
451
|
+
"Available tools:",
|
|
452
|
+
"- `glasstrace_submit_trace` - Submit trace data for debugging analysis",
|
|
453
|
+
"- `glasstrace_get_config` - Retrieve current SDK configuration",
|
|
454
|
+
"",
|
|
455
|
+
"To reconfigure, run: `npx glasstrace mcp add`",
|
|
456
|
+
""
|
|
457
|
+
].join("\n");
|
|
458
|
+
switch (agent.name) {
|
|
459
|
+
case "claude": {
|
|
460
|
+
const m = htmlMarkers();
|
|
461
|
+
return `${m.start}
|
|
462
|
+
${content}${m.end}
|
|
463
|
+
`;
|
|
464
|
+
}
|
|
465
|
+
case "codex": {
|
|
466
|
+
const m = htmlMarkers();
|
|
467
|
+
return `${m.start}
|
|
468
|
+
${content}${m.end}
|
|
469
|
+
`;
|
|
470
|
+
}
|
|
471
|
+
case "cursor": {
|
|
472
|
+
const m = hashMarkers();
|
|
473
|
+
return `${m.start}
|
|
474
|
+
${content}${m.end}
|
|
475
|
+
`;
|
|
476
|
+
}
|
|
477
|
+
case "gemini":
|
|
478
|
+
case "windsurf":
|
|
479
|
+
case "generic":
|
|
480
|
+
return "";
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// src/agent-detection/inject.ts
|
|
485
|
+
import { chmod, mkdir, readFile, writeFile } from "fs/promises";
|
|
486
|
+
import { dirname as dirname2, isAbsolute, join as join3 } from "path";
|
|
487
|
+
var HTML_START = "<!-- glasstrace:mcp:start -->";
|
|
488
|
+
var HTML_END = "<!-- glasstrace:mcp:end -->";
|
|
489
|
+
var HASH_START = "# glasstrace:mcp:start";
|
|
490
|
+
var HASH_END = "# glasstrace:mcp:end";
|
|
491
|
+
function isPermissionError(err) {
|
|
492
|
+
const code = err.code;
|
|
493
|
+
return code === "EACCES" || code === "EPERM" || code === "EROFS";
|
|
494
|
+
}
|
|
495
|
+
async function writeMcpConfig(agent, content, projectRoot) {
|
|
496
|
+
if (agent.mcpConfigPath === null) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
const configPath = agent.mcpConfigPath;
|
|
500
|
+
const parentDir = dirname2(configPath);
|
|
501
|
+
try {
|
|
502
|
+
await mkdir(parentDir, { recursive: true });
|
|
503
|
+
} catch (err) {
|
|
504
|
+
if (isPermissionError(err)) {
|
|
505
|
+
process.stderr.write(
|
|
506
|
+
`Warning: cannot create directory ${parentDir}: permission denied
|
|
507
|
+
`
|
|
508
|
+
);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
throw err;
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
await writeFile(configPath, content, { mode: 384 });
|
|
515
|
+
} catch (err) {
|
|
516
|
+
if (isPermissionError(err)) {
|
|
517
|
+
process.stderr.write(
|
|
518
|
+
`Warning: cannot write config file ${configPath}: permission denied
|
|
519
|
+
`
|
|
520
|
+
);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
throw err;
|
|
524
|
+
}
|
|
525
|
+
try {
|
|
526
|
+
await chmod(configPath, 384);
|
|
527
|
+
} catch {
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
function findMarkerBoundaries(lines) {
|
|
531
|
+
let startIdx = -1;
|
|
532
|
+
let endIdx = -1;
|
|
533
|
+
for (let i = 0; i < lines.length; i++) {
|
|
534
|
+
const trimmed = lines[i].trim();
|
|
535
|
+
if (trimmed === HTML_START || trimmed === HASH_START) {
|
|
536
|
+
startIdx = i;
|
|
537
|
+
} else if (trimmed === HTML_END || trimmed === HASH_END) {
|
|
538
|
+
if (startIdx !== -1) {
|
|
539
|
+
endIdx = i;
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (startIdx === -1 || endIdx === -1) {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
return { startIdx, endIdx };
|
|
548
|
+
}
|
|
549
|
+
async function injectInfoSection(agent, content, projectRoot) {
|
|
550
|
+
if (agent.infoFilePath === null) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (content === "") {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const filePath = agent.infoFilePath;
|
|
557
|
+
let existingContent = null;
|
|
558
|
+
try {
|
|
559
|
+
existingContent = await readFile(filePath, "utf-8");
|
|
560
|
+
} catch (err) {
|
|
561
|
+
const code = err.code;
|
|
562
|
+
if (code !== "ENOENT") {
|
|
563
|
+
if (isPermissionError(err)) {
|
|
564
|
+
process.stderr.write(
|
|
565
|
+
`Warning: cannot read info file ${filePath}: permission denied
|
|
566
|
+
`
|
|
567
|
+
);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
throw err;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (existingContent === null) {
|
|
574
|
+
try {
|
|
575
|
+
await mkdir(dirname2(filePath), { recursive: true });
|
|
576
|
+
await writeFile(filePath, content, "utf-8");
|
|
577
|
+
} catch (err) {
|
|
578
|
+
if (isPermissionError(err)) {
|
|
579
|
+
process.stderr.write(
|
|
580
|
+
`Warning: cannot write info file ${filePath}: permission denied
|
|
581
|
+
`
|
|
582
|
+
);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
throw err;
|
|
586
|
+
}
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
const lines = existingContent.split("\n");
|
|
590
|
+
const boundaries = findMarkerBoundaries(lines);
|
|
591
|
+
let newContent;
|
|
592
|
+
if (boundaries !== null) {
|
|
593
|
+
const before = lines.slice(0, boundaries.startIdx);
|
|
594
|
+
const after = lines.slice(boundaries.endIdx + 1);
|
|
595
|
+
const contentWithoutTrailingNewline = content.endsWith("\n") ? content.slice(0, -1) : content;
|
|
596
|
+
newContent = [...before, contentWithoutTrailingNewline, ...after].join("\n");
|
|
597
|
+
} else {
|
|
598
|
+
const separator = existingContent.endsWith("\n") ? "\n" : "\n\n";
|
|
599
|
+
newContent = existingContent + separator + content;
|
|
600
|
+
}
|
|
601
|
+
try {
|
|
602
|
+
await writeFile(filePath, newContent, "utf-8");
|
|
603
|
+
} catch (err) {
|
|
604
|
+
if (isPermissionError(err)) {
|
|
605
|
+
process.stderr.write(
|
|
606
|
+
`Warning: cannot write info file ${filePath}: permission denied
|
|
607
|
+
`
|
|
608
|
+
);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
throw err;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
async function updateGitignore(paths, projectRoot) {
|
|
615
|
+
const gitignorePath = join3(projectRoot, ".gitignore");
|
|
616
|
+
const relativePaths = paths.filter((p) => !isAbsolute(p));
|
|
617
|
+
if (relativePaths.length === 0) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
let existingContent = "";
|
|
621
|
+
try {
|
|
622
|
+
existingContent = await readFile(gitignorePath, "utf-8");
|
|
623
|
+
} catch (err) {
|
|
624
|
+
const code = err.code;
|
|
625
|
+
if (code !== "ENOENT") {
|
|
626
|
+
if (isPermissionError(err)) {
|
|
627
|
+
process.stderr.write(
|
|
628
|
+
`Warning: cannot read .gitignore: permission denied
|
|
629
|
+
`
|
|
630
|
+
);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
throw err;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
const existingLines = existingContent.split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
637
|
+
const existingSet = new Set(existingLines);
|
|
638
|
+
const toAdd = relativePaths.map((p) => p.trim().replace(/\\/g, "/")).filter((p) => p !== "" && !existingSet.has(p));
|
|
639
|
+
if (toAdd.length === 0) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
let updatedContent = existingContent;
|
|
643
|
+
if (updatedContent.length > 0 && !updatedContent.endsWith("\n")) {
|
|
644
|
+
updatedContent += "\n";
|
|
645
|
+
}
|
|
646
|
+
updatedContent += toAdd.join("\n") + "\n";
|
|
647
|
+
try {
|
|
648
|
+
await writeFile(gitignorePath, updatedContent, "utf-8");
|
|
649
|
+
} catch (err) {
|
|
650
|
+
if (isPermissionError(err)) {
|
|
651
|
+
process.stderr.write(
|
|
652
|
+
`Warning: cannot write .gitignore: permission denied
|
|
653
|
+
`
|
|
654
|
+
);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
throw err;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export {
|
|
662
|
+
scaffoldInstrumentation,
|
|
663
|
+
scaffoldNextConfig,
|
|
664
|
+
scaffoldEnvLocal,
|
|
665
|
+
addCoverageMapEnv,
|
|
666
|
+
scaffoldGitignore,
|
|
667
|
+
scaffoldMcpMarker,
|
|
668
|
+
detectAgents,
|
|
669
|
+
generateMcpConfig,
|
|
670
|
+
generateInfoSection,
|
|
671
|
+
writeMcpConfig,
|
|
672
|
+
injectInfoSection,
|
|
673
|
+
updateGitignore
|
|
674
|
+
};
|
|
675
|
+
//# sourceMappingURL=chunk-STECO33B.js.map
|