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
package/src/cli/main.js
CHANGED
|
@@ -3,16 +3,25 @@ import fs from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
import { confirm, select } from "@inquirer/prompts";
|
|
6
|
-
import { ChatGPTWebAdapter } from "../browser/chatgpt-web-adapter.js";
|
|
7
6
|
import { launchNativeLoginBrowser } from "../browser/native-login.js";
|
|
7
|
+
import {
|
|
8
|
+
createWebAdapter,
|
|
9
|
+
DEFAULT_PROVIDER,
|
|
10
|
+
getProvider,
|
|
11
|
+
getProviderProfileDir,
|
|
12
|
+
isProviderProfileBasename,
|
|
13
|
+
listActiveProviderIds,
|
|
14
|
+
resolveCliProviderSelection,
|
|
15
|
+
resolveProvider,
|
|
16
|
+
} from "../browser/provider-registry.js";
|
|
8
17
|
import {
|
|
9
18
|
ensureDirectory,
|
|
10
19
|
getAppDataDir,
|
|
11
|
-
getChromeProfileDir,
|
|
12
20
|
getSessionsDir,
|
|
13
21
|
getTasksDir,
|
|
14
22
|
} from "../platform/paths.js";
|
|
15
23
|
import { discoverChromeExecutable } from "../platform/chrome-discovery.js";
|
|
24
|
+
import { isProcessAlive } from "../browser/cdp-state.js";
|
|
16
25
|
import {
|
|
17
26
|
assertNativeRuntimeSupported,
|
|
18
27
|
collectDoctorReport,
|
|
@@ -25,9 +34,11 @@ import { createDefaultToolRegistry } from "../tools/default-tools.js";
|
|
|
25
34
|
import { ProcessManager } from "../tools/process-manager.js";
|
|
26
35
|
import { resolveLimits } from "../shared/limits.js";
|
|
27
36
|
import { EXPORTERS } from "../session/session-export.js";
|
|
37
|
+
import { getPackageVersion } from "../shared/package-info.js";
|
|
28
38
|
import { extractAtMentions } from "./at-files.js";
|
|
29
39
|
import {
|
|
30
40
|
classifyChatInput,
|
|
41
|
+
promptForConfirm,
|
|
31
42
|
promptForText,
|
|
32
43
|
promptForSelect,
|
|
33
44
|
readChatMessage,
|
|
@@ -39,13 +50,24 @@ import {
|
|
|
39
50
|
modeFromPromptChoice,
|
|
40
51
|
normalizeConfiguredMode,
|
|
41
52
|
} from "./mode-choice.js";
|
|
53
|
+
import { runSelfUpdate } from "./self-update.js";
|
|
54
|
+
import { runStartupChecks } from "./startup-notices.js";
|
|
55
|
+
|
|
56
|
+
// Resolves the app data dir, the provider's dedicated Chrome profile dir, and
|
|
57
|
+
// the sessions dirs. `providerId` selects which profile directory is used
|
|
58
|
+
// (each provider logs in independently); an explicit `--profile-dir` still
|
|
59
|
+
// overrides it. Defaults to the ChatGPT profile so callers that predate
|
|
60
|
+
// multi-provider support are unaffected.
|
|
61
|
+
function withResolvedProviderSelection(options) {
|
|
62
|
+
return { ...options, ...resolveCliProviderSelection(options) };
|
|
63
|
+
}
|
|
42
64
|
|
|
43
|
-
function resolveRuntimePaths(options) {
|
|
65
|
+
function resolveRuntimePaths(options, providerId = DEFAULT_PROVIDER) {
|
|
44
66
|
const appDataDir = path.resolve(options.home ?? getAppDataDir());
|
|
45
67
|
return {
|
|
46
68
|
appDataDir,
|
|
47
69
|
profileDir: path.resolve(
|
|
48
|
-
options.profileDir ??
|
|
70
|
+
options.profileDir ?? getProviderProfileDir(appDataDir, providerId),
|
|
49
71
|
),
|
|
50
72
|
sessionsDir: getSessionsDir(appDataDir),
|
|
51
73
|
legacyTasksDir: getTasksDir(appDataDir),
|
|
@@ -61,21 +83,25 @@ async function assertDirectory(directory) {
|
|
|
61
83
|
|
|
62
84
|
async function runLogin(options) {
|
|
63
85
|
assertNativeRuntimeSupported();
|
|
64
|
-
|
|
86
|
+
options = withResolvedProviderSelection(options);
|
|
87
|
+
const provider = resolveProvider(options.model ?? DEFAULT_PROVIDER);
|
|
88
|
+
const { profileDir } = resolveRuntimePaths(options, provider.id);
|
|
89
|
+
const { label, baseUrl } = provider;
|
|
65
90
|
for (;;) {
|
|
66
91
|
console.log(`Opening native Chrome profile: ${profileDir}`);
|
|
67
92
|
console.log(
|
|
68
|
-
|
|
93
|
+
`This window has no CDP flags. Finish until ${label} shows your signed-in home/chat history and no Log in button.`,
|
|
69
94
|
);
|
|
70
95
|
const browser = await launchNativeLoginBrowser({
|
|
71
96
|
profileDir,
|
|
72
97
|
chromePath: options.chromePath,
|
|
98
|
+
url: baseUrl,
|
|
73
99
|
});
|
|
74
100
|
|
|
75
101
|
try {
|
|
76
102
|
const answer = await promptForText({
|
|
77
103
|
message:
|
|
78
|
-
|
|
104
|
+
`After the signed-in ${label} home is visible, press Enter here to save and verify`,
|
|
79
105
|
});
|
|
80
106
|
if (answer == null) {
|
|
81
107
|
return;
|
|
@@ -86,7 +112,8 @@ async function runLogin(options) {
|
|
|
86
112
|
await browser.close();
|
|
87
113
|
}
|
|
88
114
|
|
|
89
|
-
const verifier =
|
|
115
|
+
const verifier = createWebAdapter({
|
|
116
|
+
provider,
|
|
90
117
|
profileDir,
|
|
91
118
|
chromePath: options.chromePath,
|
|
92
119
|
});
|
|
@@ -102,7 +129,7 @@ async function runLogin(options) {
|
|
|
102
129
|
}
|
|
103
130
|
}
|
|
104
131
|
if (authenticated) {
|
|
105
|
-
console.log(
|
|
132
|
+
console.log(`${label} login verified through a fresh CDP connection.`);
|
|
106
133
|
return;
|
|
107
134
|
}
|
|
108
135
|
} finally {
|
|
@@ -110,17 +137,22 @@ async function runLogin(options) {
|
|
|
110
137
|
}
|
|
111
138
|
|
|
112
139
|
console.log(
|
|
113
|
-
|
|
140
|
+
`${label} is still in guest mode. Reopening native Chrome; complete the final ${label} sign-in/continue step.`,
|
|
114
141
|
);
|
|
115
142
|
}
|
|
116
143
|
}
|
|
117
144
|
|
|
118
145
|
// Resets local login by deleting the dedicated Chrome profile. Login state for
|
|
119
|
-
// this app lives entirely in that profile (
|
|
120
|
-
//
|
|
146
|
+
// this app lives entirely in that profile (provider cookies + localStorage), so
|
|
147
|
+
// removing it returns wtagent to a clean guest state — useful for testing the
|
|
121
148
|
// full login → run flow. It never touches the real account server-side.
|
|
122
149
|
async function runLogout(options) {
|
|
123
|
-
|
|
150
|
+
// `logout` targets one provider's profile; unknown ids are rejected but a
|
|
151
|
+
// "planned" provider is still allowed (its profile may exist from a prior
|
|
152
|
+
// login attempt), so use getProvider rather than resolveProvider.
|
|
153
|
+
options = withResolvedProviderSelection(options);
|
|
154
|
+
const provider = getProvider(options.model ?? DEFAULT_PROVIDER);
|
|
155
|
+
const { profileDir } = resolveRuntimePaths(options, provider.id);
|
|
124
156
|
const exists = await fs.stat(profileDir)
|
|
125
157
|
.then((stat) => stat.isDirectory())
|
|
126
158
|
.catch(() => false);
|
|
@@ -129,13 +161,13 @@ async function runLogout(options) {
|
|
|
129
161
|
return;
|
|
130
162
|
}
|
|
131
163
|
|
|
132
|
-
// Guard: only ever delete something that is actually
|
|
164
|
+
// Guard: only ever delete something that is actually a dedicated profile.
|
|
133
165
|
// A profile Chrome has used contains a "Default" profile directory; otherwise
|
|
134
|
-
// require
|
|
166
|
+
// require a known provider profile basename before removing.
|
|
135
167
|
const looksLikeProfile = await fs.stat(path.join(profileDir, "Default"))
|
|
136
168
|
.then((stat) => stat.isDirectory())
|
|
137
169
|
.catch(() => false);
|
|
138
|
-
if (!looksLikeProfile && path.basename(profileDir)
|
|
170
|
+
if (!looksLikeProfile && !isProviderProfileBasename(path.basename(profileDir))) {
|
|
139
171
|
throw new Error(
|
|
140
172
|
`Refusing to delete ${profileDir}: it does not look like a wtagent Chrome profile.`,
|
|
141
173
|
);
|
|
@@ -144,7 +176,7 @@ async function runLogout(options) {
|
|
|
144
176
|
if (!options.yes) {
|
|
145
177
|
const confirmed = await confirm({
|
|
146
178
|
message:
|
|
147
|
-
`This deletes the local
|
|
179
|
+
`This deletes the local ${provider.label} session (Chrome profile at ${profileDir}) `
|
|
148
180
|
+ "and requires a new login. Continue?",
|
|
149
181
|
default: false,
|
|
150
182
|
});
|
|
@@ -156,7 +188,11 @@ async function runLogout(options) {
|
|
|
156
188
|
|
|
157
189
|
await fs.rm(profileDir, { recursive: true, force: true });
|
|
158
190
|
console.log(`Logged out. Removed ${profileDir}.`);
|
|
159
|
-
console.log(
|
|
191
|
+
console.log(
|
|
192
|
+
provider.id === DEFAULT_PROVIDER
|
|
193
|
+
? "Run `wtagent login` to sign in again."
|
|
194
|
+
: `Run \`wtagent login --model ${provider.id}\` to sign in again.`,
|
|
195
|
+
);
|
|
160
196
|
}
|
|
161
197
|
|
|
162
198
|
async function runDoctor(options) {
|
|
@@ -172,7 +208,14 @@ async function runDoctor(options) {
|
|
|
172
208
|
}
|
|
173
209
|
console.log(`Data: ${paths.appDataDir}`);
|
|
174
210
|
console.log(`Profile: ${paths.profileDir}`);
|
|
175
|
-
|
|
211
|
+
const hasWarnings = report.items.some((item) => item.status === "degraded");
|
|
212
|
+
console.log(
|
|
213
|
+
report.exitCode !== 0
|
|
214
|
+
? "Doctor: FAILED"
|
|
215
|
+
: hasWarnings
|
|
216
|
+
? "Doctor: OK (with warnings)"
|
|
217
|
+
: "Doctor: OK",
|
|
218
|
+
);
|
|
176
219
|
process.exitCode = report.exitCode;
|
|
177
220
|
}
|
|
178
221
|
|
|
@@ -184,24 +227,29 @@ class ConversationRunner {
|
|
|
184
227
|
constructor({ session, options, interactive = false }) {
|
|
185
228
|
this.session = session;
|
|
186
229
|
this.options = options;
|
|
187
|
-
|
|
230
|
+
// A conversation belongs to exactly one provider (recorded at creation).
|
|
231
|
+
// The profile dir and adapter follow from it, so resumes reuse the right
|
|
232
|
+
// login even when the CLI is invoked without --model.
|
|
233
|
+
const provider = resolveProvider(session.state.provider ?? DEFAULT_PROVIDER);
|
|
234
|
+
this.paths = resolveRuntimePaths(options, provider.id);
|
|
188
235
|
this.limits = resolveLimits({
|
|
189
236
|
modelTurnTimeoutMs: options.modelTurnTimeoutMs,
|
|
190
237
|
});
|
|
191
238
|
this.processManager = new ProcessManager();
|
|
192
|
-
this.renderer = createRenderer();
|
|
193
|
-
// "Always allow" decisions
|
|
194
|
-
// turns
|
|
239
|
+
this.renderer = createRenderer({ providerLabel: provider.label });
|
|
240
|
+
// "Always allow" decisions are scoped to this saved session. They persist
|
|
241
|
+
// across turns and `resume`, but never carry over into a different session.
|
|
195
242
|
this.approvalStore = new ApprovalStore({
|
|
196
|
-
filePath: path.join(this.
|
|
243
|
+
filePath: path.join(this.session.directory, "approvals.json"),
|
|
197
244
|
});
|
|
198
|
-
this.adapter =
|
|
245
|
+
this.adapter = createWebAdapter({
|
|
246
|
+
provider,
|
|
199
247
|
profileDir: this.paths.profileDir,
|
|
200
248
|
chromePath: options.chromePath,
|
|
201
249
|
debug: options.debug,
|
|
202
250
|
// Minimize by default; `--no-minimize` sets options.minimize === false.
|
|
203
251
|
minimized: options.minimize !== false,
|
|
204
|
-
// ESC cancels the in-flight turn while
|
|
252
|
+
// ESC cancels the in-flight turn while the model is processing. Only in
|
|
205
253
|
// interactive TTY sessions where stdin is available to listen on.
|
|
206
254
|
cancelOnEsc: interactive,
|
|
207
255
|
});
|
|
@@ -242,14 +290,15 @@ class ConversationRunner {
|
|
|
242
290
|
this.approvalStore.setAlwaysAllowedTool(toolCall.name);
|
|
243
291
|
await this.approvalStore.save();
|
|
244
292
|
console.log(
|
|
245
|
-
`Saved: ${toolCall.name} will always be allowed `
|
|
293
|
+
`Saved for this session: ${toolCall.name} will always be allowed `
|
|
246
294
|
+ `(${this.approvalStore.filePath}).`,
|
|
247
295
|
);
|
|
248
296
|
} else if (choice === "always-all") {
|
|
249
297
|
this.approvalStore.setAlwaysAllowAll();
|
|
250
298
|
await this.approvalStore.save();
|
|
251
299
|
console.log(
|
|
252
|
-
`Saved: every tool will always be allowed
|
|
300
|
+
`Saved for this session: every tool will always be allowed `
|
|
301
|
+
+ `(${this.approvalStore.filePath}).`,
|
|
253
302
|
);
|
|
254
303
|
}
|
|
255
304
|
return true;
|
|
@@ -340,6 +389,15 @@ class ConversationRunner {
|
|
|
340
389
|
this.renderer.finish();
|
|
341
390
|
await this.processManager.stopAll().catch(() => {});
|
|
342
391
|
if (keepBrowser) {
|
|
392
|
+
// Only offer to keep Chrome when this run actually launched one. A run
|
|
393
|
+
// that failed before launch (e.g. the provider profile lock was held by
|
|
394
|
+
// another live session) has no window to inspect.
|
|
395
|
+
if (!this.adapter.cdpChrome) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
await this.adapter.detach().catch((error) => {
|
|
399
|
+
console.error(`Warning: ${error.message}`);
|
|
400
|
+
});
|
|
343
401
|
console.log(
|
|
344
402
|
"The run failed; Chrome was left open for debugging. "
|
|
345
403
|
+ "Inspect the page and close it manually, or just run "
|
|
@@ -353,6 +411,56 @@ class ConversationRunner {
|
|
|
353
411
|
}
|
|
354
412
|
}
|
|
355
413
|
|
|
414
|
+
async function waitForProcessToExit(pid, timeoutMs = 60_000) {
|
|
415
|
+
const deadline = Date.now() + timeoutMs;
|
|
416
|
+
while (Date.now() < deadline) {
|
|
417
|
+
if (!isProcessAlive(pid)) {
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
421
|
+
}
|
|
422
|
+
return !isProcessAlive(pid);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// A live session holds the provider's Chrome profile lock. In an interactive
|
|
426
|
+
// session, ask the user how to proceed instead of failing outright: kill the
|
|
427
|
+
// other session, or have the user close its Chrome window so that session
|
|
428
|
+
// fails and releases the profile. Returns true when the turn should be retried.
|
|
429
|
+
async function promptToResolveProfileLock(error) {
|
|
430
|
+
const pid = Number(error.details?.pid);
|
|
431
|
+
console.log(`\n${"\x1b[33m"}${error.message}${"\x1b[0m"}`);
|
|
432
|
+
const choice = await promptForSelect({
|
|
433
|
+
message: "How should this be handled?",
|
|
434
|
+
choices: [
|
|
435
|
+
{ name: "Kill that session and continue", value: "kill" },
|
|
436
|
+
{ name: "I closed its Chrome window; retry", value: "retry" },
|
|
437
|
+
{ name: "Quit", value: "quit" },
|
|
438
|
+
],
|
|
439
|
+
});
|
|
440
|
+
if (choice == null || choice === "quit") {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
if (choice === "kill" && Number.isSafeInteger(pid) && pid > 0) {
|
|
444
|
+
console.log(`Stopping pid=${pid}...`);
|
|
445
|
+
try {
|
|
446
|
+
process.kill(pid, "SIGTERM");
|
|
447
|
+
} catch {
|
|
448
|
+
// Already gone.
|
|
449
|
+
}
|
|
450
|
+
if (!await waitForProcessToExit(pid, 5_000)) {
|
|
451
|
+
try {
|
|
452
|
+
process.kill(pid, "SIGKILL");
|
|
453
|
+
} catch {
|
|
454
|
+
// Already gone.
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// Either the holder was killed or the user closed its Chrome window: wait
|
|
459
|
+
// for the other session to release the profile, then retry the turn.
|
|
460
|
+
await waitForProcessToExit(pid, 60_000);
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
|
|
356
464
|
async function executeSession({
|
|
357
465
|
session,
|
|
358
466
|
options,
|
|
@@ -367,14 +475,19 @@ async function executeSession({
|
|
|
367
475
|
const interactive = !options.once && process.stdin.isTTY && process.stdout.isTTY;
|
|
368
476
|
const runner = new ConversationRunner({ session, options, interactive });
|
|
369
477
|
|
|
370
|
-
const onInterrupt =
|
|
478
|
+
const onInterrupt = () => {
|
|
371
479
|
if (runner.interrupted) {
|
|
372
|
-
|
|
480
|
+
console.log("\nForce quitting.");
|
|
481
|
+
// process.exit skips the finally block, so repeat the session/resume
|
|
482
|
+
// hint here — the user should always know how to continue.
|
|
483
|
+
console.log(`Session saved at: ${session.directory}`);
|
|
484
|
+
printResumeHint(session.sessionId);
|
|
485
|
+
process.exit(130);
|
|
373
486
|
}
|
|
374
487
|
runner.interrupted = true;
|
|
488
|
+
runner.adapter.escCancelRequested = true;
|
|
375
489
|
runner.renderer.stopSpinner();
|
|
376
|
-
console.log("\
|
|
377
|
-
await runner.close();
|
|
490
|
+
console.log("\nCtrl+C — cancelling. Press again to force quit.");
|
|
378
491
|
process.exitCode = 130;
|
|
379
492
|
};
|
|
380
493
|
process.on("SIGINT", onInterrupt);
|
|
@@ -394,13 +507,27 @@ async function executeSession({
|
|
|
394
507
|
let turnInPlaceRecovery = false;
|
|
395
508
|
|
|
396
509
|
for (;;) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
510
|
+
let result;
|
|
511
|
+
try {
|
|
512
|
+
result = await runner.runTurn({
|
|
513
|
+
resume: turnResume,
|
|
514
|
+
instruction: turnInstruction,
|
|
515
|
+
files: turnFiles,
|
|
516
|
+
inPlaceRecovery: turnInPlaceRecovery,
|
|
517
|
+
mode,
|
|
518
|
+
});
|
|
519
|
+
} catch (error) {
|
|
520
|
+
// Another live session holds the provider profile lock. Interactively,
|
|
521
|
+
// offer to resolve it and retry the same turn; otherwise fail cleanly
|
|
522
|
+
// (the top-level handler prints the actionable message).
|
|
523
|
+
if (error?.code === "PROFILE_LOCKED" && interactive) {
|
|
524
|
+
if (await promptToResolveProfileLock(error)) {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
throw error;
|
|
530
|
+
}
|
|
404
531
|
if (runner.interrupted) {
|
|
405
532
|
break;
|
|
406
533
|
}
|
|
@@ -418,7 +545,7 @@ async function executeSession({
|
|
|
418
545
|
throw result.error;
|
|
419
546
|
}
|
|
420
547
|
runner.renderer.hint(
|
|
421
|
-
|
|
548
|
+
`Type /retry to ask ${runner.renderer.providerLabel} to continue again, enter a new instruction, or quit`,
|
|
422
549
|
);
|
|
423
550
|
const next = await promptForNextMessage(runner, activeChatInput);
|
|
424
551
|
if (next == null) {
|
|
@@ -536,23 +663,69 @@ async function resolveMessageAttachments(runner, text) {
|
|
|
536
663
|
return files;
|
|
537
664
|
}
|
|
538
665
|
|
|
666
|
+
function isInteractiveSession(options) {
|
|
667
|
+
return !options.once && process.stdin.isTTY && process.stdout.isTTY;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async function promptForUpdate({ currentVersion, latest }) {
|
|
671
|
+
return await promptForConfirm({
|
|
672
|
+
message: `Update WTAgent from ${currentVersion} to ${latest} now?`,
|
|
673
|
+
default: true,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
async function maybeRunStartupChecks(options) {
|
|
678
|
+
return await runStartupChecks({
|
|
679
|
+
appDataDir: resolveRuntimePaths(options).appDataDir,
|
|
680
|
+
interactive: isInteractiveSession(options),
|
|
681
|
+
promptUpdate: promptForUpdate,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
async function runUpdate() {
|
|
686
|
+
const result = await runSelfUpdate();
|
|
687
|
+
if (result.status === "error") {
|
|
688
|
+
process.exitCode = 1;
|
|
689
|
+
}
|
|
690
|
+
return result;
|
|
691
|
+
}
|
|
692
|
+
|
|
539
693
|
async function runAgent(taskParts, options) {
|
|
540
694
|
assertNativeRuntimeSupported();
|
|
695
|
+
options = withResolvedProviderSelection(options);
|
|
541
696
|
const projectRoot = path.resolve(options.project ?? process.cwd());
|
|
542
697
|
await assertDirectory(projectRoot);
|
|
543
|
-
|
|
698
|
+
|
|
699
|
+
// Fail fast on an unknown/unsupported --model before any startup work.
|
|
700
|
+
const provider = resolveProvider(options.model ?? DEFAULT_PROVIDER);
|
|
701
|
+
|
|
702
|
+
const startup = await maybeRunStartupChecks(options);
|
|
703
|
+
if (startup === "updated" || startup === "aborted") {
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const interactive = isInteractiveSession(options);
|
|
544
708
|
const chatInput = interactive ? new ShellChatInput() : null;
|
|
545
709
|
|
|
546
710
|
if (interactive) {
|
|
547
|
-
printChatBanner(projectRoot);
|
|
711
|
+
printChatBanner(projectRoot, provider);
|
|
548
712
|
}
|
|
549
713
|
|
|
714
|
+
// Mode selection is abstracted per provider: some prompt (ChatGPT's
|
|
715
|
+
// Pro/Current), others silently apply their configured default at
|
|
716
|
+
// conversation start (DeepSeek → 专家模式 + 深度思考). The chosen value is
|
|
717
|
+
// handed to the adapter's selectMode() by the runtime.
|
|
550
718
|
let requestedMode;
|
|
551
|
-
if (
|
|
719
|
+
if (!provider.promptsForMode) {
|
|
720
|
+
if (options.mode != null) {
|
|
721
|
+
console.log(`Note: --mode is ignored for ${provider.label}.`);
|
|
722
|
+
}
|
|
723
|
+
requestedMode = provider.defaultMode;
|
|
724
|
+
} else if (options.mode != null) {
|
|
552
725
|
requestedMode = normalizeConfiguredMode(options.mode);
|
|
553
726
|
} else if (interactive) {
|
|
554
727
|
const modeChoice = await promptForSelect({
|
|
555
|
-
message:
|
|
728
|
+
message: `${provider.label} mode`,
|
|
556
729
|
choices: CHATGPT_MODE_CHOICES,
|
|
557
730
|
});
|
|
558
731
|
if (modeChoice == null) {
|
|
@@ -587,12 +760,13 @@ async function runAgent(taskParts, options) {
|
|
|
587
760
|
chatInput.remember(task);
|
|
588
761
|
}
|
|
589
762
|
|
|
590
|
-
const paths = resolveRuntimePaths(options);
|
|
763
|
+
const paths = resolveRuntimePaths(options, provider.id);
|
|
591
764
|
await ensureDirectory(paths.sessionsDir);
|
|
592
765
|
const session = await AgentSession.create({
|
|
593
766
|
sessionsDir: paths.sessionsDir,
|
|
594
767
|
task,
|
|
595
768
|
projectRoot,
|
|
769
|
+
provider: provider.id,
|
|
596
770
|
mode: requestedMode,
|
|
597
771
|
});
|
|
598
772
|
|
|
@@ -616,13 +790,13 @@ async function runAgent(taskParts, options) {
|
|
|
616
790
|
return await executeSession({ session, options, files, chatInput });
|
|
617
791
|
}
|
|
618
792
|
|
|
619
|
-
function printChatBanner(projectRoot) {
|
|
793
|
+
function printChatBanner(projectRoot, provider) {
|
|
620
794
|
const CYAN = "\x1b[36m";
|
|
621
795
|
const DIM = "\x1b[2m";
|
|
622
796
|
const RESET = "\x1b[0m";
|
|
623
797
|
console.log("");
|
|
624
|
-
console.log(`${CYAN}WTAgent${RESET} ${DIM}·
|
|
625
|
-
console.log(`${DIM}Enter sends · Shift+Enter newline · ESC cancels processing · multiline paste · ↑/↓ history · "exit", Ctrl+C, or Ctrl+D quits${RESET}`);
|
|
798
|
+
console.log(`${CYAN}WTAgent${RESET} ${DIM}· ${provider.label} · ${projectRoot}${RESET}`);
|
|
799
|
+
console.log(`${DIM}Enter sends · Shift+Enter or Ctrl+J newline · ESC cancels processing · multiline paste · ↑/↓ history · "exit", Ctrl+C, or Ctrl+D quits${RESET}`);
|
|
626
800
|
console.log("");
|
|
627
801
|
}
|
|
628
802
|
|
|
@@ -645,17 +819,39 @@ async function loadSession(paths, sessionId) {
|
|
|
645
819
|
|
|
646
820
|
async function runResume(sessionId, instructionParts, options) {
|
|
647
821
|
assertNativeRuntimeSupported();
|
|
822
|
+
options = withResolvedProviderSelection(options);
|
|
823
|
+
// Loading only needs the sessions dir (provider-independent); the provider is
|
|
824
|
+
// then read from the session so the run reuses the right adapter + profile.
|
|
648
825
|
const paths = resolveRuntimePaths(options);
|
|
649
826
|
await ensureDirectory(paths.sessionsDir);
|
|
650
827
|
const session = await loadSession(paths, sessionId);
|
|
651
828
|
await assertDirectory(session.state.projectRoot);
|
|
652
829
|
|
|
653
|
-
|
|
654
|
-
//
|
|
655
|
-
//
|
|
656
|
-
|
|
830
|
+
const provider = resolveProvider(session.state.provider ?? DEFAULT_PROVIDER);
|
|
831
|
+
// A conversation belongs to one provider. --model on resume is only allowed
|
|
832
|
+
// if it names the same provider; switching mid-conversation is rejected.
|
|
833
|
+
if (options.model != null && getProvider(options.model).id !== provider.id) {
|
|
834
|
+
throw new Error(
|
|
835
|
+
`Session ${sessionId} uses ${provider.label}; `
|
|
836
|
+
+ `--model ${options.model} cannot change a conversation's provider.`,
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
const startup = await maybeRunStartupChecks(options);
|
|
841
|
+
if (startup === "updated" || startup === "aborted") {
|
|
842
|
+
return null;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Like a fresh run, interactive resumes offer a mode choice (prompting
|
|
846
|
+
// providers only), defaulting to the mode the conversation is already on.
|
|
847
|
+
// `--mode` overrides it and also works non-interactively. This matters after
|
|
848
|
+
// a usage limit, where switching thinking levels can get past the block.
|
|
849
|
+
// Non-prompting providers (e.g. DeepSeek) keep the mode the existing
|
|
850
|
+
// conversation was created with — it does not need re-asserting on resume.
|
|
657
851
|
let requestedMode;
|
|
658
|
-
if (
|
|
852
|
+
if (!provider.promptsForMode) {
|
|
853
|
+
requestedMode = null;
|
|
854
|
+
} else if (options.mode != null) {
|
|
659
855
|
requestedMode = normalizeConfiguredMode(options.mode);
|
|
660
856
|
} else if (
|
|
661
857
|
!options.once
|
|
@@ -663,7 +859,7 @@ async function runResume(sessionId, instructionParts, options) {
|
|
|
663
859
|
&& process.stdout.isTTY
|
|
664
860
|
) {
|
|
665
861
|
const modeChoice = await promptForSelect({
|
|
666
|
-
message:
|
|
862
|
+
message: `${provider.label} mode`,
|
|
667
863
|
choices: CHATGPT_MODE_CHOICES,
|
|
668
864
|
default: session.state.activeMode === "Pro" ? "pro" : "current",
|
|
669
865
|
});
|
|
@@ -774,14 +970,18 @@ async function runExport(sessionId, options) {
|
|
|
774
970
|
const program = new Command()
|
|
775
971
|
.name("wtagent")
|
|
776
972
|
.description("Turn your web AI session into a local tool-using agent.")
|
|
777
|
-
.version(
|
|
973
|
+
.version(getPackageVersion())
|
|
778
974
|
.option("--home <path>", "Application data directory")
|
|
779
975
|
.option("--profile-dir <path>", "Dedicated Chrome profile directory")
|
|
780
976
|
.option("--chrome-path <path>", "Chrome/Chromium executable")
|
|
781
977
|
.option("-C, --project <path>", "Project directory", process.cwd())
|
|
978
|
+
.option(
|
|
979
|
+
"--model <provider>",
|
|
980
|
+
`Web AI provider: ${listActiveProviderIds().join(", ")} (default: ${DEFAULT_PROVIDER})`,
|
|
981
|
+
)
|
|
782
982
|
.option(
|
|
783
983
|
"--mode <name>",
|
|
784
|
-
|
|
984
|
+
"ChatGPT mode (Pro/Current), or a provider alias such as kimi",
|
|
785
985
|
)
|
|
786
986
|
.option(
|
|
787
987
|
"--once",
|
|
@@ -790,7 +990,7 @@ const program = new Command()
|
|
|
790
990
|
)
|
|
791
991
|
.option(
|
|
792
992
|
"--model-turn-timeout-ms <milliseconds>",
|
|
793
|
-
"Maximum wait for one
|
|
993
|
+
"Maximum wait for one model response (default: 10 minutes; 16 minutes for ChatGPT Pro)",
|
|
794
994
|
)
|
|
795
995
|
.option(
|
|
796
996
|
"--no-minimize",
|
|
@@ -805,6 +1005,11 @@ const program = new Command()
|
|
|
805
1005
|
await runAgent(task, command.optsWithGlobals());
|
|
806
1006
|
});
|
|
807
1007
|
|
|
1008
|
+
program
|
|
1009
|
+
.command("update")
|
|
1010
|
+
.description("Install the latest WTAgent from npm.")
|
|
1011
|
+
.action(async () => runUpdate());
|
|
1012
|
+
|
|
808
1013
|
program
|
|
809
1014
|
.command("doctor")
|
|
810
1015
|
.description("Check Node, Chrome, and local data directories.")
|
|
@@ -812,12 +1017,12 @@ program
|
|
|
812
1017
|
|
|
813
1018
|
program
|
|
814
1019
|
.command("login")
|
|
815
|
-
.description("Open the dedicated Chrome profile and wait for
|
|
1020
|
+
.description("Open the dedicated Chrome profile and wait for provider login (use --model to pick a provider).")
|
|
816
1021
|
.action(async (_, command) => runLogin(command.optsWithGlobals()));
|
|
817
1022
|
|
|
818
1023
|
program
|
|
819
1024
|
.command("logout")
|
|
820
|
-
.description("Delete the local Chrome profile to reset
|
|
1025
|
+
.description("Delete the local Chrome profile to reset a provider session (use --model to pick a provider).")
|
|
821
1026
|
.option("--yes", "Skip the confirmation prompt", false)
|
|
822
1027
|
.action(async (options, command) => {
|
|
823
1028
|
await runLogout({ ...command.optsWithGlobals(), ...options });
|
|
@@ -830,7 +1035,7 @@ program
|
|
|
830
1035
|
.argument("[instruction...]", "Optional follow-up instruction")
|
|
831
1036
|
.option(
|
|
832
1037
|
"--mode <name>",
|
|
833
|
-
|
|
1038
|
+
"ChatGPT mode (Pro/Current), or a provider alias such as kimi",
|
|
834
1039
|
)
|
|
835
1040
|
.action(async (sessionId, instruction, _, command) => {
|
|
836
1041
|
await runResume(
|
|
@@ -859,6 +1064,25 @@ program
|
|
|
859
1064
|
});
|
|
860
1065
|
|
|
861
1066
|
program.parseAsync().catch((error) => {
|
|
1067
|
+
// Expected, actionable failures carry a plain message instead of a stack
|
|
1068
|
+
// trace (e.g. the provider's Chrome profile is locked by another session).
|
|
1069
|
+
if (error?.code === "PROFILE_LOCKED") {
|
|
1070
|
+
console.error(`Error: ${error.message}`);
|
|
1071
|
+
process.exitCode = 1;
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
862
1074
|
console.error(error.stack ?? error.message);
|
|
863
1075
|
process.exitCode = 1;
|
|
1076
|
+
}).finally(() => {
|
|
1077
|
+
// By the time every command finishes, all per-command cleanup (browser
|
|
1078
|
+
// detach/close, stdin raw-mode restore, process stop) has already run. A
|
|
1079
|
+
// stray tty handle (e.g. a keypress-machinery listener left behind on a
|
|
1080
|
+
// failed turn) can still pin the event loop, leaving a dead prompt that
|
|
1081
|
+
// never returns to the shell — exit hard instead of relying on the loop
|
|
1082
|
+
// to drain naturally.
|
|
1083
|
+
if (process.exitCode != null && process.exitCode !== 0) {
|
|
1084
|
+
process.stdin.setRawMode?.(false);
|
|
1085
|
+
process.stdin.pause?.();
|
|
1086
|
+
process.exit(process.exitCode);
|
|
1087
|
+
}
|
|
864
1088
|
});
|