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 +0 -1
- package/dist/agent.js +31 -232
- package/package.json +2 -5
- package/dist/playwright-mcp-call-cli.js +0 -2
- package/dist/playwright-mcp-call.js +0 -103
- package/dist/playwright-mcp-daemon.js +0 -175
- package/runtime/bin/playwright-mcp-proxy-launcher.sh +0 -15
package/README.md
CHANGED
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,
|
|
4
|
-
import
|
|
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:
|
|
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:
|
|
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.
|
|
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/
|
|
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,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" "$@"
|