doer-agent 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,7 +11,6 @@
11
11
  주요 엔트리 포인트:
12
12
 
13
13
  - `doer-agent`: 에이전트 본체 CLI
14
- - `playwright-mcp-call`: Playwright MCP 호출용 CLI
15
14
  - `codex`: Codex 래퍼 CLI
16
15
 
17
16
  ## 요구 사항
package/dist/agent.js CHANGED
@@ -1,14 +1,10 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
- import { existsSync } from "node:fs";
3
- import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
4
- import net from "node:net";
5
- import { arch, homedir } from "node:os";
2
+ import { existsSync, statSync } from "node:fs";
3
+ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
6
5
  import path from "node:path";
7
6
  import { fileURLToPath } from "node:url";
8
7
  import { AckPolicy, connect, DeliverPolicy, JSONCodec, RetentionPolicy, StorageType } from "nats";
9
- const PLAYWRIGHT_SKIP_BROWSER_GC = "1";
10
- const PLAYWRIGHT_MCP_DAEMON_IDLE_TTL_SECONDS_DEFAULT = 10800;
11
- const PLAYWRIGHT_MCP_DAEMON_SIGNATURE_VERSION = "2026-03-15";
12
8
  const DEFAULT_SERVER_BASE_URL = "https://doer.cranix.net";
13
9
  const AGENT_MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
14
10
  const AGENT_PROJECT_DIR = path.join(AGENT_MODULE_DIR, "..");
@@ -144,18 +140,6 @@ function resolveCodexHomePath() {
144
140
  function parseEnvBoolean(value) {
145
141
  return value?.trim().toLowerCase() === "true";
146
142
  }
147
- function parseEnvStringArray(value) {
148
- if (!value?.trim()) {
149
- return [];
150
- }
151
- try {
152
- const parsed = JSON.parse(value);
153
- return Array.isArray(parsed) && parsed.every((item) => typeof item === "string") ? parsed : [];
154
- }
155
- catch {
156
- return [];
157
- }
158
- }
159
143
  function parseEnvInteger(value, fallback) {
160
144
  const normalized = value?.trim();
161
145
  if (!normalized) {
@@ -164,211 +148,6 @@ function parseEnvInteger(value, fallback) {
164
148
  const parsed = Number.parseInt(normalized, 10);
165
149
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
166
150
  }
167
- function resolvePlaywrightMcpProxyPath() {
168
- const candidates = [
169
- path.join(AGENT_PROJECT_DIR, "runtime/bin/doer-mcp-proxy"),
170
- path.join(process.cwd(), "agent/runtime/bin/doer-mcp-proxy"),
171
- path.join(process.cwd(), "runtime/bin/doer-mcp-proxy"),
172
- ];
173
- for (const candidate of candidates) {
174
- if (existsSync(candidate)) {
175
- return candidate;
176
- }
177
- }
178
- return "";
179
- }
180
- const PLAYWRIGHT_MCP_PROXY_LAUNCHER_PATH = path.join(AGENT_PROJECT_DIR, "runtime/bin/playwright-mcp-proxy-launcher.sh");
181
- function resolvePlaywrightMcpDaemonStatePaths() {
182
- const daemonDir = path.join(resolveAgentStateDir(), "playwright-mcp-daemon");
183
- return {
184
- daemonDir,
185
- socketPath: path.join(daemonDir, "playwright-mcp.sock"),
186
- pidPath: path.join(daemonDir, "daemon.pid"),
187
- metaPath: path.join(daemonDir, "daemon-meta.json"),
188
- };
189
- }
190
- function parseEnvAssignmentArgs(values) {
191
- const envPatch = {};
192
- for (const value of values) {
193
- const separatorIndex = value.indexOf("=");
194
- if (separatorIndex <= 0) {
195
- continue;
196
- }
197
- const key = value.slice(0, separatorIndex).trim();
198
- const envValue = value.slice(separatorIndex + 1);
199
- if (!key) {
200
- continue;
201
- }
202
- envPatch[key] = envValue;
203
- }
204
- return envPatch;
205
- }
206
- function escapeShellArg(value) {
207
- return `'${value.replace(/'/g, `'\"'\"'`)}'`;
208
- }
209
- async function readPidFile(pidPath) {
210
- const raw = await readFile(pidPath, "utf8").catch(() => "");
211
- const parsed = Number.parseInt(raw.trim(), 10);
212
- return Number.isInteger(parsed) && parsed > 1 ? parsed : null;
213
- }
214
- function isProcessAlive(pid) {
215
- try {
216
- process.kill(pid, 0);
217
- return true;
218
- }
219
- catch {
220
- return false;
221
- }
222
- }
223
- async function waitForPlaywrightMcpSocketReady(socketPath, timeoutMs) {
224
- const startedAt = Date.now();
225
- while (Date.now() - startedAt <= timeoutMs) {
226
- const isReady = await new Promise((resolve) => {
227
- const socket = net.createConnection({ path: socketPath });
228
- let settled = false;
229
- const finish = (value) => {
230
- if (settled) {
231
- return;
232
- }
233
- settled = true;
234
- resolve(value);
235
- };
236
- socket.once("connect", () => {
237
- socket.end();
238
- finish(true);
239
- });
240
- socket.once("error", () => {
241
- finish(false);
242
- });
243
- setTimeout(() => {
244
- socket.destroy();
245
- finish(false);
246
- }, 250).unref?.();
247
- });
248
- if (isReady) {
249
- return true;
250
- }
251
- await sleep(120);
252
- }
253
- return false;
254
- }
255
- async function stopPlaywrightMcpDaemon(paths) {
256
- const pid = await readPidFile(paths.pidPath);
257
- if (pid && isProcessAlive(pid)) {
258
- try {
259
- process.kill(pid, "SIGTERM");
260
- }
261
- catch {
262
- // ignore
263
- }
264
- const waitStartedAt = Date.now();
265
- while (Date.now() - waitStartedAt < 1800 && isProcessAlive(pid)) {
266
- await sleep(120);
267
- }
268
- if (isProcessAlive(pid)) {
269
- try {
270
- process.kill(pid, "SIGKILL");
271
- }
272
- catch {
273
- // ignore
274
- }
275
- }
276
- }
277
- await Promise.all([
278
- rename(paths.socketPath, `${paths.socketPath}.stale.${Date.now()}`).catch(() => undefined),
279
- rename(paths.pidPath, `${paths.pidPath}.stale.${Date.now()}`).catch(() => undefined),
280
- rename(paths.metaPath, `${paths.metaPath}.stale.${Date.now()}`).catch(() => undefined),
281
- ]);
282
- }
283
- async function ensureManagedPlaywrightMcpDaemon(args) {
284
- const paths = resolvePlaywrightMcpDaemonStatePaths();
285
- await mkdir(paths.daemonDir, { recursive: true });
286
- const daemonCommand = args.command;
287
- const targetEnvPatch = parseEnvAssignmentArgs(args.browserEnvArgs);
288
- const daemonCommandArgs = args.daemonArgs;
289
- const signature = JSON.stringify({
290
- version: PLAYWRIGHT_MCP_DAEMON_SIGNATURE_VERSION,
291
- daemonCommand,
292
- daemonCommandArgs,
293
- targetEnvPatch,
294
- idleTtlSeconds: parseEnvInteger(process.env.DOER_PLAYWRIGHT_MCP_DAEMON_IDLE_TTL_SECONDS, PLAYWRIGHT_MCP_DAEMON_IDLE_TTL_SECONDS_DEFAULT),
295
- });
296
- const existingMetaRaw = await readFile(paths.metaPath, "utf8").catch(() => "");
297
- let existingMeta = null;
298
- if (existingMetaRaw) {
299
- try {
300
- existingMeta = JSON.parse(existingMetaRaw);
301
- }
302
- catch {
303
- existingMeta = null;
304
- }
305
- }
306
- const existingPid = await readPidFile(paths.pidPath);
307
- if (existingMeta?.signature === signature
308
- && existingPid
309
- && isProcessAlive(existingPid)
310
- && await waitForPlaywrightMcpSocketReady(paths.socketPath, 350)) {
311
- return paths.socketPath;
312
- }
313
- await stopPlaywrightMcpDaemon(paths);
314
- const daemonScriptPath = path.join(AGENT_MODULE_DIR, "playwright-mcp-daemon.ts");
315
- const idleTtlSeconds = String(parseEnvInteger(process.env.DOER_PLAYWRIGHT_MCP_DAEMON_IDLE_TTL_SECONDS, PLAYWRIGHT_MCP_DAEMON_IDLE_TTL_SECONDS_DEFAULT));
316
- const child = spawn(process.execPath, ["--import", "tsx", daemonScriptPath], {
317
- cwd: AGENT_PROJECT_DIR,
318
- detached: true,
319
- stdio: "ignore",
320
- env: {
321
- ...process.env,
322
- DOER_PLAYWRIGHT_MCP_DAEMON_SOCKET: paths.socketPath,
323
- DOER_PLAYWRIGHT_MCP_DAEMON_IDLE_TTL_SECONDS: idleTtlSeconds,
324
- DOER_PLAYWRIGHT_MCP_TARGET_COMMAND: daemonCommand,
325
- DOER_PLAYWRIGHT_MCP_TARGET_ARGS_JSON: JSON.stringify(daemonCommandArgs),
326
- DOER_PLAYWRIGHT_MCP_TARGET_ENV_JSON: JSON.stringify(targetEnvPatch),
327
- },
328
- });
329
- child.unref();
330
- if (!child.pid) {
331
- throw new Error("failed to start playwright mcp daemon: missing pid");
332
- }
333
- await writeFile(paths.pidPath, `${child.pid}\n`, "utf8");
334
- await writeFile(paths.metaPath, `${JSON.stringify({ signature, pid: child.pid, socketPath: paths.socketPath, updatedAt: new Date().toISOString() }, null, 2)}\n`, "utf8");
335
- const ready = await waitForPlaywrightMcpSocketReady(paths.socketPath, 6000);
336
- if (!ready) {
337
- throw new Error(`playwright mcp daemon socket not ready: ${paths.socketPath}`);
338
- }
339
- return paths.socketPath;
340
- }
341
- async function ensureCodexPlaywrightMcpLauncher() {
342
- const browserEnvArgs = [
343
- `PLAYWRIGHT_SKIP_BROWSER_GC=${PLAYWRIGHT_SKIP_BROWSER_GC}`,
344
- ];
345
- const daemonArgsFromEnv = parseEnvStringArray(process.env.DOER_PLAYWRIGHT_MCP_DAEMON_ARGS_JSON);
346
- const [daemonCommandFromArgs, ...daemonArgsRest] = daemonArgsFromEnv;
347
- const daemonCommand = daemonCommandFromArgs || "npx";
348
- let daemonArgs = daemonCommandFromArgs && daemonArgsFromEnv.length > 0
349
- ? daemonArgsRest
350
- : ["-y", "@playwright/mcp"];
351
- const hasBrowserOption = daemonArgs.some((arg) => arg === "--browser" || arg.startsWith("--browser="));
352
- if (arch() === "arm64" && !hasBrowserOption) {
353
- daemonArgs = [...daemonArgs, "--browser", "chromium"];
354
- }
355
- const hasNoSandboxOption = daemonArgs.some((arg) => arg === "--no-sandbox");
356
- if (typeof process.getuid === "function" && process.getuid() === 0 && !hasNoSandboxOption) {
357
- daemonArgs = [...daemonArgs, "--no-sandbox"];
358
- }
359
- const socketPath = await ensureManagedPlaywrightMcpDaemon({
360
- command: daemonCommand,
361
- daemonArgs,
362
- browserEnvArgs,
363
- });
364
- if (!existsSync(PLAYWRIGHT_MCP_PROXY_LAUNCHER_PATH)) {
365
- throw new Error(`playwright mcp proxy launcher script not found: ${PLAYWRIGHT_MCP_PROXY_LAUNCHER_PATH}`);
366
- }
367
- return PLAYWRIGHT_MCP_PROXY_LAUNCHER_PATH;
368
- }
369
- function resolveAgentStateDir() {
370
- return process.env.DOER_AGENT_STATE_DIR?.trim() || path.join(homedir(), ".doer-agent");
371
- }
372
151
  function resolveContainerReachableServerBaseUrl(serverBaseUrl) {
373
152
  return serverBaseUrl;
374
153
  }
@@ -730,6 +509,30 @@ function resolveShellPath() {
730
509
  }
731
510
  throw new Error("No shell executable found. Set SHELL env or install /bin/sh (or bash).");
732
511
  }
512
+ function resolveTaskWorkspace(rawCwd) {
513
+ const workspaceRoot = process.env.WORKSPACE?.trim() || process.cwd();
514
+ const requestedCwd = rawCwd?.trim() || "";
515
+ const resolvedCwd = requestedCwd
516
+ ? path.isAbsolute(requestedCwd)
517
+ ? path.resolve(requestedCwd)
518
+ : path.resolve(workspaceRoot, requestedCwd)
519
+ : workspaceRoot;
520
+ if (!existsSync(resolvedCwd)) {
521
+ throw new Error(`Invalid cwd: ${requestedCwd || "(empty)"} resolved to ${resolvedCwd} (path does not exist)`);
522
+ }
523
+ let stats;
524
+ try {
525
+ stats = statSync(resolvedCwd);
526
+ }
527
+ catch (error) {
528
+ const message = error instanceof Error ? error.message : String(error);
529
+ throw new Error(`Invalid cwd: ${requestedCwd || "(empty)"} resolved to ${resolvedCwd} (${message})`);
530
+ }
531
+ if (!stats.isDirectory()) {
532
+ throw new Error(`Invalid cwd: ${requestedCwd || "(empty)"} resolved to ${resolvedCwd} (not a directory)`);
533
+ }
534
+ return resolvedCwd;
535
+ }
733
536
  async function postJson(url, body) {
734
537
  const res = await fetch(url, {
735
538
  method: "POST",
@@ -935,6 +738,7 @@ async function runTask(args) {
935
738
  userId: args.userId,
936
739
  };
937
740
  const shellPath = resolveShellPath();
741
+ const taskWorkspace = resolveTaskWorkspace(args.cwd);
938
742
  const runtimeConfig = await prepareTaskRuntimeConfig({
939
743
  serverBaseUrl: args.serverBaseUrl,
940
744
  taskId: args.taskId,
@@ -947,7 +751,6 @@ async function runTask(args) {
947
751
  userId: args.userId,
948
752
  agentToken: args.agentToken,
949
753
  });
950
- const taskWorkspace = args.cwd || process.env.WORKSPACE?.trim() || process.cwd();
951
754
  const baseTaskEnvPatch = {
952
755
  ...(runtimeConfig?.envPatch ?? {}),
953
756
  ...(codexAuth?.envPatch ?? {}),
@@ -957,10 +760,6 @@ async function runTask(args) {
957
760
  cwd: taskWorkspace,
958
761
  baseEnvPatch: baseTaskEnvPatch,
959
762
  });
960
- await ensureCodexPlaywrightMcpLauncher();
961
- const codexMcpEnvPatch = {
962
- PLAYWRIGHT_SKIP_BROWSER_GC: PLAYWRIGHT_SKIP_BROWSER_GC,
963
- };
964
763
  await recordAgentEvent({ jetstream: args.jetstream,
965
764
  serverBaseUrl: args.serverBaseUrl,
966
765
  taskId: args.taskId,
@@ -972,7 +771,8 @@ async function runTask(args) {
972
771
  pid: process.pid,
973
772
  startedAt: formatLocalTimestamp(),
974
773
  command: args.command,
975
- cwd: args.cwd,
774
+ cwd: taskWorkspace,
775
+ requestedCwd: args.cwd,
976
776
  shell: shellPath,
977
777
  ...(runtimeConfig?.meta ?? { runtimeConfigSynced: false }),
978
778
  ...(codexAuth?.meta ?? { codexAuthSynced: false }),
@@ -988,14 +788,13 @@ async function runTask(args) {
988
788
  const runtimeBinPath = path.join(AGENT_PROJECT_DIR, "runtime/bin");
989
789
  const taskPath = [runtimeBinPath, process.env.PATH || ""].filter(Boolean).join(path.delimiter);
990
790
  const child = spawn(args.command, {
991
- cwd: args.cwd || process.cwd(),
791
+ cwd: taskWorkspace,
992
792
  shell: shellPath,
993
793
  detached: process.platform !== "win32",
994
794
  env: {
995
795
  ...process.env,
996
796
  ...baseTaskEnvPatch,
997
797
  ...taskGitEnv.envPatch,
998
- ...codexMcpEnvPatch,
999
798
  PATH: taskPath,
1000
799
  DOER_AGENT_TOKEN: args.agentToken,
1001
800
  },
package/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "doer-agent",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Reverse-polling agent runtime for doer",
5
5
  "type": "module",
6
6
  "main": "dist/agent.js",
7
7
  "bin": {
8
8
  "doer-agent": "dist/cli.js",
9
- "playwright-mcp-call": "dist/playwright-mcp-call-cli.js",
10
9
  "codex": "dist/codex-cli.js"
11
10
  },
12
11
  "files": [
@@ -18,17 +17,15 @@
18
17
  "access": "public"
19
18
  },
20
19
  "scripts": {
21
- "build": "tsc -p tsconfig.build.json && chmod +x dist/cli.js dist/playwright-mcp-call-cli.js dist/codex-cli.js",
20
+ "build": "tsc -p tsconfig.build.json && chmod +x dist/cli.js dist/codex-cli.js",
22
21
  "start": "node --import tsx src/agent.ts",
23
22
  "start:dist": "node dist/agent.js",
24
23
  "codex": "codex",
25
- "playwright-mcp-call": "node --import tsx src/playwright-mcp-call.ts",
26
24
  "prepublishOnly": "npm run build"
27
25
  },
28
26
  "dependencies": {
29
27
  "@modelcontextprotocol/sdk": "^1.27.1",
30
28
  "@openai/codex-sdk": "^0.115.0",
31
- "@playwright/mcp": "0.0.68",
32
29
  "nats": "^2.29.3"
33
30
  },
34
31
  "devDependencies": {
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- import "./playwright-mcp-call.js";
@@ -1,103 +0,0 @@
1
- import { Buffer } from "node:buffer";
2
- import { existsSync } from "node:fs";
3
- import { homedir } from "node:os";
4
- import path from "node:path";
5
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
6
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
7
- function parseArgs(argv) {
8
- const parsed = { tool: "", argsBase64: "" };
9
- for (let i = 0; i < argv.length; i += 1) {
10
- const token = argv[i];
11
- if (token === "--tool") {
12
- parsed.tool = (argv[i + 1] || "").trim();
13
- i += 1;
14
- continue;
15
- }
16
- if (token === "--args-base64") {
17
- parsed.argsBase64 = (argv[i + 1] || "").trim();
18
- i += 1;
19
- }
20
- }
21
- return parsed;
22
- }
23
- function resolveProxyPath() {
24
- const candidates = [
25
- path.join(process.cwd(), "agent/runtime/bin/doer-mcp-proxy"),
26
- path.join(process.cwd(), "runtime/bin/doer-mcp-proxy"),
27
- ];
28
- for (const candidate of candidates) {
29
- if (existsSync(candidate)) {
30
- return candidate;
31
- }
32
- }
33
- return "";
34
- }
35
- function resolveSocketPath() {
36
- const explicit = (process.env.DOER_MCP_SOCKET || "").trim();
37
- if (explicit) {
38
- return explicit;
39
- }
40
- const stateDir = (process.env.DOER_AGENT_STATE_DIR || "").trim() || path.join(homedir(), ".doer-agent");
41
- return path.join(stateDir, "playwright-mcp-daemon", "playwright-mcp.sock");
42
- }
43
- function decodeToolArgs(argsBase64) {
44
- if (!argsBase64) {
45
- return {};
46
- }
47
- const raw = Buffer.from(argsBase64, "base64").toString("utf8");
48
- const parsed = JSON.parse(raw);
49
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
50
- throw new Error("decoded tool arguments must be a JSON object");
51
- }
52
- return parsed;
53
- }
54
- async function main() {
55
- const { tool, argsBase64 } = parseArgs(process.argv.slice(2));
56
- if (!tool) {
57
- throw new Error("--tool is required");
58
- }
59
- const proxyPath = resolveProxyPath();
60
- if (!proxyPath) {
61
- throw new Error("doer-mcp-proxy binary not found");
62
- }
63
- const socketPath = resolveSocketPath();
64
- const toolArgs = decodeToolArgs(argsBase64);
65
- const transport = new StdioClientTransport({
66
- command: proxyPath,
67
- args: [],
68
- env: {
69
- ...process.env,
70
- DOER_MCP_SOCKET: socketPath,
71
- },
72
- stderr: "pipe",
73
- });
74
- const client = new Client({
75
- name: "doer-agent-playwright-mcp-runner",
76
- version: "0.1.0",
77
- }, {
78
- capabilities: {},
79
- });
80
- try {
81
- await client.connect(transport);
82
- const result = await client.callTool({
83
- name: tool,
84
- arguments: toolArgs,
85
- });
86
- process.stdout.write(`${JSON.stringify({
87
- ok: true,
88
- tool,
89
- content: result.content ?? null,
90
- isError: result.isError === true,
91
- structuredContent: result.structuredContent ?? null,
92
- result,
93
- })}\n`);
94
- }
95
- finally {
96
- await client.close().catch(() => undefined);
97
- }
98
- }
99
- main().catch((error) => {
100
- const message = error instanceof Error ? error.message : String(error);
101
- process.stderr.write(`${JSON.stringify({ ok: false, error: message })}\n`);
102
- process.exit(1);
103
- });
@@ -1,175 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { existsSync } from "node:fs";
3
- import { mkdir, rm } from "node:fs/promises";
4
- import net from "node:net";
5
- import path from "node:path";
6
- function parseInteger(value, fallback) {
7
- const normalized = value?.trim();
8
- if (!normalized) {
9
- return fallback;
10
- }
11
- const parsed = Number.parseInt(normalized, 10);
12
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
13
- }
14
- const socketPath = process.env.DOER_PLAYWRIGHT_MCP_DAEMON_SOCKET?.trim() || "";
15
- const targetCommand = process.env.DOER_PLAYWRIGHT_MCP_TARGET_COMMAND?.trim() || "";
16
- const idleTtlSeconds = parseInteger(process.env.DOER_PLAYWRIGHT_MCP_DAEMON_IDLE_TTL_SECONDS, 10800);
17
- let targetArgs = [];
18
- try {
19
- const parsed = JSON.parse(process.env.DOER_PLAYWRIGHT_MCP_TARGET_ARGS_JSON || "[]");
20
- if (Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
21
- targetArgs = parsed;
22
- }
23
- }
24
- catch {
25
- targetArgs = [];
26
- }
27
- let targetEnvPatch = {};
28
- try {
29
- const parsed = JSON.parse(process.env.DOER_PLAYWRIGHT_MCP_TARGET_ENV_JSON || "{}");
30
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
31
- targetEnvPatch = Object.fromEntries(Object.entries(parsed).flatMap(([key, value]) => (typeof value === "string" ? [[key, value]] : [])));
32
- }
33
- }
34
- catch {
35
- targetEnvPatch = {};
36
- }
37
- if (!socketPath) {
38
- process.stderr.write("playwright-mcp-daemon error: DOER_PLAYWRIGHT_MCP_DAEMON_SOCKET is required\n");
39
- process.exit(1);
40
- }
41
- if (!targetCommand) {
42
- process.stderr.write("playwright-mcp-daemon error: DOER_PLAYWRIGHT_MCP_TARGET_COMMAND is required\n");
43
- process.exit(1);
44
- }
45
- let child = null;
46
- let activeClient = null;
47
- let shuttingDown = false;
48
- let lastActivityAt = Date.now();
49
- const idleTtlMs = idleTtlSeconds * 1000;
50
- function markActivity() {
51
- lastActivityAt = Date.now();
52
- }
53
- function spawnTargetIfNeeded() {
54
- if (child) {
55
- return;
56
- }
57
- child = spawn(targetCommand, targetArgs, {
58
- stdio: ["pipe", "pipe", "pipe"],
59
- env: { ...process.env, ...targetEnvPatch },
60
- });
61
- child.stdout.on("data", (chunk) => {
62
- markActivity();
63
- if (activeClient && !activeClient.destroyed) {
64
- activeClient.write(chunk);
65
- }
66
- });
67
- child.stderr.on("data", (chunk) => {
68
- process.stderr.write(chunk);
69
- });
70
- child.on("exit", () => {
71
- child = null;
72
- if (activeClient && !activeClient.destroyed) {
73
- activeClient.end();
74
- activeClient = null;
75
- }
76
- markActivity();
77
- });
78
- }
79
- async function shutdown(server) {
80
- if (shuttingDown) {
81
- return;
82
- }
83
- shuttingDown = true;
84
- if (activeClient && !activeClient.destroyed) {
85
- activeClient.destroy();
86
- activeClient = null;
87
- }
88
- if (child) {
89
- child.kill("SIGTERM");
90
- await new Promise((resolve) => {
91
- const timeout = setTimeout(() => {
92
- if (child) {
93
- child.kill("SIGKILL");
94
- }
95
- resolve();
96
- }, 2000);
97
- timeout.unref?.();
98
- child?.once("exit", () => {
99
- clearTimeout(timeout);
100
- resolve();
101
- });
102
- });
103
- child = null;
104
- }
105
- await new Promise((resolve) => server.close(() => resolve()));
106
- if (existsSync(socketPath)) {
107
- await rm(socketPath, { force: true });
108
- }
109
- process.exit(0);
110
- }
111
- async function main() {
112
- await mkdir(path.dirname(socketPath), { recursive: true });
113
- if (existsSync(socketPath)) {
114
- await rm(socketPath, { force: true });
115
- }
116
- const server = net.createServer((socket) => {
117
- if (activeClient && !activeClient.destroyed) {
118
- socket.end();
119
- return;
120
- }
121
- activeClient = socket;
122
- markActivity();
123
- spawnTargetIfNeeded();
124
- socket.on("data", (chunk) => {
125
- markActivity();
126
- spawnTargetIfNeeded();
127
- if (child && !child.killed) {
128
- child.stdin.write(chunk);
129
- }
130
- });
131
- socket.on("close", () => {
132
- if (activeClient === socket) {
133
- activeClient = null;
134
- }
135
- markActivity();
136
- });
137
- socket.on("error", () => {
138
- if (activeClient === socket) {
139
- activeClient = null;
140
- }
141
- markActivity();
142
- });
143
- });
144
- server.on("error", (error) => {
145
- process.stderr.write(`playwright-mcp-daemon error: ${error instanceof Error ? error.message : String(error)}\n`);
146
- process.exit(1);
147
- });
148
- await new Promise((resolve, reject) => {
149
- server.once("error", reject);
150
- server.listen(socketPath, () => {
151
- server.off("error", reject);
152
- resolve();
153
- });
154
- });
155
- const interval = setInterval(() => {
156
- if (shuttingDown) {
157
- return;
158
- }
159
- if (activeClient && !activeClient.destroyed) {
160
- return;
161
- }
162
- if (Date.now() - lastActivityAt < idleTtlMs) {
163
- return;
164
- }
165
- void shutdown(server);
166
- }, 30000);
167
- interval.unref?.();
168
- process.on("SIGTERM", () => {
169
- void shutdown(server);
170
- });
171
- process.on("SIGINT", () => {
172
- void shutdown(server);
173
- });
174
- }
175
- void main();
@@ -1,15 +0,0 @@
1
- #!/bin/sh
2
- set -eu
3
- SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
4
- if [ -z "${DOER_MCP_SOCKET:-}" ]; then
5
- if [ -n "${DOER_AGENT_STATE_DIR:-}" ]; then
6
- DOER_MCP_SOCKET="$DOER_AGENT_STATE_DIR/playwright-mcp-daemon/playwright-mcp.sock"
7
- elif [ -n "${HOME:-}" ]; then
8
- DOER_MCP_SOCKET="$HOME/.doer-agent/playwright-mcp-daemon/playwright-mcp.sock"
9
- else
10
- echo "playwright-mcp-proxy-launcher.sh: DOER_MCP_SOCKET is not set and neither DOER_AGENT_STATE_DIR nor HOME is available" >&2
11
- exit 1
12
- fi
13
- fi
14
- export DOER_MCP_SOCKET
15
- exec "$SCRIPT_DIR/doer-mcp-proxy" "$@"