wtagent 0.3.0 → 1.0.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.
Files changed (43) hide show
  1. package/README.md +163 -9
  2. package/package.json +9 -8
  3. package/src/artifacts/artifact-store.js +154 -0
  4. package/src/audio/music-artifact.js +66 -0
  5. package/src/audio/native-music-receiver.js +91 -0
  6. package/src/browser/base-web-adapter.js +2418 -201
  7. package/src/browser/cdp-browser.js +223 -25
  8. package/src/browser/cdp-state.js +8 -5
  9. package/src/browser/chatgpt-dom.js +179 -0
  10. package/src/browser/chatgpt-web-adapter.js +831 -69
  11. package/src/browser/claude-web-adapter.js +13 -0
  12. package/src/browser/fake-web-model-adapter.js +152 -3
  13. package/src/browser/gemini-web-adapter.js +71 -20
  14. package/src/browser/glm-web-adapter.js +2 -2
  15. package/src/browser/grok-web-adapter.js +35 -3
  16. package/src/browser/rendered-text.js +11 -0
  17. package/src/cli/i18n.js +50 -20
  18. package/src/cli/main.js +124 -40
  19. package/src/cli/prompt-input.js +68 -0
  20. package/src/image/browser-image-driver.js +238 -0
  21. package/src/image/browser-image-session-pool.js +96 -0
  22. package/src/image/image-generation-service.js +101 -0
  23. package/src/image/image-provider-driver.js +22 -0
  24. package/src/image/native-image-receiver.js +66 -0
  25. package/src/image/provider-registry.js +23 -0
  26. package/src/image/providers/chatgpt-image-driver.js +211 -0
  27. package/src/image/providers/gemini-image-driver.js +100 -0
  28. package/src/image/providers/grok-image-driver.js +193 -0
  29. package/src/platform/command-launcher.js +1 -1
  30. package/src/platform/paths.js +19 -0
  31. package/src/platform/windows-diagnostics.js +9 -16
  32. package/src/policy/path-guard.js +5 -1
  33. package/src/policy/policy-engine.js +10 -5
  34. package/src/protocol/envelope.js +74 -0
  35. package/src/protocol/markers.js +16 -0
  36. package/src/protocol/prompt-builder.js +12 -9
  37. package/src/protocol/xml-protocol.js +36 -18
  38. package/src/runtime/agent-runtime.js +1383 -238
  39. package/src/session/agent-session.js +1047 -121
  40. package/src/tools/default-tools.js +1 -1
  41. package/src/tools/image-tools.js +92 -0
  42. package/src/tools/registry.js +13 -1
  43. package/docs/technical-design.md +0 -866
package/src/cli/i18n.js CHANGED
@@ -20,6 +20,7 @@ const MESSAGES = Object.freeze({
20
20
  "option.project": "Project directory",
21
21
  "option.model": "Web AI provider: {providers} (default: {defaultProvider})",
22
22
  "option.once": "Run a single request and exit instead of a conversation",
23
+ "option.type": "Request type: agent (default) or music (Gemini only)",
23
24
  "option.timeout": "Maximum wait for one model response (default: 10 minutes)",
24
25
  "option.noMinimize": "Keep the Chrome window visible instead of minimizing it",
25
26
  "option.debug": "Write browser diagnostics",
@@ -42,7 +43,9 @@ const MESSAGES = Object.freeze({
42
43
  "login.savingProfile": "Closing native Chrome and saving the dedicated profile...",
43
44
  "login.verified": "{provider} login verified through a fresh CDP connection.",
44
45
  "login.stillGuest": "{provider} is still in guest mode. Reopening native Chrome; complete the final {provider} sign-in/continue step.",
45
- "model.chooseInBrowser": "{provider} is signed in. Choose the model you want to use in the browser if desired, then press Enter here. Press Enter immediately to keep the currently selected model.",
46
+ "model.chooseInBrowser": "{provider} is signed in.\nYou can change the model in the browser.\nPress Enter to continue, or wait 5 seconds.",
47
+ "model.continueCountdown": "Continuing in {seconds}s…",
48
+ "model.continuing": "Continuing…",
46
49
  "model.setupCancelled": "Model selection confirmation cancelled.",
47
50
  "logout.none": "No Chrome profile found at {profileDir}; already logged out.",
48
51
  "logout.refuse": "Refusing to delete {profileDir}: it does not look like a wtagent Chrome profile.",
@@ -68,7 +71,7 @@ const MESSAGES = Object.freeze({
68
71
  "session.savedAt": "Session saved at: {directory}",
69
72
  "session.ctrlC": "Ctrl+C — cancelling. Press again to force quit.",
70
73
  "session.cancelledHint": "Turn cancelled. Type a new message or quit.",
71
- "session.recoveryHint": "Type /retry to ask {provider} to continue again, enter a new instruction, or quit",
74
+ "session.recoveryHint": "Type /retry to recover the pending {provider} turn, or quit",
72
75
  "session.processesRunning": "Managed processes still running:",
73
76
  "session.resumeWith": "Resume this conversation with: wtagent resume {sessionId}",
74
77
  "attachment.attaching": "Attaching: {files}",
@@ -106,6 +109,7 @@ const MESSAGES = Object.freeze({
106
109
  "option.project": "项目目录",
107
110
  "option.model": "网页 AI 提供商:{providers}(默认:{defaultProvider})",
108
111
  "option.once": "只执行一次请求并退出,不进入连续对话",
112
+ "option.type": "请求类型:agent(默认)或 music(仅 Gemini)",
109
113
  "option.timeout": "单次模型响应最大等待时间(默认:10 分钟)",
110
114
  "option.noMinimize": "保持 Chrome 窗口可见,不自动最小化",
111
115
  "option.debug": "写入浏览器诊断信息",
@@ -128,7 +132,9 @@ const MESSAGES = Object.freeze({
128
132
  "login.savingProfile": "正在关闭原生 Chrome 并保存专用配置...",
129
133
  "login.verified": "已通过新的 CDP 连接验证 {provider} 登录状态。",
130
134
  "login.stillGuest": "{provider} 仍处于访客模式。正在重新打开 Chrome;请完成最后的登录/继续步骤。",
131
- "model.chooseInBrowser": "{provider} 已登录。如需切换模型,请直接在浏览器网页中人工选择;完成后在此按 Enter。若保持当前已选模型,直接按 Enter 即可。",
135
+ "model.chooseInBrowser": "{provider} 已登录。\n如需切换模型,请在浏览器中选择。\n按 Enter 立即继续,或等待 5 秒自动继续。",
136
+ "model.continueCountdown": "{seconds} 秒后自动继续…",
137
+ "model.continuing": "正在继续…",
132
138
  "model.setupCancelled": "模型选择确认已取消。",
133
139
  "logout.none": "在 {profileDir} 未找到 Chrome 配置;当前已是登出状态。",
134
140
  "logout.refuse": "拒绝删除 {profileDir}:它看起来不是 wtagent 的 Chrome 配置目录。",
@@ -154,7 +160,7 @@ const MESSAGES = Object.freeze({
154
160
  "session.savedAt": "会话已保存到:{directory}",
155
161
  "session.ctrlC": "Ctrl+C — 正在取消。再次按下可强制退出。",
156
162
  "session.cancelledHint": "本轮已取消。请输入新消息或退出。",
157
- "session.recoveryHint": "输入 /retry 让 {provider} 再次继续,或输入新指令,也可以退出",
163
+ "session.recoveryHint": "输入 /retry 恢复待处理的 {provider} 回合,或退出",
158
164
  "session.processesRunning": "仍在运行的托管进程:",
159
165
  "session.resumeWith": "继续此对话:wtagent resume {sessionId}",
160
166
  "attachment.attaching": "正在附加:{files}",
@@ -192,6 +198,7 @@ const MESSAGES = Object.freeze({
192
198
  "option.project": "專案目錄",
193
199
  "option.model": "網頁 AI 提供者:{providers}(預設:{defaultProvider})",
194
200
  "option.once": "只執行一次請求並退出,不進入連續對話",
201
+ "option.type": "請求類型:agent(預設)或 music(僅 Gemini)",
195
202
  "option.timeout": "單次模型回應最大等待時間(預設:10 分鐘)",
196
203
  "option.noMinimize": "保持 Chrome 視窗可見,不自動最小化",
197
204
  "option.debug": "寫入瀏覽器診斷資訊",
@@ -209,7 +216,9 @@ const MESSAGES = Object.freeze({
209
216
  "login.savingProfile": "正在關閉原生 Chrome 並儲存專用設定檔...",
210
217
  "login.verified": "已透過新的 CDP 連線驗證 {provider} 登入狀態。",
211
218
  "login.stillGuest": "{provider} 仍處於訪客模式。正在重新開啟 Chrome;請完成最後的登入/繼續步驟。",
212
- "model.chooseInBrowser": "{provider} 已登入。如需切換模型,請直接在瀏覽器頁面中自行選擇;完成後在此按 Enter。若保持目前已選模型,直接按 Enter 即可。",
219
+ "model.chooseInBrowser": "{provider} 已登入。\n如需切換模型,請在瀏覽器中選擇。\n按 Enter 立即繼續,或等待 5 秒自動繼續。",
220
+ "model.continueCountdown": "{seconds} 秒後自動繼續…",
221
+ "model.continuing": "正在繼續…",
213
222
  "model.setupCancelled": "模型選擇確認已取消。",
214
223
  "task.youPrompt": "你 ›",
215
224
  "option.yes": "略過確認提示",
@@ -241,7 +250,7 @@ const MESSAGES = Object.freeze({
241
250
  "session.savedAt": "工作階段已儲存到:{directory}",
242
251
  "session.ctrlC": "Ctrl+C — 正在取消。再次按下可強制退出。",
243
252
  "session.cancelledHint": "本輪已取消。請輸入新訊息或退出。",
244
- "session.recoveryHint": "輸入 /retry 讓 {provider} 再次繼續,或輸入新指令,也可以退出",
253
+ "session.recoveryHint": "輸入 /retry 恢復待處理的 {provider} 回合,或退出",
245
254
  "session.processesRunning": "仍在執行的受管程序:",
246
255
  "session.resumeWith": "繼續此對話:wtagent resume {sessionId}",
247
256
  "attachment.attaching": "正在附加:{files}",
@@ -278,6 +287,7 @@ const MESSAGES = Object.freeze({
278
287
  "option.project": "プロジェクトディレクトリ",
279
288
  "option.model": "Web AI プロバイダー: {providers}(既定: {defaultProvider})",
280
289
  "option.once": "会話を継続せず、1 回のリクエストを実行して終了",
290
+ "option.type": "リクエストの種類: agent(既定)または music(Gemini のみ)",
281
291
  "option.timeout": "1 回のモデル応答の最大待機時間(既定: 10 分)",
282
292
  "option.noMinimize": "Chrome ウィンドウを最小化せず表示したままにする",
283
293
  "option.debug": "ブラウザー診断を書き出す",
@@ -295,7 +305,9 @@ const MESSAGES = Object.freeze({
295
305
  "login.savingProfile": "ネイティブ Chrome を閉じ、専用プロファイルを保存しています...",
296
306
  "login.verified": "新しい CDP 接続で {provider} のログインを確認しました。",
297
307
  "login.stillGuest": "{provider} はまだゲスト状態です。Chrome を再度開きますので、最後のサインイン/続行手順を完了してください。",
298
- "model.chooseInBrowser": "{provider} にログイン済みです。必要ならブラウザー上で使用するモデルを手動で選択し、ここで Enter を押してください。現在のモデルを使う場合は、そのまま Enter を押します。",
308
+ "model.chooseInBrowser": "{provider} にログイン済みです。\n必要ならブラウザーでモデルを変更できます。\nEnter で続行、または 5 秒後に自動で続行します。",
309
+ "model.continueCountdown": "{seconds} 秒後に続行…",
310
+ "model.continuing": "続行中…",
299
311
  "model.setupCancelled": "モデル選択の確認をキャンセルしました。",
300
312
  "task.youPrompt": "あなた ›",
301
313
  "option.yes": "確認プロンプトを省略",
@@ -327,7 +339,7 @@ const MESSAGES = Object.freeze({
327
339
  "session.savedAt": "セッション保存先: {directory}",
328
340
  "session.ctrlC": "Ctrl+C — キャンセル中です。もう一度押すと強制終了します。",
329
341
  "session.cancelledHint": "このターンはキャンセルされました。新しいメッセージを入力するか終了してください。",
330
- "session.recoveryHint": "/retry で {provider} に再度続行させるか、新しい指示を入力するか、終了してください",
342
+ "session.recoveryHint": "/retry で保留中の {provider} ターンを復旧するか、終了してください",
331
343
  "session.processesRunning": "管理対象プロセスが実行中です:",
332
344
  "session.resumeWith": "この会話を再開: wtagent resume {sessionId}",
333
345
  "attachment.attaching": "添付中: {files}",
@@ -364,6 +376,7 @@ const MESSAGES = Object.freeze({
364
376
  "option.project": "프로젝트 디렉터리",
365
377
  "option.model": "웹 AI 제공자: {providers} (기본값: {defaultProvider})",
366
378
  "option.once": "대화를 계속하지 않고 요청 한 번만 실행한 뒤 종료",
379
+ "option.type": "요청 유형: agent(기본값) 또는 music(Gemini 전용)",
367
380
  "option.timeout": "모델 응답 1회의 최대 대기 시간 (기본값: 10분)",
368
381
  "option.noMinimize": "Chrome 창을 최소화하지 않고 표시",
369
382
  "option.debug": "브라우저 진단 정보 기록",
@@ -381,7 +394,9 @@ const MESSAGES = Object.freeze({
381
394
  "login.savingProfile": "네이티브 Chrome을 닫고 전용 프로필을 저장하는 중...",
382
395
  "login.verified": "새 CDP 연결을 통해 {provider} 로그인을 확인했습니다.",
383
396
  "login.stillGuest": "{provider}가 아직 게스트 모드입니다. Chrome을 다시 열고 마지막 로그인/계속 단계를 완료하세요.",
384
- "model.chooseInBrowser": "{provider}에 로그인되었습니다. 원하는 경우 브라우저에서 사용할 모델을 직접 선택한 뒤 여기서 Enter를 누르세요. 현재 선택된 모델을 유지하려면 바로 Enter를 누르면 됩니다.",
397
+ "model.chooseInBrowser": "{provider}에 로그인되었습니다.\n필요하면 브라우저에서 모델을 변경하세요.\nEnter를 누르거나 5초 후 자동으로 계속합니다.",
398
+ "model.continueCountdown": "{seconds}초 후 계속…",
399
+ "model.continuing": "계속하는 중…",
385
400
  "model.setupCancelled": "모델 선택 확인이 취소되었습니다.",
386
401
  "task.youPrompt": "나 ›",
387
402
  "option.yes": "확인 프롬프트 건너뛰기",
@@ -413,7 +428,7 @@ const MESSAGES = Object.freeze({
413
428
  "session.savedAt": "세션 저장 위치: {directory}",
414
429
  "session.ctrlC": "Ctrl+C — 취소 중입니다. 다시 누르면 강제 종료합니다.",
415
430
  "session.cancelledHint": "이번 턴이 취소되었습니다. 새 메시지를 입력하거나 종료하세요.",
416
- "session.recoveryHint": "/retry로 {provider}에 다시 계속하도록 요청하거나 새 지시를 입력하거나 종료하세요",
431
+ "session.recoveryHint": "/retry로 보류 중인 {provider} 턴을 복구하거나 종료하세요",
417
432
  "session.processesRunning": "관리 중인 프로세스가 실행 중입니다:",
418
433
  "session.resumeWith": "이 대화 재개: wtagent resume {sessionId}",
419
434
  "attachment.attaching": "첨부 중: {files}",
@@ -450,6 +465,7 @@ const MESSAGES = Object.freeze({
450
465
  "option.project": "Directorio del proyecto",
451
466
  "option.model": "Proveedor de IA web: {providers} (predeterminado: {defaultProvider})",
452
467
  "option.once": "Ejecutar una sola solicitud y salir en lugar de mantener una conversación",
468
+ "option.type": "Tipo de solicitud: agent (predeterminado) o music (solo Gemini)",
453
469
  "option.timeout": "Espera máxima por respuesta del modelo (predeterminado: 10 minutos)",
454
470
  "option.noMinimize": "Mantener visible la ventana de Chrome",
455
471
  "option.debug": "Guardar diagnósticos del navegador",
@@ -467,7 +483,9 @@ const MESSAGES = Object.freeze({
467
483
  "login.savingProfile": "Cerrando Chrome y guardando el perfil dedicado...",
468
484
  "login.verified": "Inicio de sesión de {provider} verificado mediante una nueva conexión CDP.",
469
485
  "login.stillGuest": "{provider} sigue en modo invitado. Se volverá a abrir Chrome; completa el último paso de inicio de sesión/continuación.",
470
- "model.chooseInBrowser": "{provider} ha iniciado sesión. Si quieres, selecciona manualmente en el navegador el modelo que deseas usar y después pulsa Enter aquí. Pulsa Enter directamente para conservar el modelo actual.",
486
+ "model.chooseInBrowser": "{provider} ha iniciado sesión.\nPuedes cambiar el modelo en el navegador.\nPulsa Enter para continuar o espera 5 segundos.",
487
+ "model.continueCountdown": "Continuando en {seconds}s…",
488
+ "model.continuing": "Continuando…",
471
489
  "model.setupCancelled": "Se canceló la confirmación de selección del modelo.",
472
490
  "task.youPrompt": "tú ›",
473
491
  "option.yes": "Omitir la confirmación",
@@ -499,7 +517,7 @@ const MESSAGES = Object.freeze({
499
517
  "session.savedAt": "Sesión guardada en: {directory}",
500
518
  "session.ctrlC": "Ctrl+C — cancelando. Pulsa de nuevo para forzar la salida.",
501
519
  "session.cancelledHint": "Turno cancelado. Escribe un nuevo mensaje o sal.",
502
- "session.recoveryHint": "Escribe /retry para pedir a {provider} que continúe de nuevo, introduce otra instrucción o sal",
520
+ "session.recoveryHint": "Escribe /retry para recuperar el turno pendiente de {provider}, o sal",
503
521
  "session.processesRunning": "Procesos administrados aún en ejecución:",
504
522
  "session.resumeWith": "Reanuda esta conversación con: wtagent resume {sessionId}",
505
523
  "attachment.attaching": "Adjuntando: {files}",
@@ -536,6 +554,7 @@ const MESSAGES = Object.freeze({
536
554
  "option.project": "Répertoire du projet",
537
555
  "option.model": "Fournisseur d’IA web : {providers} (par défaut : {defaultProvider})",
538
556
  "option.once": "Exécuter une seule requête puis quitter",
557
+ "option.type": "Type de requête : agent (par défaut) ou music (Gemini uniquement)",
539
558
  "option.timeout": "Attente maximale d’une réponse du modèle (10 minutes par défaut)",
540
559
  "option.noMinimize": "Garder la fenêtre Chrome visible",
541
560
  "option.debug": "Écrire les diagnostics du navigateur",
@@ -553,7 +572,9 @@ const MESSAGES = Object.freeze({
553
572
  "login.savingProfile": "Fermeture de Chrome et enregistrement du profil dédié...",
554
573
  "login.verified": "Connexion à {provider} vérifiée via une nouvelle connexion CDP.",
555
574
  "login.stillGuest": "{provider} est encore en mode invité. Chrome va être rouvert ; terminez la dernière étape de connexion/continuation.",
556
- "model.chooseInBrowser": "{provider} est connecté. Si nécessaire, choisissez manuellement dans le navigateur le modèle à utiliser, puis appuyez ici sur Entrée. Appuyez directement sur Entrée pour conserver le modèle actuel.",
575
+ "model.chooseInBrowser": "{provider} est connecté.\nVous pouvez changer de modèle dans le navigateur.\nAppuyez sur Entrée pour continuer ou attendez 5 secondes.",
576
+ "model.continueCountdown": "Reprise dans {seconds}s…",
577
+ "model.continuing": "Reprise…",
557
578
  "model.setupCancelled": "La confirmation du choix du modèle a été annulée.",
558
579
  "task.youPrompt": "vous ›",
559
580
  "option.yes": "Ignorer la demande de confirmation",
@@ -585,7 +606,7 @@ const MESSAGES = Object.freeze({
585
606
  "session.savedAt": "Session enregistrée dans : {directory}",
586
607
  "session.ctrlC": "Ctrl+C — annulation en cours. Appuyez encore pour forcer l’arrêt.",
587
608
  "session.cancelledHint": "Tour annulé. Saisissez un nouveau message ou quittez.",
588
- "session.recoveryHint": "Saisissez /retry pour demander à {provider} de continuer, entrez une nouvelle instruction ou quittez",
609
+ "session.recoveryHint": "Saisissez /retry pour récupérer le tour {provider} en attente, ou quittez",
589
610
  "session.processesRunning": "Processus gérés encore actifs :",
590
611
  "session.resumeWith": "Reprendre cette conversation avec : wtagent resume {sessionId}",
591
612
  "attachment.attaching": "Ajout de pièces jointes : {files}",
@@ -622,6 +643,7 @@ const MESSAGES = Object.freeze({
622
643
  "option.project": "Projektverzeichnis",
623
644
  "option.model": "Web-AI-Anbieter: {providers} (Standard: {defaultProvider})",
624
645
  "option.once": "Nur eine Anfrage ausführen und danach beenden",
646
+ "option.type": "Anfragetyp: agent (Standard) oder music (nur Gemini)",
625
647
  "option.timeout": "Maximale Wartezeit pro Modellantwort (Standard: 10 Minuten)",
626
648
  "option.noMinimize": "Chrome-Fenster sichtbar lassen",
627
649
  "option.debug": "Browserdiagnose schreiben",
@@ -639,7 +661,9 @@ const MESSAGES = Object.freeze({
639
661
  "login.savingProfile": "Chrome wird geschlossen und das dedizierte Profil gespeichert...",
640
662
  "login.verified": "{provider}-Anmeldung über eine neue CDP-Verbindung bestätigt.",
641
663
  "login.stillGuest": "{provider} befindet sich noch im Gastmodus. Chrome wird erneut geöffnet; schließe den letzten Anmelde-/Fortsetzen-Schritt ab.",
642
- "model.chooseInBrowser": "{provider} ist angemeldet. Wähle bei Bedarf das gewünschte Modell manuell im Browser und drücke danach hier Enter. Drücke direkt Enter, um das aktuell ausgewählte Modell beizubehalten.",
664
+ "model.chooseInBrowser": "{provider} ist angemeldet.\nDu kannst das Modell im Browser ändern.\nDrücke Enter zum Fortfahren oder warte 5 Sekunden.",
665
+ "model.continueCountdown": "Weiter in {seconds}s…",
666
+ "model.continuing": "Weiter…",
643
667
  "model.setupCancelled": "Bestätigung der Modellauswahl abgebrochen.",
644
668
  "task.youPrompt": "du ›",
645
669
  "option.yes": "Bestätigungsabfrage überspringen",
@@ -671,7 +695,7 @@ const MESSAGES = Object.freeze({
671
695
  "session.savedAt": "Sitzung gespeichert unter: {directory}",
672
696
  "session.ctrlC": "Ctrl+C — wird abgebrochen. Erneut drücken, um das Beenden zu erzwingen.",
673
697
  "session.cancelledHint": "Durchgang abgebrochen. Neue Nachricht eingeben oder beenden.",
674
- "session.recoveryHint": "Gib /retry ein, um {provider} erneut fortsetzen zu lassen, gib eine neue Anweisung ein oder beende",
698
+ "session.recoveryHint": "Gib /retry ein, um den ausstehenden {provider}-Durchlauf wiederherzustellen, oder beende",
675
699
  "session.processesRunning": "Verwaltete Prozesse laufen noch:",
676
700
  "session.resumeWith": "Diese Unterhaltung fortsetzen mit: wtagent resume {sessionId}",
677
701
  "attachment.attaching": "Anhängen: {files}",
@@ -708,6 +732,7 @@ const MESSAGES = Object.freeze({
708
732
  "option.project": "Diretório do projeto",
709
733
  "option.model": "Provedor de IA web: {providers} (padrão: {defaultProvider})",
710
734
  "option.once": "Executar apenas uma solicitação e sair",
735
+ "option.type": "Tipo de solicitação: agent (padrão) ou music (somente Gemini)",
711
736
  "option.timeout": "Espera máxima por uma resposta do modelo (padrão: 10 minutos)",
712
737
  "option.noMinimize": "Manter a janela do Chrome visível",
713
738
  "option.debug": "Gravar diagnósticos do navegador",
@@ -725,7 +750,9 @@ const MESSAGES = Object.freeze({
725
750
  "login.savingProfile": "Fechando o Chrome e salvando o perfil dedicado...",
726
751
  "login.verified": "Login de {provider} verificado por uma nova conexão CDP.",
727
752
  "login.stillGuest": "{provider} ainda está no modo visitante. O Chrome será reaberto; conclua a última etapa de login/continuação.",
728
- "model.chooseInBrowser": "{provider} está conectado. Se quiser, escolha manualmente no navegador o modelo a usar e depois pressione Enter aqui. Pressione Enter imediatamente para manter o modelo atual.",
753
+ "model.chooseInBrowser": "{provider} está conectado.\nVocê pode alterar o modelo no navegador.\nPressione Enter para continuar ou aguarde 5 segundos.",
754
+ "model.continueCountdown": "Continuando em {seconds}s…",
755
+ "model.continuing": "Continuando…",
729
756
  "model.setupCancelled": "A confirmação da seleção do modelo foi cancelada.",
730
757
  "task.youPrompt": "você ›",
731
758
  "option.yes": "Ignorar a confirmação",
@@ -757,7 +784,7 @@ const MESSAGES = Object.freeze({
757
784
  "session.savedAt": "Sessão salva em: {directory}",
758
785
  "session.ctrlC": "Ctrl+C — cancelando. Pressione novamente para forçar a saída.",
759
786
  "session.cancelledHint": "Turno cancelado. Digite uma nova mensagem ou saia.",
760
- "session.recoveryHint": "Digite /retry para pedir que {provider} continue novamente, insira uma nova instrução ou saia",
787
+ "session.recoveryHint": "Digite /retry para recuperar o turno pendente do {provider}, ou saia",
761
788
  "session.processesRunning": "Processos gerenciados ainda em execução:",
762
789
  "session.resumeWith": "Retome esta conversa com: wtagent resume {sessionId}",
763
790
  "attachment.attaching": "Anexando: {files}",
@@ -794,6 +821,7 @@ const MESSAGES = Object.freeze({
794
821
  "option.project": "Каталог проекта",
795
822
  "option.model": "Провайдер веб-ИИ: {providers} (по умолчанию: {defaultProvider})",
796
823
  "option.once": "Выполнить один запрос и завершить работу",
824
+ "option.type": "Тип запроса: agent (по умолчанию) или music (только Gemini)",
797
825
  "option.timeout": "Максимальное ожидание ответа модели (по умолчанию: 10 минут)",
798
826
  "option.noMinimize": "Не сворачивать окно Chrome",
799
827
  "option.debug": "Записывать диагностику браузера",
@@ -811,7 +839,9 @@ const MESSAGES = Object.freeze({
811
839
  "login.savingProfile": "Chrome закрывается, отдельный профиль сохраняется...",
812
840
  "login.verified": "Вход в {provider} подтверждён через новое соединение CDP.",
813
841
  "login.stillGuest": "{provider} всё ещё работает в гостевом режиме. Chrome будет открыт снова; завершите последний шаг входа/продолжения.",
814
- "model.chooseInBrowser": "{provider}: вход выполнен. При необходимости вручную выберите нужную модель в браузере, затем нажмите Enter здесь. Чтобы оставить текущую модель, сразу нажмите Enter.",
842
+ "model.chooseInBrowser": "{provider}: вход выполнен.\nПри необходимости смените модель в браузере.\nНажмите Enter или подождите 5 секунд для продолжения.",
843
+ "model.continueCountdown": "Продолжение через {seconds} с…",
844
+ "model.continuing": "Продолжение…",
815
845
  "model.setupCancelled": "Подтверждение выбора модели отменено.",
816
846
  "task.youPrompt": "вы ›",
817
847
  "option.yes": "Пропустить запрос подтверждения",
@@ -843,7 +873,7 @@ const MESSAGES = Object.freeze({
843
873
  "session.savedAt": "Сессия сохранена в: {directory}",
844
874
  "session.ctrlC": "Ctrl+C — отмена. Нажмите ещё раз для принудительного выхода.",
845
875
  "session.cancelledHint": "Ход отменён. Введите новое сообщение или выйдите.",
846
- "session.recoveryHint": "Введите /retry, чтобы снова попросить {provider} продолжить, введите новую инструкцию или выйдите",
876
+ "session.recoveryHint": "Введите /retry, чтобы восстановить ожидающий ход {provider}, или выйдите",
847
877
  "session.processesRunning": "Управляемые процессы всё ещё запущены:",
848
878
  "session.resumeWith": "Продолжить этот диалог: wtagent resume {sessionId}",
849
879
  "attachment.attaching": "Прикрепление: {files}",
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 { fileURLToPath } 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";
@@ -15,6 +16,7 @@ import {
15
16
  } from "../browser/provider-registry.js";
16
17
  import {
17
18
  ensureDirectory,
19
+ formatHomePath,
18
20
  getAppDataDir,
19
21
  getSessionsDir,
20
22
  getTasksDir,
@@ -31,13 +33,17 @@ import { PolicyEngine } from "../policy/policy-engine.js";
31
33
  import { ApprovalStore } from "../policy/approval-store.js";
32
34
  import { createDefaultToolRegistry } from "../tools/default-tools.js";
33
35
  import { ProcessManager } from "../tools/process-manager.js";
36
+ import { NativeImageReceiver } from "../image/native-image-receiver.js";
37
+ import { NativeMusicReceiver, validateGenerationType } from "../audio/native-music-receiver.js";
34
38
  import { resolveLimits } from "../shared/limits.js";
39
+ import { BrowserAdapterError } from "../shared/errors.js";
35
40
  import { EXPORTERS } from "../session/session-export.js";
36
41
  import { getPackageVersion } from "../shared/package-info.js";
37
42
  import { extractAtMentions } from "./at-files.js";
38
43
  import {
39
44
  classifyChatInput,
40
45
  promptForConfirm,
46
+ promptForContinue,
41
47
  promptForText,
42
48
  promptForSelect,
43
49
  readChatMessage,
@@ -238,6 +244,9 @@ class ConversationRunner {
238
244
  // interactive TTY sessions where stdin is available to listen on.
239
245
  cancelOnEsc: interactive,
240
246
  });
247
+ const generationType = validateGenerationType(session.state.generationType, provider.id);
248
+ this.nativeMusicReceiver = generationType === "music" ? new NativeMusicReceiver({ adapter: this.adapter }) : null;
249
+ this.nativeImageReceiver = this.nativeMusicReceiver ? null : NativeImageReceiver.forAdapter(provider.id, this.adapter);
241
250
  this.interrupted = false;
242
251
  this.closed = false;
243
252
  }
@@ -245,6 +254,8 @@ class ConversationRunner {
245
254
  #buildRuntime() {
246
255
  return new AgentRuntime({
247
256
  adapter: this.adapter,
257
+ nativeImageReceiver: this.nativeImageReceiver,
258
+ nativeMusicReceiver: this.nativeMusicReceiver,
248
259
  registry: createDefaultToolRegistry({
249
260
  processManager: this.processManager,
250
261
  limits: this.limits,
@@ -258,10 +269,12 @@ class ConversationRunner {
258
269
  this.renderer.stopSpinner();
259
270
  await adapter.restoreWindow?.();
260
271
  try {
261
- const answer = await promptForText({
272
+ const answer = await promptForContinue({
262
273
  message: t("model.chooseInBrowser", {
263
274
  provider: this.provider.label,
264
275
  }),
276
+ countdown: (seconds) => t(seconds > 0
277
+ ? "model.continueCountdown" : "model.continuing", { seconds }),
265
278
  });
266
279
  if (answer == null) {
267
280
  throw new Error(t("model.setupCancelled"));
@@ -301,13 +314,13 @@ class ConversationRunner {
301
314
  await this.approvalStore.save();
302
315
  console.log(t("approval.savedTool", {
303
316
  tool: toolCall.name,
304
- file: this.approvalStore.filePath,
317
+ file: formatHomePath(this.approvalStore.filePath),
305
318
  }));
306
319
  } else if (choice === "always-all") {
307
320
  this.approvalStore.setAlwaysAllowAll();
308
321
  await this.approvalStore.save();
309
322
  console.log(t("approval.savedAll", {
310
- file: this.approvalStore.filePath,
323
+ file: formatHomePath(this.approvalStore.filePath),
311
324
  }));
312
325
  }
313
326
  return true;
@@ -435,8 +448,14 @@ async function waitForProcessToExit(pid, timeoutMs = 60_000) {
435
448
  // session, ask the user how to proceed instead of failing outright: kill the
436
449
  // other session, or have the user close its Chrome window so that session
437
450
  // fails and releases the profile. Returns true when the turn should be retried.
438
- async function promptToResolveProfileLock(error) {
451
+ export async function promptToResolveProfileLock(error) {
439
452
  const pid = Number(error.details?.pid);
453
+ if (pid === process.pid) {
454
+ throw new Error(
455
+ "The current WTAgent process still holds its browser profile lock. Refusing to terminate itself.",
456
+ { cause: error },
457
+ );
458
+ }
440
459
  console.log(`\n${"\x1b[33m"}${error.message}${"\x1b[0m"}`);
441
460
  const choice = await promptForSelect({
442
461
  message: t("profileLock.how"),
@@ -470,7 +489,7 @@ async function promptToResolveProfileLock(error) {
470
489
  return true;
471
490
  }
472
491
 
473
- async function executeSession({
492
+ export async function executeSession({
474
493
  session,
475
494
  options,
476
495
  resume = false,
@@ -478,11 +497,14 @@ async function executeSession({
478
497
  files = [],
479
498
  chatInput = null,
480
499
  mode = null,
481
- }) {
500
+ }, {
501
+ interactive = !options.once && process.stdin.isTTY && process.stdout.isTTY,
502
+ createRunner = (config) => new ConversationRunner(config),
503
+ readNextMessage = promptForNextMessage,
504
+ } = {}) {
482
505
  const paths = resolveRuntimePaths(options);
483
506
  await ensureDirectory(paths.sessionsDir);
484
- const interactive = !options.once && process.stdin.isTTY && process.stdout.isTTY;
485
- const runner = new ConversationRunner({ session, options, interactive });
507
+ const runner = createRunner({ session, options, interactive });
486
508
 
487
509
  const onInterrupt = () => {
488
510
  if (runner.interrupted) {
@@ -545,9 +567,8 @@ async function executeSession({
545
567
  throw result.error;
546
568
  }
547
569
  runner.renderer.hint(t("session.cancelledHint"));
548
- turnResume = true;
549
- turnInPlaceRecovery = false;
550
- continue;
570
+ // Fall through to the same input prompt as a completed turn. Never
571
+ // resume automatically or reuse the cancelled instruction/attachments.
551
572
  }
552
573
  if (result?.recoveryRequired) {
553
574
  if (!interactive) {
@@ -558,11 +579,30 @@ async function executeSession({
558
579
  provider: runner.renderer.providerLabel,
559
580
  }),
560
581
  );
561
- const next = await promptForNextMessage(runner, activeChatInput);
582
+ const pendingRecovery = session.state.pendingOutbound != null
583
+ || session.state.pendingAssistantTurn != null;
584
+ let next;
585
+ let retryOnly = false;
586
+ for (;;) {
587
+ next = await readNextMessage(runner, activeChatInput, {
588
+ resolveAttachments: !pendingRecovery,
589
+ });
590
+ if (next == null) {
591
+ break;
592
+ }
593
+ retryOnly = next.text.trim().toLowerCase() === "/retry";
594
+ if (!pendingRecovery || retryOnly) {
595
+ break;
596
+ }
597
+ runner.renderer.hint(
598
+ t("session.recoveryHint", {
599
+ provider: runner.renderer.providerLabel,
600
+ }),
601
+ );
602
+ }
562
603
  if (next == null) {
563
604
  break;
564
605
  }
565
- const retryOnly = next.text.trim().toLowerCase() === "/retry";
566
606
  if (!retryOnly) {
567
607
  await session.appendInstruction(next.text, { files: next.files });
568
608
  }
@@ -593,7 +633,7 @@ async function executeSession({
593
633
  }
594
634
  }
595
635
 
596
- const next = await promptForNextMessage(runner, activeChatInput);
636
+ const next = await readNextMessage(runner, activeChatInput);
597
637
  if (next == null) {
598
638
  break;
599
639
  }
@@ -630,7 +670,11 @@ function printResumeHint(sessionId) {
630
670
  // re-prompts. Returns null for an explicit exit command, Ctrl+C, Ctrl+D, or EOF;
631
671
  // otherwise returns
632
672
  // { text, files } where files are resolved @file attachments.
633
- async function promptForNextMessage(runner, chatInput) {
673
+ async function promptForNextMessage(
674
+ runner,
675
+ chatInput,
676
+ { resolveAttachments = true } = {},
677
+ ) {
634
678
  for (;;) {
635
679
  const answer = chatInput
636
680
  ? await chatInput.read()
@@ -651,7 +695,9 @@ async function promptForNextMessage(runner, chatInput) {
651
695
  return null;
652
696
  }
653
697
  const { text } = classified;
654
- const files = await resolveMessageAttachments(runner, text);
698
+ const files = resolveAttachments
699
+ ? await resolveMessageAttachments(runner, text)
700
+ : [];
655
701
  return { text, files };
656
702
  }
657
703
  }
@@ -713,6 +759,8 @@ async function runAgent(taskParts, options) {
713
759
  // Fail fast on an unknown/unsupported --model before any startup work.
714
760
  const provider = resolveProvider(options.model ?? DEFAULT_PROVIDER);
715
761
 
762
+ const generationType = validateGenerationType(options.type, provider.id);
763
+
716
764
  const startup = await maybeRunStartupChecks(options);
717
765
  if (startup === "updated" || startup === "aborted") {
718
766
  return null;
@@ -753,6 +801,7 @@ async function runAgent(taskParts, options) {
753
801
  projectRoot,
754
802
  provider: provider.id,
755
803
  mode: null,
804
+ generationType,
756
805
  });
757
806
 
758
807
  // Resolve @file attachments in the opening task, if any. The task itself is
@@ -782,7 +831,7 @@ function printChatBanner(projectRoot, provider) {
782
831
  const DIM = "\x1b[2m";
783
832
  const RESET = "\x1b[0m";
784
833
  console.log("");
785
- console.log(`${CYAN}WTAgent${RESET} ${DIM}· ${provider.label} · ${projectRoot}${RESET}`);
834
+ console.log(`${CYAN}WTAgent${RESET} ${DIM}· ${provider.label} · ${formatHomePath(projectRoot)}${RESET}`);
786
835
  console.log(`${DIM}${t("banner.controls")}${RESET}`);
787
836
  console.log("");
788
837
  }
@@ -812,8 +861,24 @@ async function runResume(sessionId, instructionParts, options) {
812
861
  await ensureDirectory(paths.sessionsDir);
813
862
  const session = await loadSession(paths, sessionId);
814
863
  await assertDirectory(session.state.projectRoot);
864
+ const instruction = instructionParts.join(" ").trim();
865
+ if (
866
+ instruction
867
+ && (
868
+ session.state.pendingOutbound != null
869
+ || session.state.pendingAssistantTurn != null
870
+ )
871
+ ) {
872
+ throw new BrowserAdapterError(
873
+ "This session has a pending browser handoff. Resume it without an instruction or attachment first.",
874
+ { code: "RECOVERY_REQUIRES_BARE_RESUME", recoverable: false },
875
+ );
876
+ }
815
877
 
816
878
  const provider = resolveProvider(session.state.provider ?? DEFAULT_PROVIDER);
879
+ if (options.type != null && validateGenerationType(options.type, provider.id) !== (session.state.generationType ?? "agent")) {
880
+ throw new Error("--type cannot change the generation type of an existing session.");
881
+ }
817
882
  // A conversation belongs to one provider. --model on resume is only allowed
818
883
  // if it names the same provider; switching mid-conversation is rejected.
819
884
  if (options.model != null && getProvider(options.model).id !== provider.id) {
@@ -828,7 +893,6 @@ async function runResume(sessionId, instructionParts, options) {
828
893
  return null;
829
894
  }
830
895
 
831
- const instruction = instructionParts.join(" ").trim();
832
896
  let files = [];
833
897
  if (instruction) {
834
898
  const projectRoot = session.state.projectRoot;
@@ -940,6 +1004,7 @@ const program = new Command()
940
1004
  }),
941
1005
  )
942
1006
  .option("--once", t("option.once"), false)
1007
+ .option("--type <type>", t("option.type"))
943
1008
  .option("--model-turn-timeout-ms <milliseconds>", t("option.timeout"))
944
1009
  .option("--no-minimize", t("option.noMinimize"))
945
1010
  .option("--debug", t("option.debug"), false)
@@ -1002,26 +1067,45 @@ program
1002
1067
  await runExport(sessionId, { ...command.optsWithGlobals(), ...options });
1003
1068
  });
1004
1069
 
1005
- program.parseAsync().catch((error) => {
1006
- // Expected, actionable failures carry a plain message instead of a stack
1007
- // trace (e.g. the provider's Chrome profile is locked by another session).
1008
- if (error?.code === "PROFILE_LOCKED") {
1009
- console.error(`Error: ${error.message}`);
1070
+ // Resolve npm's executable symlink as well as direct `node main.js` invocations.
1071
+ // Importing the session runner in tests must not start the CLI.
1072
+ const entryPath = process.argv[1]
1073
+ ? await fs.realpath(process.argv[1]).catch(() => null)
1074
+ : null;
1075
+ const modulePath = await fs.realpath(fileURLToPath(import.meta.url)).catch(() => null);
1076
+ // npm's Windows shim can use a short path or differently cased path. Compare
1077
+ // canonical filesystem paths rather than independently encoded file URLs.
1078
+ const isEntryPoint = entryPath && modulePath && (process.platform === "win32"
1079
+ ? entryPath.toLowerCase() === modulePath.toLowerCase()
1080
+ : entryPath === modulePath);
1081
+ if (isEntryPoint) {
1082
+ await program.parseAsync().catch((error) => {
1083
+ // Expected, actionable failures carry a plain message instead of a stack
1084
+ // trace (e.g. the provider's Chrome profile is locked by another session).
1085
+ if (error?.code === "PROFILE_LOCKED") {
1086
+ console.error(`Error: ${error.message}`);
1087
+ process.exitCode = 1;
1088
+ return;
1089
+ }
1090
+ console.error(error.stack ?? error.message);
1091
+ if (error.details?.diagnosticFile) {
1092
+ const dom = error.details.dom;
1093
+ const explanation = dom?.issues?.length ? dom.issues.join(", ") : dom?.status ?? "unavailable";
1094
+ console.error(`Page compatibility: ${explanation}`);
1095
+ console.error(`Diagnostic report: ${error.details.diagnosticFile}`);
1096
+ }
1010
1097
  process.exitCode = 1;
1011
- return;
1012
- }
1013
- console.error(error.stack ?? error.message);
1014
- process.exitCode = 1;
1015
- }).finally(() => {
1016
- // By the time every command finishes, all per-command cleanup (browser
1017
- // detach/close, stdin raw-mode restore, process stop) has already run. A
1018
- // stray tty handle (e.g. a keypress-machinery listener left behind on a
1019
- // failed turn) can still pin the event loop, leaving a dead prompt that
1020
- // never returns to the shell — exit hard instead of relying on the loop
1021
- // to drain naturally.
1022
- if (process.exitCode != null && process.exitCode !== 0) {
1023
- process.stdin.setRawMode?.(false);
1024
- process.stdin.pause?.();
1025
- process.exit(process.exitCode);
1026
- }
1027
- });
1098
+ }).finally(() => {
1099
+ // By the time every command finishes, all per-command cleanup (browser
1100
+ // detach/close, stdin raw-mode restore, process stop) has already run. A
1101
+ // stray tty handle (e.g. a keypress-machinery listener left behind on a
1102
+ // failed turn) can still pin the event loop, leaving a dead prompt that
1103
+ // never returns to the shell — exit hard instead of relying on the loop
1104
+ // to drain naturally.
1105
+ if (process.exitCode != null && process.exitCode !== 0) {
1106
+ process.stdin.setRawMode?.(false);
1107
+ process.stdin.pause?.();
1108
+ process.exit(process.exitCode);
1109
+ }
1110
+ });
1111
+ }