doer-agent 0.1.8 → 0.2.0

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/dist/agent.js CHANGED
@@ -1,16 +1,18 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
2
  import { existsSync, statSync } from "node:fs";
3
- import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
4
- import { homedir } from "node:os";
3
+ import { chmod, mkdir, open, readFile, readdir, stat, writeFile } from "node:fs/promises";
5
4
  import path from "node:path";
6
5
  import { fileURLToPath } from "node:url";
7
- import { AckPolicy, connect, DeliverPolicy, JSONCodec, RetentionPolicy, StorageType } from "nats";
6
+ import { AckPolicy, connect, DeliverPolicy, JSONCodec, RetentionPolicy, StorageType, StringCodec } from "nats";
8
7
  const DEFAULT_SERVER_BASE_URL = "https://doer.cranix.net";
9
8
  const AGENT_MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
10
9
  const AGENT_PROJECT_DIR = path.join(AGENT_MODULE_DIR, "..");
11
10
  const AGENT_PACKAGE_JSON_PATH = path.join(AGENT_PROJECT_DIR, "package.json");
12
11
  let activeTaskLogContext = null;
13
12
  const activeTaskCancelRequests = new Map();
13
+ let workspaceRootOverride = null;
14
+ const fsRpcCodec = StringCodec();
15
+ const shellRpcCodec = StringCodec();
14
16
  function sanitizeUserId(userId) {
15
17
  const normalized = userId.trim().replace(/[^a-zA-Z0-9_-]/g, "_");
16
18
  return normalized.length > 0 ? normalized : "anonymous";
@@ -135,7 +137,8 @@ async function initJetStreamContext(args) {
135
137
  };
136
138
  }
137
139
  function resolveCodexHomePath() {
138
- return path.join(homedir(), ".codex");
140
+ const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
141
+ return path.join(workspaceRoot, ".codex");
139
142
  }
140
143
  function parseEnvBoolean(value) {
141
144
  return value?.trim().toLowerCase() === "true";
@@ -510,7 +513,7 @@ function resolveShellPath() {
510
513
  throw new Error("No shell executable found. Set SHELL env or install /bin/sh (or bash).");
511
514
  }
512
515
  function resolveTaskWorkspace(rawCwd) {
513
- const workspaceRoot = process.env.WORKSPACE?.trim() || process.cwd();
516
+ const workspaceRoot = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
514
517
  const requestedCwd = rawCwd?.trim() || "";
515
518
  const resolvedCwd = requestedCwd
516
519
  ? path.isAbsolute(requestedCwd)
@@ -533,6 +536,341 @@ function resolveTaskWorkspace(rawCwd) {
533
536
  }
534
537
  return resolvedCwd;
535
538
  }
539
+ function buildAgentFsRpcSubject(userId, agentId) {
540
+ return `doer.agent.fs.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
541
+ }
542
+ function buildAgentShellRpcSubject(userId, agentId) {
543
+ return `doer.agent.shell.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
544
+ }
545
+ function normalizeFsRpcPath(rawPath) {
546
+ const root = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
547
+ const raw = typeof rawPath === "string" && rawPath.trim() ? rawPath.trim() : ".";
548
+ const normalizedRaw = raw.replace(/\\/g, "/");
549
+ const useAbsolute = path.isAbsolute(normalizedRaw);
550
+ const rel = normalizedRaw.replace(/^\/+/, "") || ".";
551
+ const abs = useAbsolute ? path.resolve(normalizedRaw) : path.resolve(root, rel);
552
+ if (!useAbsolute && abs !== root && !abs.startsWith(root + path.sep)) {
553
+ throw new Error("path escapes workspace root");
554
+ }
555
+ const formatPath = (target) => {
556
+ if (useAbsolute) {
557
+ return target.split(path.sep).join("/") || "/";
558
+ }
559
+ return path.relative(root, target).split(path.sep).join("/") || ".";
560
+ };
561
+ return { abs, formatPath };
562
+ }
563
+ function parseFsRpcAction(value) {
564
+ if (value === "list" || value === "stat" || value === "fetch_file" || value === "read_text") {
565
+ return value;
566
+ }
567
+ throw new Error("unsupported action");
568
+ }
569
+ function normalizeFsRpcNumber(value, fallback) {
570
+ const n = Number(value);
571
+ if (!Number.isFinite(n)) {
572
+ return fallback;
573
+ }
574
+ return Math.floor(n);
575
+ }
576
+ async function executeFsRpc(args) {
577
+ const action = parseFsRpcAction(args.request.action);
578
+ const { abs, formatPath } = normalizeFsRpcPath(args.request.path);
579
+ if (action === "stat") {
580
+ const entry = await stat(abs);
581
+ return {
582
+ ok: true,
583
+ action,
584
+ path: formatPath(abs),
585
+ kind: entry.isDirectory() ? "dir" : "file",
586
+ size: entry.size,
587
+ mtimeMs: entry.mtimeMs,
588
+ };
589
+ }
590
+ if (action === "list") {
591
+ const entry = await stat(abs);
592
+ if (!entry.isDirectory()) {
593
+ throw new Error("path is not a directory");
594
+ }
595
+ const limit = Math.max(1, Math.min(normalizeFsRpcNumber(args.request.limit, 200), 1000));
596
+ const rows = await readdir(abs, { withFileTypes: true });
597
+ const items = await Promise.all(rows.map(async (row) => {
598
+ const child = path.join(abs, row.name);
599
+ const childStat = await stat(child);
600
+ return {
601
+ name: row.name,
602
+ path: formatPath(child),
603
+ kind: row.isDirectory() ? "dir" : "file",
604
+ size: childStat.size,
605
+ mtimeMs: childStat.mtimeMs,
606
+ };
607
+ }));
608
+ items.sort((a, b) => (a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === "dir" ? -1 : 1));
609
+ return {
610
+ ok: true,
611
+ action,
612
+ path: formatPath(abs),
613
+ items: items.slice(0, limit),
614
+ truncated: items.length > limit,
615
+ total: items.length,
616
+ };
617
+ }
618
+ if (action === "fetch_file") {
619
+ const entry = await stat(abs);
620
+ if (!entry.isFile()) {
621
+ throw new Error("path is not a file");
622
+ }
623
+ const uploadUrl = typeof args.request.uploadUrl === "string" ? args.request.uploadUrl : "";
624
+ const chatId = typeof args.request.chatId === "string" ? args.request.chatId : "";
625
+ const agentId = typeof args.request.agentId === "string" ? args.request.agentId : "";
626
+ if (!uploadUrl || !chatId || !agentId) {
627
+ throw new Error("missing upload parameters");
628
+ }
629
+ const data = await readFile(abs);
630
+ const fileName = path.basename(abs) || "file";
631
+ const form = new FormData();
632
+ form.append("file", new File([data], fileName));
633
+ form.append("chatId", chatId);
634
+ form.append("agentId", agentId);
635
+ const response = await fetch(uploadUrl, {
636
+ method: "POST",
637
+ headers: { Authorization: `Bearer ${args.agentToken}` },
638
+ body: form,
639
+ });
640
+ const text = await response.text();
641
+ let upload = {};
642
+ try {
643
+ upload = JSON.parse(text || "{}");
644
+ }
645
+ catch {
646
+ upload = {};
647
+ }
648
+ if (!response.ok) {
649
+ const message = typeof upload.error === "string" ? upload.error : `upload failed: ${response.status}`;
650
+ throw new Error(message);
651
+ }
652
+ return {
653
+ ok: true,
654
+ action,
655
+ path: formatPath(abs),
656
+ size: entry.size,
657
+ upload,
658
+ };
659
+ }
660
+ const entry = await stat(abs);
661
+ if (!entry.isFile()) {
662
+ throw new Error("path is not a file");
663
+ }
664
+ const offset = Math.max(0, normalizeFsRpcNumber(args.request.offset, 0));
665
+ const length = Math.max(1, Math.min(normalizeFsRpcNumber(args.request.length, 65536), 262144));
666
+ const encoding = typeof args.request.encoding === "string" && args.request.encoding ? args.request.encoding : "utf8";
667
+ const fd = await open(abs, "r");
668
+ try {
669
+ const buffer = Buffer.alloc(length);
670
+ const readResult = await fd.read(buffer, 0, length, offset);
671
+ const slice = buffer.subarray(0, readResult.bytesRead);
672
+ try {
673
+ const text = slice.toString(encoding);
674
+ return {
675
+ ok: true,
676
+ action,
677
+ path: formatPath(abs),
678
+ offset,
679
+ length: readResult.bytesRead,
680
+ totalSize: entry.size,
681
+ eof: offset + readResult.bytesRead >= entry.size,
682
+ encoding,
683
+ text,
684
+ bytesRead: readResult.bytesRead,
685
+ };
686
+ }
687
+ catch (error) {
688
+ const message = error instanceof Error ? error.message : "failed to decode text";
689
+ return {
690
+ ok: false,
691
+ action,
692
+ path: formatPath(abs),
693
+ error: message,
694
+ };
695
+ }
696
+ }
697
+ finally {
698
+ await fd.close();
699
+ }
700
+ }
701
+ async function handleFsRpcMessage(args) {
702
+ let payload = {};
703
+ try {
704
+ payload = JSON.parse(fsRpcCodec.decode(args.msg.data));
705
+ if (typeof payload.agentId === "string" && payload.agentId.trim() && payload.agentId !== args.agentId) {
706
+ throw new Error("agent id mismatch");
707
+ }
708
+ const result = await executeFsRpc({ request: payload, agentToken: args.agentToken });
709
+ args.msg.respond(fsRpcCodec.encode(JSON.stringify(result)));
710
+ }
711
+ catch (error) {
712
+ const message = error instanceof Error ? error.message : "unknown error";
713
+ const action = typeof payload.action === "string" ? payload.action : "";
714
+ const response = {
715
+ ok: false,
716
+ action,
717
+ path: typeof payload.path === "string" ? payload.path : ".",
718
+ error: message,
719
+ };
720
+ args.msg.respond(fsRpcCodec.encode(JSON.stringify(response)));
721
+ writeAgentError(`fs rpc failed action=${action || "unknown"} error=${message}`);
722
+ }
723
+ }
724
+ function subscribeToFsRpc(args) {
725
+ const subject = buildAgentFsRpcSubject(args.userId, args.agentId);
726
+ args.jetstream.nc.subscribe(subject, {
727
+ callback: (error, msg) => {
728
+ if (error) {
729
+ const message = error instanceof Error ? error.message : String(error);
730
+ writeAgentError(`fs rpc subscription error: ${message}`);
731
+ return;
732
+ }
733
+ void handleFsRpcMessage({
734
+ msg,
735
+ serverBaseUrl: args.serverBaseUrl,
736
+ userId: args.userId,
737
+ agentId: args.agentId,
738
+ agentToken: args.agentToken,
739
+ });
740
+ },
741
+ });
742
+ writeAgentInfo(`fs rpc subscribed subject=${subject}`);
743
+ }
744
+ function normalizeShellRpcRequest(args) {
745
+ const requestId = typeof args.request.requestId === "string" ? args.request.requestId.trim() : "";
746
+ if (!requestId) {
747
+ throw new Error("missing requestId");
748
+ }
749
+ const requestAgentId = typeof args.request.agentId === "string" ? args.request.agentId.trim() : "";
750
+ if (!requestAgentId) {
751
+ throw new Error("missing agentId");
752
+ }
753
+ if (requestAgentId !== args.agentId) {
754
+ throw new Error("agent id mismatch");
755
+ }
756
+ const command = typeof args.request.command === "string" ? args.request.command.trim() : "";
757
+ if (!command) {
758
+ throw new Error("missing command");
759
+ }
760
+ const responseSubject = typeof args.request.responseSubject === "string" ? args.request.responseSubject.trim() : "";
761
+ if (!responseSubject) {
762
+ throw new Error("missing responseSubject");
763
+ }
764
+ const cwd = typeof args.request.cwd === "string" && args.request.cwd.trim() ? args.request.cwd.trim() : null;
765
+ const timeoutRaw = Number(args.request.timeoutMs);
766
+ const timeoutMs = Number.isFinite(timeoutRaw) ? Math.max(1000, Math.min(Math.floor(timeoutRaw), 300000)) : 30000;
767
+ return { requestId, command, cwd, timeoutMs, responseSubject };
768
+ }
769
+ function publishShellRpcResponse(args) {
770
+ args.nc.publish(args.responseSubject, shellRpcCodec.encode(JSON.stringify(args.payload)));
771
+ }
772
+ async function handleShellRpcMessage(args) {
773
+ let requestId = "unknown";
774
+ let responseSubject = "";
775
+ let stdout = "";
776
+ let stderr = "";
777
+ try {
778
+ const payload = JSON.parse(shellRpcCodec.decode(args.msg.data));
779
+ const request = normalizeShellRpcRequest({ request: payload, agentId: args.agentId });
780
+ requestId = request.requestId;
781
+ responseSubject = request.responseSubject;
782
+ const shellPath = resolveShellPath();
783
+ const taskWorkspace = resolveTaskWorkspace(request.cwd);
784
+ const runtimeBinPath = path.join(AGENT_PROJECT_DIR, "runtime/bin");
785
+ const taskPath = [runtimeBinPath, process.env.PATH || ""].filter(Boolean).join(path.delimiter);
786
+ const child = spawn(request.command, {
787
+ cwd: taskWorkspace,
788
+ shell: shellPath,
789
+ detached: process.platform !== "win32",
790
+ env: {
791
+ ...process.env,
792
+ WORKSPACE: taskWorkspace,
793
+ PATH: taskPath,
794
+ },
795
+ stdio: ["ignore", "pipe", "pipe"],
796
+ });
797
+ child.stdout.setEncoding("utf8");
798
+ child.stderr.setEncoding("utf8");
799
+ child.stdout.on("data", (chunk) => {
800
+ stdout += chunk;
801
+ });
802
+ child.stderr.on("data", (chunk) => {
803
+ stderr += chunk;
804
+ });
805
+ let timedOut = false;
806
+ const timeout = setTimeout(() => {
807
+ timedOut = true;
808
+ sendSignalToTaskProcess(child, "SIGTERM");
809
+ setTimeout(() => {
810
+ sendSignalToTaskProcess(child, "SIGKILL");
811
+ }, 1000).unref?.();
812
+ }, request.timeoutMs);
813
+ timeout.unref?.();
814
+ const result = await new Promise((resolve, reject) => {
815
+ child.once("error", reject);
816
+ child.once("close", (code, signal) => {
817
+ resolve({ exitCode: typeof code === "number" ? code : null, signal });
818
+ });
819
+ }).finally(() => {
820
+ clearTimeout(timeout);
821
+ });
822
+ publishShellRpcResponse({
823
+ nc: args.jetstream.nc,
824
+ responseSubject,
825
+ payload: {
826
+ requestId,
827
+ ok: !timedOut,
828
+ exitCode: result.exitCode,
829
+ signal: result.signal,
830
+ stdout,
831
+ stderr,
832
+ ...(timedOut ? { error: `Command timed out after ${request.timeoutMs}ms` } : {}),
833
+ },
834
+ });
835
+ }
836
+ catch (error) {
837
+ const message = error instanceof Error ? error.message : String(error);
838
+ if (responseSubject) {
839
+ publishShellRpcResponse({
840
+ nc: args.jetstream.nc,
841
+ responseSubject,
842
+ payload: {
843
+ requestId,
844
+ ok: false,
845
+ exitCode: null,
846
+ signal: null,
847
+ stdout,
848
+ stderr,
849
+ error: message,
850
+ },
851
+ });
852
+ }
853
+ writeAgentError(`shell rpc failed requestId=${requestId} error=${message}`);
854
+ }
855
+ }
856
+ function subscribeToShellRpc(args) {
857
+ const subject = buildAgentShellRpcSubject(args.userId, args.agentId);
858
+ args.jetstream.nc.subscribe(subject, {
859
+ callback: (error, msg) => {
860
+ if (error) {
861
+ const message = error instanceof Error ? error.message : String(error);
862
+ writeAgentError(`shell rpc subscription error: ${message}`);
863
+ return;
864
+ }
865
+ void handleShellRpcMessage({
866
+ msg,
867
+ jetstream: args.jetstream,
868
+ agentId: args.agentId,
869
+ });
870
+ },
871
+ });
872
+ writeAgentInfo(`shell rpc subscribed subject=${subject}`);
873
+ }
536
874
  async function postJson(url, body) {
537
875
  const res = await fetch(url, {
538
876
  method: "POST",
@@ -959,9 +1297,9 @@ async function connectBootstrapWithRetry(args) {
959
1297
  async function main() {
960
1298
  const args = parseArgs(process.argv.slice(2));
961
1299
  const workspaceDir = resolveArgOrEnv(args, ["workspace-dir", "workspaceDir"], ["WORKSPACE"]);
962
- if (workspaceDir) {
963
- process.chdir(path.resolve(workspaceDir));
964
- }
1300
+ const startupWorkspaceRoot = path.resolve(workspaceDir || process.cwd());
1301
+ workspaceRootOverride = startupWorkspaceRoot;
1302
+ process.chdir(startupWorkspaceRoot);
965
1303
  const serverBaseUrlRaw = resolveArgOrEnv(args, ["server", "url"], ["DOER_AGENT_SERVER"], DEFAULT_SERVER_BASE_URL);
966
1304
  const requestedServerBaseUrl = serverBaseUrlRaw.replace(/\/$/, "");
967
1305
  const serverBaseUrl = resolveContainerReachableServerBaseUrl(requestedServerBaseUrl);
@@ -979,12 +1317,16 @@ async function main() {
979
1317
  });
980
1318
  const maxConcurrency = Math.max(1, parseEnvInteger(process.env.DOER_AGENT_MAX_CONCURRENCY, 5));
981
1319
  const agentVersion = await resolveAgentVersion();
1320
+ const initialAgentId = typeof natsBootstrap.agentId === "string" ? natsBootstrap.agentId : "";
1321
+ if (!initialAgentId) {
1322
+ throw new Error("agent id missing from bootstrap");
1323
+ }
982
1324
  process.stdout.write(`\n[doer-agent v${agentVersion}]\n`);
983
1325
  if (!usesDefaultServer) {
984
1326
  process.stdout.write(`- server: ${serverBaseUrl}\n`);
985
1327
  }
986
1328
  process.stdout.write(`- userId: ${userId}\n`);
987
- process.stdout.write(`- agentId: ${typeof natsBootstrap.agentId === "string" ? natsBootstrap.agentId : "unknown"}\n`);
1329
+ process.stdout.write(`- agentId: ${initialAgentId}\n`);
988
1330
  process.stdout.write(`\n- transport: nats\n`);
989
1331
  process.stdout.write(`- natsServers: ${jetstream.servers.join(",")}\n`);
990
1332
  process.stdout.write(`- natsStream: ${jetstream.stream}\n`);
@@ -1037,6 +1379,18 @@ async function main() {
1037
1379
  const taskPromise = taskPromiseFactory();
1038
1380
  trackInFlight(taskPromise);
1039
1381
  }
1382
+ subscribeToFsRpc({
1383
+ jetstream,
1384
+ serverBaseUrl,
1385
+ userId,
1386
+ agentId: initialAgentId,
1387
+ agentToken,
1388
+ });
1389
+ subscribeToShellRpc({
1390
+ jetstream,
1391
+ userId,
1392
+ agentId: initialAgentId,
1393
+ });
1040
1394
  for (const pendingTaskId of pendingTaskIds) {
1041
1395
  await waitForAvailableSlot();
1042
1396
  scheduleTask(async () => {
@@ -1166,6 +1520,22 @@ async function main() {
1166
1520
  natsBootstrap = refreshed.natsBootstrap;
1167
1521
  pendingTaskIds = refreshed.pendingTaskIds;
1168
1522
  jetstream = refreshed.jetstream;
1523
+ const refreshedAgentId = typeof natsBootstrap.agentId === "string" ? natsBootstrap.agentId : "";
1524
+ if (!refreshedAgentId) {
1525
+ throw new Error("agent id missing from refreshed bootstrap");
1526
+ }
1527
+ subscribeToFsRpc({
1528
+ jetstream,
1529
+ serverBaseUrl,
1530
+ userId,
1531
+ agentId: refreshedAgentId,
1532
+ agentToken,
1533
+ });
1534
+ subscribeToShellRpc({
1535
+ jetstream,
1536
+ userId,
1537
+ agentId: refreshedAgentId,
1538
+ });
1169
1539
  for (const pendingTaskId of pendingTaskIds) {
1170
1540
  await waitForAvailableSlot();
1171
1541
  scheduleTask(async () => {
@@ -0,0 +1,199 @@
1
+ #!/usr/bin/env node
2
+ import { Buffer } from "node:buffer";
3
+ import { mkdir } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { chromium } from "playwright-core";
6
+ function printHelp() {
7
+ console.log(`playwright-cdp
8
+
9
+ Usage:
10
+ PLAYWRIGHT_CDP_ENDPOINT=http://127.0.0.1:9222 \
11
+ playwright-cdp --tool <tool> --args-base64 <base64>
12
+
13
+ playwright-cdp \
14
+ --endpoint http://127.0.0.1:9222 \
15
+ --tool browser_navigate \
16
+ --args '{"url":"https://example.com"}'
17
+
18
+ Tools:
19
+ browser_list_tabs
20
+ browser_navigate
21
+ browser_click
22
+ browser_type
23
+ browser_screenshot
24
+ browser_get_html
25
+ browser_get_text
26
+ browser_eval
27
+ `);
28
+ }
29
+ function parseArgs(argv) {
30
+ const options = {};
31
+ for (let i = 0; i < argv.length; i += 1) {
32
+ const token = argv[i];
33
+ if (!token.startsWith("--"))
34
+ continue;
35
+ const key = token.slice(2);
36
+ const next = argv[i + 1];
37
+ if (!next || next.startsWith("--")) {
38
+ options[key] = true;
39
+ continue;
40
+ }
41
+ options[key] = next;
42
+ i += 1;
43
+ }
44
+ return options;
45
+ }
46
+ function requiredOption(options, key) {
47
+ const value = options[key];
48
+ if (typeof value !== "string" || !value.trim()) {
49
+ throw new Error(`--${key} is required`);
50
+ }
51
+ return value.trim();
52
+ }
53
+ function optionalOption(options, key) {
54
+ const value = options[key];
55
+ return typeof value === "string" && value.trim() ? value.trim() : null;
56
+ }
57
+ function readEndpoint(options) {
58
+ return optionalOption(options, "endpoint")
59
+ ?? process.env.PLAYWRIGHT_CDP_ENDPOINT
60
+ ?? process.env.CDP_ENDPOINT
61
+ ?? "";
62
+ }
63
+ function readTimeout(options) {
64
+ const raw = optionalOption(options, "timeout");
65
+ if (!raw)
66
+ return 10000;
67
+ const value = Number.parseInt(raw, 10);
68
+ if (Number.isNaN(value) || value < 0) {
69
+ throw new Error("--timeout must be a non-negative integer");
70
+ }
71
+ return value;
72
+ }
73
+ function readPayload(options) {
74
+ const argsJson = optionalOption(options, "args");
75
+ const argsBase64 = optionalOption(options, "args-base64");
76
+ if (!argsJson && !argsBase64) {
77
+ throw new Error("--args or --args-base64 is required");
78
+ }
79
+ const raw = argsJson ?? Buffer.from(argsBase64, "base64").toString("utf8");
80
+ const parsed = JSON.parse(raw);
81
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
82
+ throw new Error("tool args must decode to a JSON object");
83
+ }
84
+ return parsed;
85
+ }
86
+ function asString(value) {
87
+ return typeof value === "string" && value.trim() ? value.trim() : null;
88
+ }
89
+ function asBoolean(value) {
90
+ return value === true || value === "true";
91
+ }
92
+ function selectorFrom(args) {
93
+ return asString(args.selector)
94
+ ?? asString(args.locator)
95
+ ?? asString(args.query)
96
+ ?? (() => { throw new Error("selector is required in args"); })();
97
+ }
98
+ async function resolvePage(browser, args) {
99
+ const contexts = browser.contexts();
100
+ const context = contexts[0] ?? (await browser.newContext());
101
+ if (asBoolean(args.newPage)) {
102
+ return context.newPage();
103
+ }
104
+ const pageIndexRaw = args.pageIndex;
105
+ const pageIndex = typeof pageIndexRaw === "number" ? pageIndexRaw : 0;
106
+ const pages = context.pages();
107
+ return pages[pageIndex] ?? pages[0] ?? context.newPage();
108
+ }
109
+ async function main() {
110
+ const options = parseArgs(process.argv.slice(2));
111
+ if (options.help === true || options.h === true) {
112
+ printHelp();
113
+ return;
114
+ }
115
+ const endpoint = readEndpoint(options);
116
+ if (!endpoint) {
117
+ throw new Error("CDP endpoint is required via --endpoint or PLAYWRIGHT_CDP_ENDPOINT");
118
+ }
119
+ const tool = requiredOption(options, "tool");
120
+ const args = readPayload(options);
121
+ const timeout = readTimeout(options);
122
+ const browser = await chromium.connectOverCDP(endpoint, { timeout });
123
+ try {
124
+ if (tool === "browser_list_tabs") {
125
+ const payload = browser.contexts().map((context, contextIndex) => ({
126
+ contextIndex,
127
+ pages: context.pages().map((page, pageIndex) => ({
128
+ pageIndex,
129
+ url: page.url(),
130
+ })),
131
+ }));
132
+ console.log(JSON.stringify({ tool, result: payload }, null, 2));
133
+ return;
134
+ }
135
+ const page = await resolvePage(browser, args);
136
+ switch (tool) {
137
+ case "browser_navigate": {
138
+ const url = asString(args.url);
139
+ if (!url)
140
+ throw new Error("url is required in args");
141
+ await page.goto(url, { waitUntil: "domcontentloaded", timeout });
142
+ console.log(JSON.stringify({ tool, url: page.url(), title: await page.title() }, null, 2));
143
+ return;
144
+ }
145
+ case "browser_click": {
146
+ const selector = selectorFrom(args);
147
+ await page.locator(selector).click({ timeout });
148
+ console.log(JSON.stringify({ tool, selector, url: page.url() }, null, 2));
149
+ return;
150
+ }
151
+ case "browser_type": {
152
+ const selector = selectorFrom(args);
153
+ const text = asString(args.text) ?? asString(args.value);
154
+ if (!text)
155
+ throw new Error("text is required in args");
156
+ await page.locator(selector).fill(text, { timeout });
157
+ console.log(JSON.stringify({ tool, selector, length: text.length }, null, 2));
158
+ return;
159
+ }
160
+ case "browser_screenshot": {
161
+ const out = asString(args.path) ?? asString(args.out) ?? path.join("artifacts", `playwright-cdp-${Date.now()}.png`);
162
+ await mkdir(path.dirname(out), { recursive: true });
163
+ await page.screenshot({ path: out, fullPage: true });
164
+ console.log(JSON.stringify({ tool, path: path.resolve(out), url: page.url() }, null, 2));
165
+ return;
166
+ }
167
+ case "browser_get_html": {
168
+ const selector = asString(args.selector) ?? asString(args.locator);
169
+ const html = selector ? await page.locator(selector).innerHTML({ timeout }) : await page.content();
170
+ console.log(html);
171
+ return;
172
+ }
173
+ case "browser_get_text": {
174
+ const selector = asString(args.selector) ?? asString(args.locator) ?? "body";
175
+ const text = await page.locator(selector).innerText({ timeout });
176
+ console.log(text);
177
+ return;
178
+ }
179
+ case "browser_eval": {
180
+ const expression = asString(args.expression) ?? asString(args.expr);
181
+ if (!expression)
182
+ throw new Error("expression is required in args");
183
+ const value = await page.evaluate((source) => globalThis.eval(source), expression);
184
+ console.log(JSON.stringify({ tool, value }, null, 2));
185
+ return;
186
+ }
187
+ default:
188
+ throw new Error(`unsupported tool: ${tool}`);
189
+ }
190
+ }
191
+ finally {
192
+ await browser.close();
193
+ }
194
+ }
195
+ main().catch((error) => {
196
+ const message = error instanceof Error ? error.message : String(error);
197
+ console.error(message);
198
+ process.exit(1);
199
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doer-agent",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "Reverse-polling agent runtime for doer",
5
5
  "type": "module",
6
6
  "main": "dist/agent.js",