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 +379 -9
- package/dist/playwright-cdp.js +199 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
963
|
-
|
|
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: ${
|
|
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
|
+
});
|