doer-agent 0.1.5 → 0.1.7
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
package/dist/agent.js
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { chmod, mkdir, readFile,
|
|
4
|
-
import
|
|
5
|
-
import { arch, homedir } from "node:os";
|
|
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,223 +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 resolvePlaywrightMcpDaemonSocketPath(stateDir) {
|
|
182
|
-
if (process.platform === "win32") {
|
|
183
|
-
const suffix = stateDir
|
|
184
|
-
.toLowerCase()
|
|
185
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
186
|
-
.replace(/^-+|-+$/g, "")
|
|
187
|
-
.slice(-48) || "default";
|
|
188
|
-
return `\\\\.\\pipe\\doer-playwright-mcp-${suffix}`;
|
|
189
|
-
}
|
|
190
|
-
return path.join(stateDir, "playwright-mcp-daemon", "playwright-mcp.sock");
|
|
191
|
-
}
|
|
192
|
-
function resolvePlaywrightMcpDaemonStatePaths() {
|
|
193
|
-
const stateDir = resolveAgentStateDir();
|
|
194
|
-
const daemonDir = path.join(stateDir, "playwright-mcp-daemon");
|
|
195
|
-
return {
|
|
196
|
-
daemonDir,
|
|
197
|
-
socketPath: resolvePlaywrightMcpDaemonSocketPath(stateDir),
|
|
198
|
-
pidPath: path.join(daemonDir, "daemon.pid"),
|
|
199
|
-
metaPath: path.join(daemonDir, "daemon-meta.json"),
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
function parseEnvAssignmentArgs(values) {
|
|
203
|
-
const envPatch = {};
|
|
204
|
-
for (const value of values) {
|
|
205
|
-
const separatorIndex = value.indexOf("=");
|
|
206
|
-
if (separatorIndex <= 0) {
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
const key = value.slice(0, separatorIndex).trim();
|
|
210
|
-
const envValue = value.slice(separatorIndex + 1);
|
|
211
|
-
if (!key) {
|
|
212
|
-
continue;
|
|
213
|
-
}
|
|
214
|
-
envPatch[key] = envValue;
|
|
215
|
-
}
|
|
216
|
-
return envPatch;
|
|
217
|
-
}
|
|
218
|
-
function escapeShellArg(value) {
|
|
219
|
-
return `'${value.replace(/'/g, `'\"'\"'`)}'`;
|
|
220
|
-
}
|
|
221
|
-
async function readPidFile(pidPath) {
|
|
222
|
-
const raw = await readFile(pidPath, "utf8").catch(() => "");
|
|
223
|
-
const parsed = Number.parseInt(raw.trim(), 10);
|
|
224
|
-
return Number.isInteger(parsed) && parsed > 1 ? parsed : null;
|
|
225
|
-
}
|
|
226
|
-
function isProcessAlive(pid) {
|
|
227
|
-
try {
|
|
228
|
-
process.kill(pid, 0);
|
|
229
|
-
return true;
|
|
230
|
-
}
|
|
231
|
-
catch {
|
|
232
|
-
return false;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
async function waitForPlaywrightMcpSocketReady(socketPath, timeoutMs) {
|
|
236
|
-
const startedAt = Date.now();
|
|
237
|
-
while (Date.now() - startedAt <= timeoutMs) {
|
|
238
|
-
const isReady = await new Promise((resolve) => {
|
|
239
|
-
const socket = net.createConnection({ path: socketPath });
|
|
240
|
-
let settled = false;
|
|
241
|
-
const finish = (value) => {
|
|
242
|
-
if (settled) {
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
settled = true;
|
|
246
|
-
resolve(value);
|
|
247
|
-
};
|
|
248
|
-
socket.once("connect", () => {
|
|
249
|
-
socket.end();
|
|
250
|
-
finish(true);
|
|
251
|
-
});
|
|
252
|
-
socket.once("error", () => {
|
|
253
|
-
finish(false);
|
|
254
|
-
});
|
|
255
|
-
setTimeout(() => {
|
|
256
|
-
socket.destroy();
|
|
257
|
-
finish(false);
|
|
258
|
-
}, 250).unref?.();
|
|
259
|
-
});
|
|
260
|
-
if (isReady) {
|
|
261
|
-
return true;
|
|
262
|
-
}
|
|
263
|
-
await sleep(120);
|
|
264
|
-
}
|
|
265
|
-
return false;
|
|
266
|
-
}
|
|
267
|
-
async function stopPlaywrightMcpDaemon(paths) {
|
|
268
|
-
const pid = await readPidFile(paths.pidPath);
|
|
269
|
-
if (pid && isProcessAlive(pid)) {
|
|
270
|
-
try {
|
|
271
|
-
process.kill(pid, "SIGTERM");
|
|
272
|
-
}
|
|
273
|
-
catch {
|
|
274
|
-
// ignore
|
|
275
|
-
}
|
|
276
|
-
const waitStartedAt = Date.now();
|
|
277
|
-
while (Date.now() - waitStartedAt < 1800 && isProcessAlive(pid)) {
|
|
278
|
-
await sleep(120);
|
|
279
|
-
}
|
|
280
|
-
if (isProcessAlive(pid)) {
|
|
281
|
-
try {
|
|
282
|
-
process.kill(pid, "SIGKILL");
|
|
283
|
-
}
|
|
284
|
-
catch {
|
|
285
|
-
// ignore
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
await Promise.all([
|
|
290
|
-
rename(paths.socketPath, `${paths.socketPath}.stale.${Date.now()}`).catch(() => undefined),
|
|
291
|
-
rename(paths.pidPath, `${paths.pidPath}.stale.${Date.now()}`).catch(() => undefined),
|
|
292
|
-
rename(paths.metaPath, `${paths.metaPath}.stale.${Date.now()}`).catch(() => undefined),
|
|
293
|
-
]);
|
|
294
|
-
}
|
|
295
|
-
async function ensureManagedPlaywrightMcpDaemon(args) {
|
|
296
|
-
const paths = resolvePlaywrightMcpDaemonStatePaths();
|
|
297
|
-
await mkdir(paths.daemonDir, { recursive: true });
|
|
298
|
-
const daemonCommand = args.command;
|
|
299
|
-
const targetEnvPatch = parseEnvAssignmentArgs(args.browserEnvArgs);
|
|
300
|
-
const daemonCommandArgs = args.daemonArgs;
|
|
301
|
-
const signature = JSON.stringify({
|
|
302
|
-
version: PLAYWRIGHT_MCP_DAEMON_SIGNATURE_VERSION,
|
|
303
|
-
daemonCommand,
|
|
304
|
-
daemonCommandArgs,
|
|
305
|
-
targetEnvPatch,
|
|
306
|
-
idleTtlSeconds: parseEnvInteger(process.env.DOER_PLAYWRIGHT_MCP_DAEMON_IDLE_TTL_SECONDS, PLAYWRIGHT_MCP_DAEMON_IDLE_TTL_SECONDS_DEFAULT),
|
|
307
|
-
});
|
|
308
|
-
const existingMetaRaw = await readFile(paths.metaPath, "utf8").catch(() => "");
|
|
309
|
-
let existingMeta = null;
|
|
310
|
-
if (existingMetaRaw) {
|
|
311
|
-
try {
|
|
312
|
-
existingMeta = JSON.parse(existingMetaRaw);
|
|
313
|
-
}
|
|
314
|
-
catch {
|
|
315
|
-
existingMeta = null;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
const existingPid = await readPidFile(paths.pidPath);
|
|
319
|
-
if (existingMeta?.signature === signature
|
|
320
|
-
&& existingPid
|
|
321
|
-
&& isProcessAlive(existingPid)
|
|
322
|
-
&& await waitForPlaywrightMcpSocketReady(paths.socketPath, 350)) {
|
|
323
|
-
return paths.socketPath;
|
|
324
|
-
}
|
|
325
|
-
await stopPlaywrightMcpDaemon(paths);
|
|
326
|
-
const daemonScriptPath = path.join(AGENT_MODULE_DIR, "playwright-mcp-daemon.ts");
|
|
327
|
-
const idleTtlSeconds = String(parseEnvInteger(process.env.DOER_PLAYWRIGHT_MCP_DAEMON_IDLE_TTL_SECONDS, PLAYWRIGHT_MCP_DAEMON_IDLE_TTL_SECONDS_DEFAULT));
|
|
328
|
-
const child = spawn(process.execPath, ["--import", "tsx", daemonScriptPath], {
|
|
329
|
-
cwd: AGENT_PROJECT_DIR,
|
|
330
|
-
detached: true,
|
|
331
|
-
stdio: "ignore",
|
|
332
|
-
env: {
|
|
333
|
-
...process.env,
|
|
334
|
-
DOER_PLAYWRIGHT_MCP_DAEMON_SOCKET: paths.socketPath,
|
|
335
|
-
DOER_PLAYWRIGHT_MCP_DAEMON_IDLE_TTL_SECONDS: idleTtlSeconds,
|
|
336
|
-
DOER_PLAYWRIGHT_MCP_TARGET_COMMAND: daemonCommand,
|
|
337
|
-
DOER_PLAYWRIGHT_MCP_TARGET_ARGS_JSON: JSON.stringify(daemonCommandArgs),
|
|
338
|
-
DOER_PLAYWRIGHT_MCP_TARGET_ENV_JSON: JSON.stringify(targetEnvPatch),
|
|
339
|
-
},
|
|
340
|
-
});
|
|
341
|
-
child.unref();
|
|
342
|
-
if (!child.pid) {
|
|
343
|
-
throw new Error("failed to start playwright mcp daemon: missing pid");
|
|
344
|
-
}
|
|
345
|
-
await writeFile(paths.pidPath, `${child.pid}\n`, "utf8");
|
|
346
|
-
await writeFile(paths.metaPath, `${JSON.stringify({ signature, pid: child.pid, socketPath: paths.socketPath, updatedAt: new Date().toISOString() }, null, 2)}\n`, "utf8");
|
|
347
|
-
const ready = await waitForPlaywrightMcpSocketReady(paths.socketPath, 6000);
|
|
348
|
-
if (!ready) {
|
|
349
|
-
throw new Error(`playwright mcp daemon socket not ready: ${paths.socketPath}`);
|
|
350
|
-
}
|
|
351
|
-
return paths.socketPath;
|
|
352
|
-
}
|
|
353
|
-
async function ensureCodexPlaywrightMcpLauncher() {
|
|
354
|
-
const browserEnvArgs = [
|
|
355
|
-
`PLAYWRIGHT_SKIP_BROWSER_GC=${PLAYWRIGHT_SKIP_BROWSER_GC}`,
|
|
356
|
-
];
|
|
357
|
-
const daemonArgsFromEnv = parseEnvStringArray(process.env.DOER_PLAYWRIGHT_MCP_DAEMON_ARGS_JSON);
|
|
358
|
-
const [daemonCommandFromArgs, ...daemonArgsRest] = daemonArgsFromEnv;
|
|
359
|
-
const daemonCommand = daemonCommandFromArgs || "npx";
|
|
360
|
-
let daemonArgs = daemonCommandFromArgs && daemonArgsFromEnv.length > 0
|
|
361
|
-
? daemonArgsRest
|
|
362
|
-
: ["-y", "@playwright/mcp"];
|
|
363
|
-
const hasBrowserOption = daemonArgs.some((arg) => arg === "--browser" || arg.startsWith("--browser="));
|
|
364
|
-
if (arch() === "arm64" && !hasBrowserOption) {
|
|
365
|
-
daemonArgs = [...daemonArgs, "--browser", "chromium"];
|
|
366
|
-
}
|
|
367
|
-
const hasNoSandboxOption = daemonArgs.some((arg) => arg === "--no-sandbox");
|
|
368
|
-
if (typeof process.getuid === "function" && process.getuid() === 0 && !hasNoSandboxOption) {
|
|
369
|
-
daemonArgs = [...daemonArgs, "--no-sandbox"];
|
|
370
|
-
}
|
|
371
|
-
const socketPath = await ensureManagedPlaywrightMcpDaemon({
|
|
372
|
-
command: daemonCommand,
|
|
373
|
-
daemonArgs,
|
|
374
|
-
browserEnvArgs,
|
|
375
|
-
});
|
|
376
|
-
if (!existsSync(PLAYWRIGHT_MCP_PROXY_LAUNCHER_PATH)) {
|
|
377
|
-
throw new Error(`playwright mcp proxy launcher script not found: ${PLAYWRIGHT_MCP_PROXY_LAUNCHER_PATH}`);
|
|
378
|
-
}
|
|
379
|
-
return PLAYWRIGHT_MCP_PROXY_LAUNCHER_PATH;
|
|
380
|
-
}
|
|
381
|
-
function resolveAgentStateDir() {
|
|
382
|
-
return process.env.DOER_AGENT_STATE_DIR?.trim() || path.join(homedir(), ".doer-agent");
|
|
383
|
-
}
|
|
384
151
|
function resolveContainerReachableServerBaseUrl(serverBaseUrl) {
|
|
385
152
|
return serverBaseUrl;
|
|
386
153
|
}
|
|
@@ -969,11 +736,6 @@ async function runTask(args) {
|
|
|
969
736
|
cwd: taskWorkspace,
|
|
970
737
|
baseEnvPatch: baseTaskEnvPatch,
|
|
971
738
|
});
|
|
972
|
-
const codexMcpEnvPatch = {};
|
|
973
|
-
if (process.platform !== "win32") {
|
|
974
|
-
await ensureCodexPlaywrightMcpLauncher();
|
|
975
|
-
codexMcpEnvPatch.PLAYWRIGHT_SKIP_BROWSER_GC = PLAYWRIGHT_SKIP_BROWSER_GC;
|
|
976
|
-
}
|
|
977
739
|
await recordAgentEvent({ jetstream: args.jetstream,
|
|
978
740
|
serverBaseUrl: args.serverBaseUrl,
|
|
979
741
|
taskId: args.taskId,
|
|
@@ -1008,7 +770,6 @@ async function runTask(args) {
|
|
|
1008
770
|
...process.env,
|
|
1009
771
|
...baseTaskEnvPatch,
|
|
1010
772
|
...taskGitEnv.envPatch,
|
|
1011
|
-
...codexMcpEnvPatch,
|
|
1012
773
|
PATH: taskPath,
|
|
1013
774
|
DOER_AGENT_TOKEN: args.agentToken,
|
|
1014
775
|
},
|
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "doer-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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,111 +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
|
-
if (process.platform === "win32") {
|
|
42
|
-
const suffix = stateDir
|
|
43
|
-
.toLowerCase()
|
|
44
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
45
|
-
.replace(/^-+|-+$/g, "")
|
|
46
|
-
.slice(-48) || "default";
|
|
47
|
-
return `\\\\.\\pipe\\doer-playwright-mcp-${suffix}`;
|
|
48
|
-
}
|
|
49
|
-
return path.join(stateDir, "playwright-mcp-daemon", "playwright-mcp.sock");
|
|
50
|
-
}
|
|
51
|
-
function decodeToolArgs(argsBase64) {
|
|
52
|
-
if (!argsBase64) {
|
|
53
|
-
return {};
|
|
54
|
-
}
|
|
55
|
-
const raw = Buffer.from(argsBase64, "base64").toString("utf8");
|
|
56
|
-
const parsed = JSON.parse(raw);
|
|
57
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
58
|
-
throw new Error("decoded tool arguments must be a JSON object");
|
|
59
|
-
}
|
|
60
|
-
return parsed;
|
|
61
|
-
}
|
|
62
|
-
async function main() {
|
|
63
|
-
const { tool, argsBase64 } = parseArgs(process.argv.slice(2));
|
|
64
|
-
if (!tool) {
|
|
65
|
-
throw new Error("--tool is required");
|
|
66
|
-
}
|
|
67
|
-
const proxyPath = resolveProxyPath();
|
|
68
|
-
if (!proxyPath) {
|
|
69
|
-
throw new Error("doer-mcp-proxy binary not found");
|
|
70
|
-
}
|
|
71
|
-
const socketPath = resolveSocketPath();
|
|
72
|
-
const toolArgs = decodeToolArgs(argsBase64);
|
|
73
|
-
const transport = new StdioClientTransport({
|
|
74
|
-
command: proxyPath,
|
|
75
|
-
args: [],
|
|
76
|
-
env: {
|
|
77
|
-
...process.env,
|
|
78
|
-
DOER_MCP_SOCKET: socketPath,
|
|
79
|
-
},
|
|
80
|
-
stderr: "pipe",
|
|
81
|
-
});
|
|
82
|
-
const client = new Client({
|
|
83
|
-
name: "doer-agent-playwright-mcp-runner",
|
|
84
|
-
version: "0.1.0",
|
|
85
|
-
}, {
|
|
86
|
-
capabilities: {},
|
|
87
|
-
});
|
|
88
|
-
try {
|
|
89
|
-
await client.connect(transport);
|
|
90
|
-
const result = await client.callTool({
|
|
91
|
-
name: tool,
|
|
92
|
-
arguments: toolArgs,
|
|
93
|
-
});
|
|
94
|
-
process.stdout.write(`${JSON.stringify({
|
|
95
|
-
ok: true,
|
|
96
|
-
tool,
|
|
97
|
-
content: result.content ?? null,
|
|
98
|
-
isError: result.isError === true,
|
|
99
|
-
structuredContent: result.structuredContent ?? null,
|
|
100
|
-
result,
|
|
101
|
-
})}\n`);
|
|
102
|
-
}
|
|
103
|
-
finally {
|
|
104
|
-
await client.close().catch(() => undefined);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
main().catch((error) => {
|
|
108
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
109
|
-
process.stderr.write(`${JSON.stringify({ ok: false, error: message })}\n`);
|
|
110
|
-
process.exit(1);
|
|
111
|
-
});
|
|
@@ -1,180 +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
|
-
function isWindowsNamedPipePath(value) {
|
|
18
|
-
return process.platform === "win32" && value.startsWith("\\\\.\\pipe\\");
|
|
19
|
-
}
|
|
20
|
-
let targetArgs = [];
|
|
21
|
-
try {
|
|
22
|
-
const parsed = JSON.parse(process.env.DOER_PLAYWRIGHT_MCP_TARGET_ARGS_JSON || "[]");
|
|
23
|
-
if (Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
|
|
24
|
-
targetArgs = parsed;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
catch {
|
|
28
|
-
targetArgs = [];
|
|
29
|
-
}
|
|
30
|
-
let targetEnvPatch = {};
|
|
31
|
-
try {
|
|
32
|
-
const parsed = JSON.parse(process.env.DOER_PLAYWRIGHT_MCP_TARGET_ENV_JSON || "{}");
|
|
33
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
34
|
-
targetEnvPatch = Object.fromEntries(Object.entries(parsed).flatMap(([key, value]) => (typeof value === "string" ? [[key, value]] : [])));
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
targetEnvPatch = {};
|
|
39
|
-
}
|
|
40
|
-
if (!socketPath) {
|
|
41
|
-
process.stderr.write("playwright-mcp-daemon error: DOER_PLAYWRIGHT_MCP_DAEMON_SOCKET is required\n");
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
if (!targetCommand) {
|
|
45
|
-
process.stderr.write("playwright-mcp-daemon error: DOER_PLAYWRIGHT_MCP_TARGET_COMMAND is required\n");
|
|
46
|
-
process.exit(1);
|
|
47
|
-
}
|
|
48
|
-
let child = null;
|
|
49
|
-
let activeClient = null;
|
|
50
|
-
let shuttingDown = false;
|
|
51
|
-
let lastActivityAt = Date.now();
|
|
52
|
-
const idleTtlMs = idleTtlSeconds * 1000;
|
|
53
|
-
function markActivity() {
|
|
54
|
-
lastActivityAt = Date.now();
|
|
55
|
-
}
|
|
56
|
-
function spawnTargetIfNeeded() {
|
|
57
|
-
if (child) {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
child = spawn(targetCommand, targetArgs, {
|
|
61
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
62
|
-
env: { ...process.env, ...targetEnvPatch },
|
|
63
|
-
});
|
|
64
|
-
child.stdout.on("data", (chunk) => {
|
|
65
|
-
markActivity();
|
|
66
|
-
if (activeClient && !activeClient.destroyed) {
|
|
67
|
-
activeClient.write(chunk);
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
child.stderr.on("data", (chunk) => {
|
|
71
|
-
process.stderr.write(chunk);
|
|
72
|
-
});
|
|
73
|
-
child.on("exit", () => {
|
|
74
|
-
child = null;
|
|
75
|
-
if (activeClient && !activeClient.destroyed) {
|
|
76
|
-
activeClient.end();
|
|
77
|
-
activeClient = null;
|
|
78
|
-
}
|
|
79
|
-
markActivity();
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
async function shutdown(server) {
|
|
83
|
-
if (shuttingDown) {
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
shuttingDown = true;
|
|
87
|
-
if (activeClient && !activeClient.destroyed) {
|
|
88
|
-
activeClient.destroy();
|
|
89
|
-
activeClient = null;
|
|
90
|
-
}
|
|
91
|
-
if (child) {
|
|
92
|
-
child.kill("SIGTERM");
|
|
93
|
-
await new Promise((resolve) => {
|
|
94
|
-
const timeout = setTimeout(() => {
|
|
95
|
-
if (child) {
|
|
96
|
-
child.kill("SIGKILL");
|
|
97
|
-
}
|
|
98
|
-
resolve();
|
|
99
|
-
}, 2000);
|
|
100
|
-
timeout.unref?.();
|
|
101
|
-
child?.once("exit", () => {
|
|
102
|
-
clearTimeout(timeout);
|
|
103
|
-
resolve();
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
child = null;
|
|
107
|
-
}
|
|
108
|
-
await new Promise((resolve) => server.close(() => resolve()));
|
|
109
|
-
if (!isWindowsNamedPipePath(socketPath) && existsSync(socketPath)) {
|
|
110
|
-
await rm(socketPath, { force: true });
|
|
111
|
-
}
|
|
112
|
-
process.exit(0);
|
|
113
|
-
}
|
|
114
|
-
async function main() {
|
|
115
|
-
if (!isWindowsNamedPipePath(socketPath)) {
|
|
116
|
-
await mkdir(path.dirname(socketPath), { recursive: true });
|
|
117
|
-
if (existsSync(socketPath)) {
|
|
118
|
-
await rm(socketPath, { force: true });
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
const server = net.createServer((socket) => {
|
|
122
|
-
if (activeClient && !activeClient.destroyed) {
|
|
123
|
-
socket.end();
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
activeClient = socket;
|
|
127
|
-
markActivity();
|
|
128
|
-
spawnTargetIfNeeded();
|
|
129
|
-
socket.on("data", (chunk) => {
|
|
130
|
-
markActivity();
|
|
131
|
-
spawnTargetIfNeeded();
|
|
132
|
-
if (child && !child.killed) {
|
|
133
|
-
child.stdin.write(chunk);
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
socket.on("close", () => {
|
|
137
|
-
if (activeClient === socket) {
|
|
138
|
-
activeClient = null;
|
|
139
|
-
}
|
|
140
|
-
markActivity();
|
|
141
|
-
});
|
|
142
|
-
socket.on("error", () => {
|
|
143
|
-
if (activeClient === socket) {
|
|
144
|
-
activeClient = null;
|
|
145
|
-
}
|
|
146
|
-
markActivity();
|
|
147
|
-
});
|
|
148
|
-
});
|
|
149
|
-
server.on("error", (error) => {
|
|
150
|
-
process.stderr.write(`playwright-mcp-daemon error: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
151
|
-
process.exit(1);
|
|
152
|
-
});
|
|
153
|
-
await new Promise((resolve, reject) => {
|
|
154
|
-
server.once("error", reject);
|
|
155
|
-
server.listen(socketPath, () => {
|
|
156
|
-
server.off("error", reject);
|
|
157
|
-
resolve();
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
const interval = setInterval(() => {
|
|
161
|
-
if (shuttingDown) {
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
if (activeClient && !activeClient.destroyed) {
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
if (Date.now() - lastActivityAt < idleTtlMs) {
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
void shutdown(server);
|
|
171
|
-
}, 30000);
|
|
172
|
-
interval.unref?.();
|
|
173
|
-
process.on("SIGTERM", () => {
|
|
174
|
-
void shutdown(server);
|
|
175
|
-
});
|
|
176
|
-
process.on("SIGINT", () => {
|
|
177
|
-
void shutdown(server);
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
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" "$@"
|