@glasstrace/sdk 0.7.2 → 0.8.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/dist/cli/init.cjs CHANGED
@@ -35,12 +35,68 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
35
35
  function identityFingerprint(token) {
36
36
  return `sha256:${(0, import_node_crypto.createHash)("sha256").update(token).digest("hex")}`;
37
37
  }
38
- async function scaffoldInstrumentation(projectRoot, force) {
39
- const filePath = path.join(projectRoot, "instrumentation.ts");
40
- if (fs.existsSync(filePath) && !force) {
41
- return false;
38
+ function hasRegisterGlasstraceCall(content) {
39
+ return content.split("\n").some((line) => {
40
+ const uncommented = line.replace(/\/\/.*$/, "");
41
+ return /\bregisterGlasstrace\s*\(/.test(uncommented);
42
+ });
43
+ }
44
+ function injectRegisterGlasstrace(content) {
45
+ if (hasRegisterGlasstraceCall(content)) {
46
+ return { injected: false, content };
47
+ }
48
+ const registerFnRegex = /export\s+(?:async\s+)?function\s+register\s*\([^)]*\)\s*\{/;
49
+ const match = registerFnRegex.exec(content);
50
+ if (!match) {
51
+ return { injected: false, content };
52
+ }
53
+ const afterBrace = content.slice(match.index + match[0].length);
54
+ const indentMatch = /\n([ \t]+)/.exec(afterBrace);
55
+ const indent = indentMatch ? indentMatch[1] : " ";
56
+ const importLine = 'import { registerGlasstrace } from "@glasstrace/sdk";\n';
57
+ const hasGlasstraceImport = content.includes("@glasstrace/sdk");
58
+ const insertPoint = match.index + match[0].length;
59
+ const callInjection = `
60
+ ${indent}// Glasstrace must be registered before other instrumentation
61
+ ${indent}registerGlasstrace();
62
+ `;
63
+ let modified;
64
+ if (hasGlasstraceImport) {
65
+ const importRegex = /import\s*\{([^}]+)\}\s*from\s*["']@glasstrace\/sdk["']/;
66
+ const importMatch = importRegex.exec(content);
67
+ if (importMatch) {
68
+ const specifiers = importMatch[1];
69
+ const alreadyImported = specifiers.split(",").some((s) => s.trim() === "registerGlasstrace");
70
+ if (alreadyImported) {
71
+ modified = content.slice(0, insertPoint) + callInjection + content.slice(insertPoint);
72
+ } else {
73
+ const existingImports = specifiers.trimEnd();
74
+ const separator = existingImports.endsWith(",") ? " " : ", ";
75
+ const updatedImport = `import {${existingImports}${separator}registerGlasstrace} from "@glasstrace/sdk"`;
76
+ modified = content.replace(importMatch[0], updatedImport);
77
+ const newMatch = registerFnRegex.exec(modified);
78
+ if (newMatch) {
79
+ const newInsertPoint = newMatch.index + newMatch[0].length;
80
+ modified = modified.slice(0, newInsertPoint) + callInjection + modified.slice(newInsertPoint);
81
+ }
82
+ }
83
+ } else {
84
+ modified = importLine + content;
85
+ const newMatch = registerFnRegex.exec(modified);
86
+ if (newMatch) {
87
+ const newInsertPoint = newMatch.index + newMatch[0].length;
88
+ modified = modified.slice(0, newInsertPoint) + callInjection + modified.slice(newInsertPoint);
89
+ }
90
+ }
91
+ } else {
92
+ modified = importLine + content.slice(0, insertPoint) + callInjection + content.slice(insertPoint);
42
93
  }
43
- const content = `import { registerGlasstrace } from "@glasstrace/sdk";
94
+ return { injected: true, content: modified };
95
+ }
96
+ async function scaffoldInstrumentation(projectRoot) {
97
+ const filePath = path.join(projectRoot, "instrumentation.ts");
98
+ if (!fs.existsSync(filePath)) {
99
+ const content = `import { registerGlasstrace } from "@glasstrace/sdk";
44
100
 
45
101
  export async function register() {
46
102
  // Glasstrace must be registered before Prisma instrumentation
@@ -49,8 +105,19 @@ export async function register() {
49
105
  registerGlasstrace();
50
106
  }
51
107
  `;
52
- fs.writeFileSync(filePath, content, "utf-8");
53
- return true;
108
+ fs.writeFileSync(filePath, content, "utf-8");
109
+ return { action: "created" };
110
+ }
111
+ const existing = fs.readFileSync(filePath, "utf-8");
112
+ if (hasRegisterGlasstraceCall(existing)) {
113
+ return { action: "already-registered" };
114
+ }
115
+ const result = injectRegisterGlasstrace(existing);
116
+ if (result.injected) {
117
+ fs.writeFileSync(filePath, result.content, "utf-8");
118
+ return { action: "injected" };
119
+ }
120
+ return { action: "unrecognized" };
54
121
  }
55
122
  async function scaffoldNextConfig(projectRoot) {
56
123
  let configPath;
@@ -64,31 +131,34 @@ async function scaffoldNextConfig(projectRoot) {
64
131
  }
65
132
  }
66
133
  if (configPath === void 0 || configName === void 0) {
67
- return false;
134
+ return null;
68
135
  }
69
136
  const existing = fs.readFileSync(configPath, "utf-8");
137
+ if (existing.trim().length === 0) {
138
+ return { modified: false, reason: "empty-file" };
139
+ }
70
140
  if (existing.includes("withGlasstraceConfig")) {
71
- return false;
141
+ return { modified: false, reason: "already-wrapped" };
72
142
  }
73
143
  const isESM = configName.endsWith(".ts") || configName.endsWith(".mjs");
74
144
  if (isESM) {
75
145
  const importLine = 'import { withGlasstraceConfig } from "@glasstrace/sdk";\n';
76
146
  const wrapResult2 = wrapExport(existing);
77
147
  if (!wrapResult2.wrapped) {
78
- return false;
148
+ return { modified: false, reason: "no-export" };
79
149
  }
80
150
  const modified2 = importLine + "\n" + wrapResult2.content;
81
151
  fs.writeFileSync(configPath, modified2, "utf-8");
82
- return true;
152
+ return { modified: true };
83
153
  }
84
154
  const requireLine = 'const { withGlasstraceConfig } = require("@glasstrace/sdk");\n';
85
155
  const wrapResult = wrapCJSExport(existing);
86
156
  if (!wrapResult.wrapped) {
87
- return false;
157
+ return { modified: false, reason: "no-export" };
88
158
  }
89
159
  const modified = requireLine + "\n" + wrapResult.content;
90
160
  fs.writeFileSync(configPath, modified, "utf-8");
91
- return true;
161
+ return { modified: true };
92
162
  }
93
163
  function wrapExport(content) {
94
164
  const marker = "export default";
@@ -14608,6 +14678,14 @@ var init_zod = __esm({
14608
14678
  });
14609
14679
 
14610
14680
  // ../protocol/dist/index.js
14681
+ function randomHex(byteCount) {
14682
+ const bytes = new Uint8Array(byteCount);
14683
+ crypto.getRandomValues(bytes);
14684
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
14685
+ }
14686
+ function createAnonApiKey() {
14687
+ return AnonApiKeySchema.parse(`gt_anon_${randomHex(24)}`);
14688
+ }
14611
14689
  function createBuildHash(hash2) {
14612
14690
  return BuildHashSchema.parse(hash2);
14613
14691
  }
@@ -14761,6 +14839,44 @@ async function readAnonKey(projectRoot) {
14761
14839
  }
14762
14840
  return null;
14763
14841
  }
14842
+ async function getOrCreateAnonKey(projectRoot) {
14843
+ const root = projectRoot ?? process.cwd();
14844
+ const dirPath = (0, import_node_path.join)(root, GLASSTRACE_DIR);
14845
+ const keyPath = (0, import_node_path.join)(dirPath, ANON_KEY_FILE);
14846
+ const existingKey = await readAnonKey(root);
14847
+ if (existingKey !== null) {
14848
+ return existingKey;
14849
+ }
14850
+ const cached2 = ephemeralKeyCache.get(root);
14851
+ if (cached2 !== void 0) {
14852
+ return cached2;
14853
+ }
14854
+ const newKey = createAnonApiKey();
14855
+ try {
14856
+ await (0, import_promises.mkdir)(dirPath, { recursive: true, mode: 448 });
14857
+ await (0, import_promises.writeFile)(keyPath, newKey, { flag: "wx", mode: 384 });
14858
+ return newKey;
14859
+ } catch (err) {
14860
+ const code = err.code;
14861
+ if (code === "EEXIST") {
14862
+ const winnerKey = await readAnonKey(root);
14863
+ if (winnerKey !== null) {
14864
+ return winnerKey;
14865
+ }
14866
+ try {
14867
+ await (0, import_promises.writeFile)(keyPath, newKey, { mode: 384 });
14868
+ await (0, import_promises.chmod)(keyPath, 384);
14869
+ return newKey;
14870
+ } catch {
14871
+ }
14872
+ }
14873
+ ephemeralKeyCache.set(root, newKey);
14874
+ console.warn(
14875
+ `[glasstrace] Failed to persist anonymous key to ${keyPath}: ${err instanceof Error ? err.message : String(err)}. Using ephemeral key.`
14876
+ );
14877
+ return newKey;
14878
+ }
14879
+ }
14764
14880
  var import_promises, import_node_path, GLASSTRACE_DIR, ANON_KEY_FILE, ephemeralKeyCache;
14765
14881
  var init_anon_key = __esm({
14766
14882
  "src/anon-key.ts"() {
@@ -15052,8 +15168,11 @@ function generateInfoSection(agent, endpoint) {
15052
15168
  `Glasstrace is configured as an MCP server at: ${endpoint}`,
15053
15169
  "",
15054
15170
  "Available tools:",
15055
- "- `glasstrace_submit_trace` - Submit trace data for debugging analysis",
15056
- "- `glasstrace_get_config` - Retrieve current SDK configuration",
15171
+ "- `get_latest_error` - Get the most recent error trace from the current session",
15172
+ "- `get_trace` - Get a specific trace by ID or URL pattern",
15173
+ "- `get_root_cause` - Get the full span tree and root cause analysis for an error",
15174
+ "- `get_test_suggestions` - Get test suggestions based on recent errors",
15175
+ "- `get_session_timeline` - Get the timeline of all traces in the current session",
15057
15176
  "",
15058
15177
  "To reconfigure, run: `npx glasstrace mcp add`",
15059
15178
  ""
@@ -15525,6 +15644,7 @@ var init_mcp_add = __esm({
15525
15644
  // src/cli/init.ts
15526
15645
  var init_exports = {};
15527
15646
  __export(init_exports, {
15647
+ meetsNodeVersion: () => meetsNodeVersion,
15528
15648
  runInit: () => runInit
15529
15649
  });
15530
15650
  module.exports = __toCommonJS(init_exports);
@@ -15708,6 +15828,10 @@ init_detect();
15708
15828
  init_configs();
15709
15829
  init_inject();
15710
15830
  init_constants();
15831
+ function meetsNodeVersion(minMajor) {
15832
+ const [major] = process.versions.node.split(".").map(Number);
15833
+ return major >= minMajor;
15834
+ }
15711
15835
  async function promptYesNo(question, defaultValue) {
15712
15836
  if (!process.stdin.isTTY) {
15713
15837
  return defaultValue;
@@ -15739,41 +15863,40 @@ async function runInit(options) {
15739
15863
  errors.push("No package.json found. Run this command from a Node.js project root.");
15740
15864
  return { exitCode: 1, summary, warnings, errors };
15741
15865
  }
15742
- const instrumentationPath = path4.join(projectRoot, "instrumentation.ts");
15743
- const instrumentationExists = fs4.existsSync(instrumentationPath);
15744
- let shouldWriteInstrumentation = true;
15745
- if (instrumentationExists && !yes) {
15746
- shouldWriteInstrumentation = await promptYesNo(
15747
- "instrumentation.ts already exists. Overwrite?",
15748
- false
15749
- );
15750
- } else if (instrumentationExists && yes) {
15751
- shouldWriteInstrumentation = false;
15752
- }
15753
15866
  try {
15754
- const created = await scaffoldInstrumentation(projectRoot, shouldWriteInstrumentation && instrumentationExists);
15755
- if (created) {
15756
- summary.push("Created instrumentation.ts");
15757
- } else if (instrumentationExists) {
15758
- summary.push("Skipped instrumentation.ts (already exists)");
15867
+ const instrResult = await scaffoldInstrumentation(projectRoot);
15868
+ switch (instrResult.action) {
15869
+ case "created":
15870
+ summary.push("Created instrumentation.ts");
15871
+ break;
15872
+ case "injected":
15873
+ summary.push("Added registerGlasstrace() to existing instrumentation.ts");
15874
+ break;
15875
+ case "already-registered":
15876
+ summary.push("Skipped instrumentation.ts (registerGlasstrace already present)");
15877
+ break;
15878
+ case "unrecognized":
15879
+ warnings.push(
15880
+ 'instrumentation.ts exists but has no recognizable register() function.\nAdd this import at the top of your file:\n\n import { registerGlasstrace } from "@glasstrace/sdk";\n\nThen add this as the first statement in your register() function:\n\n registerGlasstrace();\n'
15881
+ );
15882
+ break;
15759
15883
  }
15760
15884
  } catch (err) {
15761
15885
  errors.push(`Failed to write instrumentation.ts: ${err instanceof Error ? err.message : String(err)}`);
15762
15886
  return { exitCode: 1, summary, warnings, errors };
15763
15887
  }
15764
15888
  try {
15765
- const wrapped = await scaffoldNextConfig(projectRoot);
15766
- if (wrapped) {
15889
+ const configResult = await scaffoldNextConfig(projectRoot);
15890
+ if (configResult?.modified) {
15767
15891
  summary.push("Wrapped next.config with withGlasstraceConfig()");
15892
+ } else if (configResult === null) {
15893
+ warnings.push("No next.config.* found. You may need to create one for Next.js projects.");
15894
+ } else if (configResult.reason === "already-wrapped") {
15895
+ summary.push("Skipped next.config (already contains withGlasstraceConfig)");
15896
+ } else if (configResult.reason === "empty-file") {
15897
+ warnings.push("next.config is empty \u2014 add a Next.js configuration export to enable wrapping");
15768
15898
  } else {
15769
- const hasNextConfig = ["next.config.ts", "next.config.js", "next.config.mjs"].some(
15770
- (name) => fs4.existsSync(path4.join(projectRoot, name))
15771
- );
15772
- if (hasNextConfig) {
15773
- summary.push("Skipped next.config (already contains withGlasstraceConfig)");
15774
- } else {
15775
- warnings.push("No next.config.* found. You may need to create one for Next.js projects.");
15776
- }
15899
+ warnings.push("next.config has no recognizable export pattern \u2014 add withGlasstraceConfig() manually");
15777
15900
  }
15778
15901
  } catch (err) {
15779
15902
  errors.push(`Failed to modify next.config: ${err instanceof Error ? err.message : String(err)}`);
@@ -15804,10 +15927,30 @@ async function runInit(options) {
15804
15927
  const ciEnv = process.env["CI"];
15805
15928
  const isCI = typeof ciEnv === "string" && ciEnv.trim() !== "" && ciEnv.toLowerCase() !== "false" && ciEnv.trim() !== "0" || process.env["GITHUB_ACTIONS"] === "true";
15806
15929
  try {
15807
- const anonKey = await readAnonKey(projectRoot);
15808
- if (anonKey !== null) {
15809
- let anyConfigWritten = false;
15810
- if (isCI) {
15930
+ const anonKey = await getOrCreateAnonKey(projectRoot);
15931
+ let anyConfigWritten = false;
15932
+ if (isCI) {
15933
+ const genericAgent = {
15934
+ name: "generic",
15935
+ mcpConfigPath: path4.join(projectRoot, ".glasstrace", "mcp.json"),
15936
+ infoFilePath: null,
15937
+ cliAvailable: false,
15938
+ registrationCommand: null
15939
+ };
15940
+ const genericConfig = generateMcpConfig(genericAgent, MCP_ENDPOINT, anonKey);
15941
+ await writeMcpConfig(genericAgent, genericConfig, projectRoot);
15942
+ if (genericAgent.mcpConfigPath !== null && fs4.existsSync(genericAgent.mcpConfigPath)) {
15943
+ anyConfigWritten = true;
15944
+ summary.push("Created .glasstrace/mcp.json (CI mode)");
15945
+ }
15946
+ } else {
15947
+ let agents;
15948
+ try {
15949
+ agents = await detectAgents(projectRoot);
15950
+ } catch (detectErr) {
15951
+ warnings.push(
15952
+ `Agent detection failed: ${detectErr instanceof Error ? detectErr.message : String(detectErr)}. Writing generic config only.`
15953
+ );
15811
15954
  const genericAgent = {
15812
15955
  name: "generic",
15813
15956
  mcpConfigPath: path4.join(projectRoot, ".glasstrace", "mcp.json"),
@@ -15819,71 +15962,47 @@ async function runInit(options) {
15819
15962
  await writeMcpConfig(genericAgent, genericConfig, projectRoot);
15820
15963
  if (genericAgent.mcpConfigPath !== null && fs4.existsSync(genericAgent.mcpConfigPath)) {
15821
15964
  anyConfigWritten = true;
15822
- summary.push("Created .glasstrace/mcp.json (CI mode)");
15823
15965
  }
15824
- } else {
15825
- let agents;
15966
+ agents = [];
15967
+ }
15968
+ const configuredNames = [];
15969
+ for (const agent of agents) {
15826
15970
  try {
15827
- agents = await detectAgents(projectRoot);
15828
- } catch (detectErr) {
15971
+ const configContent = generateMcpConfig(agent, MCP_ENDPOINT, anonKey);
15972
+ await writeMcpConfig(agent, configContent, projectRoot);
15973
+ const configExists = agent.mcpConfigPath !== null && fs4.existsSync(agent.mcpConfigPath);
15974
+ if (!configExists) {
15975
+ continue;
15976
+ }
15977
+ anyConfigWritten = true;
15978
+ const infoContent = generateInfoSection(agent, MCP_ENDPOINT);
15979
+ if (infoContent !== "") {
15980
+ await injectInfoSection(agent, infoContent, projectRoot);
15981
+ }
15982
+ if (agent.name !== "generic") {
15983
+ configuredNames.push(formatAgentName(agent.name));
15984
+ }
15985
+ } catch (agentErr) {
15829
15986
  warnings.push(
15830
- `Agent detection failed: ${detectErr instanceof Error ? detectErr.message : String(detectErr)}. Writing generic config only.`
15987
+ `Failed to configure MCP for ${agent.name}: ${agentErr instanceof Error ? agentErr.message : String(agentErr)}`
15831
15988
  );
15832
- const genericAgent = {
15833
- name: "generic",
15834
- mcpConfigPath: path4.join(projectRoot, ".glasstrace", "mcp.json"),
15835
- infoFilePath: null,
15836
- cliAvailable: false,
15837
- registrationCommand: null
15838
- };
15839
- const genericConfig = generateMcpConfig(genericAgent, MCP_ENDPOINT, anonKey);
15840
- await writeMcpConfig(genericAgent, genericConfig, projectRoot);
15841
- if (genericAgent.mcpConfigPath !== null && fs4.existsSync(genericAgent.mcpConfigPath)) {
15842
- anyConfigWritten = true;
15843
- }
15844
- agents = [];
15845
- }
15846
- const configuredNames = [];
15847
- for (const agent of agents) {
15848
- try {
15849
- const configContent = generateMcpConfig(agent, MCP_ENDPOINT, anonKey);
15850
- await writeMcpConfig(agent, configContent, projectRoot);
15851
- const configExists = agent.mcpConfigPath !== null && fs4.existsSync(agent.mcpConfigPath);
15852
- if (!configExists) {
15853
- continue;
15854
- }
15855
- anyConfigWritten = true;
15856
- const infoContent = generateInfoSection(agent, MCP_ENDPOINT);
15857
- if (infoContent !== "") {
15858
- await injectInfoSection(agent, infoContent, projectRoot);
15859
- }
15860
- if (agent.name !== "generic") {
15861
- configuredNames.push(formatAgentName(agent.name));
15862
- }
15863
- } catch (agentErr) {
15864
- warnings.push(
15865
- `Failed to configure MCP for ${agent.name}: ${agentErr instanceof Error ? agentErr.message : String(agentErr)}`
15866
- );
15867
- }
15868
- }
15869
- if (configuredNames.length > 0) {
15870
- summary.push(`Configured MCP for: ${configuredNames.join(", ")}`);
15871
- } else if (anyConfigWritten) {
15872
- summary.push("Created .glasstrace/mcp.json (generic config)");
15873
15989
  }
15874
15990
  }
15875
- await updateGitignore(
15876
- [".mcp.json", ".cursor/mcp.json", ".gemini/settings.json", ".codex/config.toml"],
15877
- projectRoot
15878
- );
15879
- if (anyConfigWritten) {
15880
- const markerCreated = await scaffoldMcpMarker(projectRoot, anonKey);
15881
- if (markerCreated) {
15882
- summary.push("Created .glasstrace/mcp-connected marker");
15883
- }
15991
+ if (configuredNames.length > 0) {
15992
+ summary.push(`Configured MCP for: ${configuredNames.join(", ")}`);
15993
+ } else if (anyConfigWritten) {
15994
+ summary.push("Created .glasstrace/mcp.json (generic config)");
15995
+ }
15996
+ }
15997
+ await updateGitignore(
15998
+ [".mcp.json", ".cursor/mcp.json", ".gemini/settings.json", ".codex/config.toml"],
15999
+ projectRoot
16000
+ );
16001
+ if (anyConfigWritten) {
16002
+ const markerCreated = await scaffoldMcpMarker(projectRoot, anonKey);
16003
+ if (markerCreated) {
16004
+ summary.push("Created .glasstrace/mcp-connected marker");
15884
16005
  }
15885
- } else {
15886
- warnings.push("No anonymous key found. Skipping MCP auto-configuration. Run init again after the SDK has generated a key.");
15887
16006
  }
15888
16007
  } catch (mcpErr) {
15889
16008
  warnings.push(
@@ -15941,6 +16060,13 @@ var scriptPath = typeof process !== "undefined" && process.argv[1] !== void 0 ?
15941
16060
  var scriptBasename = scriptPath !== void 0 ? path4.basename(scriptPath) : void 0;
15942
16061
  var isDirectExecution = scriptPath !== void 0 && (scriptPath.endsWith("/cli/init.js") || scriptPath.endsWith("/cli/init.ts") || scriptBasename === "glasstrace");
15943
16062
  if (isDirectExecution) {
16063
+ if (!meetsNodeVersion(20)) {
16064
+ process.stderr.write(
16065
+ `Error: @glasstrace/sdk requires Node.js >= 20. Current version: ${process.version}
16066
+ `
16067
+ );
16068
+ process.exit(1);
16069
+ }
15944
16070
  const subcommand = process.argv[2];
15945
16071
  if (subcommand === "mcp") {
15946
16072
  if (process.argv[3] === "add") {
@@ -16020,6 +16146,7 @@ Usage:
16020
16146
  }
16021
16147
  // Annotate the CommonJS export names for ESM import in node:
16022
16148
  0 && (module.exports = {
16149
+ meetsNodeVersion,
16023
16150
  runInit
16024
16151
  });
16025
16152
  //# sourceMappingURL=init.cjs.map