wtagent 0.2.2 → 0.4.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/README.md +4 -4
- package/docs/technical-design.md +18 -11
- package/package.json +6 -6
- package/src/browser/base-web-adapter.js +2085 -202
- package/src/browser/cdp-browser.js +223 -25
- package/src/browser/cdp-state.js +8 -5
- package/src/browser/chatgpt-web-adapter.js +803 -218
- package/src/browser/claude-web-adapter.js +13 -0
- package/src/browser/deepseek-web-adapter.js +0 -106
- package/src/browser/fake-web-model-adapter.js +137 -3
- package/src/browser/gemini-web-adapter.js +10 -0
- package/src/browser/glm-web-adapter.js +4 -91
- package/src/browser/grok-web-adapter.js +145 -0
- package/src/browser/kimi-web-adapter.js +2 -74
- package/src/browser/provider-registry.js +8 -50
- package/src/cli/i18n.js +949 -0
- package/src/cli/main.js +217 -223
- package/src/cli/prompt-input.js +349 -151
- package/src/cli/render-events.js +45 -35
- package/src/cli/self-update.js +10 -3
- package/src/platform/command-launcher.js +1 -1
- package/src/platform/paths.js +19 -0
- package/src/platform/windows-diagnostics.js +9 -16
- package/src/policy/path-guard.js +5 -1
- package/src/runtime/agent-runtime.js +1065 -237
- package/src/session/agent-session.js +975 -120
- package/src/shared/limits.js +2 -9
- package/src/tools/default-tools.js +1 -1
- package/src/browser/mode-selection.js +0 -214
- package/src/cli/mode-choice.js +0 -25
|
@@ -59,6 +59,12 @@ class CdpTimeoutError extends Error {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
function recoveryTargetUnavailable(message, cause = null) {
|
|
63
|
+
const error = new Error(message, cause ? { cause } : undefined);
|
|
64
|
+
error.code = "RECOVERY_TARGET_UNAVAILABLE";
|
|
65
|
+
return error;
|
|
66
|
+
}
|
|
67
|
+
|
|
62
68
|
async function withTimeout(promise, timeoutMs, message) {
|
|
63
69
|
let timer;
|
|
64
70
|
try {
|
|
@@ -192,12 +198,49 @@ function sameOriginPath(left, right) {
|
|
|
192
198
|
}
|
|
193
199
|
}
|
|
194
200
|
|
|
201
|
+
async function getPageTargetId(context, page, { timeoutMs = 1_500 } = {}) {
|
|
202
|
+
if (!page || typeof context?.newCDPSession !== "function") {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
let session = null;
|
|
206
|
+
const probe = (async () => {
|
|
207
|
+
session = await context.newCDPSession(page).catch(() => null);
|
|
208
|
+
if (!session) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
const { targetInfo } = await session.send("Target.getTargetInfo");
|
|
213
|
+
return targetInfo?.targetId ?? null;
|
|
214
|
+
} catch {
|
|
215
|
+
return null;
|
|
216
|
+
} finally {
|
|
217
|
+
await settleWithin(
|
|
218
|
+
Promise.resolve().then(() => session.detach()),
|
|
219
|
+
500,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
})();
|
|
223
|
+
try {
|
|
224
|
+
return await withTimeout(
|
|
225
|
+
probe,
|
|
226
|
+
timeoutMs,
|
|
227
|
+
`Timed out reading the Chrome target identity after ${timeoutMs}ms.`,
|
|
228
|
+
);
|
|
229
|
+
} catch {
|
|
230
|
+
// A target that cannot prove its identity is never eligible for selection.
|
|
231
|
+
// The still-running probe owns eventual session cleanup through its finally.
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
195
236
|
export async function launchAndConnectCdpChrome({
|
|
196
237
|
executablePath,
|
|
197
238
|
profileDir,
|
|
198
239
|
url = "about:blank",
|
|
199
240
|
minimized = false,
|
|
200
241
|
preferredUrl = null,
|
|
242
|
+
preferredTargetId = null,
|
|
243
|
+
exactTargetOnly = false,
|
|
201
244
|
}, {
|
|
202
245
|
acquireProfileLock = acquireCdpProfileLock,
|
|
203
246
|
connectOverCDP = (endpoint) => chromium.connectOverCDP(endpoint),
|
|
@@ -214,7 +257,19 @@ export async function launchAndConnectCdpChrome({
|
|
|
214
257
|
waitForExit = waitForProcessExit,
|
|
215
258
|
waitForReady = waitForCdp,
|
|
216
259
|
connectTimeoutMs = 20_000,
|
|
260
|
+
transportCloseTimeoutMs = 1_500,
|
|
217
261
|
} = {}) {
|
|
262
|
+
if (
|
|
263
|
+
exactTargetOnly
|
|
264
|
+
&& (
|
|
265
|
+
typeof preferredTargetId !== "string"
|
|
266
|
+
|| preferredTargetId.trim().length === 0
|
|
267
|
+
)
|
|
268
|
+
) {
|
|
269
|
+
throw recoveryTargetUnavailable(
|
|
270
|
+
"Exact-target recovery requires a saved Chrome target ID.",
|
|
271
|
+
);
|
|
272
|
+
}
|
|
218
273
|
const releaseProfileLock = await acquireProfileLock(profileDir);
|
|
219
274
|
let child = null;
|
|
220
275
|
let state = null;
|
|
@@ -227,9 +282,23 @@ export async function launchAndConnectCdpChrome({
|
|
|
227
282
|
}
|
|
228
283
|
|
|
229
284
|
try {
|
|
230
|
-
|
|
285
|
+
try {
|
|
286
|
+
state = await findReusable();
|
|
287
|
+
} catch (error) {
|
|
288
|
+
if (exactTargetOnly) {
|
|
289
|
+
throw recoveryTargetUnavailable(
|
|
290
|
+
"The saved Chrome instance could not be inspected for recovery.",
|
|
291
|
+
error,
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
throw error;
|
|
295
|
+
}
|
|
231
296
|
if (state) {
|
|
232
297
|
reused = true;
|
|
298
|
+
} else if (exactTargetOnly) {
|
|
299
|
+
throw recoveryTargetUnavailable(
|
|
300
|
+
"The saved Chrome instance is not available for exact-target recovery.",
|
|
301
|
+
);
|
|
233
302
|
} else {
|
|
234
303
|
await removeState(profileDir);
|
|
235
304
|
// A prior instance may have died leaving renderer children (and Chrome's
|
|
@@ -294,14 +363,28 @@ export async function launchAndConnectCdpChrome({
|
|
|
294
363
|
// without this guard Playwright hangs ~30s and leaves a dirty CDP state.
|
|
295
364
|
let context;
|
|
296
365
|
try {
|
|
297
|
-
if (reused) {
|
|
366
|
+
if (reused && !exactTargetOnly) {
|
|
298
367
|
await ensurePageTarget(state.endpoint);
|
|
299
368
|
}
|
|
300
|
-
|
|
301
|
-
connectOverCDP(state.endpoint)
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
369
|
+
const connectPromise = Promise.resolve().then(() => (
|
|
370
|
+
connectOverCDP(state.endpoint)
|
|
371
|
+
));
|
|
372
|
+
try {
|
|
373
|
+
browser = await withTimeout(
|
|
374
|
+
connectPromise,
|
|
375
|
+
connectTimeoutMs,
|
|
376
|
+
`Timed out connecting to Chrome CDP at ${state.endpoint} after ${connectTimeoutMs}ms.`,
|
|
377
|
+
);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
if (error instanceof CdpTimeoutError) {
|
|
380
|
+
// connectOverCDP is not cancellable. If it completes after our timeout,
|
|
381
|
+
// disconnect that late transport instead of leaking ownership of Chrome.
|
|
382
|
+
void connectPromise.then((lateBrowser) => (
|
|
383
|
+
settleWithin(lateBrowser?.close?.(), transportCloseTimeoutMs)
|
|
384
|
+
)).catch(() => null);
|
|
385
|
+
}
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
305
388
|
// contexts() forces a real protocol round-trip, so it hangs too when the
|
|
306
389
|
// browser main thread is stuck — keep it inside the timeout budget.
|
|
307
390
|
const contexts = await withTimeout(
|
|
@@ -311,11 +394,25 @@ export async function launchAndConnectCdpChrome({
|
|
|
311
394
|
);
|
|
312
395
|
context = contexts[0];
|
|
313
396
|
} catch (error) {
|
|
397
|
+
if (exactTargetOnly) {
|
|
398
|
+
await settleWithin(
|
|
399
|
+
Promise.resolve().then(() => browser?.close()),
|
|
400
|
+
transportCloseTimeoutMs,
|
|
401
|
+
);
|
|
402
|
+
browser = null;
|
|
403
|
+
throw recoveryTargetUnavailable(
|
|
404
|
+
"The saved Chrome instance could not be attached for exact-target recovery.",
|
|
405
|
+
error,
|
|
406
|
+
);
|
|
407
|
+
}
|
|
314
408
|
if (error instanceof CdpTimeoutError) {
|
|
315
409
|
// The verified-but-unusable instance we launched is a dead end. Kill it
|
|
316
410
|
// (only if we own it) and drop its CDP state so the next run starts
|
|
317
411
|
// clean instead of trying to reuse a hung endpoint.
|
|
318
|
-
await
|
|
412
|
+
await settleWithin(
|
|
413
|
+
Promise.resolve().then(() => browser?.close()),
|
|
414
|
+
transportCloseTimeoutMs,
|
|
415
|
+
);
|
|
319
416
|
if (!reused && child?.pid) {
|
|
320
417
|
await killTree(child.pid).catch(() => null);
|
|
321
418
|
}
|
|
@@ -328,25 +425,85 @@ export async function launchAndConnectCdpChrome({
|
|
|
328
425
|
throw error;
|
|
329
426
|
}
|
|
330
427
|
if (!context) {
|
|
331
|
-
throw
|
|
428
|
+
throw exactTargetOnly
|
|
429
|
+
? recoveryTargetUnavailable(
|
|
430
|
+
"The saved Chrome instance has no reusable browser context.",
|
|
431
|
+
)
|
|
432
|
+
: new Error("Chrome CDP connection did not expose a browser context.");
|
|
332
433
|
}
|
|
333
434
|
|
|
334
435
|
// A reused browser may still contain the previous conversation. Keep it
|
|
335
436
|
// intact and, when a preferred URL is given, reuse an existing tab already
|
|
336
437
|
// showing that conversation (origin + path) so resumed runs do not pile up
|
|
337
438
|
// tabs; otherwise create a fresh target for this CLI session.
|
|
439
|
+
const targetProbeDeadline = Date.now() + connectTimeoutMs;
|
|
440
|
+
const probeTargetId = async (candidate) => {
|
|
441
|
+
const remaining = targetProbeDeadline - Date.now();
|
|
442
|
+
if (remaining <= 0) {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
return await getPageTargetId(context, candidate, {
|
|
446
|
+
timeoutMs: Math.min(1_500, remaining),
|
|
447
|
+
});
|
|
448
|
+
};
|
|
449
|
+
const pages = context.pages();
|
|
450
|
+
const existingPageUrls = pages.map((candidate) => (
|
|
451
|
+
candidate.url?.() ?? ""
|
|
452
|
+
));
|
|
338
453
|
let page;
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
454
|
+
let preferredTabMatched = false;
|
|
455
|
+
let preferredTargetMatched = false;
|
|
456
|
+
let preferredTabAmbiguous = false;
|
|
457
|
+
let targetId = null;
|
|
458
|
+
if (exactTargetOnly) {
|
|
459
|
+
const matchingTargets = [];
|
|
460
|
+
for (const candidate of pages) {
|
|
461
|
+
const candidateTargetId = await probeTargetId(candidate);
|
|
462
|
+
if (candidateTargetId === preferredTargetId) {
|
|
463
|
+
matchingTargets.push({ page: candidate, targetId: candidateTargetId });
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (matchingTargets.length !== 1) {
|
|
467
|
+
throw recoveryTargetUnavailable(
|
|
468
|
+
matchingTargets.length === 0
|
|
469
|
+
? "The exact saved Chrome target is no longer available."
|
|
470
|
+
: "The saved Chrome target identity is ambiguous.",
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
page = matchingTargets[0].page;
|
|
474
|
+
targetId = matchingTargets[0].targetId;
|
|
475
|
+
preferredTargetMatched = true;
|
|
476
|
+
} else if (reused) {
|
|
477
|
+
if (preferredTargetId) {
|
|
478
|
+
for (const candidate of pages) {
|
|
479
|
+
const candidateTargetId = await probeTargetId(candidate);
|
|
480
|
+
if (candidateTargetId === preferredTargetId) {
|
|
481
|
+
page = candidate;
|
|
482
|
+
targetId = candidateTargetId;
|
|
483
|
+
preferredTargetMatched = true;
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (!page && preferredUrl) {
|
|
489
|
+
const matchingTabs = pages.filter((candidate) =>
|
|
342
490
|
sameOriginPath(candidate.url?.() ?? "", preferredUrl)
|
|
343
|
-
)
|
|
344
|
-
|
|
345
|
-
|
|
491
|
+
);
|
|
492
|
+
// Duplicate exact-URL tabs are ambiguous. Opening a fresh page is safer
|
|
493
|
+
// than attaching a pending result or follow-up to an arbitrary copy.
|
|
494
|
+
if (matchingTabs.length === 1) {
|
|
495
|
+
page = matchingTabs[0];
|
|
496
|
+
preferredTabMatched = true;
|
|
497
|
+
} else if (matchingTabs.length > 1) {
|
|
498
|
+
preferredTabAmbiguous = true;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
page ??= await context.newPage();
|
|
346
502
|
} else {
|
|
347
|
-
page =
|
|
503
|
+
page = pages[0] ?? await context.newPage();
|
|
348
504
|
}
|
|
349
|
-
|
|
505
|
+
targetId ??= await probeTargetId(page);
|
|
506
|
+
if (minimized && !exactTargetOnly) {
|
|
350
507
|
await setWindowState(context, page, "minimized");
|
|
351
508
|
}
|
|
352
509
|
|
|
@@ -358,35 +515,67 @@ export async function launchAndConnectCdpChrome({
|
|
|
358
515
|
page,
|
|
359
516
|
pid: state.pid,
|
|
360
517
|
reused,
|
|
518
|
+
targetId,
|
|
519
|
+
existingPageUrls,
|
|
520
|
+
exactTargetOnly,
|
|
521
|
+
preferredTabMatched,
|
|
522
|
+
preferredTargetMatched,
|
|
523
|
+
preferredTabAmbiguous,
|
|
361
524
|
// Minimize / restore the visible window on demand. The runtime restores
|
|
362
525
|
// the window when it needs the user (manual login, CAPTCHA) and
|
|
363
526
|
// re-minimizes afterward. Uses the live current page each time so it
|
|
364
527
|
// targets the window the user is actually looking at.
|
|
365
528
|
async minimize() {
|
|
366
|
-
return
|
|
529
|
+
return exactTargetOnly
|
|
530
|
+
? false
|
|
531
|
+
: await setWindowState(context, page, "minimized");
|
|
367
532
|
},
|
|
368
533
|
async restore() {
|
|
369
|
-
return
|
|
534
|
+
return exactTargetOnly
|
|
535
|
+
? false
|
|
536
|
+
: await setWindowState(context, page, "normal");
|
|
370
537
|
},
|
|
371
538
|
// Drops only the Playwright transport. Unlike close(), never asks Chrome
|
|
372
539
|
// to exit and never kills the process: used to recover from a dead CDP
|
|
373
540
|
// connection (e.g. after the Mac slept) while Chrome itself is alive.
|
|
374
541
|
// The profile lock stays held because the same CLI session will reconnect.
|
|
375
542
|
async disconnect() {
|
|
376
|
-
await settleWithin(
|
|
543
|
+
await settleWithin(
|
|
544
|
+
Promise.resolve().then(() => browser.close()),
|
|
545
|
+
transportCloseTimeoutMs,
|
|
546
|
+
);
|
|
377
547
|
},
|
|
378
548
|
// Leaves Chrome running (and its saved CDP state intact) but drops the
|
|
379
549
|
// Playwright transport and the profile lock so a later WTAgent process
|
|
380
550
|
// can reuse the window. Used after a failed run that wants the page
|
|
381
551
|
// left open for inspection without blocking the next launch.
|
|
382
552
|
async detach() {
|
|
383
|
-
|
|
384
|
-
|
|
553
|
+
try {
|
|
554
|
+
await settleWithin(
|
|
555
|
+
Promise.resolve().then(() => browser.close()),
|
|
556
|
+
transportCloseTimeoutMs,
|
|
557
|
+
);
|
|
558
|
+
} finally {
|
|
559
|
+
await releaseProfileLock();
|
|
560
|
+
}
|
|
385
561
|
},
|
|
386
562
|
async close() {
|
|
387
563
|
if (closePromise) {
|
|
388
564
|
return await closePromise;
|
|
389
565
|
}
|
|
566
|
+
if (exactTargetOnly) {
|
|
567
|
+
closePromise = (async () => {
|
|
568
|
+
try {
|
|
569
|
+
await settleWithin(
|
|
570
|
+
Promise.resolve().then(() => browser.close()),
|
|
571
|
+
transportCloseTimeoutMs,
|
|
572
|
+
);
|
|
573
|
+
} finally {
|
|
574
|
+
await releaseProfileLock();
|
|
575
|
+
}
|
|
576
|
+
})();
|
|
577
|
+
return await closePromise;
|
|
578
|
+
}
|
|
390
579
|
closePromise = (async () => {
|
|
391
580
|
let exited = false;
|
|
392
581
|
try {
|
|
@@ -396,10 +585,16 @@ export async function launchAndConnectCdpChrome({
|
|
|
396
585
|
.catch(() => null);
|
|
397
586
|
if (session) {
|
|
398
587
|
await settleWithin(session.send("Browser.close"), 1_500);
|
|
399
|
-
await settleWithin(
|
|
588
|
+
await settleWithin(
|
|
589
|
+
Promise.resolve().then(() => session.detach()),
|
|
590
|
+
500,
|
|
591
|
+
);
|
|
400
592
|
}
|
|
401
593
|
exited = await waitForExit(state.pid, 3_000);
|
|
402
|
-
await settleWithin(
|
|
594
|
+
await settleWithin(
|
|
595
|
+
Promise.resolve().then(() => browser.close()),
|
|
596
|
+
transportCloseTimeoutMs,
|
|
597
|
+
);
|
|
403
598
|
|
|
404
599
|
if (!exited) {
|
|
405
600
|
const childStillOwnsPid = child?.pid === state.pid
|
|
@@ -429,7 +624,10 @@ export async function launchAndConnectCdpChrome({
|
|
|
429
624
|
},
|
|
430
625
|
};
|
|
431
626
|
} catch (error) {
|
|
432
|
-
await
|
|
627
|
+
await settleWithin(
|
|
628
|
+
Promise.resolve().then(() => browser?.close()),
|
|
629
|
+
transportCloseTimeoutMs,
|
|
630
|
+
);
|
|
433
631
|
if (
|
|
434
632
|
child
|
|
435
633
|
&& child.exitCode == null
|
package/src/browser/cdp-state.js
CHANGED
|
@@ -545,7 +545,7 @@ export async function inspectCdpProfileState(profileDir, {
|
|
|
545
545
|
// live lock-holder pid from a recycled one: a process that started after the
|
|
546
546
|
// lock file was written cannot own it. Returns null when it cannot be
|
|
547
547
|
// determined (unknown process, platform tool unavailable).
|
|
548
|
-
async function processStartTimeMs(pid, { execFileImpl = execFileAsync } = {}) {
|
|
548
|
+
export async function processStartTimeMs(pid, { execFileImpl = execFileAsync } = {}) {
|
|
549
549
|
if (process.platform === "win32") {
|
|
550
550
|
const { stdout } = await execFileImpl(
|
|
551
551
|
"powershell.exe",
|
|
@@ -555,7 +555,7 @@ async function processStartTimeMs(pid, { execFileImpl = execFileAsync } = {}) {
|
|
|
555
555
|
"-Command",
|
|
556
556
|
`(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CreationDate`,
|
|
557
557
|
],
|
|
558
|
-
{ windowsHide: true },
|
|
558
|
+
{ windowsHide: true, timeout: 1_500 },
|
|
559
559
|
);
|
|
560
560
|
const parsed = Date.parse(String(stdout).trim());
|
|
561
561
|
return Number.isFinite(parsed) ? parsed : null;
|
|
@@ -563,10 +563,13 @@ async function processStartTimeMs(pid, { execFileImpl = execFileAsync } = {}) {
|
|
|
563
563
|
|
|
564
564
|
const { stdout } = await execFileImpl(
|
|
565
565
|
"ps",
|
|
566
|
-
["-p", String(pid), "-o", "
|
|
566
|
+
["-p", String(pid), "-o", "lstart="],
|
|
567
|
+
{
|
|
568
|
+
env: { ...process.env, LANG: "C", LC_ALL: "C" },
|
|
569
|
+
},
|
|
567
570
|
);
|
|
568
|
-
const
|
|
569
|
-
return Number.isFinite(
|
|
571
|
+
const parsed = Date.parse(String(stdout).trim());
|
|
572
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
570
573
|
}
|
|
571
574
|
|
|
572
575
|
export async function acquireCdpProfileLock(profileDir, {
|