wtagent 0.2.1 → 0.3.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/src/cli/main.js CHANGED
@@ -11,7 +11,6 @@ import {
11
11
  getProviderProfileDir,
12
12
  isProviderProfileBasename,
13
13
  listActiveProviderIds,
14
- resolveCliProviderSelection,
15
14
  resolveProvider,
16
15
  } from "../browser/provider-registry.js";
17
16
  import {
@@ -45,11 +44,7 @@ import {
45
44
  ShellChatInput,
46
45
  } from "./prompt-input.js";
47
46
  import { createRenderer } from "./render-events.js";
48
- import {
49
- CHATGPT_MODE_CHOICES,
50
- modeFromPromptChoice,
51
- normalizeConfiguredMode,
52
- } from "./mode-choice.js";
47
+ import { t } from "./i18n.js";
53
48
  import { runSelfUpdate } from "./self-update.js";
54
49
  import { runStartupChecks } from "./startup-notices.js";
55
50
 
@@ -58,10 +53,6 @@ import { runStartupChecks } from "./startup-notices.js";
58
53
  // (each provider logs in independently); an explicit `--profile-dir` still
59
54
  // overrides it. Defaults to the ChatGPT profile so callers that predate
60
55
  // multi-provider support are unaffected.
61
- function withResolvedProviderSelection(options) {
62
- return { ...options, ...resolveCliProviderSelection(options) };
63
- }
64
-
65
56
  function resolveRuntimePaths(options, providerId = DEFAULT_PROVIDER) {
66
57
  const appDataDir = path.resolve(options.home ?? getAppDataDir());
67
58
  return {
@@ -83,15 +74,12 @@ async function assertDirectory(directory) {
83
74
 
84
75
  async function runLogin(options) {
85
76
  assertNativeRuntimeSupported();
86
- options = withResolvedProviderSelection(options);
87
77
  const provider = resolveProvider(options.model ?? DEFAULT_PROVIDER);
88
78
  const { profileDir } = resolveRuntimePaths(options, provider.id);
89
79
  const { label, baseUrl } = provider;
90
80
  for (;;) {
91
- console.log(`Opening native Chrome profile: ${profileDir}`);
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
- );
81
+ console.log(t("login.openingProfile", { profileDir }));
82
+ console.log(t("login.finishSignIn", { provider: label }));
95
83
  const browser = await launchNativeLoginBrowser({
96
84
  profileDir,
97
85
  chromePath: options.chromePath,
@@ -100,13 +88,12 @@ async function runLogin(options) {
100
88
 
101
89
  try {
102
90
  const answer = await promptForText({
103
- message:
104
- `After the signed-in ${label} home is visible, press Enter here to save and verify`,
91
+ message: t("login.pressEnterVerify", { provider: label }),
105
92
  });
106
93
  if (answer == null) {
107
94
  return;
108
95
  }
109
- console.log("Closing native Chrome and saving the dedicated profile...");
96
+ console.log(t("login.savingProfile"));
110
97
  await new Promise((resolve) => setTimeout(resolve, 1_000));
111
98
  } finally {
112
99
  await browser.close();
@@ -129,16 +116,14 @@ async function runLogin(options) {
129
116
  }
130
117
  }
131
118
  if (authenticated) {
132
- console.log(`${label} login verified through a fresh CDP connection.`);
119
+ console.log(t("login.verified", { provider: label }));
133
120
  return;
134
121
  }
135
122
  } finally {
136
123
  await verifier.close();
137
124
  }
138
125
 
139
- console.log(
140
- `${label} is still in guest mode. Reopening native Chrome; complete the final ${label} sign-in/continue step.`,
141
- );
126
+ console.log(t("login.stillGuest", { provider: label }));
142
127
  }
143
128
  }
144
129
 
@@ -150,14 +135,13 @@ async function runLogout(options) {
150
135
  // `logout` targets one provider's profile; unknown ids are rejected but a
151
136
  // "planned" provider is still allowed (its profile may exist from a prior
152
137
  // login attempt), so use getProvider rather than resolveProvider.
153
- options = withResolvedProviderSelection(options);
154
138
  const provider = getProvider(options.model ?? DEFAULT_PROVIDER);
155
139
  const { profileDir } = resolveRuntimePaths(options, provider.id);
156
140
  const exists = await fs.stat(profileDir)
157
141
  .then((stat) => stat.isDirectory())
158
142
  .catch(() => false);
159
143
  if (!exists) {
160
- console.log(`No Chrome profile found at ${profileDir}; already logged out.`);
144
+ console.log(t("logout.none", { profileDir }));
161
145
  return;
162
146
  }
163
147
 
@@ -168,30 +152,29 @@ async function runLogout(options) {
168
152
  .then((stat) => stat.isDirectory())
169
153
  .catch(() => false);
170
154
  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
- );
155
+ throw new Error(t("logout.refuse", { profileDir }));
174
156
  }
175
157
 
176
158
  if (!options.yes) {
177
159
  const confirmed = await confirm({
178
- message:
179
- `This deletes the local ${provider.label} session (Chrome profile at ${profileDir}) `
180
- + "and requires a new login. Continue?",
160
+ message: t("logout.confirm", {
161
+ provider: provider.label,
162
+ profileDir,
163
+ }),
181
164
  default: false,
182
165
  });
183
166
  if (!confirmed) {
184
- console.log("Logout cancelled.");
167
+ console.log(t("logout.cancelled"));
185
168
  return;
186
169
  }
187
170
  }
188
171
 
189
172
  await fs.rm(profileDir, { recursive: true, force: true });
190
- console.log(`Logged out. Removed ${profileDir}.`);
173
+ console.log(t("logout.done", { profileDir }));
191
174
  console.log(
192
175
  provider.id === DEFAULT_PROVIDER
193
- ? "Run `wtagent login` to sign in again."
194
- : `Run \`wtagent login --model ${provider.id}\` to sign in again.`,
176
+ ? t("logout.loginAgainDefault")
177
+ : t("logout.loginAgainProvider", { provider: provider.id }),
195
178
  );
196
179
  }
197
180
 
@@ -231,6 +214,8 @@ class ConversationRunner {
231
214
  // The profile dir and adapter follow from it, so resumes reuse the right
232
215
  // login even when the CLI is invoked without --model.
233
216
  const provider = resolveProvider(session.state.provider ?? DEFAULT_PROVIDER);
217
+ this.provider = provider;
218
+ this.interactive = interactive;
234
219
  this.paths = resolveRuntimePaths(options, provider.id);
235
220
  this.limits = resolveLimits({
236
221
  modelTurnTimeoutMs: options.modelTurnTimeoutMs,
@@ -267,20 +252,45 @@ class ConversationRunner {
267
252
  policy: new PolicyEngine({ store: this.approvalStore }),
268
253
  session: this.session,
269
254
  limits: this.limits,
255
+ postAuthSetup: (
256
+ this.interactive
257
+ ? async ({ adapter }) => {
258
+ this.renderer.stopSpinner();
259
+ await adapter.restoreWindow?.();
260
+ try {
261
+ const answer = await promptForText({
262
+ message: t("model.chooseInBrowser", {
263
+ provider: this.provider.label,
264
+ }),
265
+ });
266
+ if (answer == null) {
267
+ throw new Error(t("model.setupCancelled"));
268
+ }
269
+ } finally {
270
+ await adapter.minimizeWindow?.();
271
+ }
272
+ }
273
+ : null
274
+ ),
270
275
  approval: async ({ toolCall, reasons }) => {
271
276
  this.renderer.stopSpinner();
272
- console.log(`\n${"\x1b[33m"}Approval required for ${toolCall.name}:${"\x1b[0m"}`);
277
+ console.log(
278
+ `\n${"\x1b[33m"}${t("approval.required", { tool: toolCall.name })}${"\x1b[0m"}`,
279
+ );
273
280
  for (const reason of reasons) {
274
281
  console.log(`- ${reason}`);
275
282
  }
276
283
  console.log(JSON.stringify(toolCall.args, null, 2));
277
284
  const choice = await select({
278
- message: "How should this action be handled?",
285
+ message: t("approval.how"),
279
286
  choices: [
280
- { name: "Allow once", value: "once" },
281
- { name: `Always allow ${toolCall.name}`, value: "always-tool" },
282
- { name: "Always allow everything", value: "always-all" },
283
- { name: "Deny", value: "deny" },
287
+ { name: t("approval.once"), value: "once" },
288
+ {
289
+ name: t("approval.alwaysTool", { tool: toolCall.name }),
290
+ value: "always-tool",
291
+ },
292
+ { name: t("approval.alwaysAll"), value: "always-all" },
293
+ { name: t("approval.deny"), value: "deny" },
284
294
  ],
285
295
  });
286
296
  if (choice === "deny") {
@@ -289,17 +299,16 @@ class ConversationRunner {
289
299
  if (choice === "always-tool") {
290
300
  this.approvalStore.setAlwaysAllowedTool(toolCall.name);
291
301
  await this.approvalStore.save();
292
- console.log(
293
- `Saved for this session: ${toolCall.name} will always be allowed `
294
- + `(${this.approvalStore.filePath}).`,
295
- );
302
+ console.log(t("approval.savedTool", {
303
+ tool: toolCall.name,
304
+ file: this.approvalStore.filePath,
305
+ }));
296
306
  } else if (choice === "always-all") {
297
307
  this.approvalStore.setAlwaysAllowAll();
298
308
  await this.approvalStore.save();
299
- console.log(
300
- `Saved for this session: every tool will always be allowed `
301
- + `(${this.approvalStore.filePath}).`,
302
- );
309
+ console.log(t("approval.savedAll", {
310
+ file: this.approvalStore.filePath,
311
+ }));
303
312
  }
304
313
  return true;
305
314
  },
@@ -430,18 +439,18 @@ async function promptToResolveProfileLock(error) {
430
439
  const pid = Number(error.details?.pid);
431
440
  console.log(`\n${"\x1b[33m"}${error.message}${"\x1b[0m"}`);
432
441
  const choice = await promptForSelect({
433
- message: "How should this be handled?",
442
+ message: t("profileLock.how"),
434
443
  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" },
444
+ { name: t("profileLock.kill"), value: "kill" },
445
+ { name: t("profileLock.retry"), value: "retry" },
446
+ { name: t("profileLock.quit"), value: "quit" },
438
447
  ],
439
448
  });
440
449
  if (choice == null || choice === "quit") {
441
450
  return false;
442
451
  }
443
452
  if (choice === "kill" && Number.isSafeInteger(pid) && pid > 0) {
444
- console.log(`Stopping pid=${pid}...`);
453
+ console.log(t("profileLock.stopping", { pid }));
445
454
  try {
446
455
  process.kill(pid, "SIGTERM");
447
456
  } catch {
@@ -477,17 +486,17 @@ async function executeSession({
477
486
 
478
487
  const onInterrupt = () => {
479
488
  if (runner.interrupted) {
480
- console.log("\nForce quitting.");
489
+ console.log(`\n${t("session.forceQuit")}`);
481
490
  // process.exit skips the finally block, so repeat the session/resume
482
491
  // hint here — the user should always know how to continue.
483
- console.log(`Session saved at: ${session.directory}`);
492
+ console.log(t("session.savedAt", { directory: session.directory }));
484
493
  printResumeHint(session.sessionId);
485
494
  process.exit(130);
486
495
  }
487
496
  runner.interrupted = true;
488
497
  runner.adapter.escCancelRequested = true;
489
498
  runner.renderer.stopSpinner();
490
- console.log("\nCtrl+C — cancelling. Press again to force quit.");
499
+ console.log(`\n${t("session.ctrlC")}`);
491
500
  process.exitCode = 130;
492
501
  };
493
502
  process.on("SIGINT", onInterrupt);
@@ -535,7 +544,7 @@ async function executeSession({
535
544
  if (!interactive) {
536
545
  throw result.error;
537
546
  }
538
- runner.renderer.hint("Turn cancelled. Type a new message or quit.");
547
+ runner.renderer.hint(t("session.cancelledHint"));
539
548
  turnResume = true;
540
549
  turnInPlaceRecovery = false;
541
550
  continue;
@@ -545,7 +554,9 @@ async function executeSession({
545
554
  throw result.error;
546
555
  }
547
556
  runner.renderer.hint(
548
- `Type /retry to ask ${runner.renderer.providerLabel} to continue again, enter a new instruction, or quit`,
557
+ t("session.recoveryHint", {
558
+ provider: runner.renderer.providerLabel,
559
+ }),
549
560
  );
550
561
  const next = await promptForNextMessage(runner, activeChatInput);
551
562
  if (next == null) {
@@ -574,7 +585,7 @@ async function executeSession({
574
585
  (item) => item.status === "running",
575
586
  );
576
587
  if (running.length > 0) {
577
- runner.renderer.hint("Managed processes still running:");
588
+ runner.renderer.hint(t("session.processesRunning"));
578
589
  for (const item of running) {
579
590
  runner.renderer.hint(
580
591
  ` ${item.processId} pid=${item.pid} ${item.detectedUrls.join(" ")}`,
@@ -599,7 +610,7 @@ async function executeSession({
599
610
  process.removeListener("SIGINT", onInterrupt);
600
611
  activeChatInput?.close();
601
612
  await runner.close({ keepBrowser: runFailed });
602
- console.log(`Session saved at: ${session.directory}`);
613
+ console.log(t("session.savedAt", { directory: session.directory }));
603
614
  printResumeHint(session.sessionId);
604
615
  }
605
616
  }
@@ -610,7 +621,7 @@ function printResumeHint(sessionId) {
610
621
  const rule = "─".repeat(48);
611
622
  console.log("");
612
623
  console.log(rule);
613
- console.log(`Resume this conversation with: wtagent resume ${sessionId}`);
624
+ console.log(t("session.resumeWith", { sessionId }));
614
625
  console.log(rule);
615
626
  console.log("");
616
627
  }
@@ -624,7 +635,7 @@ async function promptForNextMessage(runner, chatInput) {
624
635
  const answer = chatInput
625
636
  ? await chatInput.read()
626
637
  : await promptForText({
627
- message: "you ›",
638
+ message: t("task.youPrompt"),
628
639
  theme: { prefix: "" },
629
640
  });
630
641
  if (answer == null) {
@@ -652,12 +663,16 @@ async function resolveMessageAttachments(runner, text) {
652
663
  const { files, missing } = await extractAtMentions(text, projectRoot);
653
664
  if (files.length > 0) {
654
665
  runner.renderer.hint(
655
- `Attaching: ${files.map((file) => file.name).join(", ")}`,
666
+ t("attachment.attaching", {
667
+ files: files.map((file) => file.name).join(", "),
668
+ }),
656
669
  );
657
670
  }
658
671
  if (missing.length > 0) {
659
672
  runner.renderer.hint(
660
- `Not attached (${missing.map((m) => `${m.requested}: ${m.reason}`).join("; ")})`,
673
+ t("attachment.notAttached", {
674
+ details: missing.map((m) => `${m.requested}: ${m.reason}`).join("; "),
675
+ }),
661
676
  );
662
677
  }
663
678
  return files;
@@ -669,7 +684,7 @@ function isInteractiveSession(options) {
669
684
 
670
685
  async function promptForUpdate({ currentVersion, latest }) {
671
686
  return await promptForConfirm({
672
- message: `Update WTAgent from ${currentVersion} to ${latest} now?`,
687
+ message: t("update.prompt", { currentVersion, latest }),
673
688
  default: true,
674
689
  });
675
690
  }
@@ -692,7 +707,6 @@ async function runUpdate() {
692
707
 
693
708
  async function runAgent(taskParts, options) {
694
709
  assertNativeRuntimeSupported();
695
- options = withResolvedProviderSelection(options);
696
710
  const projectRoot = path.resolve(options.project ?? process.cwd());
697
711
  await assertDirectory(projectRoot);
698
712
 
@@ -711,35 +725,6 @@ async function runAgent(taskParts, options) {
711
725
  printChatBanner(projectRoot, provider);
712
726
  }
713
727
 
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
728
  // In interactive mode an initial task is optional: the user can just start
744
729
  // typing at the prompt. In one-shot mode a task is required.
745
730
  let task = taskParts.join(" ").trim();
@@ -747,8 +732,8 @@ async function runAgent(taskParts, options) {
747
732
  const initialMessage = interactive
748
733
  ? await readChatMessage(() => chatInput.read())
749
734
  : await promptForText({
750
- message: "Task",
751
- validate: (value) => value.trim() ? true : "Please type a message.",
735
+ message: t("task.prompt"),
736
+ validate: (value) => value.trim() ? true : t("task.required"),
752
737
  });
753
738
  if (initialMessage == null) {
754
739
  chatInput?.close();
@@ -767,7 +752,7 @@ async function runAgent(taskParts, options) {
767
752
  task,
768
753
  projectRoot,
769
754
  provider: provider.id,
770
- mode: requestedMode,
755
+ mode: null,
771
756
  });
772
757
 
773
758
  // Resolve @file attachments in the opening task, if any. The task itself is
@@ -778,12 +763,14 @@ async function runAgent(taskParts, options) {
778
763
  const { files: found, missing } = await extractAtMentions(task, projectRoot);
779
764
  files = found;
780
765
  if (found.length > 0) {
781
- console.log(`Attaching: ${found.map((file) => file.name).join(", ")}`);
766
+ console.log(t("attachment.attaching", {
767
+ files: found.map((file) => file.name).join(", "),
768
+ }));
782
769
  }
783
770
  if (missing.length > 0) {
784
- console.log(
785
- `Not attached (${missing.map((m) => `${m.requested}: ${m.reason}`).join("; ")})`,
786
- );
771
+ console.log(t("attachment.notAttached", {
772
+ details: missing.map((m) => `${m.requested}: ${m.reason}`).join("; "),
773
+ }));
787
774
  }
788
775
  }
789
776
 
@@ -796,7 +783,7 @@ function printChatBanner(projectRoot, provider) {
796
783
  const RESET = "\x1b[0m";
797
784
  console.log("");
798
785
  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}`);
786
+ console.log(`${DIM}${t("banner.controls")}${RESET}`);
800
787
  console.log("");
801
788
  }
802
789
 
@@ -819,7 +806,6 @@ async function loadSession(paths, sessionId) {
819
806
 
820
807
  async function runResume(sessionId, instructionParts, options) {
821
808
  assertNativeRuntimeSupported();
822
- options = withResolvedProviderSelection(options);
823
809
  // Loading only needs the sessions dir (provider-independent); the provider is
824
810
  // then read from the session so the run reuses the right adapter + profile.
825
811
  const paths = resolveRuntimePaths(options);
@@ -842,36 +828,6 @@ async function runResume(sessionId, instructionParts, options) {
842
828
  return null;
843
829
  }
844
830
 
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
831
  const instruction = instructionParts.join(" ").trim();
876
832
  let files = [];
877
833
  if (instruction) {
@@ -879,12 +835,14 @@ async function runResume(sessionId, instructionParts, options) {
879
835
  const { files: found, missing } = await extractAtMentions(instruction, projectRoot);
880
836
  files = found;
881
837
  if (found.length > 0) {
882
- console.log(`Attaching: ${found.map((file) => file.name).join(", ")}`);
838
+ console.log(t("attachment.attaching", {
839
+ files: found.map((file) => file.name).join(", "),
840
+ }));
883
841
  }
884
842
  if (missing.length > 0) {
885
- console.log(
886
- `Not attached (${missing.map((m) => `${m.requested}: ${m.reason}`).join("; ")})`,
887
- );
843
+ console.log(t("attachment.notAttached", {
844
+ details: missing.map((m) => `${m.requested}: ${m.reason}`).join("; "),
845
+ }));
888
846
  }
889
847
  await session.appendInstruction(instruction, { files });
890
848
  }
@@ -895,7 +853,6 @@ async function runResume(sessionId, instructionParts, options) {
895
853
  resume: true,
896
854
  instruction: instruction || null,
897
855
  files,
898
- mode: requestedMode,
899
856
  });
900
857
  }
901
858
 
@@ -923,7 +880,7 @@ async function runStatus(sessionId, options) {
923
880
  .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
924
881
  .slice(0, 20);
925
882
  if (sessions.length === 0) {
926
- console.log("No sessions found.");
883
+ console.log(t("status.none"));
927
884
  return;
928
885
  }
929
886
 
@@ -969,74 +926,56 @@ async function runExport(sessionId, options) {
969
926
 
970
927
  const program = new Command()
971
928
  .name("wtagent")
972
- .description("Turn your web AI session into a local tool-using agent.")
929
+ .description(t("app.description"))
973
930
  .version(getPackageVersion())
974
- .option("--home <path>", "Application data directory")
975
- .option("--profile-dir <path>", "Dedicated Chrome profile directory")
976
- .option("--chrome-path <path>", "Chrome/Chromium executable")
977
- .option("-C, --project <path>", "Project directory", process.cwd())
931
+ .option("--home <path>", t("option.home"))
932
+ .option("--profile-dir <path>", t("option.profileDir"))
933
+ .option("--chrome-path <path>", t("option.chromePath"))
934
+ .option("-C, --project <path>", t("option.project"), process.cwd())
978
935
  .option(
979
936
  "--model <provider>",
980
- `Web AI provider: ${listActiveProviderIds().join(", ")} (default: ${DEFAULT_PROVIDER})`,
981
- )
982
- .option(
983
- "--mode <name>",
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)",
937
+ t("option.model", {
938
+ providers: listActiveProviderIds().join(", "),
939
+ defaultProvider: DEFAULT_PROVIDER,
940
+ }),
1003
941
  )
942
+ .option("--once", t("option.once"), false)
943
+ .option("--model-turn-timeout-ms <milliseconds>", t("option.timeout"))
944
+ .option("--no-minimize", t("option.noMinimize"))
945
+ .option("--debug", t("option.debug"), false)
946
+ .argument("[task...]", t("argument.task"))
1004
947
  .action(async (task, _, command) => {
1005
948
  await runAgent(task, command.optsWithGlobals());
1006
949
  });
1007
950
 
1008
951
  program
1009
952
  .command("update")
1010
- .description("Install the latest WTAgent from npm.")
953
+ .description(t("command.update"))
1011
954
  .action(async () => runUpdate());
1012
955
 
1013
956
  program
1014
957
  .command("doctor")
1015
- .description("Check Node, Chrome, and local data directories.")
958
+ .description(t("command.doctor"))
1016
959
  .action(async (_, command) => runDoctor(command.optsWithGlobals()));
1017
960
 
1018
961
  program
1019
962
  .command("login")
1020
- .description("Open the dedicated Chrome profile and wait for provider login (use --model to pick a provider).")
963
+ .description(t("command.login"))
1021
964
  .action(async (_, command) => runLogin(command.optsWithGlobals()));
1022
965
 
1023
966
  program
1024
967
  .command("logout")
1025
- .description("Delete the local Chrome profile to reset a provider session (use --model to pick a provider).")
1026
- .option("--yes", "Skip the confirmation prompt", false)
968
+ .description(t("command.logout"))
969
+ .option("--yes", t("option.yes"), false)
1027
970
  .action(async (options, command) => {
1028
971
  await runLogout({ ...command.optsWithGlobals(), ...options });
1029
972
  });
1030
973
 
1031
974
  program
1032
975
  .command("resume")
1033
- .description("Continue an existing session or recover an interrupted run.")
1034
- .argument("<session-id>", "Saved session ID")
1035
- .argument("[instruction...]", "Optional follow-up instruction")
1036
- .option(
1037
- "--mode <name>",
1038
- "ChatGPT mode (Pro/Current), or a provider alias such as kimi",
1039
- )
976
+ .description(t("command.resume"))
977
+ .argument("<session-id>", t("argument.sessionId"))
978
+ .argument("[instruction...]", t("argument.instruction"))
1040
979
  .action(async (sessionId, instruction, _, command) => {
1041
980
  await runResume(
1042
981
  sessionId,
@@ -1047,18 +986,18 @@ program
1047
986
 
1048
987
  program
1049
988
  .command("status")
1050
- .description("List saved sessions or show one session as JSON.")
1051
- .argument("[session-id]", "Saved session ID")
989
+ .description(t("command.status"))
990
+ .argument("[session-id]", t("argument.sessionId"))
1052
991
  .action(async (sessionId, _, command) => {
1053
992
  await runStatus(sessionId, command.optsWithGlobals());
1054
993
  });
1055
994
 
1056
995
  program
1057
996
  .command("export")
1058
- .description("Export a saved session to a Codex or Claude Code session.")
1059
- .argument("<session-id>", "Saved session ID")
1060
- .option("--format <name>", "codex or claude-code", "codex")
1061
- .option("-o, --output <path>", "Write to a file instead of stdout")
997
+ .description(t("command.export"))
998
+ .argument("<session-id>", t("argument.sessionId"))
999
+ .option("--format <name>", t("option.format"), "codex")
1000
+ .option("-o, --output <path>", t("option.output"))
1062
1001
  .action(async (sessionId, options, command) => {
1063
1002
  await runExport(sessionId, { ...command.optsWithGlobals(), ...options });
1064
1003
  });