@glasstrace/sdk 1.1.2 → 1.1.3
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-Z35HKVSO.js → chunk-FGDS33I2.js} +10 -11
- package/dist/{chunk-Z35HKVSO.js.map → chunk-FGDS33I2.js.map} +1 -1
- package/dist/{chunk-C567H5EQ.js → chunk-JKI4OCFV.js} +4 -14
- package/dist/chunk-JKI4OCFV.js.map +1 -0
- package/dist/{chunk-UJ2JC7PZ.js → chunk-TWHCJKRS.js} +17 -16
- package/dist/chunk-TWHCJKRS.js.map +1 -0
- package/dist/{chunk-3LILTM3T.js → chunk-TWTWRJ25.js} +233 -9
- package/dist/chunk-TWTWRJ25.js.map +1 -0
- package/dist/cli/init.cjs +156 -17
- package/dist/cli/init.cjs.map +1 -1
- package/dist/cli/init.js +4 -4
- package/dist/cli/mcp-add.cjs.map +1 -1
- package/dist/cli/mcp-add.js +1 -1
- package/dist/cli/uninit.cjs +113 -11
- package/dist/cli/uninit.cjs.map +1 -1
- package/dist/cli/uninit.js +2 -2
- package/dist/cli/validate.cjs.map +1 -1
- package/dist/cli/validate.js +1 -1
- package/dist/index.cjs +209 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -9
- package/dist/index.d.ts +12 -9
- package/dist/index.js +3 -3
- package/dist/node-entry.cjs +209 -27
- package/dist/node-entry.cjs.map +1 -1
- package/dist/node-entry.js +3 -3
- package/package.json +1 -1
- package/dist/chunk-3LILTM3T.js.map +0 -1
- package/dist/chunk-C567H5EQ.js.map +0 -1
- package/dist/chunk-UJ2JC7PZ.js.map +0 -1
|
@@ -3,6 +3,9 @@ import {
|
|
|
3
3
|
DevApiKeySchema,
|
|
4
4
|
createAnonApiKey
|
|
5
5
|
} from "./chunk-X5MAXP5T.js";
|
|
6
|
+
import {
|
|
7
|
+
__require
|
|
8
|
+
} from "./chunk-NSBPE2FW.js";
|
|
6
9
|
|
|
7
10
|
// src/anon-key.ts
|
|
8
11
|
var GLASSTRACE_DIR = ".glasstrace";
|
|
@@ -109,6 +112,229 @@ async function getOrCreateAnonKey(projectRoot) {
|
|
|
109
112
|
}
|
|
110
113
|
}
|
|
111
114
|
|
|
115
|
+
// src/atomic-write.ts
|
|
116
|
+
function parentDir(filePath) {
|
|
117
|
+
const lastSlash = filePath.lastIndexOf("/");
|
|
118
|
+
const lastBackslash = filePath.lastIndexOf("\\");
|
|
119
|
+
const lastSep = Math.max(lastSlash, lastBackslash);
|
|
120
|
+
if (lastSep < 0) return ".";
|
|
121
|
+
if (lastSep === 0) return filePath.slice(0, 1);
|
|
122
|
+
return filePath.slice(0, lastSep);
|
|
123
|
+
}
|
|
124
|
+
var PARENT_FSYNC_SWALLOWED_CODES = /* @__PURE__ */ new Set([
|
|
125
|
+
"EISDIR",
|
|
126
|
+
"EINVAL",
|
|
127
|
+
"EPERM",
|
|
128
|
+
"ENOTSUP"
|
|
129
|
+
]);
|
|
130
|
+
function errnoCodeOf(err) {
|
|
131
|
+
if (err === null || typeof err !== "object") return void 0;
|
|
132
|
+
const code = err.code;
|
|
133
|
+
return typeof code === "string" ? code : void 0;
|
|
134
|
+
}
|
|
135
|
+
function defaultTmpPath(targetPath) {
|
|
136
|
+
return `${targetPath}.tmp`;
|
|
137
|
+
}
|
|
138
|
+
var fsPromisesCache;
|
|
139
|
+
var fsSyncCache;
|
|
140
|
+
async function loadFsPromises() {
|
|
141
|
+
if (fsPromisesCache !== void 0) {
|
|
142
|
+
if (fsPromisesCache === null) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
"node:fs/promises is unavailable in this environment; atomicWriteFile cannot be used here."
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return fsPromisesCache;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
fsPromisesCache = await import("node:fs/promises");
|
|
151
|
+
return fsPromisesCache;
|
|
152
|
+
} catch {
|
|
153
|
+
fsPromisesCache = null;
|
|
154
|
+
throw new Error(
|
|
155
|
+
"node:fs/promises is unavailable in this environment; atomicWriteFile cannot be used here."
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function loadFsSync() {
|
|
160
|
+
if (fsSyncCache !== void 0) {
|
|
161
|
+
if (fsSyncCache === null) {
|
|
162
|
+
throw new Error(
|
|
163
|
+
"node:fs is unavailable in this environment; atomicWriteFileSync cannot be used here."
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
return fsSyncCache;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
fsSyncCache = __require("node:fs");
|
|
170
|
+
return fsSyncCache;
|
|
171
|
+
} catch {
|
|
172
|
+
fsSyncCache = null;
|
|
173
|
+
throw new Error(
|
|
174
|
+
"node:fs is unavailable in this environment; atomicWriteFileSync cannot be used here."
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function atomicWriteFile(targetPath, payload, options = {}) {
|
|
179
|
+
return atomicWriteFileWithTmp(targetPath, defaultTmpPath(targetPath), payload, options);
|
|
180
|
+
}
|
|
181
|
+
async function atomicWriteFileWithTmp(targetPath, tmpPath, payload, options = {}) {
|
|
182
|
+
const mode = options.mode ?? 384;
|
|
183
|
+
const encoding = options.encoding ?? "utf-8";
|
|
184
|
+
const fsp = await loadFsPromises();
|
|
185
|
+
let handle = null;
|
|
186
|
+
try {
|
|
187
|
+
if (typeof payload === "string") {
|
|
188
|
+
await fsp.writeFile(tmpPath, payload, { encoding, mode });
|
|
189
|
+
} else {
|
|
190
|
+
await fsp.writeFile(tmpPath, payload, { mode });
|
|
191
|
+
}
|
|
192
|
+
await fsp.chmod(tmpPath, mode);
|
|
193
|
+
handle = await fsp.open(tmpPath, "r");
|
|
194
|
+
await handle.sync();
|
|
195
|
+
await handle.close();
|
|
196
|
+
handle = null;
|
|
197
|
+
await fsp.rename(tmpPath, targetPath);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
if (handle !== null) {
|
|
200
|
+
try {
|
|
201
|
+
await handle.close();
|
|
202
|
+
} catch {
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
await removeTmpResidueAsync(fsp, tmpPath);
|
|
206
|
+
throw err;
|
|
207
|
+
}
|
|
208
|
+
await fsyncParentDirAsync(targetPath, fsp);
|
|
209
|
+
}
|
|
210
|
+
async function removeTmpResidueAsync(fsp, tmpPath) {
|
|
211
|
+
try {
|
|
212
|
+
await fsp.unlink(tmpPath);
|
|
213
|
+
return;
|
|
214
|
+
} catch (err) {
|
|
215
|
+
const code = errnoCodeOf(err);
|
|
216
|
+
if (code !== "EISDIR" && code !== "EPERM") {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
await fsp.rmdir(tmpPath);
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async function fsyncParentDirAsync(targetPath, fsp) {
|
|
226
|
+
const parent = parentDir(targetPath);
|
|
227
|
+
let handle = null;
|
|
228
|
+
try {
|
|
229
|
+
handle = await fsp.open(parent, "r");
|
|
230
|
+
await handle.sync();
|
|
231
|
+
} catch (err) {
|
|
232
|
+
const code = errnoCodeOf(err);
|
|
233
|
+
if (code !== void 0 && PARENT_FSYNC_SWALLOWED_CODES.has(code)) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
throw err;
|
|
237
|
+
} finally {
|
|
238
|
+
if (handle !== null) {
|
|
239
|
+
try {
|
|
240
|
+
await handle.close();
|
|
241
|
+
} catch {
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function atomicWriteFileSync(targetPath, payload, options = {}) {
|
|
247
|
+
atomicWriteFileSyncWithTmp(targetPath, defaultTmpPath(targetPath), payload, options);
|
|
248
|
+
}
|
|
249
|
+
function atomicWriteFileSyncWithTmp(targetPath, tmpPath, payload, options = {}) {
|
|
250
|
+
const mode = options.mode ?? 384;
|
|
251
|
+
const encoding = options.encoding ?? "utf-8";
|
|
252
|
+
const fs = loadFsSync();
|
|
253
|
+
let fd = null;
|
|
254
|
+
try {
|
|
255
|
+
if (typeof payload === "string") {
|
|
256
|
+
fs.writeFileSync(tmpPath, payload, { encoding, mode });
|
|
257
|
+
} else {
|
|
258
|
+
fs.writeFileSync(tmpPath, payload, { mode });
|
|
259
|
+
}
|
|
260
|
+
fs.chmodSync(tmpPath, mode);
|
|
261
|
+
fd = fs.openSync(tmpPath, "r");
|
|
262
|
+
fs.fsyncSync(fd);
|
|
263
|
+
fs.closeSync(fd);
|
|
264
|
+
fd = null;
|
|
265
|
+
fs.renameSync(tmpPath, targetPath);
|
|
266
|
+
} catch (err) {
|
|
267
|
+
if (fd !== null) {
|
|
268
|
+
try {
|
|
269
|
+
fs.closeSync(fd);
|
|
270
|
+
} catch {
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
removeTmpResidueSync(fs, tmpPath);
|
|
274
|
+
throw err;
|
|
275
|
+
}
|
|
276
|
+
fsyncParentDirSyncWithFs(targetPath, fs);
|
|
277
|
+
}
|
|
278
|
+
function removeTmpResidueSync(fs, tmpPath) {
|
|
279
|
+
try {
|
|
280
|
+
fs.unlinkSync(tmpPath);
|
|
281
|
+
return;
|
|
282
|
+
} catch (err) {
|
|
283
|
+
const code = errnoCodeOf(err);
|
|
284
|
+
if (code !== "EISDIR" && code !== "EPERM") {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
fs.rmdirSync(tmpPath);
|
|
290
|
+
} catch {
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function fsyncParentDirSync(targetPath) {
|
|
294
|
+
fsyncParentDirSyncWithFs(targetPath, loadFsSync());
|
|
295
|
+
}
|
|
296
|
+
function fsyncParentDirSyncWithFs(targetPath, fs) {
|
|
297
|
+
const parent = parentDir(targetPath);
|
|
298
|
+
let fd = null;
|
|
299
|
+
try {
|
|
300
|
+
fd = fs.openSync(parent, "r");
|
|
301
|
+
fs.fsyncSync(fd);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
const code = errnoCodeOf(err);
|
|
304
|
+
if (code !== void 0 && PARENT_FSYNC_SWALLOWED_CODES.has(code)) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
throw err;
|
|
308
|
+
} finally {
|
|
309
|
+
if (fd !== null) {
|
|
310
|
+
try {
|
|
311
|
+
fs.closeSync(fd);
|
|
312
|
+
} catch {
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function writeAndFsyncTempSync(tmpPath, payload, options = {}) {
|
|
318
|
+
const mode = options.mode ?? 384;
|
|
319
|
+
const encoding = options.encoding ?? "utf-8";
|
|
320
|
+
const fs = loadFsSync();
|
|
321
|
+
if (typeof payload === "string") {
|
|
322
|
+
fs.writeFileSync(tmpPath, payload, { encoding, mode });
|
|
323
|
+
} else {
|
|
324
|
+
fs.writeFileSync(tmpPath, payload, { mode });
|
|
325
|
+
}
|
|
326
|
+
fs.chmodSync(tmpPath, mode);
|
|
327
|
+
const fd = fs.openSync(tmpPath, "r");
|
|
328
|
+
try {
|
|
329
|
+
fs.fsyncSync(fd);
|
|
330
|
+
} finally {
|
|
331
|
+
try {
|
|
332
|
+
fs.closeSync(fd);
|
|
333
|
+
} catch {
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
112
338
|
// src/mcp-runtime.ts
|
|
113
339
|
import { createHash } from "node:crypto";
|
|
114
340
|
var MCP_ENDPOINT = "https://api.glasstrace.dev/mcp";
|
|
@@ -332,7 +558,6 @@ async function refreshGenericMcpConfigAtRuntime(projectRoot, effective, anonKeyO
|
|
|
332
558
|
if (!modules) return { action: "absent" };
|
|
333
559
|
const dirPath = modules.path.join(projectRoot, GLASSTRACE_DIR2);
|
|
334
560
|
const configPath = modules.path.join(dirPath, MCP_CONFIG_FILE);
|
|
335
|
-
const tmpPath = configPath + ".tmp";
|
|
336
561
|
let existing;
|
|
337
562
|
try {
|
|
338
563
|
existing = await modules.fs.readFile(configPath, "utf-8");
|
|
@@ -349,18 +574,12 @@ async function refreshGenericMcpConfigAtRuntime(projectRoot, effective, anonKeyO
|
|
|
349
574
|
}
|
|
350
575
|
const replacement = genericMcpConfigContent(MCP_ENDPOINT, effective.key);
|
|
351
576
|
try {
|
|
352
|
-
await
|
|
353
|
-
await modules.fs.chmod(tmpPath, 384);
|
|
354
|
-
await modules.fs.rename(tmpPath, configPath);
|
|
577
|
+
await atomicWriteFile(configPath, replacement, { mode: 384 });
|
|
355
578
|
await writeMcpMarker(projectRoot, {
|
|
356
579
|
credentialSource: effective.source,
|
|
357
580
|
credentialHash: identityFingerprint(effective.key)
|
|
358
581
|
});
|
|
359
582
|
} catch {
|
|
360
|
-
try {
|
|
361
|
-
await modules.fs.unlink(tmpPath);
|
|
362
|
-
} catch {
|
|
363
|
-
}
|
|
364
583
|
return { action: "preserved" };
|
|
365
584
|
}
|
|
366
585
|
emitRefreshNudge(effective.source);
|
|
@@ -370,6 +589,11 @@ async function refreshGenericMcpConfigAtRuntime(projectRoot, effective, anonKeyO
|
|
|
370
589
|
export {
|
|
371
590
|
readAnonKey,
|
|
372
591
|
getOrCreateAnonKey,
|
|
592
|
+
atomicWriteFile,
|
|
593
|
+
atomicWriteFileSync,
|
|
594
|
+
atomicWriteFileSyncWithTmp,
|
|
595
|
+
fsyncParentDirSync,
|
|
596
|
+
writeAndFsyncTempSync,
|
|
373
597
|
MCP_ENDPOINT,
|
|
374
598
|
identityFingerprint,
|
|
375
599
|
mcpConfigMatches,
|
|
@@ -381,4 +605,4 @@ export {
|
|
|
381
605
|
writeMcpMarker,
|
|
382
606
|
refreshGenericMcpConfigAtRuntime
|
|
383
607
|
};
|
|
384
|
-
//# sourceMappingURL=chunk-
|
|
608
|
+
//# sourceMappingURL=chunk-TWTWRJ25.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/anon-key.ts","../src/atomic-write.ts","../src/mcp-runtime.ts"],"sourcesContent":["import { AnonApiKeySchema, DevApiKeySchema, createAnonApiKey } from \"@glasstrace/protocol\";\nimport type { AnonApiKey, DevApiKey } from \"@glasstrace/protocol\";\n\nconst GLASSTRACE_DIR = \".glasstrace\";\nconst ANON_KEY_FILE = \"anon_key\";\nconst CLAIMED_KEY_FILE = \"claimed-key\";\n\n/**\n * Lazily imports `node:fs/promises` and `node:path`. Returns `null` if\n * the modules are unavailable (non-Node environments). The result is\n * cached after first resolution.\n */\nlet fsPathCache: { fs: typeof import(\"node:fs/promises\"); path: typeof import(\"node:path\") } | null | undefined;\n\nasync function loadFsPath(): Promise<{ fs: typeof import(\"node:fs/promises\"); path: typeof import(\"node:path\") } | null> {\n if (fsPathCache !== undefined) return fsPathCache;\n try {\n const [fs, path] = await Promise.all([\n import(\"node:fs/promises\"),\n import(\"node:path\"),\n ]);\n fsPathCache = { fs, path };\n return fsPathCache;\n } catch {\n fsPathCache = null;\n return null;\n }\n}\n\n/**\n * In-memory cache for ephemeral keys when filesystem persistence fails.\n * Keyed by resolved project root to support multiple roots in tests.\n */\nconst ephemeralKeyCache = new Map<string, AnonApiKey>();\n\n/**\n * Reads an existing anonymous key from the filesystem.\n * Returns the key if valid, or null if:\n * - The file does not exist\n * - The file content is invalid\n * - An I/O error occurs\n * - `node:fs` is unavailable (non-Node environment)\n */\nexport async function readAnonKey(projectRoot?: string): Promise<AnonApiKey | null> {\n const root = projectRoot ?? process.cwd();\n\n const modules = await loadFsPath();\n if (modules) {\n const keyPath = modules.path.join(root, GLASSTRACE_DIR, ANON_KEY_FILE);\n try {\n const content = await modules.fs.readFile(keyPath, \"utf-8\");\n const result = AnonApiKeySchema.safeParse(content);\n if (result.success) {\n return result.data;\n }\n } catch {\n // Fall through to check ephemeral cache\n }\n }\n\n // Check in-memory cache (used when filesystem persistence failed\n // or when node:fs is unavailable)\n const cached = ephemeralKeyCache.get(root);\n if (cached !== undefined) {\n return cached;\n }\n\n return null;\n}\n\n/**\n * Reads a claimed developer API key persisted at\n * `.glasstrace/claimed-key`. The file is the runtime fallback used by\n * {@link import(\"./init-client.js\").writeClaimedKey} when `.env.local`\n * is not writable.\n *\n * Returns the key when the file exists and its contents pass\n * `DevApiKeySchema` validation. Returns `null` when:\n * - The file does not exist.\n * - The file content fails strict schema validation (a malformed or\n * stale value cannot be distinguished from a valid one without a\n * server roundtrip — callers should treat this as \"no key\").\n * - An I/O error occurs.\n * - `node:fs` is unavailable (non-Node environment).\n */\nexport async function readClaimedKey(projectRoot?: string): Promise<DevApiKey | null> {\n const root = projectRoot ?? process.cwd();\n\n const modules = await loadFsPath();\n if (!modules) return null;\n\n const keyPath = modules.path.join(root, GLASSTRACE_DIR, CLAIMED_KEY_FILE);\n try {\n const content = await modules.fs.readFile(keyPath, \"utf-8\");\n const trimmed = content.trim();\n const parsed = DevApiKeySchema.safeParse(trimmed);\n if (parsed.success) {\n return parsed.data;\n }\n } catch {\n // Fall through — file missing, unreadable, or invalid; treat as absent.\n }\n\n return null;\n}\n\n/**\n * Gets an existing anonymous key from the filesystem, or creates a new one.\n *\n * - If file exists and contains a valid key, returns it\n * - If file does not exist or content is invalid, generates a new key via createAnonApiKey()\n * - Writes the new key to `.glasstrace/anon_key`, creating the directory if needed\n * - On file write failure: logs a warning, caches an ephemeral in-memory key so\n * repeated calls in the same process return the same key\n * - In non-Node environments: returns an ephemeral in-memory key\n */\nexport async function getOrCreateAnonKey(projectRoot?: string): Promise<AnonApiKey> {\n const root = projectRoot ?? process.cwd();\n\n // Try reading existing key from filesystem\n const existingKey = await readAnonKey(root);\n if (existingKey !== null) {\n return existingKey;\n }\n\n // Check in-memory cache (used when filesystem is unavailable)\n const cached = ephemeralKeyCache.get(root);\n if (cached !== undefined) {\n return cached;\n }\n\n // Generate a new key\n const newKey = createAnonApiKey();\n\n // Attempt filesystem persistence (only in Node.js environments)\n const modules = await loadFsPath();\n if (!modules) {\n // No filesystem access — cache in memory\n ephemeralKeyCache.set(root, newKey);\n return newKey;\n }\n\n const dirPath = modules.path.join(root, GLASSTRACE_DIR);\n const keyPath = modules.path.join(dirPath, ANON_KEY_FILE);\n\n // Persist to filesystem using atomic create-or-fail (O_CREAT | O_EXCL)\n // to prevent TOCTOU races where concurrent cold starts both generate keys.\n try {\n await modules.fs.mkdir(dirPath, { recursive: true, mode: 0o700 });\n await modules.fs.writeFile(keyPath, newKey, { flag: \"wx\", mode: 0o600 });\n return newKey;\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"EEXIST\") {\n // Another process won the race. Retry reading their key with\n // short delays — the winner's writeFile is atomic for small\n // payloads but the filesystem may not have flushed yet.\n for (let attempt = 0; attempt < 3; attempt++) {\n const winnerKey = await readAnonKey(root);\n if (winnerKey !== null) {\n return winnerKey;\n }\n // Short delay before next retry (50ms), skip after final attempt\n if (attempt < 2) {\n await new Promise((resolve) => setTimeout(resolve, 50));\n }\n }\n // All retries exhausted — overwrite as last resort.\n // Use explicit chmod after overwrite since writeFile mode only\n // applies on creation on some platforms.\n try {\n await modules.fs.writeFile(keyPath, newKey, { mode: 0o600 });\n await modules.fs.chmod(keyPath, 0o600);\n return newKey;\n } catch {\n // Overwrite failed — fall through to ephemeral cache\n }\n }\n\n // Non-EEXIST error (EACCES, ENOTDIR, etc.) — cache in memory so\n // repeated calls get the same ephemeral key within this process.\n ephemeralKeyCache.set(root, newKey);\n console.warn(\n `[glasstrace] Failed to persist anonymous key to ${keyPath}: ${err instanceof Error ? err.message : String(err)}. Using ephemeral key.`,\n );\n return newKey;\n }\n}\n","/**\n * Atomic file-write helper.\n *\n * Implements the durability half of the SDK 2.0 atomic-write protocol\n * (`docs/component-designs/sdk-2.0.md` §4.3 / `sdk-2.0.md:416–419`):\n *\n * 1. Write the payload to a sibling temp file in the **same**\n * directory as the final target. The shared directory guarantees\n * `rename(2)` stays on the same filesystem and therefore atomic\n * per POSIX semantics.\n * 2. **fsync the temp file.** Forces data and metadata to durable\n * storage before the rename is observable.\n * 3. **rename atomically into place.** Readers see either the old\n * file contents or the new ones, never a partial write.\n * 4. **fsync the parent directory.** On POSIX, `rename(2)` durability\n * is not guaranteed until the containing directory's own metadata\n * is synced. Without this step, a power loss between rename and\n * parent-dir sync can leave the rename invisible after reboot\n * (the kernel acknowledges the syscall but the metadata never\n * reached durable storage).\n *\n * Closes the durability gap that allowed DISC-494 (anon-key unlinked\n * silently on re-init under crash interleavings).\n *\n * Out-of-scope by design:\n * - The `lstat → tmp → rename → re-lstat` TOCTOU re-check (spec\n * §4.3 steps 1–2 and 7's re-verification) is SDK 2.0 scope per\n * `sdk-2.0.md:511`.\n * - The `GLASSTRACE_TEST_CRASH_AFTER` crash-injection harness is\n * SDK 2.0 scope per `sdk-2.0.md:426`.\n * - Structured error-with-step-number reporting is SDK 2.0 scope\n * per `sdk-2.0.md:420`.\n *\n * Cross-platform behavior:\n * - On POSIX (Linux, macOS), the parent-directory fsync uses an\n * `open(O_RDONLY) → fsync → close` sequence. This is the canonical\n * way to flush directory metadata.\n * - On Windows, opening a directory for read returns `EISDIR` (and\n * `fsync` on the resulting handle would fail with `EINVAL` even\n * if the open succeeded). NTFS's rename semantics also do not\n * require an explicit directory fsync to commit the rename\n * metadata. The helper therefore swallows `EISDIR`, `EINVAL`,\n * `EPERM`, and `ENOTSUP` from the parent-dir fsync step. Any\n * other error from the open/fsync/close sequence still propagates\n * so genuine I/O failures are not silently ignored.\n *\n * Concurrency:\n * - Two processes writing the same target concurrently follow\n * last-rename-wins semantics. The helper does not lock; the\n * caller is responsible for any external mutual-exclusion. This\n * matches the existing 0.19.x behavior of every migrated call\n * site — the helper does not change concurrency guarantees, only\n * durability.\n *\n * Performance:\n * - `fsync` is intrinsically expensive on rotational media (one\n * full disk-cache flush). The sync variant is exposed for the\n * `runtime-state.ts` writer, which runs in a signal handler with\n * a strict time budget; existing callers were already issuing a\n * blocking `writeFileSync + renameSync`, so the additional cost\n * is the two `fsync` calls. On modern SSDs this remains in the\n * low-millisecond range.\n *\n * Module-load safety: `node:fs` and `node:fs/promises` are loaded\n * lazily so the module can be imported in non-Node environments\n * (Edge Runtime, browser bundles) without crashing at import time.\n * Calling any helper export in such an environment throws a clear\n * error; callers that may run on the edge must therefore probe\n * their own Node-availability before reaching this module (see\n * `init-client.ts`'s `loadFsPathAsync`).\n *\n * @internal Not re-exported from `index.ts`/`node-entry.ts`/\n * `edge-entry.ts`. Importable only from sibling SDK modules.\n */\n\nimport type { FileHandle } from \"node:fs/promises\";\n\n/**\n * Resolves the parent directory of a path without importing `node:path`,\n * so this module remains importable in non-Node environments where\n * `node:path` is unavailable. Handles both POSIX (`/`) and Windows\n * (`\\\\`) separators because Windows paths can use either form.\n *\n * Behavior matches `path.dirname` for the inputs this module receives\n * (always absolute paths produced by SDK callers): finds the last\n * separator and returns the prefix; returns `\".\"` if no separator is\n * present (a relative leaf name); preserves the root for `/foo` →\n * `/`. Edge cases like trailing-separator inputs are not exercised by\n * SDK callers so are not modeled here.\n */\nfunction parentDir(filePath: string): string {\n const lastSlash = filePath.lastIndexOf(\"/\");\n const lastBackslash = filePath.lastIndexOf(\"\\\\\");\n const lastSep = Math.max(lastSlash, lastBackslash);\n if (lastSep < 0) return \".\";\n if (lastSep === 0) return filePath.slice(0, 1); // root: \"/x\" → \"/\"\n return filePath.slice(0, lastSep);\n}\n\n/**\n * Options accepted by both `atomicWriteFile` and `atomicWriteFileSync`.\n *\n * The shape mirrors the relevant subset of `fs.writeFile`'s options\n * object. `mode` defaults to `0o600` (state files); callers writing\n * static or discoverable files (e.g., `.well-known/glasstrace.json`)\n * may pass `0o644`. `encoding` defaults to `\"utf-8\"` when the payload\n * is a string and is ignored when the payload is a `Uint8Array`.\n */\nexport interface AtomicWriteOptions {\n /**\n * POSIX file mode applied to the temp file before the rename.\n * Defaults to `0o600`. The mode applies to the temp file and is\n * carried through the rename; callers that need a different\n * post-rename mode should call `chmod` themselves after this\n * helper resolves.\n *\n * The helper re-applies this mode unconditionally via `chmod`/`chmodSync`\n * after writing, so a pre-existing temp file (e.g., residue from a\n * crashed prior run) cannot carry stale permissive bits into the\n * caller's renamed target. The fsync handle is opened read-only so\n * callers passing a read-only mode (e.g. `0o444`) are still supported.\n */\n mode?: number;\n /**\n * Encoding for string payloads. Defaults to `\"utf-8\"`. Ignored when\n * the payload is a `Uint8Array`.\n */\n encoding?: BufferEncoding;\n}\n\n/** Errno codes that the parent-dir fsync step is permitted to swallow. */\nconst PARENT_FSYNC_SWALLOWED_CODES: ReadonlySet<string> = new Set([\n \"EISDIR\",\n \"EINVAL\",\n \"EPERM\",\n \"ENOTSUP\",\n]);\n\n/**\n * Reads the `code` property off an `unknown` thrown value if present.\n * Helper avoids `as` casts on `err` and works with both plain objects\n * and `NodeJS.ErrnoException` instances.\n */\nfunction errnoCodeOf(err: unknown): string | undefined {\n if (err === null || typeof err !== \"object\") return undefined;\n const code = (err as { code?: unknown }).code;\n return typeof code === \"string\" ? code : undefined;\n}\n\n/**\n * Builds the path of the sibling temp file. The temp lives in the\n * same directory as the target so the eventual `rename(2)` stays on\n * the same filesystem.\n *\n * Callers may pre-compute their own temp paths (e.g., `<path>.tmp-\n * <pid>` for the discovery-file write to keep multi-process collisions\n * disambiguated). When they do, they call the helper's `*WithTmp`\n * variants. The default temp suffix is `.tmp` for parity with the\n * existing 0.19.x call sites.\n */\nfunction defaultTmpPath(targetPath: string): string {\n return `${targetPath}.tmp`;\n}\n\n// ---------------------------------------------------------------------------\n// Lazy module loaders\n// ---------------------------------------------------------------------------\n\nlet fsPromisesCache: typeof import(\"node:fs/promises\") | null | undefined;\nlet fsSyncCache: typeof import(\"node:fs\") | null | undefined;\n\nasync function loadFsPromises(): Promise<typeof import(\"node:fs/promises\")> {\n if (fsPromisesCache !== undefined) {\n if (fsPromisesCache === null) {\n throw new Error(\n \"node:fs/promises is unavailable in this environment; atomicWriteFile cannot be used here.\",\n );\n }\n return fsPromisesCache;\n }\n try {\n fsPromisesCache = await import(\"node:fs/promises\");\n return fsPromisesCache;\n } catch {\n fsPromisesCache = null;\n throw new Error(\n \"node:fs/promises is unavailable in this environment; atomicWriteFile cannot be used here.\",\n );\n }\n}\n\nfunction loadFsSync(): typeof import(\"node:fs\") {\n if (fsSyncCache !== undefined) {\n if (fsSyncCache === null) {\n throw new Error(\n \"node:fs is unavailable in this environment; atomicWriteFileSync cannot be used here.\",\n );\n }\n return fsSyncCache;\n }\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n fsSyncCache = require(\"node:fs\") as typeof import(\"node:fs\");\n return fsSyncCache;\n } catch {\n fsSyncCache = null;\n throw new Error(\n \"node:fs is unavailable in this environment; atomicWriteFileSync cannot be used here.\",\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// Async variant\n// ---------------------------------------------------------------------------\n\n/**\n * Atomically writes `payload` to `targetPath` using\n * `tmp + fsync(tmp) + rename + fsync(parent)` semantics.\n *\n * On any error from the write/fsync/rename steps, the helper makes a\n * best-effort attempt to remove the temp file and rethrows the\n * original error. The parent-dir fsync step swallows\n * `EISDIR`/`EINVAL`/`EPERM`/`ENOTSUP` to support platforms where\n * directory fsync is not supported (notably Windows on NTFS).\n *\n * @param targetPath Absolute path to the final destination.\n * @param payload `string` or `Uint8Array` payload.\n * @param options See {@link AtomicWriteOptions}.\n */\nexport async function atomicWriteFile(\n targetPath: string,\n payload: string | Uint8Array,\n options: AtomicWriteOptions = {},\n): Promise<void> {\n return atomicWriteFileWithTmp(targetPath, defaultTmpPath(targetPath), payload, options);\n}\n\n/**\n * Async variant accepting an explicit `tmpPath`. The temp path MUST\n * live in the same directory as `targetPath` to preserve rename\n * atomicity. Used by `cli/discovery-file.ts` to disambiguate\n * concurrent writers via a `.tmp-<pid>` suffix.\n */\nexport async function atomicWriteFileWithTmp(\n targetPath: string,\n tmpPath: string,\n payload: string | Uint8Array,\n options: AtomicWriteOptions = {},\n): Promise<void> {\n const mode = options.mode ?? 0o600;\n const encoding = options.encoding ?? \"utf-8\";\n const fsp = await loadFsPromises();\n\n let handle: FileHandle | null = null;\n try {\n // Step 1: write payload to the temp file.\n if (typeof payload === \"string\") {\n await fsp.writeFile(tmpPath, payload, { encoding, mode });\n } else {\n await fsp.writeFile(tmpPath, payload, { mode });\n }\n\n // Step 1a: re-apply the requested mode unconditionally. `writeFile`\n // only honors `mode` when it CREATES the file; if `tmpPath` is a\n // pre-existing residue from a prior crash (or a hostile actor) the\n // existing permissions are preserved, which would silently rename a\n // world-readable temp into place. Path-based `chmod` lets the fsync\n // handle below remain read-only, so callers that pass a read-only\n // mode (e.g. 0o444) are still supported.\n await fsp.chmod(tmpPath, mode);\n\n // Step 2: fsync the temp file. Open then fsync via the\n // `FileHandle.sync()` method — `writeFile` closes its internal\n // handle immediately, so we re-open here. Read-only is sufficient\n // for `fsync` and works for callers that supply a read-only `mode`.\n handle = await fsp.open(tmpPath, \"r\");\n await handle.sync();\n await handle.close();\n handle = null;\n\n // Step 3: rename into place. POSIX-atomic on same-filesystem.\n await fsp.rename(tmpPath, targetPath);\n } catch (err) {\n if (handle !== null) {\n try {\n await handle.close();\n } catch {\n // Best-effort: the original error takes precedence.\n }\n }\n await removeTmpResidueAsync(fsp, tmpPath);\n throw err;\n }\n\n // Step 4: fsync the parent directory. Failures on platforms that\n // do not support directory fsync are swallowed; genuine I/O errors\n // still propagate.\n await fsyncParentDirAsync(targetPath, fsp);\n}\n\n/**\n * Best-effort removal of the temp file after a failed atomic-write\n * step. Tries `unlink` first (the common case where the temp is a\n * regular file). If `unlink` fails with `EISDIR`/`EPERM` — meaning the\n * temp path resolves to a directory left behind by a prior crash or\n * misconfiguration — falls back to a non-recursive `rmdir`. Any error\n * from either operation is swallowed so the caller can rethrow the\n * original I/O failure.\n */\nasync function removeTmpResidueAsync(\n fsp: typeof import(\"node:fs/promises\"),\n tmpPath: string,\n): Promise<void> {\n try {\n await fsp.unlink(tmpPath);\n return;\n } catch (err) {\n const code = errnoCodeOf(err);\n if (code !== \"EISDIR\" && code !== \"EPERM\") {\n // Tmp may not exist (ENOENT), or unlink may have failed for an\n // unrelated reason. Either way, nothing more to do — the\n // original error takes precedence in the caller.\n return;\n }\n }\n try {\n await fsp.rmdir(tmpPath);\n } catch {\n // Directory may be non-empty or otherwise unremovable; the original\n // I/O failure remains the actionable error for the caller.\n }\n}\n\nasync function fsyncParentDirAsync(\n targetPath: string,\n fsp: typeof import(\"node:fs/promises\"),\n): Promise<void> {\n const parent = parentDir(targetPath);\n let handle: FileHandle | null = null;\n try {\n handle = await fsp.open(parent, \"r\");\n await handle.sync();\n } catch (err) {\n const code = errnoCodeOf(err);\n if (code !== undefined && PARENT_FSYNC_SWALLOWED_CODES.has(code)) {\n // Platform does not support directory fsync (Windows / NTFS).\n // The rename has already returned successfully; durability\n // semantics on those filesystems do not require an explicit\n // directory sync.\n return;\n }\n throw err;\n } finally {\n if (handle !== null) {\n try {\n await handle.close();\n } catch {\n // Close errors after a successful fsync are not actionable.\n }\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Sync variant\n// ---------------------------------------------------------------------------\n\n/**\n * Synchronous counterpart to {@link atomicWriteFile}. Exists for\n * `runtime-state.ts`, which writes from a signal handler that\n * cannot await. Otherwise prefer the async variant.\n */\nexport function atomicWriteFileSync(\n targetPath: string,\n payload: string | Uint8Array,\n options: AtomicWriteOptions = {},\n): void {\n atomicWriteFileSyncWithTmp(targetPath, defaultTmpPath(targetPath), payload, options);\n}\n\n/**\n * Sync variant accepting an explicit `tmpPath`. Mirrors\n * {@link atomicWriteFileWithTmp}.\n */\nexport function atomicWriteFileSyncWithTmp(\n targetPath: string,\n tmpPath: string,\n payload: string | Uint8Array,\n options: AtomicWriteOptions = {},\n): void {\n const mode = options.mode ?? 0o600;\n const encoding = options.encoding ?? \"utf-8\";\n const fs = loadFsSync();\n\n let fd: number | null = null;\n try {\n if (typeof payload === \"string\") {\n fs.writeFileSync(tmpPath, payload, { encoding, mode });\n } else {\n fs.writeFileSync(tmpPath, payload, { mode });\n }\n\n // Re-apply the requested mode unconditionally — see the matching\n // comment in `atomicWriteFileWithTmp` for the credential-leak\n // rationale when `tmpPath` is pre-existing residue.\n fs.chmodSync(tmpPath, mode);\n\n // Read-only is sufficient for `fsync` and works for callers that\n // supply a read-only `mode`.\n fd = fs.openSync(tmpPath, \"r\");\n fs.fsyncSync(fd);\n fs.closeSync(fd);\n fd = null;\n\n fs.renameSync(tmpPath, targetPath);\n } catch (err) {\n if (fd !== null) {\n try {\n fs.closeSync(fd);\n } catch {\n // Best-effort.\n }\n }\n removeTmpResidueSync(fs, tmpPath);\n throw err;\n }\n\n fsyncParentDirSyncWithFs(targetPath, fs);\n}\n\n/**\n * Synchronous counterpart to {@link removeTmpResidueAsync}. See the\n * async variant's JSDoc for the `EISDIR`/`EPERM` rationale.\n */\nfunction removeTmpResidueSync(\n fs: typeof import(\"node:fs\"),\n tmpPath: string,\n): void {\n try {\n fs.unlinkSync(tmpPath);\n return;\n } catch (err) {\n const code = errnoCodeOf(err);\n if (code !== \"EISDIR\" && code !== \"EPERM\") {\n return;\n }\n }\n try {\n fs.rmdirSync(tmpPath);\n } catch {\n // Directory may be non-empty; original error takes precedence.\n }\n}\n\n/**\n * Synchronously fsyncs the parent directory of `targetPath`. Errors\n * matching {@link PARENT_FSYNC_SWALLOWED_CODES} (Windows / NTFS does\n * not support directory fsync) are silently ignored; other errors\n * propagate. Exposed for callers like `cli/discovery-file.ts` that\n * need to compose the steps manually around a backup-rollback flow.\n *\n * @internal Sibling-module use only.\n */\nexport function fsyncParentDirSync(targetPath: string): void {\n fsyncParentDirSyncWithFs(targetPath, loadFsSync());\n}\n\nfunction fsyncParentDirSyncWithFs(\n targetPath: string,\n fs: typeof import(\"node:fs\"),\n): void {\n const parent = parentDir(targetPath);\n let fd: number | null = null;\n try {\n fd = fs.openSync(parent, \"r\");\n fs.fsyncSync(fd);\n } catch (err) {\n const code = errnoCodeOf(err);\n if (code !== undefined && PARENT_FSYNC_SWALLOWED_CODES.has(code)) {\n return;\n }\n throw err;\n } finally {\n if (fd !== null) {\n try {\n fs.closeSync(fd);\n } catch {\n // Close errors after a successful fsync are not actionable.\n }\n }\n }\n}\n\n/**\n * Synchronously writes `payload` to `tmpPath` and fsyncs the\n * resulting file so its data and metadata are durable on disk\n * before any rename is observable. Throws on any I/O error from\n * either step; on throw, the partially-written tmp file is left\n * in place (callers handle cleanup so they can decide between\n * `unlink` and a backup-rollback strategy).\n *\n * Steps 1 and 2 of the SDK 2.0 §4.3 protocol. Pair with a `rename`\n * (step 3) and {@link fsyncParentDirSync} (step 4) — or use\n * {@link atomicWriteFileSync}/{@link atomicWriteFileSyncWithTmp}\n * which compose all four steps internally.\n *\n * @internal Sibling-module use only.\n */\nexport function writeAndFsyncTempSync(\n tmpPath: string,\n payload: string | Uint8Array,\n options: AtomicWriteOptions = {},\n): void {\n const mode = options.mode ?? 0o600;\n const encoding = options.encoding ?? \"utf-8\";\n const fs = loadFsSync();\n if (typeof payload === \"string\") {\n fs.writeFileSync(tmpPath, payload, { encoding, mode });\n } else {\n fs.writeFileSync(tmpPath, payload, { mode });\n }\n // Re-apply the requested mode in case `tmpPath` already existed —\n // `writeFileSync` only honors `mode` when creating the file, so a\n // stale residue could otherwise carry permissive bits into the\n // caller's eventual rename. Path-based `chmodSync` keeps the fsync\n // handle below read-only.\n fs.chmodSync(tmpPath, mode);\n // Read-only is sufficient for `fsync` and works for callers that\n // pass a read-only `mode`.\n const fd = fs.openSync(tmpPath, \"r\");\n try {\n fs.fsyncSync(fd);\n } finally {\n try {\n fs.closeSync(fd);\n } catch {\n // Close errors after fsync are not actionable.\n }\n }\n}\n\n/**\n * Test-only: clear the cached lazy-loaded modules. Allows test suites\n * that mock `node:fs`/`node:fs/promises` to ensure the helper re-runs\n * its module probe.\n *\n * @internal Tests only.\n */\nexport function _resetModuleCacheForTesting(): void {\n fsPromisesCache = undefined;\n fsSyncCache = undefined;\n}\n","import { createHash } from \"node:crypto\";\nimport {\n AnonApiKeySchema,\n DevApiKeySchema,\n type AnonApiKey,\n type DevApiKey,\n} from \"@glasstrace/protocol\";\nimport { readAnonKey, readClaimedKey } from \"./anon-key.js\";\nimport { atomicWriteFile } from \"./atomic-write.js\";\n\n/**\n * Glasstrace MCP endpoint embedded in managed MCP configs and used by\n * the runtime claim-refresh path. Lives here (not in `cli/constants.ts`)\n * so the runtime helper can reach it without crossing the runtime/CLI\n * boundary; CLI callers import it directly from this module.\n */\nexport const MCP_ENDPOINT = \"https://api.glasstrace.dev/mcp\";\n\n/**\n * Runtime-safe MCP credential and config utilities.\n *\n * This module is loaded into user processes at SDK boot. It must not\n * import from `cli/*` or `agent-detection/*` so the runtime bundle does\n * not pull in CLI scaffolding or filesystem scanners. The boundary is\n * enforced by an import-graph guard test.\n *\n * Internal: not re-exported via `node-entry.ts` or `index.ts`.\n *\n * @module\n */\n\nlet fsPathCache:\n | { fs: typeof import(\"node:fs/promises\"); path: typeof import(\"node:path\") }\n | null\n | undefined;\n\nasync function loadFsPath(): Promise<\n | { fs: typeof import(\"node:fs/promises\"); path: typeof import(\"node:path\") }\n | null\n> {\n if (fsPathCache !== undefined) return fsPathCache;\n try {\n const [fs, path] = await Promise.all([\n import(\"node:fs/promises\"),\n import(\"node:path\"),\n ]);\n fsPathCache = { fs, path };\n return fsPathCache;\n } catch {\n fsPathCache = null;\n return null;\n }\n}\n\n/**\n * Computes a stable identity fingerprint for deduplication purposes.\n * This is NOT password hashing — the input is an opaque token used as\n * a marker identity, not a credential stored for authentication.\n *\n * @internal Exported for unit testing and for `cli/scaffolder.ts`'s\n * marker writer.\n */\nexport function identityFingerprint(token: string): string {\n return `sha256:${createHash(\"sha256\").update(token).digest(\"hex\")}`;\n}\n\n/**\n * Compares two MCP config strings for canonical-JSON equality. Returns\n * `true` when both inputs parse as JSON and produce structurally equal\n * objects after recursive key sorting; falls back to trimmed text\n * comparison for TOML and other non-JSON formats. Returns `false` on\n * parse errors that don't fall through to text comparison.\n *\n * Used to detect manually-edited MCP configs before overwriting them\n * (DISC-1247 Scenario 2c) and as the staleness signal for SDK-managed\n * configs that must be refreshed when the project's effective\n * credential changes.\n *\n * @internal Exported for unit testing only.\n */\nexport function mcpConfigMatches(\n existingContent: string,\n expectedContent: string,\n): boolean {\n const trimmedExpected = expectedContent.trim();\n\n try {\n const existingParsed: unknown = JSON.parse(existingContent);\n const expectedParsed: unknown = JSON.parse(trimmedExpected);\n return (\n JSON.stringify(canonicalize(existingParsed)) ===\n JSON.stringify(canonicalize(expectedParsed))\n );\n } catch {\n // Fall through to text comparison for TOML and other non-JSON formats.\n }\n\n return existingContent.trim() === trimmedExpected;\n}\n\nfunction canonicalize(value: unknown): unknown {\n if (Array.isArray(value)) {\n return value.map(canonicalize);\n }\n if (value !== null && typeof value === \"object\") {\n const obj = value as Record<string, unknown>;\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(obj).sort()) {\n sorted[key] = canonicalize(obj[key]);\n }\n return sorted;\n }\n return value;\n}\n\n/**\n * Parses a `.env.local` file's text content for `GLASSTRACE_API_KEY`,\n * returning the last assignment's value. Empty values\n * (`GLASSTRACE_API_KEY=`) and the `your_key_here` placeholder are\n * filtered out. Surrounding single or double quotes are stripped.\n *\n * The resolver validates the returned value against `DevApiKeySchema`\n * before accepting it; this parser is permissive on purpose so that\n * malformed values can be flagged with a `malformed-env-local`\n * warning rather than silently dropped.\n *\n * @internal Exported for unit testing only.\n */\nexport function readEnvLocalApiKey(content: string): string | null {\n let last: string | null = null;\n const regex = /^\\s*GLASSTRACE_API_KEY\\s*=\\s*(.*)$/gm;\n let match: RegExpExecArray | null;\n while ((match = regex.exec(content)) !== null) {\n const raw = match[1].trim();\n if (raw === \"\") continue;\n const unquoted = raw.replace(/^(['\"])(.*)\\1$/, \"$2\");\n if (unquoted === \"\" || unquoted === \"your_key_here\") continue;\n last = unquoted;\n }\n return last;\n}\n\n/**\n * Returns true when the given API key value looks like a claimed\n * developer key (prefix `gt_dev_`). Defensive against leading or\n * trailing whitespace.\n *\n * **This is a prefix-only check, not strict validation.** Use it as a\n * fast path for \"looks like a claimed key, do not overwrite\". The\n * effective-credential resolver validates with\n * `DevApiKeySchema.safeParse` because a `gt_dev_` prefix alone is not\n * sufficient to authenticate against the backend.\n *\n * @internal Exported for unit testing only.\n */\nexport function isDevApiKey(value: string | null | undefined): boolean {\n if (value === null || value === undefined) return false;\n return value.trim().startsWith(\"gt_dev_\");\n}\n\n/**\n * Returns true when the given API key value is a fully-valid anonymous\n * API key (matches `AnonApiKeySchema`). Used by `registerViaCli` as a\n * runtime guard so that a `DevApiKey` cannot be passed via process\n * arguments to vendor MCP CLIs (which would expose it via `ps` on\n * multi-user hosts).\n *\n * @internal Exported for unit testing only.\n */\nexport function isAnonApiKey(value: string | null | undefined): boolean {\n if (value === null || value === undefined) return false;\n return AnonApiKeySchema.safeParse(value).success;\n}\n\n/**\n * The MCP-effective credential, tagged by which on-disk source produced\n * it. `env-local` and `claimed-key` carry a branded `DevApiKey`;\n * `anon` carries a branded `AnonApiKey`. Internal — not re-exported.\n */\nexport type EffectiveMcpCredential =\n | { source: \"env-local\"; key: DevApiKey }\n | { source: \"claimed-key\"; key: DevApiKey }\n | { source: \"anon\"; key: AnonApiKey };\n\n/**\n * Surfaced when the resolver detected a recoverable anomaly the caller\n * should inform the user about without printing key material.\n *\n * - `malformed-env-local`: `.env.local` set `GLASSTRACE_API_KEY` to a\n * value that fails `DevApiKeySchema`. The resolver fell through.\n * - `claimed-key-only`: the effective credential came from\n * `.glasstrace/claimed-key` because `.env.local` had no usable dev\n * key. Suggest the user copy the key into `.env.local`.\n */\nexport type ResolveWarning = \"malformed-env-local\" | \"claimed-key-only\";\n\n/**\n * The resolved credential plus the on-disk anon key (returned\n * separately so the staleness check does not have to re-read the\n * file) and any warnings the caller should surface to the user.\n */\nexport interface ResolveResult {\n effective: EffectiveMcpCredential | null;\n anonKey: AnonApiKey | null;\n warnings: ReadonlyArray<ResolveWarning>;\n}\n\n/**\n * Resolves the MCP-effective credential for a project, in priority\n * order: `.env.local` `GLASSTRACE_API_KEY` (validated as\n * `DevApiKeySchema`) → `.glasstrace/claimed-key` (validated as\n * `DevApiKeySchema`) → `.glasstrace/anon_key` (`AnonApiKey`). Returns\n * `null` for `effective` only when no source produced a usable key.\n *\n * The function is async because it touches the filesystem. It is\n * called only on the post-claim runtime branch and from the CLI\n * commands `glasstrace init` and `glasstrace mcp add`. It is **not**\n * on the steady-state init path.\n */\nexport async function resolveEffectiveMcpCredential(\n projectRoot?: string,\n): Promise<ResolveResult> {\n const root = projectRoot ?? process.cwd();\n const warnings: ResolveWarning[] = [];\n\n const envLocalKey = await readEnvLocalDevKey(root, warnings);\n const claimedKey = envLocalKey === null ? await readClaimedKey(root) : null;\n const anonKey = await readAnonKey(root);\n\n let effective: EffectiveMcpCredential | null = null;\n if (envLocalKey !== null) {\n effective = { source: \"env-local\", key: envLocalKey };\n } else if (claimedKey !== null) {\n effective = { source: \"claimed-key\", key: claimedKey };\n warnings.push(\"claimed-key-only\");\n } else if (anonKey !== null) {\n effective = { source: \"anon\", key: anonKey };\n }\n\n return { effective, anonKey, warnings };\n}\n\nasync function readEnvLocalDevKey(\n root: string,\n warnings: ResolveWarning[],\n): Promise<DevApiKey | null> {\n const modules = await loadFsPath();\n if (!modules) return null;\n\n const envPath = modules.path.join(root, \".env.local\");\n let content: string;\n try {\n content = await modules.fs.readFile(envPath, \"utf-8\");\n } catch {\n return null;\n }\n\n const raw = readEnvLocalApiKey(content);\n if (raw === null) return null;\n\n const parsed = DevApiKeySchema.safeParse(raw);\n if (!parsed.success) {\n warnings.push(\"malformed-env-local\");\n return null;\n }\n return parsed.data;\n}\n\n/**\n * Source label for the credential a marker file describes.\n *\n * @internal\n */\nexport type MarkerCredentialSource = \"env-local\" | \"claimed-key\" | \"anon\";\n\n/**\n * Descriptor passed to {@link writeMcpMarker} and matched by\n * {@link readMcpMarker}. `credentialHash` is the\n * `identityFingerprint` of the credential actually written into the\n * managed MCP config — never the credential itself.\n *\n * @internal\n */\nexport interface MarkerTarget {\n credentialSource: MarkerCredentialSource;\n credentialHash: string;\n}\n\n/**\n * Normalized state of a `.glasstrace/mcp-connected` marker on disk.\n *\n * - `absent`: no marker file present.\n * - `valid`: a v1 or v2 marker that parsed cleanly. v1 markers are\n * reported as `credentialSource = \"anon\"` with `credentialHash`\n * taken from the legacy `keyHash` field (the v1 schema can only\n * describe an anon credential).\n * - `unknown-version`: the marker has `version > 2`. Treat as\n * not-configured so a future SDK that wrote the marker doesn't\n * block this SDK from refreshing.\n * - `corrupted`: parse failure or schema mismatch. Treat as\n * not-configured.\n *\n * @internal\n */\nexport type MarkerState =\n | { status: \"absent\" }\n | { status: \"valid\"; credentialSource: MarkerCredentialSource; credentialHash: string }\n | { status: \"unknown-version\" }\n | { status: \"corrupted\" };\n\nconst MCP_MARKER_FILE = \"mcp-connected\";\nconst GLASSTRACE_DIR = \".glasstrace\";\n\n/**\n * Reads `.glasstrace/mcp-connected` and returns its normalized state.\n * Used by `mcp add` (marker-mismatch detection) and by\n * {@link writeMcpMarker} (skip-if-match optimization).\n *\n * Reader rules per the design (`SDK-034 D3`):\n * - `version === undefined` → v1: `{ keyHash, configuredAt }`. Mapped\n * to `credentialSource: \"anon\"`, `credentialHash: keyHash`. v1's\n * `keyHash` is itself produced by `identityFingerprint`, so the\n * format matches v2 without conversion.\n * - `version === 2` → v2 reader.\n * - `version > 2` → `unknown-version` (conservative-fail).\n * - Parse failure → `corrupted` (conservative-fail).\n *\n * @internal Exported for unit testing only.\n */\nexport async function readMcpMarker(projectRoot?: string): Promise<MarkerState> {\n const root = projectRoot ?? process.cwd();\n const modules = await loadFsPath();\n if (!modules) return { status: \"absent\" };\n\n const markerPath = modules.path.join(root, GLASSTRACE_DIR, MCP_MARKER_FILE);\n let content: string;\n try {\n content = await modules.fs.readFile(markerPath, \"utf-8\");\n } catch {\n return { status: \"absent\" };\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(content);\n } catch {\n return { status: \"corrupted\" };\n }\n\n if (parsed === null || typeof parsed !== \"object\") {\n return { status: \"corrupted\" };\n }\n\n const obj = parsed as Record<string, unknown>;\n const version = obj[\"version\"];\n\n if (version === undefined) {\n // v1: { keyHash, configuredAt }\n const keyHash = obj[\"keyHash\"];\n if (typeof keyHash !== \"string\" || keyHash === \"\") {\n return { status: \"corrupted\" };\n }\n return {\n status: \"valid\",\n credentialSource: \"anon\",\n credentialHash: keyHash,\n };\n }\n\n if (version === 2) {\n const source = obj[\"credentialSource\"];\n const hash = obj[\"credentialHash\"];\n if (\n (source !== \"env-local\" && source !== \"claimed-key\" && source !== \"anon\") ||\n typeof hash !== \"string\" ||\n hash === \"\"\n ) {\n return { status: \"corrupted\" };\n }\n return {\n status: \"valid\",\n credentialSource: source,\n credentialHash: hash,\n };\n }\n\n if (typeof version === \"number\" && version > 2) {\n return { status: \"unknown-version\" };\n }\n\n return { status: \"corrupted\" };\n}\n\n/**\n * Writes a v2 `.glasstrace/mcp-connected` marker. Returns `true` when\n * the marker was created or updated, `false` when an existing marker\n * already records the same `(credentialSource, credentialHash)` pair\n * and was left untouched.\n *\n * Writer always emits v2 with `version: 2`. The legacy `keyHash`\n * field is intentionally omitted from new writes — v1 readers ignore\n * unknown fields and the duplicate would diverge over time. v3+ and\n * corrupted markers are unconditionally overwritten.\n *\n * The directory is created with `0o700` and the file with `0o600`,\n * matching existing scaffolder behavior.\n *\n * @internal Exported for unit testing only.\n */\nexport async function writeMcpMarker(\n projectRoot: string,\n target: MarkerTarget,\n): Promise<boolean> {\n const modules = await loadFsPath();\n if (!modules) return false;\n\n const dirPath = modules.path.join(projectRoot, GLASSTRACE_DIR);\n const markerPath = modules.path.join(dirPath, MCP_MARKER_FILE);\n\n const state = await readMcpMarker(projectRoot);\n if (\n state.status === \"valid\" &&\n state.credentialSource === target.credentialSource &&\n state.credentialHash === target.credentialHash\n ) {\n return false;\n }\n\n await modules.fs.mkdir(dirPath, { recursive: true, mode: 0o700 });\n\n const body = JSON.stringify(\n {\n version: 2,\n credentialSource: target.credentialSource,\n credentialHash: target.credentialHash,\n configuredAt: new Date().toISOString(),\n },\n null,\n 2,\n );\n\n await modules.fs.writeFile(markerPath, body, { mode: 0o600 });\n // writeFile mode only applies on creation on some platforms.\n await modules.fs.chmod(markerPath, 0o600);\n return true;\n}\n\nconst MCP_CONFIG_FILE = \"mcp.json\";\n\n/**\n * The set of outcomes the runtime claim-refresh helper can produce.\n *\n * - `rewrote`: `.glasstrace/mcp.json` matched the SDK-shaped output\n * for the on-disk anon key, was rewritten with the effective\n * credential, and the marker was updated.\n * - `preserved`: `.glasstrace/mcp.json` exists but does not match the\n * SDK-shaped output for the on-disk anon key. The file is left\n * untouched (the user may have hand-edited it). The marker is not\n * touched.\n * - `absent`: `.glasstrace/mcp.json` does not exist (`ENOENT`), or\n * no anon key is on disk so there is nothing to compare against. A\n * project without an anon key never had an SDK-shaped `mcp.json`\n * written by the runtime path, so this branch is a true no-op.\n * - `skipped-anon-source`: the effective credential is `null` or its\n * source is `\"anon\"`. Either way, there is no claim transition to\n * refresh for. Caller should generally gate on\n * `effective.source !== \"anon\"` before invoking the helper; this\n * branch is the runtime-side belt-and-suspenders.\n * - `skipped-not-persisted`: never reached in practice — the caller\n * in `init-client.ts` gates on `writeClaimedKey`'s `persisted` not\n * being `\"none\"`. The variant exists so an exhaustive switch in\n * the caller stays exhaustive if the gate is removed.\n *\n * @internal\n */\nexport type RuntimeRefreshAction =\n | \"rewrote\"\n | \"preserved\"\n | \"absent\"\n | \"skipped-anon-source\"\n | \"skipped-not-persisted\";\n\nlet refreshNudgeEmitted = false;\n\n/**\n * @internal Exported for unit testing only — resets the per-process\n * \"refresh nudge already emitted\" flag.\n */\nexport function __resetRefreshNudgeForTest(): void {\n refreshNudgeEmitted = false;\n}\n\n/**\n * Emits a single redacted stderr line announcing the MCP config\n * refresh. Deduplicated per process via a module-level flag — a\n * second call within the same process is a no-op. Cross-process\n * dedup (the same user running `mcp add` in another terminal moments\n * later) is explicitly out of scope.\n */\nfunction emitRefreshNudge(persistedSource: \"env-local\" | \"claimed-key\"): void {\n if (refreshNudgeEmitted) return;\n refreshNudgeEmitted = true;\n try {\n if (persistedSource === \"claimed-key\") {\n process.stderr.write(\n \"[glasstrace] MCP config refreshed for the new credential. \" +\n \"Copy .glasstrace/claimed-key into .env.local so Codex can pick it up on next restart.\\n\",\n );\n } else {\n process.stderr.write(\n \"[glasstrace] MCP config refreshed for the new credential.\\n\",\n );\n }\n } catch {\n // stderr is best-effort; refresh outcome must not depend on it.\n }\n}\n\n/**\n * Returns the SDK-shaped JSON for `.glasstrace/mcp.json` (the generic\n * MCP config used at runtime). Inlined here — and intentionally not\n * imported from `agent-detection/configs.ts` — because the runtime\n * path must not pull `agent-detection` into the runtime bundle. The\n * shape matches what `generateMcpConfig({ name: \"generic\", ... },\n * endpoint, bearer)` would produce. If the agent-detection version\n * diverges, the staleness check stops detecting SDK-managed configs;\n * a regression test against `generateMcpConfig`'s \"generic\" branch\n * lives in `tests/unit/sdk/mcp-runtime.test.ts`.\n */\nfunction genericMcpConfigContent(endpoint: string, bearer: string): string {\n return JSON.stringify(\n {\n mcpServers: {\n glasstrace: {\n url: endpoint,\n headers: {\n Authorization: `Bearer ${bearer}`,\n },\n },\n },\n },\n null,\n 2,\n );\n}\n\n/**\n * Refreshes `.glasstrace/mcp.json` after a successful account claim\n * transition has persisted a dev/account credential to disk (via\n * `writeClaimedKey`). The file is rewritten only when its content\n * matches the SDK-shaped output for the project's on-disk anon key\n * (canonical-JSON equivalence via `mcpConfigMatches` — whitespace and\n * key order are normalised before comparison). User-edited or\n * third-party `mcp.json` content is preserved.\n *\n * Atomic write protocol: write the replacement to a sibling temp\n * path, set `0o600`, then `rename` into place. This matches the\n * existing pattern at `init-client.ts` for `.glasstrace/config`,\n * `anon-key.ts` for `.glasstrace/anon_key`, and `runtime-state.ts`.\n * The temp must be on the same filesystem as the destination for the\n * `rename` to be atomic.\n *\n * The helper is invoked only on the post-claim runtime branch (see\n * `init-client.ts` `performInit`) and never on the steady-state init\n * path. It must not throw — failures during write/chmod/rename or\n * marker update surface as `\"preserved\"` so the caller's\n * `claimResult` return is preserved. The temp file is best-effort\n * cleaned up on failure to avoid leaving stale `.tmp` siblings on\n * disk.\n *\n * @internal Exported for unit testing only; not re-exported from\n * `node-entry.ts` or `index.ts`.\n */\nexport async function refreshGenericMcpConfigAtRuntime(\n projectRoot: string,\n effective: EffectiveMcpCredential | null,\n anonKeyOnDisk: AnonApiKey | null,\n): Promise<{ action: RuntimeRefreshAction }> {\n if (effective === null || effective.source === \"anon\") {\n return { action: \"skipped-anon-source\" };\n }\n\n // Dev-key-only project (no .glasstrace/anon_key on disk): the\n // staleness check has nothing to compare against. The SDK never\n // wrote mcp.json without an anon key, so there is nothing to\n // refresh.\n if (anonKeyOnDisk === null) {\n return { action: \"absent\" };\n }\n\n const modules = await loadFsPath();\n if (!modules) return { action: \"absent\" };\n\n const dirPath = modules.path.join(projectRoot, GLASSTRACE_DIR);\n const configPath = modules.path.join(dirPath, MCP_CONFIG_FILE);\n\n let existing: string;\n try {\n existing = await modules.fs.readFile(configPath, \"utf-8\");\n } catch (err) {\n const code = (err as NodeJS.ErrnoException).code;\n if (code === \"ENOENT\") {\n return { action: \"absent\" };\n }\n return { action: \"preserved\" };\n }\n\n const expectedAnon = genericMcpConfigContent(MCP_ENDPOINT, anonKeyOnDisk);\n if (!mcpConfigMatches(existing, expectedAnon)) {\n return { action: \"preserved\" };\n }\n\n // SDK-managed and stale. Replace atomically per SDK 2.0 §4.3:\n // tmp + fsync(tmp) + rename + fsync(parent). Any failure in the\n // helper or marker update path must produce a non-throw outcome\n // so the caller's claimResult return is preserved; the helper\n // best-effort cleans up the .tmp sibling on failure.\n const replacement = genericMcpConfigContent(MCP_ENDPOINT, effective.key);\n try {\n await atomicWriteFile(configPath, replacement, { mode: 0o600 });\n\n await writeMcpMarker(projectRoot, {\n credentialSource: effective.source,\n credentialHash: identityFingerprint(effective.key),\n });\n } catch {\n return { action: \"preserved\" };\n }\n\n emitRefreshNudge(effective.source);\n\n return { action: \"rewrote\" };\n}\n"],"mappings":"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","names":["fsPathCache","loadFsPath","GLASSTRACE_DIR","loadFsPath","GLASSTRACE_DIR"]}
|
package/dist/cli/init.cjs
CHANGED
|
@@ -14766,6 +14766,146 @@ var init_anon_key = __esm({
|
|
|
14766
14766
|
}
|
|
14767
14767
|
});
|
|
14768
14768
|
|
|
14769
|
+
// src/atomic-write.ts
|
|
14770
|
+
function parentDir(filePath) {
|
|
14771
|
+
const lastSlash = filePath.lastIndexOf("/");
|
|
14772
|
+
const lastBackslash = filePath.lastIndexOf("\\");
|
|
14773
|
+
const lastSep = Math.max(lastSlash, lastBackslash);
|
|
14774
|
+
if (lastSep < 0) return ".";
|
|
14775
|
+
if (lastSep === 0) return filePath.slice(0, 1);
|
|
14776
|
+
return filePath.slice(0, lastSep);
|
|
14777
|
+
}
|
|
14778
|
+
function errnoCodeOf(err) {
|
|
14779
|
+
if (err === null || typeof err !== "object") return void 0;
|
|
14780
|
+
const code = err.code;
|
|
14781
|
+
return typeof code === "string" ? code : void 0;
|
|
14782
|
+
}
|
|
14783
|
+
function defaultTmpPath(targetPath) {
|
|
14784
|
+
return `${targetPath}.tmp`;
|
|
14785
|
+
}
|
|
14786
|
+
function loadFsSync() {
|
|
14787
|
+
if (fsSyncCache !== void 0) {
|
|
14788
|
+
if (fsSyncCache === null) {
|
|
14789
|
+
throw new Error(
|
|
14790
|
+
"node:fs is unavailable in this environment; atomicWriteFileSync cannot be used here."
|
|
14791
|
+
);
|
|
14792
|
+
}
|
|
14793
|
+
return fsSyncCache;
|
|
14794
|
+
}
|
|
14795
|
+
try {
|
|
14796
|
+
fsSyncCache = require("node:fs");
|
|
14797
|
+
return fsSyncCache;
|
|
14798
|
+
} catch {
|
|
14799
|
+
fsSyncCache = null;
|
|
14800
|
+
throw new Error(
|
|
14801
|
+
"node:fs is unavailable in this environment; atomicWriteFileSync cannot be used here."
|
|
14802
|
+
);
|
|
14803
|
+
}
|
|
14804
|
+
}
|
|
14805
|
+
function atomicWriteFileSync(targetPath, payload, options = {}) {
|
|
14806
|
+
atomicWriteFileSyncWithTmp(targetPath, defaultTmpPath(targetPath), payload, options);
|
|
14807
|
+
}
|
|
14808
|
+
function atomicWriteFileSyncWithTmp(targetPath, tmpPath, payload, options = {}) {
|
|
14809
|
+
const mode = options.mode ?? 384;
|
|
14810
|
+
const encoding = options.encoding ?? "utf-8";
|
|
14811
|
+
const fs10 = loadFsSync();
|
|
14812
|
+
let fd = null;
|
|
14813
|
+
try {
|
|
14814
|
+
if (typeof payload === "string") {
|
|
14815
|
+
fs10.writeFileSync(tmpPath, payload, { encoding, mode });
|
|
14816
|
+
} else {
|
|
14817
|
+
fs10.writeFileSync(tmpPath, payload, { mode });
|
|
14818
|
+
}
|
|
14819
|
+
fs10.chmodSync(tmpPath, mode);
|
|
14820
|
+
fd = fs10.openSync(tmpPath, "r");
|
|
14821
|
+
fs10.fsyncSync(fd);
|
|
14822
|
+
fs10.closeSync(fd);
|
|
14823
|
+
fd = null;
|
|
14824
|
+
fs10.renameSync(tmpPath, targetPath);
|
|
14825
|
+
} catch (err) {
|
|
14826
|
+
if (fd !== null) {
|
|
14827
|
+
try {
|
|
14828
|
+
fs10.closeSync(fd);
|
|
14829
|
+
} catch {
|
|
14830
|
+
}
|
|
14831
|
+
}
|
|
14832
|
+
removeTmpResidueSync(fs10, tmpPath);
|
|
14833
|
+
throw err;
|
|
14834
|
+
}
|
|
14835
|
+
fsyncParentDirSyncWithFs(targetPath, fs10);
|
|
14836
|
+
}
|
|
14837
|
+
function removeTmpResidueSync(fs10, tmpPath) {
|
|
14838
|
+
try {
|
|
14839
|
+
fs10.unlinkSync(tmpPath);
|
|
14840
|
+
return;
|
|
14841
|
+
} catch (err) {
|
|
14842
|
+
const code = errnoCodeOf(err);
|
|
14843
|
+
if (code !== "EISDIR" && code !== "EPERM") {
|
|
14844
|
+
return;
|
|
14845
|
+
}
|
|
14846
|
+
}
|
|
14847
|
+
try {
|
|
14848
|
+
fs10.rmdirSync(tmpPath);
|
|
14849
|
+
} catch {
|
|
14850
|
+
}
|
|
14851
|
+
}
|
|
14852
|
+
function fsyncParentDirSync(targetPath) {
|
|
14853
|
+
fsyncParentDirSyncWithFs(targetPath, loadFsSync());
|
|
14854
|
+
}
|
|
14855
|
+
function fsyncParentDirSyncWithFs(targetPath, fs10) {
|
|
14856
|
+
const parent = parentDir(targetPath);
|
|
14857
|
+
let fd = null;
|
|
14858
|
+
try {
|
|
14859
|
+
fd = fs10.openSync(parent, "r");
|
|
14860
|
+
fs10.fsyncSync(fd);
|
|
14861
|
+
} catch (err) {
|
|
14862
|
+
const code = errnoCodeOf(err);
|
|
14863
|
+
if (code !== void 0 && PARENT_FSYNC_SWALLOWED_CODES.has(code)) {
|
|
14864
|
+
return;
|
|
14865
|
+
}
|
|
14866
|
+
throw err;
|
|
14867
|
+
} finally {
|
|
14868
|
+
if (fd !== null) {
|
|
14869
|
+
try {
|
|
14870
|
+
fs10.closeSync(fd);
|
|
14871
|
+
} catch {
|
|
14872
|
+
}
|
|
14873
|
+
}
|
|
14874
|
+
}
|
|
14875
|
+
}
|
|
14876
|
+
function writeAndFsyncTempSync(tmpPath, payload, options = {}) {
|
|
14877
|
+
const mode = options.mode ?? 384;
|
|
14878
|
+
const encoding = options.encoding ?? "utf-8";
|
|
14879
|
+
const fs10 = loadFsSync();
|
|
14880
|
+
if (typeof payload === "string") {
|
|
14881
|
+
fs10.writeFileSync(tmpPath, payload, { encoding, mode });
|
|
14882
|
+
} else {
|
|
14883
|
+
fs10.writeFileSync(tmpPath, payload, { mode });
|
|
14884
|
+
}
|
|
14885
|
+
fs10.chmodSync(tmpPath, mode);
|
|
14886
|
+
const fd = fs10.openSync(tmpPath, "r");
|
|
14887
|
+
try {
|
|
14888
|
+
fs10.fsyncSync(fd);
|
|
14889
|
+
} finally {
|
|
14890
|
+
try {
|
|
14891
|
+
fs10.closeSync(fd);
|
|
14892
|
+
} catch {
|
|
14893
|
+
}
|
|
14894
|
+
}
|
|
14895
|
+
}
|
|
14896
|
+
var PARENT_FSYNC_SWALLOWED_CODES, fsSyncCache;
|
|
14897
|
+
var init_atomic_write = __esm({
|
|
14898
|
+
"src/atomic-write.ts"() {
|
|
14899
|
+
"use strict";
|
|
14900
|
+
PARENT_FSYNC_SWALLOWED_CODES = /* @__PURE__ */ new Set([
|
|
14901
|
+
"EISDIR",
|
|
14902
|
+
"EINVAL",
|
|
14903
|
+
"EPERM",
|
|
14904
|
+
"ENOTSUP"
|
|
14905
|
+
]);
|
|
14906
|
+
}
|
|
14907
|
+
});
|
|
14908
|
+
|
|
14769
14909
|
// src/mcp-runtime.ts
|
|
14770
14910
|
async function loadFsPath2() {
|
|
14771
14911
|
if (fsPathCache2 !== void 0) return fsPathCache2;
|
|
@@ -15289,13 +15429,13 @@ async function writeMcpConfig(agent, content, projectRoot) {
|
|
|
15289
15429
|
return;
|
|
15290
15430
|
}
|
|
15291
15431
|
const configPath = agent.mcpConfigPath;
|
|
15292
|
-
const
|
|
15432
|
+
const parentDir2 = (0, import_node_path2.dirname)(configPath);
|
|
15293
15433
|
try {
|
|
15294
|
-
await (0, import_promises2.mkdir)(
|
|
15434
|
+
await (0, import_promises2.mkdir)(parentDir2, { recursive: true });
|
|
15295
15435
|
} catch (err) {
|
|
15296
15436
|
if (isPermissionError(err)) {
|
|
15297
15437
|
process.stderr.write(
|
|
15298
|
-
`Warning: cannot create directory ${
|
|
15438
|
+
`Warning: cannot create directory ${parentDir2}: permission denied
|
|
15299
15439
|
`
|
|
15300
15440
|
);
|
|
15301
15441
|
return;
|
|
@@ -15809,8 +15949,11 @@ function writeDiscoveryFile(projectRoot, anonKey) {
|
|
|
15809
15949
|
try {
|
|
15810
15950
|
fs4.mkdirSync(wellKnownDir, { recursive: true });
|
|
15811
15951
|
const payload = serializeDiscoveryPayload(anonKey, extras);
|
|
15812
|
-
fs4.writeFileSync(tmpPath, payload, { encoding: "utf-8" });
|
|
15813
15952
|
if (backupPath !== null) {
|
|
15953
|
+
writeAndFsyncTempSync(tmpPath, payload, {
|
|
15954
|
+
encoding: "utf-8",
|
|
15955
|
+
mode: 420
|
|
15956
|
+
});
|
|
15814
15957
|
fs4.renameSync(filePath, backupPath);
|
|
15815
15958
|
try {
|
|
15816
15959
|
fs4.renameSync(tmpPath, filePath);
|
|
@@ -15821,12 +15964,16 @@ function writeDiscoveryFile(projectRoot, anonKey) {
|
|
|
15821
15964
|
}
|
|
15822
15965
|
throw renameErr;
|
|
15823
15966
|
}
|
|
15967
|
+
fsyncParentDirSync(filePath);
|
|
15824
15968
|
try {
|
|
15825
15969
|
fs4.unlinkSync(backupPath);
|
|
15826
15970
|
} catch {
|
|
15827
15971
|
}
|
|
15828
15972
|
} else {
|
|
15829
|
-
|
|
15973
|
+
atomicWriteFileSyncWithTmp(filePath, tmpPath, payload, {
|
|
15974
|
+
encoding: "utf-8",
|
|
15975
|
+
mode: 420
|
|
15976
|
+
});
|
|
15830
15977
|
}
|
|
15831
15978
|
return { action: existingAction, filePath, layout };
|
|
15832
15979
|
} catch (err) {
|
|
@@ -15905,6 +16052,7 @@ var init_discovery_file = __esm({
|
|
|
15905
16052
|
fs4 = __toESM(require("node:fs"), 1);
|
|
15906
16053
|
path4 = __toESM(require("node:path"), 1);
|
|
15907
16054
|
init_dist();
|
|
16055
|
+
init_atomic_write();
|
|
15908
16056
|
WELL_KNOWN_GLASSTRACE_PATH = ".well-known/glasstrace.json";
|
|
15909
16057
|
DISCOVERY_FILE_VERSION = 1;
|
|
15910
16058
|
}
|
|
@@ -16232,21 +16380,11 @@ function writeShutdownMarker(projectRoot) {
|
|
|
16232
16380
|
return false;
|
|
16233
16381
|
}
|
|
16234
16382
|
const markerPath = path5.join(dirPath, "shutdown-requested");
|
|
16235
|
-
const tmpPath = `${markerPath}.tmp`;
|
|
16236
16383
|
const body = JSON.stringify({ requestedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
16237
16384
|
try {
|
|
16238
|
-
|
|
16239
|
-
try {
|
|
16240
|
-
fs5.chmodSync(tmpPath, 384);
|
|
16241
|
-
} catch {
|
|
16242
|
-
}
|
|
16243
|
-
fs5.renameSync(tmpPath, markerPath);
|
|
16385
|
+
atomicWriteFileSync(markerPath, body, { encoding: "utf-8", mode: 384 });
|
|
16244
16386
|
return true;
|
|
16245
16387
|
} catch {
|
|
16246
|
-
try {
|
|
16247
|
-
fs5.unlinkSync(tmpPath);
|
|
16248
|
-
} catch {
|
|
16249
|
-
}
|
|
16250
16388
|
return false;
|
|
16251
16389
|
}
|
|
16252
16390
|
}
|
|
@@ -16612,6 +16750,7 @@ var init_uninit = __esm({
|
|
|
16612
16750
|
path5 = __toESM(require("node:path"), 1);
|
|
16613
16751
|
init_constants();
|
|
16614
16752
|
init_mcp_runtime();
|
|
16753
|
+
init_atomic_write();
|
|
16615
16754
|
init_discovery_file();
|
|
16616
16755
|
MCP_CONFIG_FILES = [".mcp.json", ".cursor/mcp.json", ".gemini/settings.json"];
|
|
16617
16756
|
AGENT_INFO_FILES = [
|
|
@@ -18687,7 +18826,7 @@ async function verifyAnonKeyRegistration(projectRoot) {
|
|
|
18687
18826
|
}
|
|
18688
18827
|
const baseConfig = resolveConfig({ apiKey: devKey });
|
|
18689
18828
|
const config2 = { ...baseConfig, apiKey: devKey };
|
|
18690
|
-
const sdkVersion = true ? "1.1.
|
|
18829
|
+
const sdkVersion = true ? "1.1.3" : "0.0.0-dev";
|
|
18691
18830
|
const result = await verifyInitReachable(config2, anonKey, sdkVersion);
|
|
18692
18831
|
if (result.ok) {
|
|
18693
18832
|
return { outcome: "verified" };
|