cc-viewer 1.7.2 → 1.7.4

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 (98) hide show
  1. package/cli.js +109 -20
  2. package/dist/assets/App-CKvCavmF.js +2 -0
  3. package/dist/assets/App-Dj37fluO.css +1 -0
  4. package/dist/assets/{MdxEditorPanel-BUubiE6Y.js → MdxEditorPanel-DeSJp_ct.js} +1 -1
  5. package/dist/assets/{Mobile-CKmfRRvo.js → Mobile-CWou-jtF.js} +1 -1
  6. package/dist/assets/{_baseUniq-D9_E5ZDp.js → _baseUniq-BK7Nhp_x.js} +1 -1
  7. package/dist/assets/{arc-D4iFrlhb.js → arc-Cm4HMXz6.js} +1 -1
  8. package/dist/assets/{architectureDiagram-Q4EWVU46-f1neo7hg.js → architectureDiagram-Q4EWVU46-Co8QCowN.js} +1 -1
  9. package/dist/assets/{blockDiagram-DXYQGD6D-DWi9e9tG.js → blockDiagram-DXYQGD6D-Cudecf7x.js} +1 -1
  10. package/dist/assets/{c4Diagram-AHTNJAMY-DAdj49A2.js → c4Diagram-AHTNJAMY-4JeFoCHX.js} +1 -1
  11. package/dist/assets/{channel-B3VthT0m.js → channel-K7eT7Hjr.js} +1 -1
  12. package/dist/assets/{chunk-4BX2VUAB-BrPcAHV3.js → chunk-4BX2VUAB-BBjl6EVW.js} +1 -1
  13. package/dist/assets/{chunk-4TB4RGXK-Bvopc1V9.js → chunk-4TB4RGXK-DBP9YnwO.js} +1 -1
  14. package/dist/assets/{chunk-55IACEB6-Juw-7Z3i.js → chunk-55IACEB6--A_i9gwR.js} +1 -1
  15. package/dist/assets/{chunk-EDXVE4YY-DVYP9tpB.js → chunk-EDXVE4YY-UBsL-3qr.js} +1 -1
  16. package/dist/assets/{chunk-FMBD7UC4-VQ7VOXl3.js → chunk-FMBD7UC4-DBO0asmI.js} +1 -1
  17. package/dist/assets/{chunk-OYMX7WX6-F8Rj4ZUZ.js → chunk-OYMX7WX6-DOCcEaE6.js} +1 -1
  18. package/dist/assets/{chunk-QZHKN3VN-Dn7K3yi9.js → chunk-QZHKN3VN-B-j8QbQh.js} +1 -1
  19. package/dist/assets/{chunk-YZCP3GAM-UjO-KTsn.js → chunk-YZCP3GAM-B0WO1U-U.js} +1 -1
  20. package/dist/assets/classDiagram-6PBFFD2Q-Ba3tOmih.js +1 -0
  21. package/dist/assets/classDiagram-v2-HSJHXN6E-Ba3tOmih.js +1 -0
  22. package/dist/assets/clone-DjURvTeN.js +1 -0
  23. package/dist/assets/{cose-bilkent-S5V4N54A-BDhNj8rq.js → cose-bilkent-S5V4N54A-DQgpss_a.js} +1 -1
  24. package/dist/assets/{dagre-KV5264BT-BzdcDuyt.js → dagre-KV5264BT-CCJBKmKv.js} +1 -1
  25. package/dist/assets/{diagram-5BDNPKRD-DP_SX65N.js → diagram-5BDNPKRD-DgUcnfkU.js} +1 -1
  26. package/dist/assets/{diagram-G4DWMVQ6-B-pBFw8Q.js → diagram-G4DWMVQ6-CRpVvexw.js} +1 -1
  27. package/dist/assets/{diagram-MMDJMWI5-6XSfN1eN.js → diagram-MMDJMWI5-C3IhpsM-.js} +1 -1
  28. package/dist/assets/{diagram-TYMM5635-DJvGxvpE.js → diagram-TYMM5635-BaVbsrwt.js} +1 -1
  29. package/dist/assets/{erDiagram-SMLLAGMA-CpqdpGYj.js → erDiagram-SMLLAGMA-BxRoBUR8.js} +1 -1
  30. package/dist/assets/{flowDiagram-DWJPFMVM-C9m7wjwI.js → flowDiagram-DWJPFMVM-Du2lIzoM.js} +1 -1
  31. package/dist/assets/{ganttDiagram-T4ZO3ILL-CU0XOfY3.js → ganttDiagram-T4ZO3ILL-DR1pDqLK.js} +1 -1
  32. package/dist/assets/{gitGraphDiagram-UUTBAWPF-ipyGULnP.js → gitGraphDiagram-UUTBAWPF-DHAmE20O.js} +1 -1
  33. package/dist/assets/{graph-BR2KmES9.js → graph-D8Yv0IU1.js} +1 -1
  34. package/dist/assets/{index-C-be29ey.js → index-AOmFG_I_.js} +1 -1
  35. package/dist/assets/{index-D5tSeMU7.js → index-B6ZWIxY0.js} +1 -1
  36. package/dist/assets/{index-D5YruOKD.js → index-Bq95HjYM.js} +1 -1
  37. package/dist/assets/{index--wgRW_OB.js → index-D0pvxpcl.js} +1 -1
  38. package/dist/assets/{index-DuZQ6FqR.js → index-D2c77__8.js} +1 -1
  39. package/dist/assets/{index-C16fiVhv.js → index-DgtUyP57.js} +1 -1
  40. package/dist/assets/{index-CjsQEQc2.js → index-Dpu92rw_.js} +2 -2
  41. package/dist/assets/{index-OWzSX2T8.js → index-cCB-lhEO.js} +1 -1
  42. package/dist/assets/{infoDiagram-42DDH7IO-BQWQjfFX.js → infoDiagram-42DDH7IO-BAqRIm2G.js} +1 -1
  43. package/dist/assets/{ishikawaDiagram-UXIWVN3A-Df-KH_Hz.js → ishikawaDiagram-UXIWVN3A-CzH4tezb.js} +1 -1
  44. package/dist/assets/{journeyDiagram-VCZTEJTY-CMbB_4G9.js → journeyDiagram-VCZTEJTY-Qs4eiyuI.js} +1 -1
  45. package/dist/assets/{jszip.min-B6k2t9Ch.js → jszip.min-C0OccDsk.js} +1 -1
  46. package/dist/assets/{kanban-definition-6JOO6SKY-CImdlKN3.js → kanban-definition-6JOO6SKY-BAjl0gpV.js} +1 -1
  47. package/dist/assets/{layout-DQl4aw09.js → layout-Cf6cd9xE.js} +1 -1
  48. package/dist/assets/{linear-CLD3aPlK.js → linear-PzdCkji6.js} +1 -1
  49. package/dist/assets/mermaid.core-DV9745Lq.js +7 -0
  50. package/dist/assets/{min-_D86henJ.js → min-CHgWxbzS.js} +1 -1
  51. package/dist/assets/{mindmap-definition-QFDTVHPH-2JVcu1nk.js → mindmap-definition-QFDTVHPH-CHUjMdpi.js} +1 -1
  52. package/dist/assets/{pieDiagram-DEJITSTG-Dik-m5u7.js → pieDiagram-DEJITSTG-D_r2QC0f.js} +1 -1
  53. package/dist/assets/{quadrantDiagram-34T5L4WZ-Bm5l40Jq.js → quadrantDiagram-34T5L4WZ-DPzGygdN.js} +1 -1
  54. package/dist/assets/{requirementDiagram-MS252O5E-Ck9w5rTj.js → requirementDiagram-MS252O5E-BA2ynnGb.js} +1 -1
  55. package/dist/assets/{sankeyDiagram-XADWPNL6-BbGVCFEf.js → sankeyDiagram-XADWPNL6-XHVvXuqU.js} +1 -1
  56. package/dist/assets/{seqResourceLoaders-DFrHmgnK.css → seqResourceLoaders-0RXZfUKp.css} +1 -1
  57. package/dist/assets/seqResourceLoaders-kBEzcKLh.js +2 -0
  58. package/dist/assets/{sequenceDiagram-FGHM5R23-vjvEdksk.js → sequenceDiagram-FGHM5R23-C47sA8YH.js} +1 -1
  59. package/dist/assets/{stateDiagram-FHFEXIEX-DTqMoiB2.js → stateDiagram-FHFEXIEX-BOmer839.js} +1 -1
  60. package/dist/assets/{stateDiagram-v2-QKLJ7IA2-CNbAwnhu.js → stateDiagram-v2-QKLJ7IA2-D-kVvTHA.js} +1 -1
  61. package/dist/assets/{timeline-definition-GMOUNBTQ-Cw8O-52d.js → timeline-definition-GMOUNBTQ-CTAKVZgs.js} +1 -1
  62. package/dist/assets/{vendor-antd-DI7JL-mE.js → vendor-antd-BuJ6oz45.js} +2 -2
  63. package/dist/assets/{vendor-codemirror-B9c49dtM.js → vendor-codemirror-D2KAW4ty.js} +1 -1
  64. package/dist/assets/{vendor-mdxeditor-B6cpBtIE.js → vendor-mdxeditor-BsFVVUcj.js} +2 -2
  65. package/dist/assets/{vendor-qrcode-Dn90-s-u.js → vendor-qrcode-BpxSq04x.js} +1 -1
  66. package/dist/assets/{vendor-virtuoso-CpdSKD_6.js → vendor-virtuoso-an4WvAvS.js} +1 -1
  67. package/dist/assets/{vennDiagram-DHZGUBPP-DCuqymb7.js → vennDiagram-DHZGUBPP-CuXzoroh.js} +1 -1
  68. package/dist/assets/{wardley-RL74JXVD-BJ20wJUU.js → wardley-RL74JXVD-CFCmxnck.js} +1 -1
  69. package/dist/assets/{wardleyDiagram-NUSXRM2D-Vz6US3qt.js → wardleyDiagram-NUSXRM2D-gfhDB9SR.js} +1 -1
  70. package/dist/assets/{xychartDiagram-5P7HB3ND-CzkLKrmi.js → xychartDiagram-5P7HB3ND-X-pj66qI.js} +1 -1
  71. package/dist/index.html +4 -4
  72. package/package.json +1 -1
  73. package/server/i18n.js +120 -55
  74. package/server/interceptor.js +72 -2
  75. package/server/lib/ccswitch-import.js +251 -0
  76. package/server/lib/ensure-hooks.js +28 -2
  77. package/server/lib/log-watcher.js +8 -0
  78. package/server/lib/pid-alive.js +15 -0
  79. package/server/lib/proxy-retry.js +716 -0
  80. package/server/lib/session-start-bridge.js +121 -0
  81. package/server/lib/settings-merge.js +170 -0
  82. package/server/lib/v2/live-feed.js +189 -48
  83. package/server/lib/v2/session-owner.js +135 -0
  84. package/server/lib/v2/session-select.js +15 -2
  85. package/server/lib/v2/v2-writer.js +181 -6
  86. package/server/proxy.js +115 -11
  87. package/server/pty-manager.js +28 -4
  88. package/server/routes/events.js +44 -0
  89. package/server/routes/preferences.js +184 -1
  90. package/server/server.js +15 -1
  91. package/src/utils/contentFilter.js +17 -1
  92. package/dist/assets/App-B224r-8M.js +0 -2
  93. package/dist/assets/App-dYPa5-df.css +0 -1
  94. package/dist/assets/classDiagram-6PBFFD2Q-DmSozxoO.js +0 -1
  95. package/dist/assets/classDiagram-v2-HSJHXN6E-DmSozxoO.js +0 -1
  96. package/dist/assets/clone-BQtvy9Ae.js +0 -1
  97. package/dist/assets/mermaid.core-CN35dvKJ.js +0 -7
  98. package/dist/assets/seqResourceLoaders-C-T_EMiO.js +0 -2
package/server/i18n.js CHANGED
@@ -142,6 +142,26 @@ const i18nData = {
142
142
  "tr": " PATH'te de \"claude\" native komutu bulunamadı. @anthropic-ai/claude-code'un yüklü olduğundan emin olun",
143
143
  "uk": " Також не знайдено нативну команду \"claude\" в PATH. Переконайтеся, що @anthropic-ai/claude-code встановлено"
144
144
  },
145
+ "cli.settingsMergeFailed": {
146
+ "zh": "⚠️ 无法加载用户 --settings 值 {value}({reason}),已仅以 CC Viewer 注入的 settings 启动",
147
+ "en": "⚠️ Could not load user --settings value {value} ({reason}); launching with CC Viewer's injected settings only",
148
+ "zh-TW": "⚠️ 無法載入使用者 --settings 值 {value}({reason}),已僅以 CC Viewer 注入的 settings 啟動",
149
+ "ko": "⚠️ 사용자 --settings 값 {value}을(를) 로드할 수 없음({reason}), CC Viewer가 주입한 settings만으로 시작합니다",
150
+ "ja": "⚠️ ユーザーの --settings 値 {value} を読み込めません({reason})。CC Viewer が注入した settings のみで起動します",
151
+ "de": "⚠️ Der --settings-Wert des Benutzers {value} konnte nicht geladen werden ({reason}); Start nur mit den von CC Viewer injizierten Settings",
152
+ "es": "⚠️ No se pudo cargar el valor --settings del usuario {value} ({reason}); se inicia solo con los settings inyectados por CC Viewer",
153
+ "fr": "⚠️ Impossible de charger la valeur --settings de l'utilisateur {value} ({reason}) ; lancement avec uniquement les settings injectés par CC Viewer",
154
+ "it": "⚠️ Impossibile caricare il valore --settings dell'utente {value} ({reason}); avvio solo con i settings iniettati da CC Viewer",
155
+ "da": "⚠️ Kunne ikke indlæse brugerens --settings-værdi {value} ({reason}); starter kun med CC Viewers injicerede settings",
156
+ "pl": "⚠️ Nie można wczytać wartości --settings użytkownika {value} ({reason}); uruchamianie tylko z ustawieniami wstrzykniętymi przez CC Viewer",
157
+ "ru": "⚠️ Не удалось загрузить пользовательское значение --settings {value} ({reason}); запуск только с настройками, внедрёнными CC Viewer",
158
+ "ar": "⚠️ تعذّر تحميل قيمة --settings الخاصة بالمستخدم {value} ({reason})؛ سيتم التشغيل بإعدادات CC Viewer المحقونة فقط",
159
+ "no": "⚠️ Kunne ikke laste brukerens --settings-verdi {value} ({reason}); starter kun med CC Viewers injiserte settings",
160
+ "pt-BR": "⚠️ Não foi possível carregar o valor --settings do usuário {value} ({reason}); iniciando apenas com os settings injetados pelo CC Viewer",
161
+ "th": "⚠️ ไม่สามารถโหลดค่า --settings ของผู้ใช้ {value} ({reason}) จะเริ่มด้วย settings ที่ CC Viewer ฉีดเข้าไปเท่านั้น",
162
+ "tr": "⚠️ Kullanıcının --settings değeri {value} yüklenemedi ({reason}); yalnızca CC Viewer'ın enjekte ettiği settings ile başlatılıyor",
163
+ "uk": "⚠️ Не вдалося завантажити користувацьке значення --settings {value} ({reason}); запуск лише з налаштуваннями, впровадженими CC Viewer"
164
+ },
145
165
  "cli.inject.notFound": {
146
166
  "zh": "❌ 找不到 Claude Code cli.js: {path}",
147
167
  "en": "❌ Claude Code cli.js not found: {path}",
@@ -368,64 +388,104 @@ const i18nData = {
368
388
  "uk": "✅ Очищення в {path} не потрібне"
369
389
  },
370
390
  "cli.uninstall.hookFail": {
371
- "zh": "❌ shell hook 清理失败: {error}",
372
- "en": "❌ Failed to clean shell hook: {error}",
373
- "zh-TW": "❌ shell hook 清理失敗: {error}",
374
- "ko": "❌ shell hook 정리 실패: {error}",
375
- "ja": "❌ shell hook のクリーンアップに失敗: {error}",
376
- "de": "❌ Shell-Hook-Bereinigung fehlgeschlagen: {error}",
377
- "es": "❌ Error al limpiar el shell hook: {error}",
378
- "fr": "❌ Échec du nettoyage du shell hook : {error}",
379
- "it": "❌ Pulizia shell hook fallita: {error}",
380
- "da": "❌ Kunne ikke rense shell hook: {error}",
381
- "pl": "❌ Nie udało się wyczyścić shell hook: {error}",
382
- "ru": "❌ Не удалось очистить shell hook: {error}",
383
- "ar": "❌ فشل تنظيف shell hook: {error}",
384
- "no": "❌ Kunne ikke rense shell hook: {error}",
385
- "pt-BR": "❌ Falha ao limpar shell hook: {error}",
386
- "th": "❌ ล้าง shell hook ล้มเหลว: {error}",
387
- "tr": "❌ Shell hook temizlenemedi: {error}",
388
- "uk": "❌ Не вдалося очистити shell hook: {error}"
391
+ "zh": "❌ shell hook 清理失败 ({file}): {error}",
392
+ "en": "❌ Failed to clean shell hook in {file}: {error}",
393
+ "zh-TW": "❌ shell hook 清理失敗 ({file}): {error}",
394
+ "ko": "❌ {file}의 shell hook 정리 실패: {error}",
395
+ "ja": "❌ {file} の shell hook のクリーンアップに失敗: {error}",
396
+ "de": "❌ Shell-Hook-Bereinigung in {file} fehlgeschlagen: {error}",
397
+ "es": "❌ Error al limpiar el shell hook en {file}: {error}",
398
+ "fr": "❌ Échec du nettoyage du shell hook dans {file} : {error}",
399
+ "it": "❌ Pulizia shell hook in {file} fallita: {error}",
400
+ "da": "❌ Kunne ikke rense shell hook i {file}: {error}",
401
+ "pl": "❌ Nie udało się wyczyścić shell hook w {file}: {error}",
402
+ "ru": "❌ Не удалось очистить shell hook в {file}: {error}",
403
+ "ar": "❌ فشل تنظيف shell hook في {file}: {error}",
404
+ "no": "❌ Kunne ikke rense shell hook i {file}: {error}",
405
+ "pt-BR": "❌ Falha ao limpar shell hook em {file}: {error}",
406
+ "th": "❌ ล้าง shell hook ใน {file} ล้มเหลว: {error}",
407
+ "tr": "❌ {file} içindeki shell hook temizlenemedi: {error}",
408
+ "uk": "❌ Не вдалося очистити shell hook у {file}: {error}"
409
+ },
410
+ "cli.uninstall.corruptBlock": {
411
+ "zh": "⚠️ {file} 中的 CC-Viewer 块似乎已损坏(缺少结束标记),请手动移除",
412
+ "en": "⚠️ CC-Viewer block in {file} looks damaged (missing end marker) — please remove it manually",
413
+ "zh-TW": "⚠️ {file} 中的 CC-Viewer 區塊似乎已損壞(缺少結束標記),請手動移除",
414
+ "ko": "⚠️ {file}의 CC-Viewer 블록이 손상된 것 같습니다(종료 마커 누락) — 수동으로 제거해 주세요",
415
+ "ja": "⚠️ {file} の CC-Viewer ブロックが破損しているようです(終了マーカーが欠落)— 手動で削除してください",
416
+ "de": "⚠️ Der CC-Viewer-Block in {file} scheint beschädigt (Endmarkierung fehlt) — bitte manuell entfernen",
417
+ "es": "⚠️ El bloque CC-Viewer en {file} parece dañado (falta el marcador de fin) — elimínelo manualmente",
418
+ "fr": "⚠️ Le bloc CC-Viewer dans {file} semble endommagé (marqueur de fin manquant) — veuillez le supprimer manuellement",
419
+ "it": "⚠️ Il blocco CC-Viewer in {file} sembra danneggiato (marcatore di fine mancante) — rimuovilo manualmente",
420
+ "da": "⚠️ CC-Viewer-blokken i {file} ser beskadiget ud (slutmarkør mangler) — fjern den manuelt",
421
+ "pl": "⚠️ Blok CC-Viewer w {file} wygląda na uszkodzony (brak znacznika końca) — usuń go ręcznie",
422
+ "ru": "⚠️ Блок CC-Viewer в {file} повреждён (отсутствует конечный маркер) — удалите его вручную",
423
+ "ar": "⚠️ يبدو أن كتلة CC-Viewer في {file} تالفة (علامة النهاية مفقودة) — يرجى إزالتها يدويًا",
424
+ "no": "⚠️ CC-Viewer-blokken i {file} ser skadet ut (sluttmarkør mangler) — fjern den manuelt",
425
+ "pt-BR": "⚠️ O bloco CC-Viewer em {file} parece danificado (marcador final ausente) — remova-o manualmente",
426
+ "th": "⚠️ บล็อก CC-Viewer ใน {file} ดูเหมือนเสียหาย (ไม่มีเครื่องหมายสิ้นสุด) — โปรดลบด้วยตนเอง",
427
+ "tr": "⚠️ {file} içindeki CC-Viewer bloğu hasarlı görünüyor (bitiş işareti eksik) — lütfen elle kaldırın",
428
+ "uk": "⚠️ Блок CC-Viewer у {file} виглядає пошкодженим (відсутній кінцевий маркер) — видаліть його вручну"
429
+ },
430
+ "cli.logger.justUninstalled": {
431
+ "zh": "💡 cc-viewer 刚刚被卸载——已跳过安装。运行 'ccv -logger --force' 可重新安装集成",
432
+ "en": "💡 cc-viewer was just uninstalled — skipping install. Run 'ccv -logger --force' to reinstall the integration",
433
+ "zh-TW": "💡 cc-viewer 剛剛被卸載——已跳過安裝。運行 'ccv -logger --force' 可重新安裝集成",
434
+ "ko": "💡 cc-viewer가 방금 제거되어 설치를 건너뜁니다. 통합을 다시 설치하려면 'ccv -logger --force'를 실행하세요",
435
+ "ja": "💡 cc-viewer はアンインストールされたばかりのためインストールをスキップしました。統合を再インストールするには 'ccv -logger --force' を実行してください",
436
+ "de": "💡 cc-viewer wurde gerade deinstalliert — Installation übersprungen. Führen Sie 'ccv -logger --force' aus, um die Integration erneut zu installieren",
437
+ "es": "💡 cc-viewer se acaba de desinstalar — instalación omitida. Ejecute 'ccv -logger --force' para reinstalar la integración",
438
+ "fr": "💡 cc-viewer vient d'être désinstallé — installation ignorée. Exécutez 'ccv -logger --force' pour réinstaller l'intégration",
439
+ "it": "💡 cc-viewer è stato appena disinstallato — installazione saltata. Esegui 'ccv -logger --force' per reinstallare l'integrazione",
440
+ "da": "💡 cc-viewer blev lige afinstalleret — installation sprunget over. Kør 'ccv -logger --force' for at geninstallere integrationen",
441
+ "pl": "💡 cc-viewer został właśnie odinstalowany — instalacja pominięta. Uruchom 'ccv -logger --force', aby ponownie zainstalować integrację",
442
+ "ru": "💡 cc-viewer был только что удалён — установка пропущена. Запустите 'ccv -logger --force' для повторной установки интеграции",
443
+ "ar": "💡 تم إلغاء تثبيت cc-viewer للتو — تم تخطي التثبيت. قم بتشغيل 'ccv -logger --force' لإعادة تثبيت التكامل",
444
+ "no": "💡 cc-viewer ble nettopp avinstallert — installasjon hoppet over. Kjør 'ccv -logger --force' for å installere integrasjonen på nytt",
445
+ "pt-BR": "💡 cc-viewer acabou de ser desinstalado — instalação ignorada. Execute 'ccv -logger --force' para reinstalar a integração",
446
+ "th": "💡 cc-viewer เพิ่งถูกถอนการติดตั้ง — ข้ามการติดตั้ง รัน 'ccv -logger --force' เพื่อติดตั้งการรวมอีกครั้ง",
447
+ "tr": "💡 cc-viewer az önce kaldırıldı — kurulum atlandı. Entegrasyonu yeniden kurmak için 'ccv -logger --force' çalıştırın",
448
+ "uk": "💡 cc-viewer щойно було видалено — встановлення пропущено. Запустіть 'ccv -logger --force', щоб повторно встановити інтеграцію"
389
449
  },
390
450
  "cli.uninstall.reloadShell": {
391
- "zh": "💡 运行 'unset -f claude' 或重启终端以在当前会话中立即生效",
392
- "en": "💡 Run 'unset -f claude' or restart your terminal to apply changes in the current session",
393
- "zh-TW": "💡 運行 'unset -f claude' 或重啟終端以在當前會話中立即生效",
394
- "ko": "💡 현재 세션에 즉시 적용하려면 'unset -f claude'를 실행하거나 터미널을 재시작하세요",
395
- "ja": "💡 現在のセッションに即座に反映するには 'unset -f claude' を実行するか、ターミナルを再起動してください",
396
- "de": "💡 Führen Sie 'unset -f claude' aus oder starten Sie das Terminal neu, um die Änderungen sofort anzuwenden",
397
- "es": "💡 Ejecute 'unset -f claude' o reinicie su terminal para aplicar los cambios en la sesión actual",
398
- "fr": "💡 Exécutez 'unset -f claude' ou redémarrez votre terminal pour appliquer les changements dans la session actuelle",
399
- "it": "💡 Esegui 'unset -f claude' o riavvia il terminale per applicare le modifiche nella sessione corrente",
400
- "da": "💡 Kør 'unset -f claude' eller genstart terminalen for at anvende ændringer i den aktuelle session",
401
- "pl": "💡 Uruchom 'unset -f claude' lub zrestartuj terminal, aby zastosować zmiany w bieżącej sesji",
402
- "ru": "💡 Выполните 'unset -f claude' или перезапустите терминал для применения изменений в текущей сессии",
403
- "ar": "💡 قم بتشغيل 'unset -f claude' أو أعد تشغيل الطرفية لتطبيق التغييرات في الجلسة الحالية",
404
- "no": "💡 Kjør 'unset -f claude' eller start terminalen nytt for å anvende endringer i gjeldende økt",
405
- "pt-BR": "💡 Execute 'unset -f claude' ou reinicie o terminal para aplicar as alterações na sessão atual",
406
- "th": "💡 รัน 'unset -f claude' หรือรีสตาร์ทเทอร์มินัลเพื่อใช้การเปลี่ยนแปลงในเซสชันปัจจุบัน",
407
- "tr": "💡 Mevcut oturumda değişiklikleri uygulamak için 'unset -f claude' çalıştırın veya terminali yeniden başlatın",
408
- "uk": "💡 Виконайте 'unset -f claude' або перезапустіть термінал, щоб застосувати зміни в поточній сесії"
451
+ "zh": "💡 在需要使用 claude 的每个终端里运行 'unset -f claude'——或直接打开新终端('exec $SHELL' 也可以)",
452
+ "en": "💡 In each terminal where you'll use claude, run 'unset -f claude' or just open a new terminal ('exec $SHELL' also works)",
453
+ "zh-TW": "💡 在需要使用 claude 的每個終端裡運行 'unset -f claude'——或直接打開新終端('exec $SHELL' 也可以)",
454
+ "ko": "💡 claude를 사용할 터미널에서 'unset -f claude'를 실행하세요 — 또는 새 터미널을 여세요('exec $SHELL'도 가능)",
455
+ "ja": "💡 claude を使う各ターミナルで 'unset -f claude' を実行してください — 新しいターミナルを開いても構いません('exec $SHELL' でも可)",
456
+ "de": "💡 Führen Sie in jedem Terminal, in dem Sie claude verwenden, 'unset -f claude' aus oder öffnen Sie einfach ein neues Terminal ('exec $SHELL' geht auch)",
457
+ "es": "💡 En cada terminal donde vaya a usar claude, ejecute 'unset -f claude' o simplemente abra una nueva terminal ('exec $SHELL' también funciona)",
458
+ "fr": "💡 Dans chaque terminal où vous utiliserez claude, exécutez 'unset -f claude' ou ouvrez simplement un nouveau terminal ('exec $SHELL' fonctionne aussi)",
459
+ "it": "💡 In ogni terminale in cui userai claude, esegui 'unset -f claude' oppure apri semplicemente un nuovo terminale (funziona anche 'exec $SHELL')",
460
+ "da": "💡 Kør 'unset -f claude' i hver terminal, hvor du vil bruge claude — eller åbn blot en ny terminal ('exec $SHELL' virker også)",
461
+ "pl": "💡 W każdym terminalu, w którym będziesz używać claude, uruchom 'unset -f claude' lub po prostu otwórz nowy terminal ('exec $SHELL' też działa)",
462
+ "ru": "💡 В каждом терминале, где вы будете использовать claude, выполните 'unset -f claude' или просто откройте новый терминал ('exec $SHELL' тоже работает)",
463
+ "ar": "💡 في كل طرفية ستستخدم فيها claude، قم بتشغيل 'unset -f claude' أو افتح طرفية جديدة ('exec $SHELL' يعمل أيضًا)",
464
+ "no": "💡 Kjør 'unset -f claude' i hver terminal der du skal bruke claude eller bare åpne en ny terminal ('exec $SHELL' fungerer også)",
465
+ "pt-BR": "💡 Em cada terminal onde você usará o claude, execute 'unset -f claude' ou simplesmente abra um novo terminal ('exec $SHELL' também funciona)",
466
+ "th": "💡 ในแต่ละเทอร์มินัลที่จะใช้ claude ให้รัน 'unset -f claude' — หรือเปิดเทอร์มินัลใหม่ ('exec $SHELL' ก็ใช้ได้)",
467
+ "tr": "💡 claude kullanacağınız her terminalde 'unset -f claude' çalıştırın veya sadece yeni bir terminal açın ('exec $SHELL' de işe yarar)",
468
+ "uk": "💡 У кожному терміналі, де ви використовуватимете claude, виконайте 'unset -f claude' або просто відкрийте новий термінал ('exec $SHELL' також працює)"
409
469
  },
410
470
  "cli.uninstall.done": {
411
- "zh": "\n🗑️ CC Viewer 集成已移除 (运行 'npm uninstall -g cc-viewer' 以彻底删除命令)",
412
- "en": "\n🗑️ CC Viewer integration removed (Run 'npm uninstall -g cc-viewer' to remove command)",
413
- "zh-TW": "\n🗑️ CC Viewer 集成已移除 (運行 'npm uninstall -g cc-viewer' 以徹底刪除命令)",
414
- "ko": "\n🗑️ CC Viewer 통합 제거됨 ('npm uninstall -g cc-viewer'를 실행하여 명령 삭제)",
415
- "ja": "\n🗑️ CC Viewer 統合を削除しました (コマンドを完全に削除するには 'npm uninstall -g cc-viewer' を実행)",
416
- "de": "\n🗑️ CC Viewer-Integration entfernt (Führen Sie 'npm uninstall -g cc-viewer' aus, um den Befehl zu entfernen)",
417
- "es": "\n🗑️ Integración de CC Viewer eliminada (Ejecute 'npm uninstall -g cc-viewer' para eliminar el comando)",
418
- "fr": "\n🗑️ Intégration CC Viewer supprimée (Exécutez 'npm uninstall -g cc-viewer' pour supprimer la commande)",
419
- "it": "\n🗑️ Integrazione CC Viewer rimossa (Esegui 'npm uninstall -g cc-viewer' per rimuovere il comando)",
420
- "da": "\n🗑️ CC Viewer integration fjernet (Kør 'npm uninstall -g cc-viewer' for at fjerne kommandoen)",
421
- "pl": "\n🗑️ Integracja CC Viewer usunięta (Uruchom 'npm uninstall -g cc-viewer', aby usunąć polecenie)",
422
- "ru": "\n🗑️ Интеграция CC Viewer удалена (Запустите 'npm uninstall -g cc-viewer' для удаления команды)",
423
- "ar": "\n🗑️ تمت إزالة تكامل CC Viewer (قم بتشغيل 'npm uninstall -g cc-viewer' لإزالة الأمر)",
424
- "no": "\n🗑️ CC Viewer-integrasjon fjernet (Kjør 'npm uninstall -g cc-viewer' for å fjerne kommandoen)",
425
- "pt-BR": "\n🗑️ Integração do CC Viewer removida (Execute 'npm uninstall -g cc-viewer' para remover o comando)",
426
- "th": "\n🗑️ การรวม CC Viewer ถูกลบออก (เรียกใช้ 'npm uninstall -g cc-viewer' เพื่อลบคำสั่ง)",
427
- "tr": "\n🗑️ CC Viewer entegrasyonu kaldırıldı (Komutu kaldırmak için 'npm uninstall -g cc-viewer' çalıştırın)",
428
- "uk": "\n🗑️ Інтеграцію CC Viewer видалено (Запустіть 'npm uninstall -g cc-viewer', щоб видалити команду)"
471
+ "zh": "\n🗑️ CC Viewer 集成已移除。下一步:运行 'npm uninstall -g cc-viewer' 以删除 ccv 命令",
472
+ "en": "\n🗑️ CC Viewer integration removed. Next step: run 'npm uninstall -g cc-viewer' to remove the ccv command",
473
+ "zh-TW": "\n🗑️ CC Viewer 集成已移除。下一步:運行 'npm uninstall -g cc-viewer' 以刪除 ccv 命令",
474
+ "ko": "\n🗑️ CC Viewer 통합 제거됨. 다음 단계: 'npm uninstall -g cc-viewer'를 실행하여 ccv 명령을 삭제하세요",
475
+ "ja": "\n🗑️ CC Viewer 統合を削除しました。次のステップ: 'npm uninstall -g cc-viewer' を実行して ccv コマンドを削除してください",
476
+ "de": "\n🗑️ CC Viewer-Integration entfernt. Nächster Schritt: Führen Sie 'npm uninstall -g cc-viewer' aus, um den ccv-Befehl zu entfernen",
477
+ "es": "\n🗑️ Integración de CC Viewer eliminada. Siguiente paso: ejecute 'npm uninstall -g cc-viewer' para eliminar el comando ccv",
478
+ "fr": "\n🗑️ Intégration CC Viewer supprimée. Étape suivante : exécutez 'npm uninstall -g cc-viewer' pour supprimer la commande ccv",
479
+ "it": "\n🗑️ Integrazione CC Viewer rimossa. Passo successivo: esegui 'npm uninstall -g cc-viewer' per rimuovere il comando ccv",
480
+ "da": "\n🗑️ CC Viewer integration fjernet. Næste trin: kør 'npm uninstall -g cc-viewer' for at fjerne ccv-kommandoen",
481
+ "pl": "\n🗑️ Integracja CC Viewer usunięta. Następny krok: uruchom 'npm uninstall -g cc-viewer', aby usunąć polecenie ccv",
482
+ "ru": "\n🗑️ Интеграция CC Viewer удалена. Следующий шаг: запустите 'npm uninstall -g cc-viewer' для удаления команды ccv",
483
+ "ar": "\n🗑️ تمت إزالة تكامل CC Viewer. الخطوة التالية: قم بتشغيل 'npm uninstall -g cc-viewer' لإزالة أمر ccv",
484
+ "no": "\n🗑️ CC Viewer-integrasjon fjernet. Neste steg: kjør 'npm uninstall -g cc-viewer' for å fjerne ccv-kommandoen",
485
+ "pt-BR": "\n🗑️ Integração do CC Viewer removida. Próximo passo: execute 'npm uninstall -g cc-viewer' para remover o comando ccv",
486
+ "th": "\n🗑️ การรวม CC Viewer ถูกลบออก ขั้นตอนถัดไป: เรียกใช้ 'npm uninstall -g cc-viewer' เพื่อลบคำสั่ง ccv",
487
+ "tr": "\n🗑️ CC Viewer entegrasyonu kaldırıldı. Sonraki adım: ccv komutunu kaldırmak için 'npm uninstall -g cc-viewer' çalıştırın",
488
+ "uk": "\n🗑️ Інтеграцію CC Viewer видалено. Наступний крок: запустіть 'npm uninstall -g cc-viewer', щоб видалити команду ccv"
429
489
  },
430
490
  "server.started": {
431
491
  "zh": "\nClaude 请求监控服务已启动:",
@@ -2914,7 +2974,12 @@ const i18nData = {
2914
2974
  "th": "บอท IM '{id}' กำลังทำงานอยู่แล้ว (pid {pid}); ปฏิเสธการเริ่มอินสแตนซ์ที่สอง",
2915
2975
  "tr": "IM botu '{id}' zaten çalışıyor (pid {pid}); ikinci bir örnek başlatılması reddedildi.",
2916
2976
  "uk": "IM-бот '{id}' вже запущено (pid {pid}); запуск другого екземпляра відхилено."
2917
- }
2977
+ },
2978
+ "server.proxyStats.starting": { "zh": "代理重试统计已启动", "en": "Proxy retry stats started", "zh-TW": "代理重試統計已啟動", "ko": "프록시 재시도 통계가 시작됨", "ja": "プロキシリトライ統計を開始しました", "de": "Proxy-Wiederholungsstatistiken gestartet", "es": "Estadísticas de reintentos de proxy iniciadas", "fr": "Statistiques de relances du proxy démarrées", "it": "Statistiche retry del proxy avviate", "da": "Proxy-genforsøgsstatistik startet", "pl": "Statystyki ponownych prób proxy uruchomione", "ru": "Статистика повторов прокси запущена", "ar": "بدأت إحصائيات إعادة محاولات الوكيل", "no": "Proxy-retry-statistikk startet", "pt-BR": "Estatísticas de retry do proxy iniciadas", "th": "เริ่มสถิติการลองใหม่ของพร็อกซีแล้ว", "tr": "Proxy yeniden deneme istatistikleri başladı", "uk": "Статистику повторів проксі запущено" },
2979
+ "server.proxyStats.modeLabel": { "zh": "重试模式", "en": "Retry mode", "zh-TW": "重試模式", "ko": "재시도 모드", "ja": "リトライモード", "de": "Wiederholungsmodus", "es": "Modo de reintento", "fr": "Mode de relance", "it": "Modalità retry", "da": "Genforsøgstilstand", "pl": "Tryb ponownych prób", "ru": "Режим повторов", "ar": "وضع إعادة المحاولة", "no": "Retry-modus", "pt-BR": "Modo de retry", "th": "โหมดการลองใหม่", "tr": "Yeniden deneme modu", "uk": "Режим повторів" },
2980
+ "server.proxyStats.disabled": { "zh": "代理重试已禁用", "en": "Proxy retry disabled", "zh-TW": "代理重試已停用", "ko": "프록시 재시도 비활성화됨", "ja": "プロキシリトライは無効です", "de": "Proxy-Wiederholung deaktiviert", "es": "Reintento de proxy deshabilitado", "fr": "Relance du proxy désactivée", "it": "Retry del proxy disattivato", "da": "Proxy-genforsøg deaktiveret", "pl": "Ponowne próby proxy wyłączone", "ru": "Повторы прокси отключены", "ar": "تم تعطيل إعادة محاولات الوكيل", "no": "Proxy-retry deaktivert", "pt-BR": "Retry do proxy desativado", "th": "ปิดการลองใหม่ของพร็อกซีแล้ว", "tr": "Proxy yeniden deneme devre dışı", "uk": "Повтори проксі вимкнено" },
2981
+ "server.proxyStats.configLoaded": { "zh": "代理重试配置已加载", "en": "Proxy retry config loaded", "zh-TW": "代理重試設定已載入", "ko": "프록시 재시도 설정이 로드됨", "ja": "プロキシリトライ設定を読み込みました", "de": "Proxy-Wiederholungskonfiguration geladen", "es": "Configuración de reintento de proxy cargada", "fr": "Configuration de relance du proxy chargée", "it": "Configurazione retry del proxy caricata", "da": "Proxy-genforsøgskonfiguration indlæst", "pl": "Konfiguracja ponownych prób proxy załadowana", "ru": "Конфигурация повторов прокси загружена", "ar": "تم تحميل تكوين إعادة محاولات الوكيل", "no": "Proxy-retry-konfigurasjon lastet", "pt-BR": "Configuração de retry do proxy carregada", "th": "โหลดการตั้งค่าการลองใหม่ของพร็อกซีแล้ว", "tr": "Proxy yeniden deneme yapılandırması yüklendi", "uk": "Конфігурацію повторів проксі завантажено" },
2982
+ "server.proxyStats.refreshed": { "zh": "代理统计已刷新", "en": "Proxy stats refreshed", "zh-TW": "代理統計已重新整理", "ko": "프록시 통계가 새로고침됨", "ja": "プロキシ統計を更新しました", "de": "Proxy-Statistiken aktualisiert", "es": "Estadísticas de proxy actualizadas", "fr": "Statistiques du proxy actualisées", "it": "Statistiche del proxy aggiornate", "da": "Proxy-statistik opdateret", "pl": "Statystyki proxy odświeżone", "ru": "Статистика прокси обновлена", "ar": "تم تحديث إحصائيات الوكيل", "no": "Proxy-statistikk oppdatert", "pt-BR": "Estatísticas do proxy atualizadas", "th": "รีเฟรชสถิติพร็อกซีแล้ว", "tr": "Proxy istatistikleri yenilendi", "uk": "Статистику проксі оновлено" }
2918
2983
  };
2919
2984
 
2920
2985
  // 将 { key: { lang: text } } 转换为 { lang: { key: text } }
@@ -19,6 +19,7 @@ import { V2Writer } from './lib/v2/v2-writer.js';
19
19
  import { reportSwallowed } from './lib/error-report.js';
20
20
  import { latestMainSessionDir, sessionHasCompletedMainTurn } from './lib/v2/session-select.js';
21
21
  import { sanitizePathComponent } from './lib/v2/layout.js';
22
+ import { setRetryConfigPath, loadRetryConfig, DEFAULT_RETRY_CONFIG } from './lib/proxy-retry.js';
22
23
 
23
24
 
24
25
 
@@ -81,6 +82,26 @@ export let _cachedHaikuModel = null;
81
82
  const PROFILE_PATH = join(LOG_DIR, 'profile.json');
82
83
  let _activeProfile = null; // { id, name, baseURL?, apiKey?, effort?, ANTHROPIC_MODEL?, ANTHROPIC_DEFAULT_OPUS_MODEL?, ANTHROPIC_DEFAULT_SONNET_MODEL?, ANTHROPIC_DEFAULT_HAIKU_MODEL?, activeModel?(legacy) }
83
84
 
85
+ // ── 代理重试配置(运行时热切换,对齐 profile 模式)──
86
+ // retry-config.json 存全局共享的重试配置(mode/interval/maxRetries/maxConcurrent 等)。
87
+ // env(CCV_PROXY_RETRY_*)仍是启动默认/兜底,文件字段覆盖 env(文件优先)。
88
+ // watchFile 1.5s 跨 ccv 进程同步;proxy.js 经 namespace import 读 _retryConfigState live binding。
89
+ const RETRY_CONFIG_PATH = join(LOG_DIR, 'retry-config.json');
90
+ let _retryConfigState = { ...DEFAULT_RETRY_CONFIG }; // 可变,由 _loadRetryConfigState 刷新
91
+
92
+ // 把配置文件路径注入 proxy-retry.js(其 resolveRetryConfig 在 fileOverride=true 时读此路径)。
93
+ // 在模块加载阶段同步注入,确保后续 _loadRetryConfigState() 调用时路径已就绪。
94
+ setRetryConfigPath(RETRY_CONFIG_PATH);
95
+
96
+ /** 重读 retry-config.json + env 合并,刷新 _retryConfigState(live binding 消费方即取到新值)。 */
97
+ function _loadRetryConfigState() {
98
+ try {
99
+ _retryConfigState = loadRetryConfig();
100
+ } catch (err) {
101
+ if (process.env.CCV_DEBUG) console.error('[ccv retry-config] _loadRetryConfigState failed:', err && err.message);
102
+ }
103
+ }
104
+
84
105
  // 启动时捕获的原始配置(首次 API 请求时记录,不可变)
85
106
  let _defaultConfig = null; // { origin, authType, model }
86
107
 
@@ -200,7 +221,7 @@ function _replaceProxyAuthHeaders(headers, apiKey) {
200
221
  return { headers: newHeaders, matchedAuthKey, matchedXApiKey };
201
222
  }
202
223
 
203
- export { _activeProfile, _defaultConfig, _loadProxyProfile, PROFILE_PATH, setActiveProfileForWorkspace, getActiveProfileId };
224
+ export { _activeProfile, _defaultConfig, _loadProxyProfile, PROFILE_PATH, setActiveProfileForWorkspace, getActiveProfileId, RETRY_CONFIG_PATH, _retryConfigState, _loadRetryConfigState };
204
225
 
205
226
  // 1.7.0: the v1 single-file write path is retired — logs live in per-session
206
227
  // v2 dirs owned by V2Writer. Only the project binding (name + dir) remains
@@ -306,7 +327,10 @@ export function getLiveLogSource() {
306
327
  // newest dir once its first main req is written) — handing back exactly the
307
328
  // blank in-flight session the strict gate rejected. Excluding it makes the
308
329
  // fallback actually land on the previous, renderable conversation.
309
- const fallback = latestMainSessionDir(join(LOG_DIR, sanitizePathComponent(_projectName)), { excludeDir: dir });
330
+ // skipForeignLive: a parallel ccv window's in-flight session must never be
331
+ // served as THIS window's cold load (multi-window isolation); a crashed
332
+ // window's claim expires with its pid, so its session stays selectable.
333
+ const fallback = latestMainSessionDir(join(LOG_DIR, sanitizePathComponent(_projectName)), { excludeDir: dir, skipForeignLive: true });
310
334
  // A current-but-empty session with nothing else on disk: '' (still empty,
311
335
  // but the live feed fills it in place — no worse than before the fix).
312
336
  return fallback || dir || '';
@@ -316,6 +340,47 @@ export function getLiveLogSource() {
316
340
  }
317
341
  }
318
342
 
343
+ // SessionStart hook notify (session-start-bridge.js → /api/session-start-notify
344
+ // → server.js deps). The only source acted on is 'resume': an in-terminal
345
+ // /resume switches the running claude to a PAST conversation while the wire
346
+ // session_id may stay the same, so without this signal the writer keeps
347
+ // routing the resumed conversation into the OLD session dir (and the panel —
348
+ // keyed on `_seqEpoch = v2:<dir identity>` — never switches). The transcript
349
+ // basename is the resumed conversation's stable identity; the hook session_id
350
+ // is a fresh uuid usable as the new dir identity when nothing was recorded.
351
+ // 'startup'/'clear'/'compact' are deliberately ignored (startup needs nothing;
352
+ // /clear already has epoch machinery; teammate processes inherit
353
+ // CCVIEWER_PORT and fire startup events at this endpoint — the source gate
354
+ // drops them).
355
+ export function markSessionStart(payload) {
356
+ try {
357
+ const { source, sessionId, transcriptPath, cwd } = payload || {};
358
+ if (source !== 'resume') return;
359
+ if (!transcriptPath || typeof transcriptPath !== 'string') {
360
+ console.warn('[ccv session-start] resume signal without transcript_path — ignored');
361
+ return;
362
+ }
363
+ // Soft cross-project guard: a foreign project's claude that somehow
364
+ // carries our CCVIEWER_PORT must not re-bind THIS project's writer. Only
365
+ // enforced when both sides are known (workspace mode may leave
366
+ // _projectName empty until bound).
367
+ if (cwd && typeof cwd === 'string' && _projectName) {
368
+ const cwdProject = basename(cwd).replace(/[^a-zA-Z0-9_\-\.]/g, '_');
369
+ if (cwdProject !== _projectName) {
370
+ console.warn(`[ccv session-start] resume signal from project "${cwdProject}" ignored (bound to "${_projectName}")`);
371
+ return;
372
+ }
373
+ }
374
+ const transcriptUuid = basename(transcriptPath, '.jsonl');
375
+ _v2Writer.beginResumeSwitch({
376
+ transcriptUuid,
377
+ hookSid: (typeof sessionId === 'string' && sessionId) ? sessionId : null,
378
+ });
379
+ } catch (err) {
380
+ reportSwallowed('session-start.mark', err);
381
+ }
382
+ }
383
+
319
384
  // P2 (-c migration guidance): three detection channels, any one marks this
320
385
  // launch as "continuing an older conversation" — the migrate prompt then
321
386
  // re-prompts even a dismissed user, because the conversation's first half
@@ -376,6 +441,11 @@ _syncContinuationMode(); // seed from the CLI env at module load (`ccv -c`)
376
441
  _loadProxyProfile();
377
442
  try { watchFile(PROFILE_PATH, { interval: 1500 }, _loadProxyProfile); } catch { }
378
443
 
444
+ // Retry config: initial load + watchFile cross-process sync (UI writes
445
+ // retry-config.json → hot-reloaded within 1.5s, mirroring PROFILE_PATH above).
446
+ _loadRetryConfigState();
447
+ try { watchFile(RETRY_CONFIG_PATH, { interval: 1500 }, _loadRetryConfigState); } catch { }
448
+
379
449
  // Kept as an awaited boot barrier for callers; nothing asynchronous remains
380
450
  // since the v1 resume flow retired (v2 sessions key off wire session_ids).
381
451
  const _initPromise = Promise.resolve();
@@ -0,0 +1,251 @@
1
+ // cc-switch config import library (pure functions + read-only SQLite).
2
+ //
3
+ // cc-switch is a Tauri desktop app that stores AI provider credentials in the
4
+ // providers table of a SQLite database (cc-switch.db). The settings_config column
5
+ // is a JSON string containing env.ANTHROPIC_BASE_URL / env.ANTHROPIC_AUTH_TOKEN /
6
+ // env.ANTHROPIC_MODEL, etc.
7
+ //
8
+ // This module locates the cc-switch data directory cross-platform (mac/linux/windows),
9
+ // opens the db read-only, and maps providers with app_type='claude' into cc-viewer's
10
+ // profile format. It performs no writes and never touches profile.json — persistence
11
+ // is delegated to the caller (preferences.js POST /api/ccswitch-import).
12
+ //
13
+ // Design notes:
14
+ // - Open SQLite read-only (readOnly:true) to avoid SQLITE_BUSY locks while cc-switch is running
15
+ // - Multi-path probing: each platform tries the standard Tauri dir first, then falls back to ~/.cc-switch/
16
+ // - settings_config parsing is fully fault-tolerant: null / non-JSON / missing env are all skipped, never throws
17
+ // - Profile ids get a ccs_ prefix to distinguish from user-created proxy_ prefixed ones; updates are idempotent (re-imports update, never duplicate)
18
+ // - Only app_type='claude' is imported; codex is skipped (different format, cc-viewer does not support OpenAI auth)
19
+ // - Never sets active automatically: import only populates the list; switching is decided by the caller/UI (avoids clobbering the user's current selection)
20
+
21
+ import { existsSync } from 'node:fs';
22
+ import { join } from 'node:path';
23
+ import { homedir, platform } from 'node:os';
24
+
25
+ // Dynamically import node:sqlite — built into Node 22.5+ (22.5 needs --experimental-sqlite flag,
26
+ // stable in 23+ / 26). Dynamic import is used so that a missing module on older Node does not
27
+ // bring down the entire interceptor chain.
28
+ let _DatabaseSync = null;
29
+ async function getDatabaseSync() {
30
+ if (_DatabaseSync !== null) return _DatabaseSync;
31
+ try {
32
+ const m = await import('node:sqlite');
33
+ _DatabaseSync = m.DatabaseSync || null;
34
+ } catch {
35
+ _DatabaseSync = null;
36
+ }
37
+ return _DatabaseSync;
38
+ }
39
+
40
+ // Cross-platform candidate paths (ordered by priority; the first existsSync hit wins).
41
+ // cc-switch hardcodes ~/.cc-switch/cc-switch.db on ALL platforms (get_app_config_dir() in its
42
+ // config.rs; the Tauri identifier com.ccswitch.desktop does NOT affect the DB path). So that
43
+ // path is probed first. The platform-specific Tauri app-data dirs below are kept only as
44
+ // legacy fallbacks in case some very old cc-switch version wrote there — never first — so a
45
+ // stale/empty leftover file in them cannot shadow the real ~/.cc-switch/cc-switch.db.
46
+ function candidateDbPaths(opts) {
47
+ // opts is for testing only (inject platform/home/env without touching the runtime);
48
+ // production callers omit it and use the real homedir()/platform()/process.env.
49
+ const home = opts && opts.home != null ? opts.home : homedir();
50
+ const plat = opts && opts.plat != null ? opts.plat : platform();
51
+ const env = opts && opts.env ? opts.env : process.env;
52
+ const paths = [join(home, '.cc-switch', 'cc-switch.db')]; // primary on every platform
53
+ if (plat === 'darwin') {
54
+ paths.push(join(home, 'Library', 'Application Support', 'cc-switch', 'cc-switch.db'));
55
+ paths.push(join(home, 'Library', 'Application Support', 'com.ccswitch.desktop', 'cc-switch.db'));
56
+ } else if (plat === 'win32') {
57
+ const appdata = env.APPDATA || join(home, 'AppData', 'Roaming');
58
+ const localappdata = env.LOCALAPPDATA || join(home, 'AppData', 'Local');
59
+ paths.push(join(appdata, 'cc-switch', 'cc-switch.db'));
60
+ paths.push(join(localappdata, 'cc-switch', 'cc-switch.db'));
61
+ } else {
62
+ // linux and other unix
63
+ const xdg = env.XDG_DATA_HOME;
64
+ if (xdg) paths.push(join(xdg, 'cc-switch', 'cc-switch.db'));
65
+ paths.push(join(home, '.local', 'share', 'cc-switch', 'cc-switch.db'));
66
+ }
67
+ return paths;
68
+ }
69
+ // Exported for unit tests so the win32/darwin priority ordering can be exercised on any host.
70
+ export { candidateDbPaths as _candidateDbPathsForTest };
71
+
72
+ // Test hook: override the cached DatabaseSync — pass false to simulate a runtime
73
+ // without node:sqlite (the getter returns the cached falsy value and the callers
74
+ // take the unavailable path), or null to restore lazy detection.
75
+ export function _setDatabaseSyncForTest(v) { _DatabaseSync = v; }
76
+
77
+ // Returns the first existing cc-switch.db path, or null if none found.
78
+ export function findCcSwitchDbPath() {
79
+ for (const p of candidateDbPaths()) {
80
+ try {
81
+ if (existsSync(p)) return p;
82
+ } catch { /* ignore */ }
83
+ }
84
+ return null;
85
+ }
86
+
87
+ // Map of model fields in settings_config.env → cc-viewer profile fields.
88
+ // Note: cc-switch has variants suffixed with _NAME (e.g. ANTHROPIC_DEFAULT_SONNET_MODEL_NAME);
89
+ // cc-viewer profiles only use the standard field names without _NAME, so we take only the
90
+ // standard names and ignore the _NAME variants.
91
+ const ENV_FIELD_MAP = {
92
+ ANTHROPIC_BASE_URL: 'baseURL',
93
+ ANTHROPIC_AUTH_TOKEN: 'apiKey',
94
+ ANTHROPIC_API_KEY: 'apiKey', // rare, but kept for compatibility
95
+ ANTHROPIC_MODEL: 'ANTHROPIC_MODEL',
96
+ ANTHROPIC_DEFAULT_OPUS_MODEL: 'ANTHROPIC_DEFAULT_OPUS_MODEL',
97
+ ANTHROPIC_DEFAULT_SONNET_MODEL: 'ANTHROPIC_DEFAULT_SONNET_MODEL',
98
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
99
+ // cc-switch's "通用配置" effort toggle writes CLAUDE_CODE_EFFORT_LEVEL (e.g. "max").
100
+ // cc-viewer's interceptor injects profile.effort as output_config.effort, so mapping
101
+ // this env var preserves the user's effort setting across import (was previously dropped).
102
+ CLAUDE_CODE_EFFORT_LEVEL: 'effort',
103
+ };
104
+
105
+ // Map a cc-switch providers row into a cc-viewer profile object.
106
+ // settings_config is a JSON string stored in a TEXT column. Returns null when the row is unusable (no credentials / no baseURL).
107
+ export function mapProviderToProfile(row) {
108
+ if (!row || !row.id || !row.name) return null;
109
+ if (row.app_type && row.app_type !== 'claude') return null; // only import claude
110
+ let cfg = null;
111
+ try {
112
+ cfg = typeof row.settings_config === 'string'
113
+ ? JSON.parse(row.settings_config)
114
+ : row.settings_config;
115
+ } catch { return null; }
116
+ if (!cfg || typeof cfg !== 'object') return null;
117
+ const env = cfg.env;
118
+ if (!env || typeof env !== 'object') return null;
119
+
120
+ const profile = {
121
+ id: `ccs_${row.id}`,
122
+ name: String(row.name),
123
+ baseURL: '',
124
+ apiKey: '',
125
+ effort: '', // CLAUDE_CODE_EFFORT_LEVEL → output_config.effort (injected by interceptor)
126
+ ANTHROPIC_MODEL: '',
127
+ ANTHROPIC_DEFAULT_OPUS_MODEL: '',
128
+ ANTHROPIC_DEFAULT_SONNET_MODEL: '',
129
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: '',
130
+ source: 'cc-switch', // source marker; UI may show an "imported" badge from this
131
+ };
132
+
133
+ let hasCredential = false;
134
+ for (const [envKey, profileKey] of Object.entries(ENV_FIELD_MAP)) {
135
+ if (typeof env[envKey] === 'string' && env[envKey]) {
136
+ // Prefer whichever credential appears first (ANTHROPIC_AUTH_TOKEN is ordered before ANTHROPIC_API_KEY;
137
+ // if both exist and apiKey was already set by the former, do not overwrite it)
138
+ if (profileKey === 'apiKey' && hasCredential) continue;
139
+ profile[profileKey] = env[envKey];
140
+ if (profileKey === 'apiKey') hasCredential = true;
141
+ }
142
+ }
143
+
144
+ // A valid credential requires both baseURL and apiKey (rows with only non-credential config, e.g. Claude Official, are skipped)
145
+ if (!profile.baseURL || !profile.apiKey) return null;
146
+ return profile;
147
+ }
148
+
149
+ // Read claude providers from cc-switch.db and return a cc-viewer profile array.
150
+ // Opened read-only with full fault tolerance: db cannot open / table missing / no rows → empty array + error message.
151
+ export async function readCcSwitchProviders(dbPath) {
152
+ const DatabaseSync = await getDatabaseSync();
153
+ if (!DatabaseSync) {
154
+ // Keep the 'node:sqlite unavailable' prefix stable — the UI keys a dedicated
155
+ // localized message off it (ui.proxy.ccswitchNodeUnsupported).
156
+ return { profiles: [], error: 'node:sqlite unavailable on this runtime (requires Node >= 22.5 with --experimental-sqlite, or >= 23.4)' };
157
+ }
158
+ let db = null;
159
+ try {
160
+ // Read-only: cc-switch stays unaffected by our reads (no SQLITE_BUSY)
161
+ db = new DatabaseSync(dbPath, { readOnly: true });
162
+ } catch (err) {
163
+ return { profiles: [], error: `cannot open db: ${err && err.message}` };
164
+ }
165
+ try {
166
+ // providers table existence check. A query that throws here means the file is
167
+ // unreadable as a SQLite db (corrupt / non-SQLite / truncated) — the real cause
168
+ // must surface, not be masked as "table not found". Let it propagate to the
169
+ // outer catch, which formats it as `query failed: <message>`.
170
+ const r = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='providers'").get();
171
+ if (!r) return { profiles: [], error: `providers table not found in ${dbPath}` };
172
+
173
+ const rows = db.prepare(
174
+ "SELECT id, app_type, name, settings_config, is_current FROM providers WHERE app_type = 'claude' ORDER BY sort_index, name"
175
+ ).all();
176
+
177
+ const profiles = [];
178
+ let currentId = null;
179
+ for (const row of rows) {
180
+ const p = mapProviderToProfile(row);
181
+ if (p) {
182
+ profiles.push(p);
183
+ if (row.is_current) currentId = p.id;
184
+ }
185
+ }
186
+ return { profiles, currentId, error: null };
187
+ } catch (err) {
188
+ return { profiles: [], error: `query failed: ${err && err.message}` };
189
+ } finally {
190
+ try { db.close(); } catch { /* best effort */ }
191
+ }
192
+ }
193
+
194
+ // Top-level convenience: probe path + read, returns { profiles, currentId, dbPath, error }.
195
+ // Shared by GET /api/ccswitch-providers (preview) and POST /api/ccswitch-import (apply).
196
+ export async function discoverCcSwitchProviders() {
197
+ const dbPath = findCcSwitchDbPath();
198
+ if (!dbPath) return { profiles: [], currentId: null, dbPath: null, error: 'cc-switch not found' };
199
+ const result = await readCcSwitchProviders(dbPath);
200
+ return { ...result, dbPath };
201
+ }
202
+
203
+ // Merge cc-switch-imported profiles into cc-viewer's existing profile list.
204
+ // Rules:
205
+ // - ccs_-prefixed (previously imported): matched by id, updated with fresh data (credential refresh)
206
+ // - proxy_-prefixed (user-created) and any entry without a ccs_ id (including id-less ones): preserved as-is
207
+ // - newly seen ccs_ ids: appended to the end of the list
208
+ // - max (built-in default) is always present and first — seeded when missing, mirroring
209
+ // proxyProfilesPost's invariant, so a first-ever import can never write a list without it
210
+ // Returns { profiles, imported, updated } counts.
211
+ export function mergeImportedProfiles(existing, importedList) {
212
+ const existingArr = Array.isArray(existing) ? existing : [];
213
+ const importedMap = new Map();
214
+ for (const p of importedList) importedMap.set(p.id, p);
215
+
216
+ const result = [];
217
+ let newCount = 0;
218
+ let updatedCount = 0;
219
+
220
+ // Built-in max always first — reuse the existing entry, or seed the same shape
221
+ // proxyProfilesPost injects. Without this, a fresh install importing before ever
222
+ // saving a proxy would write a profiles list with no Default option.
223
+ const existingMax = existingArr.find(p => p && p.id === 'max');
224
+ result.push(existingMax || { id: 'max', name: 'Default' });
225
+ // Everything that is not max and not ccs_-sourced (user proxy_ profiles, unknown or
226
+ // id-less entries) is preserved verbatim — this function only owns the ccs_ namespace
227
+ for (const p of existingArr) {
228
+ if (!p || p.id === 'max') continue;
229
+ if (!p.id || !p.id.startsWith('ccs_')) {
230
+ result.push(p);
231
+ }
232
+ }
233
+ // ccs_-sourced: use the freshly imported data
234
+ for (const p of existingArr) {
235
+ if (p && p.id && p.id.startsWith('ccs_')) {
236
+ if (importedMap.has(p.id)) {
237
+ result.push(importedMap.get(p.id));
238
+ importedMap.delete(p.id);
239
+ updatedCount++;
240
+ }
241
+ // If the imported data no longer has this id (deleted on the cc-switch side), drop it (no stale entries)
242
+ }
243
+ }
244
+ // Newly seen ccs_ profiles (not previously imported)
245
+ for (const p of importedMap.values()) {
246
+ result.push(p);
247
+ newCount++;
248
+ }
249
+
250
+ return { profiles: result, imported: newCount, updated: updatedCount };
251
+ }