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
package/src/cli/main.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
4
5
|
import { Command } from "commander";
|
|
5
6
|
import { confirm, select } from "@inquirer/prompts";
|
|
6
7
|
import { launchNativeLoginBrowser } from "../browser/native-login.js";
|
|
@@ -11,11 +12,11 @@ import {
|
|
|
11
12
|
getProviderProfileDir,
|
|
12
13
|
isProviderProfileBasename,
|
|
13
14
|
listActiveProviderIds,
|
|
14
|
-
resolveCliProviderSelection,
|
|
15
15
|
resolveProvider,
|
|
16
16
|
} from "../browser/provider-registry.js";
|
|
17
17
|
import {
|
|
18
18
|
ensureDirectory,
|
|
19
|
+
formatHomePath,
|
|
19
20
|
getAppDataDir,
|
|
20
21
|
getSessionsDir,
|
|
21
22
|
getTasksDir,
|
|
@@ -33,6 +34,7 @@ import { ApprovalStore } from "../policy/approval-store.js";
|
|
|
33
34
|
import { createDefaultToolRegistry } from "../tools/default-tools.js";
|
|
34
35
|
import { ProcessManager } from "../tools/process-manager.js";
|
|
35
36
|
import { resolveLimits } from "../shared/limits.js";
|
|
37
|
+
import { BrowserAdapterError } from "../shared/errors.js";
|
|
36
38
|
import { EXPORTERS } from "../session/session-export.js";
|
|
37
39
|
import { getPackageVersion } from "../shared/package-info.js";
|
|
38
40
|
import { extractAtMentions } from "./at-files.js";
|
|
@@ -45,11 +47,7 @@ import {
|
|
|
45
47
|
ShellChatInput,
|
|
46
48
|
} from "./prompt-input.js";
|
|
47
49
|
import { createRenderer } from "./render-events.js";
|
|
48
|
-
import {
|
|
49
|
-
CHATGPT_MODE_CHOICES,
|
|
50
|
-
modeFromPromptChoice,
|
|
51
|
-
normalizeConfiguredMode,
|
|
52
|
-
} from "./mode-choice.js";
|
|
50
|
+
import { t } from "./i18n.js";
|
|
53
51
|
import { runSelfUpdate } from "./self-update.js";
|
|
54
52
|
import { runStartupChecks } from "./startup-notices.js";
|
|
55
53
|
|
|
@@ -58,10 +56,6 @@ import { runStartupChecks } from "./startup-notices.js";
|
|
|
58
56
|
// (each provider logs in independently); an explicit `--profile-dir` still
|
|
59
57
|
// overrides it. Defaults to the ChatGPT profile so callers that predate
|
|
60
58
|
// multi-provider support are unaffected.
|
|
61
|
-
function withResolvedProviderSelection(options) {
|
|
62
|
-
return { ...options, ...resolveCliProviderSelection(options) };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
59
|
function resolveRuntimePaths(options, providerId = DEFAULT_PROVIDER) {
|
|
66
60
|
const appDataDir = path.resolve(options.home ?? getAppDataDir());
|
|
67
61
|
return {
|
|
@@ -83,15 +77,12 @@ async function assertDirectory(directory) {
|
|
|
83
77
|
|
|
84
78
|
async function runLogin(options) {
|
|
85
79
|
assertNativeRuntimeSupported();
|
|
86
|
-
options = withResolvedProviderSelection(options);
|
|
87
80
|
const provider = resolveProvider(options.model ?? DEFAULT_PROVIDER);
|
|
88
81
|
const { profileDir } = resolveRuntimePaths(options, provider.id);
|
|
89
82
|
const { label, baseUrl } = provider;
|
|
90
83
|
for (;;) {
|
|
91
|
-
console.log(
|
|
92
|
-
console.log(
|
|
93
|
-
`This window has no CDP flags. Finish until ${label} shows your signed-in home/chat history and no Log in button.`,
|
|
94
|
-
);
|
|
84
|
+
console.log(t("login.openingProfile", { profileDir }));
|
|
85
|
+
console.log(t("login.finishSignIn", { provider: label }));
|
|
95
86
|
const browser = await launchNativeLoginBrowser({
|
|
96
87
|
profileDir,
|
|
97
88
|
chromePath: options.chromePath,
|
|
@@ -100,13 +91,12 @@ async function runLogin(options) {
|
|
|
100
91
|
|
|
101
92
|
try {
|
|
102
93
|
const answer = await promptForText({
|
|
103
|
-
message:
|
|
104
|
-
`After the signed-in ${label} home is visible, press Enter here to save and verify`,
|
|
94
|
+
message: t("login.pressEnterVerify", { provider: label }),
|
|
105
95
|
});
|
|
106
96
|
if (answer == null) {
|
|
107
97
|
return;
|
|
108
98
|
}
|
|
109
|
-
console.log("
|
|
99
|
+
console.log(t("login.savingProfile"));
|
|
110
100
|
await new Promise((resolve) => setTimeout(resolve, 1_000));
|
|
111
101
|
} finally {
|
|
112
102
|
await browser.close();
|
|
@@ -129,16 +119,14 @@ async function runLogin(options) {
|
|
|
129
119
|
}
|
|
130
120
|
}
|
|
131
121
|
if (authenticated) {
|
|
132
|
-
console.log(
|
|
122
|
+
console.log(t("login.verified", { provider: label }));
|
|
133
123
|
return;
|
|
134
124
|
}
|
|
135
125
|
} finally {
|
|
136
126
|
await verifier.close();
|
|
137
127
|
}
|
|
138
128
|
|
|
139
|
-
console.log(
|
|
140
|
-
`${label} is still in guest mode. Reopening native Chrome; complete the final ${label} sign-in/continue step.`,
|
|
141
|
-
);
|
|
129
|
+
console.log(t("login.stillGuest", { provider: label }));
|
|
142
130
|
}
|
|
143
131
|
}
|
|
144
132
|
|
|
@@ -150,14 +138,13 @@ async function runLogout(options) {
|
|
|
150
138
|
// `logout` targets one provider's profile; unknown ids are rejected but a
|
|
151
139
|
// "planned" provider is still allowed (its profile may exist from a prior
|
|
152
140
|
// login attempt), so use getProvider rather than resolveProvider.
|
|
153
|
-
options = withResolvedProviderSelection(options);
|
|
154
141
|
const provider = getProvider(options.model ?? DEFAULT_PROVIDER);
|
|
155
142
|
const { profileDir } = resolveRuntimePaths(options, provider.id);
|
|
156
143
|
const exists = await fs.stat(profileDir)
|
|
157
144
|
.then((stat) => stat.isDirectory())
|
|
158
145
|
.catch(() => false);
|
|
159
146
|
if (!exists) {
|
|
160
|
-
console.log(
|
|
147
|
+
console.log(t("logout.none", { profileDir }));
|
|
161
148
|
return;
|
|
162
149
|
}
|
|
163
150
|
|
|
@@ -168,30 +155,29 @@ async function runLogout(options) {
|
|
|
168
155
|
.then((stat) => stat.isDirectory())
|
|
169
156
|
.catch(() => false);
|
|
170
157
|
if (!looksLikeProfile && !isProviderProfileBasename(path.basename(profileDir))) {
|
|
171
|
-
throw new Error(
|
|
172
|
-
`Refusing to delete ${profileDir}: it does not look like a wtagent Chrome profile.`,
|
|
173
|
-
);
|
|
158
|
+
throw new Error(t("logout.refuse", { profileDir }));
|
|
174
159
|
}
|
|
175
160
|
|
|
176
161
|
if (!options.yes) {
|
|
177
162
|
const confirmed = await confirm({
|
|
178
|
-
message:
|
|
179
|
-
|
|
180
|
-
|
|
163
|
+
message: t("logout.confirm", {
|
|
164
|
+
provider: provider.label,
|
|
165
|
+
profileDir,
|
|
166
|
+
}),
|
|
181
167
|
default: false,
|
|
182
168
|
});
|
|
183
169
|
if (!confirmed) {
|
|
184
|
-
console.log("
|
|
170
|
+
console.log(t("logout.cancelled"));
|
|
185
171
|
return;
|
|
186
172
|
}
|
|
187
173
|
}
|
|
188
174
|
|
|
189
175
|
await fs.rm(profileDir, { recursive: true, force: true });
|
|
190
|
-
console.log(
|
|
176
|
+
console.log(t("logout.done", { profileDir }));
|
|
191
177
|
console.log(
|
|
192
178
|
provider.id === DEFAULT_PROVIDER
|
|
193
|
-
? "
|
|
194
|
-
:
|
|
179
|
+
? t("logout.loginAgainDefault")
|
|
180
|
+
: t("logout.loginAgainProvider", { provider: provider.id }),
|
|
195
181
|
);
|
|
196
182
|
}
|
|
197
183
|
|
|
@@ -231,6 +217,8 @@ class ConversationRunner {
|
|
|
231
217
|
// The profile dir and adapter follow from it, so resumes reuse the right
|
|
232
218
|
// login even when the CLI is invoked without --model.
|
|
233
219
|
const provider = resolveProvider(session.state.provider ?? DEFAULT_PROVIDER);
|
|
220
|
+
this.provider = provider;
|
|
221
|
+
this.interactive = interactive;
|
|
234
222
|
this.paths = resolveRuntimePaths(options, provider.id);
|
|
235
223
|
this.limits = resolveLimits({
|
|
236
224
|
modelTurnTimeoutMs: options.modelTurnTimeoutMs,
|
|
@@ -267,20 +255,45 @@ class ConversationRunner {
|
|
|
267
255
|
policy: new PolicyEngine({ store: this.approvalStore }),
|
|
268
256
|
session: this.session,
|
|
269
257
|
limits: this.limits,
|
|
258
|
+
postAuthSetup: (
|
|
259
|
+
this.interactive
|
|
260
|
+
? async ({ adapter }) => {
|
|
261
|
+
this.renderer.stopSpinner();
|
|
262
|
+
await adapter.restoreWindow?.();
|
|
263
|
+
try {
|
|
264
|
+
const answer = await promptForText({
|
|
265
|
+
message: t("model.chooseInBrowser", {
|
|
266
|
+
provider: this.provider.label,
|
|
267
|
+
}),
|
|
268
|
+
});
|
|
269
|
+
if (answer == null) {
|
|
270
|
+
throw new Error(t("model.setupCancelled"));
|
|
271
|
+
}
|
|
272
|
+
} finally {
|
|
273
|
+
await adapter.minimizeWindow?.();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
: null
|
|
277
|
+
),
|
|
270
278
|
approval: async ({ toolCall, reasons }) => {
|
|
271
279
|
this.renderer.stopSpinner();
|
|
272
|
-
console.log(
|
|
280
|
+
console.log(
|
|
281
|
+
`\n${"\x1b[33m"}${t("approval.required", { tool: toolCall.name })}${"\x1b[0m"}`,
|
|
282
|
+
);
|
|
273
283
|
for (const reason of reasons) {
|
|
274
284
|
console.log(`- ${reason}`);
|
|
275
285
|
}
|
|
276
286
|
console.log(JSON.stringify(toolCall.args, null, 2));
|
|
277
287
|
const choice = await select({
|
|
278
|
-
message: "
|
|
288
|
+
message: t("approval.how"),
|
|
279
289
|
choices: [
|
|
280
|
-
{ name: "
|
|
281
|
-
{
|
|
282
|
-
|
|
283
|
-
|
|
290
|
+
{ name: t("approval.once"), value: "once" },
|
|
291
|
+
{
|
|
292
|
+
name: t("approval.alwaysTool", { tool: toolCall.name }),
|
|
293
|
+
value: "always-tool",
|
|
294
|
+
},
|
|
295
|
+
{ name: t("approval.alwaysAll"), value: "always-all" },
|
|
296
|
+
{ name: t("approval.deny"), value: "deny" },
|
|
284
297
|
],
|
|
285
298
|
});
|
|
286
299
|
if (choice === "deny") {
|
|
@@ -289,17 +302,16 @@ class ConversationRunner {
|
|
|
289
302
|
if (choice === "always-tool") {
|
|
290
303
|
this.approvalStore.setAlwaysAllowedTool(toolCall.name);
|
|
291
304
|
await this.approvalStore.save();
|
|
292
|
-
console.log(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
);
|
|
305
|
+
console.log(t("approval.savedTool", {
|
|
306
|
+
tool: toolCall.name,
|
|
307
|
+
file: formatHomePath(this.approvalStore.filePath),
|
|
308
|
+
}));
|
|
296
309
|
} else if (choice === "always-all") {
|
|
297
310
|
this.approvalStore.setAlwaysAllowAll();
|
|
298
311
|
await this.approvalStore.save();
|
|
299
|
-
console.log(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
);
|
|
312
|
+
console.log(t("approval.savedAll", {
|
|
313
|
+
file: formatHomePath(this.approvalStore.filePath),
|
|
314
|
+
}));
|
|
303
315
|
}
|
|
304
316
|
return true;
|
|
305
317
|
},
|
|
@@ -426,22 +438,28 @@ async function waitForProcessToExit(pid, timeoutMs = 60_000) {
|
|
|
426
438
|
// session, ask the user how to proceed instead of failing outright: kill the
|
|
427
439
|
// other session, or have the user close its Chrome window so that session
|
|
428
440
|
// fails and releases the profile. Returns true when the turn should be retried.
|
|
429
|
-
async function promptToResolveProfileLock(error) {
|
|
441
|
+
export async function promptToResolveProfileLock(error) {
|
|
430
442
|
const pid = Number(error.details?.pid);
|
|
443
|
+
if (pid === process.pid) {
|
|
444
|
+
throw new Error(
|
|
445
|
+
"The current WTAgent process still holds its browser profile lock. Refusing to terminate itself.",
|
|
446
|
+
{ cause: error },
|
|
447
|
+
);
|
|
448
|
+
}
|
|
431
449
|
console.log(`\n${"\x1b[33m"}${error.message}${"\x1b[0m"}`);
|
|
432
450
|
const choice = await promptForSelect({
|
|
433
|
-
message: "
|
|
451
|
+
message: t("profileLock.how"),
|
|
434
452
|
choices: [
|
|
435
|
-
{ name: "
|
|
436
|
-
{ name: "
|
|
437
|
-
{ name: "
|
|
453
|
+
{ name: t("profileLock.kill"), value: "kill" },
|
|
454
|
+
{ name: t("profileLock.retry"), value: "retry" },
|
|
455
|
+
{ name: t("profileLock.quit"), value: "quit" },
|
|
438
456
|
],
|
|
439
457
|
});
|
|
440
458
|
if (choice == null || choice === "quit") {
|
|
441
459
|
return false;
|
|
442
460
|
}
|
|
443
461
|
if (choice === "kill" && Number.isSafeInteger(pid) && pid > 0) {
|
|
444
|
-
console.log(
|
|
462
|
+
console.log(t("profileLock.stopping", { pid }));
|
|
445
463
|
try {
|
|
446
464
|
process.kill(pid, "SIGTERM");
|
|
447
465
|
} catch {
|
|
@@ -461,7 +479,7 @@ async function promptToResolveProfileLock(error) {
|
|
|
461
479
|
return true;
|
|
462
480
|
}
|
|
463
481
|
|
|
464
|
-
async function executeSession({
|
|
482
|
+
export async function executeSession({
|
|
465
483
|
session,
|
|
466
484
|
options,
|
|
467
485
|
resume = false,
|
|
@@ -469,25 +487,28 @@ async function executeSession({
|
|
|
469
487
|
files = [],
|
|
470
488
|
chatInput = null,
|
|
471
489
|
mode = null,
|
|
472
|
-
}
|
|
490
|
+
}, {
|
|
491
|
+
interactive = !options.once && process.stdin.isTTY && process.stdout.isTTY,
|
|
492
|
+
createRunner = (config) => new ConversationRunner(config),
|
|
493
|
+
readNextMessage = promptForNextMessage,
|
|
494
|
+
} = {}) {
|
|
473
495
|
const paths = resolveRuntimePaths(options);
|
|
474
496
|
await ensureDirectory(paths.sessionsDir);
|
|
475
|
-
const
|
|
476
|
-
const runner = new ConversationRunner({ session, options, interactive });
|
|
497
|
+
const runner = createRunner({ session, options, interactive });
|
|
477
498
|
|
|
478
499
|
const onInterrupt = () => {
|
|
479
500
|
if (runner.interrupted) {
|
|
480
|
-
console.log("
|
|
501
|
+
console.log(`\n${t("session.forceQuit")}`);
|
|
481
502
|
// process.exit skips the finally block, so repeat the session/resume
|
|
482
503
|
// hint here — the user should always know how to continue.
|
|
483
|
-
console.log(
|
|
504
|
+
console.log(t("session.savedAt", { directory: session.directory }));
|
|
484
505
|
printResumeHint(session.sessionId);
|
|
485
506
|
process.exit(130);
|
|
486
507
|
}
|
|
487
508
|
runner.interrupted = true;
|
|
488
509
|
runner.adapter.escCancelRequested = true;
|
|
489
510
|
runner.renderer.stopSpinner();
|
|
490
|
-
console.log("
|
|
511
|
+
console.log(`\n${t("session.ctrlC")}`);
|
|
491
512
|
process.exitCode = 130;
|
|
492
513
|
};
|
|
493
514
|
process.on("SIGINT", onInterrupt);
|
|
@@ -535,23 +556,43 @@ async function executeSession({
|
|
|
535
556
|
if (!interactive) {
|
|
536
557
|
throw result.error;
|
|
537
558
|
}
|
|
538
|
-
runner.renderer.hint("
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
continue;
|
|
559
|
+
runner.renderer.hint(t("session.cancelledHint"));
|
|
560
|
+
// Fall through to the same input prompt as a completed turn. Never
|
|
561
|
+
// resume automatically or reuse the cancelled instruction/attachments.
|
|
542
562
|
}
|
|
543
563
|
if (result?.recoveryRequired) {
|
|
544
564
|
if (!interactive) {
|
|
545
565
|
throw result.error;
|
|
546
566
|
}
|
|
547
567
|
runner.renderer.hint(
|
|
548
|
-
|
|
568
|
+
t("session.recoveryHint", {
|
|
569
|
+
provider: runner.renderer.providerLabel,
|
|
570
|
+
}),
|
|
549
571
|
);
|
|
550
|
-
const
|
|
572
|
+
const pendingRecovery = session.state.pendingOutbound != null
|
|
573
|
+
|| session.state.pendingAssistantTurn != null;
|
|
574
|
+
let next;
|
|
575
|
+
let retryOnly = false;
|
|
576
|
+
for (;;) {
|
|
577
|
+
next = await readNextMessage(runner, activeChatInput, {
|
|
578
|
+
resolveAttachments: !pendingRecovery,
|
|
579
|
+
});
|
|
580
|
+
if (next == null) {
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
retryOnly = next.text.trim().toLowerCase() === "/retry";
|
|
584
|
+
if (!pendingRecovery || retryOnly) {
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
runner.renderer.hint(
|
|
588
|
+
t("session.recoveryHint", {
|
|
589
|
+
provider: runner.renderer.providerLabel,
|
|
590
|
+
}),
|
|
591
|
+
);
|
|
592
|
+
}
|
|
551
593
|
if (next == null) {
|
|
552
594
|
break;
|
|
553
595
|
}
|
|
554
|
-
const retryOnly = next.text.trim().toLowerCase() === "/retry";
|
|
555
596
|
if (!retryOnly) {
|
|
556
597
|
await session.appendInstruction(next.text, { files: next.files });
|
|
557
598
|
}
|
|
@@ -574,7 +615,7 @@ async function executeSession({
|
|
|
574
615
|
(item) => item.status === "running",
|
|
575
616
|
);
|
|
576
617
|
if (running.length > 0) {
|
|
577
|
-
runner.renderer.hint("
|
|
618
|
+
runner.renderer.hint(t("session.processesRunning"));
|
|
578
619
|
for (const item of running) {
|
|
579
620
|
runner.renderer.hint(
|
|
580
621
|
` ${item.processId} pid=${item.pid} ${item.detectedUrls.join(" ")}`,
|
|
@@ -582,7 +623,7 @@ async function executeSession({
|
|
|
582
623
|
}
|
|
583
624
|
}
|
|
584
625
|
|
|
585
|
-
const next = await
|
|
626
|
+
const next = await readNextMessage(runner, activeChatInput);
|
|
586
627
|
if (next == null) {
|
|
587
628
|
break;
|
|
588
629
|
}
|
|
@@ -599,7 +640,7 @@ async function executeSession({
|
|
|
599
640
|
process.removeListener("SIGINT", onInterrupt);
|
|
600
641
|
activeChatInput?.close();
|
|
601
642
|
await runner.close({ keepBrowser: runFailed });
|
|
602
|
-
console.log(
|
|
643
|
+
console.log(t("session.savedAt", { directory: session.directory }));
|
|
603
644
|
printResumeHint(session.sessionId);
|
|
604
645
|
}
|
|
605
646
|
}
|
|
@@ -610,7 +651,7 @@ function printResumeHint(sessionId) {
|
|
|
610
651
|
const rule = "─".repeat(48);
|
|
611
652
|
console.log("");
|
|
612
653
|
console.log(rule);
|
|
613
|
-
console.log(
|
|
654
|
+
console.log(t("session.resumeWith", { sessionId }));
|
|
614
655
|
console.log(rule);
|
|
615
656
|
console.log("");
|
|
616
657
|
}
|
|
@@ -619,12 +660,16 @@ function printResumeHint(sessionId) {
|
|
|
619
660
|
// re-prompts. Returns null for an explicit exit command, Ctrl+C, Ctrl+D, or EOF;
|
|
620
661
|
// otherwise returns
|
|
621
662
|
// { text, files } where files are resolved @file attachments.
|
|
622
|
-
async function promptForNextMessage(
|
|
663
|
+
async function promptForNextMessage(
|
|
664
|
+
runner,
|
|
665
|
+
chatInput,
|
|
666
|
+
{ resolveAttachments = true } = {},
|
|
667
|
+
) {
|
|
623
668
|
for (;;) {
|
|
624
669
|
const answer = chatInput
|
|
625
670
|
? await chatInput.read()
|
|
626
671
|
: await promptForText({
|
|
627
|
-
message: "
|
|
672
|
+
message: t("task.youPrompt"),
|
|
628
673
|
theme: { prefix: "" },
|
|
629
674
|
});
|
|
630
675
|
if (answer == null) {
|
|
@@ -640,7 +685,9 @@ async function promptForNextMessage(runner, chatInput) {
|
|
|
640
685
|
return null;
|
|
641
686
|
}
|
|
642
687
|
const { text } = classified;
|
|
643
|
-
const files =
|
|
688
|
+
const files = resolveAttachments
|
|
689
|
+
? await resolveMessageAttachments(runner, text)
|
|
690
|
+
: [];
|
|
644
691
|
return { text, files };
|
|
645
692
|
}
|
|
646
693
|
}
|
|
@@ -652,12 +699,16 @@ async function resolveMessageAttachments(runner, text) {
|
|
|
652
699
|
const { files, missing } = await extractAtMentions(text, projectRoot);
|
|
653
700
|
if (files.length > 0) {
|
|
654
701
|
runner.renderer.hint(
|
|
655
|
-
|
|
702
|
+
t("attachment.attaching", {
|
|
703
|
+
files: files.map((file) => file.name).join(", "),
|
|
704
|
+
}),
|
|
656
705
|
);
|
|
657
706
|
}
|
|
658
707
|
if (missing.length > 0) {
|
|
659
708
|
runner.renderer.hint(
|
|
660
|
-
|
|
709
|
+
t("attachment.notAttached", {
|
|
710
|
+
details: missing.map((m) => `${m.requested}: ${m.reason}`).join("; "),
|
|
711
|
+
}),
|
|
661
712
|
);
|
|
662
713
|
}
|
|
663
714
|
return files;
|
|
@@ -669,7 +720,7 @@ function isInteractiveSession(options) {
|
|
|
669
720
|
|
|
670
721
|
async function promptForUpdate({ currentVersion, latest }) {
|
|
671
722
|
return await promptForConfirm({
|
|
672
|
-
message:
|
|
723
|
+
message: t("update.prompt", { currentVersion, latest }),
|
|
673
724
|
default: true,
|
|
674
725
|
});
|
|
675
726
|
}
|
|
@@ -692,7 +743,6 @@ async function runUpdate() {
|
|
|
692
743
|
|
|
693
744
|
async function runAgent(taskParts, options) {
|
|
694
745
|
assertNativeRuntimeSupported();
|
|
695
|
-
options = withResolvedProviderSelection(options);
|
|
696
746
|
const projectRoot = path.resolve(options.project ?? process.cwd());
|
|
697
747
|
await assertDirectory(projectRoot);
|
|
698
748
|
|
|
@@ -711,35 +761,6 @@ async function runAgent(taskParts, options) {
|
|
|
711
761
|
printChatBanner(projectRoot, provider);
|
|
712
762
|
}
|
|
713
763
|
|
|
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.
|
|
718
|
-
let requestedMode;
|
|
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) {
|
|
725
|
-
requestedMode = normalizeConfiguredMode(options.mode);
|
|
726
|
-
} else if (interactive) {
|
|
727
|
-
const modeChoice = await promptForSelect({
|
|
728
|
-
message: `${provider.label} mode`,
|
|
729
|
-
choices: CHATGPT_MODE_CHOICES,
|
|
730
|
-
});
|
|
731
|
-
if (modeChoice == null) {
|
|
732
|
-
chatInput?.close();
|
|
733
|
-
console.log("");
|
|
734
|
-
return null;
|
|
735
|
-
}
|
|
736
|
-
requestedMode = modeFromPromptChoice(modeChoice);
|
|
737
|
-
} else {
|
|
738
|
-
// Non-interactive callers cannot answer a picker. Preserve the current web
|
|
739
|
-
// setting unless they explicitly opt into `--mode Pro`.
|
|
740
|
-
requestedMode = null;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
764
|
// In interactive mode an initial task is optional: the user can just start
|
|
744
765
|
// typing at the prompt. In one-shot mode a task is required.
|
|
745
766
|
let task = taskParts.join(" ").trim();
|
|
@@ -747,8 +768,8 @@ async function runAgent(taskParts, options) {
|
|
|
747
768
|
const initialMessage = interactive
|
|
748
769
|
? await readChatMessage(() => chatInput.read())
|
|
749
770
|
: await promptForText({
|
|
750
|
-
message: "
|
|
751
|
-
validate: (value) => value.trim() ? true : "
|
|
771
|
+
message: t("task.prompt"),
|
|
772
|
+
validate: (value) => value.trim() ? true : t("task.required"),
|
|
752
773
|
});
|
|
753
774
|
if (initialMessage == null) {
|
|
754
775
|
chatInput?.close();
|
|
@@ -767,7 +788,7 @@ async function runAgent(taskParts, options) {
|
|
|
767
788
|
task,
|
|
768
789
|
projectRoot,
|
|
769
790
|
provider: provider.id,
|
|
770
|
-
mode:
|
|
791
|
+
mode: null,
|
|
771
792
|
});
|
|
772
793
|
|
|
773
794
|
// Resolve @file attachments in the opening task, if any. The task itself is
|
|
@@ -778,12 +799,14 @@ async function runAgent(taskParts, options) {
|
|
|
778
799
|
const { files: found, missing } = await extractAtMentions(task, projectRoot);
|
|
779
800
|
files = found;
|
|
780
801
|
if (found.length > 0) {
|
|
781
|
-
console.log(
|
|
802
|
+
console.log(t("attachment.attaching", {
|
|
803
|
+
files: found.map((file) => file.name).join(", "),
|
|
804
|
+
}));
|
|
782
805
|
}
|
|
783
806
|
if (missing.length > 0) {
|
|
784
|
-
console.log(
|
|
785
|
-
|
|
786
|
-
);
|
|
807
|
+
console.log(t("attachment.notAttached", {
|
|
808
|
+
details: missing.map((m) => `${m.requested}: ${m.reason}`).join("; "),
|
|
809
|
+
}));
|
|
787
810
|
}
|
|
788
811
|
}
|
|
789
812
|
|
|
@@ -795,8 +818,8 @@ function printChatBanner(projectRoot, provider) {
|
|
|
795
818
|
const DIM = "\x1b[2m";
|
|
796
819
|
const RESET = "\x1b[0m";
|
|
797
820
|
console.log("");
|
|
798
|
-
console.log(`${CYAN}WTAgent${RESET} ${DIM}· ${provider.label} · ${projectRoot}${RESET}`);
|
|
799
|
-
console.log(`${DIM}
|
|
821
|
+
console.log(`${CYAN}WTAgent${RESET} ${DIM}· ${provider.label} · ${formatHomePath(projectRoot)}${RESET}`);
|
|
822
|
+
console.log(`${DIM}${t("banner.controls")}${RESET}`);
|
|
800
823
|
console.log("");
|
|
801
824
|
}
|
|
802
825
|
|
|
@@ -819,13 +842,25 @@ async function loadSession(paths, sessionId) {
|
|
|
819
842
|
|
|
820
843
|
async function runResume(sessionId, instructionParts, options) {
|
|
821
844
|
assertNativeRuntimeSupported();
|
|
822
|
-
options = withResolvedProviderSelection(options);
|
|
823
845
|
// Loading only needs the sessions dir (provider-independent); the provider is
|
|
824
846
|
// then read from the session so the run reuses the right adapter + profile.
|
|
825
847
|
const paths = resolveRuntimePaths(options);
|
|
826
848
|
await ensureDirectory(paths.sessionsDir);
|
|
827
849
|
const session = await loadSession(paths, sessionId);
|
|
828
850
|
await assertDirectory(session.state.projectRoot);
|
|
851
|
+
const instruction = instructionParts.join(" ").trim();
|
|
852
|
+
if (
|
|
853
|
+
instruction
|
|
854
|
+
&& (
|
|
855
|
+
session.state.pendingOutbound != null
|
|
856
|
+
|| session.state.pendingAssistantTurn != null
|
|
857
|
+
)
|
|
858
|
+
) {
|
|
859
|
+
throw new BrowserAdapterError(
|
|
860
|
+
"This session has a pending browser handoff. Resume it without an instruction or attachment first.",
|
|
861
|
+
{ code: "RECOVERY_REQUIRES_BARE_RESUME", recoverable: false },
|
|
862
|
+
);
|
|
863
|
+
}
|
|
829
864
|
|
|
830
865
|
const provider = resolveProvider(session.state.provider ?? DEFAULT_PROVIDER);
|
|
831
866
|
// A conversation belongs to one provider. --model on resume is only allowed
|
|
@@ -842,49 +877,20 @@ async function runResume(sessionId, instructionParts, options) {
|
|
|
842
877
|
return null;
|
|
843
878
|
}
|
|
844
879
|
|
|
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.
|
|
851
|
-
let requestedMode;
|
|
852
|
-
if (!provider.promptsForMode) {
|
|
853
|
-
requestedMode = null;
|
|
854
|
-
} else if (options.mode != null) {
|
|
855
|
-
requestedMode = normalizeConfiguredMode(options.mode);
|
|
856
|
-
} else if (
|
|
857
|
-
!options.once
|
|
858
|
-
&& process.stdin.isTTY
|
|
859
|
-
&& process.stdout.isTTY
|
|
860
|
-
) {
|
|
861
|
-
const modeChoice = await promptForSelect({
|
|
862
|
-
message: `${provider.label} mode`,
|
|
863
|
-
choices: CHATGPT_MODE_CHOICES,
|
|
864
|
-
default: session.state.activeMode === "Pro" ? "pro" : "current",
|
|
865
|
-
});
|
|
866
|
-
if (modeChoice == null) {
|
|
867
|
-
console.log("");
|
|
868
|
-
return null;
|
|
869
|
-
}
|
|
870
|
-
requestedMode = modeFromPromptChoice(modeChoice);
|
|
871
|
-
} else {
|
|
872
|
-
requestedMode = null;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
const instruction = instructionParts.join(" ").trim();
|
|
876
880
|
let files = [];
|
|
877
881
|
if (instruction) {
|
|
878
882
|
const projectRoot = session.state.projectRoot;
|
|
879
883
|
const { files: found, missing } = await extractAtMentions(instruction, projectRoot);
|
|
880
884
|
files = found;
|
|
881
885
|
if (found.length > 0) {
|
|
882
|
-
console.log(
|
|
886
|
+
console.log(t("attachment.attaching", {
|
|
887
|
+
files: found.map((file) => file.name).join(", "),
|
|
888
|
+
}));
|
|
883
889
|
}
|
|
884
890
|
if (missing.length > 0) {
|
|
885
|
-
console.log(
|
|
886
|
-
|
|
887
|
-
);
|
|
891
|
+
console.log(t("attachment.notAttached", {
|
|
892
|
+
details: missing.map((m) => `${m.requested}: ${m.reason}`).join("; "),
|
|
893
|
+
}));
|
|
888
894
|
}
|
|
889
895
|
await session.appendInstruction(instruction, { files });
|
|
890
896
|
}
|
|
@@ -895,7 +901,6 @@ async function runResume(sessionId, instructionParts, options) {
|
|
|
895
901
|
resume: true,
|
|
896
902
|
instruction: instruction || null,
|
|
897
903
|
files,
|
|
898
|
-
mode: requestedMode,
|
|
899
904
|
});
|
|
900
905
|
}
|
|
901
906
|
|
|
@@ -923,7 +928,7 @@ async function runStatus(sessionId, options) {
|
|
|
923
928
|
.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
|
|
924
929
|
.slice(0, 20);
|
|
925
930
|
if (sessions.length === 0) {
|
|
926
|
-
console.log("
|
|
931
|
+
console.log(t("status.none"));
|
|
927
932
|
return;
|
|
928
933
|
}
|
|
929
934
|
|
|
@@ -969,74 +974,56 @@ async function runExport(sessionId, options) {
|
|
|
969
974
|
|
|
970
975
|
const program = new Command()
|
|
971
976
|
.name("wtagent")
|
|
972
|
-
.description("
|
|
977
|
+
.description(t("app.description"))
|
|
973
978
|
.version(getPackageVersion())
|
|
974
|
-
.option("--home <path>", "
|
|
975
|
-
.option("--profile-dir <path>", "
|
|
976
|
-
.option("--chrome-path <path>", "
|
|
977
|
-
.option("-C, --project <path>", "
|
|
979
|
+
.option("--home <path>", t("option.home"))
|
|
980
|
+
.option("--profile-dir <path>", t("option.profileDir"))
|
|
981
|
+
.option("--chrome-path <path>", t("option.chromePath"))
|
|
982
|
+
.option("-C, --project <path>", t("option.project"), process.cwd())
|
|
978
983
|
.option(
|
|
979
984
|
"--model <provider>",
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
"ChatGPT mode (Pro/Current), or a provider alias such as kimi",
|
|
985
|
-
)
|
|
986
|
-
.option(
|
|
987
|
-
"--once",
|
|
988
|
-
"Run a single request and exit instead of a conversation",
|
|
989
|
-
false,
|
|
990
|
-
)
|
|
991
|
-
.option(
|
|
992
|
-
"--model-turn-timeout-ms <milliseconds>",
|
|
993
|
-
"Maximum wait for one model response (default: 10 minutes; 16 minutes for ChatGPT Pro)",
|
|
994
|
-
)
|
|
995
|
-
.option(
|
|
996
|
-
"--no-minimize",
|
|
997
|
-
"Keep the Chrome window visible instead of minimizing it",
|
|
998
|
-
)
|
|
999
|
-
.option("--debug", "Write browser diagnostics", false)
|
|
1000
|
-
.argument(
|
|
1001
|
-
"[task...]",
|
|
1002
|
-
"Initial request (optional; you can also type at the prompt)",
|
|
985
|
+
t("option.model", {
|
|
986
|
+
providers: listActiveProviderIds().join(", "),
|
|
987
|
+
defaultProvider: DEFAULT_PROVIDER,
|
|
988
|
+
}),
|
|
1003
989
|
)
|
|
990
|
+
.option("--once", t("option.once"), false)
|
|
991
|
+
.option("--model-turn-timeout-ms <milliseconds>", t("option.timeout"))
|
|
992
|
+
.option("--no-minimize", t("option.noMinimize"))
|
|
993
|
+
.option("--debug", t("option.debug"), false)
|
|
994
|
+
.argument("[task...]", t("argument.task"))
|
|
1004
995
|
.action(async (task, _, command) => {
|
|
1005
996
|
await runAgent(task, command.optsWithGlobals());
|
|
1006
997
|
});
|
|
1007
998
|
|
|
1008
999
|
program
|
|
1009
1000
|
.command("update")
|
|
1010
|
-
.description("
|
|
1001
|
+
.description(t("command.update"))
|
|
1011
1002
|
.action(async () => runUpdate());
|
|
1012
1003
|
|
|
1013
1004
|
program
|
|
1014
1005
|
.command("doctor")
|
|
1015
|
-
.description("
|
|
1006
|
+
.description(t("command.doctor"))
|
|
1016
1007
|
.action(async (_, command) => runDoctor(command.optsWithGlobals()));
|
|
1017
1008
|
|
|
1018
1009
|
program
|
|
1019
1010
|
.command("login")
|
|
1020
|
-
.description("
|
|
1011
|
+
.description(t("command.login"))
|
|
1021
1012
|
.action(async (_, command) => runLogin(command.optsWithGlobals()));
|
|
1022
1013
|
|
|
1023
1014
|
program
|
|
1024
1015
|
.command("logout")
|
|
1025
|
-
.description("
|
|
1026
|
-
.option("--yes", "
|
|
1016
|
+
.description(t("command.logout"))
|
|
1017
|
+
.option("--yes", t("option.yes"), false)
|
|
1027
1018
|
.action(async (options, command) => {
|
|
1028
1019
|
await runLogout({ ...command.optsWithGlobals(), ...options });
|
|
1029
1020
|
});
|
|
1030
1021
|
|
|
1031
1022
|
program
|
|
1032
1023
|
.command("resume")
|
|
1033
|
-
.description("
|
|
1034
|
-
.argument("<session-id>", "
|
|
1035
|
-
.argument("[instruction...]", "
|
|
1036
|
-
.option(
|
|
1037
|
-
"--mode <name>",
|
|
1038
|
-
"ChatGPT mode (Pro/Current), or a provider alias such as kimi",
|
|
1039
|
-
)
|
|
1024
|
+
.description(t("command.resume"))
|
|
1025
|
+
.argument("<session-id>", t("argument.sessionId"))
|
|
1026
|
+
.argument("[instruction...]", t("argument.instruction"))
|
|
1040
1027
|
.action(async (sessionId, instruction, _, command) => {
|
|
1041
1028
|
await runResume(
|
|
1042
1029
|
sessionId,
|
|
@@ -1047,42 +1034,49 @@ program
|
|
|
1047
1034
|
|
|
1048
1035
|
program
|
|
1049
1036
|
.command("status")
|
|
1050
|
-
.description("
|
|
1051
|
-
.argument("[session-id]", "
|
|
1037
|
+
.description(t("command.status"))
|
|
1038
|
+
.argument("[session-id]", t("argument.sessionId"))
|
|
1052
1039
|
.action(async (sessionId, _, command) => {
|
|
1053
1040
|
await runStatus(sessionId, command.optsWithGlobals());
|
|
1054
1041
|
});
|
|
1055
1042
|
|
|
1056
1043
|
program
|
|
1057
1044
|
.command("export")
|
|
1058
|
-
.description("
|
|
1059
|
-
.argument("<session-id>", "
|
|
1060
|
-
.option("--format <name>", "
|
|
1061
|
-
.option("-o, --output <path>", "
|
|
1045
|
+
.description(t("command.export"))
|
|
1046
|
+
.argument("<session-id>", t("argument.sessionId"))
|
|
1047
|
+
.option("--format <name>", t("option.format"), "codex")
|
|
1048
|
+
.option("-o, --output <path>", t("option.output"))
|
|
1062
1049
|
.action(async (sessionId, options, command) => {
|
|
1063
1050
|
await runExport(sessionId, { ...command.optsWithGlobals(), ...options });
|
|
1064
1051
|
});
|
|
1065
1052
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1053
|
+
// Resolve npm's executable symlink as well as direct `node main.js` invocations.
|
|
1054
|
+
// Importing the session runner in tests must not start the CLI.
|
|
1055
|
+
const entryPath = process.argv[1]
|
|
1056
|
+
? await fs.realpath(process.argv[1]).catch(() => null)
|
|
1057
|
+
: null;
|
|
1058
|
+
if (entryPath && import.meta.url === pathToFileURL(entryPath).href) {
|
|
1059
|
+
await 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
|
+
}
|
|
1067
|
+
console.error(error.stack ?? error.message);
|
|
1071
1068
|
process.exitCode = 1;
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
process.exit(process.exitCode);
|
|
1087
|
-
}
|
|
1088
|
-
});
|
|
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
|
+
}
|
|
1081
|
+
});
|
|
1082
|
+
}
|