wtagent 0.1.0 → 0.2.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 +50 -36
- 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 +276 -59
- 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/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) {
|
|
@@ -184,24 +220,29 @@ class ConversationRunner {
|
|
|
184
220
|
constructor({ session, options, interactive = false }) {
|
|
185
221
|
this.session = session;
|
|
186
222
|
this.options = options;
|
|
187
|
-
|
|
223
|
+
// A conversation belongs to exactly one provider (recorded at creation).
|
|
224
|
+
// The profile dir and adapter follow from it, so resumes reuse the right
|
|
225
|
+
// login even when the CLI is invoked without --model.
|
|
226
|
+
const provider = resolveProvider(session.state.provider ?? DEFAULT_PROVIDER);
|
|
227
|
+
this.paths = resolveRuntimePaths(options, provider.id);
|
|
188
228
|
this.limits = resolveLimits({
|
|
189
229
|
modelTurnTimeoutMs: options.modelTurnTimeoutMs,
|
|
190
230
|
});
|
|
191
231
|
this.processManager = new ProcessManager();
|
|
192
|
-
this.renderer = createRenderer();
|
|
193
|
-
// "Always allow" decisions
|
|
194
|
-
// turns
|
|
232
|
+
this.renderer = createRenderer({ providerLabel: provider.label });
|
|
233
|
+
// "Always allow" decisions are scoped to this saved session. They persist
|
|
234
|
+
// across turns and `resume`, but never carry over into a different session.
|
|
195
235
|
this.approvalStore = new ApprovalStore({
|
|
196
|
-
filePath: path.join(this.
|
|
236
|
+
filePath: path.join(this.session.directory, "approvals.json"),
|
|
197
237
|
});
|
|
198
|
-
this.adapter =
|
|
238
|
+
this.adapter = createWebAdapter({
|
|
239
|
+
provider,
|
|
199
240
|
profileDir: this.paths.profileDir,
|
|
200
241
|
chromePath: options.chromePath,
|
|
201
242
|
debug: options.debug,
|
|
202
243
|
// Minimize by default; `--no-minimize` sets options.minimize === false.
|
|
203
244
|
minimized: options.minimize !== false,
|
|
204
|
-
// ESC cancels the in-flight turn while
|
|
245
|
+
// ESC cancels the in-flight turn while the model is processing. Only in
|
|
205
246
|
// interactive TTY sessions where stdin is available to listen on.
|
|
206
247
|
cancelOnEsc: interactive,
|
|
207
248
|
});
|
|
@@ -242,14 +283,15 @@ class ConversationRunner {
|
|
|
242
283
|
this.approvalStore.setAlwaysAllowedTool(toolCall.name);
|
|
243
284
|
await this.approvalStore.save();
|
|
244
285
|
console.log(
|
|
245
|
-
`Saved: ${toolCall.name} will always be allowed `
|
|
286
|
+
`Saved for this session: ${toolCall.name} will always be allowed `
|
|
246
287
|
+ `(${this.approvalStore.filePath}).`,
|
|
247
288
|
);
|
|
248
289
|
} else if (choice === "always-all") {
|
|
249
290
|
this.approvalStore.setAlwaysAllowAll();
|
|
250
291
|
await this.approvalStore.save();
|
|
251
292
|
console.log(
|
|
252
|
-
`Saved: every tool will always be allowed
|
|
293
|
+
`Saved for this session: every tool will always be allowed `
|
|
294
|
+
+ `(${this.approvalStore.filePath}).`,
|
|
253
295
|
);
|
|
254
296
|
}
|
|
255
297
|
return true;
|
|
@@ -340,6 +382,15 @@ class ConversationRunner {
|
|
|
340
382
|
this.renderer.finish();
|
|
341
383
|
await this.processManager.stopAll().catch(() => {});
|
|
342
384
|
if (keepBrowser) {
|
|
385
|
+
// Only offer to keep Chrome when this run actually launched one. A run
|
|
386
|
+
// that failed before launch (e.g. the provider profile lock was held by
|
|
387
|
+
// another live session) has no window to inspect.
|
|
388
|
+
if (!this.adapter.cdpChrome) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
await this.adapter.detach().catch((error) => {
|
|
392
|
+
console.error(`Warning: ${error.message}`);
|
|
393
|
+
});
|
|
343
394
|
console.log(
|
|
344
395
|
"The run failed; Chrome was left open for debugging. "
|
|
345
396
|
+ "Inspect the page and close it manually, or just run "
|
|
@@ -353,6 +404,56 @@ class ConversationRunner {
|
|
|
353
404
|
}
|
|
354
405
|
}
|
|
355
406
|
|
|
407
|
+
async function waitForProcessToExit(pid, timeoutMs = 60_000) {
|
|
408
|
+
const deadline = Date.now() + timeoutMs;
|
|
409
|
+
while (Date.now() < deadline) {
|
|
410
|
+
if (!isProcessAlive(pid)) {
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
414
|
+
}
|
|
415
|
+
return !isProcessAlive(pid);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// A live session holds the provider's Chrome profile lock. In an interactive
|
|
419
|
+
// session, ask the user how to proceed instead of failing outright: kill the
|
|
420
|
+
// other session, or have the user close its Chrome window so that session
|
|
421
|
+
// fails and releases the profile. Returns true when the turn should be retried.
|
|
422
|
+
async function promptToResolveProfileLock(error) {
|
|
423
|
+
const pid = Number(error.details?.pid);
|
|
424
|
+
console.log(`\n${"\x1b[33m"}${error.message}${"\x1b[0m"}`);
|
|
425
|
+
const choice = await promptForSelect({
|
|
426
|
+
message: "How should this be handled?",
|
|
427
|
+
choices: [
|
|
428
|
+
{ name: "Kill that session and continue", value: "kill" },
|
|
429
|
+
{ name: "I closed its Chrome window; retry", value: "retry" },
|
|
430
|
+
{ name: "Quit", value: "quit" },
|
|
431
|
+
],
|
|
432
|
+
});
|
|
433
|
+
if (choice == null || choice === "quit") {
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
if (choice === "kill" && Number.isSafeInteger(pid) && pid > 0) {
|
|
437
|
+
console.log(`Stopping pid=${pid}...`);
|
|
438
|
+
try {
|
|
439
|
+
process.kill(pid, "SIGTERM");
|
|
440
|
+
} catch {
|
|
441
|
+
// Already gone.
|
|
442
|
+
}
|
|
443
|
+
if (!await waitForProcessToExit(pid, 5_000)) {
|
|
444
|
+
try {
|
|
445
|
+
process.kill(pid, "SIGKILL");
|
|
446
|
+
} catch {
|
|
447
|
+
// Already gone.
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// Either the holder was killed or the user closed its Chrome window: wait
|
|
452
|
+
// for the other session to release the profile, then retry the turn.
|
|
453
|
+
await waitForProcessToExit(pid, 60_000);
|
|
454
|
+
return true;
|
|
455
|
+
}
|
|
456
|
+
|
|
356
457
|
async function executeSession({
|
|
357
458
|
session,
|
|
358
459
|
options,
|
|
@@ -367,14 +468,19 @@ async function executeSession({
|
|
|
367
468
|
const interactive = !options.once && process.stdin.isTTY && process.stdout.isTTY;
|
|
368
469
|
const runner = new ConversationRunner({ session, options, interactive });
|
|
369
470
|
|
|
370
|
-
const onInterrupt =
|
|
471
|
+
const onInterrupt = () => {
|
|
371
472
|
if (runner.interrupted) {
|
|
372
|
-
|
|
473
|
+
console.log("\nForce quitting.");
|
|
474
|
+
// process.exit skips the finally block, so repeat the session/resume
|
|
475
|
+
// hint here — the user should always know how to continue.
|
|
476
|
+
console.log(`Session saved at: ${session.directory}`);
|
|
477
|
+
printResumeHint(session.sessionId);
|
|
478
|
+
process.exit(130);
|
|
373
479
|
}
|
|
374
480
|
runner.interrupted = true;
|
|
481
|
+
runner.adapter.escCancelRequested = true;
|
|
375
482
|
runner.renderer.stopSpinner();
|
|
376
|
-
console.log("\
|
|
377
|
-
await runner.close();
|
|
483
|
+
console.log("\nCtrl+C — cancelling. Press again to force quit.");
|
|
378
484
|
process.exitCode = 130;
|
|
379
485
|
};
|
|
380
486
|
process.on("SIGINT", onInterrupt);
|
|
@@ -394,13 +500,27 @@ async function executeSession({
|
|
|
394
500
|
let turnInPlaceRecovery = false;
|
|
395
501
|
|
|
396
502
|
for (;;) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
503
|
+
let result;
|
|
504
|
+
try {
|
|
505
|
+
result = await runner.runTurn({
|
|
506
|
+
resume: turnResume,
|
|
507
|
+
instruction: turnInstruction,
|
|
508
|
+
files: turnFiles,
|
|
509
|
+
inPlaceRecovery: turnInPlaceRecovery,
|
|
510
|
+
mode,
|
|
511
|
+
});
|
|
512
|
+
} catch (error) {
|
|
513
|
+
// Another live session holds the provider profile lock. Interactively,
|
|
514
|
+
// offer to resolve it and retry the same turn; otherwise fail cleanly
|
|
515
|
+
// (the top-level handler prints the actionable message).
|
|
516
|
+
if (error?.code === "PROFILE_LOCKED" && interactive) {
|
|
517
|
+
if (await promptToResolveProfileLock(error)) {
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
404
524
|
if (runner.interrupted) {
|
|
405
525
|
break;
|
|
406
526
|
}
|
|
@@ -418,7 +538,7 @@ async function executeSession({
|
|
|
418
538
|
throw result.error;
|
|
419
539
|
}
|
|
420
540
|
runner.renderer.hint(
|
|
421
|
-
|
|
541
|
+
`Type /retry to ask ${runner.renderer.providerLabel} to continue again, enter a new instruction, or quit`,
|
|
422
542
|
);
|
|
423
543
|
const next = await promptForNextMessage(runner, activeChatInput);
|
|
424
544
|
if (next == null) {
|
|
@@ -536,23 +656,69 @@ async function resolveMessageAttachments(runner, text) {
|
|
|
536
656
|
return files;
|
|
537
657
|
}
|
|
538
658
|
|
|
659
|
+
function isInteractiveSession(options) {
|
|
660
|
+
return !options.once && process.stdin.isTTY && process.stdout.isTTY;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
async function promptForUpdate({ currentVersion, latest }) {
|
|
664
|
+
return await promptForConfirm({
|
|
665
|
+
message: `Update WTAgent from ${currentVersion} to ${latest} now?`,
|
|
666
|
+
default: true,
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
async function maybeRunStartupChecks(options) {
|
|
671
|
+
return await runStartupChecks({
|
|
672
|
+
appDataDir: resolveRuntimePaths(options).appDataDir,
|
|
673
|
+
interactive: isInteractiveSession(options),
|
|
674
|
+
promptUpdate: promptForUpdate,
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async function runUpdate() {
|
|
679
|
+
const result = await runSelfUpdate();
|
|
680
|
+
if (result.status === "error") {
|
|
681
|
+
process.exitCode = 1;
|
|
682
|
+
}
|
|
683
|
+
return result;
|
|
684
|
+
}
|
|
685
|
+
|
|
539
686
|
async function runAgent(taskParts, options) {
|
|
540
687
|
assertNativeRuntimeSupported();
|
|
688
|
+
options = withResolvedProviderSelection(options);
|
|
541
689
|
const projectRoot = path.resolve(options.project ?? process.cwd());
|
|
542
690
|
await assertDirectory(projectRoot);
|
|
543
|
-
|
|
691
|
+
|
|
692
|
+
// Fail fast on an unknown/unsupported --model before any startup work.
|
|
693
|
+
const provider = resolveProvider(options.model ?? DEFAULT_PROVIDER);
|
|
694
|
+
|
|
695
|
+
const startup = await maybeRunStartupChecks(options);
|
|
696
|
+
if (startup === "updated" || startup === "aborted") {
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const interactive = isInteractiveSession(options);
|
|
544
701
|
const chatInput = interactive ? new ShellChatInput() : null;
|
|
545
702
|
|
|
546
703
|
if (interactive) {
|
|
547
|
-
printChatBanner(projectRoot);
|
|
704
|
+
printChatBanner(projectRoot, provider);
|
|
548
705
|
}
|
|
549
706
|
|
|
707
|
+
// Mode selection is abstracted per provider: some prompt (ChatGPT's
|
|
708
|
+
// Pro/Current), others silently apply their configured default at
|
|
709
|
+
// conversation start (DeepSeek → 专家模式 + 深度思考). The chosen value is
|
|
710
|
+
// handed to the adapter's selectMode() by the runtime.
|
|
550
711
|
let requestedMode;
|
|
551
|
-
if (
|
|
712
|
+
if (!provider.promptsForMode) {
|
|
713
|
+
if (options.mode != null) {
|
|
714
|
+
console.log(`Note: --mode is ignored for ${provider.label}.`);
|
|
715
|
+
}
|
|
716
|
+
requestedMode = provider.defaultMode;
|
|
717
|
+
} else if (options.mode != null) {
|
|
552
718
|
requestedMode = normalizeConfiguredMode(options.mode);
|
|
553
719
|
} else if (interactive) {
|
|
554
720
|
const modeChoice = await promptForSelect({
|
|
555
|
-
message:
|
|
721
|
+
message: `${provider.label} mode`,
|
|
556
722
|
choices: CHATGPT_MODE_CHOICES,
|
|
557
723
|
});
|
|
558
724
|
if (modeChoice == null) {
|
|
@@ -587,12 +753,13 @@ async function runAgent(taskParts, options) {
|
|
|
587
753
|
chatInput.remember(task);
|
|
588
754
|
}
|
|
589
755
|
|
|
590
|
-
const paths = resolveRuntimePaths(options);
|
|
756
|
+
const paths = resolveRuntimePaths(options, provider.id);
|
|
591
757
|
await ensureDirectory(paths.sessionsDir);
|
|
592
758
|
const session = await AgentSession.create({
|
|
593
759
|
sessionsDir: paths.sessionsDir,
|
|
594
760
|
task,
|
|
595
761
|
projectRoot,
|
|
762
|
+
provider: provider.id,
|
|
596
763
|
mode: requestedMode,
|
|
597
764
|
});
|
|
598
765
|
|
|
@@ -616,13 +783,13 @@ async function runAgent(taskParts, options) {
|
|
|
616
783
|
return await executeSession({ session, options, files, chatInput });
|
|
617
784
|
}
|
|
618
785
|
|
|
619
|
-
function printChatBanner(projectRoot) {
|
|
786
|
+
function printChatBanner(projectRoot, provider) {
|
|
620
787
|
const CYAN = "\x1b[36m";
|
|
621
788
|
const DIM = "\x1b[2m";
|
|
622
789
|
const RESET = "\x1b[0m";
|
|
623
790
|
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}`);
|
|
791
|
+
console.log(`${CYAN}WTAgent${RESET} ${DIM}· ${provider.label} · ${projectRoot}${RESET}`);
|
|
792
|
+
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
793
|
console.log("");
|
|
627
794
|
}
|
|
628
795
|
|
|
@@ -645,17 +812,39 @@ async function loadSession(paths, sessionId) {
|
|
|
645
812
|
|
|
646
813
|
async function runResume(sessionId, instructionParts, options) {
|
|
647
814
|
assertNativeRuntimeSupported();
|
|
815
|
+
options = withResolvedProviderSelection(options);
|
|
816
|
+
// Loading only needs the sessions dir (provider-independent); the provider is
|
|
817
|
+
// then read from the session so the run reuses the right adapter + profile.
|
|
648
818
|
const paths = resolveRuntimePaths(options);
|
|
649
819
|
await ensureDirectory(paths.sessionsDir);
|
|
650
820
|
const session = await loadSession(paths, sessionId);
|
|
651
821
|
await assertDirectory(session.state.projectRoot);
|
|
652
822
|
|
|
653
|
-
|
|
654
|
-
//
|
|
655
|
-
//
|
|
656
|
-
|
|
823
|
+
const provider = resolveProvider(session.state.provider ?? DEFAULT_PROVIDER);
|
|
824
|
+
// A conversation belongs to one provider. --model on resume is only allowed
|
|
825
|
+
// if it names the same provider; switching mid-conversation is rejected.
|
|
826
|
+
if (options.model != null && getProvider(options.model).id !== provider.id) {
|
|
827
|
+
throw new Error(
|
|
828
|
+
`Session ${sessionId} uses ${provider.label}; `
|
|
829
|
+
+ `--model ${options.model} cannot change a conversation's provider.`,
|
|
830
|
+
);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const startup = await maybeRunStartupChecks(options);
|
|
834
|
+
if (startup === "updated" || startup === "aborted") {
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Like a fresh run, interactive resumes offer a mode choice (prompting
|
|
839
|
+
// providers only), defaulting to the mode the conversation is already on.
|
|
840
|
+
// `--mode` overrides it and also works non-interactively. This matters after
|
|
841
|
+
// a usage limit, where switching thinking levels can get past the block.
|
|
842
|
+
// Non-prompting providers (e.g. DeepSeek) keep the mode the existing
|
|
843
|
+
// conversation was created with — it does not need re-asserting on resume.
|
|
657
844
|
let requestedMode;
|
|
658
|
-
if (
|
|
845
|
+
if (!provider.promptsForMode) {
|
|
846
|
+
requestedMode = null;
|
|
847
|
+
} else if (options.mode != null) {
|
|
659
848
|
requestedMode = normalizeConfiguredMode(options.mode);
|
|
660
849
|
} else if (
|
|
661
850
|
!options.once
|
|
@@ -663,7 +852,7 @@ async function runResume(sessionId, instructionParts, options) {
|
|
|
663
852
|
&& process.stdout.isTTY
|
|
664
853
|
) {
|
|
665
854
|
const modeChoice = await promptForSelect({
|
|
666
|
-
message:
|
|
855
|
+
message: `${provider.label} mode`,
|
|
667
856
|
choices: CHATGPT_MODE_CHOICES,
|
|
668
857
|
default: session.state.activeMode === "Pro" ? "pro" : "current",
|
|
669
858
|
});
|
|
@@ -774,14 +963,18 @@ async function runExport(sessionId, options) {
|
|
|
774
963
|
const program = new Command()
|
|
775
964
|
.name("wtagent")
|
|
776
965
|
.description("Turn your web AI session into a local tool-using agent.")
|
|
777
|
-
.version(
|
|
966
|
+
.version(getPackageVersion())
|
|
778
967
|
.option("--home <path>", "Application data directory")
|
|
779
968
|
.option("--profile-dir <path>", "Dedicated Chrome profile directory")
|
|
780
969
|
.option("--chrome-path <path>", "Chrome/Chromium executable")
|
|
781
970
|
.option("-C, --project <path>", "Project directory", process.cwd())
|
|
971
|
+
.option(
|
|
972
|
+
"--model <provider>",
|
|
973
|
+
`Web AI provider: ${listActiveProviderIds().join(", ")} (default: ${DEFAULT_PROVIDER})`,
|
|
974
|
+
)
|
|
782
975
|
.option(
|
|
783
976
|
"--mode <name>",
|
|
784
|
-
|
|
977
|
+
"ChatGPT mode (Pro/Current), or a provider alias such as kimi",
|
|
785
978
|
)
|
|
786
979
|
.option(
|
|
787
980
|
"--once",
|
|
@@ -790,7 +983,7 @@ const program = new Command()
|
|
|
790
983
|
)
|
|
791
984
|
.option(
|
|
792
985
|
"--model-turn-timeout-ms <milliseconds>",
|
|
793
|
-
"Maximum wait for one
|
|
986
|
+
"Maximum wait for one model response (default: 10 minutes; 16 minutes for ChatGPT Pro)",
|
|
794
987
|
)
|
|
795
988
|
.option(
|
|
796
989
|
"--no-minimize",
|
|
@@ -805,6 +998,11 @@ const program = new Command()
|
|
|
805
998
|
await runAgent(task, command.optsWithGlobals());
|
|
806
999
|
});
|
|
807
1000
|
|
|
1001
|
+
program
|
|
1002
|
+
.command("update")
|
|
1003
|
+
.description("Install the latest WTAgent from npm.")
|
|
1004
|
+
.action(async () => runUpdate());
|
|
1005
|
+
|
|
808
1006
|
program
|
|
809
1007
|
.command("doctor")
|
|
810
1008
|
.description("Check Node, Chrome, and local data directories.")
|
|
@@ -812,12 +1010,12 @@ program
|
|
|
812
1010
|
|
|
813
1011
|
program
|
|
814
1012
|
.command("login")
|
|
815
|
-
.description("Open the dedicated Chrome profile and wait for
|
|
1013
|
+
.description("Open the dedicated Chrome profile and wait for provider login (use --model to pick a provider).")
|
|
816
1014
|
.action(async (_, command) => runLogin(command.optsWithGlobals()));
|
|
817
1015
|
|
|
818
1016
|
program
|
|
819
1017
|
.command("logout")
|
|
820
|
-
.description("Delete the local Chrome profile to reset
|
|
1018
|
+
.description("Delete the local Chrome profile to reset a provider session (use --model to pick a provider).")
|
|
821
1019
|
.option("--yes", "Skip the confirmation prompt", false)
|
|
822
1020
|
.action(async (options, command) => {
|
|
823
1021
|
await runLogout({ ...command.optsWithGlobals(), ...options });
|
|
@@ -830,7 +1028,7 @@ program
|
|
|
830
1028
|
.argument("[instruction...]", "Optional follow-up instruction")
|
|
831
1029
|
.option(
|
|
832
1030
|
"--mode <name>",
|
|
833
|
-
|
|
1031
|
+
"ChatGPT mode (Pro/Current), or a provider alias such as kimi",
|
|
834
1032
|
)
|
|
835
1033
|
.action(async (sessionId, instruction, _, command) => {
|
|
836
1034
|
await runResume(
|
|
@@ -859,6 +1057,25 @@ program
|
|
|
859
1057
|
});
|
|
860
1058
|
|
|
861
1059
|
program.parseAsync().catch((error) => {
|
|
1060
|
+
// Expected, actionable failures carry a plain message instead of a stack
|
|
1061
|
+
// trace (e.g. the provider's Chrome profile is locked by another session).
|
|
1062
|
+
if (error?.code === "PROFILE_LOCKED") {
|
|
1063
|
+
console.error(`Error: ${error.message}`);
|
|
1064
|
+
process.exitCode = 1;
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
862
1067
|
console.error(error.stack ?? error.message);
|
|
863
1068
|
process.exitCode = 1;
|
|
1069
|
+
}).finally(() => {
|
|
1070
|
+
// By the time every command finishes, all per-command cleanup (browser
|
|
1071
|
+
// detach/close, stdin raw-mode restore, process stop) has already run. A
|
|
1072
|
+
// stray tty handle (e.g. a keypress-machinery listener left behind on a
|
|
1073
|
+
// failed turn) can still pin the event loop, leaving a dead prompt that
|
|
1074
|
+
// never returns to the shell — exit hard instead of relying on the loop
|
|
1075
|
+
// to drain naturally.
|
|
1076
|
+
if (process.exitCode != null && process.exitCode !== 0) {
|
|
1077
|
+
process.stdin.setRawMode?.(false);
|
|
1078
|
+
process.stdin.pause?.();
|
|
1079
|
+
process.exit(process.exitCode);
|
|
1080
|
+
}
|
|
864
1081
|
});
|