@brainmcp/brainmcp 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +72 -5
  2. package/dist/agent-config.js +229 -32
  3. package/dist/commands/auth.d.ts +47 -0
  4. package/dist/commands/auth.js +390 -0
  5. package/dist/commands/auth.js.map +1 -0
  6. package/dist/commands/capture.d.ts +34 -0
  7. package/dist/commands/capture.js +349 -0
  8. package/dist/commands/capture.js.map +1 -0
  9. package/dist/commands/dispatch.d.ts +43 -0
  10. package/dist/commands/dispatch.js +287 -0
  11. package/dist/commands/dispatch.js.map +1 -0
  12. package/dist/commands/index.d.ts +4 -0
  13. package/dist/commands/index.js +180 -36
  14. package/dist/commands/index.js.map +1 -1
  15. package/dist/hooks/adapters.d.ts +14 -0
  16. package/dist/hooks/adapters.js +41 -0
  17. package/dist/hooks/adapters.js.map +1 -0
  18. package/dist/hooks/claude.d.ts +24 -0
  19. package/dist/hooks/claude.js +36 -0
  20. package/dist/hooks/claude.js.map +1 -0
  21. package/dist/hooks/codex.d.ts +18 -0
  22. package/dist/hooks/codex.js +36 -0
  23. package/dist/hooks/codex.js.map +1 -0
  24. package/dist/hooks/cursor.d.ts +22 -0
  25. package/dist/hooks/cursor.js +26 -0
  26. package/dist/hooks/cursor.js.map +1 -0
  27. package/dist/hooks/managed-hooks.d.ts +30 -0
  28. package/dist/hooks/managed-hooks.js +189 -0
  29. package/dist/hooks/managed-hooks.js.map +1 -0
  30. package/dist/hooks/vscode.d.ts +22 -0
  31. package/dist/hooks/vscode.js +27 -0
  32. package/dist/hooks/vscode.js.map +1 -0
  33. package/dist/index.js +7 -102
  34. package/dist/index.js.map +1 -1
  35. package/dist/lib/api-client.d.ts +32 -0
  36. package/dist/lib/api-client.js +203 -0
  37. package/dist/lib/api-client.js.map +1 -0
  38. package/dist/lib/callback-server.d.ts +17 -0
  39. package/dist/lib/callback-server.js +145 -0
  40. package/dist/lib/callback-server.js.map +1 -0
  41. package/dist/lib/cli-contracts.d.ts +193 -0
  42. package/dist/lib/cli-contracts.js +242 -0
  43. package/dist/lib/cli-contracts.js.map +1 -0
  44. package/dist/lib/config.d.ts +4 -2
  45. package/dist/lib/config.js +11 -0
  46. package/dist/lib/config.js.map +1 -1
  47. package/dist/lib/credentials.d.ts +89 -0
  48. package/dist/lib/credentials.js +304 -0
  49. package/dist/lib/credentials.js.map +1 -0
  50. package/dist/lib/http.d.ts +3 -0
  51. package/dist/lib/http.js +50 -0
  52. package/dist/lib/http.js.map +1 -0
  53. package/dist/lib/managed.d.ts +8 -0
  54. package/dist/lib/managed.js +72 -6
  55. package/dist/lib/managed.js.map +1 -1
  56. package/dist/lib/oauth-client.d.ts +55 -0
  57. package/dist/lib/oauth-client.js +154 -0
  58. package/dist/lib/oauth-client.js.map +1 -0
  59. package/dist/lib/pkce.d.ts +14 -0
  60. package/dist/lib/pkce.js +33 -0
  61. package/dist/lib/pkce.js.map +1 -0
  62. package/dist/lib/vault.d.ts +26 -0
  63. package/dist/lib/vault.js +221 -0
  64. package/dist/lib/vault.js.map +1 -0
  65. package/dist/runtime/brainmcp-hook-runtime.mjs +3211 -0
  66. package/dist/runtime/capture.d.ts +83 -0
  67. package/dist/runtime/capture.js +332 -0
  68. package/dist/runtime/capture.js.map +1 -0
  69. package/dist/runtime/diagnostics.d.ts +7 -0
  70. package/dist/runtime/diagnostics.js +75 -0
  71. package/dist/runtime/diagnostics.js.map +1 -0
  72. package/dist/runtime/doctor-checks.d.ts +14 -0
  73. package/dist/runtime/doctor-checks.js +205 -0
  74. package/dist/runtime/doctor-checks.js.map +1 -0
  75. package/dist/runtime/flush.d.ts +30 -0
  76. package/dist/runtime/flush.js +193 -0
  77. package/dist/runtime/flush.js.map +1 -0
  78. package/dist/runtime/hook-dispatcher.d.ts +31 -0
  79. package/dist/runtime/hook-dispatcher.js +381 -0
  80. package/dist/runtime/hook-dispatcher.js.map +1 -0
  81. package/dist/runtime/index.d.ts +12 -0
  82. package/dist/runtime/index.js +69 -0
  83. package/dist/runtime/index.js.map +1 -0
  84. package/dist/runtime/installer.d.ts +26 -0
  85. package/dist/runtime/installer.js +134 -0
  86. package/dist/runtime/installer.js.map +1 -0
  87. package/dist/runtime/lock.d.ts +19 -0
  88. package/dist/runtime/lock.js +116 -0
  89. package/dist/runtime/lock.js.map +1 -0
  90. package/dist/runtime/orient.d.ts +55 -0
  91. package/dist/runtime/orient.js +255 -0
  92. package/dist/runtime/orient.js.map +1 -0
  93. package/dist/runtime/queue.d.ts +7 -0
  94. package/dist/runtime/queue.js +80 -0
  95. package/dist/runtime/queue.js.map +1 -0
  96. package/dist/runtime/retention.d.ts +26 -0
  97. package/dist/runtime/retention.js +175 -0
  98. package/dist/runtime/retention.js.map +1 -0
  99. package/dist/runtime/session.d.ts +68 -0
  100. package/dist/runtime/session.js +283 -0
  101. package/dist/runtime/session.js.map +1 -0
  102. package/dist/runtime/storage.d.ts +32 -0
  103. package/dist/runtime/storage.js +236 -0
  104. package/dist/runtime/storage.js.map +1 -0
  105. package/package.json +3 -2
@@ -0,0 +1,3211 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/runtime/index.ts
4
+ import fs11 from "node:fs";
5
+ import path11 from "node:path";
6
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
7
+
8
+ // src/runtime/hook-dispatcher.ts
9
+ import crypto3 from "node:crypto";
10
+
11
+ // src/lib/cli-contracts.ts
12
+ var PREFERENCES_SCHEMA_VERSION = 1;
13
+ var SESSION_LEDGER_SCHEMA_VERSION = 1;
14
+ var QUEUE_SCHEMA_VERSION = 1;
15
+ function validatePreferencesRecord(data) {
16
+ if (!data || typeof data !== "object") {
17
+ throw new Error("Preferences must be an object");
18
+ }
19
+ const obj = data;
20
+ if (obj.version !== PREFERENCES_SCHEMA_VERSION) {
21
+ throw new Error(`Unsupported preferences schema version: ${String(obj.version)}`);
22
+ }
23
+ if (!obj.consent || typeof obj.consent !== "object") {
24
+ throw new Error("Preferences must include a consent object");
25
+ }
26
+ const consent = obj.consent;
27
+ if (typeof consent.captureEnabled !== "boolean") {
28
+ throw new Error("consent.captureEnabled must be a boolean");
29
+ }
30
+ if (typeof consent.includePrompts !== "boolean") {
31
+ throw new Error("consent.includePrompts must be a boolean");
32
+ }
33
+ if (typeof consent.policyVersion !== "string" || !consent.policyVersion) {
34
+ throw new Error("consent.policyVersion must be a non-empty string");
35
+ }
36
+ return {
37
+ version: PREFERENCES_SCHEMA_VERSION,
38
+ consent: {
39
+ captureEnabled: consent.captureEnabled,
40
+ includePrompts: consent.includePrompts,
41
+ policyVersion: consent.policyVersion,
42
+ consentedAt: typeof consent.consentedAt === "string" ? consent.consentedAt : void 0,
43
+ revision: typeof consent.revision === "number" && Number.isSafeInteger(consent.revision) ? Math.max(0, consent.revision) : 0
44
+ },
45
+ enabledWorkspaceId: typeof obj.enabledWorkspaceId === "string" ? obj.enabledWorkspaceId : void 0,
46
+ nudgeEnabled: typeof obj.nudgeEnabled === "boolean" ? obj.nudgeEnabled : false
47
+ };
48
+ }
49
+ function validateSessionLedgerRecord(data) {
50
+ if (!data || typeof data !== "object") {
51
+ throw new Error("Session ledger must be an object");
52
+ }
53
+ const obj = data;
54
+ if (obj.version !== SESSION_LEDGER_SCHEMA_VERSION) {
55
+ throw new Error(`Unsupported session ledger schema version: ${String(obj.version)}`);
56
+ }
57
+ if (typeof obj.sessionKey !== "string" || !obj.sessionKey) {
58
+ throw new Error("Session ledger requires a sessionKey");
59
+ }
60
+ return {
61
+ version: SESSION_LEDGER_SCHEMA_VERSION,
62
+ sessionKey: obj.sessionKey,
63
+ createdAt: typeof obj.createdAt === "string" ? obj.createdAt : (/* @__PURE__ */ new Date()).toISOString(),
64
+ lastEventAt: typeof obj.lastEventAt === "string" ? obj.lastEventAt : (/* @__PURE__ */ new Date()).toISOString(),
65
+ eventSequence: typeof obj.eventSequence === "number" ? obj.eventSequence : 0,
66
+ lastMeaningfulAt: typeof obj.lastMeaningfulAt === "string" ? obj.lastMeaningfulAt : void 0,
67
+ lastMeaningfulSequence: typeof obj.lastMeaningfulSequence === "number" ? obj.lastMeaningfulSequence : void 0,
68
+ lastSuccessfulWriteBackAt: typeof obj.lastSuccessfulWriteBackAt === "string" ? obj.lastSuccessfulWriteBackAt : void 0,
69
+ lastSuccessfulWriteBackSequence: typeof obj.lastSuccessfulWriteBackSequence === "number" ? obj.lastSuccessfulWriteBackSequence : void 0,
70
+ lastCapturedAt: typeof obj.lastCapturedAt === "string" ? obj.lastCapturedAt : void 0,
71
+ lastCapturedSequence: typeof obj.lastCapturedSequence === "number" ? obj.lastCapturedSequence : void 0,
72
+ lastNudgedAt: typeof obj.lastNudgedAt === "string" ? obj.lastNudgedAt : void 0,
73
+ lastNudgedSequence: typeof obj.lastNudgedSequence === "number" ? obj.lastNudgedSequence : void 0,
74
+ editedPaths: Array.isArray(obj.editedPaths) ? obj.editedPaths.filter((p) => typeof p === "string").slice(0, 100) : [],
75
+ prompts: Array.isArray(obj.prompts) ? obj.prompts.filter((p) => typeof p === "string").slice(0, 50) : void 0,
76
+ assistantSummary: typeof obj.assistantSummary === "string" ? obj.assistantSummary.slice(0, 4e3) : void 0,
77
+ model: typeof obj.model === "string" ? obj.model.slice(0, 120) : void 0,
78
+ deliveryIds: Array.isArray(obj.deliveryIds) ? obj.deliveryIds.filter((d) => typeof d === "string").slice(-100) : [],
79
+ captureRevision: typeof obj.captureRevision === "number" ? obj.captureRevision : void 0,
80
+ projectKey: typeof obj.projectKey === "string" ? obj.projectKey : void 0,
81
+ workspaceId: typeof obj.workspaceId === "string" ? obj.workspaceId : void 0
82
+ };
83
+ }
84
+ function validateQueueItemRecord(data) {
85
+ if (!data || typeof data !== "object") {
86
+ throw new Error("Queue item must be an object");
87
+ }
88
+ const obj = data;
89
+ if (obj.version !== QUEUE_SCHEMA_VERSION) {
90
+ throw new Error(`Unsupported queue schema version: ${String(obj.version)}`);
91
+ }
92
+ if (typeof obj.digestId !== "string" || !obj.digestId) {
93
+ throw new Error("Queue item requires a digestId");
94
+ }
95
+ if (typeof obj.workspaceId !== "string" || !obj.workspaceId) {
96
+ throw new Error("Queue item requires a workspaceId");
97
+ }
98
+ return {
99
+ version: QUEUE_SCHEMA_VERSION,
100
+ digestId: obj.digestId,
101
+ workspaceId: obj.workspaceId,
102
+ captureTrigger: obj.captureTrigger ?? "manual",
103
+ policyVersion: typeof obj.policyVersion === "string" ? obj.policyVersion : "2026-09-02.capture-v1",
104
+ digestText: typeof obj.digestText === "string" ? obj.digestText : "",
105
+ authorizationId: typeof obj.authorizationId === "string" ? obj.authorizationId : void 0,
106
+ consentRevision: typeof obj.consentRevision === "number" && Number.isSafeInteger(obj.consentRevision) ? Math.max(0, obj.consentRevision) : void 0,
107
+ status: obj.status ?? "pending",
108
+ attempts: typeof obj.attempts === "number" ? obj.attempts : 0,
109
+ nextRetryAt: typeof obj.nextRetryAt === "string" ? obj.nextRetryAt : void 0,
110
+ lastError: typeof obj.lastError === "string" ? obj.lastError : void 0,
111
+ createdAt: typeof obj.createdAt === "string" ? obj.createdAt : (/* @__PURE__ */ new Date()).toISOString(),
112
+ updatedAt: typeof obj.updatedAt === "string" ? obj.updatedAt : typeof obj.createdAt === "string" ? obj.createdAt : (/* @__PURE__ */ new Date()).toISOString(),
113
+ deliveredAt: typeof obj.deliveredAt === "string" ? obj.deliveredAt : void 0,
114
+ deliveryLease: obj.deliveryLease && typeof obj.deliveryLease === "object" && typeof obj.deliveryLease.owner === "string" && typeof obj.deliveryLease.expiresAt === "string" ? obj.deliveryLease : void 0
115
+ };
116
+ }
117
+ function scrubSecrets(input) {
118
+ return input.replace(/\b(bearer\s+)?([A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,})\b/gi, "[redacted-jwt]").replace(/\b(AKIA[0-9A-Z]{16})\b/g, "[redacted-access-key]").replace(/(password|passwd|api[_-]?key|secret|token)\s*[:=]\s*([^\s"'`<>{}]{8,})/gi, "$1=[redacted-secret]");
119
+ }
120
+ function filterRelativePaths(paths, rootDir) {
121
+ const normalizedRoot = rootDir.replace(/\\/g, "/").replace(/\/+$/, "");
122
+ const out = [];
123
+ for (const raw of paths) {
124
+ const p = raw.replace(/\\/g, "/");
125
+ if (p.startsWith("/") || /^[a-zA-Z]:\//.test(p)) {
126
+ if (p.startsWith(normalizedRoot + "/")) {
127
+ out.push(p.slice(normalizedRoot.length + 1));
128
+ }
129
+ } else if (!p.startsWith("../") && !p.includes("/../")) {
130
+ out.push(p);
131
+ }
132
+ }
133
+ return out;
134
+ }
135
+
136
+ // src/hooks/claude.ts
137
+ function parseClaudeEvent(raw, eventName = "SessionStart") {
138
+ const input = raw && typeof raw === "object" ? raw : {};
139
+ return {
140
+ client: "claude-code",
141
+ event: eventName,
142
+ sessionKey: input.session_id || "default_session",
143
+ cwd: input.cwd,
144
+ source: input.source,
145
+ transcriptPath: input.transcript_path,
146
+ prompt: input.prompt,
147
+ toolName: input.tool_name,
148
+ toolInput: input.tool_input,
149
+ toolResult: input.tool_response ?? input.tool_result,
150
+ stopHookActive: Boolean(input.stop_hook_active),
151
+ model: input.model
152
+ };
153
+ }
154
+ function emitClaudeOutput(event, additionalContext) {
155
+ if (event.event === "StopNudge" && !event.stopHookActive && additionalContext) {
156
+ return { decision: "block", reason: additionalContext };
157
+ }
158
+ if (event.event === "Stop" || event.stopHookActive) {
159
+ return {};
160
+ }
161
+ if (event.event === "SessionStart" && additionalContext) {
162
+ return {
163
+ hookSpecificOutput: {
164
+ hookEventName: "SessionStart",
165
+ additionalContext
166
+ }
167
+ };
168
+ }
169
+ return {};
170
+ }
171
+
172
+ // src/hooks/codex.ts
173
+ function parseCodexEvent(raw, eventName = "SessionStart") {
174
+ const input = raw && typeof raw === "object" ? raw : {};
175
+ return {
176
+ client: "codex",
177
+ event: eventName,
178
+ sessionKey: input.session_id || "default_session",
179
+ cwd: input.cwd,
180
+ source: input.source,
181
+ status: input.status ?? input.reason,
182
+ prompt: input.prompt,
183
+ toolName: input.tool_name,
184
+ toolInput: input.tool_input,
185
+ toolResult: input.tool_result ?? input.tool_response,
186
+ model: input.model
187
+ };
188
+ }
189
+ function emitCodexOutput(event, additionalContext) {
190
+ if (event.event === "SessionStart" && additionalContext) {
191
+ return {
192
+ hookSpecificOutput: {
193
+ hookEventName: "SessionStart",
194
+ additionalContext
195
+ }
196
+ };
197
+ }
198
+ return {};
199
+ }
200
+
201
+ // src/hooks/cursor.ts
202
+ function parseCursorEvent(raw, eventName = "sessionStart") {
203
+ const input = raw && typeof raw === "object" ? raw : {};
204
+ return {
205
+ client: "cursor",
206
+ event: eventName,
207
+ sessionKey: input.session_id || input.conversation_id || "default_session",
208
+ workspaceRoots: input.workspace_roots,
209
+ cwd: input.cwd,
210
+ prompt: input.prompt,
211
+ assistantText: input.text,
212
+ toolName: input.tool_name,
213
+ toolInput: input.tool_input,
214
+ toolResult: input.tool_result ?? input.tool_output,
215
+ status: input.final_status ?? input.reason,
216
+ model: input.model_id ?? input.model
217
+ };
218
+ }
219
+ function emitCursorOutput(event, additionalContext) {
220
+ if (event.event === "sessionStart" && additionalContext) {
221
+ return {
222
+ additional_context: additionalContext
223
+ };
224
+ }
225
+ return {};
226
+ }
227
+
228
+ // src/hooks/vscode.ts
229
+ function parseVsCodeEvent(raw, eventName = "sessionStart") {
230
+ const input = raw && typeof raw === "object" ? raw : {};
231
+ return {
232
+ client: "vscode",
233
+ event: eventName,
234
+ sessionKey: input.session_id || input.sessionId || "default_session",
235
+ cwd: input.cwd || input.workspace,
236
+ source: input.source,
237
+ prompt: input.prompt,
238
+ toolName: input.tool_name,
239
+ toolInput: input.tool_input,
240
+ toolResult: input.tool_result ?? input.tool_response,
241
+ model: input.model
242
+ };
243
+ }
244
+ function emitVsCodeOutput(event, additionalContext) {
245
+ if ((event.event === "SessionStart" || event.event === "sessionStart") && additionalContext) {
246
+ return {
247
+ hookSpecificOutput: {
248
+ hookEventName: "SessionStart",
249
+ additionalContext
250
+ }
251
+ };
252
+ }
253
+ return {};
254
+ }
255
+
256
+ // src/hooks/adapters.ts
257
+ function parseHostEvent(client, raw, eventName) {
258
+ switch (client) {
259
+ case "claude-code":
260
+ return parseClaudeEvent(raw, eventName);
261
+ case "codex":
262
+ return parseCodexEvent(raw, eventName);
263
+ case "cursor":
264
+ return parseCursorEvent(raw, eventName);
265
+ case "vscode":
266
+ return parseVsCodeEvent(raw, eventName);
267
+ default:
268
+ throw new Error(`Unsupported hook host: ${client}`);
269
+ }
270
+ }
271
+ function emitHostOutput(client, event, additionalContext) {
272
+ switch (client) {
273
+ case "claude-code":
274
+ return emitClaudeOutput(event, additionalContext);
275
+ case "codex":
276
+ return emitCodexOutput(event, additionalContext);
277
+ case "cursor":
278
+ return emitCursorOutput(event, additionalContext);
279
+ case "vscode":
280
+ return emitVsCodeOutput(event, additionalContext);
281
+ default:
282
+ return {};
283
+ }
284
+ }
285
+
286
+ // src/runtime/session.ts
287
+ import fs6 from "node:fs";
288
+ import path6 from "node:path";
289
+ import { createHash } from "node:crypto";
290
+
291
+ // src/runtime/queue.ts
292
+ import fs2 from "node:fs";
293
+ import path2 from "node:path";
294
+
295
+ // src/runtime/storage.ts
296
+ import fs from "node:fs";
297
+ import path from "node:path";
298
+ import os from "node:os";
299
+ import crypto from "node:crypto";
300
+ var CURRENT_ACCOUNT_SCHEMA_VERSION = 1;
301
+ var SERVICE_ACCOUNT_ID = "service";
302
+ var ACCOUNTS_DIR_NAME = "accounts";
303
+ var CURRENT_ACCOUNT_FILENAME = "current.json";
304
+ var LEGACY_STATE_NAMES = [
305
+ "preferences.json",
306
+ "credentials.json",
307
+ "credentials.lock",
308
+ "credentials.backend.json",
309
+ "queue",
310
+ "sessions",
311
+ "cache",
312
+ "locks",
313
+ "logs"
314
+ ];
315
+ function getBrainHome() {
316
+ const override = process.env.BRAINMCP_HOME ?? process.env.BRAIN_HOME;
317
+ if (override) {
318
+ return path.resolve(override);
319
+ }
320
+ return path.join(os.homedir(), ".brainmcp");
321
+ }
322
+ function sanitizeAccountId(accountId) {
323
+ const trimmed = accountId.trim();
324
+ if (!trimmed) {
325
+ throw new Error("Account id is required");
326
+ }
327
+ if (/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(trimmed) && !trimmed.includes("..")) {
328
+ return trimmed;
329
+ }
330
+ const digest = crypto.createHash("sha256").update(trimmed).digest("hex").slice(0, 32);
331
+ return `id-${digest}`;
332
+ }
333
+ function getCurrentAccountPath(brainHome) {
334
+ return path.join(brainHome ?? getBrainHome(), CURRENT_ACCOUNT_FILENAME);
335
+ }
336
+ function validateCurrentAccountRecord(data) {
337
+ if (!data || typeof data !== "object") {
338
+ throw new Error("Current account pointer must be an object");
339
+ }
340
+ const obj = data;
341
+ if (obj.version !== CURRENT_ACCOUNT_SCHEMA_VERSION) {
342
+ throw new Error(`Unsupported current-account schema version: ${String(obj.version)}`);
343
+ }
344
+ if (typeof obj.accountId !== "string" || !obj.accountId.trim()) {
345
+ throw new Error("Current account pointer requires accountId");
346
+ }
347
+ return {
348
+ version: CURRENT_ACCOUNT_SCHEMA_VERSION,
349
+ accountId: sanitizeAccountId(obj.accountId),
350
+ updatedAt: typeof obj.updatedAt === "string" ? obj.updatedAt : (/* @__PURE__ */ new Date()).toISOString()
351
+ };
352
+ }
353
+ function getCurrentAccountId(brainHome) {
354
+ const record = readSafeJson(getCurrentAccountPath(brainHome), validateCurrentAccountRecord);
355
+ return record?.accountId ?? null;
356
+ }
357
+ function getAccountStateDir(brainHome, accountId) {
358
+ const home = path.resolve(brainHome);
359
+ const id = sanitizeAccountId(accountId);
360
+ return path.join(home, ACCOUNTS_DIR_NAME, id);
361
+ }
362
+ function resolveStateDir(brainHome) {
363
+ const home = brainHome ?? getBrainHome();
364
+ const accountId = getCurrentAccountId(home);
365
+ if (!accountId) return home;
366
+ const accountsDir = path.join(path.resolve(home), ACCOUNTS_DIR_NAME);
367
+ ensureDirectory0700(accountsDir);
368
+ const dir = getAccountStateDir(home, accountId);
369
+ ensureDirectory0700(dir);
370
+ return dir;
371
+ }
372
+ function switchCurrentAccount(brainHome, accountId) {
373
+ const home = path.resolve(brainHome);
374
+ ensureDirectory0700(home);
375
+ const id = sanitizeAccountId(accountId);
376
+ const accountsDir = path.join(home, ACCOUNTS_DIR_NAME);
377
+ ensureDirectory0700(accountsDir);
378
+ const stateDir = getAccountStateDir(home, id);
379
+ ensureDirectory0700(stateDir);
380
+ writeAtomic0600(
381
+ getCurrentAccountPath(home),
382
+ JSON.stringify(
383
+ {
384
+ version: CURRENT_ACCOUNT_SCHEMA_VERSION,
385
+ accountId: id,
386
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
387
+ },
388
+ null,
389
+ 2
390
+ )
391
+ );
392
+ return stateDir;
393
+ }
394
+ function clearCurrentAccount(brainHome) {
395
+ const currentPath = getCurrentAccountPath(brainHome);
396
+ if (!fs.existsSync(currentPath)) return;
397
+ assertNotSymlink(currentPath);
398
+ fs.unlinkSync(currentPath);
399
+ }
400
+ function migrateLegacyFlatState(brainHome, accountId) {
401
+ const home = path.resolve(brainHome);
402
+ const dest = getAccountStateDir(home, accountId);
403
+ ensureDirectory0700(path.join(home, ACCOUNTS_DIR_NAME));
404
+ ensureDirectory0700(dest);
405
+ const migrated = [];
406
+ for (const name of LEGACY_STATE_NAMES) {
407
+ const from = path.join(home, name);
408
+ const to = path.join(dest, name);
409
+ if (!fs.existsSync(from) || fs.existsSync(to)) continue;
410
+ assertNotSymlink(from);
411
+ fs.renameSync(from, to);
412
+ migrated.push(name);
413
+ }
414
+ return migrated;
415
+ }
416
+ function activateAccount(brainHome, accountId) {
417
+ const migratedInto = switchCurrentAccount(brainHome, accountId);
418
+ migrateLegacyFlatState(brainHome, accountId);
419
+ return migratedInto;
420
+ }
421
+ function ensureDirectory0700(dirPath) {
422
+ const resolved = path.resolve(dirPath);
423
+ if (fs.existsSync(resolved)) {
424
+ const stat = fs.lstatSync(resolved);
425
+ if (stat.isSymbolicLink()) {
426
+ throw new Error(`Security violation: directory ${resolved} is a symbolic link`);
427
+ }
428
+ if (!stat.isDirectory()) {
429
+ throw new Error(`Path ${resolved} is not a directory`);
430
+ }
431
+ if (process.platform !== "win32") {
432
+ const mode = stat.mode & 511;
433
+ if (mode !== 448) {
434
+ fs.chmodSync(resolved, 448);
435
+ }
436
+ }
437
+ return;
438
+ }
439
+ let existingParent = path.dirname(resolved);
440
+ while (!fs.existsSync(existingParent)) {
441
+ const next = path.dirname(existingParent);
442
+ if (next === existingParent) break;
443
+ existingParent = next;
444
+ }
445
+ if (fs.existsSync(existingParent) && fs.lstatSync(existingParent).isSymbolicLink()) {
446
+ throw new Error(`Security violation: directory parent ${existingParent} is a symbolic link`);
447
+ }
448
+ fs.mkdirSync(resolved, { recursive: true, mode: 448 });
449
+ }
450
+ function assertNotSymlink(targetPath) {
451
+ const resolved = path.resolve(targetPath);
452
+ const stat = fs.lstatSync(resolved, { throwIfNoEntry: false });
453
+ if (stat?.isSymbolicLink()) {
454
+ throw new Error(`Security violation: ${resolved} is a symbolic link`);
455
+ }
456
+ }
457
+ function writeAtomic0600(filePath, content) {
458
+ const resolved = path.resolve(filePath);
459
+ const dir = path.dirname(resolved);
460
+ ensureDirectory0700(dir);
461
+ assertNotSymlink(resolved);
462
+ const tmpPath = `${resolved}.tmp.${crypto.randomUUID()}`;
463
+ const noFollow = typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0;
464
+ try {
465
+ const fd = fs.openSync(
466
+ tmpPath,
467
+ fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY | noFollow,
468
+ 384
469
+ );
470
+ try {
471
+ fs.writeSync(fd, content, 0, "utf8");
472
+ fs.fsyncSync(fd);
473
+ } finally {
474
+ fs.closeSync(fd);
475
+ }
476
+ fs.renameSync(tmpPath, resolved);
477
+ } catch (error) {
478
+ try {
479
+ fs.unlinkSync(tmpPath);
480
+ } catch {
481
+ }
482
+ throw error;
483
+ }
484
+ if (process.platform !== "win32") {
485
+ fs.chmodSync(resolved, 384);
486
+ }
487
+ }
488
+ function readSafeJson(filePath, validator) {
489
+ const resolved = path.resolve(filePath);
490
+ if (!fs.existsSync(resolved)) {
491
+ return null;
492
+ }
493
+ assertNotSymlink(resolved);
494
+ const stat = fs.lstatSync(resolved);
495
+ if (!stat.isFile()) {
496
+ throw new Error(`Security violation: ${resolved} is not a regular file`);
497
+ }
498
+ if (process.platform !== "win32") {
499
+ const mode = stat.mode & 511;
500
+ if (mode !== 384) {
501
+ throw new Error(`Security violation: ${resolved} permissions are ${mode.toString(8)}, expected 600`);
502
+ }
503
+ if (process.getuid && stat.uid !== process.getuid()) {
504
+ throw new Error(`Security violation: ${resolved} is not owned by the current user`);
505
+ }
506
+ }
507
+ const raw = fs.readFileSync(resolved, "utf8");
508
+ try {
509
+ const parsed = JSON.parse(raw);
510
+ if (validator) {
511
+ return validator(parsed);
512
+ }
513
+ return parsed;
514
+ } catch (err) {
515
+ throw new Error(`Corrupt or invalid JSON in ${resolved}: ${err instanceof Error ? err.message : String(err)}`);
516
+ }
517
+ }
518
+ function getPreferencesPath(baseDir) {
519
+ return path.join(resolveStateDir(baseDir), "preferences.json");
520
+ }
521
+ function readPreferences(baseDir) {
522
+ return readSafeJson(getPreferencesPath(baseDir), validatePreferencesRecord);
523
+ }
524
+ function writePreferences(prefs, baseDir) {
525
+ writeAtomic0600(getPreferencesPath(baseDir), JSON.stringify(prefs, null, 2));
526
+ }
527
+
528
+ // src/runtime/queue.ts
529
+ function getQueueDir(baseDir) {
530
+ const queueDir = path2.join(resolveStateDir(baseDir), "queue");
531
+ ensureDirectory0700(queueDir);
532
+ return queueDir;
533
+ }
534
+ function getQueueItemPath(digestId, baseDir) {
535
+ const queueDir = getQueueDir(baseDir);
536
+ const safeId = digestId.replace(/[^a-zA-Z0-9_-]/g, "_");
537
+ return path2.join(queueDir, `${safeId}.json`);
538
+ }
539
+ function enqueueCaptureItem(item, baseDir) {
540
+ const now = (/* @__PURE__ */ new Date()).toISOString();
541
+ const record = {
542
+ version: QUEUE_SCHEMA_VERSION,
543
+ digestId: item.digestId,
544
+ workspaceId: item.workspaceId,
545
+ captureTrigger: item.captureTrigger,
546
+ policyVersion: item.policyVersion,
547
+ digestText: item.digestText,
548
+ authorizationId: item.authorizationId,
549
+ consentRevision: item.consentRevision,
550
+ status: "pending",
551
+ attempts: 0,
552
+ createdAt: now,
553
+ updatedAt: now
554
+ };
555
+ const filePath = getQueueItemPath(record.digestId, baseDir);
556
+ writeAtomic0600(filePath, JSON.stringify(record, null, 2));
557
+ return record;
558
+ }
559
+ function listQueueItems(baseDir) {
560
+ const queueDir = getQueueDir(baseDir);
561
+ const entries = fs2.readdirSync(queueDir);
562
+ const items = [];
563
+ for (const entry of entries) {
564
+ if (!entry.endsWith(".json")) continue;
565
+ const filePath = path2.join(queueDir, entry);
566
+ try {
567
+ assertNotSymlink(filePath);
568
+ const item = readSafeJson(filePath, validateQueueItemRecord);
569
+ if (item) {
570
+ items.push(item);
571
+ }
572
+ } catch {
573
+ }
574
+ }
575
+ return items.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
576
+ }
577
+ function updateQueueItem(digestId, updates, baseDir) {
578
+ const filePath = getQueueItemPath(digestId, baseDir);
579
+ const existing = readSafeJson(filePath, validateQueueItemRecord);
580
+ if (!existing) {
581
+ return null;
582
+ }
583
+ const updated = {
584
+ ...existing,
585
+ ...updates,
586
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
587
+ };
588
+ writeAtomic0600(filePath, JSON.stringify(updated, null, 2));
589
+ return updated;
590
+ }
591
+ function removeQueueItem(digestId, baseDir) {
592
+ const filePath = getQueueItemPath(digestId, baseDir);
593
+ if (fs2.existsSync(filePath)) {
594
+ assertNotSymlink(filePath);
595
+ fs2.unlinkSync(filePath);
596
+ return true;
597
+ }
598
+ return false;
599
+ }
600
+
601
+ // src/runtime/diagnostics.ts
602
+ import fs3 from "node:fs";
603
+ import path3 from "node:path";
604
+ var SENSITIVE_KEY_PATTERNS = [
605
+ /token/i,
606
+ /bearer/i,
607
+ /auth/i,
608
+ /secret/i,
609
+ /cookie/i,
610
+ /password/i,
611
+ /verifier/i,
612
+ /code/i,
613
+ /prompt/i,
614
+ /assistant/i,
615
+ /content/i,
616
+ /(api|secret|private|access|auth)key$/i,
617
+ /^key$/i
618
+ ];
619
+ function scrubMetadata(value) {
620
+ if (value === null || value === void 0) {
621
+ return value;
622
+ }
623
+ if (typeof value !== "object") {
624
+ if (typeof value === "string") {
625
+ if (/bearer\s+[a-zA-Z0-9._-]+/i.test(value) || /brain_[a-zA-Z0-9_-]{20,}/i.test(value)) {
626
+ return "[REDACTED_TOKEN]";
627
+ }
628
+ }
629
+ return value;
630
+ }
631
+ if (Array.isArray(value)) {
632
+ return value.map(scrubMetadata);
633
+ }
634
+ const result = {};
635
+ for (const [k, v] of Object.entries(value)) {
636
+ if (SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(k))) {
637
+ result[k] = "[REDACTED]";
638
+ } else {
639
+ result[k] = scrubMetadata(v);
640
+ }
641
+ }
642
+ return result;
643
+ }
644
+ function getLogsDir(baseDir) {
645
+ const logsDir = path3.join(resolveStateDir(baseDir), "logs");
646
+ ensureDirectory0700(logsDir);
647
+ return logsDir;
648
+ }
649
+ function logRuntimeEvent(level, event, metadata, baseDir) {
650
+ try {
651
+ const logsDir = getLogsDir(baseDir);
652
+ const logFile = path3.join(logsDir, "runtime.log");
653
+ assertNotSymlink(logFile);
654
+ if (fs3.existsSync(logFile) && fs3.lstatSync(logFile).size >= 5 * 1024 * 1024) {
655
+ fs3.truncateSync(logFile, 0);
656
+ }
657
+ const entry = {
658
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
659
+ level,
660
+ event,
661
+ metadata: metadata ? scrubMetadata(metadata) : void 0
662
+ };
663
+ fs3.appendFileSync(logFile, JSON.stringify(entry) + "\n", { mode: 384 });
664
+ } catch {
665
+ }
666
+ }
667
+
668
+ // src/runtime/lock.ts
669
+ import fs4 from "node:fs";
670
+ import path4 from "node:path";
671
+ import { randomUUID } from "node:crypto";
672
+ var STALE_LOCK_MS = 5e3;
673
+ function getSessionLockPath(sessionId, baseDir) {
674
+ const locksDir = path4.join(resolveStateDir(baseDir), "locks");
675
+ ensureDirectory0700(locksDir);
676
+ const safeSessionId = sessionId.replace(/[^a-zA-Z0-9_-]/g, "_");
677
+ return path4.join(locksDir, `${safeSessionId}.lock`);
678
+ }
679
+ function isProcessAlive(pid) {
680
+ if (!Number.isInteger(pid) || pid <= 0) return false;
681
+ try {
682
+ process.kill(pid, 0);
683
+ return true;
684
+ } catch (err) {
685
+ return err.code === "EPERM";
686
+ }
687
+ }
688
+ async function acquireSessionLock(sessionId, options) {
689
+ const lockPath = getSessionLockPath(sessionId, options?.baseDir);
690
+ const timeoutMs = options?.timeoutMs ?? 5e3;
691
+ const startTime = Date.now();
692
+ const pollInterval = 20;
693
+ while (true) {
694
+ try {
695
+ assertNotSymlink(lockPath);
696
+ const metadata = {
697
+ pid: process.pid,
698
+ createdAt: Date.now(),
699
+ sessionId,
700
+ owner: randomUUID()
701
+ };
702
+ fs4.writeFileSync(lockPath, JSON.stringify(metadata), { flag: "wx", mode: 384 });
703
+ let released = false;
704
+ return {
705
+ release: () => {
706
+ if (released) return;
707
+ released = true;
708
+ try {
709
+ if (fs4.existsSync(lockPath)) {
710
+ const content = fs4.readFileSync(lockPath, "utf8");
711
+ const parsed = JSON.parse(content);
712
+ if (parsed.owner === metadata.owner) {
713
+ fs4.unlinkSync(lockPath);
714
+ }
715
+ }
716
+ } catch {
717
+ }
718
+ }
719
+ };
720
+ } catch (err) {
721
+ if (err.code === "EEXIST") {
722
+ const now = Date.now();
723
+ try {
724
+ const raw = fs4.readFileSync(lockPath, "utf8");
725
+ const meta = JSON.parse(raw);
726
+ const age = now - meta.createdAt;
727
+ if (age > STALE_LOCK_MS && !isProcessAlive(meta.pid)) {
728
+ try {
729
+ fs4.unlinkSync(lockPath);
730
+ continue;
731
+ } catch {
732
+ }
733
+ }
734
+ } catch {
735
+ try {
736
+ if (now - fs4.lstatSync(lockPath).mtimeMs > STALE_LOCK_MS) {
737
+ fs4.unlinkSync(lockPath);
738
+ continue;
739
+ }
740
+ } catch {
741
+ }
742
+ }
743
+ if (now - startTime > timeoutMs) {
744
+ throw new Error(`Timed out waiting for session lock: ${sessionId} (waited ${now - startTime}ms)`);
745
+ }
746
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
747
+ continue;
748
+ }
749
+ throw err;
750
+ }
751
+ }
752
+ }
753
+ async function withSessionLock(sessionId, fn, options) {
754
+ const lock = await acquireSessionLock(sessionId, options);
755
+ try {
756
+ return await fn();
757
+ } finally {
758
+ lock.release();
759
+ }
760
+ }
761
+
762
+ // src/runtime/flush.ts
763
+ import { randomUUID as randomUUID2 } from "node:crypto";
764
+ import fs5 from "node:fs";
765
+ import path5 from "node:path";
766
+ var MAX_CAPTURE_QUEUE_ITEMS = 50;
767
+ var MAX_CAPTURE_QUEUE_BYTES = 10 * 1024 * 1024;
768
+ var MAX_CAPTURE_ATTEMPTS = 8;
769
+ var BACKOFF_MS = [1e3, 2e3, 4e3, 8e3, 16e3, 32e3, 32e3, 32e3];
770
+ function apiCaptureTrigger(trigger) {
771
+ if (trigger === "pre_compact") return "precompact";
772
+ return trigger;
773
+ }
774
+ function classifyCaptureFailure(status, message) {
775
+ if (status === 401 || /unauthorized|invalid_token|auth_required/i.test(message ?? "")) {
776
+ return "auth_required";
777
+ }
778
+ if (status === 403 || /missing required MCP OAuth scope/i.test(message ?? "")) {
779
+ return "terminal";
780
+ }
781
+ if (status === 400 || status !== null && status >= 400 && status < 500 && status !== 429) {
782
+ return "non_retryable";
783
+ }
784
+ if (status === 429 || status !== null && status >= 500 || status === null) {
785
+ return "retryable";
786
+ }
787
+ return "retryable";
788
+ }
789
+ function nextRetryAt(attempts, now = Date.now(), jitterMs = Math.floor(Math.random() * 500)) {
790
+ const base = BACKOFF_MS[Math.min(Math.max(attempts, 1), BACKOFF_MS.length) - 1] ?? 32e3;
791
+ return new Date(now + base + jitterMs).toISOString();
792
+ }
793
+ function queueByteSize(baseDir) {
794
+ const dir = getQueueDir(baseDir);
795
+ let total = 0;
796
+ for (const entry of fs5.readdirSync(dir)) {
797
+ if (!entry.endsWith(".json")) continue;
798
+ try {
799
+ total += fs5.statSync(path5.join(dir, entry)).size;
800
+ } catch {
801
+ }
802
+ }
803
+ return total;
804
+ }
805
+ function canEnqueueCapture(baseDir) {
806
+ const items = listQueueItems(baseDir).filter((item) => item.status === "pending" || item.status === "sending" || item.status === "delivery_unknown");
807
+ if (items.length >= MAX_CAPTURE_QUEUE_ITEMS) return false;
808
+ return queueByteSize(baseDir) < MAX_CAPTURE_QUEUE_BYTES;
809
+ }
810
+ async function postDigest(apiClient, item, signal, beforeSend) {
811
+ try {
812
+ await apiClient.request(`/api/workspaces/${item.workspaceId}/digests`, {
813
+ method: "POST",
814
+ body: {
815
+ clientDigestId: item.digestId,
816
+ trigger: apiCaptureTrigger(item.captureTrigger),
817
+ capturePolicyVersion: item.policyVersion,
818
+ digestText: item.digestText.trim() || "Session capture with no assistant summary.",
819
+ source: "mcp_tool"
820
+ },
821
+ signal,
822
+ expectedAuthorizationId: item.authorizationId,
823
+ beforeSend,
824
+ timeoutMs: 5e3
825
+ });
826
+ return { ok: true, status: 200 };
827
+ } catch (error) {
828
+ const message = error instanceof Error ? error.message : String(error);
829
+ const statusFromError = error && typeof error === "object" && "status" in error && typeof error.status === "number" ? error.status : null;
830
+ const statusMatch = message.match(/status (\d{3})/i);
831
+ const status = statusFromError ?? (statusMatch ? Number(statusMatch[1]) : null);
832
+ return { ok: false, status, message };
833
+ }
834
+ }
835
+ async function flushCaptureQueue(options) {
836
+ options = { ...options, baseDir: resolveStateDir(options.baseDir) };
837
+ const result = { sent: 0, failed: 0, skipped: 0, unknown: 0 };
838
+ const now = options.now ?? Date.now();
839
+ const items = listQueueItems(options.baseDir);
840
+ const credentials = options.credentials === void 0 ? await options.apiClient.getEffectiveCredentials() : options.credentials;
841
+ for (const item of items) {
842
+ if (options.signal?.aborted) break;
843
+ if (options.digestId && item.digestId !== options.digestId) continue;
844
+ if (item.status === "delivered" || item.status === "cancelled" || item.status === "failed") {
845
+ result.skipped += 1;
846
+ continue;
847
+ }
848
+ if (item.nextRetryAt && Date.parse(item.nextRetryAt) > now) {
849
+ result.skipped += 1;
850
+ continue;
851
+ }
852
+ if (!options.includeManual && item.captureTrigger === "manual") {
853
+ continue;
854
+ }
855
+ const isAutomatic = item.captureTrigger !== "manual";
856
+ const consentStillMatches = () => {
857
+ const prefs = readPreferences(options.baseDir);
858
+ return prefs?.consent?.captureEnabled === true && (!isAutomatic || options.allowAutomatic === true) && item.consentRevision !== void 0 && item.consentRevision === (prefs.consent.revision ?? 0) && (!isAutomatic || item.workspaceId === prefs.enabledWorkspaceId);
859
+ };
860
+ if (isAutomatic) {
861
+ if (!consentStillMatches()) {
862
+ if (item.status === "pending") {
863
+ updateQueueItem(
864
+ item.digestId,
865
+ { status: "cancelled", lastError: "Capture consent or project policy changed before delivery" },
866
+ options.baseDir
867
+ );
868
+ }
869
+ result.skipped += 1;
870
+ continue;
871
+ }
872
+ }
873
+ const currentAuthorizationId = credentials?.authType === "oauth" ? credentials.authorizationId : void 0;
874
+ if (!item.authorizationId || !currentAuthorizationId || item.authorizationId !== currentAuthorizationId) {
875
+ updateQueueItem(
876
+ item.digestId,
877
+ { status: "cancelled", lastError: "Capture item belongs to a different or unknown authorization" },
878
+ options.baseDir
879
+ );
880
+ result.skipped += 1;
881
+ continue;
882
+ }
883
+ if (!credentials?.scopes?.includes("digests:write") || !credentials.workspaces?.includes(item.workspaceId)) {
884
+ result.skipped += 1;
885
+ continue;
886
+ }
887
+ const owner = randomUUID2();
888
+ const claimed = await withSessionLock("capture-queue", () => {
889
+ const current = listQueueItems(options.baseDir).find((queued) => queued.digestId === item.digestId);
890
+ if (!current || ["delivered", "cancelled", "failed"].includes(current.status) || current.deliveryLease && Date.parse(current.deliveryLease.expiresAt) > Date.now() || current.nextRetryAt && Date.parse(current.nextRetryAt) > now || !consentStillMatches() || options.signal?.aborted) return null;
891
+ return updateQueueItem(item.digestId, {
892
+ status: "sending",
893
+ deliveryLease: { owner, expiresAt: new Date(Date.now() + 1e4).toISOString() }
894
+ }, options.baseDir);
895
+ }, { baseDir: options.baseDir, timeoutMs: 250 });
896
+ if (!claimed) {
897
+ result.skipped += 1;
898
+ continue;
899
+ }
900
+ const finish = (updates) => withSessionLock("capture-queue", () => {
901
+ const current = listQueueItems(options.baseDir).find((queued) => queued.digestId === item.digestId);
902
+ if (current?.deliveryLease?.owner !== owner) return;
903
+ updateQueueItem(item.digestId, { ...updates, deliveryLease: void 0 }, options.baseDir);
904
+ }, { baseDir: options.baseDir, timeoutMs: 250 });
905
+ const response = await postDigest(options.apiClient, claimed, options.signal, consentStillMatches);
906
+ if (response.ok) {
907
+ await finish({ status: "delivered", deliveredAt: (/* @__PURE__ */ new Date()).toISOString(), lastError: void 0, nextRetryAt: void 0 });
908
+ result.sent += 1;
909
+ continue;
910
+ }
911
+ const klass = classifyCaptureFailure(response.status, response.message);
912
+ if (klass === "auth_required" || klass === "terminal" || klass === "non_retryable") {
913
+ await finish({ status: "failed", lastError: response.message ?? "non-retryable capture failure", attempts: claimed.attempts + 1 });
914
+ result.failed += 1;
915
+ continue;
916
+ }
917
+ const attempts = claimed.attempts + 1;
918
+ if (attempts >= MAX_CAPTURE_ATTEMPTS) {
919
+ await finish({ status: "failed", attempts, lastError: response.message ?? "retry limit reached" });
920
+ result.failed += 1;
921
+ continue;
922
+ }
923
+ const unknown = response.status === null;
924
+ await finish({
925
+ status: unknown ? "delivery_unknown" : "pending",
926
+ attempts,
927
+ nextRetryAt: nextRetryAt(attempts, now),
928
+ lastError: response.message
929
+ });
930
+ if (unknown) result.unknown += 1;
931
+ else result.failed += 1;
932
+ }
933
+ logRuntimeEvent(
934
+ "info",
935
+ "capture_flush",
936
+ { ...result },
937
+ options.baseDir
938
+ );
939
+ return result;
940
+ }
941
+
942
+ // src/runtime/session.ts
943
+ function parseBrainToolResult(toolName, toolResult) {
944
+ toolName = toolName.replace(/^mcp__brain__/, "").replace(/^mcp__brainmcp__/, "");
945
+ if (!toolName || !toolResult) {
946
+ return { isWriteBack: false };
947
+ }
948
+ if (toolName === "brain_session_digest" || toolName === "brain_digest_pending" || toolName === "brain_digest_complete" || toolName.includes("read") || toolName.includes("overview") || toolName.includes("search") || toolName.includes("list")) {
949
+ return { isWriteBack: false };
950
+ }
951
+ let resultObj = toolResult;
952
+ if (typeof toolResult === "string") {
953
+ try {
954
+ resultObj = JSON.parse(toolResult);
955
+ } catch {
956
+ return { isWriteBack: false };
957
+ }
958
+ }
959
+ if (typeof resultObj !== "object" || resultObj === null) {
960
+ return { isWriteBack: false };
961
+ }
962
+ if (resultObj.isError || resultObj.error) {
963
+ return { isWriteBack: false, status: "error" };
964
+ }
965
+ if (resultObj.structuredContent) return parseBrainToolResult(toolName, resultObj.structuredContent);
966
+ if (resultObj.kind === "tool_error") return { isWriteBack: false, status: "error" };
967
+ if (resultObj.data && typeof resultObj.data === "object") return parseBrainToolResult(toolName, resultObj.data);
968
+ if (Array.isArray(resultObj.content)) {
969
+ for (const block of resultObj.content.slice(0, 8)) {
970
+ if (block?.type !== "text" || typeof block.text !== "string") continue;
971
+ const parsed = parseBrainToolResult(toolName, block.text);
972
+ if (parsed.isWriteBack) return parsed;
973
+ }
974
+ return { isWriteBack: false };
975
+ }
976
+ if (toolName === "brain_change_propose" || toolName === "brain_remember" || toolName === "brain_amend" || toolName === "brain_tags_set") {
977
+ const proposal = resultObj.proposal ?? resultObj;
978
+ const status = proposal.status ?? resultObj.status;
979
+ const proposalId = proposal.id ?? resultObj.id ?? resultObj.proposalId;
980
+ if (status === "applied" && proposalId) {
981
+ const isIdempotent = Boolean(resultObj.idempotentReplay || resultObj.idempotent || proposal.idempotent);
982
+ return {
983
+ isWriteBack: true,
984
+ actionKind: isIdempotent ? "idempotent" : "proposal_applied",
985
+ status: "applied",
986
+ proposalId
987
+ };
988
+ }
989
+ if (proposalId && (status === "pending" || status === "pending_review" || status === "review_required" || status === "queued")) {
990
+ return {
991
+ isWriteBack: true,
992
+ actionKind: "proposal_created",
993
+ status: "pending_review",
994
+ proposalId,
995
+ note: "Proposal created awaiting review"
996
+ };
997
+ }
998
+ if (status === "rejected" || status === "failed") {
999
+ return { isWriteBack: false, status };
1000
+ }
1001
+ if (proposalId) {
1002
+ return {
1003
+ isWriteBack: true,
1004
+ actionKind: "proposal_created",
1005
+ proposalId,
1006
+ status: status ?? "created"
1007
+ };
1008
+ }
1009
+ }
1010
+ return { isWriteBack: false };
1011
+ }
1012
+ function getSessionLedgerPath(sessionKey, baseDir) {
1013
+ const sessionsDir = path6.join(resolveStateDir(baseDir), "sessions");
1014
+ ensureDirectory0700(sessionsDir);
1015
+ const safeKey = sessionKey.replace(/[^a-zA-Z0-9_-]/g, "_");
1016
+ return path6.join(sessionsDir, `${safeKey}.json`);
1017
+ }
1018
+ function loadSessionLedger(sessionKey, baseDir) {
1019
+ const filePath = getSessionLedgerPath(sessionKey, baseDir);
1020
+ const existing = readSafeJson(filePath, validateSessionLedgerRecord);
1021
+ if (existing) {
1022
+ return existing;
1023
+ }
1024
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1025
+ return {
1026
+ version: SESSION_LEDGER_SCHEMA_VERSION,
1027
+ sessionKey,
1028
+ createdAt: now,
1029
+ lastEventAt: now,
1030
+ eventSequence: 0,
1031
+ editedPaths: [],
1032
+ deliveryIds: []
1033
+ };
1034
+ }
1035
+ function saveSessionLedger(sessionKey, ledger, baseDir) {
1036
+ const filePath = getSessionLedgerPath(sessionKey, baseDir);
1037
+ writeAtomic0600(filePath, JSON.stringify(ledger, null, 2));
1038
+ }
1039
+ function loadMostRecentSessionLedger(baseDir, projectKey) {
1040
+ const sessionsDir = path6.join(resolveStateDir(baseDir), "sessions");
1041
+ if (!fs6.existsSync(sessionsDir)) return null;
1042
+ const directory = fs6.lstatSync(sessionsDir);
1043
+ if (directory.isSymbolicLink() || !directory.isDirectory()) {
1044
+ throw new Error(`Security violation: ${sessionsDir} is not a safe session directory`);
1045
+ }
1046
+ let latest = null;
1047
+ let latestAt = Number.NEGATIVE_INFINITY;
1048
+ for (const entry of fs6.readdirSync(sessionsDir).slice(0, 1e3)) {
1049
+ if (!entry.endsWith(".json")) continue;
1050
+ const filePath = path6.join(sessionsDir, entry);
1051
+ try {
1052
+ const ledger = readSafeJson(filePath, validateSessionLedgerRecord);
1053
+ if (!ledger) continue;
1054
+ if (projectKey && ledger.projectKey !== projectKey) continue;
1055
+ const timestamp = Date.parse(ledger.lastEventAt);
1056
+ const comparable = Number.isFinite(timestamp) ? timestamp : fs6.lstatSync(filePath).mtimeMs;
1057
+ if (comparable > latestAt) {
1058
+ latest = ledger;
1059
+ latestAt = comparable;
1060
+ }
1061
+ } catch {
1062
+ }
1063
+ }
1064
+ return latest;
1065
+ }
1066
+ async function recordSessionEvent(input) {
1067
+ return withSessionLock(
1068
+ input.sessionKey,
1069
+ async () => {
1070
+ const ledger = loadSessionLedger(input.sessionKey, input.baseDir);
1071
+ const timestamp = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
1072
+ if (input.captureRevision !== void 0 && (ledger.captureRevision !== input.captureRevision || ledger.workspaceId !== input.workspaceId)) {
1073
+ ledger.prompts = [];
1074
+ ledger.editedPaths = [];
1075
+ ledger.assistantSummary = void 0;
1076
+ ledger.model = void 0;
1077
+ ledger.lastCapturedSequence = ledger.eventSequence;
1078
+ ledger.captureRevision = input.captureRevision;
1079
+ ledger.workspaceId = input.workspaceId;
1080
+ }
1081
+ ledger.eventSequence += 1;
1082
+ ledger.lastEventAt = timestamp;
1083
+ if (input.projectKey) ledger.projectKey = input.projectKey;
1084
+ if (input.editedPath && !ledger.editedPaths.includes(input.editedPath)) {
1085
+ if (ledger.editedPaths.length >= 100) {
1086
+ ledger.editedPaths.shift();
1087
+ }
1088
+ ledger.editedPaths.push(input.editedPath);
1089
+ }
1090
+ if (input.prompt && input.persistPrompts) {
1091
+ if (!ledger.prompts) ledger.prompts = [];
1092
+ if (ledger.prompts.length >= 50) {
1093
+ ledger.prompts.shift();
1094
+ }
1095
+ ledger.prompts.push(scrubSecrets(input.prompt).slice(0, 1e3));
1096
+ }
1097
+ if (input.assistantSummary?.trim()) {
1098
+ ledger.assistantSummary = scrubSecrets(input.assistantSummary.trim()).slice(0, 4e3);
1099
+ }
1100
+ if (input.model?.trim()) {
1101
+ ledger.model = input.model.trim().slice(0, 120);
1102
+ }
1103
+ if (input.deliveryId && !ledger.deliveryIds.includes(input.deliveryId)) {
1104
+ ledger.deliveryIds.push(input.deliveryId);
1105
+ ledger.deliveryIds = ledger.deliveryIds.slice(-100);
1106
+ }
1107
+ if (input.isMeaningful) {
1108
+ ledger.lastMeaningfulAt = timestamp;
1109
+ ledger.lastMeaningfulSequence = Math.max(
1110
+ ledger.lastMeaningfulSequence ?? 0,
1111
+ ledger.eventSequence
1112
+ );
1113
+ }
1114
+ if (input.toolResult) {
1115
+ const parsed = parseBrainToolResult(
1116
+ input.toolResult.toolName,
1117
+ input.toolResult.result
1118
+ );
1119
+ if (parsed.isWriteBack) {
1120
+ ledger.lastSuccessfulWriteBackAt = timestamp;
1121
+ ledger.lastSuccessfulWriteBackSequence = Math.max(
1122
+ ledger.lastSuccessfulWriteBackSequence ?? 0,
1123
+ ledger.eventSequence
1124
+ );
1125
+ }
1126
+ }
1127
+ saveSessionLedger(input.sessionKey, ledger, input.baseDir);
1128
+ return ledger;
1129
+ },
1130
+ { baseDir: input.baseDir, timeoutMs: 250 }
1131
+ );
1132
+ }
1133
+ async function markSessionCaptured(input) {
1134
+ return withSessionLock(
1135
+ input.sessionKey,
1136
+ async () => {
1137
+ const ledger = loadSessionLedger(input.sessionKey, input.baseDir);
1138
+ ledger.lastCapturedAt = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
1139
+ ledger.lastCapturedSequence = Math.max(
1140
+ ledger.lastCapturedSequence ?? 0,
1141
+ input.capturedThroughSequence
1142
+ );
1143
+ if (!ledger.deliveryIds.includes(input.digestId)) ledger.deliveryIds.push(input.digestId);
1144
+ ledger.deliveryIds = ledger.deliveryIds.slice(-100);
1145
+ saveSessionLedger(input.sessionKey, ledger, input.baseDir);
1146
+ return ledger;
1147
+ },
1148
+ { baseDir: input.baseDir }
1149
+ );
1150
+ }
1151
+ async function queueSessionCapture(input) {
1152
+ return withSessionLock(input.sessionKey, () => withSessionLock("capture-queue", () => {
1153
+ const ledger = loadSessionLedger(input.sessionKey, input.baseDir);
1154
+ if ((ledger.lastCapturedSequence ?? 0) >= input.capturedThroughSequence) return null;
1155
+ const hash = createHash("sha256").update(JSON.stringify([
1156
+ input.sessionKey,
1157
+ input.capturedThroughSequence,
1158
+ input.item.authorizationId,
1159
+ input.item.consentRevision,
1160
+ input.item.workspaceId
1161
+ ])).digest("hex");
1162
+ const digestId = `${hash.slice(0, 8)}-${hash.slice(8, 12)}-4${hash.slice(13, 16)}-8${hash.slice(17, 20)}-${hash.slice(20, 32)}`;
1163
+ let item = listQueueItems(input.baseDir).find((queued) => queued.digestId === digestId);
1164
+ if (!item) {
1165
+ if (!canEnqueueCapture(input.baseDir)) return null;
1166
+ let digestText = input.item.digestText;
1167
+ try {
1168
+ const payload = JSON.parse(digestText);
1169
+ if (payload && typeof payload === "object" && "clientDigestId" in payload) digestText = JSON.stringify({ ...payload, clientDigestId: digestId });
1170
+ } catch {
1171
+ }
1172
+ item = enqueueCaptureItem({ ...input.item, digestId, digestText }, input.baseDir);
1173
+ }
1174
+ ledger.lastCapturedAt = item.createdAt;
1175
+ ledger.lastCapturedSequence = input.capturedThroughSequence;
1176
+ ledger.deliveryIds = [...ledger.deliveryIds, item.digestId].slice(-100);
1177
+ saveSessionLedger(input.sessionKey, ledger, input.baseDir);
1178
+ return item;
1179
+ }, { baseDir: input.baseDir, timeoutMs: 250 }), { baseDir: input.baseDir, timeoutMs: 250 });
1180
+ }
1181
+ async function markSessionNudged(input) {
1182
+ return withSessionLock(
1183
+ input.sessionKey,
1184
+ async () => {
1185
+ const ledger = loadSessionLedger(input.sessionKey, input.baseDir);
1186
+ ledger.lastNudgedAt = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
1187
+ ledger.lastNudgedSequence = Math.max(
1188
+ ledger.lastNudgedSequence ?? 0,
1189
+ input.nudgedThroughSequence
1190
+ );
1191
+ saveSessionLedger(input.sessionKey, ledger, input.baseDir);
1192
+ return ledger;
1193
+ },
1194
+ { baseDir: input.baseDir }
1195
+ );
1196
+ }
1197
+
1198
+ // src/runtime/retention.ts
1199
+ import fs7 from "node:fs";
1200
+ import path7 from "node:path";
1201
+ var SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1e3;
1202
+ var FOURTEEN_DAYS_MS = 14 * 24 * 60 * 60 * 1e3;
1203
+ var FIVE_MB_BYTES = 5 * 1024 * 1024;
1204
+ function listOwnedJsonFiles(directory, accepts) {
1205
+ if (!fs7.existsSync(directory)) return [];
1206
+ assertNotSymlink(directory);
1207
+ return fs7.readdirSync(directory).flatMap((entry) => {
1208
+ if (!accepts(entry)) return [];
1209
+ const filePath = path7.join(directory, entry);
1210
+ try {
1211
+ const stat = fs7.lstatSync(filePath);
1212
+ return stat.isFile() && !stat.isSymbolicLink() ? [filePath] : [];
1213
+ } catch {
1214
+ return [];
1215
+ }
1216
+ });
1217
+ }
1218
+ function previewCapturePurge(baseDir) {
1219
+ const stateDir = resolveStateDir(baseDir);
1220
+ return {
1221
+ queueItems: listOwnedJsonFiles(path7.join(stateDir, "queue"), (name) => name.endsWith(".json")).length,
1222
+ sessionLedgers: listOwnedJsonFiles(path7.join(stateDir, "sessions"), (name) => name.endsWith(".json")).length,
1223
+ orientationCacheFiles: listOwnedJsonFiles(
1224
+ path7.join(stateDir, "cache"),
1225
+ (name) => name.startsWith("orient-") && name.endsWith(".json")
1226
+ ).length
1227
+ };
1228
+ }
1229
+ function purgeCaptureLocalState(baseDir) {
1230
+ const stateDir = resolveStateDir(baseDir);
1231
+ const groups = [
1232
+ listOwnedJsonFiles(path7.join(stateDir, "queue"), (name) => name.endsWith(".json")),
1233
+ listOwnedJsonFiles(path7.join(stateDir, "sessions"), (name) => name.endsWith(".json")),
1234
+ listOwnedJsonFiles(path7.join(stateDir, "cache"), (name) => name.startsWith("orient-") && name.endsWith(".json"))
1235
+ ];
1236
+ const removed = {
1237
+ queueItems: 0,
1238
+ sessionLedgers: 0,
1239
+ orientationCacheFiles: 0
1240
+ };
1241
+ const keys = ["queueItems", "sessionLedgers", "orientationCacheFiles"];
1242
+ groups.forEach((files, index) => {
1243
+ for (const filePath of files) {
1244
+ try {
1245
+ fs7.unlinkSync(filePath);
1246
+ removed[keys[index]] += 1;
1247
+ } catch {
1248
+ }
1249
+ }
1250
+ });
1251
+ return removed;
1252
+ }
1253
+ function pruneLocalState(options) {
1254
+ const baseDir = resolveStateDir(options?.baseDir ?? getBrainHome());
1255
+ const now = options?.now ?? Date.now();
1256
+ const maxQueueAge = options?.maxQueueAgeMs ?? SEVEN_DAYS_MS;
1257
+ const maxSessionAge = options?.maxSessionAgeMs ?? FOURTEEN_DAYS_MS;
1258
+ const maxLogSize = options?.maxLogSizeBytes ?? FIVE_MB_BYTES;
1259
+ let prunedQueueCount = 0;
1260
+ let prunedSessionCount = 0;
1261
+ let prunedLockCount = 0;
1262
+ let logRotated = false;
1263
+ try {
1264
+ const items = listQueueItems(baseDir);
1265
+ for (const item of items) {
1266
+ const createdAtMs = new Date(item.createdAt).getTime();
1267
+ if (now - createdAtMs > maxQueueAge || item.status === "delivered") {
1268
+ if (removeQueueItem(item.digestId, baseDir)) {
1269
+ prunedQueueCount += 1;
1270
+ }
1271
+ }
1272
+ }
1273
+ } catch {
1274
+ }
1275
+ const sessionsDir = path7.join(baseDir, "sessions");
1276
+ if (fs7.existsSync(sessionsDir)) {
1277
+ try {
1278
+ assertNotSymlink(sessionsDir);
1279
+ const sessionFiles = fs7.readdirSync(sessionsDir);
1280
+ for (const file of sessionFiles) {
1281
+ if (!file.endsWith(".json")) continue;
1282
+ const filePath = path7.join(sessionsDir, file);
1283
+ try {
1284
+ const stat = fs7.lstatSync(filePath);
1285
+ if (!stat.isSymbolicLink() && now - stat.mtimeMs > maxSessionAge) {
1286
+ fs7.unlinkSync(filePath);
1287
+ prunedSessionCount += 1;
1288
+ }
1289
+ } catch {
1290
+ }
1291
+ }
1292
+ } catch {
1293
+ }
1294
+ }
1295
+ const locksDir = path7.join(baseDir, "locks");
1296
+ if (fs7.existsSync(locksDir)) {
1297
+ try {
1298
+ assertNotSymlink(locksDir);
1299
+ const lockFiles = fs7.readdirSync(locksDir);
1300
+ for (const file of lockFiles) {
1301
+ if (!file.endsWith(".lock")) continue;
1302
+ const filePath = path7.join(locksDir, file);
1303
+ try {
1304
+ const stat = fs7.lstatSync(filePath);
1305
+ if (!stat.isSymbolicLink() && now - stat.mtimeMs > 60 * 1e3) {
1306
+ try {
1307
+ if (isProcessAlive(JSON.parse(fs7.readFileSync(filePath, "utf8")).pid)) continue;
1308
+ } catch {
1309
+ }
1310
+ fs7.unlinkSync(filePath);
1311
+ prunedLockCount += 1;
1312
+ }
1313
+ } catch {
1314
+ }
1315
+ }
1316
+ } catch {
1317
+ }
1318
+ }
1319
+ const logFile = path7.join(baseDir, "logs", "runtime.log");
1320
+ if (fs7.existsSync(logFile)) {
1321
+ try {
1322
+ assertNotSymlink(logFile);
1323
+ const stat = fs7.lstatSync(logFile);
1324
+ if (!stat.isSymbolicLink() && stat.size > maxLogSize) {
1325
+ const keepBytes = 1024 * 1024;
1326
+ const buffer = Buffer.alloc(keepBytes);
1327
+ const fd = fs7.openSync(logFile, "r");
1328
+ fs7.readSync(fd, buffer, 0, keepBytes, stat.size - keepBytes);
1329
+ fs7.closeSync(fd);
1330
+ fs7.writeFileSync(logFile, buffer, { mode: 384 });
1331
+ logRotated = true;
1332
+ }
1333
+ } catch {
1334
+ }
1335
+ }
1336
+ return {
1337
+ prunedQueueCount,
1338
+ prunedSessionCount,
1339
+ prunedLockCount,
1340
+ logRotated
1341
+ };
1342
+ }
1343
+
1344
+ // src/lib/http.ts
1345
+ async function withRequestDeadline(timeoutMs, signal, action) {
1346
+ const controller = new AbortController();
1347
+ const abort = () => controller.abort(signal?.reason);
1348
+ signal?.addEventListener("abort", abort, { once: true });
1349
+ if (signal?.aborted) abort();
1350
+ const timer = setTimeout(() => controller.abort(new Error("Request timed out")), timeoutMs);
1351
+ let onAbort;
1352
+ try {
1353
+ controller.signal.throwIfAborted();
1354
+ const cancelled = new Promise((_, reject) => {
1355
+ onAbort = () => reject(new Error("Request cancelled or timed out"));
1356
+ controller.signal.addEventListener("abort", onAbort, { once: true });
1357
+ });
1358
+ return await Promise.race([action(controller.signal), cancelled]);
1359
+ } finally {
1360
+ clearTimeout(timer);
1361
+ signal?.removeEventListener("abort", abort);
1362
+ if (onAbort) controller.signal.removeEventListener("abort", onAbort);
1363
+ }
1364
+ }
1365
+ var BRAIN_ORIGINS = /* @__PURE__ */ new Set([
1366
+ "https://mcp.brainmcp.ai",
1367
+ "https://api.brainmcp.ai",
1368
+ "https://dash.brainmcp.ai"
1369
+ ]);
1370
+ function validateHttpUrl(value, trustedBase) {
1371
+ let url;
1372
+ try {
1373
+ url = new URL(value);
1374
+ } catch {
1375
+ throw new Error("Invalid API or OAuth endpoint URL.");
1376
+ }
1377
+ const loopback = url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "[::1]";
1378
+ if (url.protocol !== "https:" && !(loopback && url.protocol === "http:") || url.username || url.password || url.hash) {
1379
+ throw new Error("API and OAuth endpoints require HTTPS (HTTP is allowed only on loopback), without credentials or fragments.");
1380
+ }
1381
+ if (trustedBase) {
1382
+ const origin = new URL(trustedBase).origin;
1383
+ if (url.origin !== origin && !(BRAIN_ORIGINS.has(origin) && BRAIN_ORIGINS.has(url.origin))) {
1384
+ throw new Error("OAuth endpoint origin does not match the trusted Brain issuer.");
1385
+ }
1386
+ }
1387
+ return url;
1388
+ }
1389
+
1390
+ // src/runtime/capture.ts
1391
+ import crypto2 from "node:crypto";
1392
+ import { execFileSync } from "node:child_process";
1393
+ import fs8 from "node:fs";
1394
+ import path8 from "node:path";
1395
+ var MAX_DIGEST_BYTES = 14 * 1024;
1396
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1397
+ var STOP_EVENTS = /* @__PURE__ */ new Set(["Stop", "StopNudge", "stop", "SessionStop", "session_stop"]);
1398
+ var CAPTURE_EVENTS = /* @__PURE__ */ new Set([
1399
+ "PreCompact",
1400
+ "preCompact",
1401
+ "pre_compact",
1402
+ "precompact",
1403
+ "SessionEnd",
1404
+ "sessionEnd",
1405
+ "session_end",
1406
+ "manual"
1407
+ ]);
1408
+ function isStopEvent(event, stopHookActive) {
1409
+ return stopHookActive === true || STOP_EVENTS.has(event);
1410
+ }
1411
+ function captureTriggerForEvent(event) {
1412
+ if (event === "SessionEnd" || event === "sessionEnd" || event === "session_end") return "session_end";
1413
+ if (event === "PreCompact" || event === "preCompact" || event === "pre_compact" || event === "precompact") {
1414
+ return "pre_compact";
1415
+ }
1416
+ if (event === "manual") return "manual";
1417
+ return null;
1418
+ }
1419
+ function readProjectCaptureRestrictions(startDir) {
1420
+ const result = { captureDisabled: false, promptsDisabled: false };
1421
+ if (!startDir) return result;
1422
+ let dir = path8.resolve(startDir);
1423
+ for (let i = 0; i < 16; i += 1) {
1424
+ const candidate = path8.join(dir, ".brainmcp.json");
1425
+ if (fs8.lstatSync(candidate, { throwIfNoEntry: false })) {
1426
+ try {
1427
+ const stat = fs8.lstatSync(candidate);
1428
+ if (stat.isSymbolicLink() || !stat.isFile() || stat.size > 64 * 1024) return { captureDisabled: true, promptsDisabled: true };
1429
+ const parsed = JSON.parse(fs8.readFileSync(candidate, "utf8"));
1430
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return { captureDisabled: true, promptsDisabled: true };
1431
+ const consent = parsed.consent && typeof parsed.consent === "object" ? parsed.consent : void 0;
1432
+ if (parsed.capture === false || parsed.captureEnabled === false || consent?.captureEnabled === false) {
1433
+ result.captureDisabled = true;
1434
+ }
1435
+ if (parsed.includePrompts === false || consent?.includePrompts === false) {
1436
+ result.promptsDisabled = true;
1437
+ }
1438
+ } catch {
1439
+ return { captureDisabled: true, promptsDisabled: true };
1440
+ }
1441
+ return result;
1442
+ }
1443
+ const parent = path8.dirname(dir);
1444
+ if (parent === dir) break;
1445
+ dir = parent;
1446
+ }
1447
+ return result;
1448
+ }
1449
+ function resolveCaptureWorkspaceId(prefs) {
1450
+ const candidate = prefs?.enabledWorkspaceId?.trim();
1451
+ if (candidate && UUID_RE.test(candidate)) return candidate;
1452
+ return null;
1453
+ }
1454
+ function captureProjectKey(cwd) {
1455
+ let dir = fs8.realpathSync(path8.resolve(cwd));
1456
+ const original = dir;
1457
+ for (let i = 0; i < 32; i += 1) {
1458
+ if (fs8.existsSync(path8.join(dir, ".git")) || fs8.lstatSync(path8.join(dir, ".brainmcp.json"), { throwIfNoEntry: false })) break;
1459
+ const parent = path8.dirname(dir);
1460
+ if (parent === dir) {
1461
+ dir = original;
1462
+ break;
1463
+ }
1464
+ dir = parent;
1465
+ }
1466
+ return crypto2.createHash("sha256").update(dir).digest("hex");
1467
+ }
1468
+ function collectGitMetadata(cwd, runner) {
1469
+ if (!cwd) return void 0;
1470
+ const run = runner ?? ((args) => execFileSync("git", ["-C", cwd, ...args], {
1471
+ encoding: "utf8",
1472
+ timeout: 125,
1473
+ maxBuffer: 64 * 1024,
1474
+ stdio: ["ignore", "pipe", "ignore"]
1475
+ }).trim());
1476
+ const safely = (args) => {
1477
+ try {
1478
+ return run(args).trim();
1479
+ } catch {
1480
+ return "";
1481
+ }
1482
+ };
1483
+ const root = safely(["rev-parse", "--show-toplevel"]);
1484
+ if (!root) return void 0;
1485
+ const branch = safely(["branch", "--show-current"]);
1486
+ const commit = safely(["rev-parse", "--short=12", "HEAD"]);
1487
+ const numstat = safely(["diff", "--numstat", "HEAD"]);
1488
+ const changedFiles = safely(["status", "--porcelain=v1", "--untracked-files=no"]);
1489
+ let insertions = 0;
1490
+ let deletions = 0;
1491
+ for (const line of numstat.split("\n")) {
1492
+ if (!line) continue;
1493
+ const [added, removed] = line.split(" ");
1494
+ if (/^\d+$/.test(added ?? "")) insertions += Number(added);
1495
+ if (/^\d+$/.test(removed ?? "")) deletions += Number(removed);
1496
+ }
1497
+ const filesChanged = changedFiles ? changedFiles.split("\n").filter(Boolean).length : 0;
1498
+ return {
1499
+ repository: path8.basename(root),
1500
+ branch: branch || void 0,
1501
+ commit: commit || void 0,
1502
+ diffStats: { filesChanged, insertions, deletions }
1503
+ };
1504
+ }
1505
+ function truncateUtf8(value, maxBytes) {
1506
+ let output = "";
1507
+ let bytes = 0;
1508
+ for (const character of value) {
1509
+ const characterBytes = Buffer.byteLength(character, "utf8");
1510
+ if (bytes + characterBytes > maxBytes) break;
1511
+ output += character;
1512
+ bytes += characterBytes;
1513
+ }
1514
+ return output;
1515
+ }
1516
+ function buildDigestPayload(input) {
1517
+ const clientDigestId = crypto2.randomUUID();
1518
+ const safePaths = [];
1519
+ if (input.paths) {
1520
+ for (const p of input.paths) {
1521
+ if (typeof p !== "string") continue;
1522
+ const normalized = path8.normalize(p.trim());
1523
+ if (path8.isAbsolute(normalized)) continue;
1524
+ if (normalized.startsWith("..") || normalized.includes("../") || normalized.includes("..\\")) continue;
1525
+ safePaths.push(truncateUtf8(scrubSecrets(normalized), 180));
1526
+ }
1527
+ }
1528
+ const safePrompts = [];
1529
+ if (input.includePrompts && input.prompts) {
1530
+ for (const pr of input.prompts) {
1531
+ if (typeof pr === "string" && pr.trim()) {
1532
+ safePrompts.push(truncateUtf8(scrubSecrets(pr), 800));
1533
+ }
1534
+ }
1535
+ }
1536
+ const payload = {
1537
+ clientDigestId,
1538
+ trigger: input.trigger,
1539
+ workspaceId: input.workspaceId,
1540
+ host: input.host ? truncateUtf8(scrubSecrets(input.host), 80) : void 0,
1541
+ model: input.model ? truncateUtf8(scrubSecrets(input.model), 120) : void 0,
1542
+ summary: input.summary ? truncateUtf8(scrubSecrets(input.summary), 4e3) : void 0,
1543
+ relativePaths: safePaths.slice(0, 50),
1544
+ gitMetadata: input.gitMetadata ? {
1545
+ branch: input.gitMetadata.branch ? truncateUtf8(scrubSecrets(input.gitMetadata.branch), 180) : void 0,
1546
+ commit: input.gitMetadata.commit ? truncateUtf8(scrubSecrets(input.gitMetadata.commit), 180) : void 0,
1547
+ repository: input.gitMetadata.repository ? truncateUtf8(scrubSecrets(input.gitMetadata.repository), 180) : void 0,
1548
+ diffStats: input.gitMetadata.diffStats ? {
1549
+ filesChanged: Math.max(0, Math.min(1e5, Math.trunc(input.gitMetadata.diffStats.filesChanged))),
1550
+ insertions: Math.max(0, Math.min(1e7, Math.trunc(input.gitMetadata.diffStats.insertions))),
1551
+ deletions: Math.max(0, Math.min(1e7, Math.trunc(input.gitMetadata.diffStats.deletions)))
1552
+ } : void 0
1553
+ } : void 0,
1554
+ prompts: safePrompts.length > 0 ? safePrompts.slice(0, 10) : void 0
1555
+ };
1556
+ while (Buffer.byteLength(JSON.stringify(payload), "utf8") > MAX_DIGEST_BYTES) {
1557
+ if (payload.prompts?.length) {
1558
+ payload.prompts.pop();
1559
+ if (payload.prompts.length === 0) payload.prompts = void 0;
1560
+ continue;
1561
+ }
1562
+ if (payload.relativePaths?.length) {
1563
+ payload.relativePaths.pop();
1564
+ if (payload.relativePaths.length === 0) payload.relativePaths = void 0;
1565
+ continue;
1566
+ }
1567
+ if (payload.summary) {
1568
+ payload.summary = truncateUtf8(
1569
+ payload.summary,
1570
+ Math.max(0, Buffer.byteLength(payload.summary, "utf8") - 512)
1571
+ );
1572
+ if (!payload.summary) payload.summary = void 0;
1573
+ continue;
1574
+ }
1575
+ payload.gitMetadata = void 0;
1576
+ break;
1577
+ }
1578
+ return payload;
1579
+ }
1580
+ function serializeDigestPayload(payload) {
1581
+ const json = JSON.stringify(payload);
1582
+ const bytes = Buffer.byteLength(json, "utf8");
1583
+ if (bytes > MAX_DIGEST_BYTES) {
1584
+ throw new Error(`Digest payload exceeds max byte cap: ${bytes} > ${MAX_DIGEST_BYTES}`);
1585
+ }
1586
+ return json;
1587
+ }
1588
+ function isCaptureEligible(event, prefs, projectPolicy) {
1589
+ if (isStopEvent(event)) {
1590
+ return false;
1591
+ }
1592
+ if (projectPolicy?.captureDisabled) {
1593
+ return false;
1594
+ }
1595
+ if (!prefs?.consent?.captureEnabled) {
1596
+ return false;
1597
+ }
1598
+ return CAPTURE_EVENTS.has(event);
1599
+ }
1600
+
1601
+ // src/runtime/orient.ts
1602
+ import fs9 from "node:fs";
1603
+ import path9 from "node:path";
1604
+ var ORIENT_CACHE_TTL_MS = 10 * 60 * 1e3;
1605
+ var ORIENT_CONTEXT_MAX_CHARS = 2400;
1606
+ var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1607
+ var SESSION_START_EVENTS = /* @__PURE__ */ new Set([
1608
+ "SessionStart",
1609
+ "sessionStart",
1610
+ "session_start",
1611
+ "startup"
1612
+ ]);
1613
+ function isSessionStartEvent(event) {
1614
+ return SESSION_START_EVENTS.has(event);
1615
+ }
1616
+ function isUserPromptEvent(event) {
1617
+ return event === "UserPromptSubmit" || event === "UserPrompt" || event === "prompt" || event === "beforeSubmitPrompt";
1618
+ }
1619
+ function resolveOrientWorkspaceId(creds, listed) {
1620
+ if (!creds || creds.authType !== "oauth" || !creds.accessToken) {
1621
+ return { workspaceId: null, reason: "login" };
1622
+ }
1623
+ if (creds.defaultWorkspaceId && UUID_RE2.test(creds.defaultWorkspaceId)) {
1624
+ return { workspaceId: creds.defaultWorkspaceId, reason: "ok" };
1625
+ }
1626
+ const fromCreds = (creds.workspaces ?? []).filter((id) => UUID_RE2.test(id));
1627
+ if (fromCreds.length === 1) {
1628
+ return { workspaceId: fromCreds[0], reason: "ok" };
1629
+ }
1630
+ if (listed) {
1631
+ const defaults = listed.filter((ws) => ws.isDefault);
1632
+ if (defaults.length === 1 && UUID_RE2.test(defaults[0].id)) {
1633
+ return { workspaceId: defaults[0].id, reason: "ok" };
1634
+ }
1635
+ if (listed.length === 1 && UUID_RE2.test(listed[0].id)) {
1636
+ return { workspaceId: listed[0].id, reason: "ok" };
1637
+ }
1638
+ if (listed.length !== 1) {
1639
+ return { workspaceId: null, reason: "select" };
1640
+ }
1641
+ }
1642
+ if (fromCreds.length !== 1) {
1643
+ return { workspaceId: null, reason: fromCreds.length === 0 ? "select" : "select" };
1644
+ }
1645
+ return { workspaceId: null, reason: "select" };
1646
+ }
1647
+ function formatOrientAdditionalContext(payload) {
1648
+ if (!payload || typeof payload !== "object") {
1649
+ return "BrainMCP orientation is available. Call brain_workspace_overview before non-trivial work.";
1650
+ }
1651
+ const data = payload;
1652
+ const lines = ["BrainMCP orientation (no user prompt was sent)."];
1653
+ const workspace = data.workspace && typeof data.workspace === "object" ? data.workspace : void 0;
1654
+ if (typeof workspace?.name === "string") {
1655
+ lines.push(`Workspace: ${workspace.name}`);
1656
+ }
1657
+ const review = data.review && typeof data.review === "object" ? data.review : void 0;
1658
+ if (typeof review?.writePath === "string") {
1659
+ lines.push(`Write path: ${review.writePath}`);
1660
+ }
1661
+ if (review?.humanReviewRequired === true) {
1662
+ lines.push("Human review is required before graph writes apply.");
1663
+ }
1664
+ if (typeof data.pendingProposalCount === "number" && data.pendingProposalCount > 0) {
1665
+ lines.push(`Pending proposals: ${data.pendingProposalCount}`);
1666
+ }
1667
+ if (typeof data.dueWorkflowCount === "number" && data.dueWorkflowCount > 0) {
1668
+ lines.push(`Due workflows: ${data.dueWorkflowCount}`);
1669
+ }
1670
+ const rules = data.rules && typeof data.rules === "object" ? data.rules : void 0;
1671
+ for (const rule of rules?.items?.slice(0, 3) ?? []) {
1672
+ if (typeof rule.body === "string" && rule.body.trim()) {
1673
+ lines.push(`Rule: ${rule.body.trim().slice(0, 280)}`);
1674
+ }
1675
+ }
1676
+ if (typeof data.writeBackHint === "string" && data.writeBackHint.trim()) {
1677
+ lines.push(data.writeBackHint.trim());
1678
+ } else {
1679
+ lines.push("Propose only new durable project knowledge with brain_change_propose after meaningful work.");
1680
+ }
1681
+ const text = lines.join("\n");
1682
+ return text.length > ORIENT_CONTEXT_MAX_CHARS ? `${text.slice(0, ORIENT_CONTEXT_MAX_CHARS - 1)}\u2026` : text;
1683
+ }
1684
+ function loginOrientationMessage() {
1685
+ return "BrainMCP CLI is not signed in. Run `brainmcp login`, then retry orientation. No remote request was made.";
1686
+ }
1687
+ function selectWorkspaceOrientationMessage() {
1688
+ return "BrainMCP has more than one authorized workspace. Pass workspaceId or set a default before orientation. No workspace was guessed.";
1689
+ }
1690
+ function readProjectWorkspaceId(startDir) {
1691
+ if (!startDir) return { kind: "none" };
1692
+ let dir = path9.resolve(startDir);
1693
+ for (let depth = 0; depth < 16; depth += 1) {
1694
+ const configPath = path9.join(dir, ".brainmcp.json");
1695
+ if (fs9.existsSync(configPath)) {
1696
+ try {
1697
+ const stat = fs9.lstatSync(configPath);
1698
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 64 * 1024) {
1699
+ return { kind: "invalid", configPath };
1700
+ }
1701
+ const parsed = JSON.parse(fs9.readFileSync(configPath, "utf8"));
1702
+ const workspaceId = typeof parsed.workspaceId === "string" ? parsed.workspaceId.trim() : "";
1703
+ return UUID_RE2.test(workspaceId) ? { kind: "workspace", configPath, workspaceId } : { kind: "invalid", configPath };
1704
+ } catch {
1705
+ return { kind: "invalid", configPath };
1706
+ }
1707
+ }
1708
+ const parent = path9.dirname(dir);
1709
+ if (parent === dir) break;
1710
+ dir = parent;
1711
+ }
1712
+ return { kind: "none" };
1713
+ }
1714
+ function invalidProjectWorkspaceMessage() {
1715
+ return "BrainMCP found an invalid nearest `.brainmcp.json` workspaceId and did not guess another workspace. Fix or remove that project policy file.";
1716
+ }
1717
+ async function fetchSessionStartOrientation(input) {
1718
+ const initial = resolveOrientWorkspaceId(input.creds);
1719
+ if (initial.reason === "login") {
1720
+ return loginOrientationMessage();
1721
+ }
1722
+ const roots = (input.workspaceRoots?.length ? input.workspaceRoots : input.cwd ? [input.cwd] : []).filter((root, index, all) => Boolean(root) && all.indexOf(root) === index);
1723
+ const projectResolutions = roots.map((root) => readProjectWorkspaceId(root));
1724
+ if (projectResolutions.some((resolution) => resolution.kind === "invalid")) {
1725
+ return invalidProjectWorkspaceMessage();
1726
+ }
1727
+ const projectWorkspaceIds = projectResolutions.filter((resolution) => resolution.kind === "workspace").map((resolution) => resolution.workspaceId);
1728
+ const hasUnboundRoot = roots.length === 0 || projectResolutions.some((resolution) => resolution.kind === "none");
1729
+ let fallbackWorkspaceId = initial.workspaceId;
1730
+ if (hasUnboundRoot && !fallbackWorkspaceId) {
1731
+ try {
1732
+ const listed = await input.apiClient.request(
1733
+ "/api/mcp/workspaces",
1734
+ { timeoutMs: 800, signal: input.signal }
1735
+ );
1736
+ const next = resolveOrientWorkspaceId(input.creds, listed.workspaces ?? []);
1737
+ fallbackWorkspaceId = next.workspaceId;
1738
+ } catch {
1739
+ fallbackWorkspaceId = null;
1740
+ }
1741
+ }
1742
+ const workspaceIds = [.../* @__PURE__ */ new Set([
1743
+ ...projectWorkspaceIds,
1744
+ ...hasUnboundRoot && fallbackWorkspaceId ? [fallbackWorkspaceId] : []
1745
+ ])];
1746
+ if (workspaceIds.length === 0) return selectWorkspaceOrientationMessage();
1747
+ const results = await Promise.allSettled(
1748
+ workspaceIds.map(async (workspaceId) => {
1749
+ const payload = await input.apiClient.request(
1750
+ `/api/workspaces/${workspaceId}/orient`,
1751
+ { timeoutMs: 800, signal: input.signal }
1752
+ );
1753
+ return formatOrientAdditionalContext(payload);
1754
+ })
1755
+ );
1756
+ const contexts = results.filter((result) => result.status === "fulfilled").map((result) => result.value);
1757
+ if (contexts.length === 0) {
1758
+ throw new Error("No project workspace orientation request succeeded.");
1759
+ }
1760
+ if (hasUnboundRoot && !fallbackWorkspaceId) {
1761
+ contexts.push("One or more workspace roots have no resolvable project/default workspace; no workspace was guessed for those roots.");
1762
+ }
1763
+ const combined = contexts.join("\n\n---\n\n");
1764
+ return combined.length > ORIENT_CONTEXT_MAX_CHARS ? `${combined.slice(0, ORIENT_CONTEXT_MAX_CHARS - 1)}\u2026` : combined;
1765
+ }
1766
+ async function safeSessionStartOrientation(input) {
1767
+ if (!input.apiClient) {
1768
+ return input.creds?.authType === "oauth" ? selectWorkspaceOrientationMessage() : loginOrientationMessage();
1769
+ }
1770
+ try {
1771
+ return await fetchSessionStartOrientation(input);
1772
+ } catch (error) {
1773
+ logRuntimeEvent(
1774
+ "warn",
1775
+ "orient_fetch_failed",
1776
+ { error: error instanceof Error ? error.message : String(error) },
1777
+ input.baseDir
1778
+ );
1779
+ return "BrainMCP orientation is temporarily unavailable. Continue locally and retry brain_workspace_overview when possible.";
1780
+ }
1781
+ }
1782
+
1783
+ // src/lib/api-client.ts
1784
+ import { randomUUID as randomUUID3 } from "node:crypto";
1785
+
1786
+ // src/types/agent-config-stub.ts
1787
+ var BRAIN_MCP_ENDPOINT = "https://mcp.brainmcp.ai/mcp";
1788
+ var MCP_OAUTH_CLIENT_IDS = {
1789
+ antigravity: "antigravity",
1790
+ brainmcpCli: "brainmcp-cli",
1791
+ claudeCode: "claude-code",
1792
+ codex: "codex",
1793
+ cursor: "cursor",
1794
+ generic: "generic-mcp"
1795
+ };
1796
+ var BRAINMCP_CLI_OAUTH_CLIENT_ID = MCP_OAUTH_CLIENT_IDS.brainmcpCli;
1797
+ var BRAINMCP_CLI_CLIENT_ID = "brainmcp-cli";
1798
+ var BRAINMCP_CLI_ALLOWED_SCOPES = [
1799
+ "graph:read",
1800
+ "skills:read",
1801
+ "workflows:read",
1802
+ "offline_access",
1803
+ "digests:write"
1804
+ ];
1805
+ var BRAINMCP_CLI_DEFAULT_SCOPES = [
1806
+ "graph:read",
1807
+ "skills:read",
1808
+ "workflows:read",
1809
+ "offline_access"
1810
+ ];
1811
+ var CONSENT_ATTEMPT_TTL_MS = 10 * 60 * 1e3;
1812
+
1813
+ // src/lib/oauth-client.ts
1814
+ function grantedCliScopes(requested, tokenScope) {
1815
+ const allowed = new Set(BRAINMCP_CLI_ALLOWED_SCOPES);
1816
+ const raw = tokenScope === void 0 ? requested : tokenScope.split(/\s+/).filter(Boolean);
1817
+ if (raw.some((scope) => !allowed.has(scope) || !requested.includes(scope))) {
1818
+ throw new Error("The OAuth server granted unrequested or disallowed CLI scopes.");
1819
+ }
1820
+ const granted = [...new Set(raw)];
1821
+ if (requested.some((scope) => !granted.includes(scope))) {
1822
+ throw new Error("The OAuth server did not grant required CLI scopes.");
1823
+ }
1824
+ return granted;
1825
+ }
1826
+ function validateTokenResponse(value) {
1827
+ const token = value;
1828
+ if (!token || typeof token.access_token !== "string" || !token.access_token.trim() || typeof token.token_type !== "string" || token.token_type.toLowerCase() !== "bearer" || token.refresh_token !== void 0 && (typeof token.refresh_token !== "string" || !token.refresh_token.trim()) || token.scope !== void 0 && typeof token.scope !== "string" || token.expires_in !== void 0 && (typeof token.expires_in !== "number" || !Number.isFinite(token.expires_in) || token.expires_in <= 0)) {
1829
+ throw new Error("OAuth server returned an invalid token response.");
1830
+ }
1831
+ return token;
1832
+ }
1833
+ var DEFAULT_ISSUER = "https://mcp.brainmcp.ai";
1834
+ var OAuthClient = class {
1835
+ issuer;
1836
+ clientId;
1837
+ fetchFn;
1838
+ timeoutMs;
1839
+ constructor(options = {}) {
1840
+ this.issuer = (options.issuerUrl ?? process.env.BRAINMCP_API_URL ?? DEFAULT_ISSUER).replace(/\/+$/, "");
1841
+ this.clientId = options.clientId ?? BRAINMCP_CLI_CLIENT_ID;
1842
+ this.fetchFn = options.fetchFn ?? fetch;
1843
+ this.timeoutMs = options.timeoutMs ?? 1e4;
1844
+ validateHttpUrl(this.issuer);
1845
+ }
1846
+ async discoverEndpoints(signal) {
1847
+ const discoveryUrl = `${this.issuer}/.well-known/oauth-authorization-server`;
1848
+ return withRequestDeadline(this.timeoutMs, signal, async (requestSignal) => {
1849
+ const res = await this.fetchFn(discoveryUrl, { signal: requestSignal, redirect: "error" });
1850
+ if (res.ok) {
1851
+ const meta = await res.json();
1852
+ if (meta && typeof meta === "object" && !Array.isArray(meta)) {
1853
+ const endpoints = {
1854
+ issuer: meta.issuer ?? this.issuer,
1855
+ authorizationEndpoint: meta.authorization_endpoint ?? `${this.issuer}/api/auth/oauth2/authorize`,
1856
+ tokenEndpoint: meta.token_endpoint ?? `${this.issuer}/api/auth/oauth2/token`,
1857
+ revocationEndpoint: meta.revocation_endpoint ?? `${this.issuer}/api/auth/oauth2/revoke`
1858
+ };
1859
+ for (const endpoint of Object.values(endpoints)) validateHttpUrl(endpoint, this.issuer);
1860
+ return endpoints;
1861
+ }
1862
+ throw new Error("OAuth server returned invalid discovery metadata.");
1863
+ }
1864
+ if (res.status !== 404) throw new Error(`OAuth discovery failed (status ${res.status}).`);
1865
+ return {
1866
+ issuer: this.issuer,
1867
+ authorizationEndpoint: `${this.issuer}/api/auth/oauth2/authorize`,
1868
+ tokenEndpoint: `${this.issuer}/api/auth/oauth2/token`,
1869
+ revocationEndpoint: `${this.issuer}/api/auth/oauth2/revoke`
1870
+ };
1871
+ });
1872
+ }
1873
+ buildAuthorizationUrl(input) {
1874
+ const scopes = input.scopes ?? [...BRAINMCP_CLI_DEFAULT_SCOPES];
1875
+ const url = validateHttpUrl(input.authorizationEndpoint, this.issuer);
1876
+ url.searchParams.set("response_type", "code");
1877
+ url.searchParams.set("client_id", this.clientId);
1878
+ url.searchParams.set("redirect_uri", input.redirectUri);
1879
+ url.searchParams.set("scope", scopes.join(" "));
1880
+ url.searchParams.set("state", input.pkce.state);
1881
+ url.searchParams.set("code_challenge", input.pkce.challenge);
1882
+ url.searchParams.set("code_challenge_method", input.pkce.method);
1883
+ url.searchParams.set("prompt", "consent");
1884
+ url.searchParams.set("resource", BRAIN_MCP_ENDPOINT);
1885
+ return url.toString();
1886
+ }
1887
+ async exchangeCodeForTokens(input) {
1888
+ const body = new URLSearchParams({
1889
+ grant_type: "authorization_code",
1890
+ client_id: this.clientId,
1891
+ code: input.code,
1892
+ redirect_uri: input.redirectUri,
1893
+ code_verifier: input.codeVerifier,
1894
+ resource: BRAIN_MCP_ENDPOINT
1895
+ });
1896
+ validateHttpUrl(input.tokenEndpoint, this.issuer);
1897
+ return withRequestDeadline(this.timeoutMs, input.signal, async (signal) => {
1898
+ const res = await this.fetchFn(input.tokenEndpoint, {
1899
+ signal,
1900
+ redirect: "error",
1901
+ method: "POST",
1902
+ headers: {
1903
+ "Content-Type": "application/x-www-form-urlencoded",
1904
+ Accept: "application/json"
1905
+ },
1906
+ body: body.toString()
1907
+ });
1908
+ if (!res.ok) {
1909
+ throw new Error(`Token exchange failed (status ${res.status}).`);
1910
+ }
1911
+ return validateTokenResponse(await res.json());
1912
+ });
1913
+ }
1914
+ async refreshAccessToken(input) {
1915
+ const body = new URLSearchParams({
1916
+ grant_type: "refresh_token",
1917
+ client_id: this.clientId,
1918
+ refresh_token: input.refreshToken,
1919
+ resource: BRAIN_MCP_ENDPOINT
1920
+ });
1921
+ validateHttpUrl(input.tokenEndpoint, this.issuer);
1922
+ return withRequestDeadline(this.timeoutMs, input.signal, async (signal) => {
1923
+ const res = await this.fetchFn(input.tokenEndpoint, {
1924
+ signal,
1925
+ redirect: "error",
1926
+ method: "POST",
1927
+ headers: {
1928
+ "Content-Type": "application/x-www-form-urlencoded",
1929
+ Accept: "application/json"
1930
+ },
1931
+ body: body.toString()
1932
+ });
1933
+ if (!res.ok) {
1934
+ throw new Error(`Token refresh failed (status ${res.status}).`);
1935
+ }
1936
+ return validateTokenResponse(await res.json());
1937
+ });
1938
+ }
1939
+ async revokeToken(input) {
1940
+ const body = new URLSearchParams({
1941
+ client_id: this.clientId,
1942
+ token: input.token
1943
+ });
1944
+ validateHttpUrl(input.revocationEndpoint, this.issuer);
1945
+ return withRequestDeadline(this.timeoutMs, input.signal, async (signal) => {
1946
+ const res = await this.fetchFn(input.revocationEndpoint, {
1947
+ signal,
1948
+ redirect: "error",
1949
+ method: "POST",
1950
+ headers: {
1951
+ "Content-Type": "application/x-www-form-urlencoded"
1952
+ },
1953
+ body: body.toString()
1954
+ });
1955
+ return res.ok;
1956
+ });
1957
+ }
1958
+ };
1959
+
1960
+ // src/lib/api-client.ts
1961
+ var ApiClient = class {
1962
+ apiUrl;
1963
+ store;
1964
+ oauthClient;
1965
+ fetchFn;
1966
+ serviceToken;
1967
+ defaultTimeoutMs;
1968
+ constructor(options) {
1969
+ this.apiUrl = (options.apiUrl ?? process.env.BRAINMCP_API_URL ?? "https://mcp.brainmcp.ai").replace(/\/+$/, "");
1970
+ this.store = options.store;
1971
+ this.fetchFn = options.fetchFn ?? fetch;
1972
+ this.oauthClient = options.oauthClient ?? new OAuthClient({ issuerUrl: this.apiUrl, fetchFn: this.fetchFn });
1973
+ this.serviceToken = options.serviceTokenEnv ?? process.env.BRAINMCP_SERVICE_TOKEN;
1974
+ this.defaultTimeoutMs = options.timeoutMs ?? 15e3;
1975
+ validateHttpUrl(this.apiUrl);
1976
+ }
1977
+ async getEffectiveCredentials() {
1978
+ const stored = await this.store.get();
1979
+ if (stored?.authType === "oauth" && stored.accessToken) {
1980
+ return stored;
1981
+ }
1982
+ if (this.serviceToken) {
1983
+ return {
1984
+ version: 1,
1985
+ authType: "service",
1986
+ serviceToken: this.serviceToken,
1987
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1988
+ };
1989
+ }
1990
+ return stored;
1991
+ }
1992
+ async request(path12, options = {}) {
1993
+ return withRequestDeadline(options.timeoutMs ?? this.defaultTimeoutMs, options.signal, async (signal) => {
1994
+ const explicitAuthorization = new Headers(options.headers).has("Authorization");
1995
+ const useStoredAuth = !options.skipAuth && !explicitAuthorization;
1996
+ const creds = useStoredAuth ? await this.getEffectiveCredentials() : null;
1997
+ signal.throwIfAborted();
1998
+ if (options.expectedAuthorizationId && creds?.authorizationId !== options.expectedAuthorizationId) {
1999
+ throw new Error("Companion authorization changed before the request.");
2000
+ }
2001
+ const url = new URL(path12, `${this.apiUrl}/`);
2002
+ if (url.origin !== new URL(this.apiUrl).origin || url.username || url.password) {
2003
+ throw new Error("API requests must use the configured API origin.");
2004
+ }
2005
+ const isOrientEndpoint = url.pathname.endsWith("/orient");
2006
+ if (creds?.authType === "service" && isOrientEndpoint) {
2007
+ throw new Error(
2008
+ "Service tokens cannot access /orient. Interactive companion OAuth credentials are required for session orientation."
2009
+ );
2010
+ }
2011
+ const requestId = randomUUID3();
2012
+ const execute = async (token2) => {
2013
+ const headers = new Headers(options.headers);
2014
+ headers.set("x-request-id", requestId);
2015
+ headers.set("Accept", "application/json");
2016
+ if (options.body && !headers.has("Content-Type")) {
2017
+ headers.set("Content-Type", "application/json");
2018
+ }
2019
+ if (token2 && useStoredAuth) {
2020
+ headers.set("Authorization", `Bearer ${token2}`);
2021
+ }
2022
+ signal.throwIfAborted();
2023
+ if (options.beforeSend && !options.beforeSend()) throw new Error("Request cancelled because capture consent changed.");
2024
+ return await this.fetchFn(url.toString(), {
2025
+ method: options.method ?? (options.body ? "POST" : "GET"),
2026
+ headers,
2027
+ body: options.body ? JSON.stringify(options.body) : void 0,
2028
+ signal,
2029
+ redirect: "error"
2030
+ });
2031
+ };
2032
+ let token = creds?.authType === "oauth" ? creds.accessToken ?? null : creds?.serviceToken ?? null;
2033
+ let res = await execute(token);
2034
+ if (res.status === 401 && creds?.authType === "oauth" && creds.refreshToken) {
2035
+ await res.body?.cancel();
2036
+ const refreshed = await this.refreshTokensWithLock(token, creds, signal);
2037
+ if (refreshed) {
2038
+ token = refreshed.accessToken ?? null;
2039
+ res = await execute(token);
2040
+ }
2041
+ }
2042
+ if (!res.ok) {
2043
+ const error = new Error(`API request failed (status ${res.status})`);
2044
+ error.status = res.status;
2045
+ throw error;
2046
+ }
2047
+ if (res.status === 204) {
2048
+ return void 0;
2049
+ }
2050
+ return await res.json();
2051
+ });
2052
+ }
2053
+ async refreshTokensWithLock(failedAccessToken, original, signal) {
2054
+ const leaseHolder = randomUUID3();
2055
+ const leaseDurationMs = 15e3;
2056
+ let refreshToken = null;
2057
+ for (let attempt = 0; attempt < 50; attempt += 1) {
2058
+ signal.throwIfAborted();
2059
+ const claim = await this.store.withRefreshLock(async (current) => {
2060
+ signal.throwIfAborted();
2061
+ if (current?.authorizationId !== original.authorizationId || current?.userId !== original.userId) {
2062
+ return { kind: "missing", current: null };
2063
+ }
2064
+ if (!current || current.authType !== "oauth" || !current.refreshToken) {
2065
+ return { kind: "missing", current };
2066
+ }
2067
+ if (current.accessToken && current.accessToken !== failedAccessToken) {
2068
+ return { kind: "reuse", current };
2069
+ }
2070
+ const leaseExpiry = current.refreshLease ? Date.parse(current.refreshLease.expiresAt) : Number.NaN;
2071
+ if (current.refreshLease && current.refreshLease.leaseHolder !== leaseHolder && Number.isFinite(leaseExpiry) && leaseExpiry > Date.now()) {
2072
+ return { kind: "wait", current };
2073
+ }
2074
+ const updated = {
2075
+ ...current,
2076
+ refreshLease: {
2077
+ leaseHolder,
2078
+ expiresAt: new Date(Date.now() + leaseDurationMs).toISOString(),
2079
+ revision: (current.refreshLease?.revision ?? 0) + 1
2080
+ },
2081
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2082
+ };
2083
+ await this.store.set(updated);
2084
+ return { kind: "claimed", current: updated };
2085
+ });
2086
+ if (claim.kind === "missing") return claim.current;
2087
+ if (claim.kind === "reuse") return claim.current;
2088
+ if (claim.kind === "claimed") {
2089
+ refreshToken = claim.current.refreshToken ?? null;
2090
+ break;
2091
+ }
2092
+ await new Promise((resolve) => setTimeout(resolve, 80 + Math.floor(Math.random() * 40)));
2093
+ }
2094
+ if (!refreshToken) return null;
2095
+ try {
2096
+ const endpoints = await this.oauthClient.discoverEndpoints(signal);
2097
+ const tokenResponse = await this.oauthClient.refreshAccessToken({
2098
+ tokenEndpoint: endpoints.tokenEndpoint,
2099
+ refreshToken,
2100
+ signal
2101
+ });
2102
+ return await this.store.withRefreshLock(async (current) => {
2103
+ signal.throwIfAborted();
2104
+ if (!current || current.authType !== "oauth" || current.authorizationId !== original.authorizationId || current.userId !== original.userId) return null;
2105
+ if (current.refreshLease?.leaseHolder !== leaseHolder) {
2106
+ return current.accessToken !== failedAccessToken ? current : null;
2107
+ }
2108
+ if (current.refreshToken && current.refreshToken !== refreshToken) {
2109
+ const reused = { ...current, refreshLease: void 0, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
2110
+ await this.store.set(reused);
2111
+ return reused;
2112
+ }
2113
+ const now = /* @__PURE__ */ new Date();
2114
+ const scopes = tokenResponse.scope === void 0 ? current.scopes : grantedCliScopes(current.scopes ?? [], tokenResponse.scope);
2115
+ const updated = {
2116
+ ...current,
2117
+ accessToken: tokenResponse.access_token,
2118
+ refreshToken: tokenResponse.refresh_token ?? current.refreshToken,
2119
+ scopes,
2120
+ expiresAt: tokenResponse.expires_in ? new Date(now.getTime() + tokenResponse.expires_in * 1e3).toISOString() : current.expiresAt,
2121
+ refreshLease: void 0,
2122
+ updatedAt: now.toISOString()
2123
+ };
2124
+ await this.store.set(updated);
2125
+ return updated;
2126
+ });
2127
+ } catch (error) {
2128
+ await this.store.withRefreshLock(async (current) => {
2129
+ if (current?.authType === "oauth" && current.refreshLease?.leaseHolder === leaseHolder) {
2130
+ await this.store.set({
2131
+ ...current,
2132
+ refreshLease: void 0,
2133
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2134
+ });
2135
+ }
2136
+ });
2137
+ throw error;
2138
+ }
2139
+ }
2140
+ };
2141
+
2142
+ // src/lib/credentials.ts
2143
+ import { AsyncLocalStorage } from "node:async_hooks";
2144
+ import {
2145
+ closeSync,
2146
+ constants,
2147
+ existsSync,
2148
+ fsyncSync,
2149
+ lstatSync,
2150
+ mkdirSync,
2151
+ openSync,
2152
+ readFileSync,
2153
+ renameSync,
2154
+ unlinkSync,
2155
+ writeFileSync
2156
+ } from "node:fs";
2157
+ import { dirname, join } from "node:path";
2158
+
2159
+ // src/lib/vault.ts
2160
+ import { execFile, spawn } from "node:child_process";
2161
+ import { promisify } from "node:util";
2162
+ var execFileAsync = promisify(execFile);
2163
+ var VAULT_SERVICE = "brainmcp.credentials";
2164
+ var LEGACY_VAULT_ACCOUNT = "oauth";
2165
+ var VAULT_ACCOUNT = LEGACY_VAULT_ACCOUNT;
2166
+ var EXEC_TIMEOUT_MS = 4e3;
2167
+ function vaultAccountKey(accountId) {
2168
+ return `oauth:${accountId}`;
2169
+ }
2170
+ var UnavailableSecretVault = class {
2171
+ name = "unavailable";
2172
+ async isAvailable() {
2173
+ return false;
2174
+ }
2175
+ async getSecret() {
2176
+ return null;
2177
+ }
2178
+ async setSecret() {
2179
+ throw new Error("OS credential vault is unavailable.");
2180
+ }
2181
+ async deleteSecret() {
2182
+ }
2183
+ };
2184
+ var MacosSecurityVault = class {
2185
+ name = "macos-keychain";
2186
+ async isAvailable() {
2187
+ try {
2188
+ await execFileAsync("which", ["security"], { timeout: EXEC_TIMEOUT_MS });
2189
+ return true;
2190
+ } catch {
2191
+ return false;
2192
+ }
2193
+ }
2194
+ async getSecret(key = VAULT_ACCOUNT) {
2195
+ try {
2196
+ const { stdout } = await execFileAsync(
2197
+ "security",
2198
+ ["find-generic-password", "-s", VAULT_SERVICE, "-a", key, "-w"],
2199
+ { timeout: EXEC_TIMEOUT_MS }
2200
+ );
2201
+ const value = stdout.trim();
2202
+ if (!value) return null;
2203
+ if (value.startsWith("{")) return value;
2204
+ if (/^[0-9a-fA-F]+$/.test(value) && value.length % 2 === 0) {
2205
+ const decoded = Buffer.from(value, "hex").toString("utf8");
2206
+ return decoded.length > 0 ? decoded : null;
2207
+ }
2208
+ return value;
2209
+ } catch {
2210
+ return null;
2211
+ }
2212
+ }
2213
+ async setSecret(value, key = VAULT_ACCOUNT) {
2214
+ const hex = Buffer.from(value, "utf8").toString("hex");
2215
+ await new Promise((resolve, reject) => {
2216
+ const child = spawn("security", ["-i"], { stdio: ["pipe", "ignore", "ignore"] });
2217
+ const timer = setTimeout(() => {
2218
+ child.kill("SIGKILL");
2219
+ reject(new Error("macOS keychain store timed out"));
2220
+ }, EXEC_TIMEOUT_MS);
2221
+ const done = (fn) => {
2222
+ clearTimeout(timer);
2223
+ fn();
2224
+ };
2225
+ child.on("error", (error) => done(() => reject(error)));
2226
+ child.on("close", (code) => {
2227
+ if (code === 0) done(() => resolve());
2228
+ else done(() => reject(new Error("macOS keychain store failed")));
2229
+ });
2230
+ child.stdin.write(`add-generic-password -U -s ${VAULT_SERVICE} -a ${key} -w ${hex}
2231
+ `);
2232
+ child.stdin.end();
2233
+ });
2234
+ }
2235
+ async deleteSecret(key = VAULT_ACCOUNT) {
2236
+ try {
2237
+ await execFileAsync(
2238
+ "security",
2239
+ ["delete-generic-password", "-s", VAULT_SERVICE, "-a", key],
2240
+ { timeout: EXEC_TIMEOUT_MS }
2241
+ );
2242
+ } catch {
2243
+ }
2244
+ }
2245
+ };
2246
+ var LinuxSecretToolVault = class {
2247
+ name = "libsecret";
2248
+ async isAvailable() {
2249
+ try {
2250
+ await execFileAsync("which", ["secret-tool"], { timeout: EXEC_TIMEOUT_MS });
2251
+ return true;
2252
+ } catch {
2253
+ return false;
2254
+ }
2255
+ }
2256
+ async getSecret(key = VAULT_ACCOUNT) {
2257
+ try {
2258
+ const { stdout } = await execFileAsync(
2259
+ "secret-tool",
2260
+ ["lookup", "service", VAULT_SERVICE, "account", key],
2261
+ { timeout: EXEC_TIMEOUT_MS }
2262
+ );
2263
+ const value = stdout.trim();
2264
+ return value.length > 0 ? value : null;
2265
+ } catch {
2266
+ return null;
2267
+ }
2268
+ }
2269
+ async setSecret(value, key = VAULT_ACCOUNT) {
2270
+ await new Promise((resolve, reject) => {
2271
+ const child = spawn(
2272
+ "secret-tool",
2273
+ ["store", "--label", "BrainMCP CLI", "service", VAULT_SERVICE, "account", key],
2274
+ { stdio: ["pipe", "ignore", "ignore"] }
2275
+ );
2276
+ const timer = setTimeout(() => {
2277
+ child.kill("SIGKILL");
2278
+ reject(new Error("secret-tool store timed out"));
2279
+ }, EXEC_TIMEOUT_MS);
2280
+ const done = (fn) => {
2281
+ clearTimeout(timer);
2282
+ fn();
2283
+ };
2284
+ child.on("error", (error) => done(() => reject(error)));
2285
+ child.on("close", (code) => {
2286
+ if (code === 0) done(() => resolve());
2287
+ else done(() => reject(new Error("secret-tool store failed")));
2288
+ });
2289
+ child.stdin.write(value);
2290
+ child.stdin.end();
2291
+ });
2292
+ }
2293
+ async deleteSecret(key = VAULT_ACCOUNT) {
2294
+ try {
2295
+ await execFileAsync(
2296
+ "secret-tool",
2297
+ ["clear", "service", VAULT_SERVICE, "account", key],
2298
+ { timeout: EXEC_TIMEOUT_MS }
2299
+ );
2300
+ } catch {
2301
+ }
2302
+ }
2303
+ };
2304
+ var WindowsCredentialVault = class {
2305
+ name = "windows-credential-manager";
2306
+ async isAvailable() {
2307
+ return process.platform === "win32";
2308
+ }
2309
+ async getSecret(key = VAULT_ACCOUNT) {
2310
+ try {
2311
+ const { stdout } = await execFileAsync(
2312
+ "powershell.exe",
2313
+ [
2314
+ "-NoProfile",
2315
+ "-Command",
2316
+ `try { $v = New-Object Windows.Security.Credentials.PasswordVault; ($v.Retrieve('${VAULT_SERVICE}','${key}')).Password } catch { '' }`
2317
+ ],
2318
+ { timeout: EXEC_TIMEOUT_MS }
2319
+ );
2320
+ const value = stdout.trim();
2321
+ return value.length > 0 ? value : null;
2322
+ } catch {
2323
+ return null;
2324
+ }
2325
+ }
2326
+ async setSecret(value, key = VAULT_ACCOUNT) {
2327
+ await new Promise((resolve, reject) => {
2328
+ const child = spawn(
2329
+ "powershell.exe",
2330
+ [
2331
+ "-NoProfile",
2332
+ "-Command",
2333
+ `$svc='${VAULT_SERVICE}'; $acct='${key}'; $p=[Console]::In.ReadToEnd(); $v=New-Object Windows.Security.Credentials.PasswordVault; try { $v.Remove($v.Retrieve($svc,$acct)) } catch {}; $v.Add((New-Object Windows.Security.Credentials.PasswordCredential($svc,$acct,$p)))`
2334
+ ],
2335
+ { stdio: ["pipe", "ignore", "ignore"] }
2336
+ );
2337
+ const timer = setTimeout(() => {
2338
+ child.kill("SIGKILL");
2339
+ reject(new Error("Windows PasswordVault store timed out"));
2340
+ }, EXEC_TIMEOUT_MS);
2341
+ const done = (fn) => {
2342
+ clearTimeout(timer);
2343
+ fn();
2344
+ };
2345
+ child.on("error", (error) => done(() => reject(error)));
2346
+ child.on("close", (code) => {
2347
+ if (code === 0) done(() => resolve());
2348
+ else done(() => reject(new Error("Windows PasswordVault store failed")));
2349
+ });
2350
+ child.stdin.write(value);
2351
+ child.stdin.end();
2352
+ });
2353
+ }
2354
+ async deleteSecret(key = VAULT_ACCOUNT) {
2355
+ try {
2356
+ await execFileAsync(
2357
+ "powershell.exe",
2358
+ [
2359
+ "-NoProfile",
2360
+ "-Command",
2361
+ `try { $v = New-Object Windows.Security.Credentials.PasswordVault; $v.Remove($v.Retrieve('${VAULT_SERVICE}','${key}')) } catch {}`
2362
+ ],
2363
+ { timeout: EXEC_TIMEOUT_MS }
2364
+ );
2365
+ } catch {
2366
+ }
2367
+ }
2368
+ };
2369
+ function createPlatformVault() {
2370
+ if (process.platform === "darwin") return new MacosSecurityVault();
2371
+ if (process.platform === "linux") return new LinuxSecretToolVault();
2372
+ if (process.platform === "win32") return new WindowsCredentialVault();
2373
+ return new UnavailableSecretVault();
2374
+ }
2375
+
2376
+ // src/lib/credentials.ts
2377
+ var FileCredentialStore = class {
2378
+ lockContext = new AsyncLocalStorage();
2379
+ baseDir;
2380
+ warn;
2381
+ allowInsecure;
2382
+ constructor(options = {}) {
2383
+ this.baseDir = options.baseDir ?? getBrainHome();
2384
+ this.warn = options.warn ?? ((msg) => console.warn(`[brainmcp credentials] ${msg}`));
2385
+ this.allowInsecure = options.allowInsecureModesForTesting ?? false;
2386
+ }
2387
+ credentialPaths() {
2388
+ const stateDir = this.lockContext.getStore() ?? resolveStateDir(this.baseDir);
2389
+ return {
2390
+ stateDir,
2391
+ filePath: join(stateDir, "credentials.json"),
2392
+ lockPath: join(stateDir, "credentials.lock")
2393
+ };
2394
+ }
2395
+ assertSafePath(targetPath, checkFileExists = true) {
2396
+ if (this.allowInsecure) return;
2397
+ const dir = dirname(targetPath);
2398
+ if (existsSync(dir)) {
2399
+ const dirStat = lstatSync(dir);
2400
+ if (dirStat.isSymbolicLink()) {
2401
+ throw new Error(`Insecure configuration: directory ${dir} is a symbolic link.`);
2402
+ }
2403
+ if (process.platform !== "win32") {
2404
+ const mode = dirStat.mode & 511;
2405
+ if (mode !== 448) {
2406
+ throw new Error(
2407
+ `Insecure permissions on ${dir}: permissions are ${mode.toString(8)}, must be 700.`
2408
+ );
2409
+ }
2410
+ if (process.getuid && dirStat.uid !== process.getuid()) {
2411
+ throw new Error(`Insecure ownership: directory ${dir} is not owned by current user.`);
2412
+ }
2413
+ }
2414
+ }
2415
+ if (existsSync(targetPath)) {
2416
+ const fileStat = lstatSync(targetPath);
2417
+ if (fileStat.isSymbolicLink()) {
2418
+ throw new Error(`Insecure configuration: file ${targetPath} is a symbolic link.`);
2419
+ }
2420
+ if (!fileStat.isFile()) {
2421
+ throw new Error(`Insecure configuration: ${targetPath} is not a regular file.`);
2422
+ }
2423
+ if (process.platform !== "win32") {
2424
+ const mode = fileStat.mode & 511;
2425
+ if (mode !== 384) {
2426
+ throw new Error(
2427
+ `Insecure permissions on ${targetPath}: permissions are ${mode.toString(8)}, must be 600.`
2428
+ );
2429
+ }
2430
+ if (process.getuid && fileStat.uid !== process.getuid()) {
2431
+ throw new Error(`Insecure ownership: file ${targetPath} is not owned by current user.`);
2432
+ }
2433
+ }
2434
+ } else if (checkFileExists) {
2435
+ }
2436
+ }
2437
+ ensureDirectory() {
2438
+ if (!existsSync(this.baseDir)) {
2439
+ mkdirSync(this.baseDir, { recursive: true, mode: 448 });
2440
+ } else if (!this.allowInsecure) {
2441
+ const homeStat = lstatSync(this.baseDir);
2442
+ if (homeStat.isSymbolicLink()) {
2443
+ throw new Error(`Insecure configuration: directory ${this.baseDir} is a symbolic link.`);
2444
+ }
2445
+ }
2446
+ const paths = this.credentialPaths();
2447
+ ensureDirectory0700(paths.stateDir);
2448
+ this.assertSafePath(paths.filePath, false);
2449
+ return paths;
2450
+ }
2451
+ async get() {
2452
+ const { filePath } = this.credentialPaths();
2453
+ if (!existsSync(filePath)) {
2454
+ return null;
2455
+ }
2456
+ this.assertSafePath(filePath);
2457
+ try {
2458
+ const raw = readFileSync(filePath, "utf-8");
2459
+ const parsed = JSON.parse(raw);
2460
+ if (parsed.version !== 1) {
2461
+ this.warn(`Unsupported credentials schema version: ${parsed.version}`);
2462
+ return null;
2463
+ }
2464
+ return parsed;
2465
+ } catch (error) {
2466
+ this.warn(`Failed to read credentials: ${error instanceof Error ? error.message : String(error)}`);
2467
+ return null;
2468
+ }
2469
+ }
2470
+ async set(credentials) {
2471
+ const { filePath, stateDir } = this.ensureDirectory();
2472
+ const tempFile = join(
2473
+ stateDir,
2474
+ `credentials.tmp.${process.pid}.${Math.random().toString(36).slice(2, 8)}`
2475
+ );
2476
+ const data = JSON.stringify(credentials, null, 2);
2477
+ try {
2478
+ const fd = openSync(
2479
+ tempFile,
2480
+ constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | (typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0),
2481
+ 384
2482
+ );
2483
+ try {
2484
+ writeFileSync(fd, data, "utf-8");
2485
+ fsyncSync(fd);
2486
+ } finally {
2487
+ closeSync(fd);
2488
+ }
2489
+ this.assertSafePath(tempFile);
2490
+ renameSync(tempFile, filePath);
2491
+ this.assertSafePath(filePath);
2492
+ } catch (error) {
2493
+ try {
2494
+ if (existsSync(tempFile)) unlinkSync(tempFile);
2495
+ } catch {
2496
+ }
2497
+ throw error;
2498
+ }
2499
+ }
2500
+ async clear() {
2501
+ const { filePath } = this.credentialPaths();
2502
+ if (existsSync(filePath)) {
2503
+ this.assertSafePath(filePath);
2504
+ unlinkSync(filePath);
2505
+ }
2506
+ }
2507
+ async adoptLegacySecret() {
2508
+ }
2509
+ async withRefreshLock(action) {
2510
+ const stateDir = this.credentialPaths().stateDir;
2511
+ return withSessionLock("credentials", () => this.lockContext.run(stateDir, async () => action(await this.get())), { baseDir: stateDir });
2512
+ }
2513
+ };
2514
+ var VaultCredentialStore = class {
2515
+ constructor(vault, lockStore, baseDir) {
2516
+ this.vault = vault;
2517
+ this.lockStore = lockStore;
2518
+ this.baseDir = baseDir;
2519
+ }
2520
+ vault;
2521
+ lockStore;
2522
+ baseDir;
2523
+ lockContext = new AsyncLocalStorage();
2524
+ get backend() {
2525
+ return "os_vault";
2526
+ }
2527
+ backendMarkerPath() {
2528
+ return join(this.lockContext.getStore()?.stateDir ?? resolveStateDir(this.baseDir ?? getBrainHome()), "credentials.backend.json");
2529
+ }
2530
+ secretKey() {
2531
+ if (this.lockContext.getStore()) return this.lockContext.getStore().key;
2532
+ const accountId = getCurrentAccountId(this.baseDir ?? getBrainHome());
2533
+ return accountId ? vaultAccountKey(accountId) : LEGACY_VAULT_ACCOUNT;
2534
+ }
2535
+ async get() {
2536
+ if (!existsSync(this.backendMarkerPath())) return null;
2537
+ assertNotSymlink(this.backendMarkerPath());
2538
+ const raw = await this.vault.getSecret(this.secretKey());
2539
+ if (!raw) return null;
2540
+ try {
2541
+ const parsed = JSON.parse(raw);
2542
+ if (parsed.version !== 1) return null;
2543
+ return parsed;
2544
+ } catch {
2545
+ return null;
2546
+ }
2547
+ }
2548
+ async set(credentials) {
2549
+ const marker = this.backendMarkerPath();
2550
+ await this.vault.setSecret(JSON.stringify(credentials), this.secretKey());
2551
+ ensureDirectory0700(dirname(marker));
2552
+ writeAtomic0600(
2553
+ marker,
2554
+ JSON.stringify({ backend: "os_vault", updatedAt: (/* @__PURE__ */ new Date()).toISOString() })
2555
+ );
2556
+ }
2557
+ async clear() {
2558
+ const marker = this.backendMarkerPath();
2559
+ await this.vault.deleteSecret(this.secretKey());
2560
+ if (existsSync(marker)) {
2561
+ assertNotSymlink(marker);
2562
+ unlinkSync(marker);
2563
+ }
2564
+ }
2565
+ async adoptLegacySecret() {
2566
+ const accountId = getCurrentAccountId(this.baseDir ?? getBrainHome());
2567
+ if (!accountId) return;
2568
+ const namespaced = vaultAccountKey(accountId);
2569
+ if (await this.vault.getSecret(namespaced)) return;
2570
+ const legacy = await this.vault.getSecret(LEGACY_VAULT_ACCOUNT);
2571
+ if (!legacy) return;
2572
+ await this.vault.setSecret(legacy, namespaced);
2573
+ await this.vault.deleteSecret(LEGACY_VAULT_ACCOUNT);
2574
+ writeAtomic0600(this.backendMarkerPath(), JSON.stringify({ backend: "os_vault", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
2575
+ }
2576
+ async withRefreshLock(action) {
2577
+ const context = { key: this.secretKey(), stateDir: resolveStateDir(this.baseDir ?? getBrainHome()) };
2578
+ return this.lockStore.withRefreshLock(() => this.lockContext.run(context, async () => action(await this.get())));
2579
+ }
2580
+ };
2581
+ var FILE_CREDENTIALS_ACK_FILENAME = "allow-file-credentials";
2582
+ function fileCredentialsAcknowledged(baseDir) {
2583
+ if (process.env.BRAINMCP_ALLOW_FILE_CREDENTIALS === "1") return true;
2584
+ const home = baseDir ?? getBrainHome();
2585
+ const acknowledgement = join(home, FILE_CREDENTIALS_ACK_FILENAME);
2586
+ if (!existsSync(acknowledgement)) return false;
2587
+ const stat = lstatSync(acknowledgement);
2588
+ if (stat.isSymbolicLink() || !stat.isFile()) return false;
2589
+ if (process.platform !== "win32") {
2590
+ if ((stat.mode & 511) !== 384) return false;
2591
+ if (process.getuid && stat.uid !== process.getuid()) return false;
2592
+ }
2593
+ return true;
2594
+ }
2595
+ function writeFileCredentialsAcknowledgement(baseDir) {
2596
+ const home = baseDir ?? getBrainHome();
2597
+ ensureDirectory0700(home);
2598
+ writeAtomic0600(
2599
+ join(home, FILE_CREDENTIALS_ACK_FILENAME),
2600
+ JSON.stringify({ acknowledgedAt: (/* @__PURE__ */ new Date()).toISOString() })
2601
+ );
2602
+ }
2603
+ async function createDefaultCredentialStore(options = {}) {
2604
+ const warn = options.warn ?? ((msg) => console.warn(`[brainmcp credentials] ${msg}`));
2605
+ const fileStore = new FileCredentialStore({
2606
+ baseDir: options.baseDir,
2607
+ warn
2608
+ });
2609
+ const vault = options.vault ?? (options.baseDir ? new UnavailableSecretVault() : createPlatformVault());
2610
+ const home = options.baseDir ?? getBrainHome();
2611
+ const existingFile = existsSync(join(resolveStateDir(home), "credentials.json"));
2612
+ if (existingFile) return { store: fileStore, backend: "file" };
2613
+ if (await vault.isAvailable()) {
2614
+ return { store: new VaultCredentialStore(vault, fileStore, options.baseDir), backend: "os_vault" };
2615
+ }
2616
+ const allowFile = options.allowFileCredentials === true || existingFile || fileCredentialsAcknowledged(options.baseDir);
2617
+ if (!allowFile) {
2618
+ throw new Error(
2619
+ "OS credential vault is unavailable. Re-run `brainmcp login --allow-file-credentials` to store tokens in an owner-only ~/.brainmcp file (0700/0600, no symlinks)."
2620
+ );
2621
+ }
2622
+ if (!existingFile) {
2623
+ warn(
2624
+ "OS credential vault is unavailable; using the owner-only ~/.brainmcp file store. This is an explicit fallback, not the default."
2625
+ );
2626
+ }
2627
+ if (options.persistFileAcknowledgement) {
2628
+ writeFileCredentialsAcknowledgement(options.baseDir);
2629
+ }
2630
+ return { store: fileStore, backend: "file" };
2631
+ }
2632
+
2633
+ // src/runtime/hook-dispatcher.ts
2634
+ var MAX_STDIN_BYTES = 1024 * 1024;
2635
+ var DEFAULT_EVENT_TIMEOUT_MS = 1e3;
2636
+ var SESSION_END_TIMEOUT_MS = 3e3;
2637
+ function isSessionEndEvent(event) {
2638
+ return event === "SessionEnd" || event === "sessionEnd" || event === "session_end";
2639
+ }
2640
+ function editedPathFromEvent(event) {
2641
+ if (!event.cwd || !event.toolInput || typeof event.toolInput !== "object") return void 0;
2642
+ const input = event.toolInput;
2643
+ const candidate = [input.file_path, input.filePath, input.path].find(
2644
+ (value) => typeof value === "string" && value.trim().length > 0
2645
+ );
2646
+ return candidate ? filterRelativePaths([candidate], event.cwd)[0] : void 0;
2647
+ }
2648
+ async function readBoundedStdin(stream = process.stdin, maxBytes = MAX_STDIN_BYTES, signal) {
2649
+ return new Promise((resolve, reject) => {
2650
+ let totalBytes = 0;
2651
+ const chunks = [];
2652
+ let settled = false;
2653
+ const cleanup = () => {
2654
+ stream.removeListener("data", data);
2655
+ stream.removeListener("end", end);
2656
+ stream.removeListener("error", fail);
2657
+ signal?.removeEventListener("abort", abort);
2658
+ stream.pause();
2659
+ };
2660
+ const fail = (err) => {
2661
+ if (settled) return;
2662
+ settled = true;
2663
+ cleanup();
2664
+ reject(err);
2665
+ };
2666
+ const abort = () => fail(new Error("Hook input timed out"));
2667
+ const data = (chunk) => {
2668
+ const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
2669
+ totalBytes += buf.length;
2670
+ if (totalBytes > maxBytes) {
2671
+ fail(new Error(`Input exceeded maximum size limit of ${maxBytes} bytes`));
2672
+ return;
2673
+ }
2674
+ chunks.push(buf);
2675
+ };
2676
+ const end = () => {
2677
+ if (settled) return;
2678
+ settled = true;
2679
+ cleanup();
2680
+ resolve(Buffer.concat(chunks).toString("utf8"));
2681
+ };
2682
+ stream.on("data", data);
2683
+ stream.on("end", end);
2684
+ stream.on("error", fail);
2685
+ signal?.addEventListener("abort", abort, { once: true });
2686
+ if (signal?.aborted) abort();
2687
+ });
2688
+ }
2689
+ function hostOutput(event, additionalContext) {
2690
+ try {
2691
+ return emitHostOutput(event.client, event, additionalContext);
2692
+ } catch {
2693
+ return {};
2694
+ }
2695
+ }
2696
+ async function loadRuntimeClient(options) {
2697
+ if (options?.apiClient) {
2698
+ return {
2699
+ apiClient: options.apiClient,
2700
+ creds: options.credentials === void 0 ? await options.apiClient.getEffectiveCredentials() : options.credentials
2701
+ };
2702
+ }
2703
+ try {
2704
+ const { store } = await createDefaultCredentialStore({
2705
+ baseDir: options?.baseDir,
2706
+ warn: () => void 0
2707
+ });
2708
+ const apiClient = new ApiClient({ store });
2709
+ return { apiClient, creds: await apiClient.getEffectiveCredentials() };
2710
+ } catch {
2711
+ return { apiClient: null, creds: null };
2712
+ }
2713
+ }
2714
+ async function dispatchHookEvent(event, options) {
2715
+ const timeoutMs = options?.timeoutMs ?? (isSessionEndEvent(event.event) ? SESSION_END_TIMEOUT_MS : DEFAULT_EVENT_TIMEOUT_MS);
2716
+ const controller = new AbortController();
2717
+ const onExternalAbort = () => controller.abort();
2718
+ options?.signal?.addEventListener("abort", onExternalAbort, { once: true });
2719
+ if (options?.signal?.aborted) controller.abort();
2720
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
2721
+ const credentialOptions = options;
2722
+ try {
2723
+ if ((!event.sessionKey || event.sessionKey === "default_session") && !isSessionStartEvent(event.event)) {
2724
+ return { handled: false, emptyFallback: true, outputPayload: {} };
2725
+ }
2726
+ event = { ...event, sessionKey: crypto3.createHash("sha256").update(JSON.stringify([
2727
+ event.client,
2728
+ event.sessionKey,
2729
+ event.workspaceRoots?.length ? [...event.workspaceRoots].sort() : [event.cwd ?? ""]
2730
+ ])).digest("hex") };
2731
+ options = { ...options, baseDir: resolveStateDir(options?.baseDir) };
2732
+ if (controller.signal.aborted) {
2733
+ throw new Error("Hook dispatch timed out");
2734
+ }
2735
+ const prefs = readPreferences(options?.baseDir);
2736
+ const roots = event.workspaceRoots?.length ? event.workspaceRoots : event.cwd ? [event.cwd] : [];
2737
+ const policies = roots.map(readProjectCaptureRestrictions);
2738
+ const projectWorkspaces = roots.map(readProjectWorkspaceId);
2739
+ const projectPolicy = {
2740
+ captureDisabled: policies.some((policy) => policy.captureDisabled) || projectWorkspaces.some((workspace) => workspace.kind === "invalid" || workspace.kind === "workspace" && workspace.workspaceId !== prefs?.enabledWorkspaceId),
2741
+ promptsDisabled: policies.some((policy) => policy.promptsDisabled)
2742
+ };
2743
+ const persistPrompts = Boolean(
2744
+ prefs?.consent?.captureEnabled && prefs?.consent?.includePrompts && !projectPolicy.promptsDisabled
2745
+ );
2746
+ const captureRuntimeEnabled = Boolean(
2747
+ prefs?.consent?.captureEnabled && !projectPolicy.captureDisabled
2748
+ );
2749
+ if (event.event !== "StopNudge" && isStopEvent(event.event, event.stopHookActive)) {
2750
+ logRuntimeEvent(
2751
+ "info",
2752
+ "hook_stop_ignored",
2753
+ { event: event.event, sessionKey: event.sessionKey, client: event.client },
2754
+ options?.baseDir
2755
+ );
2756
+ return {
2757
+ handled: true,
2758
+ emptyFallback: false,
2759
+ outputPayload: hostOutput(event)
2760
+ };
2761
+ }
2762
+ if (!isSessionStartEvent(event.event) && !captureRuntimeEnabled && !event.nudgeLedger && event.event !== "StopNudge") {
2763
+ return {
2764
+ handled: true,
2765
+ emptyFallback: false,
2766
+ outputPayload: hostOutput(event)
2767
+ };
2768
+ }
2769
+ if (isSessionStartEvent(event.event) && !captureRuntimeEnabled && !event.nudgeLedger) {
2770
+ const { apiClient: apiClient2, creds: creds2 } = await withRequestDeadline(timeoutMs, controller.signal, () => loadRuntimeClient(credentialOptions));
2771
+ const additionalContext2 = apiClient2 ? await safeSessionStartOrientation({
2772
+ apiClient: apiClient2,
2773
+ creds: creds2,
2774
+ signal: controller.signal,
2775
+ baseDir: options?.baseDir,
2776
+ cwd: event.cwd,
2777
+ workspaceRoots: event.workspaceRoots
2778
+ }) : loginOrientationMessage();
2779
+ if (controller.signal.aborted) throw new Error("Hook dispatch timed out");
2780
+ return {
2781
+ handled: true,
2782
+ emptyFallback: false,
2783
+ outputPayload: hostOutput(event, additionalContext2)
2784
+ };
2785
+ }
2786
+ const isMeaningful = Boolean(
2787
+ event.prompt || event.assistantText || event.event === "UserPrompt" || event.event === "prompt" || event.toolName && !/read|list/i.test(event.toolName)
2788
+ );
2789
+ if (isSessionStartEvent(event.event)) pruneLocalState({ baseDir: options.baseDir });
2790
+ const ledger = await recordSessionEvent({
2791
+ sessionKey: event.sessionKey,
2792
+ baseDir: options?.baseDir,
2793
+ isMeaningful,
2794
+ editedPath: captureRuntimeEnabled ? editedPathFromEvent(event) : void 0,
2795
+ prompt: persistPrompts ? event.prompt : void 0,
2796
+ assistantSummary: captureRuntimeEnabled ? event.assistantText : void 0,
2797
+ model: captureRuntimeEnabled ? event.model : void 0,
2798
+ persistPrompts,
2799
+ captureRevision: captureRuntimeEnabled ? prefs?.consent.revision ?? 0 : void 0,
2800
+ workspaceId: captureRuntimeEnabled ? prefs?.enabledWorkspaceId : void 0,
2801
+ projectKey: event.cwd ? captureProjectKey(event.cwd) : void 0,
2802
+ toolResult: event.toolName ? {
2803
+ toolName: event.toolName,
2804
+ result: event.toolResult
2805
+ } : void 0
2806
+ });
2807
+ if (controller.signal.aborted) {
2808
+ throw new Error("Hook dispatch timed out");
2809
+ }
2810
+ if (event.event === "StopNudge") {
2811
+ const lastMeaningful = ledger.lastMeaningfulSequence ?? 0;
2812
+ const alreadyHandledThrough = Math.max(
2813
+ ledger.lastSuccessfulWriteBackSequence ?? 0,
2814
+ ledger.lastNudgedSequence ?? 0
2815
+ );
2816
+ if (!event.stopHookActive && lastMeaningful > alreadyHandledThrough) {
2817
+ await markSessionNudged({
2818
+ sessionKey: event.sessionKey,
2819
+ nudgedThroughSequence: lastMeaningful,
2820
+ baseDir: options?.baseDir
2821
+ });
2822
+ return {
2823
+ handled: true,
2824
+ emptyFallback: false,
2825
+ outputPayload: hostOutput(
2826
+ event,
2827
+ "Before finishing, consider whether this work produced genuinely new durable project knowledge. If so, use brain_remember, brain_amend, or brain_change_propose; otherwise continue without writing."
2828
+ )
2829
+ };
2830
+ }
2831
+ return { handled: true, emptyFallback: false, outputPayload: {} };
2832
+ }
2833
+ if (isStopEvent(event.event, event.stopHookActive)) {
2834
+ logRuntimeEvent(
2835
+ "info",
2836
+ "hook_stop_ignored",
2837
+ {
2838
+ event: event.event,
2839
+ sessionKey: event.sessionKey,
2840
+ client: event.client
2841
+ },
2842
+ options?.baseDir
2843
+ );
2844
+ return {
2845
+ handled: true,
2846
+ emptyFallback: false,
2847
+ outputPayload: hostOutput(event)
2848
+ };
2849
+ }
2850
+ const { apiClient, creds } = await withRequestDeadline(timeoutMs, controller.signal, () => loadRuntimeClient(credentialOptions));
2851
+ let additionalContext;
2852
+ if (isSessionStartEvent(event.event)) {
2853
+ additionalContext = apiClient ? await safeSessionStartOrientation({
2854
+ apiClient,
2855
+ creds,
2856
+ signal: controller.signal,
2857
+ baseDir: options?.baseDir,
2858
+ cwd: event.cwd,
2859
+ workspaceRoots: event.workspaceRoots
2860
+ }) : loginOrientationMessage();
2861
+ } else if (isUserPromptEvent(event.event)) {
2862
+ additionalContext = void 0;
2863
+ }
2864
+ if (controller.signal.aborted) {
2865
+ throw new Error("Hook dispatch timed out");
2866
+ }
2867
+ const trigger = captureTriggerForEvent(event.event);
2868
+ const hasUncapturedMeaningfulSlice = (ledger.lastMeaningfulSequence ?? 0) > (ledger.lastCapturedSequence ?? 0);
2869
+ if (trigger && isCaptureEligible(event.event, prefs, projectPolicy) && hasUncapturedMeaningfulSlice) {
2870
+ const workspaceId = resolveCaptureWorkspaceId(prefs);
2871
+ if (!workspaceId) {
2872
+ logRuntimeEvent(
2873
+ "warn",
2874
+ "capture_workspace_unresolved",
2875
+ {
2876
+ event: event.event,
2877
+ sessionKey: event.sessionKey
2878
+ },
2879
+ options?.baseDir
2880
+ );
2881
+ } else if (!canEnqueueCapture(options?.baseDir)) {
2882
+ logRuntimeEvent(
2883
+ "warn",
2884
+ "capture_queue_capped",
2885
+ { event: event.event, sessionKey: event.sessionKey },
2886
+ options?.baseDir
2887
+ );
2888
+ } else {
2889
+ if (creds?.authType !== "oauth" || !creds.authorizationId || !creds.scopes?.includes("digests:write") || !creds.workspaces?.includes(workspaceId)) {
2890
+ logRuntimeEvent(
2891
+ "warn",
2892
+ "capture_authorization_unresolved",
2893
+ { event: event.event, sessionKey: event.sessionKey },
2894
+ options?.baseDir
2895
+ );
2896
+ } else {
2897
+ const payload = buildDigestPayload({
2898
+ trigger,
2899
+ workspaceId,
2900
+ host: event.client,
2901
+ model: event.model ?? ledger.model,
2902
+ summary: ledger.assistantSummary,
2903
+ paths: ledger.editedPaths,
2904
+ gitMetadata: collectGitMetadata(event.cwd ?? event.workspaceRoots?.[0]),
2905
+ prompts: ledger.prompts,
2906
+ includePrompts: Boolean(prefs?.consent?.includePrompts && !projectPolicy.promptsDisabled)
2907
+ });
2908
+ const digestId = payload.clientDigestId;
2909
+ await queueSessionCapture({
2910
+ sessionKey: event.sessionKey,
2911
+ capturedThroughSequence: ledger.lastMeaningfulSequence ?? ledger.eventSequence,
2912
+ baseDir: options?.baseDir,
2913
+ item: {
2914
+ digestId,
2915
+ workspaceId,
2916
+ captureTrigger: trigger,
2917
+ policyVersion: prefs?.consent?.policyVersion ?? "2026-09-02.capture-v1",
2918
+ digestText: serializeDigestPayload(payload),
2919
+ authorizationId: creds.authorizationId,
2920
+ consentRevision: prefs?.consent?.revision ?? 0
2921
+ }
2922
+ });
2923
+ logRuntimeEvent(
2924
+ "info",
2925
+ "capture_queued",
2926
+ {
2927
+ sessionKey: event.sessionKey,
2928
+ digestId,
2929
+ trigger
2930
+ },
2931
+ options?.baseDir
2932
+ );
2933
+ }
2934
+ }
2935
+ }
2936
+ const canFlush = Boolean(apiClient) && creds?.authType === "oauth" && Boolean(creds.accessToken) && Boolean(creds.authorizationId) && Array.isArray(creds.scopes) && creds.scopes.includes("digests:write");
2937
+ const isSessionEnd = isSessionEndEvent(event.event);
2938
+ if (canFlush && apiClient && !controller.signal.aborted && !isSessionStartEvent(event.event) && !isSessionEnd) {
2939
+ await flushCaptureQueue({
2940
+ apiClient,
2941
+ baseDir: options?.baseDir,
2942
+ signal: controller.signal,
2943
+ allowAutomatic: prefs?.consent?.captureEnabled === true && !projectPolicy.captureDisabled,
2944
+ credentials: creds
2945
+ }).catch(() => void 0);
2946
+ }
2947
+ logRuntimeEvent(
2948
+ "info",
2949
+ "hook_event_processed",
2950
+ {
2951
+ event: event.event,
2952
+ sessionKey: event.sessionKey,
2953
+ client: event.client
2954
+ },
2955
+ options?.baseDir
2956
+ );
2957
+ return {
2958
+ handled: true,
2959
+ emptyFallback: false,
2960
+ outputPayload: hostOutput(event, additionalContext)
2961
+ };
2962
+ } catch (err) {
2963
+ logRuntimeEvent(
2964
+ "warn",
2965
+ "hook_dispatch_failed",
2966
+ {
2967
+ event: event.event,
2968
+ sessionKey: event.sessionKey,
2969
+ error: err instanceof Error ? err.message : String(err)
2970
+ },
2971
+ options?.baseDir
2972
+ );
2973
+ return {
2974
+ handled: false,
2975
+ emptyFallback: true,
2976
+ outputPayload: {},
2977
+ error: err instanceof Error ? err.message : String(err)
2978
+ };
2979
+ } finally {
2980
+ clearTimeout(timeoutId);
2981
+ options?.signal?.removeEventListener("abort", onExternalAbort);
2982
+ }
2983
+ }
2984
+
2985
+ // src/runtime/installer.ts
2986
+ import fs10 from "node:fs";
2987
+ import path10 from "node:path";
2988
+ import crypto4 from "node:crypto";
2989
+ import { createRequire } from "node:module";
2990
+ import { fileURLToPath } from "node:url";
2991
+ var RUNTIME_VERSION_RE = /^[0-9A-Za-z][0-9A-Za-z._-]{0,63}$/;
2992
+ function normalizeRuntimeVersion(version) {
2993
+ const normalized = version.trim().replace(/^v/, "");
2994
+ if (!RUNTIME_VERSION_RE.test(normalized) || normalized.includes("..")) {
2995
+ throw new Error(`Invalid BrainMCP runtime version: ${version}`);
2996
+ }
2997
+ return normalized;
2998
+ }
2999
+ function getRuntimesDir(baseDir) {
3000
+ const home = baseDir ?? getBrainHome();
3001
+ const runtimesDir = path10.join(home, "runtime");
3002
+ ensureDirectory0700(runtimesDir);
3003
+ return runtimesDir;
3004
+ }
3005
+ function getVersionedRuntimeDir(version, baseDir) {
3006
+ const runtimesDir = getRuntimesDir(baseDir);
3007
+ const versionDir = path10.join(runtimesDir, `v${normalizeRuntimeVersion(version)}`);
3008
+ ensureDirectory0700(versionDir);
3009
+ return versionDir;
3010
+ }
3011
+ function getVersionedRuntimePath(version, baseDir) {
3012
+ const versionDir = getVersionedRuntimeDir(version, baseDir);
3013
+ return path10.join(versionDir, "brainmcp-hook-runtime.mjs");
3014
+ }
3015
+ function installRuntimeBundle(version, bundleContent, baseDir) {
3016
+ const versionDir = getVersionedRuntimeDir(version, baseDir);
3017
+ const targetPath = path10.join(versionDir, "brainmcp-hook-runtime.mjs");
3018
+ assertNotSymlink(targetPath);
3019
+ const tmpPath = `${targetPath}.tmp.${crypto4.randomUUID()}`;
3020
+ try {
3021
+ fs10.writeFileSync(tmpPath, bundleContent, { flag: "wx", mode: 493 });
3022
+ fs10.renameSync(tmpPath, targetPath);
3023
+ } catch (error) {
3024
+ try {
3025
+ fs10.unlinkSync(tmpPath);
3026
+ } catch {
3027
+ }
3028
+ throw error;
3029
+ }
3030
+ return {
3031
+ version,
3032
+ runtimePath: targetPath,
3033
+ installedAt: (/* @__PURE__ */ new Date()).toISOString()
3034
+ };
3035
+ }
3036
+ function listInstalledRuntimes(baseDir) {
3037
+ const runtimesDir = getRuntimesDir(baseDir);
3038
+ const entries = fs10.readdirSync(runtimesDir);
3039
+ const runtimes = [];
3040
+ for (const entry of entries) {
3041
+ if (!entry.startsWith("v")) continue;
3042
+ const versionDir = path10.join(runtimesDir, entry);
3043
+ const runtimePath = path10.join(versionDir, "brainmcp-hook-runtime.mjs");
3044
+ if (fs10.existsSync(runtimePath)) {
3045
+ try {
3046
+ const stat = fs10.lstatSync(runtimePath);
3047
+ if (!stat.isSymbolicLink()) {
3048
+ runtimes.push({
3049
+ version: entry.replace(/^v/, ""),
3050
+ runtimePath,
3051
+ installedAt: stat.mtime.toISOString()
3052
+ });
3053
+ }
3054
+ } catch {
3055
+ }
3056
+ }
3057
+ }
3058
+ return runtimes;
3059
+ }
3060
+ function removeRuntimeVersion(version, referencedPaths, baseDir) {
3061
+ const targetPath = getVersionedRuntimePath(version, baseDir);
3062
+ const normalizedTarget = path10.resolve(targetPath);
3063
+ const isReferenced = referencedPaths.some((ref) => {
3064
+ const normRef = path10.resolve(ref);
3065
+ return normRef === normalizedTarget || normRef.includes(`/v${normalizeRuntimeVersion(version)}/`);
3066
+ });
3067
+ if (isReferenced) {
3068
+ return {
3069
+ removed: false,
3070
+ blockedReason: `Runtime v${version} is still referenced by an active host configuration`
3071
+ };
3072
+ }
3073
+ const versionDir = getVersionedRuntimeDir(version, baseDir);
3074
+ if (fs10.existsSync(versionDir)) {
3075
+ fs10.rmSync(versionDir, { recursive: true, force: true });
3076
+ return { removed: true };
3077
+ }
3078
+ return { removed: false, blockedReason: `Runtime v${version} does not exist` };
3079
+ }
3080
+ function cliPackageVersion() {
3081
+ try {
3082
+ const require2 = createRequire(import.meta.url);
3083
+ return String(require2("../../package.json").version);
3084
+ } catch {
3085
+ return "0.1.13";
3086
+ }
3087
+ }
3088
+ function loadPackagedRuntimeBundle() {
3089
+ const here = path10.dirname(fileURLToPath(import.meta.url));
3090
+ const candidates = [
3091
+ path10.join(here, "brainmcp-hook-runtime.mjs"),
3092
+ path10.join(here, "../../dist/runtime/brainmcp-hook-runtime.mjs")
3093
+ ];
3094
+ for (const candidate of candidates) {
3095
+ if (fs10.existsSync(candidate)) {
3096
+ return fs10.readFileSync(candidate, "utf8");
3097
+ }
3098
+ }
3099
+ return null;
3100
+ }
3101
+
3102
+ // src/runtime/index.ts
3103
+ async function runHookRuntimeCli(args = process.argv.slice(2)) {
3104
+ try {
3105
+ await withRequestDeadline(args[1]?.toLowerCase().includes("sessionend") ? 2500 : 1500, void 0, async (signal) => {
3106
+ const rawInput = await readBoundedStdin(process.stdin, void 0, signal);
3107
+ if (!rawInput.trim()) {
3108
+ process.stdout.write("{}\n");
3109
+ process.exitCode = 0;
3110
+ return;
3111
+ }
3112
+ let parsed;
3113
+ try {
3114
+ parsed = JSON.parse(rawInput);
3115
+ } catch {
3116
+ process.stdout.write("{}\n");
3117
+ process.exitCode = 0;
3118
+ return;
3119
+ }
3120
+ const host = String(args[0] || parsed.client || "claude-code");
3121
+ const eventName = String(args[1] || parsed.event || parsed.hook_event_name || "GenericEvent");
3122
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Invalid hook input");
3123
+ const normalizedEvent = parseHostEvent(host, parsed, eventName);
3124
+ normalizedEvent.nudgeLedger = args.includes("--nudge-ledger");
3125
+ const res = await dispatchHookEvent(normalizedEvent, { signal });
3126
+ signal.throwIfAborted();
3127
+ process.stdout.write(JSON.stringify(res.outputPayload ?? {}) + "\n");
3128
+ process.exitCode = 0;
3129
+ });
3130
+ } catch {
3131
+ logRuntimeEvent("warn", "hook_input_or_deadline_failed");
3132
+ process.stdout.write("{}\n");
3133
+ process.exitCode = 0;
3134
+ }
3135
+ }
3136
+ function isMainModule() {
3137
+ const entry = process.argv[1];
3138
+ if (!entry) return false;
3139
+ try {
3140
+ return fs11.realpathSync(fileURLToPath2(import.meta.url)) === fs11.realpathSync(path11.resolve(entry));
3141
+ } catch {
3142
+ return false;
3143
+ }
3144
+ }
3145
+ if (isMainModule()) {
3146
+ void runHookRuntimeCli();
3147
+ }
3148
+ export {
3149
+ ACCOUNTS_DIR_NAME,
3150
+ CURRENT_ACCOUNT_FILENAME,
3151
+ CURRENT_ACCOUNT_SCHEMA_VERSION,
3152
+ DEFAULT_EVENT_TIMEOUT_MS,
3153
+ MAX_STDIN_BYTES,
3154
+ SERVICE_ACCOUNT_ID,
3155
+ SESSION_END_TIMEOUT_MS,
3156
+ acquireSessionLock,
3157
+ activateAccount,
3158
+ assertNotSymlink,
3159
+ clearCurrentAccount,
3160
+ cliPackageVersion,
3161
+ dispatchHookEvent,
3162
+ enqueueCaptureItem,
3163
+ ensureDirectory0700,
3164
+ getAccountStateDir,
3165
+ getBrainHome,
3166
+ getCurrentAccountId,
3167
+ getCurrentAccountPath,
3168
+ getLogsDir,
3169
+ getPreferencesPath,
3170
+ getQueueDir,
3171
+ getQueueItemPath,
3172
+ getRuntimesDir,
3173
+ getSessionLedgerPath,
3174
+ getSessionLockPath,
3175
+ getVersionedRuntimeDir,
3176
+ getVersionedRuntimePath,
3177
+ installRuntimeBundle,
3178
+ isProcessAlive,
3179
+ listInstalledRuntimes,
3180
+ listQueueItems,
3181
+ loadMostRecentSessionLedger,
3182
+ loadPackagedRuntimeBundle,
3183
+ loadSessionLedger,
3184
+ logRuntimeEvent,
3185
+ markSessionCaptured,
3186
+ markSessionNudged,
3187
+ migrateLegacyFlatState,
3188
+ normalizeRuntimeVersion,
3189
+ parseBrainToolResult,
3190
+ previewCapturePurge,
3191
+ pruneLocalState,
3192
+ purgeCaptureLocalState,
3193
+ queueSessionCapture,
3194
+ readBoundedStdin,
3195
+ readPreferences,
3196
+ readSafeJson,
3197
+ recordSessionEvent,
3198
+ removeQueueItem,
3199
+ removeRuntimeVersion,
3200
+ resolveStateDir,
3201
+ runHookRuntimeCli,
3202
+ sanitizeAccountId,
3203
+ saveSessionLedger,
3204
+ scrubMetadata,
3205
+ switchCurrentAccount,
3206
+ updateQueueItem,
3207
+ validateCurrentAccountRecord,
3208
+ withSessionLock,
3209
+ writeAtomic0600,
3210
+ writePreferences
3211
+ };