wtagent 0.1.0-alpha.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/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "wtagent",
3
+ "version": "0.1.0-alpha.0",
4
+ "description": "Turn your own web AI session into a local tool-using agent.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/XiXian42/wtagent.git"
8
+ },
9
+ "homepage": "https://github.com/XiXian42/wtagent#readme",
10
+ "bugs": {
11
+ "url": "https://github.com/XiXian42/wtagent/issues"
12
+ },
13
+ "type": "module",
14
+ "bin": {
15
+ "wtagent": "src/cli/main.js"
16
+ },
17
+ "keywords": [
18
+ "agent",
19
+ "web-ai",
20
+ "chatgpt",
21
+ "cli",
22
+ "local-tools",
23
+ "browser"
24
+ ],
25
+ "files": [
26
+ "src",
27
+ "README.md",
28
+ "docs/technical-design.md"
29
+ ],
30
+ "scripts": {
31
+ "test": "node --test",
32
+ "test:unit": "node --test test/protocol.test.js test/policy.test.js test/tools.test.js test/prompt-builder.test.js test/session-export.test.js test/platform.test.js test/cli.test.js test/at-files.test.js",
33
+ "test:integration": "node --test test/runtime.test.js",
34
+ "check": "node scripts/check-syntax.js",
35
+ "doctor": "node src/cli/main.js doctor",
36
+ "login": "node src/cli/main.js login",
37
+ "prepack": "npm run check && npm test",
38
+ "release:alpha": "npm publish --tag alpha"
39
+ },
40
+ "engines": {
41
+ "node": ">=20.17.0"
42
+ },
43
+ "license": "MIT",
44
+ "publishConfig": {
45
+ "registry": "https://registry.npmjs.org/",
46
+ "access": "public"
47
+ },
48
+ "dependencies": {
49
+ "@inquirer/prompts": "^8.5.2",
50
+ "commander": "^15.0.0",
51
+ "fast-xml-parser": "^5.10.1",
52
+ "playwright-core": "^1.62.0",
53
+ "zod": "^4.4.3"
54
+ }
55
+ }
@@ -0,0 +1,170 @@
1
+ import net from "node:net";
2
+ import { spawn } from "node:child_process";
3
+ import { chromium } from "playwright-core";
4
+ import { killProcessTree } from "../tools/process-utils.js";
5
+
6
+ async function reservePort() {
7
+ return await new Promise((resolve, reject) => {
8
+ const server = net.createServer();
9
+ server.unref();
10
+ server.once("error", reject);
11
+ server.listen(0, "127.0.0.1", () => {
12
+ const address = server.address();
13
+ const port = typeof address === "object" ? address.port : null;
14
+ server.close((error) => {
15
+ if (error) {
16
+ reject(error);
17
+ } else {
18
+ resolve(port);
19
+ }
20
+ });
21
+ });
22
+ });
23
+ }
24
+
25
+ async function waitForCdp({
26
+ endpoint,
27
+ child,
28
+ timeoutMs = 15_000,
29
+ }) {
30
+ const deadline = Date.now() + timeoutMs;
31
+ let lastError;
32
+
33
+ while (Date.now() < deadline) {
34
+ if (child.exitCode != null || child.signalCode != null) {
35
+ throw new Error(
36
+ `Chrome exited before CDP became ready (exit=${child.exitCode}, signal=${child.signalCode}).`,
37
+ );
38
+ }
39
+
40
+ try {
41
+ const response = await fetch(`${endpoint}/json/version`);
42
+ if (response.ok) {
43
+ const version = await response.json();
44
+ if (version.webSocketDebuggerUrl) {
45
+ return;
46
+ }
47
+ }
48
+ } catch (error) {
49
+ lastError = error;
50
+ }
51
+
52
+ await new Promise((resolve) => setTimeout(resolve, 100));
53
+ }
54
+
55
+ throw new Error(
56
+ `Chrome CDP did not become ready at ${endpoint}: ${lastError?.message ?? "timeout"}`,
57
+ );
58
+ }
59
+
60
+ async function waitForExit(child, timeoutMs) {
61
+ if (child.exitCode != null || child.signalCode != null) {
62
+ return true;
63
+ }
64
+
65
+ return await new Promise((resolve) => {
66
+ const timer = setTimeout(() => resolve(false), timeoutMs);
67
+ timer.unref?.();
68
+ child.once("close", () => {
69
+ clearTimeout(timer);
70
+ resolve(true);
71
+ });
72
+ });
73
+ }
74
+
75
+ // Sets the OS window state (e.g. "minimized" / "normal") of the window hosting
76
+ // `page` via the CDP Browser domain. On macOS the Chromium launch flags for
77
+ // minimizing (--start-minimized) and off-screen positioning are ignored or
78
+ // clamped, but Browser.setWindowBounds works reliably and does not throttle the
79
+ // page (ChatGPT still renders while minimized). Best-effort: any failure is
80
+ // swallowed so window chrome never breaks the run.
81
+ async function setWindowState(context, page, windowState) {
82
+ try {
83
+ const session = await context.newCDPSession(page);
84
+ const { targetInfo } = await session.send("Target.getTargetInfo");
85
+ const { windowId } = await session.send("Browser.getWindowForTarget", {
86
+ targetId: targetInfo.targetId,
87
+ });
88
+ await session.send("Browser.setWindowBounds", {
89
+ windowId,
90
+ bounds: { windowState },
91
+ });
92
+ await session.detach().catch(() => null);
93
+ return true;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ export async function launchAndConnectCdpChrome({
100
+ executablePath,
101
+ profileDir,
102
+ url = "about:blank",
103
+ minimized = false,
104
+ }) {
105
+ const port = await reservePort();
106
+ const endpoint = `http://127.0.0.1:${port}`;
107
+ const child = spawn(
108
+ executablePath,
109
+ [
110
+ `--remote-debugging-port=${port}`,
111
+ "--remote-debugging-address=127.0.0.1",
112
+ `--user-data-dir=${profileDir}`,
113
+ "--profile-directory=Default",
114
+ "--no-first-run",
115
+ "--no-default-browser-check",
116
+ url,
117
+ ],
118
+ {
119
+ detached: process.platform !== "win32",
120
+ stdio: "ignore",
121
+ windowsHide: false,
122
+ },
123
+ );
124
+
125
+ await new Promise((resolve, reject) => {
126
+ child.once("spawn", resolve);
127
+ child.once("error", reject);
128
+ });
129
+
130
+ try {
131
+ await waitForCdp({ endpoint, child });
132
+ const browser = await chromium.connectOverCDP(endpoint);
133
+ const context = browser.contexts()[0];
134
+ if (!context) {
135
+ throw new Error("Chrome CDP connection did not expose a browser context.");
136
+ }
137
+
138
+ const page = context.pages()[0] ?? await context.newPage();
139
+ if (minimized) {
140
+ await setWindowState(context, page, "minimized");
141
+ }
142
+
143
+ return {
144
+ browser,
145
+ context,
146
+ child,
147
+ endpoint,
148
+ // Minimize / restore the visible window on demand. The runtime restores
149
+ // the window when it needs the user (manual login, CAPTCHA) and
150
+ // re-minimizes afterward. Uses the live current page each time so it
151
+ // targets the window the user is actually looking at.
152
+ async minimize() {
153
+ return await setWindowState(context, context.pages()[0] ?? page, "minimized");
154
+ },
155
+ async restore() {
156
+ return await setWindowState(context, context.pages()[0] ?? page, "normal");
157
+ },
158
+ async close() {
159
+ await browser.close().catch(() => null);
160
+ if (!await waitForExit(child, 2_000)) {
161
+ await killProcessTree(child.pid);
162
+ await waitForExit(child, 2_000);
163
+ }
164
+ },
165
+ };
166
+ } catch (error) {
167
+ await killProcessTree(child.pid);
168
+ throw error;
169
+ }
170
+ }