wtagent 0.1.0 → 0.2.1
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 +53 -35
- package/package.json +2 -2
- package/src/browser/base-web-adapter.js +1178 -0
- package/src/browser/cdp-browser.js +62 -0
- package/src/browser/cdp-state.js +52 -4
- package/src/browser/chatgpt-web-adapter.js +301 -992
- package/src/browser/claude-web-adapter.js +205 -0
- package/src/browser/deepseek-web-adapter.js +337 -0
- package/src/browser/gemini-web-adapter.js +465 -0
- package/src/browser/glm-web-adapter.js +246 -0
- package/src/browser/kimi-web-adapter.js +285 -0
- package/src/browser/provider-registry.js +199 -0
- package/src/cli/main.js +284 -60
- package/src/cli/notice-store.js +125 -0
- package/src/cli/prompt-input.js +504 -170
- package/src/cli/render-events.js +14 -8
- package/src/cli/self-update.js +145 -0
- package/src/cli/startup-notices.js +161 -0
- package/src/platform/paths.js +7 -0
- package/src/platform/windows-diagnostics.js +37 -6
- package/src/protocol/markers.js +3 -2
- package/src/protocol/prompt-builder.js +9 -5
- package/src/protocol/xml-protocol.js +193 -11
- package/src/runtime/agent-runtime.js +131 -18
- package/src/session/agent-session.js +4 -1
- package/src/session/canonical-transcript.js +2 -2
- package/src/session/session-export.js +2 -2
- package/src/shared/fetch-json.js +24 -0
- package/src/shared/limits.js +11 -1
- package/src/shared/package-info.js +23 -0
- package/src/shared/usage-limit.js +2 -2
|
@@ -108,6 +108,55 @@ async function waitForCdp({
|
|
|
108
108
|
);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// A headful Chrome process can stay alive in background mode after its final
|
|
112
|
+
// window is closed. Its browser CDP endpoint remains healthy, but recent
|
|
113
|
+
// Playwright versions cannot initialize the default context when Chrome has no
|
|
114
|
+
// page target (`Browser.setDownloadBehavior`: context management unsupported).
|
|
115
|
+
// Recreate one blank tab through Chrome's own local debugging endpoint before
|
|
116
|
+
// connecting. Existing tabs and conversations are left untouched.
|
|
117
|
+
export async function ensureCdpPageTarget(
|
|
118
|
+
endpoint,
|
|
119
|
+
{ fetchImpl = fetch, timeoutMs = 1_500 } = {},
|
|
120
|
+
) {
|
|
121
|
+
let targets;
|
|
122
|
+
try {
|
|
123
|
+
const response = await fetchImpl(`${endpoint}/json/list`, {
|
|
124
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
125
|
+
});
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
targets = await response.json();
|
|
130
|
+
} catch {
|
|
131
|
+
// This preflight is compatibility hardening. If the optional discovery
|
|
132
|
+
// endpoint is unavailable, preserve the normal Playwright connect path and
|
|
133
|
+
// let it report the authoritative CDP error.
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!Array.isArray(targets) || targets.some((target) => target?.type === "page")) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const response = await fetchImpl(
|
|
142
|
+
`${endpoint}/json/new?${encodeURIComponent("about:blank")}`,
|
|
143
|
+
{ method: "PUT", signal: AbortSignal.timeout(timeoutMs) },
|
|
144
|
+
);
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Chrome CDP at ${endpoint} has no page target and could not create one `
|
|
148
|
+
+ `(HTTP ${response.status}).`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
const target = await response.json();
|
|
152
|
+
if (target?.type !== "page" || !target?.webSocketDebuggerUrl) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`Chrome CDP at ${endpoint} returned an invalid new-page target.`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
111
160
|
// Sets the OS window state (e.g. "minimized" / "normal") of the window hosting
|
|
112
161
|
// `page` via the CDP Browser domain. On macOS the Chromium launch flags for
|
|
113
162
|
// minimizing (--start-minimized) and off-screen positioning are ignored or
|
|
@@ -153,6 +202,7 @@ export async function launchAndConnectCdpChrome({
|
|
|
153
202
|
acquireProfileLock = acquireCdpProfileLock,
|
|
154
203
|
connectOverCDP = (endpoint) => chromium.connectOverCDP(endpoint),
|
|
155
204
|
discoverReusable = discoverReusableCdpState,
|
|
205
|
+
ensurePageTarget = ensureCdpPageTarget,
|
|
156
206
|
fetchVersion = fetchCdpVersion,
|
|
157
207
|
killTree = killProcessTree,
|
|
158
208
|
matchesState = processMatchesCdpState,
|
|
@@ -244,6 +294,9 @@ export async function launchAndConnectCdpChrome({
|
|
|
244
294
|
// without this guard Playwright hangs ~30s and leaves a dirty CDP state.
|
|
245
295
|
let context;
|
|
246
296
|
try {
|
|
297
|
+
if (reused) {
|
|
298
|
+
await ensurePageTarget(state.endpoint);
|
|
299
|
+
}
|
|
247
300
|
browser = await withTimeout(
|
|
248
301
|
connectOverCDP(state.endpoint),
|
|
249
302
|
connectTimeoutMs,
|
|
@@ -318,9 +371,18 @@ export async function launchAndConnectCdpChrome({
|
|
|
318
371
|
// Drops only the Playwright transport. Unlike close(), never asks Chrome
|
|
319
372
|
// to exit and never kills the process: used to recover from a dead CDP
|
|
320
373
|
// connection (e.g. after the Mac slept) while Chrome itself is alive.
|
|
374
|
+
// The profile lock stays held because the same CLI session will reconnect.
|
|
321
375
|
async disconnect() {
|
|
322
376
|
await settleWithin(browser.close(), 1_500);
|
|
323
377
|
},
|
|
378
|
+
// Leaves Chrome running (and its saved CDP state intact) but drops the
|
|
379
|
+
// Playwright transport and the profile lock so a later WTAgent process
|
|
380
|
+
// can reuse the window. Used after a failed run that wants the page
|
|
381
|
+
// left open for inspection without blocking the next launch.
|
|
382
|
+
async detach() {
|
|
383
|
+
await settleWithin(browser.close(), 1_500);
|
|
384
|
+
await releaseProfileLock();
|
|
385
|
+
},
|
|
324
386
|
async close() {
|
|
325
387
|
if (closePromise) {
|
|
326
388
|
return await closePromise;
|
package/src/browser/cdp-state.js
CHANGED
|
@@ -541,9 +541,38 @@ export async function inspectCdpProfileState(profileDir, {
|
|
|
541
541
|
};
|
|
542
542
|
}
|
|
543
543
|
|
|
544
|
+
// The instant a process was started, in epoch milliseconds. Used to tell a
|
|
545
|
+
// live lock-holder pid from a recycled one: a process that started after the
|
|
546
|
+
// lock file was written cannot own it. Returns null when it cannot be
|
|
547
|
+
// determined (unknown process, platform tool unavailable).
|
|
548
|
+
async function processStartTimeMs(pid, { execFileImpl = execFileAsync } = {}) {
|
|
549
|
+
if (process.platform === "win32") {
|
|
550
|
+
const { stdout } = await execFileImpl(
|
|
551
|
+
"powershell.exe",
|
|
552
|
+
[
|
|
553
|
+
"-NoProfile",
|
|
554
|
+
"-NonInteractive",
|
|
555
|
+
"-Command",
|
|
556
|
+
`(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CreationDate`,
|
|
557
|
+
],
|
|
558
|
+
{ windowsHide: true },
|
|
559
|
+
);
|
|
560
|
+
const parsed = Date.parse(String(stdout).trim());
|
|
561
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const { stdout } = await execFileImpl(
|
|
565
|
+
"ps",
|
|
566
|
+
["-p", String(pid), "-o", "etimes="],
|
|
567
|
+
);
|
|
568
|
+
const seconds = Number.parseInt(String(stdout), 10);
|
|
569
|
+
return Number.isFinite(seconds) ? Date.now() - seconds * 1_000 : null;
|
|
570
|
+
}
|
|
571
|
+
|
|
544
572
|
export async function acquireCdpProfileLock(profileDir, {
|
|
545
573
|
ownerPid = process.pid,
|
|
546
574
|
isAlive = isProcessAlive,
|
|
575
|
+
processStartTime = processStartTimeMs,
|
|
547
576
|
} = {}) {
|
|
548
577
|
const resolvedProfile = path.resolve(profileDir);
|
|
549
578
|
const filePath = lockPath(resolvedProfile);
|
|
@@ -581,10 +610,29 @@ export async function acquireCdpProfileLock(profileDir, {
|
|
|
581
610
|
}
|
|
582
611
|
const existing = await readInitializedLock(filePath);
|
|
583
612
|
if (existing && isAlive(Number(existing.pid))) {
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
613
|
+
// A live pid usually IS the real holder, but a crashed session's pid
|
|
614
|
+
// can be recycled by an unrelated process. A process that started
|
|
615
|
+
// after the lock was written cannot own it, so the lock is stale and
|
|
616
|
+
// may be removed. When the start time is unknown, assume the holder is
|
|
617
|
+
// real rather than deleting a lock we cannot verify is stale.
|
|
618
|
+
const startedAt = await processStartTime(Number(existing.pid))
|
|
619
|
+
.catch(() => null);
|
|
620
|
+
const lockCreatedAt = Date.parse(existing.createdAt ?? "");
|
|
621
|
+
const recycled = Number.isFinite(lockCreatedAt)
|
|
622
|
+
&& startedAt != null
|
|
623
|
+
&& startedAt > lockCreatedAt + 2_000;
|
|
624
|
+
if (!recycled) {
|
|
625
|
+
const error = new Error(
|
|
626
|
+
`Another WTAgent session (pid=${existing.pid}) is already using `
|
|
627
|
+
+ `${resolvedProfile}.`,
|
|
628
|
+
);
|
|
629
|
+
error.code = "PROFILE_LOCKED";
|
|
630
|
+
error.details = {
|
|
631
|
+
pid: Number(existing.pid),
|
|
632
|
+
profileDir: resolvedProfile,
|
|
633
|
+
};
|
|
634
|
+
throw error;
|
|
635
|
+
}
|
|
588
636
|
}
|
|
589
637
|
if (!existing) {
|
|
590
638
|
const stat = await fs.stat(filePath).catch(() => null);
|