@rubytech/create-maxy 1.0.892 → 1.0.894

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 (45) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/oauth-llm/dist/index.d.ts +2 -2
  3. package/payload/platform/lib/oauth-llm/dist/index.js +1 -1
  4. package/payload/platform/lib/oauth-llm/src/index.ts +2 -2
  5. package/payload/platform/plugins/admin/PLUGIN.md +1 -2
  6. package/payload/platform/plugins/admin/mcp/dist/__tests__/skill-load-required-inputs.test.d.ts +2 -0
  7. package/payload/platform/plugins/admin/mcp/dist/__tests__/skill-load-required-inputs.test.d.ts.map +1 -0
  8. package/payload/platform/plugins/admin/mcp/dist/__tests__/skill-load-required-inputs.test.js +141 -0
  9. package/payload/platform/plugins/admin/mcp/dist/__tests__/skill-load-required-inputs.test.js.map +1 -0
  10. package/payload/platform/plugins/admin/mcp/dist/index.js +19 -59
  11. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  12. package/payload/platform/plugins/admin/mcp/dist/skill-resolution.d.ts +1 -0
  13. package/payload/platform/plugins/admin/mcp/dist/skill-resolution.d.ts.map +1 -1
  14. package/payload/platform/plugins/admin/mcp/dist/skill-resolution.js +48 -0
  15. package/payload/platform/plugins/admin/mcp/dist/skill-resolution.js.map +1 -1
  16. package/payload/platform/plugins/docs/references/troubleshooting.md +2 -0
  17. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/llm-classifier.test.js +1 -1
  18. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/llm-classifier.test.js.map +1 -1
  19. package/payload/platform/templates/agents/admin/IDENTITY.md +4 -2
  20. package/payload/premium-plugins/real-agency/plugins/brochures/skills/make-brochure/SKILL.md +3 -0
  21. package/payload/premium-plugins/real-agency/plugins/brochures/skills/property-brochure/SKILL.md +2 -0
  22. package/payload/server/chunk-4S22HQC6.js +9440 -0
  23. package/payload/server/chunk-FSJLLIEA.js +3570 -0
  24. package/payload/server/client-pool-XM2AWBV5.js +34 -0
  25. package/payload/server/maxy-edge.js +2 -2
  26. package/payload/server/public/assets/{Checkbox-CeujDRv0.js → Checkbox-C6ZCsPvl.js} +1 -1
  27. package/payload/server/public/assets/{admin-BM3orGyK.js → admin-BGhZNd6Q.js} +6 -6
  28. package/payload/server/public/assets/data-CWQQl_ZG.js +1 -0
  29. package/payload/server/public/assets/graph-CRSLozxc.js +1 -0
  30. package/payload/server/public/assets/{graph-labels-Co03qEv5.js → graph-labels-CQyZQ0u6.js} +1 -1
  31. package/payload/server/public/assets/jsx-runtime-DvanDPKm.css +1 -0
  32. package/payload/server/public/assets/{page-C4E0CWHe.js → page-DOA9eA6p.js} +1 -1
  33. package/payload/server/public/assets/{page-DGLz4ozf.js → page-TARBO-Yr.js} +1 -1
  34. package/payload/server/public/assets/{public-rILg7e8-.js → public-9hQfbymC.js} +1 -1
  35. package/payload/server/public/assets/{useVoiceRecorder-D3Upd7Q3.js → useVoiceRecorder-C3xcgryC.js} +1 -1
  36. package/payload/server/public/data.html +5 -5
  37. package/payload/server/public/graph.html +6 -6
  38. package/payload/server/public/index.html +8 -8
  39. package/payload/server/public/public.html +5 -5
  40. package/payload/server/server.js +91 -118
  41. package/payload/platform/plugins/docs/references/adherence.md +0 -98
  42. package/payload/server/public/assets/data-LYciLZK9.js +0 -1
  43. package/payload/server/public/assets/graph-C-SKAbGX.js +0 -1
  44. package/payload/server/public/assets/jsx-runtime-BcZkJOEw.css +0 -1
  45. /package/payload/server/public/assets/{jsx-runtime-BWYXu1CT.js → jsx-runtime-vPsBTwUp.js} +0 -0
@@ -0,0 +1,3570 @@
1
+ import {
2
+ createNewAdminConversation,
3
+ ensureConversation,
4
+ getSession,
5
+ isMessageUseful,
6
+ persistMessage,
7
+ setConversationAgentSessionId,
8
+ setSessionStoreRef
9
+ } from "./chunk-FBTNBSB4.js";
10
+
11
+ // app/lib/claude-agent/client-pool.ts
12
+ import { query } from "@anthropic-ai/claude-agent-sdk";
13
+
14
+ // app/lib/claude-agent/logging.ts
15
+ import { spawnSync } from "child_process";
16
+ import { resolve } from "path";
17
+ import { platform as osPlatform, devNull } from "os";
18
+ import { readFileSync, readdirSync, mkdirSync, createWriteStream, statSync, unlinkSync, appendFileSync } from "fs";
19
+ import { lookup as dnsLookup } from "dns/promises";
20
+ import { createConnection as netConnect } from "net";
21
+ import { StringDecoder } from "string_decoder";
22
+ var LOG_RETENTION_DAYS = 7;
23
+ var isoTs = () => (/* @__PURE__ */ new Date()).toISOString();
24
+ var BROWSER_TOOL_PREFIXES = [
25
+ "mcp__plugin_playwright_playwright__",
26
+ "mcp__plugin_chrome-devtools-mcp_chrome-devtools__"
27
+ ];
28
+ function isBrowserTool(name) {
29
+ return BROWSER_TOOL_PREFIXES.some((p) => name.startsWith(p));
30
+ }
31
+ var DIAG_HARD_CAP_MS = 5e3;
32
+ var DIAG_DNS_TIMEOUT_MS = 2e3;
33
+ var DIAG_TCP_TIMEOUT_MS = 3e3;
34
+ var DIAG_HTTP_TIMEOUT_MS = 4e3;
35
+ function quoteDiag(value) {
36
+ return JSON.stringify(value);
37
+ }
38
+ function extractUrl(toolName, input) {
39
+ if (input === null || typeof input !== "object") return void 0;
40
+ const obj = input;
41
+ if (toolName === "WebFetch" && typeof obj.url === "string") return obj.url;
42
+ if (isBrowserTool(toolName) && typeof obj.url === "string") return obj.url;
43
+ if (typeof obj.url === "string" && /^https?:\/\//.test(obj.url)) return obj.url;
44
+ return void 0;
45
+ }
46
+ var FULL_REDACT_ENV_VARS = /* @__PURE__ */ new Set(["HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY"]);
47
+ function redactEnvField(name) {
48
+ const value = process.env[name];
49
+ if (!value) return `${name.toLowerCase()}=absent`;
50
+ if (FULL_REDACT_ENV_VARS.has(name)) {
51
+ return `${name.toLowerCase()}=present`;
52
+ }
53
+ const suffix = value.length > 40 ? value.slice(-40) : value;
54
+ return `${name.toLowerCase()}=present suffix=${quoteDiag(suffix)}`;
55
+ }
56
+ async function probeDns(host, family) {
57
+ const label = family === 4 ? "dns_a" : "dns_aaaa";
58
+ const start = Date.now();
59
+ let timer;
60
+ try {
61
+ const result = await Promise.race([
62
+ dnsLookup(host, { family, verbatim: true }),
63
+ new Promise((_, reject) => {
64
+ timer = setTimeout(() => reject(new Error("timeout")), DIAG_DNS_TIMEOUT_MS);
65
+ })
66
+ ]);
67
+ if (timer) clearTimeout(timer);
68
+ const ms = Date.now() - start;
69
+ return `${label}=${result.address} ${label}_ms=${ms}`;
70
+ } catch (err) {
71
+ if (timer) clearTimeout(timer);
72
+ const ms = Date.now() - start;
73
+ const msg = err instanceof Error ? err.message : String(err);
74
+ return `${label}=err ${label}_err=${quoteDiag(msg.slice(0, 60))} ${label}_ms=${ms}`;
75
+ }
76
+ }
77
+ async function probeTcp(host, port) {
78
+ const start = Date.now();
79
+ return new Promise((resolvePromise) => {
80
+ let settled = false;
81
+ const sock = netConnect({ host, port, family: 0 });
82
+ const finish = (result) => {
83
+ if (settled) return;
84
+ settled = true;
85
+ try {
86
+ sock.destroy();
87
+ } catch {
88
+ }
89
+ resolvePromise(result);
90
+ };
91
+ const timer = setTimeout(() => finish(`tcp=timeout tcp_ms=${Date.now() - start}`), DIAG_TCP_TIMEOUT_MS);
92
+ sock.once("connect", () => {
93
+ clearTimeout(timer);
94
+ finish(`tcp=ok tcp_ms=${Date.now() - start}`);
95
+ });
96
+ sock.once("error", (err) => {
97
+ clearTimeout(timer);
98
+ const msg = err instanceof Error ? err.message : String(err);
99
+ finish(`tcp=err tcp_err=${quoteDiag(msg.slice(0, 60))} tcp_ms=${Date.now() - start}`);
100
+ });
101
+ });
102
+ }
103
+ async function probeHttp(url) {
104
+ const start = Date.now();
105
+ const controller = new AbortController();
106
+ const timer = setTimeout(() => controller.abort(), DIAG_HTTP_TIMEOUT_MS);
107
+ try {
108
+ const res = await fetch(url, { method: "HEAD", redirect: "manual", signal: controller.signal });
109
+ clearTimeout(timer);
110
+ return `http_status=${res.status} http_ms=${Date.now() - start}`;
111
+ } catch (err) {
112
+ clearTimeout(timer);
113
+ const msg = err instanceof Error ? err.message : String(err);
114
+ return `http_status=err http_err=${quoteDiag(msg.slice(0, 60))} http_ms=${Date.now() - start}`;
115
+ }
116
+ }
117
+ async function runFailureDiagnostic(toolName, toolInput) {
118
+ const inputKeys = toolInput !== null && typeof toolInput === "object" ? Object.keys(toolInput).join(",") : "";
119
+ const envFields = [
120
+ redactEnvField("HTTPS_PROXY"),
121
+ redactEnvField("HTTP_PROXY"),
122
+ redactEnvField("NO_PROXY"),
123
+ redactEnvField("NODE_OPTIONS")
124
+ ].join(" ");
125
+ const url = extractUrl(toolName, toolInput);
126
+ if (!url) {
127
+ return `diag_url=none input_keys=[${inputKeys}] ${envFields}`;
128
+ }
129
+ let host;
130
+ let port;
131
+ try {
132
+ const parsed = new URL(url);
133
+ host = parsed.hostname;
134
+ port = parsed.port ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80;
135
+ } catch {
136
+ return `diag_url=unparseable input_keys=[${inputKeys}] ${envFields}`;
137
+ }
138
+ const probes = Promise.allSettled([
139
+ probeDns(host, 4),
140
+ probeDns(host, 6),
141
+ probeTcp(host, port),
142
+ probeHttp(url)
143
+ ]);
144
+ let capTimer;
145
+ const capped = await Promise.race([
146
+ probes,
147
+ new Promise((resolvePromise) => {
148
+ capTimer = setTimeout(() => resolvePromise("__diag_timeout__"), DIAG_HARD_CAP_MS);
149
+ })
150
+ ]);
151
+ if (capTimer) clearTimeout(capTimer);
152
+ if (capped === "__diag_timeout__") {
153
+ return `diag_host=${host} diag_port=${port} diag_timeout=true input_keys=[${inputKeys}] ${envFields}`;
154
+ }
155
+ const fields = capped.map((r) => r.status === "fulfilled" ? r.value : `probe_err=${quoteDiag(String(r.reason).slice(0, 40))}`).join(" ");
156
+ return `diag_host=${host} diag_port=${port} ${fields} input_keys=[${inputKeys}] ${envFields}`;
157
+ }
158
+ function agentLogStream(name, accountDir, sessionKey) {
159
+ if (!sessionKey) {
160
+ throw new Error(`agentLogStream: sessionKey is required (name=${name}) \u2014 chat-route entry binds it for every channel`);
161
+ }
162
+ const filenameBytes = Buffer.byteLength(name) + Buffer.byteLength(sessionKey) + 5;
163
+ if (filenameBytes > 240) {
164
+ logTeeLog(
165
+ `[log-tee] writer-bind-rejected reason=key-too-long sessionKey-bytes=${Buffer.byteLength(sessionKey)} name=${name} filename-bytes=${filenameBytes}`
166
+ );
167
+ return openNoOpStream(sessionKey, name);
168
+ }
169
+ const logDir = resolve(accountDir, "logs");
170
+ mkdirSync(logDir, { recursive: true });
171
+ purgeOldLogs(logDir, `${name}-`);
172
+ const logPath = resolve(logDir, `${name}-${sessionKey}.log`);
173
+ const stream = createWriteStream(logPath, { flags: "a" });
174
+ stream.once("error", (err) => {
175
+ logTeeLog(
176
+ `[log-tee] writer-bind-failed sessionKey=${sessionKey.slice(0, 8)} name=${name} errno=${err.code ?? "unknown"} path=${JSON.stringify(logPath)}`
177
+ );
178
+ openStreamLogs.delete(stream);
179
+ });
180
+ registerStreamLog(stream, { path: logPath, sessionKey, name });
181
+ return stream;
182
+ }
183
+ function openNoOpStream(sessionKey, name) {
184
+ const stream = createWriteStream(devNull, { flags: "a" });
185
+ stream.once("error", (err) => {
186
+ logTeeLog(`[log-tee] devnull-error sessionKey=${sessionKey.slice(0, 8)} name=${name} errno=${err.code ?? "unknown"}`);
187
+ openStreamLogs.delete(stream);
188
+ });
189
+ registerStreamLog(stream, { path: devNull, sessionKey, name });
190
+ return stream;
191
+ }
192
+ var openStreamLogs = /* @__PURE__ */ new Map();
193
+ function registerStreamLog(stream, entry) {
194
+ openStreamLogs.set(stream, entry);
195
+ stream.once("close", () => {
196
+ openStreamLogs.delete(stream);
197
+ logTeeLog(`[log-tee] deregister sessionKey=${entry.sessionKey.slice(0, 8)} path=${JSON.stringify(entry.path)}`);
198
+ });
199
+ logTeeLog(`[log-tee] file-created sessionKey=${entry.sessionKey.slice(0, 8)} path=${JSON.stringify(entry.path)} first-token-at=${isoTs()}`);
200
+ }
201
+ var LOG_TEE_TAG_RE = /^\[[a-zA-Z][a-zA-Z0-9:_\-]*\]/;
202
+ var originalConsoleError = null;
203
+ var originalConsoleLog = null;
204
+ var logTeeInstalled = false;
205
+ var logTeeCycleTimer = null;
206
+ var logTeeAdherenceTimer = null;
207
+ var logTeeLinesEmitted = 0;
208
+ var logTeeLinesRouted = 0;
209
+ var logTeeBytesRouted = 0;
210
+ var logTeeFailCount = 0;
211
+ function logTeeLog(line) {
212
+ (originalConsoleError ?? console.error.bind(console))(line);
213
+ }
214
+ function appendToActiveStreams(line) {
215
+ if (openStreamLogs.size === 0) return;
216
+ const ts = isoTs();
217
+ const teeLine = `[${ts}] ${line.replace(/\n$/, "")}
218
+ `;
219
+ let routed = 0;
220
+ for (const entry of openStreamLogs.values()) {
221
+ try {
222
+ appendFileSync(entry.path, teeLine);
223
+ routed++;
224
+ logTeeBytesRouted += teeLine.length;
225
+ } catch (err) {
226
+ logTeeFailCount++;
227
+ const msg = err instanceof Error ? err.message : String(err);
228
+ logTeeLog(`[log-tee] FAIL emit reason=${JSON.stringify(msg.slice(0, 80))} path=${JSON.stringify(entry.path)}`);
229
+ }
230
+ }
231
+ if (routed > 0) {
232
+ logTeeLinesRouted += routed;
233
+ logTeeLinesEmitted++;
234
+ }
235
+ }
236
+ function installLogTee() {
237
+ if (logTeeInstalled) return;
238
+ logTeeInstalled = true;
239
+ originalConsoleError = console.error.bind(console);
240
+ originalConsoleLog = console.log.bind(console);
241
+ const wrap = (orig) => {
242
+ return (...args) => {
243
+ orig(...args);
244
+ const rendered = args.map((a) => typeof a === "string" ? a : safeStringify(a)).join(" ");
245
+ if (LOG_TEE_TAG_RE.test(rendered)) {
246
+ appendToActiveStreams(rendered);
247
+ }
248
+ };
249
+ };
250
+ console.error = wrap(originalConsoleError);
251
+ console.log = wrap(originalConsoleLog);
252
+ logTeeCycleTimer = setInterval(() => {
253
+ const active = openStreamLogs.size;
254
+ logTeeLog(
255
+ `[log-tee] cycle activeSessions=${active} linesEmitted=${logTeeLinesEmitted} linesRouted=${logTeeLinesRouted} bytesRouted=${logTeeBytesRouted} failCount=${logTeeFailCount}`
256
+ );
257
+ }, 3e4);
258
+ logTeeCycleTimer.unref?.();
259
+ logTeeAdherenceTimer = setInterval(() => {
260
+ runAdherenceCheck();
261
+ }, 60 * 60 * 1e3);
262
+ logTeeAdherenceTimer.unref?.();
263
+ logTeeLog(`[log-tee] installed pid=${process.pid}`);
264
+ }
265
+ function safeStringify(value) {
266
+ try {
267
+ if (value instanceof Error) return value.stack ?? value.message;
268
+ return JSON.stringify(value);
269
+ } catch {
270
+ return String(value);
271
+ }
272
+ }
273
+ function emitMissingOnResolve(sessionKey, surface, reason) {
274
+ logTeeLog(`[log-tee] missing-on-resolve sessionKey=${sessionKey.slice(0, 8)} surface=${surface} reason=${JSON.stringify(reason.slice(0, 120))}`);
275
+ }
276
+ function runAdherenceCheck() {
277
+ const start = Date.now();
278
+ let sessions = 0;
279
+ let misses = 0;
280
+ try {
281
+ const accountsRoot = resolveAccountsRoot();
282
+ if (!accountsRoot) {
283
+ logTeeLog(`[log-tee] adherence-check window=24h sessions=0 misses=0 ts=${isoTs()} note=accounts-root-unresolved`);
284
+ return;
285
+ }
286
+ const sessionKeysOnDisk = collectSessionKeysFromOpenStreams();
287
+ sessions = sessionKeysOnDisk.size;
288
+ for (const key of sessionKeysOnDisk) {
289
+ if (!resolveStreamLogPath(accountsRoot, key)) {
290
+ misses++;
291
+ emitMissingOnResolve(key, "adherence-check", "file-not-on-disk");
292
+ }
293
+ }
294
+ } catch (err) {
295
+ const msg = err instanceof Error ? err.message : String(err);
296
+ logTeeLog(`[log-tee] adherence-check-err reason=${JSON.stringify(msg.slice(0, 120))}`);
297
+ return;
298
+ }
299
+ const ms = Date.now() - start;
300
+ logTeeLog(`[log-tee] adherence-check window=24h sessions=${sessions} misses=${misses} ms=${ms} ts=${isoTs()}`);
301
+ }
302
+ function collectSessionKeysFromOpenStreams() {
303
+ const keys = /* @__PURE__ */ new Set();
304
+ for (const entry of openStreamLogs.values()) {
305
+ keys.add(entry.sessionKey);
306
+ }
307
+ return keys;
308
+ }
309
+ function resolveAccountsRoot() {
310
+ const envRoot = process.env.ADHERENCE_INSTALL_DIR;
311
+ if (envRoot) return resolve(envRoot, "data", "accounts");
312
+ const platformRoot2 = process.env.MAXY_PLATFORM_ROOT;
313
+ if (platformRoot2) return resolve(platformRoot2, "..", "data", "accounts");
314
+ return resolve(process.cwd(), "..", "..", "data", "accounts");
315
+ }
316
+ function resolveStreamLogPath(accountsRoot, sessionKey) {
317
+ let entries;
318
+ try {
319
+ entries = readdirSync(accountsRoot);
320
+ } catch {
321
+ return null;
322
+ }
323
+ for (const acct of entries) {
324
+ const logsDir = resolve(accountsRoot, acct, "logs");
325
+ let logFiles;
326
+ try {
327
+ logFiles = readdirSync(logsDir);
328
+ } catch {
329
+ continue;
330
+ }
331
+ const wanted = `claude-agent-stream-${sessionKey}.log`;
332
+ if (logFiles.includes(wanted)) return resolve(logsDir, wanted);
333
+ }
334
+ return null;
335
+ }
336
+ if (process.env.NODE_ENV !== "test") {
337
+ installLogTee();
338
+ }
339
+ function sigtermFlushStreamLogs(reason, source) {
340
+ const ts = (/* @__PURE__ */ new Date()).toISOString();
341
+ for (const entry of openStreamLogs.values()) {
342
+ const line = `[${ts}] [server-sigterm] reason=${reason} sessionKey=${entry.sessionKey} name=${entry.name} source=${source}
343
+ `;
344
+ try {
345
+ appendFileSync(entry.path, line);
346
+ } catch (err) {
347
+ const msg = err instanceof Error ? err.message : String(err);
348
+ console.error(`[server-sigterm-flush-err] path=${entry.path} reason=${msg}`);
349
+ }
350
+ }
351
+ }
352
+ function purgeOldLogs(logDir, prefix) {
353
+ const cutoff = Date.now() - LOG_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
354
+ let entries;
355
+ try {
356
+ entries = readdirSync(logDir);
357
+ } catch (err) {
358
+ const msg = err instanceof Error ? err.message : String(err);
359
+ console.error(`[log-purge-err] readdir dir=${logDir} prefix=${prefix} reason=${msg}`);
360
+ return;
361
+ }
362
+ for (const file of entries) {
363
+ if (!file.startsWith(prefix)) continue;
364
+ const filePath = resolve(logDir, file);
365
+ try {
366
+ if (statSync(filePath).mtimeMs < cutoff) unlinkSync(filePath);
367
+ } catch (err) {
368
+ const msg = err instanceof Error ? err.message : String(err);
369
+ console.error(`[log-purge-err] file=${file} reason=${msg}`);
370
+ }
371
+ }
372
+ }
373
+
374
+ // app/lib/claude-agent/session-store.ts
375
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from "crypto";
376
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync2, mkdirSync as mkdirSync4 } from "fs";
377
+ import { dirname as dirname2 } from "path";
378
+
379
+ // app/lib/claude-agent/stream-log-writer.ts
380
+ import { createWriteStream as createWriteStream2, mkdirSync as mkdirSync3 } from "fs";
381
+ import { devNull as devNull2 } from "os";
382
+ import { resolve as resolve7 } from "path";
383
+ import { appendFileSync as appendFileSyncForAdminTelemetry } from "fs";
384
+ import { dirname } from "path";
385
+
386
+ // app/lib/claude-agent/spawn-env.ts
387
+ import { resolve as resolve6 } from "path";
388
+ import { existsSync as existsSync5, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
389
+ import { execFileSync } from "child_process";
390
+
391
+ // app/lib/claude-agent/account.ts
392
+ import { resolve as resolve3 } from "path";
393
+ import { readFileSync as readFileSync4, readdirSync as readdirSync2, existsSync as existsSync3, statSync as statSync2 } from "fs";
394
+
395
+ // ../lib/brand-templating/src/index.ts
396
+ import { join } from "path";
397
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
398
+ var PLACEHOLDER = "{{productName}}";
399
+ var cachedProductName = null;
400
+ function brandJsonPath() {
401
+ const platformRoot2 = process.env.MAXY_PLATFORM_ROOT;
402
+ if (!platformRoot2) {
403
+ throw new Error(
404
+ "[skill-loader] MAXY_PLATFORM_ROOT not set \u2014 cannot resolve brand.json"
405
+ );
406
+ }
407
+ return join(platformRoot2, "config", "brand.json");
408
+ }
409
+ function getBrandProductName() {
410
+ if (cachedProductName !== null) return cachedProductName;
411
+ const path = brandJsonPath();
412
+ if (!existsSync(path)) {
413
+ throw new Error(`[skill-loader] brand.json missing at ${path}`);
414
+ }
415
+ let parsed;
416
+ try {
417
+ parsed = JSON.parse(readFileSync2(path, "utf-8"));
418
+ } catch (err) {
419
+ throw new Error(
420
+ `[skill-loader] brand.json unreadable at ${path}: ${err instanceof Error ? err.message : String(err)}`
421
+ );
422
+ }
423
+ const value = parsed.productName;
424
+ if (typeof value !== "string" || value.trim() === "") {
425
+ throw new Error(
426
+ `[skill-loader] brand.json at ${path} has missing or empty productName`
427
+ );
428
+ }
429
+ cachedProductName = value;
430
+ return value;
431
+ }
432
+ function substituteBrandPlaceholders(content, sourcePath) {
433
+ if (!content.includes(PLACEHOLDER)) return content;
434
+ let productName;
435
+ try {
436
+ productName = getBrandProductName();
437
+ } catch (err) {
438
+ console.error(
439
+ `[skill-loader] ERROR: brand.json missing \u2014 cannot resolve productName for skill ${sourcePath}`
440
+ );
441
+ throw err;
442
+ }
443
+ const occurrences = content.split(PLACEHOLDER).length - 1;
444
+ const substituted = content.split(PLACEHOLDER).join(productName);
445
+ console.log(
446
+ `[skill-loader] brand-substituted productName=${productName} skill=${sourcePath} occurrences=${occurrences}`
447
+ );
448
+ return substituted;
449
+ }
450
+
451
+ // app/lib/paths.ts
452
+ import { homedir } from "os";
453
+ import { resolve as resolve2, join as join2 } from "path";
454
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
455
+ var configDirName = ".maxy";
456
+ var commercialMode = false;
457
+ var vncDisplayNum = 99;
458
+ var rfbPortNum = 5900;
459
+ var websockifyPortNum = 6080;
460
+ var cdpPortNum = 9222;
461
+ var portSource = "dev-default";
462
+ var platformRoot = process.env.MAXY_PLATFORM_ROOT;
463
+ if (platformRoot) {
464
+ const brandPath = join2(platformRoot, "config", "brand.json");
465
+ if (existsSync2(brandPath)) {
466
+ let brand;
467
+ try {
468
+ brand = JSON.parse(readFileSync3(brandPath, "utf-8"));
469
+ } catch (err) {
470
+ const detail = (err instanceof Error ? err.message : String(err)).slice(0, 160);
471
+ console.error(`[paths] error reason=brand-config-missing path=${brandPath} detail="parse failed: ${detail.replace(/"/g, "'")}"`);
472
+ throw err;
473
+ }
474
+ if (typeof brand.configDir === "string") configDirName = brand.configDir;
475
+ if (brand.commercialMode === true) commercialMode = true;
476
+ if (typeof brand.vncDisplay === "number") vncDisplayNum = brand.vncDisplay;
477
+ const brandLabel = configDirName.replace(/^\./, "");
478
+ const required = [
479
+ ["rfbPort", brand.rfbPort],
480
+ ["websockifyPort", brand.websockifyPort],
481
+ ["cdpPort", brand.cdpPort]
482
+ ];
483
+ for (const [field, value] of required) {
484
+ if (typeof value !== "number") {
485
+ const keys = Object.keys(brand).join(",");
486
+ console.error(`[paths] error reason=cdp-port-unresolved brand=${brandLabel} path=${brandPath} field=${field} json_keys=${keys}`);
487
+ throw new Error(`brand.json at ${brandPath} missing required field: ${field}`);
488
+ }
489
+ }
490
+ rfbPortNum = brand.rfbPort;
491
+ websockifyPortNum = brand.websockifyPort;
492
+ cdpPortNum = brand.cdpPort;
493
+ portSource = "brand.json";
494
+ }
495
+ }
496
+ var MAXY_DIR = resolve2(homedir(), configDirName);
497
+ var BRAND_NAME = configDirName.replace(/^\./, "");
498
+ var COMMERCIAL_MODE = commercialMode;
499
+ var VNC_DISPLAY = `:${vncDisplayNum}`;
500
+ var RFB_PORT = rfbPortNum;
501
+ var WEBSOCKIFY_PORT = websockifyPortNum;
502
+ var CDP_PORT = cdpPortNum;
503
+ console.log(
504
+ `[paths] brand=${configDirName.replace(/^\./, "")} vncDisplay=${vncDisplayNum} rfbPort=${RFB_PORT} websockifyPort=${WEBSOCKIFY_PORT} cdpPort=${CDP_PORT} source=${portSource}`
505
+ );
506
+ var CHROMIUM_PROFILE_DIR = resolve2(homedir(), configDirName, "chromium-profile");
507
+ var PLATFORM_ROOT = process.env.MAXY_PLATFORM_ROOT ?? resolve2(process.cwd(), "..");
508
+ var USERS_FILE = resolve2(MAXY_DIR, "users.json");
509
+ var LOG_DIR = resolve2(MAXY_DIR, "logs");
510
+ var BIN_DIR = resolve2(MAXY_DIR, "bin");
511
+ var REMOTE_PASSWORD_FILE = resolve2(MAXY_DIR, ".remote-password");
512
+ var REMOTE_SESSION_SECRET_FILE = resolve2(MAXY_DIR, "credentials", "remote-session-secret");
513
+ var ADMIN_SESSION_SECRET_FILE = resolve2(MAXY_DIR, "credentials", "admin-session-secret");
514
+ var TELEGRAM_WEBHOOK_SECRET_FILE = resolve2(MAXY_DIR, ".telegram-webhook-secret");
515
+ var TELEGRAM_ADMIN_WEBHOOK_SECRET_FILE = resolve2(MAXY_DIR, ".telegram-admin-webhook-secret");
516
+ var CLAUDE_CREDENTIALS_FILE = resolve2(MAXY_DIR, ".claude", ".credentials.json");
517
+
518
+ // app/lib/claude-agent/account.ts
519
+ var PLATFORM_ROOT2 = process.env.MAXY_PLATFORM_ROOT ?? resolve3(process.cwd(), "..");
520
+ var ACCOUNTS_DIR = resolve3(PLATFORM_ROOT2, "..", "data/accounts");
521
+ if (!existsSync3(PLATFORM_ROOT2)) {
522
+ throw new Error(
523
+ `PLATFORM_ROOT does not exist: ${PLATFORM_ROOT2}
524
+ Set the MAXY_PLATFORM_ROOT environment variable to the absolute path of the platform directory.`
525
+ );
526
+ }
527
+ function hasStubAccountDir() {
528
+ if (!existsSync3(ACCOUNTS_DIR)) return { stub: false, dirs: [] };
529
+ const stubs = [];
530
+ const entries = readdirSync2(ACCOUNTS_DIR, { withFileTypes: true });
531
+ for (const entry of entries) {
532
+ if (!entry.isDirectory()) continue;
533
+ if (entry.name.startsWith(".")) continue;
534
+ const configPath = resolve3(ACCOUNTS_DIR, entry.name, "account.json");
535
+ if (!existsSync3(configPath)) stubs.push(entry.name);
536
+ }
537
+ return { stub: stubs.length > 0, dirs: stubs };
538
+ }
539
+ function resolveAccount() {
540
+ if (!existsSync3(ACCOUNTS_DIR)) return null;
541
+ let usersJsonUserId = null;
542
+ if (existsSync3(USERS_FILE)) {
543
+ try {
544
+ const raw = readFileSync4(USERS_FILE, "utf-8").trim();
545
+ if (raw) {
546
+ const users = JSON.parse(raw);
547
+ if (users.length > 0) {
548
+ usersJsonUserId = users[0].userId;
549
+ }
550
+ }
551
+ } catch {
552
+ }
553
+ }
554
+ const entries = readdirSync2(ACCOUNTS_DIR, { withFileTypes: true });
555
+ let fallback = null;
556
+ for (const entry of entries) {
557
+ if (!entry.isDirectory()) continue;
558
+ const configPath = resolve3(ACCOUNTS_DIR, entry.name, "account.json");
559
+ if (!existsSync3(configPath)) continue;
560
+ const raw = readFileSync4(configPath, "utf-8");
561
+ let config;
562
+ try {
563
+ config = JSON.parse(raw);
564
+ } catch {
565
+ console.error(`[maxy] account.json is corrupt at ${configPath} \u2014 skipping`);
566
+ continue;
567
+ }
568
+ if (!config.adminModel || !config.publicModel) {
569
+ throw new Error(
570
+ `[maxy] account.json at ${configPath} is missing required model fields (adminModel / publicModel). Update account.json with valid model identifiers.`
571
+ );
572
+ }
573
+ const result = {
574
+ accountId: config.accountId,
575
+ accountDir: resolve3(ACCOUNTS_DIR, entry.name),
576
+ config
577
+ };
578
+ if (usersJsonUserId && config.admins?.some((a) => a.userId === usersJsonUserId)) {
579
+ return result;
580
+ }
581
+ if (!fallback) {
582
+ fallback = result;
583
+ }
584
+ }
585
+ if (usersJsonUserId && fallback) {
586
+ console.warn(
587
+ `[maxy] resolveAccount: no account matches users.json userId ${usersJsonUserId} \u2014 falling back to ${fallback.accountId}`
588
+ );
589
+ }
590
+ return fallback;
591
+ }
592
+ function readAgentFile(accountDir, agentName, filename) {
593
+ const filePath = resolve3(accountDir, "agents", agentName, filename);
594
+ if (!existsSync3(filePath)) return null;
595
+ const raw = readFileSync4(filePath, "utf-8");
596
+ if (filename.endsWith(".md")) {
597
+ return substituteBrandPlaceholders(raw, filePath);
598
+ }
599
+ return raw;
600
+ }
601
+ function readIdentity(accountDir, agentName) {
602
+ return readAgentFile(accountDir, agentName, "IDENTITY.md");
603
+ }
604
+ var RESERVED_SLUGS = /* @__PURE__ */ new Set(["admin", "api", "assets", "brand", "bot", "privacy"]);
605
+ var SLUG_PATTERN = /^[a-z][a-z0-9-]{2,49}$/;
606
+ function validateAgentSlug(slug) {
607
+ if (!SLUG_PATTERN.test(slug)) return false;
608
+ if (RESERVED_SLUGS.has(slug)) return false;
609
+ return true;
610
+ }
611
+ function resolveDefaultAgentSlug(accountDir) {
612
+ const configPath = resolve3(accountDir, "account.json");
613
+ if (!existsSync3(configPath)) {
614
+ console.error("[agent-resolve] account.json not found \u2014 cannot resolve defaultAgent");
615
+ return null;
616
+ }
617
+ let config;
618
+ try {
619
+ config = JSON.parse(readFileSync4(configPath, "utf-8"));
620
+ } catch (err) {
621
+ console.error("[agent-resolve] failed to read account.json:", err);
622
+ return null;
623
+ }
624
+ if (!config.defaultAgent) {
625
+ console.error("[agent-resolve] defaultAgent not configured in account.json \u2014 set it via the connect-whatsapp skill");
626
+ return null;
627
+ }
628
+ const agentConfigPath = resolve3(accountDir, "agents", config.defaultAgent, "config.json");
629
+ if (!existsSync3(agentConfigPath)) {
630
+ console.error(`[agent-resolve] defaultAgent="${config.defaultAgent}" has no config.json at ${agentConfigPath}`);
631
+ return null;
632
+ }
633
+ return config.defaultAgent;
634
+ }
635
+ function estimateTokens(text) {
636
+ return Math.ceil(text.length / 4);
637
+ }
638
+ function resolveAgentConfig(accountDir, agentName) {
639
+ let model = null;
640
+ let plugins = null;
641
+ let status = null;
642
+ let displayName = null;
643
+ let image = null;
644
+ let imageShape = null;
645
+ let showAgentName = false;
646
+ let liveMemory = false;
647
+ let knowledgeKeywords = null;
648
+ let accessMode = "open";
649
+ const MAX_KNOWLEDGE_KEYWORDS = 5;
650
+ const configRaw = readAgentFile(accountDir, agentName, "config.json");
651
+ if (configRaw) {
652
+ let parsed;
653
+ try {
654
+ parsed = JSON.parse(configRaw);
655
+ } catch {
656
+ console.warn(`[agent-config] ${agentName}/config.json: invalid JSON \u2014 using defaults`);
657
+ parsed = {};
658
+ }
659
+ model = typeof parsed.model === "string" ? parsed.model : null;
660
+ plugins = Array.isArray(parsed.plugins) ? parsed.plugins : null;
661
+ status = typeof parsed.status === "string" ? parsed.status : null;
662
+ displayName = typeof parsed.displayName === "string" ? parsed.displayName : null;
663
+ image = typeof parsed.image === "string" ? parsed.image : null;
664
+ if (typeof parsed.imageShape === "string" && ["circle", "rounded"].includes(parsed.imageShape)) {
665
+ imageShape = parsed.imageShape;
666
+ }
667
+ if (parsed.showAgentName === true) {
668
+ showAgentName = true;
669
+ } else if (parsed.showAgentName === "none") {
670
+ showAgentName = "none";
671
+ }
672
+ if (image || imageShape || showAgentName) {
673
+ console.log(`[agent-config] ${agentName}: image=${image || "(none)"} imageShape=${imageShape || "(none)"} showAgentName=${showAgentName}`);
674
+ }
675
+ if (typeof parsed.accessMode === "string" && ["gated", "paid"].includes(parsed.accessMode)) {
676
+ accessMode = parsed.accessMode;
677
+ }
678
+ if (typeof parsed.liveMemory === "boolean") {
679
+ liveMemory = parsed.liveMemory;
680
+ } else if (typeof parsed.liveMemory === "string") {
681
+ const lower = parsed.liveMemory.toLowerCase();
682
+ if (lower === "true") {
683
+ liveMemory = true;
684
+ console.warn(`[agent-config] ${agentName}: liveMemory is string "true" \u2014 coercing to boolean. Fix the config to use a boolean value.`);
685
+ } else if (lower === "false") {
686
+ liveMemory = false;
687
+ console.warn(`[agent-config] ${agentName}: liveMemory is string "false" \u2014 coercing to boolean. Fix the config to use a boolean value.`);
688
+ } else {
689
+ throw new Error(`[agent-config] ${agentName}: liveMemory has invalid string value "${parsed.liveMemory}" \u2014 expected boolean or "true"/"false"`);
690
+ }
691
+ } else if (parsed.liveMemory !== void 0 && parsed.liveMemory !== null) {
692
+ throw new Error(`[agent-config] ${agentName}: liveMemory has invalid type ${typeof parsed.liveMemory} \u2014 expected boolean or "true"/"false"`);
693
+ }
694
+ if (Array.isArray(parsed.knowledgeKeywords) && parsed.knowledgeKeywords.length > 0) {
695
+ const filtered = parsed.knowledgeKeywords.filter((k) => typeof k === "string" && k.trim()).map((k) => k.replace(/,/g, "").trim().toLowerCase()).filter(Boolean);
696
+ if (filtered.length > MAX_KNOWLEDGE_KEYWORDS) {
697
+ console.warn(`[agent-config] ${agentName}: knowledgeKeywords has ${filtered.length} entries \u2014 capping at ${MAX_KNOWLEDGE_KEYWORDS}`);
698
+ }
699
+ knowledgeKeywords = filtered.length > 0 ? filtered.slice(0, MAX_KNOWLEDGE_KEYWORDS) : null;
700
+ }
701
+ }
702
+ let knowledge = null;
703
+ let knowledgeBaked = false;
704
+ const agentDir = resolve3(accountDir, "agents", agentName);
705
+ const knowledgePath = resolve3(agentDir, "KNOWLEDGE.md");
706
+ const summaryPath = resolve3(agentDir, "KNOWLEDGE-SUMMARY.md");
707
+ const hasKnowledge = existsSync3(knowledgePath);
708
+ const hasSummary = existsSync3(summaryPath);
709
+ if (hasKnowledge && hasSummary) {
710
+ const knowledgeMtime = statSync2(knowledgePath).mtimeMs;
711
+ const summaryMtime = statSync2(summaryPath).mtimeMs;
712
+ if (summaryMtime >= knowledgeMtime) {
713
+ knowledge = readFileSync4(summaryPath, "utf-8");
714
+ } else {
715
+ console.warn(`[agent-config] ${agentName}: KNOWLEDGE-SUMMARY.md is stale (KNOWLEDGE.md is newer) \u2014 using full knowledge`);
716
+ knowledge = readFileSync4(knowledgePath, "utf-8");
717
+ }
718
+ knowledgeBaked = true;
719
+ } else if (hasKnowledge) {
720
+ knowledge = readFileSync4(knowledgePath, "utf-8");
721
+ knowledgeBaked = true;
722
+ }
723
+ let budget = null;
724
+ const identityRaw = readAgentFile(accountDir, agentName, "IDENTITY.md");
725
+ const soulRaw = readAgentFile(accountDir, agentName, "SOUL.md");
726
+ if (identityRaw || soulRaw || knowledge) {
727
+ const identityTokens = identityRaw ? estimateTokens(identityRaw) : 0;
728
+ const soulTokens = soulRaw ? estimateTokens(soulRaw) : 0;
729
+ const knowledgeTokens = knowledge ? estimateTokens(knowledge) : 0;
730
+ budget = {
731
+ identity: identityTokens,
732
+ soul: soulTokens,
733
+ knowledge: knowledgeTokens,
734
+ plugins: 0,
735
+ total: identityTokens + soulTokens + knowledgeTokens
736
+ };
737
+ }
738
+ return { model, plugins, status, displayName, image, imageShape, showAgentName, knowledge, knowledgeBaked, liveMemory, knowledgeKeywords, budget, accessMode };
739
+ }
740
+ function getDefaultAccountId() {
741
+ return resolveAccount()?.accountId ?? null;
742
+ }
743
+ function resolveUserAccounts(userId) {
744
+ if (!existsSync3(ACCOUNTS_DIR)) return [];
745
+ const results = [];
746
+ const entries = readdirSync2(ACCOUNTS_DIR, { withFileTypes: true });
747
+ for (const entry of entries) {
748
+ if (!entry.isDirectory()) continue;
749
+ const configPath = resolve3(ACCOUNTS_DIR, entry.name, "account.json");
750
+ if (!existsSync3(configPath)) continue;
751
+ let config;
752
+ try {
753
+ config = JSON.parse(readFileSync4(configPath, "utf-8"));
754
+ } catch {
755
+ console.error(`[session] account.json corrupt at ${configPath} \u2014 skipping`);
756
+ continue;
757
+ }
758
+ const adminEntry = config.admins?.find((a) => a.userId === userId);
759
+ if (adminEntry) {
760
+ results.push({
761
+ accountId: config.accountId,
762
+ accountDir: resolve3(ACCOUNTS_DIR, entry.name),
763
+ config,
764
+ role: adminEntry.role
765
+ });
766
+ }
767
+ }
768
+ return results;
769
+ }
770
+
771
+ // app/lib/claude-agent/neo4j-uri.ts
772
+ import { resolve as resolve4 } from "path";
773
+ import { readFileSync as readFileSync5 } from "fs";
774
+ import { createConnection as netConnect2 } from "net";
775
+ var PLATFORM_ROOT3 = process.env.MAXY_PLATFORM_ROOT ?? resolve4(process.cwd(), "..");
776
+ var cachedBrandHostname = null;
777
+ function readBrandHostname() {
778
+ if (cachedBrandHostname !== null) return cachedBrandHostname;
779
+ try {
780
+ const brandPath = resolve4(PLATFORM_ROOT3, "config", "brand.json");
781
+ const parsed = JSON.parse(readFileSync5(brandPath, "utf-8"));
782
+ cachedBrandHostname = typeof parsed.hostname === "string" && parsed.hostname.length > 0 ? parsed.hostname : "maxy";
783
+ } catch {
784
+ cachedBrandHostname = "maxy";
785
+ }
786
+ return cachedBrandHostname;
787
+ }
788
+ var DEFAULT_NEO4J_PORT = 7687;
789
+ function parseBrandNeo4jPort(rawJson) {
790
+ if (rawJson === null) return DEFAULT_NEO4J_PORT;
791
+ try {
792
+ const parsed = JSON.parse(rawJson);
793
+ const n = parsed.neo4jPort;
794
+ return typeof n === "number" && Number.isInteger(n) && n > 0 && n <= 65535 ? n : DEFAULT_NEO4J_PORT;
795
+ } catch {
796
+ return DEFAULT_NEO4J_PORT;
797
+ }
798
+ }
799
+ var cachedBrandNeo4jPort = null;
800
+ function readBrandNeo4jPort() {
801
+ if (cachedBrandNeo4jPort !== null) return cachedBrandNeo4jPort;
802
+ try {
803
+ const raw = readFileSync5(resolve4(PLATFORM_ROOT3, "config", "brand.json"), "utf-8");
804
+ cachedBrandNeo4jPort = parseBrandNeo4jPort(raw);
805
+ } catch (err) {
806
+ const code = err.code;
807
+ if (code !== "ENOENT") throw err;
808
+ cachedBrandNeo4jPort = DEFAULT_NEO4J_PORT;
809
+ }
810
+ return cachedBrandNeo4jPort;
811
+ }
812
+ var NEO4J_URI_SCHEMES = /* @__PURE__ */ new Set([
813
+ "bolt:",
814
+ "bolt+s:",
815
+ "bolt+ssc:",
816
+ "neo4j:",
817
+ "neo4j+s:",
818
+ "neo4j+ssc:"
819
+ ]);
820
+ function extractNeo4jPort(uri) {
821
+ try {
822
+ const url = new URL(uri);
823
+ if (!NEO4J_URI_SCHEMES.has(url.protocol)) return null;
824
+ if (url.port === "") return null;
825
+ const n = Number(url.port);
826
+ return Number.isInteger(n) && n > 0 && n <= 65535 ? n : null;
827
+ } catch {
828
+ return null;
829
+ }
830
+ }
831
+ function validateNeo4jUri(uri, brandPort, envFilePath) {
832
+ if (!uri) {
833
+ throw new Error(
834
+ "NEO4J_URI unset \u2014 refusing to default to bolt://localhost:7687. Set NEO4J_URI in the gateway's env (systemd EnvironmentFile or shell .env)."
835
+ );
836
+ }
837
+ const safeUri = JSON.stringify(uri);
838
+ const uriPort = extractNeo4jPort(uri);
839
+ if (uriPort === null) {
840
+ throw new Error(
841
+ `NEO4J_URI=${safeUri} has no explicit port on a recognised Neo4j scheme; brand.json declares neo4jPort :${brandPort}. Edit ${envFilePath} NEO4J_URI or correct BRAND.neo4jPort.`
842
+ );
843
+ }
844
+ if (uriPort !== brandPort) {
845
+ throw new Error(
846
+ `NEO4J_URI=${safeUri} port :${uriPort} disagrees with brand.json neo4jPort :${brandPort}. Edit ${envFilePath} NEO4J_URI or correct BRAND.neo4jPort.`
847
+ );
848
+ }
849
+ return uri;
850
+ }
851
+ function decideNeo4jUri(uri, listeningPorts, brandPort, envFilePath) {
852
+ if (!uri) {
853
+ throw new Error(
854
+ "NEO4J_URI unset \u2014 refusing to default to bolt://localhost:7687. Set NEO4J_URI in the gateway's env (systemd EnvironmentFile or shell .env)."
855
+ );
856
+ }
857
+ const count = listeningPorts.size;
858
+ if (count === 0) {
859
+ const probedCsv = Array.from(listeningPorts).join(",");
860
+ throw new Error(
861
+ `NEO4J_URI=${JSON.stringify(uri)} \u2014 no Neo4j listening on [${probedCsv}]; start neo4j.service or neo4j-${readBrandHostname()}.service, or edit ${envFilePath} to name a port a Neo4j is bound to.`
862
+ );
863
+ }
864
+ if (count === 1) {
865
+ const uriPort = extractNeo4jPort(uri);
866
+ if (uriPort === null) {
867
+ throw new Error(
868
+ `NEO4J_URI=${JSON.stringify(uri)} has no explicit port on a recognised Neo4j scheme. Edit ${envFilePath} NEO4J_URI.`
869
+ );
870
+ }
871
+ const [listening] = listeningPorts;
872
+ if (uriPort !== listening) {
873
+ throw new Error(
874
+ `NEO4J_URI=${JSON.stringify(uri)} port :${uriPort} not listening; only :${listening} is live on this device. Edit ${envFilePath} to match, or start neo4j-${readBrandHostname()}.service.`
875
+ );
876
+ }
877
+ return uri;
878
+ }
879
+ return validateNeo4jUri(uri, brandPort, envFilePath);
880
+ }
881
+ async function detectListeningNeo4jPorts(ports, timeoutMs, host) {
882
+ if (ports.size === 0) return /* @__PURE__ */ new Set();
883
+ const probes = Array.from(ports).map(
884
+ (port) => new Promise((resolvePromise) => {
885
+ let settled = false;
886
+ const sock = netConnect2({ host, port, family: 0 });
887
+ const finish = (listening) => {
888
+ if (settled) return;
889
+ settled = true;
890
+ try {
891
+ sock.destroy();
892
+ } catch {
893
+ }
894
+ resolvePromise({ port, listening });
895
+ };
896
+ const timer = setTimeout(() => finish(false), timeoutMs);
897
+ sock.once("connect", () => {
898
+ clearTimeout(timer);
899
+ finish(true);
900
+ });
901
+ sock.once("error", () => {
902
+ clearTimeout(timer);
903
+ finish(false);
904
+ });
905
+ })
906
+ );
907
+ const results = await Promise.all(probes);
908
+ return new Set(results.filter((r) => r.listening).map((r) => r.port));
909
+ }
910
+ var cachedListeningPortsPromise = null;
911
+ async function requireNeo4jUri() {
912
+ const envUri = process.env.NEO4J_URI;
913
+ const brandPort = readBrandNeo4jPort();
914
+ const envFilePath = resolve4(MAXY_DIR, ".env");
915
+ const candidates = /* @__PURE__ */ new Set([brandPort, DEFAULT_NEO4J_PORT]);
916
+ const uriPort = envUri ? extractNeo4jPort(envUri) : null;
917
+ if (uriPort !== null) candidates.add(uriPort);
918
+ if (cachedListeningPortsPromise === null) {
919
+ cachedListeningPortsPromise = (async () => {
920
+ let listening = await detectListeningNeo4jPorts(candidates, 250, "127.0.0.1");
921
+ if (listening.size === 0) {
922
+ listening = await detectListeningNeo4jPorts(candidates, 1e3, "127.0.0.1");
923
+ }
924
+ if (listening.size === 0) {
925
+ listening = await detectListeningNeo4jPorts(candidates, 1e3, "::1");
926
+ }
927
+ return listening;
928
+ })();
929
+ }
930
+ const listeningPorts = await cachedListeningPortsPromise;
931
+ const listeningCsv = Array.from(listeningPorts).sort((a, b) => a - b).join(",");
932
+ const mode = listeningPorts.size === 0 ? "none" : listeningPorts.size === 1 ? "single" : "co-tenant";
933
+ console.error(
934
+ `[neo4j-probe] listening=[${listeningCsv}] envPort=${uriPort ?? "unset"} brandPort=${brandPort} mode=${mode}`
935
+ );
936
+ return decideNeo4jUri(envUri, listeningPorts, brandPort, envFilePath);
937
+ }
938
+
939
+ // app/lib/claude-agent/plugin-manifest.ts
940
+ import { resolve as resolve5, join as join3 } from "path";
941
+ import {
942
+ readFileSync as readFileSync6,
943
+ writeFileSync,
944
+ readdirSync as readdirSync3,
945
+ existsSync as existsSync4,
946
+ mkdirSync as mkdirSync2,
947
+ statSync as statSync3,
948
+ cpSync,
949
+ rmSync
950
+ } from "fs";
951
+ import { spawn as spawn2 } from "child_process";
952
+
953
+ // app/lib/specialist-domains-clause.ts
954
+ var SPECIALIST_DOMAINS_ROUTING_CLAUSE = "Specialist subagents own these domains. Match user intent to a tool below and delegate to the specialist. Fall back to memory-search only when no tool here matches. ToolSearch is a last-resort escape hatch for tools not listed in any domain \u2014 not a routing mechanism; prefer admitting ignorance over discovering.";
955
+
956
+ // app/lib/claude-agent/plugin-manifest.ts
957
+ function isMissingPluginDir(pluginDir) {
958
+ if (parsePluginFrontmatter(pluginDir)) return false;
959
+ const mcpEntry = resolve5(PLATFORM_ROOT2, "plugins", pluginDir, "mcp/dist/index.js");
960
+ return !existsSync4(mcpEntry);
961
+ }
962
+ function findMissingPlugins(enabledPlugins) {
963
+ if (!Array.isArray(enabledPlugins) || enabledPlugins.length === 0) return [];
964
+ return enabledPlugins.filter(isMissingPluginDir);
965
+ }
966
+ function parsePluginFrontmatter(pluginDir) {
967
+ const pluginPath = resolve5(PLATFORM_ROOT2, "plugins", pluginDir, "PLUGIN.md");
968
+ if (!existsSync4(pluginPath)) return null;
969
+ let raw;
970
+ try {
971
+ raw = readFileSync6(pluginPath, "utf-8");
972
+ } catch {
973
+ console.warn(`[plugins] cannot read ${pluginPath}`);
974
+ return null;
975
+ }
976
+ const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
977
+ if (!fmMatch) return null;
978
+ const fm = fmMatch[1];
979
+ const nameMatch = fm.match(/^name:\s*(.+)$/m);
980
+ const descRaw = fm.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? "";
981
+ const description = descRaw.replace(/^"(.*)"$/, "$1");
982
+ const fmLines = fm.split("\n");
983
+ function parseYamlList(fieldName) {
984
+ const items = [];
985
+ let inside = false;
986
+ for (const line of fmLines) {
987
+ if (new RegExp(`^${fieldName}:`).test(line)) {
988
+ if (new RegExp(`^${fieldName}:\\s*\\[]`).test(line)) break;
989
+ inside = true;
990
+ continue;
991
+ }
992
+ if (inside) {
993
+ const m = line.match(/^ - (.+)/);
994
+ if (m) {
995
+ items.push(m[1].trim());
996
+ } else {
997
+ break;
998
+ }
999
+ }
1000
+ }
1001
+ return items;
1002
+ }
1003
+ const tools = parseYamlList("tools");
1004
+ const hidden = parseYamlList("hidden");
1005
+ const requires = parseYamlList("requires");
1006
+ let platform = {};
1007
+ const metaMatch = fm.match(/^metadata:\s*(.+)$/m);
1008
+ if (metaMatch) {
1009
+ try {
1010
+ const metadata = JSON.parse(metaMatch[1]);
1011
+ if (metadata.platform && typeof metadata.platform === "object") {
1012
+ platform = metadata.platform;
1013
+ }
1014
+ } catch {
1015
+ console.warn(`[plugins] ${pluginDir}/PLUGIN.md: invalid metadata JSON`);
1016
+ }
1017
+ }
1018
+ return { name: nameMatch?.[1]?.trim() ?? pluginDir, description, tools, hidden, requires, platform };
1019
+ }
1020
+ function readBundleSubPlugins(bundlePath) {
1021
+ if (!existsSync4(bundlePath)) return [];
1022
+ let raw;
1023
+ try {
1024
+ raw = readFileSync6(bundlePath, "utf-8");
1025
+ } catch {
1026
+ return [];
1027
+ }
1028
+ const fm = raw.match(/^---\n([\s\S]*?)\n---/);
1029
+ if (!fm) return [];
1030
+ const subs = [];
1031
+ let inPlugins = false;
1032
+ for (const line of fm[1].split("\n")) {
1033
+ if (/^plugins:/.test(line)) {
1034
+ inPlugins = true;
1035
+ continue;
1036
+ }
1037
+ if (inPlugins) {
1038
+ const m = line.match(/^\s+- (.+)/);
1039
+ if (m) subs.push(m[1].trim());
1040
+ else break;
1041
+ }
1042
+ }
1043
+ return subs;
1044
+ }
1045
+ function walkPremiumBundles() {
1046
+ if (BRAND_NAME === "maxy") return [];
1047
+ const stagingRoot = resolve5(PLATFORM_ROOT2, "../premium-plugins");
1048
+ if (!existsSync4(stagingRoot)) return [];
1049
+ let entries;
1050
+ try {
1051
+ entries = readdirSync3(stagingRoot);
1052
+ } catch {
1053
+ return [];
1054
+ }
1055
+ const result = [];
1056
+ for (const bundle of entries) {
1057
+ const bundleDir = resolve5(stagingRoot, bundle);
1058
+ try {
1059
+ if (!statSync3(bundleDir).isDirectory()) continue;
1060
+ } catch {
1061
+ continue;
1062
+ }
1063
+ const declared = readBundleSubPlugins(join3(bundleDir, "BUNDLE.md"));
1064
+ result.push({ bundle, subs: declared.length > 0 ? declared : [bundle] });
1065
+ }
1066
+ return result;
1067
+ }
1068
+ function autoDeliverPremiumPlugins() {
1069
+ const TAG = "[premium-auto-deliver]";
1070
+ const bundles = walkPremiumBundles();
1071
+ if (bundles.length === 0) return;
1072
+ const stagingRoot = resolve5(PLATFORM_ROOT2, "../premium-plugins");
1073
+ const pluginsDir = resolve5(PLATFORM_ROOT2, "plugins");
1074
+ for (const { bundle, subs } of bundles) {
1075
+ const stagingDir = resolve5(stagingRoot, bundle);
1076
+ const bundlePath = join3(stagingDir, "BUNDLE.md");
1077
+ const isBundle = existsSync4(bundlePath);
1078
+ if (isBundle) {
1079
+ let delivered = 0;
1080
+ let skipped = 0;
1081
+ for (const sub of subs) {
1082
+ const target = resolve5(pluginsDir, sub);
1083
+ if (existsSync4(resolve5(target, "PLUGIN.md"))) {
1084
+ skipped++;
1085
+ continue;
1086
+ }
1087
+ const source = resolve5(stagingDir, "plugins", sub);
1088
+ if (!existsSync4(source)) {
1089
+ console.log(`${TAG} ${bundle}/${sub}: source missing in staging \u2014 skipping`);
1090
+ continue;
1091
+ }
1092
+ try {
1093
+ cpSync(source, target, { recursive: true });
1094
+ delivered++;
1095
+ } catch (err) {
1096
+ console.log(`${TAG} ${bundle}/${sub}: copy failed \u2014 ${err instanceof Error ? err.message : String(err)}`);
1097
+ }
1098
+ }
1099
+ console.log(`${TAG} ${bundle} (bundle): ${delivered} delivered, ${skipped} already present`);
1100
+ } else {
1101
+ const target = resolve5(pluginsDir, bundle);
1102
+ if (existsSync4(resolve5(target, "PLUGIN.md"))) {
1103
+ console.log(`${TAG} ${bundle}: already present \u2014 skipping`);
1104
+ } else {
1105
+ try {
1106
+ cpSync(stagingDir, target, { recursive: true });
1107
+ console.log(`${TAG} ${bundle} (standalone): delivered`);
1108
+ } catch (err) {
1109
+ console.log(`${TAG} ${bundle}: copy failed \u2014 ${err instanceof Error ? err.message : String(err)}`);
1110
+ }
1111
+ }
1112
+ }
1113
+ }
1114
+ }
1115
+ function reconcileEnabledPlugins(accountDir, config) {
1116
+ const TAG = "[premium-auto-deliver]";
1117
+ if (!accountDir || !config) return;
1118
+ const bundles = walkPremiumBundles();
1119
+ const pluginsDir = resolve5(PLATFORM_ROOT2, "plugins");
1120
+ const bundleNames = [];
1121
+ const allSubs = [];
1122
+ for (const { bundle, subs } of bundles) {
1123
+ bundleNames.push(bundle);
1124
+ for (const sub of subs) {
1125
+ if (!existsSync4(resolve5(pluginsDir, sub, "PLUGIN.md"))) continue;
1126
+ allSubs.push(sub);
1127
+ }
1128
+ }
1129
+ const current = Array.isArray(config.enabledPlugins) ? config.enabledPlugins : [];
1130
+ const currentSet = new Set(current);
1131
+ const added = [];
1132
+ for (const sub of allSubs) {
1133
+ if (currentSet.has(sub)) continue;
1134
+ currentSet.add(sub);
1135
+ added.push(sub);
1136
+ }
1137
+ console.log(`${TAG} brand=${BRAND_NAME} bundles=[${bundleNames.join(",")}] subs=[${allSubs.join(",")}] stamped=${added.length}`);
1138
+ if (added.length === 0) return;
1139
+ const merged = [...currentSet];
1140
+ const configPath = resolve5(accountDir, "account.json");
1141
+ try {
1142
+ const raw = readFileSync6(configPath, "utf-8");
1143
+ const parsed = JSON.parse(raw);
1144
+ parsed.enabledPlugins = merged;
1145
+ writeFileSync(configPath, JSON.stringify(parsed, null, 2) + "\n");
1146
+ config.enabledPlugins = merged;
1147
+ } catch (err) {
1148
+ console.error(`${TAG} enabled-stamp write failed \u2014 ${err instanceof Error ? err.message : String(err)}`);
1149
+ }
1150
+ }
1151
+ var USER_MIRROR_MARKER = ".user-mirror";
1152
+ function autoDeliverUserPlugins(accountDir) {
1153
+ const TAG = "[user-plugins-deliver]";
1154
+ const userPluginsDir = resolve5(accountDir, "plugins");
1155
+ if (!existsSync4(userPluginsDir)) return;
1156
+ const pluginsDir = resolve5(PLATFORM_ROOT2, "plugins");
1157
+ let entries;
1158
+ try {
1159
+ entries = readdirSync3(userPluginsDir);
1160
+ } catch (err) {
1161
+ console.log(`${TAG} accountDir=${accountDir} read-failed reason=${err instanceof Error ? err.message : String(err)}`);
1162
+ return;
1163
+ }
1164
+ let mirrored = 0;
1165
+ let skipped = 0;
1166
+ for (const pluginName of entries) {
1167
+ const source = resolve5(userPluginsDir, pluginName);
1168
+ const sourcePluginMd = resolve5(source, "PLUGIN.md");
1169
+ if (!existsSync4(sourcePluginMd)) continue;
1170
+ const sourceMarker = resolve5(source, USER_MIRROR_MARKER);
1171
+ if (!existsSync4(sourceMarker)) {
1172
+ try {
1173
+ writeFileSync(sourceMarker, `pluginName=${pluginName}
1174
+ source=${source}
1175
+ `);
1176
+ console.log(`${TAG} ${pluginName}: canonical missing marker \u2014 stamped`);
1177
+ } catch (err) {
1178
+ console.error(`${TAG} ${pluginName}: marker stamp failed \u2014 ${err instanceof Error ? err.message : String(err)}`);
1179
+ }
1180
+ }
1181
+ const target = resolve5(pluginsDir, pluginName);
1182
+ const targetMarker = resolve5(target, USER_MIRROR_MARKER);
1183
+ if (existsSync4(target) && !existsSync4(targetMarker)) {
1184
+ console.log(`${TAG} ${pluginName}: shipped plugin owns this name \u2014 skipping user mirror`);
1185
+ skipped++;
1186
+ continue;
1187
+ }
1188
+ try {
1189
+ cpSync(source, target, { recursive: true, force: true });
1190
+ mirrored++;
1191
+ } catch (err) {
1192
+ console.error(`${TAG} ${pluginName}: copy failed \u2014 ${err instanceof Error ? err.message : String(err)}`);
1193
+ }
1194
+ }
1195
+ console.log(`${TAG} accountDir=${accountDir} mirrored=${mirrored} skipped=${skipped}`);
1196
+ }
1197
+ var PLUGIN_RENAMES = {
1198
+ "real-agency-loop": "loop",
1199
+ "real-agency-sales": "estate-sales",
1200
+ "real-agency-vendors": "vendors",
1201
+ "real-agency-buyers": "buyers",
1202
+ "real-agency-listings": "listings",
1203
+ "real-agency-leads": "leads",
1204
+ "real-agency-coaching": "estate-coaching",
1205
+ "real-agency-business": "estate-business",
1206
+ "real-agency-onboarding": "estate-onboarding",
1207
+ "real-agency-teaching": "estate-teaching"
1208
+ };
1209
+ function migratePluginRenames(accountDir, config) {
1210
+ if (!Array.isArray(config.enabledPlugins) || config.enabledPlugins.length === 0) return;
1211
+ const TAG = "[plugin-rename-migrate]";
1212
+ let changed = false;
1213
+ const migrated = config.enabledPlugins.map((name) => {
1214
+ const newName = PLUGIN_RENAMES[name];
1215
+ if (newName) {
1216
+ console.log(`${TAG} ${name} \u2192 ${newName}`);
1217
+ changed = true;
1218
+ return newName;
1219
+ }
1220
+ return name;
1221
+ });
1222
+ if (!changed) return;
1223
+ const configPath = resolve5(accountDir, "account.json");
1224
+ try {
1225
+ const raw = readFileSync6(configPath, "utf-8");
1226
+ const parsed = JSON.parse(raw);
1227
+ parsed.enabledPlugins = migrated;
1228
+ writeFileSync(configPath, JSON.stringify(parsed, null, 2) + "\n");
1229
+ config.enabledPlugins = migrated;
1230
+ console.log(`${TAG} account.json updated (${migrated.length} plugins)`);
1231
+ } catch (err) {
1232
+ console.error(`${TAG} failed to update account.json \u2014 ${err instanceof Error ? err.message : String(err)}`);
1233
+ }
1234
+ const pluginsDir = resolve5(PLATFORM_ROOT2, "plugins");
1235
+ for (const oldName of Object.keys(PLUGIN_RENAMES)) {
1236
+ const orphan = resolve5(pluginsDir, oldName);
1237
+ if (!existsSync4(orphan)) continue;
1238
+ try {
1239
+ rmSync(orphan, { recursive: true });
1240
+ console.log(`${TAG} removed orphan: ${oldName}`);
1241
+ } catch (err) {
1242
+ console.log(`${TAG} orphan removal failed: ${oldName} \u2014 ${err instanceof Error ? err.message : String(err)}`);
1243
+ }
1244
+ }
1245
+ }
1246
+ function autoDeliverBundleAgents(accountDir) {
1247
+ const TAG = "[bundle-agent-deliver]";
1248
+ const bundles = walkPremiumBundles();
1249
+ if (bundles.length === 0) return;
1250
+ const stagingRoot = resolve5(PLATFORM_ROOT2, "../premium-plugins");
1251
+ const specialistsDir = resolve5(accountDir, "specialists", "agents");
1252
+ if (!existsSync4(specialistsDir)) mkdirSync2(specialistsDir, { recursive: true });
1253
+ const agentsmdPath = resolve5(accountDir, "agents", "admin", "AGENTS.md");
1254
+ let agentsmd = "";
1255
+ try {
1256
+ agentsmd = existsSync4(agentsmdPath) ? readFileSync6(agentsmdPath, "utf-8") : "";
1257
+ } catch {
1258
+ }
1259
+ let delivered = 0;
1260
+ for (const { bundle } of bundles) {
1261
+ const bundleAgentsDir = resolve5(stagingRoot, bundle, "agents");
1262
+ if (!existsSync4(bundleAgentsDir)) continue;
1263
+ let entries;
1264
+ try {
1265
+ entries = readdirSync3(bundleAgentsDir).filter((f) => f.endsWith(".md"));
1266
+ } catch {
1267
+ continue;
1268
+ }
1269
+ for (const filename of entries) {
1270
+ const target = resolve5(specialistsDir, filename);
1271
+ if (existsSync4(target)) continue;
1272
+ const source = resolve5(bundleAgentsDir, filename);
1273
+ try {
1274
+ cpSync(source, target);
1275
+ } catch (err) {
1276
+ console.log(`${TAG} copy failed: ${filename} \u2014 ${err instanceof Error ? err.message : String(err)}`);
1277
+ continue;
1278
+ }
1279
+ try {
1280
+ const content = readFileSync6(target, "utf-8");
1281
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
1282
+ if (fmMatch) {
1283
+ const nameMatch = fmMatch[1].match(/^name:\s*(.+)/m);
1284
+ const descMatch = fmMatch[1].match(/^description:\s*"?([^"\n]+)"?/m);
1285
+ if (nameMatch && descMatch) {
1286
+ const agentName = nameMatch[1].trim();
1287
+ const agentDesc = descMatch[1].trim();
1288
+ if (!agentsmd.includes(`specialists:${agentName}`)) agentsmd += `- **specialists:${agentName}**: ${agentDesc}
1289
+ `;
1290
+ }
1291
+ }
1292
+ } catch {
1293
+ console.log(`${TAG} ${filename}: delivered but frontmatter parse failed`);
1294
+ }
1295
+ delivered++;
1296
+ console.log(`${TAG} ${filename}: delivered`);
1297
+ }
1298
+ }
1299
+ if (delivered > 0) {
1300
+ try {
1301
+ writeFileSync(agentsmdPath, agentsmd);
1302
+ console.log(`${TAG} AGENTS.md updated (${delivered} agents added)`);
1303
+ } catch (err) {
1304
+ console.error(`${TAG} AGENTS.md update failed \u2014 ${err instanceof Error ? err.message : String(err)}`);
1305
+ }
1306
+ }
1307
+ }
1308
+ function assemblePublicPluginContent(pluginDir) {
1309
+ const pluginRoot = resolve5(PLATFORM_ROOT2, "plugins", pluginDir);
1310
+ const pluginPath = resolve5(pluginRoot, "PLUGIN.md");
1311
+ let raw;
1312
+ try {
1313
+ raw = readFileSync6(pluginPath, "utf-8");
1314
+ } catch {
1315
+ return null;
1316
+ }
1317
+ const pluginBodyRaw = raw.replace(/^---\n[\s\S]*?\n---\n*/, "").trim();
1318
+ if (!pluginBodyRaw) return null;
1319
+ const pluginBody = substituteBrandPlaceholders(pluginBodyRaw, pluginPath);
1320
+ const parts = [pluginBody];
1321
+ let skillCount = 0;
1322
+ let refCount = 0;
1323
+ const skillsDir = resolve5(pluginRoot, "skills");
1324
+ let skillDirs;
1325
+ try {
1326
+ skillDirs = readdirSync3(skillsDir).sort();
1327
+ } catch {
1328
+ return { body: pluginBody, skillCount: 0, refCount: 0 };
1329
+ }
1330
+ for (const skillName of skillDirs) {
1331
+ const skillDir = resolve5(skillsDir, skillName);
1332
+ let skillRaw;
1333
+ try {
1334
+ skillRaw = readFileSync6(resolve5(skillDir, "SKILL.md"), "utf-8");
1335
+ } catch {
1336
+ continue;
1337
+ }
1338
+ const fmMatch = skillRaw.match(/^---\n([\s\S]*?)\n---/);
1339
+ let publicEmbed = true;
1340
+ const publicExcludeReferences = [];
1341
+ if (fmMatch) {
1342
+ const fm = fmMatch[1];
1343
+ const embedMatch = fm.match(/^publicEmbed:\s*(.+)$/m);
1344
+ if (embedMatch && embedMatch[1].trim() === "false") publicEmbed = false;
1345
+ let insideExclude = false;
1346
+ for (const line of fm.split("\n")) {
1347
+ if (/^publicExcludeReferences:/.test(line)) {
1348
+ if (/^publicExcludeReferences:\s*\[]/.test(line)) break;
1349
+ insideExclude = true;
1350
+ continue;
1351
+ }
1352
+ if (insideExclude) {
1353
+ const m = line.match(/^ - (.+)/);
1354
+ if (m) {
1355
+ publicExcludeReferences.push(m[1].trim());
1356
+ } else {
1357
+ break;
1358
+ }
1359
+ }
1360
+ }
1361
+ }
1362
+ if (!publicEmbed) {
1363
+ console.log(`[plugins] ${pluginDir}/skills/${skillName}: publicEmbed=false \u2014 skipping for public`);
1364
+ continue;
1365
+ }
1366
+ const skillBodyRaw = skillRaw.replace(/^---\n[\s\S]*?\n---\n*/, "").trim();
1367
+ if (!skillBodyRaw) continue;
1368
+ const skillPath = resolve5(skillDir, "SKILL.md");
1369
+ const skillBody = substituteBrandPlaceholders(skillBodyRaw, skillPath);
1370
+ parts.push(`
1371
+ <!-- skill: ${skillName} -->`);
1372
+ parts.push(skillBody);
1373
+ const refsDir = resolve5(skillDir, "references");
1374
+ let refFiles;
1375
+ try {
1376
+ refFiles = readdirSync3(refsDir).filter((f) => f.endsWith(".md")).filter((f) => !publicExcludeReferences.includes(f)).sort();
1377
+ } catch {
1378
+ parts.push(`<!-- /skill: ${skillName} -->`);
1379
+ skillCount++;
1380
+ continue;
1381
+ }
1382
+ for (const refFile of refFiles) {
1383
+ try {
1384
+ const refPath = resolve5(refsDir, refFile);
1385
+ const refRaw = readFileSync6(refPath, "utf-8").trim();
1386
+ if (refRaw) {
1387
+ const refContent = substituteBrandPlaceholders(refRaw, refPath);
1388
+ parts.push(`
1389
+ <!-- reference: ${refFile} -->`);
1390
+ parts.push(refContent);
1391
+ parts.push(`<!-- /reference: ${refFile} -->`);
1392
+ refCount++;
1393
+ }
1394
+ } catch {
1395
+ console.warn(`[plugins] ${pluginDir}/skills/${skillName}/references/${refFile}: unreadable \u2014 skipping`);
1396
+ }
1397
+ }
1398
+ parts.push(`<!-- /skill: ${skillName} -->`);
1399
+ skillCount++;
1400
+ }
1401
+ return { body: parts.join("\n"), skillCount, refCount };
1402
+ }
1403
+ function loadEmbeddedPlugins(agentType, selectedPlugins, enabledPlugins) {
1404
+ const pluginsDir = resolve5(PLATFORM_ROOT2, "plugins");
1405
+ let dirs;
1406
+ try {
1407
+ dirs = readdirSync3(pluginsDir);
1408
+ } catch {
1409
+ console.warn(`[plugins] cannot read plugins directory: ${pluginsDir}`);
1410
+ return [];
1411
+ }
1412
+ const results = [];
1413
+ for (const dir of dirs) {
1414
+ const parsed = parsePluginFrontmatter(dir);
1415
+ if (!parsed) continue;
1416
+ const { platform } = parsed;
1417
+ const embed = platform.embed;
1418
+ if (!Array.isArray(embed) || !embed.includes(agentType)) {
1419
+ if (typeof embed === "boolean") {
1420
+ console.warn(`[plugins] ${dir}/PLUGIN.md: embed must be an array of agent types, got boolean \u2014 skipping`);
1421
+ } else {
1422
+ console.log(`[plugins] ${dir}: no embed for agentType=${agentType} (embed=${JSON.stringify(embed)}) \u2014 skipping`);
1423
+ }
1424
+ continue;
1425
+ }
1426
+ if (platform.optional) {
1427
+ const enabled = Array.isArray(enabledPlugins) ? enabledPlugins : [];
1428
+ if (!enabled.includes(dir)) {
1429
+ console.log(`[plugins] ${dir}: optional plugin not enabled \u2014 skipping embed`);
1430
+ continue;
1431
+ }
1432
+ }
1433
+ if (parsed.requires.length > 0) {
1434
+ const enabled = Array.isArray(enabledPlugins) ? enabledPlugins : [];
1435
+ const missing = parsed.requires.filter((req) => {
1436
+ if (enabled.includes(req)) return false;
1437
+ const reqParsed = parsePluginFrontmatter(req);
1438
+ return !(reqParsed && !reqParsed.platform.optional);
1439
+ });
1440
+ if (missing.length > 0) {
1441
+ console.warn(`[plugins] ${dir}: requires [${missing.join(", ")}] but they are not enabled \u2014 skipping`);
1442
+ continue;
1443
+ }
1444
+ }
1445
+ if (selectedPlugins && !selectedPlugins.includes(dir)) {
1446
+ console.log(`[plugins] ${dir}: not in selectedPlugins for agentType=${agentType} \u2014 skipping`);
1447
+ continue;
1448
+ }
1449
+ if (agentType === "public") {
1450
+ const assembled = assemblePublicPluginContent(dir);
1451
+ if (assembled) {
1452
+ results.push({ name: dir, chars: assembled.body.length, body: assembled.body });
1453
+ console.log(`[plugins] loaded ${dir} for public (${assembled.body.length} chars, ${assembled.skillCount} skills, ${assembled.refCount} refs)`);
1454
+ }
1455
+ } else {
1456
+ const pluginPath = resolve5(pluginsDir, dir, "PLUGIN.md");
1457
+ let raw;
1458
+ try {
1459
+ raw = readFileSync6(pluginPath, "utf-8");
1460
+ } catch (err) {
1461
+ console.warn(`[plugins] ${dir}: failed to read PLUGIN.md for ${agentType} embed: ${String(err)}`);
1462
+ continue;
1463
+ }
1464
+ const bodyRaw = raw.replace(/^---\n[\s\S]*?\n---\n*/, "").trim();
1465
+ const body = bodyRaw ? substituteBrandPlaceholders(bodyRaw, pluginPath) : "";
1466
+ if (body) {
1467
+ results.push({ name: dir, chars: body.length, body });
1468
+ console.log(`[plugins] loaded ${dir} for ${agentType} (${body.length} chars)`);
1469
+ }
1470
+ }
1471
+ }
1472
+ if (results.length === 0) {
1473
+ console.log(`[plugins] no embedded plugins for agentType=${agentType}`);
1474
+ } else if (agentType === "public") {
1475
+ const totalChars = results.reduce((sum, r) => sum + r.chars, 0);
1476
+ console.log(`[plugins] total embedded for public: ${totalChars} chars across ${results.length} plugins`);
1477
+ }
1478
+ return results;
1479
+ }
1480
+ var mcpToolsCache = /* @__PURE__ */ new Map();
1481
+ function fetchMcpToolsList(pluginDir) {
1482
+ const cached = mcpToolsCache.get(pluginDir);
1483
+ if (cached) return Promise.resolve(cached);
1484
+ const serverPath = resolve5(PLATFORM_ROOT2, "plugins", pluginDir, "mcp/dist/index.js");
1485
+ if (!existsSync4(serverPath)) return Promise.resolve([]);
1486
+ const startMs = Date.now();
1487
+ console.error(`[spawn-env] STREAM_LOG_PATH=unset site=toolslist pluginDir=${pluginDir} reason=discovery-no-conversation`);
1488
+ return new Promise((resolvePromise) => {
1489
+ const proc = spawn2(process.execPath, [serverPath], {
1490
+ env: { ...process.env, PLATFORM_ROOT: PLATFORM_ROOT2, ACCOUNT_ID: "__toolslist__", PLATFORM_PORT: process.env.PORT ?? "19200" }
1491
+ });
1492
+ let buffer = "";
1493
+ let stderrBuf = "";
1494
+ let settled = false;
1495
+ const TOOLS_LIST_ID = 2;
1496
+ const settle = (tools, reason) => {
1497
+ if (settled) return;
1498
+ settled = true;
1499
+ proc.kill();
1500
+ const elapsed = Date.now() - startMs;
1501
+ if (tools.length > 0) {
1502
+ mcpToolsCache.set(pluginDir, tools);
1503
+ console.error(`[plugin-manifest] ${pluginDir}: ${tools.length} tools via MCP (${elapsed}ms)`);
1504
+ } else {
1505
+ console.error(`[plugin-manifest] ${pluginDir}: tools/list failed (${reason}) (${elapsed}ms)${stderrBuf ? ` stderr: ${stderrBuf.slice(0, 300)}` : ""}`);
1506
+ }
1507
+ resolvePromise(tools);
1508
+ };
1509
+ proc.stdout.on("data", (chunk) => {
1510
+ buffer += chunk.toString();
1511
+ const lines = buffer.split("\n");
1512
+ buffer = lines.pop() ?? "";
1513
+ for (const line of lines) {
1514
+ if (!line.trim()) continue;
1515
+ let msg;
1516
+ try {
1517
+ msg = JSON.parse(line);
1518
+ } catch {
1519
+ continue;
1520
+ }
1521
+ if (msg.id === 1) {
1522
+ proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n");
1523
+ proc.stdin.write(JSON.stringify({ jsonrpc: "2.0", id: TOOLS_LIST_ID, method: "tools/list", params: {} }) + "\n");
1524
+ } else if (msg.id === TOOLS_LIST_ID) {
1525
+ const result = msg.result;
1526
+ const rawTools = result?.tools;
1527
+ if (Array.isArray(rawTools)) {
1528
+ const tools = rawTools.filter((t) => typeof t.name === "string" && t.name.length > 0).map((t) => ({ name: t.name, description: typeof t.description === "string" ? t.description : "" }));
1529
+ settle(tools, "success");
1530
+ } else {
1531
+ settle([], "no tools array in response");
1532
+ }
1533
+ }
1534
+ }
1535
+ });
1536
+ proc.stderr.on("data", (chunk) => {
1537
+ stderrBuf += chunk.toString();
1538
+ });
1539
+ proc.on("error", (err) => settle([], `spawn error: ${err.message}`));
1540
+ proc.on("close", (code) => {
1541
+ if (!settled) settle([], `process exited with code ${code}`);
1542
+ });
1543
+ proc.stdin.write(JSON.stringify({
1544
+ jsonrpc: "2.0",
1545
+ id: 1,
1546
+ method: "initialize",
1547
+ params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "maxy-manifest", version: "1.0" } }
1548
+ }) + "\n");
1549
+ setTimeout(() => settle([], "timeout (5s)"), 5e3);
1550
+ });
1551
+ }
1552
+ var SPECIALIST_PLUGIN_DOMAINS = {
1553
+ scheduling: ["personal-assistant"],
1554
+ cloudflare: ["personal-assistant"],
1555
+ telegram: ["personal-assistant"],
1556
+ whatsapp: ["personal-assistant"],
1557
+ anthropic: ["personal-assistant"],
1558
+ email: ["personal-assistant"],
1559
+ contacts: ["personal-assistant"],
1560
+ tasks: ["project-manager"],
1561
+ projects: ["project-manager"],
1562
+ workflows: ["project-manager"],
1563
+ waitlist: ["project-manager"],
1564
+ "deep-research": ["research-assistant"],
1565
+ replicate: ["research-assistant", "content-producer"],
1566
+ loop: ["negotiator", "valuer"]
1567
+ };
1568
+ async function buildPluginManifest(enabledPlugins) {
1569
+ const pluginsDir = resolve5(PLATFORM_ROOT2, "plugins");
1570
+ let dirs;
1571
+ try {
1572
+ dirs = readdirSync3(pluginsDir);
1573
+ } catch {
1574
+ return "";
1575
+ }
1576
+ const enabled = Array.isArray(enabledPlugins) ? enabledPlugins : [];
1577
+ const activePlugins = [];
1578
+ for (const dir of dirs.sort()) {
1579
+ const parsed = parsePluginFrontmatter(dir);
1580
+ if (!parsed) continue;
1581
+ if (parsed.platform.optional && !enabled.includes(dir)) continue;
1582
+ if (parsed.requires.length > 0) {
1583
+ const missing = parsed.requires.filter((req) => {
1584
+ if (enabled.includes(req)) return false;
1585
+ const reqParsed = parsePluginFrontmatter(req);
1586
+ return !reqParsed || !!reqParsed.platform.optional;
1587
+ });
1588
+ if (missing.length > 0) continue;
1589
+ }
1590
+ activePlugins.push({ dir, parsed });
1591
+ }
1592
+ const specialistPlugins = [];
1593
+ const adminPlugins = [];
1594
+ for (const { dir, parsed } of activePlugins) {
1595
+ const specialists = SPECIALIST_PLUGIN_DOMAINS[dir];
1596
+ if (specialists) {
1597
+ specialistPlugins.push({ dir, parsed, specialists });
1598
+ } else {
1599
+ adminPlugins.push({ dir, parsed });
1600
+ }
1601
+ }
1602
+ const [adminMcpResults, specialistMcpResults] = await Promise.all([
1603
+ Promise.all(adminPlugins.map(({ dir }) => fetchMcpToolsList(dir))),
1604
+ Promise.all(specialistPlugins.map(({ dir }) => fetchMcpToolsList(dir)))
1605
+ ]);
1606
+ const lines = ["<plugin-manifest>"];
1607
+ lines.push('\nPlugin skills and references live in the bundled npm package, not on disk under `<accountDir>/agents/admin/plugins/`. Load skills with `mcp__admin__skill-load skillName="<name>"` and references with `mcp__admin__plugin-read`. `Read`/`Glob`/`Bash` against these paths will fail.');
1608
+ let totalTools = 0;
1609
+ let mcpSourced = 0;
1610
+ let fallbackSourced = 0;
1611
+ let noServer = 0;
1612
+ const specialistGroups = /* @__PURE__ */ new Map();
1613
+ for (const { dir, specialists } of specialistPlugins) {
1614
+ for (const specialist of specialists) {
1615
+ const existing = specialistGroups.get(specialist) ?? [];
1616
+ existing.push(dir);
1617
+ specialistGroups.set(specialist, existing);
1618
+ }
1619
+ }
1620
+ const specialistToolDetails = /* @__PURE__ */ new Map();
1621
+ let specialistMcpCount = 0;
1622
+ for (let i = 0; i < specialistPlugins.length; i++) {
1623
+ const { dir, parsed } = specialistPlugins[i];
1624
+ const mcpTools = specialistMcpResults[i];
1625
+ if (mcpTools.length > 0) {
1626
+ specialistMcpCount++;
1627
+ const hiddenSet = new Set(parsed.hidden);
1628
+ const visibleTools = mcpTools.filter((t) => !hiddenSet.has(t.name));
1629
+ totalTools += visibleTools.length;
1630
+ if (visibleTools.length > 0) {
1631
+ const desc = parsed.description ? parsed.description.split(/\.\s/)[0].replace(/\.$/, "") : "";
1632
+ specialistToolDetails.set(dir, { desc, tools: visibleTools });
1633
+ }
1634
+ }
1635
+ }
1636
+ if (specialistGroups.size > 0) {
1637
+ lines.push("\n<specialist-domains>");
1638
+ lines.push(SPECIALIST_DOMAINS_ROUTING_CLAUSE);
1639
+ for (const [specialist, plugins] of specialistGroups) {
1640
+ lines.push(`
1641
+ specialists:${specialist}: ${plugins.join(", ")}`);
1642
+ for (const plugin of plugins) {
1643
+ const details = specialistToolDetails.get(plugin);
1644
+ if (!details) continue;
1645
+ for (const tool of details.tools) {
1646
+ const toolDesc = tool.description ? tool.description.split(/\.\s/)[0].replace(/\.$/, "") : "";
1647
+ if (!toolDesc) {
1648
+ console.error(`[plugin-manifest] WARN: specialist tool missing description plugin=${plugin} tool=${tool.name}`);
1649
+ }
1650
+ lines.push(toolDesc ? ` ${tool.name} \u2014 ${toolDesc}` : ` ${tool.name}`);
1651
+ }
1652
+ }
1653
+ }
1654
+ lines.push("</specialist-domains>");
1655
+ }
1656
+ for (let j = 0; j < adminPlugins.length; j++) {
1657
+ const { dir, parsed } = adminPlugins[j];
1658
+ const mcpTools = adminMcpResults[j];
1659
+ const pluginRoot = resolve5(pluginsDir, dir);
1660
+ const skills = [];
1661
+ const references = [];
1662
+ const scanDir = (base, prefix, target) => {
1663
+ const scanPath = resolve5(pluginRoot, base);
1664
+ if (!existsSync4(scanPath)) return;
1665
+ try {
1666
+ const walk = (current, rel) => {
1667
+ for (const entry of readdirSync3(current)) {
1668
+ const full = resolve5(current, entry);
1669
+ try {
1670
+ const stat = statSync3(full);
1671
+ if (stat.isDirectory()) walk(full, `${rel}${entry}/`);
1672
+ else if (entry.endsWith(".md")) target.push(`${prefix}${rel}${entry}`);
1673
+ } catch {
1674
+ }
1675
+ }
1676
+ };
1677
+ walk(scanPath, "");
1678
+ } catch {
1679
+ }
1680
+ };
1681
+ scanDir("skills", "skills/", skills);
1682
+ scanDir("references", "references/", references);
1683
+ const hiddenSet = new Set(parsed.hidden);
1684
+ let toolLines = [];
1685
+ if (mcpTools.length > 0) {
1686
+ mcpSourced++;
1687
+ for (const tool of mcpTools) {
1688
+ if (hiddenSet.has(tool.name)) continue;
1689
+ const desc = tool.description ? tool.description.split(/\.\s/)[0].replace(/\.$/, "") : "";
1690
+ toolLines.push(desc ? ` ${tool.name} \u2014 ${desc}` : ` ${tool.name}`);
1691
+ }
1692
+ } else if (parsed.tools.length > 0) {
1693
+ const serverPath = resolve5(PLATFORM_ROOT2, "plugins", dir, "mcp/dist/index.js");
1694
+ if (existsSync4(serverPath)) {
1695
+ fallbackSourced++;
1696
+ console.error(`[plugin-manifest] ${dir}: tools/list empty \u2014 fallback to frontmatter (${parsed.tools.length} tools)`);
1697
+ }
1698
+ for (const name of parsed.tools) {
1699
+ if (hiddenSet.has(name)) continue;
1700
+ toolLines.push(` ${name}`);
1701
+ }
1702
+ } else {
1703
+ noServer++;
1704
+ }
1705
+ totalTools += toolLines.length;
1706
+ lines.push(`
1707
+ ## ${dir}`);
1708
+ if (parsed.description) lines.push(parsed.description);
1709
+ if (toolLines.length > 0) lines.push(`Tools:
1710
+ ${toolLines.join("\n")}`);
1711
+ if (skills.length > 0) {
1712
+ const skillNames = skills.map((s) => s.match(/^skills\/([a-z0-9][a-z0-9-]*)\/SKILL\.md$/)?.[1]).filter((s) => s !== void 0 && s.length > 0);
1713
+ if (skillNames.length > 0) lines.push(`Skills (load via mcp__admin__skill-load with skillName="<name>"): ${skillNames.join(", ")}`);
1714
+ }
1715
+ if (references.length > 0) lines.push(`References (load via mcp__admin__plugin-read with pluginName="${dir}" file="<path>"): ${references.join(", ")}`);
1716
+ }
1717
+ lines.push("\n</plugin-manifest>");
1718
+ console.error(`[plugin-manifest] doctrine-line=present`);
1719
+ console.error(`[plugin-manifest] plugins=${activePlugins.length} (specialist=${specialistPlugins.length} admin=${adminPlugins.length}) tools=${totalTools} (mcp=${mcpSourced + specialistMcpCount} fallback=${fallbackSourced} no-server=${noServer})`);
1720
+ const dormantLines = [];
1721
+ for (const dir of dirs.sort()) {
1722
+ const parsed = parsePluginFrontmatter(dir);
1723
+ if (!parsed) continue;
1724
+ if (!parsed.platform.optional) continue;
1725
+ if (enabled.includes(dir)) continue;
1726
+ if (parsed.platform.always) continue;
1727
+ let entry = `${dir} \u2014 ${parsed.description}`;
1728
+ if (parsed.tools.length > 0) entry += `
1729
+ tools: ${parsed.tools.join(", ")}`;
1730
+ dormantLines.push(entry);
1731
+ }
1732
+ if (dormantLines.length > 0) {
1733
+ lines.push("");
1734
+ lines.push("<dormant-plugins>");
1735
+ lines.push("These plugins are available but not activated. When the user describes a task that one of these plugins handles, mention the capability and offer to activate it. Do not prompt unprompted \u2014 only nudge when the user's intent aligns with a dormant plugin's purpose. If you have already suggested this plugin earlier in the conversation, do not suggest it again.");
1736
+ lines.push("");
1737
+ lines.push(dormantLines.join("\n\n"));
1738
+ lines.push("</dormant-plugins>");
1739
+ }
1740
+ return lines.join("\n");
1741
+ }
1742
+ function validateComponentData(componentName, data) {
1743
+ if (componentName !== "single-select" && componentName !== "multi-select") return null;
1744
+ const options = data?.options;
1745
+ if (!Array.isArray(options)) return `${componentName}: "options" must be an array, got ${typeof options}`;
1746
+ if (options.length === 0) return `${componentName}: "options" array is empty \u2014 at least one option required`;
1747
+ for (let i = 0; i < options.length; i++) {
1748
+ const opt = options[i];
1749
+ if (!opt || typeof opt !== "object") return `${componentName}: options[${i}] is not an object`;
1750
+ if (typeof opt.label !== "string" || opt.label.trim() === "") return `${componentName}: options[${i}] missing non-empty "label"`;
1751
+ if (typeof opt.value !== "string" || opt.value.trim() === "") return `${componentName}: options[${i}] missing non-empty "value"`;
1752
+ }
1753
+ return null;
1754
+ }
1755
+
1756
+ // app/lib/claude-agent/spawn-env.ts
1757
+ function streamLogPathFor(accountId, logKey) {
1758
+ const logDir = resolve6(ACCOUNTS_DIR, accountId, "logs");
1759
+ const streamLogPath = resolve6(logDir, `claude-agent-stream-${logKey}.log`);
1760
+ return { logDir, streamLogPath };
1761
+ }
1762
+ function buildSpawnEnv(accountId, accountDir, sessionKey) {
1763
+ if (!sessionKey) {
1764
+ throw new Error(`buildSpawnEnv: sessionKey is required (accountId=${accountId.slice(0, 8)})`);
1765
+ }
1766
+ const { logDir, streamLogPath } = streamLogPathFor(accountId, sessionKey);
1767
+ return {
1768
+ ...process.env,
1769
+ PLATFORM_ROOT: PLATFORM_ROOT2,
1770
+ ACCOUNT_DIR: accountDir,
1771
+ ACCOUNT_ID: accountId,
1772
+ LOG_DIR: logDir,
1773
+ STREAM_LOG_PATH: streamLogPath
1774
+ };
1775
+ }
1776
+ var _claudeBinaryPath;
1777
+ function resolveClaudeBinary() {
1778
+ if (_claudeBinaryPath !== void 0) return _claudeBinaryPath || void 0;
1779
+ if (process.env.CLAUDE_CODE_EXECUTABLE) {
1780
+ _claudeBinaryPath = process.env.CLAUDE_CODE_EXECUTABLE;
1781
+ return _claudeBinaryPath;
1782
+ }
1783
+ try {
1784
+ _claudeBinaryPath = execFileSync("which", ["claude"], { encoding: "utf8" }).trim() || "";
1785
+ return _claudeBinaryPath || void 0;
1786
+ } catch {
1787
+ _claudeBinaryPath = "";
1788
+ return void 0;
1789
+ }
1790
+ }
1791
+ var MCP_SPAWN_TEE_ENTRY = resolve6(PLATFORM_ROOT2, "lib/mcp-spawn-tee/dist/index.js");
1792
+ function withSpawnTee(name, entry, env) {
1793
+ return {
1794
+ command: "node",
1795
+ args: [MCP_SPAWN_TEE_ENTRY, entry],
1796
+ env: { ...env, MCP_SPAWN_TEE_NAME: name }
1797
+ };
1798
+ }
1799
+ async function resolveAdminConversationNodeId(accountId, conversationId) {
1800
+ const session = getSession();
1801
+ try {
1802
+ const res = await session.run(
1803
+ `MATCH (c:AdminConversation {conversationId: $conversationId, accountId: $accountId})
1804
+ RETURN elementId(c) AS elementId
1805
+ LIMIT 1`,
1806
+ { conversationId, accountId }
1807
+ );
1808
+ if (res.records.length === 0) {
1809
+ console.error(
1810
+ `[spawn-env] CONVERSATION_NODE_ID resolution: no :AdminConversation for conversationId=${conversationId.slice(0, 8)}\u2026 accountId=${accountId.slice(0, 8)}\u2026 \u2014 omitting env`
1811
+ );
1812
+ return void 0;
1813
+ }
1814
+ return res.records[0].get("elementId");
1815
+ } catch (err) {
1816
+ console.error(
1817
+ `[spawn-env] CONVERSATION_NODE_ID resolution failed: ${err instanceof Error ? err.message : String(err)} \u2014 omitting env`
1818
+ );
1819
+ return void 0;
1820
+ } finally {
1821
+ await session.close();
1822
+ }
1823
+ }
1824
+ async function getMcpServers(accountId, sessionKey, conversationId, userId, enabledPlugins) {
1825
+ if (!sessionKey) {
1826
+ throw new Error(`getMcpServers: sessionKey is required (accountId=${accountId.slice(0, 8)})`);
1827
+ }
1828
+ if (!conversationId) {
1829
+ throw new Error(`getMcpServers: conversationId is required (accountId=${accountId.slice(0, 8)})`);
1830
+ }
1831
+ const { logDir: LOG_DIR2, streamLogPath: STREAM_LOG_PATH } = streamLogPathFor(accountId, sessionKey);
1832
+ const conversationNodeId = await resolveAdminConversationNodeId(accountId, conversationId);
1833
+ const baseEnv = {
1834
+ ACCOUNT_ID: accountId,
1835
+ PLATFORM_ROOT: PLATFORM_ROOT2,
1836
+ LOG_DIR: LOG_DIR2,
1837
+ STREAM_LOG_PATH,
1838
+ NEO4J_URI: await requireNeo4jUri(),
1839
+ // Pass sessionId so MCP servers can stamp `createdBySession` on graph
1840
+ // writes per-handler (not module-load — that captured undefined and
1841
+ // produced the empty-session-uuid bug).
1842
+ SESSION_ID: conversationId
1843
+ };
1844
+ if (conversationNodeId) {
1845
+ baseEnv.CONVERSATION_NODE_ID = conversationNodeId;
1846
+ }
1847
+ const servers = {
1848
+ "memory": withSpawnTee(
1849
+ "memory",
1850
+ resolve6(PLATFORM_ROOT2, "plugins/memory/mcp/dist/index.js"),
1851
+ { ...baseEnv, ...userId ? { USER_ID: userId } : {} }
1852
+ ),
1853
+ "contacts": withSpawnTee(
1854
+ "contacts",
1855
+ resolve6(PLATFORM_ROOT2, "plugins/contacts/mcp/dist/index.js"),
1856
+ { ...baseEnv }
1857
+ ),
1858
+ "whatsapp": withSpawnTee(
1859
+ "whatsapp",
1860
+ resolve6(PLATFORM_ROOT2, "plugins/whatsapp/mcp/dist/index.js"),
1861
+ { ...baseEnv, PLATFORM_PORT: process.env.PORT ?? "19200" }
1862
+ ),
1863
+ "admin": withSpawnTee(
1864
+ "admin",
1865
+ resolve6(PLATFORM_ROOT2, "plugins/admin/mcp/dist/index.js"),
1866
+ { ...baseEnv, PLATFORM_PORT: process.env.PORT ?? "19200", ...userId ? { USER_ID: userId } : {} }
1867
+ ),
1868
+ "scheduling": withSpawnTee(
1869
+ "scheduling",
1870
+ resolve6(PLATFORM_ROOT2, "plugins/scheduling/mcp/dist/index.js"),
1871
+ // Task 793: USER_ID required for (accountId, userId)-keyed UserProfile reads.
1872
+ { ...baseEnv, ...userId ? { USER_ID: userId } : {} }
1873
+ ),
1874
+ "tasks": withSpawnTee(
1875
+ "tasks",
1876
+ resolve6(PLATFORM_ROOT2, "plugins/tasks/mcp/dist/index.js"),
1877
+ { ...baseEnv }
1878
+ ),
1879
+ "email": withSpawnTee(
1880
+ "email",
1881
+ resolve6(PLATFORM_ROOT2, "plugins/email/mcp/dist/index.js"),
1882
+ { ...baseEnv }
1883
+ ),
1884
+ // Workflows MCP — persistent admin-session server for list/get/update/delete/
1885
+ // validate/runs (the "manage" verbs). The same binary also spawns ad-hoc from
1886
+ // the heartbeat cron for `workflow-execute` via plugins/workflows/.mcp.json;
1887
+ // the two paths are independent. Without this entry the permission allowlist
1888
+ // at ADMIN_CORE_TOOLS grants tools the session never registers, and the model
1889
+ // ToolSearches fruitlessly before degrading to a task-create stand-in.
1890
+ "workflows": withSpawnTee(
1891
+ "workflows",
1892
+ resolve6(PLATFORM_ROOT2, "plugins/workflows/mcp/dist/index.js"),
1893
+ { ...baseEnv }
1894
+ ),
1895
+ // Playwright MCP server — browser automation for browser-specialist.
1896
+ // Key matches Claude Code's plugin naming: plugin_{plugin}_{server} so tools
1897
+ // appear as mcp__plugin_playwright_playwright__browser_navigate etc.
1898
+ // Hardcoded here rather than depending on the plugin cache (which can be wiped
1899
+ // by Claude Code updates or installer re-runs). The --cdp-endpoint connects to
1900
+ // the always-on Chromium instance started by vnc.sh on this brand's
1901
+ // VNC display + CDP_PORT (Tasks 553 + 924; per-brand). Port comes from
1902
+ // paths.ts so a freshly-installed brand's MCP shim points at its own
1903
+ // Chromium without re-bundling.
1904
+ // Spawned via `npx` (not `node`), so the spawn-tee wrapper does not apply.
1905
+ "plugin_playwright_playwright": {
1906
+ command: "npx",
1907
+ args: ["-y", "@playwright/mcp@latest", "--cdp-endpoint", `http://127.0.0.1:${CDP_PORT}`, "--caps", "pdf"]
1908
+ },
1909
+ // Graph MCP shim — spawns upstream `uvx mcp-neo4j-cypher` namespaced
1910
+ // `maxy-graph`, wraps stderr with our tee, and emits one `[graph-query]`
1911
+ // line per tool call. Credentials flow from the brand's own NEO4J_URI +
1912
+ // config/.neo4j-password so a per-brand admin session only ever sees
1913
+ // its own Neo4j instance (isolation is enforced upstream by the
1914
+ // installer's process/port boundary per MAXY-PRD.md:627, not in any
1915
+ // application-layer filter).
1916
+ //
1917
+ // Task 796: NEO4J_READ_ONLY no longer hardcoded — the shim's default
1918
+ // (NEO4J_READ_ONLY=false) lets the upstream register
1919
+ // `write_neo4j_cypher` alongside `read_neo4j_cypher`. Per-agent gating
1920
+ // via `tools:` frontmatter + ADMIN_CORE_TOOLS confines the surface;
1921
+ // only `database-operator.md` and the parent admin spawn allow-list
1922
+ // the write tool. Dev sandboxes that need read-only mode can set
1923
+ // `NEO4J_READ_ONLY=true` in the parent process env — the shim respects
1924
+ // the env var when present.
1925
+ "graph": withSpawnTee(
1926
+ "graph",
1927
+ resolve6(PLATFORM_ROOT2, "lib/graph-mcp/dist/index.js"),
1928
+ {
1929
+ ...baseEnv,
1930
+ BRAND: readBrandHostname(),
1931
+ NEO4J_USERNAME: process.env.NEO4J_USERNAME ?? process.env.NEO4J_USER ?? "neo4j",
1932
+ NEO4J_NAMESPACE: "maxy-graph",
1933
+ NEO4J_RESPONSE_TOKEN_LIMIT: "20000"
1934
+ }
1935
+ )
1936
+ };
1937
+ const tgConfig = resolveAccount()?.config?.telegram;
1938
+ const tgBotToken = tgConfig?.publicBotToken ?? tgConfig?.adminBotToken;
1939
+ if (tgBotToken) {
1940
+ servers["telegram"] = withSpawnTee(
1941
+ "telegram",
1942
+ resolve6(PLATFORM_ROOT2, "plugins/telegram/mcp/dist/index.js"),
1943
+ { ...baseEnv, TELEGRAM_BOT_TOKEN: tgBotToken }
1944
+ );
1945
+ } else {
1946
+ console.error("[plugins] telegram MCP: skipped (no bot token in account.json telegram config)");
1947
+ }
1948
+ servers["cloudflare"] = withSpawnTee(
1949
+ "cloudflare",
1950
+ resolve6(PLATFORM_ROOT2, "plugins/cloudflare/mcp/dist/index.js"),
1951
+ { ...baseEnv, PLATFORM_PORT: process.env.PORT ?? "19200" }
1952
+ );
1953
+ if (process.env.OUTLOOK_CLIENT_ID) {
1954
+ servers["outlook"] = withSpawnTee(
1955
+ "outlook",
1956
+ resolve6(PLATFORM_ROOT2, "plugins/outlook/mcp/dist/index.js"),
1957
+ {
1958
+ ...baseEnv,
1959
+ OUTLOOK_CLIENT_ID: process.env.OUTLOOK_CLIENT_ID,
1960
+ OUTLOOK_TENANT_ID: process.env.OUTLOOK_TENANT_ID ?? "common"
1961
+ }
1962
+ );
1963
+ } else {
1964
+ console.error(
1965
+ "[plugins] outlook MCP: skipped (OUTLOOK_CLIENT_ID not set; see plugins/outlook/references/auth.md)"
1966
+ );
1967
+ }
1968
+ if (Array.isArray(enabledPlugins) && enabledPlugins.length > 0) {
1969
+ for (const dir of enabledPlugins) {
1970
+ if (servers[dir]) continue;
1971
+ if (isMissingPluginDir(dir)) {
1972
+ console.error(
1973
+ `[plugins] WARN: '${dir}' in enabledPlugins but plugin directory missing \u2014 agent will have no '${dir}' tools (auto-delivery may have failed; run premium-deliver or restart the service)`
1974
+ );
1975
+ continue;
1976
+ }
1977
+ const parsed = parsePluginFrontmatter(dir);
1978
+ if (!parsed) continue;
1979
+ if (!parsed.platform.optional) continue;
1980
+ if (parsed.requires.length > 0) {
1981
+ const missing = parsed.requires.filter((req) => {
1982
+ if (enabledPlugins.includes(req)) return false;
1983
+ const reqParsed = parsePluginFrontmatter(req);
1984
+ return !reqParsed || !!reqParsed.platform.optional;
1985
+ });
1986
+ if (missing.length > 0) {
1987
+ console.warn(`[plugins] ${dir} MCP: requires [${missing.join(", ")}] not enabled \u2014 skipping`);
1988
+ continue;
1989
+ }
1990
+ }
1991
+ const mcpDir = resolve6(PLATFORM_ROOT2, "plugins", dir, "mcp");
1992
+ if (!existsSync5(mcpDir)) continue;
1993
+ const mcpEntry = resolve6(PLATFORM_ROOT2, "plugins", dir, "mcp/dist/index.js");
1994
+ if (!existsSync5(mcpEntry)) {
1995
+ console.error(
1996
+ `[plugins] WARN: '${dir}' enabled and mcp/ present but dist/index.js missing \u2014 agent will have no '${dir}' tools (rebuild required)`
1997
+ );
1998
+ continue;
1999
+ }
2000
+ servers[dir] = withSpawnTee(dir, mcpEntry, { ...baseEnv });
2001
+ console.log(`[plugins] optional MCP server started: ${dir}`);
2002
+ }
2003
+ }
2004
+ console.error(`[plugins] MCP servers for session: [${Object.keys(servers).join(", ")}] (${Object.keys(servers).length} total)`);
2005
+ console.error(`[boot] bundleMtime=${getBundleMtimeIso()} conversationId=${conversationId.slice(-8)}`);
2006
+ return servers;
2007
+ }
2008
+ var cachedBundleMtimeIso;
2009
+ function getBundleMtimeIso() {
2010
+ if (cachedBundleMtimeIso) return cachedBundleMtimeIso;
2011
+ const entry = process.argv[1];
2012
+ if (!entry) {
2013
+ cachedBundleMtimeIso = "unknown";
2014
+ return cachedBundleMtimeIso;
2015
+ }
2016
+ try {
2017
+ const stat = statSync4(entry);
2018
+ cachedBundleMtimeIso = stat.mtime.toISOString();
2019
+ } catch {
2020
+ cachedBundleMtimeIso = "unknown";
2021
+ }
2022
+ return cachedBundleMtimeIso;
2023
+ }
2024
+ var ADMIN_CORE_TOOLS = [
2025
+ "Read",
2026
+ "Write",
2027
+ "Edit",
2028
+ "Bash",
2029
+ "Glob",
2030
+ "Grep",
2031
+ "Agent",
2032
+ // Upstream mcp-neo4j-cypher (namespaced maxy-graph).
2033
+ // FastMCP joins NEO4J_NAMESPACE to each upstream tool name with a hyphen,
2034
+ // and allowedTools matching is string-exact under --permission-mode dontAsk —
2035
+ // these names must equal the init-message tool list byte-for-byte.
2036
+ //
2037
+ // Task 796 + Task 968: both read_neo4j_cypher and write_neo4j_cypher are
2038
+ // allow-listed at the parent admin spawn so the database-operator subagent
2039
+ // inherits permission. Neither is named at IDENTITY-level as a routine
2040
+ // admin tool; admin's prompt routes ANY raw cypher — read or write — to
2041
+ // the operator (per IDENTITY.md routing rule and database-operator.md's
2042
+ // first-sentence description, which is what surfaces in admin's
2043
+ // <specialist-domains> routing block via plugin-manifest.ts). Admin's
2044
+ // routine read surface is the wrapped tools (memory-search, memory-rank,
2045
+ // conversation-search, profile-read); routine writes use schema-aware
2046
+ // memory-write / memory-update / memory-find-candidates / memory-delete.
2047
+ // Raw cypher is reserved for the operator because admin's # SCHEMA block
2048
+ // carries labels and edges only — not the property dictionary — so inline
2049
+ // cypher routinely targets non-existent properties (failure mode 676504f1:
2050
+ // RETURN h.hostname instead of h.hostnameValue, with the 01N52 warning
2051
+ // dropped at the envelope).
2052
+ "mcp__graph__maxy-graph-read_neo4j_cypher",
2053
+ "mcp__graph__maxy-graph-write_neo4j_cypher",
2054
+ "mcp__graph__maxy-graph-get_neo4j_schema",
2055
+ "mcp__memory__memory-search",
2056
+ "mcp__memory__memory-rank",
2057
+ "mcp__memory__memory-write",
2058
+ "mcp__memory__memory-archive-write",
2059
+ "mcp__memory__memory-reindex",
2060
+ "mcp__memory__session-compact",
2061
+ "mcp__memory__session-compact-status",
2062
+ "mcp__memory__conversation-search",
2063
+ "mcp__contacts__contact-create",
2064
+ "mcp__contacts__contact-lookup",
2065
+ "mcp__contacts__contact-update",
2066
+ "mcp__contacts__contact-delete",
2067
+ "mcp__contacts__contact-list",
2068
+ "mcp__contacts__contact-export",
2069
+ "mcp__contacts__contact-erase",
2070
+ "mcp__contacts__group-create",
2071
+ "mcp__contacts__group-manage",
2072
+ "mcp__telegram__message",
2073
+ "mcp__telegram__message-history",
2074
+ "mcp__telegram__telegram-webhook-register",
2075
+ "mcp__whatsapp__whatsapp-login-start",
2076
+ "mcp__whatsapp__whatsapp-login-wait",
2077
+ "mcp__whatsapp__whatsapp-status",
2078
+ "mcp__whatsapp__whatsapp-disconnect",
2079
+ "mcp__whatsapp__whatsapp-send",
2080
+ "mcp__whatsapp__whatsapp-send-document",
2081
+ "mcp__whatsapp__whatsapp-config",
2082
+ "mcp__whatsapp__whatsapp-activity",
2083
+ "mcp__whatsapp__whatsapp-conversations",
2084
+ "mcp__whatsapp__whatsapp-messages",
2085
+ "mcp__whatsapp__whatsapp-conversation-graph-state",
2086
+ "mcp__whatsapp__whatsapp-group-info",
2087
+ "mcp__admin__system-status",
2088
+ "mcp__admin__public-hostname",
2089
+ "mcp__admin__remote-auth-status",
2090
+ "mcp__admin__brand-settings",
2091
+ "mcp__admin__account-manage",
2092
+ "mcp__admin__account-update",
2093
+ "mcp__admin__logs-read",
2094
+ "mcp__admin__plugin-read",
2095
+ "mcp__admin__skill-load",
2096
+ "mcp__admin__store-skill",
2097
+ "mcp__admin__render-component",
2098
+ "mcp__admin__session-reset",
2099
+ "mcp__admin__session-resume",
2100
+ "mcp__admin__admin-add",
2101
+ "mcp__admin__admin-remove",
2102
+ "mcp__admin__admin-list",
2103
+ "mcp__admin__admin-update-pin",
2104
+ "mcp__admin__anthropic-setup",
2105
+ "mcp__admin__api-key-store",
2106
+ "mcp__admin__api-key-verify",
2107
+ "mcp__admin__file-attach",
2108
+ "mcp__admin__action-pending",
2109
+ "mcp__admin__action-approve",
2110
+ "mcp__admin__action-reject",
2111
+ "mcp__admin__action-edit",
2112
+ "mcp__tasks__task-create",
2113
+ "mcp__tasks__task-update",
2114
+ "mcp__tasks__task-list",
2115
+ "mcp__tasks__task-get",
2116
+ "mcp__tasks__task-relate",
2117
+ "mcp__tasks__task-complete",
2118
+ "mcp__tasks__task-ready",
2119
+ "mcp__tasks__session-list",
2120
+ "mcp__tasks__session-name",
2121
+ "mcp__tasks__project-create",
2122
+ "mcp__tasks__project-list",
2123
+ "mcp__tasks__project-get",
2124
+ "mcp__tasks__project-update",
2125
+ "mcp__tasks__project-complete",
2126
+ "mcp__scheduling__schedule-event",
2127
+ "mcp__scheduling__schedule-list",
2128
+ "mcp__scheduling__schedule-get",
2129
+ "mcp__scheduling__schedule-update",
2130
+ "mcp__scheduling__schedule-cancel",
2131
+ "mcp__scheduling__schedule-export-ics",
2132
+ "mcp__scheduling__schedule-import-ics",
2133
+ "mcp__scheduling__time-resolve",
2134
+ "mcp__admin__qr-generate",
2135
+ "mcp__admin__wifi",
2136
+ "mcp__email__email-setup",
2137
+ "mcp__email__email-read",
2138
+ "mcp__email__email-send",
2139
+ "mcp__email__email-reply",
2140
+ "mcp__email__email-search",
2141
+ "mcp__email__email-graph-query",
2142
+ "mcp__email__email-otp-extract",
2143
+ "mcp__email__email-status",
2144
+ "mcp__email__email-auto-respond-config",
2145
+ "mcp__outlook__outlook-account-register",
2146
+ "mcp__outlook__outlook-mail-list",
2147
+ "mcp__outlook__outlook-mail-search",
2148
+ "mcp__outlook__outlook-calendar-list",
2149
+ "mcp__outlook__outlook-calendar-event",
2150
+ "mcp__outlook__outlook-contacts-list",
2151
+ "mcp__outlook__outlook-mailbox-info",
2152
+ "mcp__workflows__workflow-create",
2153
+ "mcp__workflows__workflow-list",
2154
+ "mcp__workflows__workflow-get",
2155
+ "mcp__workflows__workflow-update",
2156
+ "mcp__workflows__workflow-delete",
2157
+ "mcp__workflows__workflow-validate",
2158
+ "mcp__workflows__workflow-execute",
2159
+ "mcp__workflows__workflow-runs",
2160
+ "WebSearch",
2161
+ "WebFetch"
2162
+ ];
2163
+ var ADMIN_EAGER_TOOLS = [
2164
+ // Graph (eagerness minted by graph-mcp shim's tools/list interceptor).
2165
+ "mcp__graph__maxy-graph-read_neo4j_cypher",
2166
+ "mcp__graph__maxy-graph-write_neo4j_cypher",
2167
+ "mcp__graph__maxy-graph-get_neo4j_schema",
2168
+ // Memory.
2169
+ "mcp__memory__memory-search",
2170
+ "mcp__memory__memory-rank",
2171
+ "mcp__memory__memory-write",
2172
+ "mcp__memory__memory-archive-write",
2173
+ "mcp__memory__memory-reindex",
2174
+ "mcp__memory__session-compact",
2175
+ "mcp__memory__session-compact-status",
2176
+ "mcp__memory__conversation-search",
2177
+ // Contacts.
2178
+ "mcp__contacts__contact-create",
2179
+ "mcp__contacts__contact-lookup",
2180
+ "mcp__contacts__contact-update",
2181
+ "mcp__contacts__contact-delete",
2182
+ "mcp__contacts__contact-list",
2183
+ "mcp__contacts__contact-export",
2184
+ "mcp__contacts__contact-erase",
2185
+ "mcp__contacts__group-create",
2186
+ "mcp__contacts__group-manage",
2187
+ // Telegram.
2188
+ "mcp__telegram__message",
2189
+ "mcp__telegram__message-history",
2190
+ "mcp__telegram__telegram-webhook-register",
2191
+ // WhatsApp.
2192
+ "mcp__whatsapp__whatsapp-login-start",
2193
+ "mcp__whatsapp__whatsapp-login-wait",
2194
+ "mcp__whatsapp__whatsapp-status",
2195
+ "mcp__whatsapp__whatsapp-disconnect",
2196
+ "mcp__whatsapp__whatsapp-send",
2197
+ "mcp__whatsapp__whatsapp-send-document",
2198
+ "mcp__whatsapp__whatsapp-config",
2199
+ "mcp__whatsapp__whatsapp-activity",
2200
+ "mcp__whatsapp__whatsapp-conversations",
2201
+ "mcp__whatsapp__whatsapp-messages",
2202
+ "mcp__whatsapp__whatsapp-conversation-graph-state",
2203
+ "mcp__whatsapp__whatsapp-group-info",
2204
+ // Admin.
2205
+ "mcp__admin__system-status",
2206
+ "mcp__admin__public-hostname",
2207
+ "mcp__admin__remote-auth-status",
2208
+ "mcp__admin__brand-settings",
2209
+ "mcp__admin__account-manage",
2210
+ "mcp__admin__account-update",
2211
+ "mcp__admin__logs-read",
2212
+ "mcp__admin__plugin-read",
2213
+ "mcp__admin__skill-load",
2214
+ "mcp__admin__store-skill",
2215
+ "mcp__admin__render-component",
2216
+ "mcp__admin__session-reset",
2217
+ "mcp__admin__session-resume",
2218
+ "mcp__admin__admin-add",
2219
+ "mcp__admin__admin-remove",
2220
+ "mcp__admin__admin-list",
2221
+ "mcp__admin__admin-update-pin",
2222
+ "mcp__admin__anthropic-setup",
2223
+ "mcp__admin__api-key-store",
2224
+ "mcp__admin__api-key-verify",
2225
+ "mcp__admin__file-attach",
2226
+ "mcp__admin__action-pending",
2227
+ "mcp__admin__action-approve",
2228
+ "mcp__admin__action-reject",
2229
+ "mcp__admin__action-edit",
2230
+ "mcp__admin__qr-generate",
2231
+ "mcp__admin__wifi",
2232
+ // Tasks.
2233
+ "mcp__tasks__task-create",
2234
+ "mcp__tasks__task-update",
2235
+ "mcp__tasks__task-list",
2236
+ "mcp__tasks__task-get",
2237
+ "mcp__tasks__task-relate",
2238
+ "mcp__tasks__task-complete",
2239
+ "mcp__tasks__task-ready",
2240
+ "mcp__tasks__session-list",
2241
+ "mcp__tasks__session-name",
2242
+ "mcp__tasks__project-create",
2243
+ "mcp__tasks__project-list",
2244
+ "mcp__tasks__project-get",
2245
+ "mcp__tasks__project-update",
2246
+ "mcp__tasks__project-complete",
2247
+ // Scheduling.
2248
+ "mcp__scheduling__schedule-event",
2249
+ "mcp__scheduling__schedule-list",
2250
+ "mcp__scheduling__schedule-get",
2251
+ "mcp__scheduling__schedule-update",
2252
+ "mcp__scheduling__schedule-cancel",
2253
+ "mcp__scheduling__schedule-export-ics",
2254
+ "mcp__scheduling__schedule-import-ics",
2255
+ "mcp__scheduling__time-resolve",
2256
+ // Email.
2257
+ "mcp__email__email-setup",
2258
+ "mcp__email__email-read",
2259
+ "mcp__email__email-send",
2260
+ "mcp__email__email-reply",
2261
+ "mcp__email__email-search",
2262
+ "mcp__email__email-graph-query",
2263
+ "mcp__email__email-otp-extract",
2264
+ "mcp__email__email-status",
2265
+ "mcp__email__email-auto-respond-config",
2266
+ // Outlook.
2267
+ "mcp__outlook__outlook-account-register",
2268
+ "mcp__outlook__outlook-mail-list",
2269
+ "mcp__outlook__outlook-mail-search",
2270
+ "mcp__outlook__outlook-calendar-list",
2271
+ "mcp__outlook__outlook-calendar-event",
2272
+ "mcp__outlook__outlook-contacts-list",
2273
+ "mcp__outlook__outlook-mailbox-info",
2274
+ // Workflows.
2275
+ "mcp__workflows__workflow-create",
2276
+ "mcp__workflows__workflow-list",
2277
+ "mcp__workflows__workflow-get",
2278
+ "mcp__workflows__workflow-update",
2279
+ "mcp__workflows__workflow-delete",
2280
+ "mcp__workflows__workflow-validate",
2281
+ "mcp__workflows__workflow-execute",
2282
+ "mcp__workflows__workflow-runs"
2283
+ ];
2284
+ function logToolSurface(conversationId, allowedTools) {
2285
+ const allowedSet = new Set(allowedTools);
2286
+ const eagerIntent = ADMIN_EAGER_TOOLS.filter((name) => allowedSet.has(name)).length;
2287
+ const eagerMissing = ADMIN_EAGER_TOOLS.filter((name) => !allowedSet.has(name));
2288
+ const convTag = conversationId ? conversationId.slice(0, 8) : "unknown";
2289
+ console.error(
2290
+ `[tool-surface] session=${convTag} permission_allowed=${allowedTools.length} eager_intent=${eagerIntent} eager_set_size=${ADMIN_EAGER_TOOLS.length}${eagerMissing.length > 0 ? ` missing_from_allowlist=[${eagerMissing.slice(0, 5).join(",")}${eagerMissing.length > 5 ? `,+${eagerMissing.length - 5}` : ""}]` : ""}`
2291
+ );
2292
+ }
2293
+ function getAdminAllowedTools(enabledPlugins) {
2294
+ const tools = [...ADMIN_CORE_TOOLS];
2295
+ if (Array.isArray(enabledPlugins) && enabledPlugins.length > 0) {
2296
+ const pluginsDir = resolve6(PLATFORM_ROOT2, "plugins");
2297
+ let dirs;
2298
+ try {
2299
+ dirs = readdirSync4(pluginsDir);
2300
+ } catch {
2301
+ return tools;
2302
+ }
2303
+ for (const dir of dirs) {
2304
+ if (!enabledPlugins.includes(dir)) continue;
2305
+ const parsed = parsePluginFrontmatter(dir);
2306
+ if (!parsed?.platform.optional) continue;
2307
+ for (const toolName of parsed.tools) {
2308
+ tools.push(`mcp__${dir}__${toolName}`);
2309
+ }
2310
+ }
2311
+ }
2312
+ return tools;
2313
+ }
2314
+ function assembleAllowedToolsForAdminSpawn(enabledPlugins) {
2315
+ return getAdminAllowedTools(enabledPlugins);
2316
+ }
2317
+
2318
+ // app/lib/claude-agent/stream-log-writer.ts
2319
+ var FILENAME_BYTE_BUDGET = 240;
2320
+ var PRE_TOKEN_BUFFER_CAP_BYTES = 64 * 1024;
2321
+ var SESSIONKEY_REGEX = /^(sk_[a-f0-9]{16}|[a-zA-Z0-9_-]{1,64})$/;
2322
+ var handles = /* @__PURE__ */ new Map();
2323
+ function getStreamLogHandle(accountDir, sessionKey) {
2324
+ if (!sessionKey) {
2325
+ throw new Error(`getStreamLogHandle: sessionKey is required (accountDir=${accountDir})`);
2326
+ }
2327
+ const cached = handles.get(sessionKey);
2328
+ if (cached) return cached;
2329
+ if (!SESSIONKEY_REGEX.test(sessionKey)) {
2330
+ console.error(
2331
+ `[stream-log-writer] reject reason=invalid-sessionkey sessionKey=${JSON.stringify(sessionKey.slice(0, 64))}`
2332
+ );
2333
+ return makeNoOpHandle(buildPath(accountDir, sessionKey));
2334
+ }
2335
+ const path = buildPath(accountDir, sessionKey);
2336
+ const filenameBytes = Buffer.byteLength(`claude-agent-stream-${sessionKey}.log`);
2337
+ if (filenameBytes > FILENAME_BYTE_BUDGET) {
2338
+ console.error(
2339
+ `[stream-log-writer] reject reason=key-too-long sessionKey-bytes=${Buffer.byteLength(sessionKey)} filename-bytes=${filenameBytes}`
2340
+ );
2341
+ return makeNoOpHandle(path);
2342
+ }
2343
+ const handle = makeHandle(path, sessionKey);
2344
+ handles.set(sessionKey, handle);
2345
+ return handle;
2346
+ }
2347
+ function releaseStreamLogHandle(sessionKey) {
2348
+ const h = handles.get(sessionKey);
2349
+ if (!h) return;
2350
+ h.end();
2351
+ handles.delete(sessionKey);
2352
+ }
2353
+ function buildPath(accountDir, sessionKey) {
2354
+ return resolve7(accountDir, "logs", `claude-agent-stream-${sessionKey}.log`);
2355
+ }
2356
+ function makeHandle(path, sessionKey) {
2357
+ let stream = null;
2358
+ let firstTokenReceived = false;
2359
+ let buffer = [];
2360
+ let bufferBytes = 0;
2361
+ let bufferBoundHitOnce = false;
2362
+ let pendingErrorListeners = [];
2363
+ function ensureLogDir() {
2364
+ try {
2365
+ mkdirSync3(resolve7(path, ".."), { recursive: true });
2366
+ return true;
2367
+ } catch (err) {
2368
+ console.error(
2369
+ `[stream-log-writer] mkdir-failed sessionKey=${sessionKey.slice(0, 8)} path=${JSON.stringify(path)} reason=${err instanceof Error ? err.message : String(err)}`
2370
+ );
2371
+ return false;
2372
+ }
2373
+ }
2374
+ function openStream(firstTokenBytesLen) {
2375
+ if (stream) return;
2376
+ if (!ensureLogDir()) {
2377
+ stream = createWriteStream2(devNull2, { flags: "a" });
2378
+ stream.once("error", () => {
2379
+ });
2380
+ return;
2381
+ }
2382
+ stream = createWriteStream2(path, { flags: "a" });
2383
+ stream.once("error", (err) => {
2384
+ console.error(
2385
+ `[stream-log-writer] writer-bind-failed sessionKey=${sessionKey.slice(0, 8)} errno=${err.code ?? "unknown"} path=${JSON.stringify(path)}`
2386
+ );
2387
+ handles.delete(sessionKey);
2388
+ });
2389
+ for (const cb of pendingErrorListeners) {
2390
+ stream.on("error", cb);
2391
+ }
2392
+ pendingErrorListeners = [];
2393
+ registerStreamLog(stream, { path, sessionKey, name: "claude-agent-stream" });
2394
+ console.error(
2395
+ `[stream-log-writer] open sessionKey=${sessionKey.slice(0, 8)} path=${JSON.stringify(path)} firstTokenBytes=${firstTokenBytesLen} ts=${(/* @__PURE__ */ new Date()).toISOString()}`
2396
+ );
2397
+ }
2398
+ function flushBuffer() {
2399
+ if (!stream || buffer.length === 0) return { count: 0, bytes: 0 };
2400
+ const count = buffer.length;
2401
+ const bytes = bufferBytes;
2402
+ for (const line of buffer) {
2403
+ stream.write(line);
2404
+ }
2405
+ buffer = [];
2406
+ bufferBytes = 0;
2407
+ return { count, bytes };
2408
+ }
2409
+ return {
2410
+ get path() {
2411
+ return path;
2412
+ },
2413
+ get firstTokenReceived() {
2414
+ return firstTokenReceived;
2415
+ },
2416
+ get destroyed() {
2417
+ return stream?.destroyed ?? false;
2418
+ },
2419
+ get writableEnded() {
2420
+ return stream?.writableEnded ?? false;
2421
+ },
2422
+ write(bytes) {
2423
+ if (firstTokenReceived && stream && !stream.destroyed && !stream.writableEnded) {
2424
+ stream.write(bytes);
2425
+ return;
2426
+ }
2427
+ const len = Buffer.byteLength(bytes);
2428
+ if (bufferBytes + len > PRE_TOKEN_BUFFER_CAP_BYTES) {
2429
+ while (buffer.length > 0 && bufferBytes + len > PRE_TOKEN_BUFFER_CAP_BYTES) {
2430
+ const dropped = buffer.shift();
2431
+ bufferBytes -= Buffer.byteLength(dropped);
2432
+ }
2433
+ if (!bufferBoundHitOnce) {
2434
+ bufferBoundHitOnce = true;
2435
+ console.error(
2436
+ `[stream-log-writer] buffer-bound-hit sessionKey=${sessionKey.slice(0, 8)} cap=${PRE_TOKEN_BUFFER_CAP_BYTES}`
2437
+ );
2438
+ }
2439
+ }
2440
+ buffer.push(bytes);
2441
+ bufferBytes += len;
2442
+ const trimmed = bytes.endsWith("\n") ? bytes.slice(0, -1) : bytes;
2443
+ console.error(`[stream-log-writer:pretoken] sessionKey=${sessionKey.slice(0, 8)} ${trimmed}`);
2444
+ },
2445
+ writeToken(bytes) {
2446
+ if (!firstTokenReceived) {
2447
+ const firstBytesLen = Buffer.byteLength(bytes);
2448
+ openStream(firstBytesLen);
2449
+ firstTokenReceived = true;
2450
+ const flush = flushBuffer();
2451
+ if (flush.count > 0) {
2452
+ console.error(
2453
+ `[stream-log-writer] buffer-flush sessionKey=${sessionKey.slice(0, 8)} bytes=${flush.bytes} count=${flush.count} firstTokenBytes=${firstBytesLen}`
2454
+ );
2455
+ }
2456
+ }
2457
+ if (stream && !stream.destroyed && !stream.writableEnded) {
2458
+ stream.write(bytes);
2459
+ }
2460
+ },
2461
+ end() {
2462
+ if (stream && !stream.writableEnded) {
2463
+ try {
2464
+ stream.end();
2465
+ } catch {
2466
+ }
2467
+ }
2468
+ buffer = [];
2469
+ bufferBytes = 0;
2470
+ },
2471
+ on(event, cb) {
2472
+ if (stream) {
2473
+ stream.on(event, cb);
2474
+ } else {
2475
+ pendingErrorListeners.push(cb);
2476
+ }
2477
+ }
2478
+ };
2479
+ }
2480
+ function makeNoOpHandle(path) {
2481
+ return {
2482
+ get path() {
2483
+ return path;
2484
+ },
2485
+ get firstTokenReceived() {
2486
+ return false;
2487
+ },
2488
+ get destroyed() {
2489
+ return false;
2490
+ },
2491
+ get writableEnded() {
2492
+ return false;
2493
+ },
2494
+ write(_bytes) {
2495
+ },
2496
+ writeToken(_bytes) {
2497
+ },
2498
+ end() {
2499
+ },
2500
+ on(_event, _cb) {
2501
+ }
2502
+ };
2503
+ }
2504
+ function appendStreamLogLine(accountId, sessionKey, line) {
2505
+ if (!accountId || !sessionKey) {
2506
+ console.error(`[stream-log-writer] reject reason=missing-identifier accountId=${accountId ? "set" : "empty"} sessionKey=${sessionKey ? "set" : "empty"}`);
2507
+ return;
2508
+ }
2509
+ let streamLogPath;
2510
+ try {
2511
+ streamLogPath = streamLogPathFor(accountId, sessionKey).streamLogPath;
2512
+ } catch (err) {
2513
+ console.error(`[stream-log-writer] reject reason=path-resolve-failed err=${err instanceof Error ? err.message : String(err)} accountId=${accountId.slice(0, 8)} sessionKey=${sessionKey.slice(0, 12)}`);
2514
+ return;
2515
+ }
2516
+ try {
2517
+ mkdirSync3(dirname(streamLogPath), { recursive: true });
2518
+ appendFileSyncForAdminTelemetry(streamLogPath, line.endsWith("\n") ? line : `${line}
2519
+ `);
2520
+ } catch (err) {
2521
+ console.error(`[stream-log-writer] append-failed err=${err instanceof Error ? err.message : String(err)} path=${streamLogPath}`);
2522
+ }
2523
+ }
2524
+
2525
+ // app/lib/claude-agent/session-store.ts
2526
+ function findFirstSubstantiveUserMessage(turns) {
2527
+ for (const t of turns) {
2528
+ if (t.role !== "user") continue;
2529
+ if (isMessageUseful(t.content)) return t.content;
2530
+ }
2531
+ return null;
2532
+ }
2533
+ var sessionStore = /* @__PURE__ */ new Map();
2534
+ function getSession2(cacheKey) {
2535
+ return sessionStore.get(cacheKey);
2536
+ }
2537
+ setSessionStoreRef(sessionStore);
2538
+ var TOKEN_PREFIX = "v1.";
2539
+ var TOKEN_VERSION = "adm";
2540
+ var TOKEN_TTL_MS = 24 * 60 * 60 * 1e3;
2541
+ var TOKEN_SECRET_BYTES = 32;
2542
+ var TOKEN_NONCE_BYTES = 16;
2543
+ var cachedAdminSecret = null;
2544
+ function getAdminSessionSecret() {
2545
+ if (cachedAdminSecret) return cachedAdminSecret;
2546
+ try {
2547
+ const hex2 = readFileSync7(ADMIN_SESSION_SECRET_FILE, "utf-8").trim();
2548
+ if (hex2.length === TOKEN_SECRET_BYTES * 2) {
2549
+ cachedAdminSecret = Buffer.from(hex2, "hex");
2550
+ return cachedAdminSecret;
2551
+ }
2552
+ } catch {
2553
+ }
2554
+ const fresh = randomBytes(TOKEN_SECRET_BYTES).toString("hex");
2555
+ try {
2556
+ mkdirSync4(dirname2(ADMIN_SESSION_SECRET_FILE), { recursive: true, mode: 448 });
2557
+ writeFileSync2(ADMIN_SESSION_SECRET_FILE, fresh, { mode: 384, flag: "wx" });
2558
+ } catch {
2559
+ }
2560
+ let hex;
2561
+ try {
2562
+ hex = readFileSync7(ADMIN_SESSION_SECRET_FILE, "utf-8").trim();
2563
+ } catch (err) {
2564
+ const msg = err instanceof Error ? err.message : String(err);
2565
+ throw new Error(`admin-session-secret unreadable at ${ADMIN_SESSION_SECRET_FILE}: ${msg}`);
2566
+ }
2567
+ cachedAdminSecret = Buffer.from(hex, "hex");
2568
+ return cachedAdminSecret;
2569
+ }
2570
+ function base64urlEncode(buf) {
2571
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2572
+ }
2573
+ function base64urlDecode(s) {
2574
+ const padded = s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - s.length % 4) % 4);
2575
+ return Buffer.from(padded, "base64");
2576
+ }
2577
+ function signDigest(payloadJson, secret) {
2578
+ return createHmac("sha256", secret).update(payloadJson).digest();
2579
+ }
2580
+ function fingerprintSessionKey(raw) {
2581
+ if (typeof raw !== "string" || !raw) return raw;
2582
+ if (/^sk_[a-f0-9]{16}$/.test(raw)) return raw;
2583
+ if (!raw.startsWith("v1.")) return raw;
2584
+ return `sk_${createHash("sha256").update(raw).digest("hex").slice(0, 16)}`;
2585
+ }
2586
+ function mintAdminSessionToken(identity) {
2587
+ const payload = {
2588
+ v: TOKEN_VERSION,
2589
+ a: identity.accountId,
2590
+ u: identity.userId,
2591
+ c: Date.now(),
2592
+ n: randomBytes(TOKEN_NONCE_BYTES).toString("hex")
2593
+ };
2594
+ const payloadJson = JSON.stringify(payload);
2595
+ const payloadB64 = base64urlEncode(Buffer.from(payloadJson, "utf-8"));
2596
+ const sigB64 = base64urlEncode(signDigest(payloadJson, getAdminSessionSecret()));
2597
+ return `${TOKEN_PREFIX}${payloadB64}.${sigB64}`;
2598
+ }
2599
+ function parseAdminSessionToken(token) {
2600
+ if (!token.startsWith(TOKEN_PREFIX)) return null;
2601
+ const rest = token.slice(TOKEN_PREFIX.length);
2602
+ const dot = rest.indexOf(".");
2603
+ if (dot === -1) return null;
2604
+ const payloadB64 = rest.slice(0, dot);
2605
+ const sigB64 = rest.slice(dot + 1);
2606
+ if (!payloadB64 || !sigB64) return null;
2607
+ let payloadJson;
2608
+ let providedDigest;
2609
+ try {
2610
+ payloadJson = base64urlDecode(payloadB64).toString("utf-8");
2611
+ providedDigest = base64urlDecode(sigB64);
2612
+ } catch {
2613
+ return null;
2614
+ }
2615
+ const expectedDigest = signDigest(payloadJson, getAdminSessionSecret());
2616
+ if (providedDigest.length !== expectedDigest.length || !timingSafeEqual(providedDigest, expectedDigest)) {
2617
+ return null;
2618
+ }
2619
+ let parsed;
2620
+ try {
2621
+ parsed = JSON.parse(payloadJson);
2622
+ } catch {
2623
+ return null;
2624
+ }
2625
+ if (!parsed || typeof parsed !== "object") return null;
2626
+ const p = parsed;
2627
+ if (p.v !== TOKEN_VERSION) return null;
2628
+ if (typeof p.a !== "string" || !p.a) return null;
2629
+ if (typeof p.u !== "string" || !p.u) return null;
2630
+ if (typeof p.c !== "number" || !Number.isFinite(p.c)) return null;
2631
+ if (typeof p.n !== "string" || !p.n) return null;
2632
+ return p;
2633
+ }
2634
+ function tryRehydrateAdminSession(cacheKey, signedToken) {
2635
+ const payload = parseAdminSessionToken(signedToken);
2636
+ if (!payload) return { kind: "invalid-token" };
2637
+ const ageMs = Date.now() - payload.c;
2638
+ if (ageMs > TOKEN_TTL_MS) return { kind: "expired", ageMs };
2639
+ sessionStore.set(cacheKey, {
2640
+ createdAt: Date.now(),
2641
+ agentType: "admin",
2642
+ accountId: payload.a,
2643
+ userId: payload.u,
2644
+ wantsPriorConversation: true
2645
+ });
2646
+ console.log(`[session-rehydrate-from-token] cacheKey=${cacheKey.slice(0, 12)}\u2026 accountId=${payload.a.slice(0, 8)} userId=${payload.u.slice(0, 8)} ageMs=${ageMs}`);
2647
+ return { kind: "ok", payload, ageMs };
2648
+ }
2649
+ function setWantsPriorConversation(cacheKey) {
2650
+ const session = sessionStore.get(cacheKey);
2651
+ if (session) session.wantsPriorConversation = true;
2652
+ }
2653
+ function consumeWantsPriorConversation(cacheKey) {
2654
+ const session = sessionStore.get(cacheKey);
2655
+ if (!session || !session.wantsPriorConversation) return false;
2656
+ delete session.wantsPriorConversation;
2657
+ return true;
2658
+ }
2659
+ function registerSession(cacheKey, agentType, accountId, agentName, userId, userName, role) {
2660
+ const existing = sessionStore.get(cacheKey);
2661
+ if (existing) {
2662
+ existing.agentType = agentType;
2663
+ existing.accountId = accountId;
2664
+ existing.agentName = agentName ?? existing.agentName;
2665
+ existing.userId = userId ?? existing.userId;
2666
+ existing.userName = userName ?? existing.userName;
2667
+ existing.role = role ?? existing.role;
2668
+ return;
2669
+ }
2670
+ sessionStore.set(cacheKey, { createdAt: Date.now(), agentType, accountId, agentName, userId, userName, role });
2671
+ }
2672
+ function registerResumedSession(cacheKey, accountId, agentName, conversationId, messages) {
2673
+ const messageHistory = messages.map((m) => ({
2674
+ role: m.role,
2675
+ content: m.content,
2676
+ timestamp: m.timestamp ?? Date.now()
2677
+ }));
2678
+ sessionStore.set(cacheKey, {
2679
+ createdAt: Date.now(),
2680
+ agentType: "public",
2681
+ accountId,
2682
+ agentName,
2683
+ conversationId,
2684
+ messageHistory
2685
+ });
2686
+ }
2687
+ function getSessionMessages(cacheKey) {
2688
+ return sessionStore.get(cacheKey)?.messageHistory;
2689
+ }
2690
+ function clearSessionHistory(cacheKey) {
2691
+ const session = sessionStore.get(cacheKey);
2692
+ if (!session) return void 0;
2693
+ const previousConversationId = session.conversationId;
2694
+ session.agentSessionId = void 0;
2695
+ session.pendingCompactionSummary = void 0;
2696
+ session.stalledSubagents = void 0;
2697
+ session.pendingTrimmedMessages = void 0;
2698
+ session.pendingCommitmentOffers = void 0;
2699
+ session.pendingTurns = void 0;
2700
+ session.stallResume = void 0;
2701
+ session.assistantTurnCount = void 0;
2702
+ return previousConversationId;
2703
+ }
2704
+ function bufferPendingTurn(cacheKey, turn) {
2705
+ const session = sessionStore.get(cacheKey);
2706
+ if (!session) {
2707
+ console.error(`[conversation-gate] bufferPendingTurn: session not found cacheKey=${cacheKey.slice(0, 8)}\u2026`);
2708
+ return;
2709
+ }
2710
+ if (!session.pendingTurns) session.pendingTurns = [];
2711
+ session.pendingTurns.push(turn);
2712
+ console.log(`[conversation-gate] ${(/* @__PURE__ */ new Date()).toISOString()} buffered cacheKey=${cacheKey.slice(0, 8)} role=${turn.role} turnCount=${session.pendingTurns.filter((t) => t.role === "user").length}`);
2713
+ }
2714
+ function getPendingTurnCount(cacheKey) {
2715
+ const buf = sessionStore.get(cacheKey)?.pendingTurns;
2716
+ if (!buf) return 0;
2717
+ let n = 0;
2718
+ for (const t of buf) if (t.role === "user") n++;
2719
+ return n;
2720
+ }
2721
+ function drainPendingTurns(cacheKey) {
2722
+ const session = sessionStore.get(cacheKey);
2723
+ if (!session?.pendingTurns || session.pendingTurns.length === 0) return void 0;
2724
+ const drained = session.pendingTurns;
2725
+ session.pendingTurns = void 0;
2726
+ return drained;
2727
+ }
2728
+ function isFlushError(o) {
2729
+ return o !== null && "error" in o;
2730
+ }
2731
+ async function maybeFlushConversationBuffer(cacheKey, agentType, accountId) {
2732
+ const sk8 = cacheKey.slice(0, 8);
2733
+ const session = sessionStore.get(cacheKey);
2734
+ if (!session) {
2735
+ console.log(`[admin/conversation-flush] cacheKey=${sk8} agentType=${agentType} result=missing-session`);
2736
+ return { error: "missing-session" };
2737
+ }
2738
+ if (session.conversationId) {
2739
+ console.log(`[admin/conversation-flush] cacheKey=${sk8} agentType=${agentType} result=already-flushed conversationId=${session.conversationId.slice(0, 8)}`);
2740
+ return { conversationId: session.conversationId, buffered: [] };
2741
+ }
2742
+ const bufferedCount = session.pendingTurns?.length ?? 0;
2743
+ if (bufferedCount === 0) {
2744
+ console.log(`[admin/conversation-flush] cacheKey=${sk8} agentType=${agentType} result=empty-buffer`);
2745
+ return null;
2746
+ }
2747
+ if (session.flushInFlight) return session.flushInFlight;
2748
+ const attempt = (async () => {
2749
+ let conversationId = null;
2750
+ if (agentType === "admin") {
2751
+ const userId = session.userId;
2752
+ if (!userId) {
2753
+ console.error(`[admin/conversation-flush] cacheKey=${sk8} agentType=admin result=missing-userId bufferedCount=${bufferedCount}`);
2754
+ return { error: "missing-userId" };
2755
+ }
2756
+ conversationId = await createNewAdminConversation(userId, accountId, cacheKey, session.userName);
2757
+ } else {
2758
+ conversationId = (await ensureConversation(accountId, "public", cacheKey, void 0, session.agentName, void 0)).conversationId;
2759
+ }
2760
+ if (!conversationId) {
2761
+ console.error(`[admin/conversation-flush] cacheKey=${sk8} agentType=${agentType} result=writer-failed bufferedCount=${bufferedCount}`);
2762
+ return { error: "writer-failed" };
2763
+ }
2764
+ session.conversationId = conversationId;
2765
+ if (agentType === "admin" && session.agentSessionId) {
2766
+ setConversationAgentSessionId(conversationId, session.agentSessionId).catch(() => {
2767
+ });
2768
+ }
2769
+ const buffered = drainPendingTurns(cacheKey) ?? [];
2770
+ for (const turn of buffered) {
2771
+ persistMessage(conversationId, turn.role, turn.content, accountId, turn.tokens, turn.timestamp, turn.sender, turn.components, turn.attachments).catch((err) => {
2772
+ console.error(`[admin/conversation-flush] replay persistMessage failed role=${turn.role}: ${err instanceof Error ? err.message : String(err)}`);
2773
+ });
2774
+ }
2775
+ console.log(`[admin/conversation-flush] cacheKey=${sk8} agentType=${agentType} result=ok conversationId=${conversationId.slice(0, 8)} bufferedMessages=${buffered.length}`);
2776
+ return { conversationId, buffered };
2777
+ })();
2778
+ session.flushInFlight = attempt;
2779
+ try {
2780
+ return await attempt;
2781
+ } finally {
2782
+ if (session.flushInFlight === attempt) session.flushInFlight = void 0;
2783
+ }
2784
+ }
2785
+ function isDmChannelCacheKey(cacheKey) {
2786
+ return cacheKey.startsWith("whatsapp:") || cacheKey.startsWith("telegram:");
2787
+ }
2788
+ function getAgentNameForSession(cacheKey) {
2789
+ return sessionStore.get(cacheKey)?.agentName;
2790
+ }
2791
+ function listAdminSessionsInProgress(accountId, userId) {
2792
+ const rows = [];
2793
+ for (const [cacheKey, session] of Array.from(sessionStore.entries())) {
2794
+ if (session.agentType !== "admin") continue;
2795
+ if (session.accountId !== accountId) continue;
2796
+ if (session.userId !== userId) continue;
2797
+ if (session.conversationId) continue;
2798
+ rows.push({ cacheKey, createdAt: session.createdAt });
2799
+ }
2800
+ rows.sort((a, b) => b.createdAt - a.createdAt);
2801
+ return rows;
2802
+ }
2803
+ function validateSession(cacheKey, agentType, signedSessionToken) {
2804
+ const session = sessionStore.get(cacheKey);
2805
+ if (!session) {
2806
+ if (agentType === "admin" && signedSessionToken) {
2807
+ const outcome = tryRehydrateAdminSession(cacheKey, signedSessionToken);
2808
+ if (outcome.kind === "ok") return { ok: true };
2809
+ if (outcome.kind === "expired") return { ok: false, reason: "session-expired-age" };
2810
+ }
2811
+ return { ok: false, reason: "session-not-registered" };
2812
+ }
2813
+ if (session.agentType !== agentType) return { ok: false, reason: "agent-type-mismatch" };
2814
+ if (Date.now() - session.createdAt > 24 * 60 * 60 * 1e3) {
2815
+ sessionStore.delete(cacheKey);
2816
+ return { ok: false, reason: "session-expired-age" };
2817
+ }
2818
+ if (session.grantExpiresAt && Date.now() > session.grantExpiresAt) {
2819
+ sessionStore.delete(cacheKey);
2820
+ return { ok: false, reason: "grant-expired" };
2821
+ }
2822
+ return { ok: true };
2823
+ }
2824
+ function storeAgentSessionId(cacheKey, agentSessionId) {
2825
+ const session = sessionStore.get(cacheKey);
2826
+ if (session) {
2827
+ session.agentSessionId = agentSessionId;
2828
+ console.error(`[session-store] storeAgentSessionId cacheKey=${cacheKey.slice(0, 12)}\u2026 sessionId=${agentSessionId.slice(0, 8)}\u2026`);
2829
+ if (session.agentType === "admin" && session.conversationId) {
2830
+ setConversationAgentSessionId(session.conversationId, agentSessionId).catch(() => {
2831
+ });
2832
+ }
2833
+ } else {
2834
+ console.error(`[session-store] storeAgentSessionId SKIPPED \u2014 no session entry cacheKey=${cacheKey.slice(0, 12)}\u2026 sessionId=${agentSessionId.slice(0, 8)}\u2026`);
2835
+ }
2836
+ }
2837
+ function getAgentSessionId(cacheKey) {
2838
+ return sessionStore.get(cacheKey)?.agentSessionId;
2839
+ }
2840
+ function setAgentSessionId(cacheKey, agentSessionId) {
2841
+ const session = sessionStore.get(cacheKey);
2842
+ if (!session) return false;
2843
+ session.agentSessionId = agentSessionId;
2844
+ return true;
2845
+ }
2846
+ function storePendingCompactionSummary(cacheKey, summary) {
2847
+ const session = sessionStore.get(cacheKey);
2848
+ if (session) session.pendingCompactionSummary = summary;
2849
+ }
2850
+ function consumePendingCompactionSummary(cacheKey) {
2851
+ const session = sessionStore.get(cacheKey);
2852
+ if (!session) return void 0;
2853
+ const summary = session.pendingCompactionSummary;
2854
+ delete session.pendingCompactionSummary;
2855
+ return summary;
2856
+ }
2857
+ function getAccountIdForSession(cacheKey) {
2858
+ return sessionStore.get(cacheKey)?.accountId;
2859
+ }
2860
+ function getUserIdForSession(cacheKey) {
2861
+ return sessionStore.get(cacheKey)?.userId;
2862
+ }
2863
+ function getUserNameForSession(cacheKey) {
2864
+ return sessionStore.get(cacheKey)?.userName;
2865
+ }
2866
+ function getRoleForSession(cacheKey) {
2867
+ return sessionStore.get(cacheKey)?.role;
2868
+ }
2869
+ function getConversationIdForSession(cacheKey) {
2870
+ return sessionStore.get(cacheKey)?.conversationId;
2871
+ }
2872
+ function getSessionKeyByConversationId(conversationId) {
2873
+ for (const [cacheKey, session] of sessionStore.entries()) {
2874
+ if (session.conversationId === conversationId) return cacheKey;
2875
+ }
2876
+ return void 0;
2877
+ }
2878
+ function setConversationIdForSession(cacheKey, conversationId) {
2879
+ const session = sessionStore.get(cacheKey);
2880
+ if (!session) return false;
2881
+ session.conversationId = conversationId;
2882
+ return true;
2883
+ }
2884
+ function unregisterSession(cacheKey) {
2885
+ releaseStreamLogHandle(cacheKey);
2886
+ return sessionStore.delete(cacheKey);
2887
+ }
2888
+ function getGroupSlugForSession(cacheKey) {
2889
+ return sessionStore.get(cacheKey)?.groupSlug;
2890
+ }
2891
+ function getVisitorIdForSession(cacheKey) {
2892
+ return sessionStore.get(cacheKey)?.visitorId;
2893
+ }
2894
+ function setGroupContextForSession(cacheKey, context) {
2895
+ const session = sessionStore.get(cacheKey);
2896
+ if (!session) return false;
2897
+ session.groupSlug = context.groupSlug;
2898
+ session.groupName = context.groupName;
2899
+ session.conversationId = context.conversationId;
2900
+ session.visitorId = context.visitorId;
2901
+ session.senderDisplayName = context.senderDisplayName;
2902
+ return true;
2903
+ }
2904
+ function registerGrantSession(cacheKey, accountId, agentName, opts) {
2905
+ sessionStore.set(cacheKey, {
2906
+ createdAt: Date.now(),
2907
+ agentType: "public",
2908
+ accountId,
2909
+ agentName,
2910
+ grantId: opts.grantId,
2911
+ grantExpiresAt: opts.grantExpiresAt,
2912
+ grantStatus: opts.grantStatus,
2913
+ grantDisplayName: opts.grantDisplayName,
2914
+ grantContactValue: opts.grantContactValue,
2915
+ setupRequired: opts.setupRequired
2916
+ });
2917
+ }
2918
+ function getGrantForSession(cacheKey) {
2919
+ const session = sessionStore.get(cacheKey);
2920
+ if (!session?.grantId) return void 0;
2921
+ return {
2922
+ grantId: session.grantId,
2923
+ grantExpiresAt: session.grantExpiresAt,
2924
+ grantStatus: session.grantStatus,
2925
+ grantDisplayName: session.grantDisplayName,
2926
+ grantContactValue: session.grantContactValue,
2927
+ setupRequired: session.setupRequired
2928
+ };
2929
+ }
2930
+ function completeGrantSetup(cacheKey) {
2931
+ const session = sessionStore.get(cacheKey);
2932
+ if (session) {
2933
+ session.setupRequired = false;
2934
+ session.grantStatus = "active";
2935
+ }
2936
+ }
2937
+ function getMessageHistory(cacheKey) {
2938
+ const session = sessionStore.get(cacheKey);
2939
+ if (!session) return [];
2940
+ if (!session.messageHistory) session.messageHistory = [];
2941
+ return session.messageHistory;
2942
+ }
2943
+ function appendMessage(cacheKey, role, content, timestamp) {
2944
+ if (!content) return;
2945
+ const session = sessionStore.get(cacheKey);
2946
+ if (!session) {
2947
+ console.error(`[managed] appendMessage: session not found for key ${cacheKey.slice(0, 8)}\u2026 (store size: ${sessionStore.size})`);
2948
+ return;
2949
+ }
2950
+ if (!session.messageHistory) session.messageHistory = [];
2951
+ session.messageHistory.push({ role, content, timestamp: timestamp ?? Date.now() });
2952
+ }
2953
+ function storePendingTrimmedMessages(cacheKey, messages) {
2954
+ const session = sessionStore.get(cacheKey);
2955
+ if (session) session.pendingTrimmedMessages = messages;
2956
+ }
2957
+ function consumePendingTrimmedMessages(cacheKey) {
2958
+ const session = sessionStore.get(cacheKey);
2959
+ if (!session) return void 0;
2960
+ const messages = session.pendingTrimmedMessages;
2961
+ delete session.pendingTrimmedMessages;
2962
+ return messages;
2963
+ }
2964
+ function storeStalledSubagent(cacheKey, info) {
2965
+ const session = sessionStore.get(cacheKey);
2966
+ if (!session) {
2967
+ console.error(`[stall-recovery] storeStalledSubagent: session not found for key ${cacheKey.slice(0, 8)}\u2026 \u2014 stall context lost`);
2968
+ return;
2969
+ }
2970
+ if (!session.stalledSubagents) session.stalledSubagents = [];
2971
+ session.stalledSubagents.push(info);
2972
+ }
2973
+ function consumeStalledSubagents(cacheKey) {
2974
+ const session = sessionStore.get(cacheKey);
2975
+ if (!session) return void 0;
2976
+ const stalls = session.stalledSubagents;
2977
+ delete session.stalledSubagents;
2978
+ return stalls && stalls.length > 0 ? stalls : void 0;
2979
+ }
2980
+ function setPendingCommitmentOffers(cacheKey, offers) {
2981
+ const session = sessionStore.get(cacheKey);
2982
+ if (session) session.pendingCommitmentOffers = offers;
2983
+ }
2984
+ function getAgentTypeForSession(cacheKey) {
2985
+ return sessionStore.get(cacheKey)?.agentType;
2986
+ }
2987
+ function clearMessageHistory(cacheKey) {
2988
+ const session = sessionStore.get(cacheKey);
2989
+ if (session) session.messageHistory = [];
2990
+ }
2991
+ var clearAgentSessionIdHandlers = [];
2992
+ function onClearAgentSessionId(handler) {
2993
+ clearAgentSessionIdHandlers.push(handler);
2994
+ }
2995
+ var evictPoolHandlers = [];
2996
+ function onEvictPool(handler) {
2997
+ evictPoolHandlers.push(handler);
2998
+ }
2999
+ function clearAgentSessionId(cacheKey, reason) {
3000
+ const session = sessionStore.get(cacheKey);
3001
+ if (session) {
3002
+ session.agentSessionId = void 0;
3003
+ session.lastClearReason = reason;
3004
+ console.error(`[session-store] clearAgentSessionId cacheKey=${cacheKey.slice(0, 12)}\u2026 reason=${reason}`);
3005
+ } else {
3006
+ console.error(`[session-store] clearAgentSessionId SKIPPED \u2014 no session entry cacheKey=${cacheKey.slice(0, 12)}\u2026 reason=${reason}`);
3007
+ }
3008
+ for (const handler of clearAgentSessionIdHandlers) {
3009
+ try {
3010
+ handler(cacheKey, reason);
3011
+ } catch (err) {
3012
+ console.error(`[session-store] clearAgentSessionId handler threw: ${err instanceof Error ? err.message : String(err)}`);
3013
+ }
3014
+ }
3015
+ }
3016
+ function evictPool(cacheKey, reason) {
3017
+ for (const handler of evictPoolHandlers) {
3018
+ try {
3019
+ handler(cacheKey, reason);
3020
+ } catch (err) {
3021
+ console.error(`[session-store] evictPool handler threw: ${err instanceof Error ? err.message : String(err)}`);
3022
+ }
3023
+ }
3024
+ }
3025
+ function storeRecoveryHandoff(cacheKey, info) {
3026
+ const session = sessionStore.get(cacheKey);
3027
+ if (!session) {
3028
+ console.error(`[recovery-handoff] storeRecoveryHandoff: session not found for key ${cacheKey.slice(0, 8)}\u2026 \u2014 handoff context lost reason=${info.reason}`);
3029
+ return;
3030
+ }
3031
+ session.recoveryHandoff = { reason: info.reason, summary: info.summary, createdAt: Date.now() };
3032
+ }
3033
+ function consumeRecoveryHandoff(cacheKey) {
3034
+ const session = sessionStore.get(cacheKey);
3035
+ if (!session?.recoveryHandoff) return void 0;
3036
+ const handoff = session.recoveryHandoff;
3037
+ delete session.recoveryHandoff;
3038
+ delete session.lastClearReason;
3039
+ return handoff;
3040
+ }
3041
+ function getLastClearReason(cacheKey) {
3042
+ return sessionStore.get(cacheKey)?.lastClearReason;
3043
+ }
3044
+ function clearLastClearReason(cacheKey) {
3045
+ const session = sessionStore.get(cacheKey);
3046
+ if (session) delete session.lastClearReason;
3047
+ }
3048
+ function storeStallResume(cacheKey, info) {
3049
+ const session = sessionStore.get(cacheKey);
3050
+ if (!session) {
3051
+ console.error(`[stall-resume] storeStallResume: session not found for key ${cacheKey.slice(0, 8)}\u2026 \u2014 resume context lost kind=${info.kind}`);
3052
+ return;
3053
+ }
3054
+ session.stallResume = { ...info, createdAt: Date.now() };
3055
+ }
3056
+ function consumeStallResume(cacheKey) {
3057
+ const session = sessionStore.get(cacheKey);
3058
+ if (!session?.stallResume) return void 0;
3059
+ const payload = session.stallResume;
3060
+ delete session.stallResume;
3061
+ return payload;
3062
+ }
3063
+
3064
+ // app/lib/claude-agent/client-pool.ts
3065
+ var AsyncQueue = class {
3066
+ buffer = [];
3067
+ resolvers = [];
3068
+ closed = false;
3069
+ push(item) {
3070
+ if (this.closed) return;
3071
+ const resolver = this.resolvers.shift();
3072
+ if (resolver) resolver({ value: item, done: false });
3073
+ else this.buffer.push(item);
3074
+ }
3075
+ close() {
3076
+ if (this.closed) return;
3077
+ this.closed = true;
3078
+ while (this.resolvers.length > 0) {
3079
+ const r = this.resolvers.shift();
3080
+ r({ value: void 0, done: true });
3081
+ }
3082
+ }
3083
+ [Symbol.asyncIterator]() {
3084
+ return {
3085
+ next: () => new Promise((resolve8) => {
3086
+ if (this.buffer.length > 0) {
3087
+ resolve8({ value: this.buffer.shift(), done: false });
3088
+ } else if (this.closed) {
3089
+ resolve8({ value: void 0, done: true });
3090
+ } else {
3091
+ this.resolvers.push(resolve8);
3092
+ }
3093
+ }),
3094
+ return: async () => {
3095
+ this.close();
3096
+ return { value: void 0, done: true };
3097
+ }
3098
+ };
3099
+ }
3100
+ };
3101
+ var clientPool = /* @__PURE__ */ new Map();
3102
+ var DEFAULT_IDLE_MS = 30 * 60 * 1e3;
3103
+ var idleEvictMs = (() => {
3104
+ const v = Number(process.env.CLAUDE_CLIENT_IDLE_MS);
3105
+ return Number.isFinite(v) && v > 0 ? v : DEFAULT_IDLE_MS;
3106
+ })();
3107
+ var ABORT_SDK_TIMEOUT_MS = 2e3;
3108
+ function acquireClient(cacheKey, opts, streamLog) {
3109
+ const existing = clientPool.get(cacheKey);
3110
+ if (existing) {
3111
+ existing.lastUsedAt = Date.now();
3112
+ existing.turnsServed += 1;
3113
+ safeWrite(
3114
+ streamLog,
3115
+ `[${isoTs()}] [client-warm-reuse] cacheKey=${sk(cacheKey)} ageMs=${Date.now() - existing.createdAt} turnsServed=${existing.turnsServed} cachedTokens=${existing.cachedTokensLastTurn}
3116
+ `
3117
+ );
3118
+ return { entry: existing, isCold: false };
3119
+ }
3120
+ const userQueue = new AsyncQueue();
3121
+ const abortController = new AbortController();
3122
+ let q;
3123
+ try {
3124
+ const sdkOptions = opts.buildSdkOptions();
3125
+ q = query({ prompt: userQueue, options: { ...sdkOptions, abortController } });
3126
+ } catch (err) {
3127
+ const reason = err instanceof Error ? err.message : String(err);
3128
+ safeWrite(
3129
+ streamLog,
3130
+ `[${isoTs()}] [client-spawn-error] cacheKey=${sk(cacheKey)} reason=${JSON.stringify(reason.slice(0, 200))}
3131
+ `
3132
+ );
3133
+ throw err;
3134
+ }
3135
+ const entry = {
3136
+ query: q,
3137
+ userQueue,
3138
+ abortController,
3139
+ inflight: null,
3140
+ createdAt: Date.now(),
3141
+ lastUsedAt: Date.now(),
3142
+ turnsServed: 1,
3143
+ resumedFromSessionId: opts.resumedFromSessionId,
3144
+ cachedTokensLastTurn: -1,
3145
+ accountId: opts.accountId,
3146
+ accountDir: opts.accountDir,
3147
+ logKey: opts.logKey
3148
+ };
3149
+ clientPool.set(cacheKey, entry);
3150
+ safeWrite(
3151
+ streamLog,
3152
+ `[${isoTs()}] [client-cold-create] cacheKey=${sk(cacheKey)} resumedFrom=${opts.resumedFromSessionId ?? "none"} createdAtMs=${entry.createdAt}
3153
+ `
3154
+ );
3155
+ safeWrite(
3156
+ streamLog,
3157
+ `[${isoTs()}] [client-sdk-argv] cacheKey=${sk(cacheKey)} cwd=${opts.accountDir} resume=${opts.resumedFromSessionId ?? "none"} configDir=${process.env.CLAUDE_CONFIG_DIR ?? "<unset>"}
3158
+ `
3159
+ );
3160
+ return { entry, isCold: true };
3161
+ }
3162
+ function pushUserMessage(entry, content) {
3163
+ const msg = {
3164
+ type: "user",
3165
+ message: { role: "user", content },
3166
+ parent_tool_use_id: null
3167
+ };
3168
+ entry.userQueue.push(msg);
3169
+ }
3170
+ function pushUserToolResult(entry, toolUseId, content, isError = false) {
3171
+ const msg = {
3172
+ type: "user",
3173
+ message: {
3174
+ role: "user",
3175
+ content: [{ type: "tool_result", tool_use_id: toolUseId, content, is_error: isError }]
3176
+ },
3177
+ parent_tool_use_id: null
3178
+ };
3179
+ entry.userQueue.push(msg);
3180
+ }
3181
+ function setInflight(entry, promise) {
3182
+ entry.inflight = promise;
3183
+ promise.finally(() => {
3184
+ if (entry.inflight === promise) entry.inflight = null;
3185
+ }).catch(() => {
3186
+ });
3187
+ }
3188
+ function recordCachedTokens(entry, cachedTokens) {
3189
+ entry.cachedTokensLastTurn = cachedTokens;
3190
+ }
3191
+ function getActiveClient(cacheKey) {
3192
+ return clientPool.get(cacheKey);
3193
+ }
3194
+ function appendAbortLine(entry, line) {
3195
+ const handle = getStreamLogHandle(entry.accountDir, entry.logKey);
3196
+ try {
3197
+ handle.write(line);
3198
+ } catch {
3199
+ }
3200
+ console.error(line.trimEnd());
3201
+ }
3202
+ async function interruptClient(cacheKey, _streamLog) {
3203
+ void _streamLog;
3204
+ const entry = clientPool.get(cacheKey);
3205
+ if (!entry) return;
3206
+ const startedAt = Date.now();
3207
+ let resolved = false;
3208
+ const timeout = new Promise(
3209
+ (resolve8) => setTimeout(() => resolve8("timeout"), ABORT_SDK_TIMEOUT_MS)
3210
+ );
3211
+ const sdkAbort = entry.query.interrupt().then(() => {
3212
+ resolved = true;
3213
+ return "ok";
3214
+ }).catch((err) => {
3215
+ resolved = true;
3216
+ return { error: err instanceof Error ? err.message : String(err) };
3217
+ });
3218
+ const result = await Promise.race([sdkAbort, timeout]);
3219
+ const durationMs = Date.now() - startedAt;
3220
+ if (result === "timeout" && !resolved) {
3221
+ appendAbortLine(
3222
+ entry,
3223
+ `[${isoTs()}] [client-abort] cacheKey=${sk(cacheKey)} method=signal durationMs=${durationMs}
3224
+ `
3225
+ );
3226
+ evictClient(cacheKey, "abort-signal-fallback", null);
3227
+ return;
3228
+ }
3229
+ if (typeof result === "object" && "error" in result) {
3230
+ appendAbortLine(
3231
+ entry,
3232
+ `[${isoTs()}] [client-abort] cacheKey=${sk(cacheKey)} method=sdk durationMs=${durationMs} error=${JSON.stringify(result.error.slice(0, 120))}
3233
+ `
3234
+ );
3235
+ evictClient(cacheKey, "abort-error", null);
3236
+ return;
3237
+ }
3238
+ appendAbortLine(
3239
+ entry,
3240
+ `[${isoTs()}] [client-abort] cacheKey=${sk(cacheKey)} method=sdk durationMs=${durationMs}
3241
+ `
3242
+ );
3243
+ }
3244
+ function evictClient(cacheKey, reason, streamLog) {
3245
+ const entry = clientPool.get(cacheKey);
3246
+ if (!entry) return false;
3247
+ const ageMs = Date.now() - entry.createdAt;
3248
+ clientPool.delete(cacheKey);
3249
+ try {
3250
+ entry.userQueue.close();
3251
+ } catch {
3252
+ }
3253
+ try {
3254
+ entry.query.close();
3255
+ } catch {
3256
+ }
3257
+ const line = `[${isoTs()}] [client-evict] reason=${reason} cacheKey=${sk(cacheKey)} ageMs=${ageMs} turnsServed=${entry.turnsServed}
3258
+ `;
3259
+ if (streamLog) {
3260
+ safeWrite(streamLog, line);
3261
+ } else {
3262
+ appendAbortLine(entry, line);
3263
+ }
3264
+ return true;
3265
+ }
3266
+ function acquireOneShotClient(cacheKey, opts, streamLog) {
3267
+ const userQueue = new AsyncQueue();
3268
+ const abortController = new AbortController();
3269
+ let q;
3270
+ try {
3271
+ const sdkOptions = opts.buildSdkOptions();
3272
+ q = query({ prompt: userQueue, options: { ...sdkOptions, abortController } });
3273
+ } catch (err) {
3274
+ const reason = err instanceof Error ? err.message : String(err);
3275
+ safeWrite(
3276
+ streamLog,
3277
+ `[${isoTs()}] [client-spawn-error] cacheKey=${sk(cacheKey)} site=compaction-one-shot reason=${JSON.stringify(reason.slice(0, 200))}
3278
+ `
3279
+ );
3280
+ throw err;
3281
+ }
3282
+ const entry = {
3283
+ query: q,
3284
+ userQueue,
3285
+ abortController,
3286
+ inflight: null,
3287
+ createdAt: Date.now(),
3288
+ lastUsedAt: Date.now(),
3289
+ turnsServed: 1,
3290
+ resumedFromSessionId: opts.resumedFromSessionId,
3291
+ cachedTokensLastTurn: -1,
3292
+ accountId: opts.accountId,
3293
+ accountDir: opts.accountDir,
3294
+ logKey: opts.logKey
3295
+ };
3296
+ safeWrite(
3297
+ streamLog,
3298
+ `[${isoTs()}] [client-cold-create] reason=compaction-one-shot cacheKey=${sk(cacheKey)} createdAtMs=${entry.createdAt}
3299
+ `
3300
+ );
3301
+ let evicted = false;
3302
+ const evict = (reason) => {
3303
+ if (evicted) return;
3304
+ evicted = true;
3305
+ const ageMs = Date.now() - entry.createdAt;
3306
+ try {
3307
+ entry.userQueue.close();
3308
+ } catch {
3309
+ }
3310
+ try {
3311
+ entry.query.close();
3312
+ } catch {
3313
+ }
3314
+ safeWrite(
3315
+ streamLog,
3316
+ `[${isoTs()}] [client-evict] reason=${reason} cacheKey=${sk(cacheKey)} ageMs=${ageMs}
3317
+ `
3318
+ );
3319
+ };
3320
+ return { entry, evict };
3321
+ }
3322
+ function recordCrash(cacheKey, reason, streamLog) {
3323
+ const entry = clientPool.get(cacheKey);
3324
+ if (!entry) return;
3325
+ const tail = JSON.stringify(reason.slice(-512));
3326
+ clientPool.delete(cacheKey);
3327
+ try {
3328
+ entry.userQueue.close();
3329
+ } catch {
3330
+ }
3331
+ try {
3332
+ entry.query.close();
3333
+ } catch {
3334
+ }
3335
+ safeWrite(
3336
+ streamLog,
3337
+ `[${isoTs()}] [client-crash] cacheKey=${sk(cacheKey)} reason=${JSON.stringify(reason.slice(0, 80))} tail=${tail}
3338
+ `
3339
+ );
3340
+ }
3341
+ var IDLE_TICK_MS = 6e4;
3342
+ var idleTickHandle = null;
3343
+ function evictIdleTick() {
3344
+ const now = Date.now();
3345
+ for (const [cacheKey, entry] of Array.from(clientPool.entries())) {
3346
+ if (entry.inflight !== null) continue;
3347
+ if (now - entry.lastUsedAt < idleEvictMs) continue;
3348
+ const ageMs = now - entry.createdAt;
3349
+ clientPool.delete(cacheKey);
3350
+ try {
3351
+ entry.userQueue.close();
3352
+ } catch {
3353
+ }
3354
+ try {
3355
+ entry.query.close();
3356
+ } catch {
3357
+ }
3358
+ console.error(
3359
+ `[${isoTs()}] [client-evict] reason=idle cacheKey=${sk(cacheKey)} ageMs=${ageMs} idleMs=${now - entry.lastUsedAt} turnsServed=${entry.turnsServed}`
3360
+ );
3361
+ }
3362
+ }
3363
+ function startIdleEvictTick() {
3364
+ if (idleTickHandle) return;
3365
+ idleTickHandle = setInterval(evictIdleTick, IDLE_TICK_MS);
3366
+ if (idleTickHandle.unref) idleTickHandle.unref();
3367
+ }
3368
+ function stopIdleEvictTick() {
3369
+ if (idleTickHandle) {
3370
+ clearInterval(idleTickHandle);
3371
+ idleTickHandle = null;
3372
+ }
3373
+ }
3374
+ startIdleEvictTick();
3375
+ onClearAgentSessionId((cacheKey, reason) => {
3376
+ const entry = clientPool.get(cacheKey);
3377
+ if (!entry) return;
3378
+ const ageMs = Date.now() - entry.createdAt;
3379
+ clientPool.delete(cacheKey);
3380
+ try {
3381
+ entry.userQueue.close();
3382
+ } catch {
3383
+ }
3384
+ try {
3385
+ entry.query.close();
3386
+ } catch {
3387
+ }
3388
+ console.error(
3389
+ `[${isoTs()}] [client-evict] reason=clearAgentSessionId-${reason} cacheKey=${sk(cacheKey)} ageMs=${ageMs} turnsServed=${entry.turnsServed}`
3390
+ );
3391
+ });
3392
+ onEvictPool((cacheKey, reason) => {
3393
+ const entry = clientPool.get(cacheKey);
3394
+ if (!entry) return;
3395
+ const ageMs = Date.now() - entry.createdAt;
3396
+ clientPool.delete(cacheKey);
3397
+ try {
3398
+ entry.userQueue.close();
3399
+ } catch {
3400
+ }
3401
+ try {
3402
+ entry.query.close();
3403
+ } catch {
3404
+ }
3405
+ console.error(
3406
+ `[${isoTs()}] [client-evict] reason=evictPool-${reason} cacheKey=${sk(cacheKey)} ageMs=${ageMs} turnsServed=${entry.turnsServed}`
3407
+ );
3408
+ });
3409
+ function sk(cacheKey) {
3410
+ return cacheKey.length > 12 ? cacheKey.slice(0, 12) + "\u2026" : cacheKey;
3411
+ }
3412
+ function safeWrite(stream, line) {
3413
+ if (!stream || stream.destroyed) return;
3414
+ if (stream.writableEnded) return;
3415
+ try {
3416
+ stream.write(line);
3417
+ } catch {
3418
+ }
3419
+ }
3420
+ function _poolSnapshotForTest() {
3421
+ const now = Date.now();
3422
+ return Array.from(clientPool.entries()).map(([cacheKey, entry]) => ({
3423
+ cacheKey,
3424
+ ageMs: now - entry.createdAt,
3425
+ idleMs: now - entry.lastUsedAt,
3426
+ turnsServed: entry.turnsServed,
3427
+ inflight: entry.inflight !== null
3428
+ }));
3429
+ }
3430
+ function _evictAllForTest(reason = "test-tear-down") {
3431
+ for (const cacheKey of Array.from(clientPool.keys())) {
3432
+ const entry = clientPool.get(cacheKey);
3433
+ if (!entry) continue;
3434
+ clientPool.delete(cacheKey);
3435
+ try {
3436
+ entry.userQueue.close();
3437
+ } catch {
3438
+ }
3439
+ try {
3440
+ entry.query.close();
3441
+ } catch {
3442
+ }
3443
+ }
3444
+ void reason;
3445
+ }
3446
+
3447
+ export {
3448
+ MAXY_DIR,
3449
+ BRAND_NAME,
3450
+ COMMERCIAL_MODE,
3451
+ VNC_DISPLAY,
3452
+ RFB_PORT,
3453
+ WEBSOCKIFY_PORT,
3454
+ CDP_PORT,
3455
+ CHROMIUM_PROFILE_DIR,
3456
+ USERS_FILE,
3457
+ LOG_DIR,
3458
+ BIN_DIR,
3459
+ REMOTE_PASSWORD_FILE,
3460
+ REMOTE_SESSION_SECRET_FILE,
3461
+ TELEGRAM_WEBHOOK_SECRET_FILE,
3462
+ TELEGRAM_ADMIN_WEBHOOK_SECRET_FILE,
3463
+ CLAUDE_CREDENTIALS_FILE,
3464
+ substituteBrandPlaceholders,
3465
+ PLATFORM_ROOT2 as PLATFORM_ROOT,
3466
+ ACCOUNTS_DIR,
3467
+ hasStubAccountDir,
3468
+ resolveAccount,
3469
+ readAgentFile,
3470
+ readIdentity,
3471
+ validateAgentSlug,
3472
+ resolveDefaultAgentSlug,
3473
+ resolveAgentConfig,
3474
+ getDefaultAccountId,
3475
+ resolveUserAccounts,
3476
+ isoTs,
3477
+ isBrowserTool,
3478
+ runFailureDiagnostic,
3479
+ agentLogStream,
3480
+ emitMissingOnResolve,
3481
+ sigtermFlushStreamLogs,
3482
+ findMissingPlugins,
3483
+ walkPremiumBundles,
3484
+ autoDeliverPremiumPlugins,
3485
+ reconcileEnabledPlugins,
3486
+ autoDeliverUserPlugins,
3487
+ migratePluginRenames,
3488
+ autoDeliverBundleAgents,
3489
+ loadEmbeddedPlugins,
3490
+ buildPluginManifest,
3491
+ validateComponentData,
3492
+ streamLogPathFor,
3493
+ buildSpawnEnv,
3494
+ resolveClaudeBinary,
3495
+ getMcpServers,
3496
+ getBundleMtimeIso,
3497
+ logToolSurface,
3498
+ assembleAllowedToolsForAdminSpawn,
3499
+ getStreamLogHandle,
3500
+ appendStreamLogLine,
3501
+ findFirstSubstantiveUserMessage,
3502
+ getSession2 as getSession,
3503
+ fingerprintSessionKey,
3504
+ mintAdminSessionToken,
3505
+ setWantsPriorConversation,
3506
+ consumeWantsPriorConversation,
3507
+ registerSession,
3508
+ registerResumedSession,
3509
+ getSessionMessages,
3510
+ clearSessionHistory,
3511
+ bufferPendingTurn,
3512
+ getPendingTurnCount,
3513
+ drainPendingTurns,
3514
+ isFlushError,
3515
+ maybeFlushConversationBuffer,
3516
+ isDmChannelCacheKey,
3517
+ getAgentNameForSession,
3518
+ listAdminSessionsInProgress,
3519
+ validateSession,
3520
+ storeAgentSessionId,
3521
+ getAgentSessionId,
3522
+ setAgentSessionId,
3523
+ storePendingCompactionSummary,
3524
+ consumePendingCompactionSummary,
3525
+ getAccountIdForSession,
3526
+ getUserIdForSession,
3527
+ getUserNameForSession,
3528
+ getRoleForSession,
3529
+ getConversationIdForSession,
3530
+ getSessionKeyByConversationId,
3531
+ setConversationIdForSession,
3532
+ unregisterSession,
3533
+ getGroupSlugForSession,
3534
+ getVisitorIdForSession,
3535
+ setGroupContextForSession,
3536
+ registerGrantSession,
3537
+ getGrantForSession,
3538
+ completeGrantSetup,
3539
+ getMessageHistory,
3540
+ appendMessage,
3541
+ storePendingTrimmedMessages,
3542
+ consumePendingTrimmedMessages,
3543
+ storeStalledSubagent,
3544
+ consumeStalledSubagents,
3545
+ setPendingCommitmentOffers,
3546
+ getAgentTypeForSession,
3547
+ clearMessageHistory,
3548
+ clearAgentSessionId,
3549
+ evictPool,
3550
+ storeRecoveryHandoff,
3551
+ consumeRecoveryHandoff,
3552
+ getLastClearReason,
3553
+ clearLastClearReason,
3554
+ storeStallResume,
3555
+ consumeStallResume,
3556
+ acquireClient,
3557
+ pushUserMessage,
3558
+ pushUserToolResult,
3559
+ setInflight,
3560
+ recordCachedTokens,
3561
+ getActiveClient,
3562
+ interruptClient,
3563
+ evictClient,
3564
+ acquireOneShotClient,
3565
+ recordCrash,
3566
+ startIdleEvictTick,
3567
+ stopIdleEvictTick,
3568
+ _poolSnapshotForTest,
3569
+ _evictAllForTest
3570
+ };