negotium 0.1.37 → 0.1.39

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 (64) hide show
  1. package/README.md +19 -0
  2. package/dist/agent-helpers.js +15637 -5674
  3. package/dist/agent-helpers.js.map +85 -37
  4. package/dist/browser-runtime.js +1600 -0
  5. package/dist/browser-runtime.js.map +28 -0
  6. package/dist/hosted-agent.js +8 -8
  7. package/dist/hosted-agent.js.map +6 -5
  8. package/dist/main.js +3790 -3113
  9. package/dist/main.js.map +33 -29
  10. package/dist/mcp-factories.js +15141 -301
  11. package/dist/mcp-factories.js.map +100 -7
  12. package/dist/prompts.js +117 -49
  13. package/dist/prompts.js.map +3 -3
  14. package/dist/query-runtime.js +310 -1
  15. package/dist/query-runtime.js.map +10 -5
  16. package/dist/runtime/src/agents/archiver.ts +407 -296
  17. package/dist/runtime/src/agents/mcp-tools/ask-user.ts +440 -274
  18. package/dist/runtime/src/agents/mcp-tools/self-config.ts +139 -65
  19. package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +1037 -816
  20. package/dist/runtime/src/agents/public-helpers.ts +82 -0
  21. package/dist/runtime/src/agents/self-config-core.ts +574 -278
  22. package/dist/runtime/src/agents/topic-cleanup.ts +167 -129
  23. package/dist/runtime/src/index.ts +4 -1
  24. package/dist/runtime/src/mcp/factories/index.ts +9 -0
  25. package/dist/runtime/src/mcp/session-comm/topic-catalog.ts +132 -0
  26. package/dist/runtime/src/mcp/session-comm/topics.ts +23 -57
  27. package/dist/runtime/src/platform/mcp-config.ts +2 -4
  28. package/dist/runtime/src/platform/playwright/browser-processes.ts +3 -2
  29. package/dist/runtime/src/platform/playwright/capability.ts +5 -0
  30. package/dist/runtime/src/platform/playwright/manager.ts +380 -66
  31. package/dist/runtime/src/platform/playwright/public-runtime.ts +45 -0
  32. package/dist/runtime/src/platform/playwright/transport-probe.ts +89 -0
  33. package/dist/runtime/src/prompts/builders.ts +192 -51
  34. package/dist/runtime/src/query/active-rooms.ts +2 -0
  35. package/dist/runtime/src/query/public-runtime.ts +8 -0
  36. package/dist/runtime/src/query/state.ts +69 -33
  37. package/dist/runtime/src/query/types.ts +1 -0
  38. package/dist/runtime/src/runtime/playwright-turn-abort.ts +59 -0
  39. package/dist/runtime/src/runtime/turn-event-stream.ts +23 -6
  40. package/dist/runtime/src/runtime/turn-runner.ts +6 -1
  41. package/dist/runtime/src/version.ts +1 -1
  42. package/dist/types/packages/core/src/agents/api-topic-agent-switch.d.ts +38 -0
  43. package/dist/types/packages/core/src/agents/archiver.d.ts +52 -16
  44. package/dist/types/packages/core/src/agents/mcp-tools/ask-user.d.ts +75 -4
  45. package/dist/types/packages/core/src/agents/mcp-tools/self-config.d.ts +17 -0
  46. package/dist/types/packages/core/src/agents/mcp-tools/spawn-subagent.d.ts +112 -25
  47. package/dist/types/packages/core/src/agents/public-helpers.d.ts +7 -0
  48. package/dist/types/packages/core/src/agents/self-config-core.d.ts +185 -0
  49. package/dist/types/packages/core/src/agents/topic-cleanup.d.ts +30 -24
  50. package/dist/types/packages/core/src/mcp/factories/index.d.ts +1 -0
  51. package/dist/types/packages/core/src/mcp/session-comm/topic-catalog.d.ts +46 -0
  52. package/dist/types/packages/core/src/platform/mcp-config.d.ts +2 -1
  53. package/dist/types/packages/core/src/platform/playwright/capability.d.ts +1 -0
  54. package/dist/types/packages/core/src/platform/playwright/manager.d.ts +65 -5
  55. package/dist/types/packages/core/src/platform/playwright/public-runtime.d.ts +4 -0
  56. package/dist/types/packages/core/src/platform/playwright/transport-probe.d.ts +13 -0
  57. package/dist/types/packages/core/src/prompts/builders.d.ts +29 -3
  58. package/dist/types/packages/core/src/query/active-rooms.d.ts +2 -0
  59. package/dist/types/packages/core/src/query/public-runtime.d.ts +1 -0
  60. package/dist/types/packages/core/src/query/state.d.ts +13 -0
  61. package/dist/types/packages/core/src/query/types.d.ts +2 -1
  62. package/dist/types/packages/core/src/runtime/playwright-turn-abort.d.ts +12 -0
  63. package/dist/types/packages/core/src/version.d.ts +1 -1
  64. package/package.json +6 -1
@@ -0,0 +1,1600 @@
1
+ // @bun
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __toESM = (mod, isNodeMode, target) => {
8
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
9
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
+ for (let key of __getOwnPropNames(mod))
11
+ if (!__hasOwnProp.call(to, key))
12
+ __defProp(to, key, {
13
+ get: () => mod[key],
14
+ enumerable: true
15
+ });
16
+ return to;
17
+ };
18
+ var __require = import.meta.require;
19
+
20
+ // ../../packages/core/src/platform/playwright/browser-processes.ts
21
+ import { execFileSync as execFileSync2 } from "child_process";
22
+ import { readdirSync, unlinkSync } from "fs";
23
+ import { resolve as resolve4, sep } from "path";
24
+
25
+ // ../../packages/core/src/platform/config.ts
26
+ import { execFileSync } from "child_process";
27
+ import { randomBytes } from "crypto";
28
+ import {
29
+ accessSync,
30
+ chmodSync,
31
+ constants,
32
+ existsSync,
33
+ mkdirSync,
34
+ readFileSync,
35
+ writeFileSync
36
+ } from "fs";
37
+ import { homedir } from "os";
38
+ import { dirname, join, resolve } from "path";
39
+ import { fileURLToPath } from "url";
40
+
41
+ // ../../packages/core/src/platform/config-helpers.ts
42
+ function readEnvText(env, key) {
43
+ const value = env[key]?.trim();
44
+ return value || undefined;
45
+ }
46
+ function parseRuntimePort(value, fallback) {
47
+ if (!value)
48
+ return fallback;
49
+ const port = Number.parseInt(value, 10);
50
+ return Number.isInteger(port) && port > 0 && port <= 65535 ? port : fallback;
51
+ }
52
+
53
+ // ../../packages/core/src/platform/logger.ts
54
+ import pino from "pino";
55
+ function createStdioLogger(options = {}) {
56
+ const development = options.development ?? true;
57
+ return pino({
58
+ level: options.level ?? process.env.LOG_LEVEL ?? "info",
59
+ transport: development ? {
60
+ target: "pino-pretty",
61
+ options: {
62
+ colorize: true,
63
+ translateTime: "SYS:yyyy-mm-dd HH:MM:ss",
64
+ destination: 2
65
+ }
66
+ } : undefined
67
+ }, pino.destination(2));
68
+ }
69
+ var logger = createStdioLogger();
70
+
71
+ // ../../packages/core/src/types.ts
72
+ var SUPPORTED_AGENTS = ["maestro", "claude", "codex"];
73
+ function isAgentKind(value) {
74
+ return typeof value === "string" && SUPPORTED_AGENTS.includes(value);
75
+ }
76
+
77
+ // ../../packages/core/src/platform/config.ts
78
+ function envText(envKey) {
79
+ return readEnvText(process.env, envKey);
80
+ }
81
+ function resolveAgentEnv(envKey, fallback, legacyEnvKey) {
82
+ const value = envText(envKey) ?? (legacyEnvKey ? envText(legacyEnvKey) : undefined);
83
+ return isAgentKind(value) ? value : fallback;
84
+ }
85
+ var HOME = homedir();
86
+ function resolveProjectRoot() {
87
+ const moduleDir = dirname(fileURLToPath(import.meta.url));
88
+ const packagedRuntime = resolve(moduleDir, "runtime");
89
+ if (existsSync(resolve(packagedRuntime, "src")))
90
+ return packagedRuntime;
91
+ return resolve(moduleDir, "../..");
92
+ }
93
+ var PROJECT_ROOT = resolveProjectRoot();
94
+ function resolveDependencyBin(name) {
95
+ let dir = PROJECT_ROOT;
96
+ while (true) {
97
+ const candidate = resolve(dir, "node_modules", ".bin", name);
98
+ if (existsSync(candidate))
99
+ return candidate;
100
+ const parent = dirname(dir);
101
+ if (parent === dir)
102
+ return candidate;
103
+ dir = parent;
104
+ }
105
+ }
106
+ var STATE_DIR_ENV = envText("NEGOTIUM_STATE_DIR");
107
+ var STATE_DIR = STATE_DIR_ENV ? resolve(STATE_DIR_ENV) : resolve(HOME, ".negotium");
108
+ function resolveLocalStateDir(envKey, stateName) {
109
+ const envValue = envText(envKey);
110
+ if (envValue)
111
+ return resolve(envValue);
112
+ return resolve(STATE_DIR, stateName);
113
+ }
114
+ function parsePortEnv(envValue, fallback) {
115
+ return parseRuntimePort(envValue, fallback);
116
+ }
117
+ var WORKSPACE_DIR = resolveLocalStateDir("NEGOTIUM_WORKSPACE_DIR", "workspace");
118
+ var TOPIC_WORKSPACE_DIR = resolve(WORKSPACE_DIR, "topics");
119
+ var SHARED_WIKI_DIR = resolve(WORKSPACE_DIR, "wiki");
120
+ var CONTEXTS_DIR = resolve(WORKSPACE_DIR, "contexts");
121
+ var BROWSER_PROFILES_DIR = resolve(WORKSPACE_DIR, "browser-profiles");
122
+ var DM_WORKSPACE_DIR = resolve(WORKSPACE_DIR, "dm");
123
+ var SESSION_WORKSPACE_DIR = resolve(WORKSPACE_DIR, "sessions");
124
+ var CLAUDE_EXECUTABLE_ENV = envText("NEGOTIUM_CLAUDE_EXECUTABLE");
125
+ var CLAUDE_EXECUTABLE = CLAUDE_EXECUTABLE_ENV ? resolve(CLAUDE_EXECUTABLE_ENV) : undefined;
126
+ function resolveBrowserMcpBin(envValue) {
127
+ const override = envValue?.trim();
128
+ if (override) {
129
+ if (!/(^|\/)(mcp-patchright|mcp-patchright-http\.mjs)$/.test(override)) {
130
+ throw new Error("NEGOTIUM_BROWSER_MCP_BIN must point to the authenticated mcp-patchright wrapper.");
131
+ }
132
+ return override;
133
+ }
134
+ return PATCHRIGHT_MCP_BIN;
135
+ }
136
+ var PATCHRIGHT_MCP_BIN = resolve(PROJECT_ROOT, "scripts/mcp-patchright-http.mjs");
137
+ var PLAYWRIGHT_MCP_BIN = resolveBrowserMcpBin(envText("NEGOTIUM_BROWSER_MCP_BIN"));
138
+ var BROWSER_RS_VERSION = "v0.1.13";
139
+ var BROWSER_RS_MIN_SECURE_VERSION = "0.1.13";
140
+ function versionAtLeast(actualVersion, minimumVersion) {
141
+ const actual = actualVersion.split(".").map(Number);
142
+ const minimum = minimumVersion.split(".").map(Number);
143
+ if (actual.some(Number.isNaN) || minimum.some(Number.isNaN))
144
+ return false;
145
+ for (let index = 0;index < minimum.length; index += 1) {
146
+ if ((actual[index] ?? 0) > (minimum[index] ?? 0))
147
+ return true;
148
+ if ((actual[index] ?? 0) < (minimum[index] ?? 0))
149
+ return false;
150
+ }
151
+ return true;
152
+ }
153
+ function browserRsMeetsMinimumVersion(candidate) {
154
+ try {
155
+ const output = execFileSync(candidate, ["--version"], {
156
+ encoding: "utf8",
157
+ timeout: 2000,
158
+ stdio: ["ignore", "pipe", "ignore"]
159
+ }).trim();
160
+ const match = output.match(/^browser-rs (\d+)\.(\d+)\.(\d+)$/);
161
+ if (!match)
162
+ return false;
163
+ return versionAtLeast(match.slice(1).join("."), BROWSER_RS_MIN_SECURE_VERSION);
164
+ } catch {
165
+ return false;
166
+ }
167
+ }
168
+ function resolveBrowserRsBin(envValue) {
169
+ const override = envValue?.trim();
170
+ if (!override && !versionAtLeast(BROWSER_RS_VERSION.replace(/^v/, ""), BROWSER_RS_MIN_SECURE_VERSION)) {
171
+ return;
172
+ }
173
+ const candidate = override ? resolve(override) : resolve(STATE_DIR, "bin", "browser-rs", BROWSER_RS_VERSION, "browser-rs");
174
+ try {
175
+ accessSync(candidate, constants.X_OK);
176
+ return browserRsMeetsMinimumVersion(candidate) ? candidate : undefined;
177
+ } catch {
178
+ return;
179
+ }
180
+ }
181
+ var BROWSER_RS_BIN = resolveBrowserRsBin(envText("NEGOTIUM_BROWSER_RS_BIN"));
182
+ function resolveBrowserProxy() {
183
+ const raw = envText("BROWSER_PROXY_URL");
184
+ if (!raw)
185
+ return null;
186
+ let url;
187
+ try {
188
+ url = new URL(raw);
189
+ } catch {
190
+ logger.warn({ raw }, "Ignoring malformed BROWSER_PROXY_URL");
191
+ return null;
192
+ }
193
+ const server = `${url.protocol}//${url.host}`;
194
+ const proxy = { server };
195
+ if (url.username)
196
+ proxy.username = decodeURIComponent(url.username);
197
+ if (url.password)
198
+ proxy.password = decodeURIComponent(url.password);
199
+ const bypass = envText("BROWSER_PROXY_BYPASS");
200
+ if (bypass)
201
+ proxy.bypass = bypass;
202
+ return proxy;
203
+ }
204
+ var TSX_BIN = resolveDependencyBin("tsx");
205
+ var TSCONFIG_PATH = resolve(PROJECT_ROOT, "tsconfig.json");
206
+ var SESSION_COMM_SERVER = resolve(PROJECT_ROOT, "src/mcp/session-comm/server.ts");
207
+ var TASK_SERVER = resolve(PROJECT_ROOT, "src/mcp/task-server.ts");
208
+ var BROWSER_MCP_SSE_PROXY_SERVER = resolve(PROJECT_ROOT, "src/mcp/browser-sse-proxy-server.ts");
209
+ var CANONICAL_MCP_PROXY_SERVER = resolve(PROJECT_ROOT, "src/mcp/canonical-proxy-server.ts");
210
+ var WIKI_SERVER = resolve(PROJECT_ROOT, "src/mcp/wiki-server.ts");
211
+ var TOKEN_STATS_SERVER = resolve(PROJECT_ROOT, "src/mcp/token-stats-server.ts");
212
+ var COMPACTION_LOG_SERVER = resolve(PROJECT_ROOT, "src/mcp/compaction-log-server.ts");
213
+ var SYSTEM_HEALTH_SERVER = resolve(PROJECT_ROOT, "src/mcp/system-health-server.ts");
214
+ var AGENT_HEALTH_SERVER = resolve(PROJECT_ROOT, "src/mcp/agent-health-server.ts");
215
+ var BACKGROUND_BASH_SERVER = resolve(PROJECT_ROOT, "src/mcp/background-bash-server.ts");
216
+ var VAULT_SERVER = resolve(PROJECT_ROOT, "src/mcp/vault-server.ts");
217
+ var BG_BASH_BASE_PORT = parsePortEnv(process.env.BG_BASH_BASE_PORT, 9700);
218
+ var BG_BASH_MAX_PORT = parsePortEnv(process.env.BG_BASH_MAX_PORT, 9799);
219
+ function loadOrCreateLocalSecret(envKey, filename, options = {}) {
220
+ const envValue = envText(envKey);
221
+ const secretFile = resolve(STATE_DIR, filename);
222
+ mkdirSync(dirname(secretFile), { recursive: true });
223
+ if (envValue) {
224
+ if (options.persistEnvValue) {
225
+ writeFileSync(secretFile, `${envValue}
226
+ `, { mode: 384 });
227
+ chmodSync(secretFile, 384);
228
+ }
229
+ return envValue;
230
+ }
231
+ if (existsSync(secretFile)) {
232
+ const stored = readFileSync(secretFile, "utf-8").trim();
233
+ if (stored) {
234
+ chmodSync(secretFile, 384);
235
+ return stored;
236
+ }
237
+ }
238
+ const secret = randomBytes(32).toString("base64url");
239
+ try {
240
+ writeFileSync(secretFile, `${secret}
241
+ `, { mode: 384, flag: "wx" });
242
+ return secret;
243
+ } catch (error) {
244
+ if (error.code !== "EEXIST")
245
+ throw error;
246
+ const stored = readFileSync(secretFile, "utf-8").trim();
247
+ if (!stored)
248
+ throw new Error(`Secret file exists but is empty: ${secretFile}`);
249
+ chmodSync(secretFile, 384);
250
+ return stored;
251
+ }
252
+ }
253
+ var RUNTIME_MCP_SECRET = loadOrCreateLocalSecret("RUNTIME_MCP_SECRET", "runtime-mcp-secret");
254
+ var NODE_CONTROL_TOKEN = loadOrCreateLocalSecret("NEGOTIUM_CONTROL_TOKEN", "node-control-token");
255
+ var VAULT_MASTER_KEY = loadOrCreateLocalSecret("NEGOTIUM_VAULT_MASTER_KEY", "vault-master-key", { persistEnvValue: true });
256
+ delete process.env.NEGOTIUM_VAULT_MASTER_KEY;
257
+ var NEGOTIUM_PORT = parseInt(process.env.NEGOTIUM_PORT || "7777", 10);
258
+ var hostname = process.env.HOSTNAME || "127.0.0.1";
259
+ var DATA_DIR = resolveLocalStateDir("NEGOTIUM_DATA_DIR", "data");
260
+ var LOG_DIR = resolveLocalStateDir("NEGOTIUM_LOG_DIR", "logs");
261
+ var SESSIONS_DB = process.env.SESSIONS_DB_PATH ? resolve(process.env.SESSIONS_DB_PATH) : resolve(DATA_DIR, "sessions.db");
262
+ var DEBUG_FILE = resolve(DATA_DIR, "debug-users.json");
263
+ var USERS_LOG_DIR = resolve(DATA_DIR, "users");
264
+ var RUN_DIR = resolveLocalStateDir("NEGOTIUM_RUN_DIR", "run");
265
+ var PROGRESS_DIR = resolve(RUN_DIR, "progress");
266
+ var DM_CMD_DIR = resolve(RUN_DIR, "dm-commands");
267
+ var DM_RESP_DIR = resolve(RUN_DIR, "dm-responses");
268
+ var SESSION_INBOX_DIR = resolve(RUN_DIR, "session-inbox");
269
+ var SESSION_ASKS_DIR = resolve(RUN_DIR, "session-asks");
270
+ var PLAYWRIGHT_BASE_PORT = parsePortEnv(process.env.PLAYWRIGHT_BASE_PORT, 9100);
271
+ var PLAYWRIGHT_MAX_PORT = parsePortEnv(process.env.PLAYWRIGHT_MAX_PORT, 9499);
272
+ var PLAYWRIGHT_PORTS_DIR = resolve(RUN_DIR, "playwright-ports");
273
+ mkdirSync(STATE_DIR, { recursive: true });
274
+ mkdirSync(DATA_DIR, { recursive: true });
275
+ mkdirSync(LOG_DIR, { recursive: true });
276
+ mkdirSync(PROGRESS_DIR, { recursive: true });
277
+ mkdirSync(DM_CMD_DIR, { recursive: true });
278
+ mkdirSync(DM_RESP_DIR, { recursive: true });
279
+ mkdirSync(SESSION_INBOX_DIR, { recursive: true });
280
+ mkdirSync(SESSION_ASKS_DIR, { recursive: true });
281
+ mkdirSync(PLAYWRIGHT_PORTS_DIR, { recursive: true });
282
+ mkdirSync(WORKSPACE_DIR, { recursive: true });
283
+ mkdirSync(TOPIC_WORKSPACE_DIR, { recursive: true });
284
+ mkdirSync(SHARED_WIKI_DIR, { recursive: true });
285
+ mkdirSync(CONTEXTS_DIR, { recursive: true });
286
+ mkdirSync(BROWSER_PROFILES_DIR, { recursive: true });
287
+ mkdirSync(DM_WORKSPACE_DIR, { recursive: true });
288
+ mkdirSync(SESSION_WORKSPACE_DIR, { recursive: true });
289
+ var ACTIVE_QUERY_STALE_MS = 10 * 60 * 1000;
290
+ var AGENTS_PROMPTS_DIR = resolve(PROJECT_ROOT, "src/prompts/agents");
291
+ var RESOURCES_DIR = resolve(PROJECT_ROOT, "src/resources");
292
+ var FALLBACK_AGENT = resolveAgentEnv("FALLBACK_AGENT", "maestro", "DEFAULT_AGENT");
293
+ var SESSION_AGENT = resolveAgentEnv("SESSION_AGENT", FALLBACK_AGENT);
294
+ var GATEWAY_AGENT = resolveAgentEnv("GATEWAY_AGENT", FALLBACK_AGENT);
295
+ var FALLBACK_MODEL = envText("FALLBACK_MODEL") ?? envText("DEFAULT_MODEL");
296
+ function resolveModelEnv(envKey, agentConst) {
297
+ return envText(envKey) ?? (agentConst === FALLBACK_AGENT ? FALLBACK_MODEL : undefined);
298
+ }
299
+ var SESSION_MODEL = resolveModelEnv("SESSION_MODEL", SESSION_AGENT);
300
+ var GATEWAY_MODEL = resolveModelEnv("GATEWAY_MODEL", GATEWAY_AGENT);
301
+ var FFMPEG_BIN = envText("FFMPEG_BIN");
302
+ var FFPROBE_BIN = envText("FFPROBE_BIN");
303
+ var PYTHON_BIN = envText("PYTHON_BIN") ?? "python3";
304
+ var FASTER_WHISPER_WRAPPER = envText("FASTER_WHISPER_WRAPPER") ?? resolve(PROJECT_ROOT, "scripts/faster-whisper-wrapper.py");
305
+ var WHISPER_MODEL = envText("WHISPER_MODEL_FILE") ?? "turbo";
306
+ var TESSERACT_BIN = envText("TESSERACT_BIN") ?? "tesseract";
307
+ var PDFTOTEXT_BIN = envText("PDFTOTEXT_BIN") ?? "pdftotext";
308
+ var _envMaxTellDepth = Number.parseInt(process.env.MAX_TELL_DEPTH ?? "", 10);
309
+ var MAX_TELL_DEPTH = Number.isInteger(_envMaxTellDepth) && _envMaxTellDepth > 0 ? _envMaxTellDepth : 20;
310
+
311
+ // ../../packages/core/src/platform/delay.ts
312
+ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
313
+
314
+ // ../../packages/core/src/platform/playwright/manager-utils.ts
315
+ import { resolve as resolve2 } from "path";
316
+ function selectIdleEvictionKey(candidates, pinnedKeys, busyKeys, now, maxIdleMs) {
317
+ const pinned = new Set(pinnedKeys);
318
+ const busy = new Set(busyKeys);
319
+ let oldest = null;
320
+ for (const [key, instance] of candidates) {
321
+ if (pinned.has(key) || busy.has(key))
322
+ continue;
323
+ if (now - instance.lastUsedAt < maxIdleMs)
324
+ continue;
325
+ if (!oldest || instance.lastUsedAt < oldest.lastUsedAt) {
326
+ oldest = { key, lastUsedAt: instance.lastUsedAt };
327
+ }
328
+ }
329
+ return oldest?.key ?? null;
330
+ }
331
+ function selectReusablePort(minPort, maxPort, reservedPorts, isOccupied) {
332
+ for (let port = minPort;port <= maxPort; port++) {
333
+ if (reservedPorts.has(port) || isOccupied(port))
334
+ continue;
335
+ return port;
336
+ }
337
+ return null;
338
+ }
339
+ function extractUserDataDirArg(cmdline) {
340
+ const match = cmdline.match(/--user-data-dir(?:\s+|=)(\S+)/);
341
+ return match ? match[1] : null;
342
+ }
343
+ function browserProcessMatchesExpectedProfile(cmdline, expectedUserDataDir) {
344
+ const actualUserDataDir = extractUserDataDirArg(cmdline);
345
+ return actualUserDataDir !== null && resolve2(actualUserDataDir) === resolve2(expectedUserDataDir);
346
+ }
347
+ function waitForChildProcessExit(proc, timeoutMs) {
348
+ if (proc.exitCode !== null || proc.signalCode !== null)
349
+ return Promise.resolve(true);
350
+ return new Promise((resolveWait) => {
351
+ let settled = false;
352
+ let timer;
353
+ const finish = (exited) => {
354
+ if (settled)
355
+ return;
356
+ settled = true;
357
+ clearTimeout(timer);
358
+ proc.off("exit", onExit);
359
+ resolveWait(exited);
360
+ };
361
+ const onExit = () => finish(true);
362
+ proc.once("exit", onExit);
363
+ timer = setTimeout(() => finish(proc.exitCode !== null || proc.signalCode !== null), timeoutMs);
364
+ if (typeof timer === "object")
365
+ timer.unref?.();
366
+ if (proc.exitCode !== null || proc.signalCode !== null)
367
+ finish(true);
368
+ });
369
+ }
370
+ function waitForChildProcessSpawnError(proc) {
371
+ return new Promise((_resolve, reject) => {
372
+ proc.once("error", reject);
373
+ });
374
+ }
375
+
376
+ // ../../packages/core/src/storage/storage-host.ts
377
+ import { mkdirSync as mkdirSync2 } from "fs";
378
+ import { homedir as homedir2 } from "os";
379
+ import { dirname as dirname2, join as join2, resolve as resolve3 } from "path";
380
+
381
+ // ../../packages/core/src/storage/sqlite.ts
382
+ var isBun = typeof process.versions.bun === "string";
383
+ var Database;
384
+ if (isBun) {
385
+ ({ Database } = await import("bun:sqlite"));
386
+ } else {
387
+ const nodeSqliteSpecifier = ["node", "sqlite"].join(":");
388
+ const { DatabaseSync } = await import(nodeSqliteSpecifier);
389
+
390
+ class NodeDatabase {
391
+ #db;
392
+ constructor(path, options = {}) {
393
+ this.#db = options.readonly ? new DatabaseSync(path, { readOnly: true }) : new DatabaseSync(path);
394
+ }
395
+ query(sql) {
396
+ return this.#db.prepare(sql);
397
+ }
398
+ prepare(sql) {
399
+ return this.#db.prepare(sql);
400
+ }
401
+ exec(sql) {
402
+ this.#db.exec(sql);
403
+ }
404
+ run(sql, ...params) {
405
+ return this.#db.prepare(sql).run(...params);
406
+ }
407
+ transaction(fn) {
408
+ const run = (begin) => (...args) => {
409
+ this.#db.exec(begin);
410
+ try {
411
+ const result = fn(...args);
412
+ this.#db.exec("COMMIT");
413
+ return result;
414
+ } catch (err) {
415
+ this.#db.exec("ROLLBACK");
416
+ throw err;
417
+ }
418
+ };
419
+ const tx = run("BEGIN");
420
+ tx.deferred = run("BEGIN DEFERRED");
421
+ tx.immediate = run("BEGIN IMMEDIATE");
422
+ tx.exclusive = run("BEGIN EXCLUSIVE");
423
+ return tx;
424
+ }
425
+ close() {
426
+ this.#db.close();
427
+ }
428
+ }
429
+ Database = NodeDatabase;
430
+ }
431
+
432
+ // ../../packages/core/src/storage/storage-host.ts
433
+ var configuredHost = {};
434
+ var fallbackDatabase = null;
435
+ var fallbackDatabasePath = null;
436
+ var schemaInitializers = [];
437
+ var initializedSchemas = new WeakMap;
438
+ var initializingDatabases = new WeakSet;
439
+ function envPath(name, fallback) {
440
+ const value = process.env[name]?.trim();
441
+ return resolve3(value || fallback);
442
+ }
443
+ function defaultStateDir() {
444
+ return envPath("NEGOTIUM_STATE_DIR", join2(homedir2(), ".negotium"));
445
+ }
446
+ function defaultDataDir() {
447
+ return envPath("NEGOTIUM_DATA_DIR", join2(defaultStateDir(), "data"));
448
+ }
449
+ function defaultSessionsDatabasePath() {
450
+ return envPath("SESSIONS_DB_PATH", join2(resolveStorageDataDir(), "sessions.db"));
451
+ }
452
+ function initializeDatabase(database) {
453
+ database.exec("PRAGMA journal_mode = WAL");
454
+ database.exec("PRAGMA foreign_keys = ON");
455
+ database.exec("PRAGMA busy_timeout = 5000");
456
+ database.exec("PRAGMA wal_autocheckpoint = 1000");
457
+ try {
458
+ database.exec("PRAGMA wal_checkpoint(TRUNCATE)");
459
+ } catch {}
460
+ }
461
+ function defaultDatabase() {
462
+ const path = defaultSessionsDatabasePath();
463
+ if (fallbackDatabase && fallbackDatabasePath === path)
464
+ return fallbackDatabase;
465
+ if (fallbackDatabase)
466
+ fallbackDatabase.close();
467
+ mkdirSync2(dirname2(path), { recursive: true });
468
+ fallbackDatabase = new Database(path, { create: true });
469
+ fallbackDatabasePath = path;
470
+ initializeDatabase(fallbackDatabase);
471
+ return fallbackDatabase;
472
+ }
473
+ function resolveStorageDatabase() {
474
+ return configuredHost.database ?? defaultDatabase();
475
+ }
476
+ function resolveStorageDataDir() {
477
+ return configuredHost.dataDir ?? defaultDataDir();
478
+ }
479
+ function ensureStorageSchemas(database = resolveStorageDatabase()) {
480
+ if (initializingDatabases.has(database))
481
+ return;
482
+ let initialized = initializedSchemas.get(database);
483
+ if (!initialized) {
484
+ initialized = new Set;
485
+ initializedSchemas.set(database, initialized);
486
+ }
487
+ initializingDatabases.add(database);
488
+ try {
489
+ for (const entry of schemaInitializers) {
490
+ if (initialized.has(entry.initialize))
491
+ continue;
492
+ initialized.add(entry.initialize);
493
+ try {
494
+ entry.initialize(database);
495
+ } catch (error) {
496
+ initialized.delete(entry.initialize);
497
+ throw error;
498
+ }
499
+ }
500
+ } finally {
501
+ initializingDatabases.delete(database);
502
+ }
503
+ }
504
+ var internalStorageDatabase = new Proxy({}, {
505
+ get(_target, property) {
506
+ const database = resolveStorageDatabase();
507
+ ensureStorageSchemas(database);
508
+ const value = Reflect.get(database, property, database);
509
+ return typeof value === "function" ? value.bind(database) : value;
510
+ },
511
+ set(_target, property, value) {
512
+ const database = resolveStorageDatabase();
513
+ ensureStorageSchemas(database);
514
+ return Reflect.set(database, property, value, database);
515
+ }
516
+ });
517
+
518
+ // ../../packages/core/src/storage/forum-db.ts
519
+ var db = internalStorageDatabase;
520
+
521
+ // ../../packages/core/src/storage/runtime-leases.ts
522
+ import { randomUUID } from "crypto";
523
+
524
+ // ../../packages/core/src/storage/runtime-topic-state.ts
525
+ db.exec(`
526
+ CREATE TABLE IF NOT EXISTS runtime_topic_state (
527
+ topic_id TEXT PRIMARY KEY,
528
+ epoch INTEGER NOT NULL DEFAULT 0,
529
+ maintenance INTEGER NOT NULL DEFAULT 0 CHECK (maintenance IN (0, 1)),
530
+ maintenance_owner TEXT,
531
+ heartbeat_at INTEGER
532
+ )
533
+ `);
534
+
535
+ // ../../packages/core/src/storage/runtime-leases.ts
536
+ var RUNTIME_INSTANCE_ID = `${process.pid}-${randomUUID()}`;
537
+ db.exec(`
538
+ CREATE TABLE IF NOT EXISTS runtime_turn_leases (
539
+ topic_id TEXT PRIMARY KEY,
540
+ query_id TEXT NOT NULL UNIQUE,
541
+ owner_id TEXT NOT NULL,
542
+ origin TEXT NOT NULL,
543
+ started_at INTEGER NOT NULL,
544
+ heartbeat_at INTEGER NOT NULL,
545
+ abort_requested INTEGER NOT NULL DEFAULT 0 CHECK (abort_requested IN (0, 1)),
546
+ abort_reason TEXT CHECK (abort_reason IS NULL OR abort_reason IN ('internal', 'external'))
547
+ )
548
+ `);
549
+ db.exec("CREATE INDEX IF NOT EXISTS idx_runtime_turn_leases_heartbeat ON runtime_turn_leases(heartbeat_at)");
550
+
551
+ // ../../packages/core/src/storage/runtime-process-leases.ts
552
+ var PROCESS_LEASE_STALE_MS = 5000;
553
+ db.exec(`
554
+ CREATE TABLE IF NOT EXISTS runtime_process_leases (
555
+ role TEXT PRIMARY KEY,
556
+ owner_id TEXT NOT NULL UNIQUE,
557
+ pid INTEGER NOT NULL,
558
+ started_at INTEGER NOT NULL,
559
+ heartbeat_at INTEGER NOT NULL
560
+ )
561
+ `);
562
+ db.exec("CREATE INDEX IF NOT EXISTS idx_runtime_process_leases_heartbeat ON runtime_process_leases(heartbeat_at)");
563
+ function rowToLease(row) {
564
+ return {
565
+ role: row.role,
566
+ ownerId: row.owner_id,
567
+ pid: Number(row.pid),
568
+ startedAt: Number(row.started_at),
569
+ heartbeatAt: Number(row.heartbeat_at)
570
+ };
571
+ }
572
+ function getRuntimeProcessLease(role, now = Date.now(), staleMs = PROCESS_LEASE_STALE_MS) {
573
+ const row = db.query("SELECT * FROM runtime_process_leases WHERE role = ?").get(role);
574
+ if (!row)
575
+ return null;
576
+ const lease = rowToLease(row);
577
+ return now - lease.heartbeatAt <= staleMs ? lease : null;
578
+ }
579
+
580
+ // ../../packages/core/src/platform/playwright/browser-processes.ts
581
+ function isPortInUse(port) {
582
+ try {
583
+ execFileSync2("lsof", ["-i", `:${port}`, "-t"], { stdio: "pipe" });
584
+ return true;
585
+ } catch {
586
+ return false;
587
+ }
588
+ }
589
+ async function killPlaywrightOnPort(port, expectedUserDataDir) {
590
+ try {
591
+ const pids = execFileSync2("lsof", ["-i", `:${port}`, "-t"], { stdio: "pipe" }).toString().trim();
592
+ if (!pids)
593
+ return;
594
+ for (const pid of pids.split(`
595
+ `)) {
596
+ try {
597
+ const cmdline = execFileSync2("ps", ["-p", pid, "-o", "command="], { stdio: "pipe" }).toString().trim();
598
+ if (!cmdline.includes("mcp-patchright-http.mjs")) {
599
+ logger.warn({ port, pid, cmdline: cmdline.slice(0, 80) }, "Port occupied by non-browser-MCP process, skipping");
600
+ continue;
601
+ }
602
+ if (expectedUserDataDir) {
603
+ const otherDataDir = extractUserDataDirArg(cmdline);
604
+ if (!browserProcessMatchesExpectedProfile(cmdline, expectedUserDataDir)) {
605
+ logger.warn({
606
+ port,
607
+ pid,
608
+ otherDataDir,
609
+ expectedUserDataDir
610
+ }, "Port occupied by another topic's playwright-mcp, skipping");
611
+ continue;
612
+ }
613
+ }
614
+ const pidNum = parseInt(pid, 10);
615
+ if (!Number.isNaN(pidNum)) {
616
+ killProcessTreeChildren(pidNum);
617
+ process.kill(pidNum, "SIGKILL");
618
+ }
619
+ logger.info({ pid, port }, "Killed zombie mcp-patchright");
620
+ } catch (e) {
621
+ logger.warn({ err: e, port }, "Failed to inspect process occupying port");
622
+ }
623
+ }
624
+ } catch (e) {
625
+ logger.warn({ err: e, port }, "Failed to check processes on port");
626
+ }
627
+ const start = Date.now();
628
+ while (Date.now() - start < 3000) {
629
+ if (!isPortInUse(port))
630
+ return;
631
+ await delay(200);
632
+ }
633
+ }
634
+ function killBrowserProcsForUserDataDir(userDataDir) {
635
+ const target = resolve4(userDataDir);
636
+ const profileRoot = resolve4(BROWSER_PROFILES_DIR);
637
+ if (target !== profileRoot && !target.startsWith(`${profileRoot}${sep}`))
638
+ return;
639
+ let pids;
640
+ try {
641
+ pids = execFileSync2("pgrep", ["-f", "--", userDataDir], { stdio: "pipe" }).toString().trim();
642
+ } catch {
643
+ return;
644
+ }
645
+ if (!pids)
646
+ return;
647
+ for (const pid of pids.split(`
648
+ `)) {
649
+ const pidNum = parseInt(pid.trim(), 10);
650
+ if (Number.isNaN(pidNum) || pidNum === process.pid)
651
+ continue;
652
+ try {
653
+ const cmdline = execFileSync2("ps", ["-p", String(pidNum), "-o", "command="], {
654
+ stdio: "pipe"
655
+ }).toString().trim();
656
+ const argDir = extractUserDataDirArg(cmdline);
657
+ if (!argDir || resolve4(argDir) !== target)
658
+ continue;
659
+ killProcessTreeChildren(pidNum);
660
+ process.kill(pidNum, "SIGKILL");
661
+ logger.info({ pid: pidNum, userDataDir }, "Reaped orphaned browser process by user-data-dir");
662
+ } catch (e) {
663
+ logger.debug({ err: e, pid: pidNum, userDataDir }, "Failed to reap browser proc by dir");
664
+ }
665
+ }
666
+ }
667
+ function selectOrphanBrowserPids(procs, liveUserDataDirs, profileRoot, selfPid) {
668
+ const root = resolve4(profileRoot);
669
+ const live = new Set([...liveUserDataDirs].map((d) => resolve4(d)));
670
+ const out = [];
671
+ for (const { pid, userDataDir } of procs) {
672
+ if (pid === selfPid || !userDataDir)
673
+ continue;
674
+ const dir = resolve4(userDataDir);
675
+ if (dir !== root && !dir.startsWith(`${root}${sep}`))
676
+ continue;
677
+ if (live.has(dir))
678
+ continue;
679
+ out.push(pid);
680
+ }
681
+ return out;
682
+ }
683
+ function isBrowserJanitorOwner(leaseOwnerPid, selfPid) {
684
+ return leaseOwnerPid === selfPid;
685
+ }
686
+ function reapOrphanBrowsers(liveUserDataDirs) {
687
+ const daemonLease = getRuntimeProcessLease("node-daemon", Date.now(), Number.POSITIVE_INFINITY);
688
+ if (!isBrowserJanitorOwner(daemonLease?.pid ?? null, process.pid))
689
+ return;
690
+ const profileRoot = resolve4(BROWSER_PROFILES_DIR);
691
+ let pids;
692
+ try {
693
+ pids = execFileSync2("pgrep", ["-f", "--", profileRoot], { stdio: "pipe" }).toString().trim();
694
+ } catch {
695
+ return;
696
+ }
697
+ if (!pids)
698
+ return;
699
+ const procs = [];
700
+ for (const pid of pids.split(`
701
+ `)) {
702
+ const pidNum = parseInt(pid.trim(), 10);
703
+ if (Number.isNaN(pidNum))
704
+ continue;
705
+ try {
706
+ const cmdline = execFileSync2("ps", ["-p", String(pidNum), "-o", "command="], {
707
+ stdio: "pipe"
708
+ }).toString().trim();
709
+ procs.push({ pid: pidNum, userDataDir: extractUserDataDirArg(cmdline) });
710
+ } catch {}
711
+ }
712
+ const orphanPids = selectOrphanBrowserPids(procs, liveUserDataDirs, profileRoot, process.pid);
713
+ for (const pid of orphanPids) {
714
+ try {
715
+ killProcessTreeChildren(pid);
716
+ process.kill(pid, "SIGKILL");
717
+ logger.info({ pid }, "Reaped orphaned browser process (janitor)");
718
+ } catch (e) {
719
+ logger.debug({ err: e, pid }, "Janitor failed to reap orphaned browser process");
720
+ }
721
+ }
722
+ }
723
+ async function isHealthy(port) {
724
+ for (const path of ["/health", "/sse?owner=__negotium_health__"]) {
725
+ try {
726
+ const res = await fetch(`http://127.0.0.1:${port}${path}`, {
727
+ signal: AbortSignal.timeout(2000)
728
+ });
729
+ await res.body?.cancel();
730
+ if (res.ok)
731
+ return true;
732
+ } catch {}
733
+ }
734
+ return false;
735
+ }
736
+ function cleanSingletonFiles(userDataDir) {
737
+ try {
738
+ const files = readdirSync(userDataDir);
739
+ for (const f of files) {
740
+ if (f.startsWith("Singleton")) {
741
+ try {
742
+ unlinkSync(resolve4(userDataDir, f));
743
+ logger.info({ file: f, userDataDir }, "Removed stale Singleton file");
744
+ } catch (e) {
745
+ logger.warn({ err: e, file: f }, "Failed to remove stale Chrome Singleton file");
746
+ }
747
+ }
748
+ }
749
+ } catch {}
750
+ }
751
+ function killProcessTreeChildren(pid) {
752
+ try {
753
+ const children = execFileSync2("pgrep", ["-P", String(pid)], { stdio: "pipe" }).toString().trim();
754
+ if (!children)
755
+ return;
756
+ const childPids = children.split(`
757
+ `).map((cpid) => parseInt(cpid, 10)).filter((pidNum) => !Number.isNaN(pidNum));
758
+ for (const childPid of childPids) {
759
+ killProcessTreeChildren(childPid);
760
+ }
761
+ for (const signal of ["SIGTERM", "SIGKILL"]) {
762
+ for (const childPid of childPids) {
763
+ try {
764
+ process.kill(childPid, signal);
765
+ } catch (e) {
766
+ logger.debug({ err: e, pid: childPid, signal }, "Failed to signal browser child process");
767
+ }
768
+ }
769
+ }
770
+ logger.info({ parentPid: pid, childPids }, "Killed browser-MCP child process tree");
771
+ } catch {}
772
+ }
773
+ // ../../packages/core/src/platform/playwright/manager.ts
774
+ import { execFileSync as execFileSync3, spawn } from "child_process";
775
+ import { createHash, randomBytes as randomBytes2, timingSafeEqual } from "crypto";
776
+ import {
777
+ cpSync,
778
+ existsSync as existsSync2,
779
+ mkdirSync as mkdirSync3,
780
+ readFileSync as readFileSync2,
781
+ renameSync,
782
+ rmSync,
783
+ unlinkSync as unlinkSync2,
784
+ writeFileSync as writeFileSync2
785
+ } from "fs";
786
+ import { dirname as dirname3, join as join3, resolve as resolve6 } from "path";
787
+
788
+ // ../../packages/core/src/platform/playwright/headed-launch.ts
789
+ import { accessSync as accessSync2, constants as constants2 } from "fs";
790
+ import { delimiter, isAbsolute, resolve as resolve5 } from "path";
791
+ function findExecutableOnPath(command, environment = process.env) {
792
+ const candidates = isAbsolute(command) ? [command] : (environment.PATH ?? "").split(delimiter).filter(Boolean).map((directory) => resolve5(directory, command));
793
+ for (const candidate of candidates) {
794
+ try {
795
+ accessSync2(candidate, constants2.X_OK);
796
+ return candidate;
797
+ } catch {}
798
+ }
799
+ return null;
800
+ }
801
+ function resolveHeadedPlaywrightSpawn(command, args, options = {}) {
802
+ const platform = options.platform ?? process.platform;
803
+ const environment = options.environment ?? process.env;
804
+ const hasDisplay = Boolean(environment.DISPLAY?.trim() || environment.WAYLAND_DISPLAY?.trim());
805
+ if (platform !== "linux" || hasDisplay) {
806
+ return { command, args: [...args], virtualDisplay: false };
807
+ }
808
+ const xvfbRun = (options.findExecutable ?? findExecutableOnPath)("xvfb-run", environment);
809
+ if (!xvfbRun) {
810
+ throw new Error("Headed Playwright on Linux requires DISPLAY/WAYLAND_DISPLAY or xvfb-run; install Xvfb and ensure xvfb-run is on PATH");
811
+ }
812
+ return {
813
+ command: xvfbRun,
814
+ args: ["-a", "-s", "-screen 0 1440x1000x24", command, ...args],
815
+ virtualDisplay: true
816
+ };
817
+ }
818
+
819
+ // ../../packages/core/src/platform/playwright/transport-probe.ts
820
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
821
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
822
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
823
+
824
+ // ../../packages/core/src/platform/playwright/capability.ts
825
+ import { createHmac } from "crypto";
826
+ function browserOwnerCapability(capability, owner) {
827
+ return createHmac("sha256", capability).update(owner).digest("hex");
828
+ }
829
+
830
+ // ../../packages/core/src/platform/playwright/transport-probe.ts
831
+ var PROBE_OWNER = "__negotium_transport_probe__";
832
+ var PROBE_TIMEOUT_MS = 3000;
833
+ async function probeMcpTransport(transport, options = {}) {
834
+ const client = new Client({ name: "negotium-browser-healthcheck", version: "1.0.0" });
835
+ let timeout;
836
+ try {
837
+ const tools = await Promise.race([
838
+ (async () => {
839
+ await client.connect(transport);
840
+ const result = await client.listTools();
841
+ if (result.tools.length === 0)
842
+ throw new Error("browser MCP returned no tools");
843
+ await options.terminate?.();
844
+ return result.tools;
845
+ })(),
846
+ new Promise((_, reject) => {
847
+ timeout = setTimeout(() => reject(new Error("browser MCP transport probe timed out")), options.timeoutMs ?? PROBE_TIMEOUT_MS);
848
+ timeout.unref?.();
849
+ })
850
+ ]);
851
+ return tools.length > 0;
852
+ } catch {
853
+ return false;
854
+ } finally {
855
+ if (timeout)
856
+ clearTimeout(timeout);
857
+ try {
858
+ await client.close();
859
+ } catch {}
860
+ }
861
+ }
862
+ async function probePlaywrightMcpTransports(port, capability, fetchImpl = fetch) {
863
+ const query = new URLSearchParams({ owner: PROBE_OWNER });
864
+ const baseUrl = `http://127.0.0.1:${port}`;
865
+ const ownerCapability = browserOwnerCapability(capability, PROBE_OWNER);
866
+ const capabilityHeaders = { "X-Browser-Capability": ownerCapability };
867
+ const authenticatedFetch = (input, init) => {
868
+ const headers = new Headers(init?.headers);
869
+ headers.set("X-Browser-Capability", ownerCapability);
870
+ return fetchImpl(input, { ...init, headers });
871
+ };
872
+ const sseTransport = new SSEClientTransport(new URL(`${baseUrl}/sse?${query}`), {
873
+ fetch: authenticatedFetch,
874
+ eventSourceInit: { fetch: authenticatedFetch },
875
+ requestInit: { headers: capabilityHeaders }
876
+ });
877
+ if (!await probeMcpTransport(sseTransport))
878
+ return false;
879
+ const streamableTransport = new StreamableHTTPClientTransport(new URL(`${baseUrl}/mcp?${query}`), {
880
+ fetch: authenticatedFetch,
881
+ requestInit: { headers: capabilityHeaders }
882
+ });
883
+ return probeMcpTransport(streamableTransport, {
884
+ terminate: () => streamableTransport.terminateSession()
885
+ });
886
+ }
887
+
888
+ // ../../packages/core/src/security/sanitize.ts
889
+ function sanitizeTopicName(name, lowercase = false) {
890
+ const safe = name.replace(/[^a-zA-Z0-9\uAC00-\uD7A3_-]/g, "_") || "_";
891
+ return lowercase ? safe.toLowerCase() : safe;
892
+ }
893
+
894
+ // ../../packages/core/src/storage/browser-profiles.ts
895
+ var DEFAULT_BROWSER_PROFILE = "default";
896
+ function normalizeBrowserProfileName(name) {
897
+ const value = name.trim().toLowerCase();
898
+ if (!/^[a-z0-9][a-z0-9_-]{0,47}$/.test(value)) {
899
+ throw new Error("Profile name must be 1-48 lowercase letters, numbers, '-' or '_'.");
900
+ }
901
+ return value;
902
+ }
903
+ db.exec(`
904
+ CREATE TABLE IF NOT EXISTS browser_profiles (
905
+ owner_id TEXT NOT NULL,
906
+ name TEXT NOT NULL,
907
+ created_at TEXT NOT NULL,
908
+ PRIMARY KEY (owner_id, name)
909
+ )
910
+ `);
911
+ function getBrowserProfileOwner(topicId, fallbackUserId) {
912
+ return db.query("SELECT browser_profile_owner FROM api_topics WHERE id = ?").get(topicId)?.browser_profile_owner ?? fallbackUserId;
913
+ }
914
+ function getTopicBrowserProfile(topicId) {
915
+ const value = db.query("SELECT browser_profile FROM api_topics WHERE id = ?").get(topicId)?.browser_profile;
916
+ return value || DEFAULT_BROWSER_PROFILE;
917
+ }
918
+ function hasBrowserProfileTopic(topicId) {
919
+ return Boolean(db.query("SELECT id FROM api_topics WHERE id = ?").get(topicId));
920
+ }
921
+ function createBrowserProfile(ownerId, rawName) {
922
+ const name = normalizeBrowserProfileName(rawName);
923
+ if (name === DEFAULT_BROWSER_PROFILE)
924
+ return name;
925
+ db.query("INSERT OR IGNORE INTO browser_profiles (owner_id, name, created_at) VALUES (?, ?, ?)").run(ownerId, name, new Date().toISOString());
926
+ return name;
927
+ }
928
+ function assignTopicBrowserProfile(opts) {
929
+ const actualOwner = getBrowserProfileOwner(opts.topicId, "");
930
+ if (!actualOwner)
931
+ throw new Error(`Topic "${opts.topicId}" not found or has no owner.`);
932
+ if (actualOwner !== opts.actorUserId) {
933
+ throw new Error(`Only the topic owner can change its browser profile.`);
934
+ }
935
+ const profile = normalizeBrowserProfileName(opts.profile);
936
+ if (profile !== DEFAULT_BROWSER_PROFILE)
937
+ createBrowserProfile(actualOwner, profile);
938
+ const previous = getTopicBrowserProfile(opts.topicId);
939
+ const result = db.query("UPDATE api_topics SET browser_profile = ? WHERE id = ?").run(profile, opts.topicId);
940
+ if (Number(result.changes ?? 0) !== 1)
941
+ throw new Error(`Topic "${opts.topicId}" not found.`);
942
+ return { previous, profile };
943
+ }
944
+ // ../../packages/core/src/platform/playwright/manager.ts
945
+ function makeInstanceKey(userId, topic) {
946
+ return resolvePlaywrightTopicBinding(userId, topic).instanceKey;
947
+ }
948
+ function resolvePlaywrightTopicBinding(userId, topic) {
949
+ return validateProfileBinding(managerHost.resolveTopicBinding(userId, topic));
950
+ }
951
+ function defaultTopicBinding(userId, topic) {
952
+ if (!topic)
953
+ return managerHost.resolveNamedBinding(userId, "default");
954
+ const ownerId = getBrowserProfileOwner(topic, userId);
955
+ const profile = migrateLegacyTopicProfile(ownerId, topic);
956
+ return managerHost.resolveNamedBinding(ownerId, profile);
957
+ }
958
+ function makeBrowserProfileInstanceKey(ownerId, rawProfile) {
959
+ return resolvePlaywrightProfileBinding(ownerId, rawProfile).instanceKey;
960
+ }
961
+ function resolvePlaywrightProfileBinding(ownerId, rawProfile) {
962
+ return validateProfileBinding(managerHost.resolveNamedBinding(ownerId, rawProfile));
963
+ }
964
+ function validateProfileBinding(binding) {
965
+ if (!binding || typeof binding.instanceKey !== "string" || !binding.instanceKey.trim() || typeof binding.ownerId !== "string" || !binding.ownerId.trim() || typeof binding.profile !== "string" || !binding.profile.trim()) {
966
+ throw new Error("invalid Playwright profile binding");
967
+ }
968
+ return Object.freeze({ ...binding });
969
+ }
970
+ function defaultNamedBinding(ownerId, rawProfile) {
971
+ const profile = normalizeBrowserProfileName(rawProfile);
972
+ const instanceKey = `profile:${encodeURIComponent(ownerId)}:${profile}`;
973
+ return {
974
+ instanceKey,
975
+ ownerId,
976
+ profile
977
+ };
978
+ }
979
+ function legacyBrowserProfileName(topic) {
980
+ return `legacy_${createHash("sha256").update(topic).digest("hex").slice(0, 12)}`;
981
+ }
982
+ function migrateLegacyTopicProfile(ownerId, topic) {
983
+ const current = getTopicBrowserProfile(topic);
984
+ if (current !== "default" || !hasBrowserProfileTopic(topic))
985
+ return current;
986
+ const legacyDir = resolve6(BROWSER_PROFILES_DIR, sanitizeTopicName(topic));
987
+ if (!existsSync2(legacyDir))
988
+ return current;
989
+ const profile = legacyBrowserProfileName(topic);
990
+ const profileDir = defaultProfileDir(ownerId, profile);
991
+ mkdirSync3(dirname3(profileDir), { recursive: true });
992
+ if (!existsSync2(profileDir))
993
+ renameSync(legacyDir, profileDir);
994
+ assignTopicBrowserProfile({ topicId: topic, actorUserId: ownerId, profile });
995
+ logger.info({ ownerId, topic, profile }, "Adopted legacy topic browser profile");
996
+ return profile;
997
+ }
998
+ function defaultChildEnvironment(context) {
999
+ const { capability, ownerId, proxy, browserRsBin, environment } = context;
1000
+ return {
1001
+ ...environment,
1002
+ NEGOTIUM_BROWSER_CAPABILITY: capability,
1003
+ NEGOTIUM_BROWSER_VAULT_USER_ID: ownerId,
1004
+ ...browserRsBin && !proxy ? { NEGOTIUM_BROWSER_RS_BIN: browserRsBin } : {},
1005
+ ...proxy ? {
1006
+ NEGOTIUM_BROWSER_PROXY_SERVER: proxy.server,
1007
+ ...proxy.username ? { NEGOTIUM_BROWSER_PROXY_USERNAME: proxy.username } : {},
1008
+ ...proxy.password ? { NEGOTIUM_BROWSER_PROXY_PASSWORD: proxy.password } : {},
1009
+ ...proxy.bypass ? { NEGOTIUM_BROWSER_PROXY_BYPASS: proxy.bypass } : {}
1010
+ } : {}
1011
+ };
1012
+ }
1013
+ var defaultManagerHost = Object.freeze({
1014
+ portsDir: PLAYWRIGHT_PORTS_DIR,
1015
+ basePort: PLAYWRIGHT_BASE_PORT,
1016
+ maxPort: PLAYWRIGHT_MAX_PORT,
1017
+ browserBin: PLAYWRIGHT_MCP_BIN,
1018
+ fallbackBrowserBin: PATCHRIGHT_MCP_BIN,
1019
+ browserRsBin: BROWSER_RS_BIN,
1020
+ resolveProxy: resolveBrowserProxy,
1021
+ resolveTopicBinding: defaultTopicBinding,
1022
+ resolveNamedBinding: defaultNamedBinding,
1023
+ resolveInstanceDataDir(instanceKey) {
1024
+ const { ownerId, profile } = parseInstanceKey(instanceKey);
1025
+ return defaultProfileDir(ownerId, profile);
1026
+ },
1027
+ createChildEnvironment: defaultChildEnvironment,
1028
+ cleanupBrowserProcessesForDataDir: killBrowserProcsForUserDataDir,
1029
+ reapOrphanBrowsers
1030
+ });
1031
+ var managerHost = defaultManagerHost;
1032
+ function configurePlaywrightManagerHost(overrides) {
1033
+ if (instances.size > 0 || spawning.size > 0 || pinnedInstances.size > 0) {
1034
+ throw new Error("cannot configure Playwright manager while browser instances are active");
1035
+ }
1036
+ const next = { ...managerHost, ...overrides };
1037
+ if (!next.portsDir || !Number.isInteger(next.basePort) || !Number.isInteger(next.maxPort)) {
1038
+ throw new Error("invalid Playwright manager paths or port range");
1039
+ }
1040
+ if (next.basePort < 1 || next.maxPort > 65535 || next.basePort > next.maxPort) {
1041
+ throw new Error("invalid Playwright manager port range");
1042
+ }
1043
+ if (!next.browserBin || !next.fallbackBrowserBin) {
1044
+ throw new Error("Playwright manager browser binaries are required");
1045
+ }
1046
+ if (next.resolveInstanceDataDir !== defaultManagerHost.resolveInstanceDataDir && (next.cleanupBrowserProcessesForDataDir === defaultManagerHost.cleanupBrowserProcessesForDataDir || next.reapOrphanBrowsers === defaultManagerHost.reapOrphanBrowsers)) {
1047
+ throw new Error("custom Playwright profile paths require host crash cleanup and orphan sweep hooks");
1048
+ }
1049
+ managerHost = Object.freeze(next);
1050
+ return managerHost;
1051
+ }
1052
+ function getPlaywrightManagerHost() {
1053
+ return managerHost;
1054
+ }
1055
+ function resetPlaywrightManagerHost() {
1056
+ configurePlaywrightManagerHost(defaultManagerHost);
1057
+ }
1058
+ function parseInstanceKey(instanceKey) {
1059
+ const match = /^profile:([^:]+):(.+)$/.exec(instanceKey);
1060
+ if (!match)
1061
+ return { ownerId: "legacy", profile: sanitizeTopicName(instanceKey) };
1062
+ return {
1063
+ ownerId: decodeURIComponent(match[1]),
1064
+ profile: normalizeBrowserProfileName(match[2])
1065
+ };
1066
+ }
1067
+ function portFileName(instanceKey) {
1068
+ return createHash("sha256").update(instanceKey).digest("hex").slice(0, 24);
1069
+ }
1070
+ function writePortFile(instanceKey, port) {
1071
+ try {
1072
+ mkdirSync3(managerHost.portsDir, { recursive: true });
1073
+ writeFileSync2(join3(managerHost.portsDir, portFileName(instanceKey)), String(port));
1074
+ } catch (e) {
1075
+ logger.warn({ err: e, instanceKey, port }, "Failed to save playwright port file");
1076
+ }
1077
+ }
1078
+ function deletePortFile(instanceKey) {
1079
+ try {
1080
+ unlinkSync2(join3(managerHost.portsDir, portFileName(instanceKey)));
1081
+ } catch (e) {
1082
+ logger.warn({ err: e, instanceKey }, "Failed to delete playwright port file");
1083
+ }
1084
+ }
1085
+ function readPortFile(instanceKey) {
1086
+ try {
1087
+ const port = Number.parseInt(readFileSync2(join3(managerHost.portsDir, portFileName(instanceKey)), "utf8").trim(), 10);
1088
+ return Number.isInteger(port) ? port : null;
1089
+ } catch {
1090
+ return null;
1091
+ }
1092
+ }
1093
+ var MAX_IDLE_MS = 2 * 60 * 60 * 1000;
1094
+ var instances = new Map;
1095
+ function assertInstanceOwner(instanceKey, ownerId) {
1096
+ const existing = instances.get(instanceKey);
1097
+ if (existing && existing.ownerId !== ownerId) {
1098
+ throw new Error(`Playwright profile binding owner changed for "${instanceKey}" (${existing.ownerId} -> ${ownerId})`);
1099
+ }
1100
+ }
1101
+ var usedPorts = new Set;
1102
+ var spawning = new Map;
1103
+ async function withPlaywrightInstanceMaintenance(rawKeys, operation) {
1104
+ const keys = [...new Set(rawKeys)].sort();
1105
+ const lockedKeys = new Set(keys);
1106
+ for (;; ) {
1107
+ const pending = keys.map((key) => spawning.get(key)).filter((promise) => promise !== undefined);
1108
+ if (pending.length > 0) {
1109
+ await Promise.allSettled(pending);
1110
+ continue;
1111
+ }
1112
+ let releaseBarrier;
1113
+ const barrier = new Promise((resolveBarrier) => {
1114
+ releaseBarrier = () => resolveBarrier(null);
1115
+ });
1116
+ for (const key of keys)
1117
+ spawning.set(key, barrier);
1118
+ try {
1119
+ return await operation({
1120
+ async stopInstance(instanceKey) {
1121
+ if (!lockedKeys.has(instanceKey)) {
1122
+ throw new Error(`Playwright maintenance does not own instance "${instanceKey}"`);
1123
+ }
1124
+ const process2 = instances.get(instanceKey)?.process;
1125
+ if (!process2)
1126
+ return false;
1127
+ killInstance(instanceKey);
1128
+ if (!await waitForChildProcessExit(process2, 3000)) {
1129
+ logger.warn({ instanceKey, pid: process2.pid }, "Playwright MCP ignored SIGTERM during profile maintenance");
1130
+ try {
1131
+ process2.kill("SIGKILL");
1132
+ } catch (error) {
1133
+ logger.warn({ err: error, instanceKey, pid: process2.pid }, "Failed to SIGKILL Playwright MCP during profile maintenance");
1134
+ }
1135
+ await waitForChildProcessExit(process2, 1000);
1136
+ }
1137
+ return true;
1138
+ }
1139
+ });
1140
+ } finally {
1141
+ for (const key of keys) {
1142
+ if (spawning.get(key) === barrier)
1143
+ spawning.delete(key);
1144
+ }
1145
+ releaseBarrier();
1146
+ }
1147
+ }
1148
+ }
1149
+ function stopPlaywrightInstance(instanceKey) {
1150
+ return withPlaywrightInstanceMaintenance([instanceKey], ({ stopInstance }) => stopInstance(instanceKey));
1151
+ }
1152
+ function withPlaywrightProfileMaintenance(ownerId, rawProfile, operation) {
1153
+ const binding = resolvePlaywrightProfileBinding(ownerId, rawProfile);
1154
+ return withPlaywrightInstanceMaintenance([binding.instanceKey], async (control) => {
1155
+ assertInstanceOwner(binding.instanceKey, binding.ownerId);
1156
+ return operation(binding, control);
1157
+ });
1158
+ }
1159
+ function stopPlaywrightProfile(ownerId, rawProfile) {
1160
+ return withPlaywrightProfileMaintenance(ownerId, rawProfile, (binding, { stopInstance }) => stopInstance(binding.instanceKey));
1161
+ }
1162
+ var pinnedInstances = new Map;
1163
+ function pinPlaywrightInstance(instanceKey) {
1164
+ pinnedInstances.set(instanceKey, (pinnedInstances.get(instanceKey) ?? 0) + 1);
1165
+ }
1166
+ function unpinPlaywrightInstance(instanceKey) {
1167
+ const count = pinnedInstances.get(instanceKey);
1168
+ if (count === undefined)
1169
+ return;
1170
+ if (count <= 1)
1171
+ pinnedInstances.delete(instanceKey);
1172
+ else
1173
+ pinnedInstances.set(instanceKey, count - 1);
1174
+ }
1175
+ function getPlaywrightCapability(instanceKey) {
1176
+ return instances.get(instanceKey)?.capability;
1177
+ }
1178
+ function resolvePlaywrightCapabilityOwner(capability) {
1179
+ if (!capability)
1180
+ return;
1181
+ const provided = Buffer.from(capability);
1182
+ for (const instance of instances.values()) {
1183
+ const expected = Buffer.from(instance.capability);
1184
+ if (provided.length === expected.length && timingSafeEqual(provided, expected)) {
1185
+ return instance.ownerId;
1186
+ }
1187
+ }
1188
+ return;
1189
+ }
1190
+ function reapPlaywrightOrphans() {
1191
+ managerHost.reapOrphanBrowsers([...instances.keys()].map(resolveUserDataDir));
1192
+ }
1193
+ var playwrightFailureHandlers = new Set;
1194
+ function onPlaywrightFailure(handler) {
1195
+ playwrightFailureHandlers.add(handler);
1196
+ return () => playwrightFailureHandlers.delete(handler);
1197
+ }
1198
+ function notifyPlaywrightFailure(instanceKey, failure) {
1199
+ for (const handler of playwrightFailureHandlers) {
1200
+ try {
1201
+ handler(instanceKey, failure);
1202
+ } catch (err) {
1203
+ logger.warn({ err, instanceKey, failure }, "Playwright failure handler failed");
1204
+ }
1205
+ }
1206
+ }
1207
+ function evictIdleInstance() {
1208
+ const now = Date.now();
1209
+ const oldestKey = selectIdleEvictionKey(instances, pinnedInstances.keys(), spawning.keys(), now, MAX_IDLE_MS);
1210
+ if (oldestKey) {
1211
+ const instance = instances.get(oldestKey);
1212
+ if (!instance)
1213
+ return null;
1214
+ const idleMin = ((now - instance.lastUsedAt) / 60000).toFixed(0);
1215
+ logger.info({ key: oldestKey, idleMin }, "Evicting idle playwright instance");
1216
+ killInstance(oldestKey);
1217
+ return instance.port;
1218
+ }
1219
+ return null;
1220
+ }
1221
+ async function allocatePort(expectedUserDataDir) {
1222
+ for (let port = managerHost.basePort;port <= managerHost.maxPort; port++) {
1223
+ if (usedPorts.has(port))
1224
+ continue;
1225
+ usedPorts.add(port);
1226
+ if (isPortInUse(port)) {
1227
+ logger.warn({ port }, "Port occupied by external process, attempting cleanup");
1228
+ await killPlaywrightOnPort(port, expectedUserDataDir);
1229
+ if (isPortInUse(port)) {
1230
+ usedPorts.delete(port);
1231
+ continue;
1232
+ }
1233
+ }
1234
+ return port;
1235
+ }
1236
+ const evictedPort = evictIdleInstance();
1237
+ if (evictedPort !== null) {
1238
+ await waitForPortRelease(evictedPort);
1239
+ const reusablePort = selectReusablePort(managerHost.basePort, managerHost.maxPort, usedPorts, isPortInUse);
1240
+ if (reusablePort !== null) {
1241
+ usedPorts.add(reusablePort);
1242
+ return reusablePort;
1243
+ }
1244
+ }
1245
+ throw new Error(`No available ports for Playwright MCP (${instances.size} active instances, range ${managerHost.basePort}-${managerHost.maxPort})`);
1246
+ }
1247
+ function releasePort(port) {
1248
+ usedPorts.delete(port);
1249
+ }
1250
+ function ownerDirectory(ownerId) {
1251
+ const digest = createHash("sha256").update(ownerId).digest("hex").slice(0, 16);
1252
+ return `${sanitizeTopicName(ownerId).slice(0, 24)}_${digest}`;
1253
+ }
1254
+ function defaultProfileDir(ownerId, profile) {
1255
+ return resolve6(BROWSER_PROFILES_DIR, "profiles", ownerDirectory(ownerId), profile);
1256
+ }
1257
+ function resolveUserDataDir(instanceKey) {
1258
+ return managerHost.resolveInstanceDataDir(instanceKey);
1259
+ }
1260
+ function killInstance(instanceKey, opts) {
1261
+ const inst = instances.get(instanceKey);
1262
+ if (!inst)
1263
+ return;
1264
+ if (inst.process.pid) {
1265
+ killProcessTreeChildren(inst.process.pid);
1266
+ }
1267
+ try {
1268
+ inst.process.kill("SIGTERM");
1269
+ } catch (e) {
1270
+ logger.warn({ err: e, instanceKey }, "Failed to kill playwright instance");
1271
+ }
1272
+ inst.process.removeAllListeners("error");
1273
+ inst.process.removeAllListeners("exit");
1274
+ cleanSingletonFiles(resolveUserDataDir(instanceKey));
1275
+ if (!opts?.keepPort)
1276
+ releasePort(inst.port);
1277
+ instances.delete(instanceKey);
1278
+ deletePortFile(instanceKey);
1279
+ logger.info({ instanceKey, port: inst.port, keepPort: !!opts?.keepPort }, "Killed Playwright MCP (with cleanup)");
1280
+ }
1281
+ async function spawnPlaywright(instanceKey, ownerId, reservedPort, browserBin = managerHost.browserBin, allowFallback = true) {
1282
+ const userDataDir = resolveUserDataDir(instanceKey);
1283
+ const port = reservedPort ?? await allocatePort(userDataDir);
1284
+ mkdirSync3(userDataDir, { recursive: true });
1285
+ const mcpArgs = [
1286
+ "--port",
1287
+ String(port),
1288
+ "--host",
1289
+ "127.0.0.1",
1290
+ "--headed",
1291
+ "--user-data-dir",
1292
+ userDataDir
1293
+ ];
1294
+ const proxy = managerHost.resolveProxy();
1295
+ const capability = randomBytes2(32).toString("hex");
1296
+ const childEnv = {
1297
+ ...managerHost.createChildEnvironment({
1298
+ instanceKey,
1299
+ ownerId,
1300
+ capability,
1301
+ proxy,
1302
+ browserRsBin: managerHost.browserRsBin,
1303
+ environment: process.env
1304
+ }),
1305
+ NEGOTIUM_BROWSER_CAPABILITY: capability
1306
+ };
1307
+ if (proxy) {
1308
+ logger.info({ instanceKey, proxyServer: proxy.server }, "Browser egress proxy enabled");
1309
+ }
1310
+ const command = browserBin.endsWith(".mjs") ? process.execPath : browserBin;
1311
+ const args = browserBin.endsWith(".mjs") ? [browserBin, ...mcpArgs] : mcpArgs;
1312
+ let spawnSpec;
1313
+ try {
1314
+ spawnSpec = resolveHeadedPlaywrightSpawn(command, args, { environment: childEnv });
1315
+ } catch (err) {
1316
+ releasePort(port);
1317
+ throw err;
1318
+ }
1319
+ let proc;
1320
+ try {
1321
+ proc = spawn(spawnSpec.command, spawnSpec.args, {
1322
+ stdio: "ignore",
1323
+ detached: false,
1324
+ env: childEnv
1325
+ });
1326
+ } catch (err) {
1327
+ releasePort(port);
1328
+ throw err;
1329
+ }
1330
+ const spawnError = waitForChildProcessSpawnError(proc);
1331
+ const reapCrashedBrowser = () => {
1332
+ const userDataDir2 = resolveUserDataDir(instanceKey);
1333
+ managerHost.cleanupBrowserProcessesForDataDir(userDataDir2);
1334
+ cleanSingletonFiles(userDataDir2);
1335
+ };
1336
+ proc.once("error", (err) => {
1337
+ logger.error({ err, instanceKey }, "Playwright MCP error");
1338
+ if (instances.get(instanceKey)?.process === proc) {
1339
+ releasePort(port);
1340
+ instances.delete(instanceKey);
1341
+ reapCrashedBrowser();
1342
+ notifyPlaywrightFailure(instanceKey, {
1343
+ reason: "process-error",
1344
+ error: err instanceof Error ? err.message : String(err)
1345
+ });
1346
+ }
1347
+ });
1348
+ proc.once("exit", (code) => {
1349
+ logger.info({ instanceKey, code }, "Playwright MCP exited");
1350
+ const wasOurs = instances.get(instanceKey)?.process === proc;
1351
+ if (wasOurs) {
1352
+ releasePort(port);
1353
+ instances.delete(instanceKey);
1354
+ reapCrashedBrowser();
1355
+ notifyPlaywrightFailure(instanceKey, { reason: "exit", code });
1356
+ }
1357
+ });
1358
+ const now = Date.now();
1359
+ instances.set(instanceKey, {
1360
+ process: proc,
1361
+ port,
1362
+ ownerId,
1363
+ startedAt: now,
1364
+ lastUsedAt: now,
1365
+ capability
1366
+ });
1367
+ const ready = await Promise.race([
1368
+ (async () => await waitForServer(port, 1e4) && await supportsOwnerCleanup(port, capability) && await probePlaywrightMcpTransports(port, capability))(),
1369
+ spawnError
1370
+ ]);
1371
+ if (!ready) {
1372
+ const exitCode = proc.exitCode;
1373
+ killInstance(instanceKey);
1374
+ if (allowFallback && browserBin !== managerHost.fallbackBrowserBin) {
1375
+ logger.warn({ instanceKey, browserBin, fallback: managerHost.fallbackBrowserBin }, "Preferred browser MCP unavailable or lacks owner isolation; using Patchright fallback");
1376
+ return spawnPlaywright(instanceKey, ownerId, undefined, managerHost.fallbackBrowserBin, false);
1377
+ }
1378
+ throw new Error(`Playwright MCP failed health check after spawn on port ${port}` + (exitCode === null ? "" : ` (exitCode=${exitCode})`));
1379
+ }
1380
+ writePortFile(instanceKey, port);
1381
+ logger.info({ instanceKey, port, pid: proc.pid, ready, virtualDisplay: spawnSpec.virtualDisplay }, "Playwright MCP started");
1382
+ return port;
1383
+ }
1384
+ async function supportsOwnerCleanup(port, capability) {
1385
+ try {
1386
+ const query = new URLSearchParams({ owner: "__negotium_capability_probe__" });
1387
+ const response = await fetch(`http://127.0.0.1:${port}/owners?${query}`, {
1388
+ method: "DELETE",
1389
+ headers: {
1390
+ "X-Browser-Capability": capability
1391
+ },
1392
+ signal: AbortSignal.timeout(2000)
1393
+ });
1394
+ await response.body?.cancel();
1395
+ return response.ok;
1396
+ } catch {
1397
+ return false;
1398
+ }
1399
+ }
1400
+ async function ensurePlaywright(userId, topic) {
1401
+ const binding = resolvePlaywrightTopicBinding(userId, topic);
1402
+ const { instanceKey, ownerId } = binding;
1403
+ const inProgress = spawning.get(instanceKey);
1404
+ if (inProgress) {
1405
+ const port = await inProgress;
1406
+ assertInstanceOwner(instanceKey, ownerId);
1407
+ if (port !== null)
1408
+ return port;
1409
+ return ensurePlaywright(userId, topic);
1410
+ }
1411
+ const promise = (async () => {
1412
+ const existing = instances.get(instanceKey);
1413
+ assertInstanceOwner(instanceKey, ownerId);
1414
+ if (existing && !existing.process.killed && existing.process.exitCode === null) {
1415
+ if (await probePlaywrightMcpTransports(existing.port, existing.capability)) {
1416
+ existing.lastUsedAt = Date.now();
1417
+ return existing.port;
1418
+ }
1419
+ logger.warn({ instanceKey }, "Playwright MCP unresponsive, restarting");
1420
+ const oldPort = existing.port;
1421
+ killInstance(instanceKey);
1422
+ notifyPlaywrightFailure(instanceKey, { reason: "unhealthy" });
1423
+ await waitForPortRelease(oldPort);
1424
+ cleanSingletonFiles(resolveUserDataDir(instanceKey));
1425
+ } else if (existing) {
1426
+ releasePort(existing.port);
1427
+ instances.delete(instanceKey);
1428
+ cleanSingletonFiles(resolveUserDataDir(instanceKey));
1429
+ notifyPlaywrightFailure(instanceKey, {
1430
+ reason: "exit",
1431
+ code: existing.process.exitCode
1432
+ });
1433
+ }
1434
+ return spawnPlaywright(instanceKey, ownerId);
1435
+ })().finally(() => spawning.delete(instanceKey));
1436
+ spawning.set(instanceKey, promise);
1437
+ return promise;
1438
+ }
1439
+ async function waitForPortRelease(port, timeoutMs = 3000) {
1440
+ const start = Date.now();
1441
+ while (Date.now() - start < timeoutMs) {
1442
+ if (!isPortInUse(port))
1443
+ return;
1444
+ await delay(200);
1445
+ }
1446
+ }
1447
+ async function ensureBrowserProfile(ownerId, rawProfile) {
1448
+ const binding = resolvePlaywrightProfileBinding(ownerId, rawProfile);
1449
+ const instanceKey = binding.instanceKey;
1450
+ const inProgress = spawning.get(instanceKey);
1451
+ if (inProgress) {
1452
+ const port = await inProgress;
1453
+ assertInstanceOwner(instanceKey, binding.ownerId);
1454
+ const capability = instances.get(instanceKey)?.capability;
1455
+ if (port !== null && capability && await probePlaywrightMcpTransports(port, capability)) {
1456
+ return port;
1457
+ }
1458
+ if (port === null)
1459
+ return ensureBrowserProfile(ownerId, rawProfile);
1460
+ throw new Error(`Browser profile "${rawProfile}" failed to start.`);
1461
+ }
1462
+ const promise = (async () => {
1463
+ const publishedPort = readPortFile(instanceKey);
1464
+ const existing = instances.get(instanceKey);
1465
+ assertInstanceOwner(instanceKey, binding.ownerId);
1466
+ if (publishedPort !== null && !existing) {
1467
+ logger.warn({ instanceKey, publishedPort }, "Recycling untracked browser profile process from stale port file");
1468
+ await killPlaywrightOnPort(publishedPort, resolveUserDataDir(instanceKey));
1469
+ await waitForPortRelease(publishedPort);
1470
+ deletePortFile(instanceKey);
1471
+ }
1472
+ if (existing && !existing.process.killed && existing.process.exitCode === null) {
1473
+ if (await probePlaywrightMcpTransports(existing.port, existing.capability)) {
1474
+ existing.lastUsedAt = Date.now();
1475
+ return existing.port;
1476
+ }
1477
+ const oldPort = existing.port;
1478
+ killInstance(instanceKey);
1479
+ notifyPlaywrightFailure(instanceKey, { reason: "unhealthy" });
1480
+ await waitForPortRelease(oldPort);
1481
+ cleanSingletonFiles(resolveUserDataDir(instanceKey));
1482
+ } else if (existing) {
1483
+ releasePort(existing.port);
1484
+ instances.delete(instanceKey);
1485
+ cleanSingletonFiles(resolveUserDataDir(instanceKey));
1486
+ notifyPlaywrightFailure(instanceKey, {
1487
+ reason: "exit",
1488
+ code: existing.process.exitCode
1489
+ });
1490
+ }
1491
+ const port = await spawnPlaywright(instanceKey, binding.ownerId);
1492
+ const capability = instances.get(instanceKey)?.capability;
1493
+ if (!capability || !await probePlaywrightMcpTransports(port, capability)) {
1494
+ killInstance(instanceKey);
1495
+ throw new Error(`Browser profile "${rawProfile}" did not pass its health check.`);
1496
+ }
1497
+ return port;
1498
+ })().finally(() => spawning.delete(instanceKey));
1499
+ spawning.set(instanceKey, promise);
1500
+ return promise;
1501
+ }
1502
+ async function closeBrowserOwnerTabs(ownerId, rawProfile, owner) {
1503
+ const binding = resolvePlaywrightProfileBinding(ownerId, rawProfile);
1504
+ const instanceKey = binding.instanceKey;
1505
+ const inProgress = spawning.get(instanceKey);
1506
+ if (inProgress)
1507
+ await inProgress;
1508
+ assertInstanceOwner(instanceKey, binding.ownerId);
1509
+ const instance = instances.get(instanceKey);
1510
+ if (!instance)
1511
+ return 0;
1512
+ const port = instance.port;
1513
+ const query = new URLSearchParams({ owner });
1514
+ const response = await fetch(`http://127.0.0.1:${port}/owners?${query}`, {
1515
+ method: "DELETE",
1516
+ headers: {
1517
+ "X-Browser-Capability": instance.capability
1518
+ },
1519
+ signal: AbortSignal.timeout(5000)
1520
+ });
1521
+ if (!response.ok)
1522
+ throw new Error(`browser MCP owner cleanup failed (${response.status})`);
1523
+ const result = await response.json();
1524
+ return typeof result.closed === "number" ? result.closed : 0;
1525
+ }
1526
+ async function waitForServer(port, timeoutMs) {
1527
+ const start = Date.now();
1528
+ while (Date.now() - start < timeoutMs) {
1529
+ if (await isHealthy(port))
1530
+ return true;
1531
+ await delay(300);
1532
+ }
1533
+ logger.warn({ port, timeoutMs }, "Playwright MCP not ready before timeout");
1534
+ return false;
1535
+ }
1536
+ function resolveTopicProfileDir(userId, topic) {
1537
+ return resolveUserDataDir(makeInstanceKey(userId, topic));
1538
+ }
1539
+ async function killAllPlaywright() {
1540
+ const procs = [...instances.entries()].map(([key, inst]) => ({ key, proc: inst.process }));
1541
+ for (const { key } of procs)
1542
+ killInstance(key);
1543
+ await Promise.all(procs.map(async ({ key, proc }) => {
1544
+ if (await waitForChildProcessExit(proc, 3000))
1545
+ return;
1546
+ logger.warn({ instanceKey: key, pid: proc.pid }, "Playwright MCP ignored SIGTERM");
1547
+ try {
1548
+ proc.kill("SIGKILL");
1549
+ } catch (err) {
1550
+ logger.warn({ err, instanceKey: key, pid: proc.pid }, "Failed to SIGKILL Playwright MCP");
1551
+ }
1552
+ if (!await waitForChildProcessExit(proc, 1000)) {
1553
+ logger.warn({ instanceKey: key, pid: proc.pid }, "Playwright MCP did not report exit");
1554
+ }
1555
+ }));
1556
+ }
1557
+ setInterval(() => {
1558
+ while (evictIdleInstance() !== null) {}
1559
+ try {
1560
+ reapPlaywrightOrphans();
1561
+ } catch (e) {
1562
+ logger.debug({ err: e }, "reapOrphanBrowsers sweep failed");
1563
+ }
1564
+ }, 30 * 60 * 1000).unref();
1565
+ export {
1566
+ withPlaywrightProfileMaintenance,
1567
+ withPlaywrightInstanceMaintenance,
1568
+ waitForChildProcessSpawnError,
1569
+ waitForChildProcessExit,
1570
+ unpinPlaywrightInstance,
1571
+ stopPlaywrightProfile,
1572
+ stopPlaywrightInstance,
1573
+ selectReusablePort,
1574
+ selectOrphanBrowserPids,
1575
+ selectIdleEvictionKey,
1576
+ resolveTopicProfileDir,
1577
+ resolvePlaywrightTopicBinding,
1578
+ resolvePlaywrightProfileBinding,
1579
+ resolvePlaywrightCapabilityOwner,
1580
+ resetPlaywrightManagerHost,
1581
+ reapPlaywrightOrphans,
1582
+ probePlaywrightMcpTransports,
1583
+ probeMcpTransport,
1584
+ pinPlaywrightInstance,
1585
+ onPlaywrightFailure,
1586
+ makeInstanceKey,
1587
+ makeBrowserProfileInstanceKey,
1588
+ killAllPlaywright,
1589
+ isBrowserJanitorOwner,
1590
+ getPlaywrightManagerHost,
1591
+ getPlaywrightCapability,
1592
+ extractUserDataDirArg,
1593
+ ensurePlaywright,
1594
+ ensureBrowserProfile,
1595
+ configurePlaywrightManagerHost,
1596
+ closeBrowserOwnerTabs,
1597
+ browserProcessMatchesExpectedProfile
1598
+ };
1599
+
1600
+ //# debugId=712A86561C9BF9CC64756E2164756E21