negotium 0.1.25 → 0.1.26

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.
@@ -0,0 +1,61 @@
1
+ import { accessSync, constants } from "node:fs";
2
+ import { delimiter, isAbsolute, resolve } from "node:path";
3
+
4
+ export interface HeadedPlaywrightSpawnSpec {
5
+ command: string;
6
+ args: string[];
7
+ virtualDisplay: boolean;
8
+ }
9
+
10
+ interface HeadedPlaywrightSpawnOptions {
11
+ platform?: NodeJS.Platform;
12
+ environment?: NodeJS.ProcessEnv;
13
+ findExecutable?: (command: string, environment: NodeJS.ProcessEnv) => string | null;
14
+ }
15
+
16
+ function findExecutableOnPath(
17
+ command: string,
18
+ environment: NodeJS.ProcessEnv = process.env,
19
+ ): string | null {
20
+ const candidates = isAbsolute(command)
21
+ ? [command]
22
+ : (environment.PATH ?? "")
23
+ .split(delimiter)
24
+ .filter(Boolean)
25
+ .map((directory) => resolve(directory, command));
26
+ for (const candidate of candidates) {
27
+ try {
28
+ accessSync(candidate, constants.X_OK);
29
+ return candidate;
30
+ } catch {
31
+ // Keep searching PATH.
32
+ }
33
+ }
34
+ return null;
35
+ }
36
+
37
+ /** Select the safe argv-only launcher for a visible browser MCP. */
38
+ export function resolveHeadedPlaywrightSpawn(
39
+ command: string,
40
+ args: readonly string[],
41
+ options: HeadedPlaywrightSpawnOptions = {},
42
+ ): HeadedPlaywrightSpawnSpec {
43
+ const platform = options.platform ?? process.platform;
44
+ const environment = options.environment ?? process.env;
45
+ const hasDisplay = Boolean(environment.DISPLAY?.trim() || environment.WAYLAND_DISPLAY?.trim());
46
+ if (platform !== "linux" || hasDisplay) {
47
+ return { command, args: [...args], virtualDisplay: false };
48
+ }
49
+
50
+ const xvfbRun = (options.findExecutable ?? findExecutableOnPath)("xvfb-run", environment);
51
+ if (!xvfbRun) {
52
+ throw new Error(
53
+ "Headed Playwright on Linux requires DISPLAY/WAYLAND_DISPLAY or xvfb-run; install Xvfb and ensure xvfb-run is on PATH",
54
+ );
55
+ }
56
+ return {
57
+ command: xvfbRun,
58
+ args: ["-a", "-s", "-screen 0 1440x1000x24", command, ...args],
59
+ virtualDisplay: true,
60
+ };
61
+ }
@@ -22,8 +22,11 @@ import {
22
22
  resolveBrowserProxy,
23
23
  } from "#platform/config";
24
24
  import { delay } from "#platform/delay";
25
-
26
25
  import { logger } from "#platform/logger";
26
+ import {
27
+ type HeadedPlaywrightSpawnSpec,
28
+ resolveHeadedPlaywrightSpawn,
29
+ } from "#platform/playwright/headed-launch";
27
30
  import { sanitizeTopicName } from "#security/sanitize";
28
31
  import {
29
32
  assignTopicBrowserProfile,
@@ -783,11 +786,25 @@ async function spawnPlaywright(
783
786
 
784
787
  const command = browserBin.endsWith(".mjs") ? process.execPath : browserBin;
785
788
  const args = browserBin.endsWith(".mjs") ? [browserBin, ...mcpArgs] : mcpArgs;
786
- const proc = spawn(command, args, {
787
- stdio: "ignore",
788
- detached: false,
789
- env: childEnv,
790
- });
789
+ let spawnSpec: HeadedPlaywrightSpawnSpec;
790
+ try {
791
+ spawnSpec = resolveHeadedPlaywrightSpawn(command, args, { environment: childEnv });
792
+ } catch (err) {
793
+ releasePort(port);
794
+ throw err;
795
+ }
796
+ let proc: ChildProcess;
797
+ try {
798
+ proc = spawn(spawnSpec.command, spawnSpec.args, {
799
+ stdio: "ignore",
800
+ detached: false,
801
+ env: childEnv,
802
+ });
803
+ } catch (err) {
804
+ releasePort(port);
805
+ throw err;
806
+ }
807
+ const spawnError = waitForChildProcessSpawnError(proc);
791
808
 
792
809
  // When the MCP dies on its own (crash / OOM), its Chrome subtree is reparented
793
810
  // to init and escapes the tracked-instance map. Reap it by user-data-dir here
@@ -835,8 +852,11 @@ async function spawnPlaywright(
835
852
  capability,
836
853
  });
837
854
 
838
- const ready =
839
- (await waitForServer(port, 10_000)) && (await supportsOwnerCleanup(port, capability));
855
+ const ready = await Promise.race([
856
+ (async () =>
857
+ (await waitForServer(port, 10_000)) && (await supportsOwnerCleanup(port, capability)))(),
858
+ spawnError,
859
+ ]);
840
860
  if (!ready) {
841
861
  const exitCode = proc.exitCode;
842
862
  killInstance(instanceKey);
@@ -853,7 +873,10 @@ async function spawnPlaywright(
853
873
  );
854
874
  }
855
875
  writePortFile(instanceKey, port);
856
- logger.info({ instanceKey, port, pid: proc.pid, ready }, "Playwright MCP started");
876
+ logger.info(
877
+ { instanceKey, port, pid: proc.pid, ready, virtualDisplay: spawnSpec.virtualDisplay },
878
+ "Playwright MCP started",
879
+ );
857
880
  return port;
858
881
  }
859
882
 
@@ -1234,6 +1257,13 @@ export function waitForChildProcessExit(proc: ChildProcess, timeoutMs: number):
1234
1257
  });
1235
1258
  }
1236
1259
 
1260
+ /** Reject immediately when the OS cannot launch a child process. */
1261
+ export function waitForChildProcessSpawnError(proc: Pick<ChildProcess, "once">): Promise<never> {
1262
+ return new Promise<never>((_resolve, reject) => {
1263
+ proc.once("error", reject);
1264
+ });
1265
+ }
1266
+
1237
1267
  // Proactively evict all idle instances every 30 minutes
1238
1268
  // Every 30 minutes: evict idle instances, then reap any orphaned browser the
1239
1269
  // tracked-instance map has lost sight of. The orphan reap is only a backstop —
@@ -1 +1 @@
1
- export const NEGOTIUM_VERSION = "0.1.25";
1
+ export const NEGOTIUM_VERSION = "0.1.26";
@@ -1 +1 @@
1
- export declare const NEGOTIUM_VERSION = "0.1.25";
1
+ export declare const NEGOTIUM_VERSION = "0.1.26";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "negotium",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "type": "module",
5
5
  "description": "Install the Negotium multi-agent runtime and CLI with one package",
6
6
  "license": "Apache-2.0",