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/LICENSE +21 -0
- package/README.md +83 -0
- package/docs/technical-design.md +838 -0
- package/package.json +55 -0
- package/src/browser/cdp-browser.js +170 -0
- package/src/browser/chatgpt-web-adapter.js +717 -0
- package/src/browser/fake-web-model-adapter.js +86 -0
- package/src/browser/mode-selection.js +179 -0
- package/src/browser/native-login.js +49 -0
- package/src/cli/at-files.js +93 -0
- package/src/cli/main.js +629 -0
- package/src/cli/render-events.js +367 -0
- package/src/platform/chrome-discovery.js +94 -0
- package/src/platform/paths.js +73 -0
- package/src/policy/path-guard.js +114 -0
- package/src/policy/policy-engine.js +277 -0
- package/src/protocol/markers.js +64 -0
- package/src/protocol/prompt-builder.js +152 -0
- package/src/protocol/xml-protocol.js +333 -0
- package/src/runtime/agent-runtime.js +636 -0
- package/src/session/agent-session.js +569 -0
- package/src/session/canonical-transcript.js +145 -0
- package/src/session/session-export.js +202 -0
- package/src/session/task-session.js +5 -0
- package/src/shared/errors.js +52 -0
- package/src/shared/limits.js +31 -0
- package/src/tools/default-tools.js +555 -0
- package/src/tools/process-manager.js +116 -0
- package/src/tools/process-utils.js +31 -0
- package/src/tools/registry.js +129 -0
- package/src/tools/safe-env.js +67 -0
- package/src/tools/terminal-exec.js +118 -0
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
|
+
}
|