cc-viewer 1.3.8 → 1.4.1

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/dist/index.html CHANGED
@@ -6,8 +6,8 @@
6
6
  <title>Claude Code Viewer</title>
7
7
  <link rel="icon" href="/favicon.ico?v=1">
8
8
  <link rel="shortcut icon" href="/favicon.ico?v=1">
9
- <script type="module" crossorigin src="/assets/index-D3R8xYSC.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-DmF5H2KN.css">
9
+ <script type="module" crossorigin src="/assets/index-B0OFShed.js"></script>
10
+ <link rel="stylesheet" crossorigin href="/assets/index-A993yV8C.css">
11
11
  </head>
12
12
  <body>
13
13
  <div id="root"></div>
package/i18n.js CHANGED
@@ -186,6 +186,46 @@ const i18nData = {
186
186
  "en": "CC Viewer CLI\n\nUsage:\n ccv [options]\n ccv run -- <command> [args...]\n\nOptions:\n -h, --help Show help\n -v, --version Show version\n --uninstall Remove CC Viewer integration\n\nNotes:\n Running ccv without arguments installs/repairs the Claude Code hook.",
187
187
  "zh-TW": "CC Viewer CLI\n\n用法:\n ccv [options]\n ccv run -- <command> [args...]\n\n選項:\n -h, --help 顯示說明\n -v, --version 顯示版本\n --uninstall 移除 CC Viewer 整合\n\n說明:\n 直接執行 ccv 會安裝/修復 Claude Code 的整合 Hook。"
188
188
  },
189
+ "cli.cMode.notFound": {
190
+ "zh": "错误: 未找到 claude 命令,请确认已安装 Claude Code",
191
+ "en": "Error: claude command not found. Please make sure Claude Code is installed",
192
+ "zh-TW": "錯誤: 未找到 claude 命令,請確認已安裝 Claude Code",
193
+ "ko": "오류: claude 명령을 찾을 수 없습니다. Claude Code가 설치되어 있는지 확인하세요",
194
+ "ja": "エラー: claude コマンドが見つかりません。Claude Code がインストールされていることを確認してください",
195
+ "de": "Fehler: claude-Befehl nicht gefunden. Bitte stellen Sie sicher, dass Claude Code installiert ist",
196
+ "es": "Error: comando claude no encontrado. Asegúrese de que Claude Code esté instalado",
197
+ "fr": "Erreur : commande claude introuvable. Veuillez vérifier que Claude Code est installé",
198
+ "it": "Errore: comando claude non trovato. Assicurati che Claude Code sia installato",
199
+ "da": "Fejl: claude-kommando ikke fundet. Sørg for at Claude Code er installeret",
200
+ "pl": "Błąd: nie znaleziono polecenia claude. Upewnij się, że Claude Code jest zainstalowany",
201
+ "ru": "Ошибка: команда claude не найдена. Убедитесь, что Claude Code установлен",
202
+ "ar": "خطأ: أمر claude غير موجود. يرجى التأكد من تثبيت Claude Code",
203
+ "no": "Feil: claude-kommando ikke funnet. Sørg for at Claude Code er installert",
204
+ "pt-BR": "Erro: comando claude não encontrado. Certifique-se de que o Claude Code está instalado",
205
+ "th": "ข้อผิดพลาด: ไม่พบคำสั่ง claude กรุณาตรวจสอบว่าได้ติดตั้ง Claude Code แล้ว",
206
+ "tr": "Hata: claude komutu bulunamadı. Claude Code'un kurulu olduğundan emin olun",
207
+ "uk": "Помилка: команду claude не знайдено. Переконайтеся, що Claude Code встановлено"
208
+ },
209
+ "cli.cMode.starting": {
210
+ "zh": "正在启动 CC Viewer CLI 模式...",
211
+ "en": "Starting CC Viewer CLI mode...",
212
+ "zh-TW": "正在啟動 CC Viewer CLI 模式...",
213
+ "ko": "CC Viewer CLI 모드를 시작하는 중...",
214
+ "ja": "CC Viewer CLI モードを起動中...",
215
+ "de": "CC Viewer CLI-Modus wird gestartet...",
216
+ "es": "Iniciando modo CLI de CC Viewer...",
217
+ "fr": "Démarrage du mode CLI de CC Viewer...",
218
+ "it": "Avvio modalità CLI di CC Viewer...",
219
+ "da": "Starter CC Viewer CLI-tilstand...",
220
+ "pl": "Uruchamianie trybu CLI CC Viewer...",
221
+ "ru": "Запуск режима CLI CC Viewer...",
222
+ "ar": "جاري تشغيل وضع CLI في CC Viewer...",
223
+ "no": "Starter CC Viewer CLI-modus...",
224
+ "pt-BR": "Iniciando modo CLI do CC Viewer...",
225
+ "th": "กำลังเริ่มโหมด CLI ของ CC Viewer...",
226
+ "tr": "CC Viewer CLI modu başlatılıyor...",
227
+ "uk": "Запуск режиму CLI CC Viewer..."
228
+ },
189
229
  "cli.usage.uninstallHint": {
190
230
  "zh": "",
191
231
  "en": "",
@@ -1318,6 +1358,26 @@ const i18nData = {
1318
1358
  "tr": "MainAgent konuşma verisi yok",
1319
1359
  "uk": "Немає даних діалогу MainAgent"
1320
1360
  },
1361
+ "ui.terminal.exited": {
1362
+ "zh": "[进程已退出,退出码: {code}]",
1363
+ "en": "[Process exited with code {code}]",
1364
+ "zh-TW": "[程序已退出,退出碼: {code}]",
1365
+ "ko": "[프로세스가 코드 {code}(으)로 종료되었습니다]",
1366
+ "ja": "[プロセスがコード {code} で終了しました]",
1367
+ "de": "[Prozess mit Code {code} beendet]",
1368
+ "es": "[Proceso finalizado con código {code}]",
1369
+ "fr": "[Processus terminé avec le code {code}]",
1370
+ "it": "[Processo terminato con codice {code}]",
1371
+ "da": "[Proces afsluttet med kode {code}]",
1372
+ "pl": "[Proces zakończony z kodem {code}]",
1373
+ "ru": "[Процесс завершился с кодом {code}]",
1374
+ "ar": "[انتهت العملية برمز {code}]",
1375
+ "no": "[Prosess avsluttet med kode {code}]",
1376
+ "pt-BR": "[Processo encerrado com código {code}]",
1377
+ "th": "[กระบวนการออกด้วยรหัส {code}]",
1378
+ "tr": "[İşlem {code} koduyla çıkış yaptı]",
1379
+ "uk": "[Процес завершився з кодом {code}]"
1380
+ },
1321
1381
  "ui.diffWithPrev": {
1322
1382
  "zh": "与上次请求的差异",
1323
1383
  "en": "Diff with previous request",
@@ -2217,6 +2277,86 @@ const i18nData = {
2217
2277
  "th": "รวมบันทึกสำเร็จ",
2218
2278
  "tr": "Loglar başarıyla birleştirildi",
2219
2279
  "uk": "Логи успішно об'єднано"
2280
+ },
2281
+ "ui.askQuestion": {
2282
+ "zh": "提问",
2283
+ "en": "Question",
2284
+ "zh-TW": "提問",
2285
+ "ko": "질문",
2286
+ "ja": "質問",
2287
+ "de": "Frage",
2288
+ "es": "Pregunta",
2289
+ "fr": "Question",
2290
+ "it": "Domanda",
2291
+ "da": "Spørgsmål",
2292
+ "pl": "Pytanie",
2293
+ "ru": "Вопрос",
2294
+ "ar": "سؤال",
2295
+ "no": "Spørsmål",
2296
+ "pt-BR": "Pergunta",
2297
+ "th": "คำถาม",
2298
+ "tr": "Soru",
2299
+ "uk": "Питання"
2300
+ },
2301
+ "ui.enterPlanMode": {
2302
+ "zh": "进入计划模式",
2303
+ "en": "Entering Plan Mode",
2304
+ "zh-TW": "進入計劃模式",
2305
+ "ko": "계획 모드 진입",
2306
+ "ja": "プランモードに入ります",
2307
+ "de": "Planungsmodus wird gestartet",
2308
+ "es": "Entrando en modo de planificación",
2309
+ "fr": "Entrée en mode planification",
2310
+ "it": "Accesso alla modalità pianificazione",
2311
+ "da": "Går ind i planlægningstilstand",
2312
+ "pl": "Wchodzenie w tryb planowania",
2313
+ "ru": "Вход в режим планирования",
2314
+ "ar": "الدخول في وضع التخطيط",
2315
+ "no": "Går inn i planleggingsmodus",
2316
+ "pt-BR": "Entrando no modo de planejamento",
2317
+ "th": "เข้าสู่โหมดวางแผน",
2318
+ "tr": "Plan moduna giriliyor",
2319
+ "uk": "Вхід у режим планування"
2320
+ },
2321
+ "ui.exitPlanMode": {
2322
+ "zh": "计划就绪,等待审批",
2323
+ "en": "Plan Ready for Approval",
2324
+ "zh-TW": "計劃就緒,等待審批",
2325
+ "ko": "계획 준비 완료, 승인 대기",
2326
+ "ja": "プラン準備完了、承認待ち",
2327
+ "de": "Plan bereit zur Genehmigung",
2328
+ "es": "Plan listo para aprobación",
2329
+ "fr": "Plan prêt pour approbation",
2330
+ "it": "Piano pronto per l'approvazione",
2331
+ "da": "Plan klar til godkendelse",
2332
+ "pl": "Plan gotowy do zatwierdzenia",
2333
+ "ru": "План готов к утверждению",
2334
+ "ar": "الخطة جاهزة للموافقة",
2335
+ "no": "Plan klar for godkjenning",
2336
+ "pt-BR": "Plano pronto para aprovação",
2337
+ "th": "แผนพร้อมรอการอนุมัติ",
2338
+ "tr": "Plan onaya hazır",
2339
+ "uk": "План готовий до затвердження"
2340
+ },
2341
+ "ui.allowedPrompts": {
2342
+ "zh": "请求的权限",
2343
+ "en": "Requested Permissions",
2344
+ "zh-TW": "請求的權限",
2345
+ "ko": "요청된 권한",
2346
+ "ja": "要求された権限",
2347
+ "de": "Angeforderte Berechtigungen",
2348
+ "es": "Permisos solicitados",
2349
+ "fr": "Permissions demandées",
2350
+ "it": "Permessi richiesti",
2351
+ "da": "Anmodede tilladelser",
2352
+ "pl": "Żądane uprawnienia",
2353
+ "ru": "Запрошенные разрешения",
2354
+ "ar": "الأذونات المطلوبة",
2355
+ "no": "Forespurte tillatelser",
2356
+ "pt-BR": "Permissões solicitadas",
2357
+ "th": "สิทธิ์ที่ร้องขอ",
2358
+ "tr": "İstenen izinler",
2359
+ "uk": "Запитані дозволи"
2220
2360
  }
2221
2361
  };
2222
2362
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-viewer",
3
- "version": "1.3.8",
3
+ "version": "1.4.1",
4
4
  "description": "Claude Code Logger visualization management tool",
5
5
  "license": "MIT",
6
6
  "main": "server.js",
@@ -50,16 +50,24 @@
50
50
  "findcc.js",
51
51
  "stats-worker.js",
52
52
  "updater.js",
53
+ "pty-manager.js",
53
54
  "locales/",
54
55
  "concepts/"
55
56
  ],
56
57
  "devDependencies": {
57
- "react": "^18.3.1",
58
- "react-dom": "^18.3.1",
58
+ "@vitejs/plugin-react": "^4.5.2",
59
+ "@xterm/addon-fit": "^0.11.0",
60
+ "@xterm/xterm": "^6.0.0",
59
61
  "antd": "^5.29.2",
60
62
  "marked": "^17.0.2",
63
+ "react": "^18.3.1",
64
+ "react-dom": "^18.3.1",
61
65
  "react-json-view-lite": "^2.1.0",
62
- "vite": "^6.3.5",
63
- "@vitejs/plugin-react": "^4.5.2"
66
+ "vite": "^6.3.5"
67
+ },
68
+ "dependencies": {
69
+ "@xterm/addon-web-links": "^0.12.0",
70
+ "node-pty": "^1.1.0",
71
+ "ws": "^8.19.0"
64
72
  }
65
73
  }
package/pty-manager.js ADDED
@@ -0,0 +1,125 @@
1
+ import { resolveNativePath } from './findcc.js';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { join, dirname } from 'node:path';
4
+ import { chmodSync, statSync } from 'node:fs';
5
+ import { platform, arch } from 'node:os';
6
+
7
+ let ptyProcess = null;
8
+ let dataListeners = [];
9
+ let exitListeners = [];
10
+ let lastExitCode = null;
11
+ let outputBuffer = '';
12
+ const MAX_BUFFER = 200000;
13
+
14
+ function fixSpawnHelperPermissions() {
15
+ try {
16
+ const __dirname = dirname(fileURLToPath(import.meta.url));
17
+ const os = platform();
18
+ const cpu = arch();
19
+ const helperPath = join(__dirname, 'node_modules', 'node-pty', 'prebuilds', `${os}-${cpu}`, 'spawn-helper');
20
+ const stat = statSync(helperPath);
21
+ if (!(stat.mode & 0o111)) {
22
+ chmodSync(helperPath, stat.mode | 0o755);
23
+ }
24
+ } catch {}
25
+ }
26
+
27
+ export async function spawnClaude(proxyPort, cwd) {
28
+ if (ptyProcess) {
29
+ throw new Error('PTY process already running');
30
+ }
31
+
32
+ const ptyMod = await import('node-pty');
33
+ const pty = ptyMod.default || ptyMod;
34
+
35
+ fixSpawnHelperPermissions();
36
+
37
+ const claudePath = resolveNativePath();
38
+ if (!claudePath) {
39
+ throw new Error('claude not found');
40
+ }
41
+
42
+ const env = { ...process.env };
43
+ env.ANTHROPIC_BASE_URL = `http://127.0.0.1:${proxyPort}`;
44
+
45
+ const settingsJson = JSON.stringify({
46
+ env: { ANTHROPIC_BASE_URL: env.ANTHROPIC_BASE_URL }
47
+ });
48
+
49
+ const args = ['--settings', settingsJson];
50
+
51
+ lastExitCode = null;
52
+ outputBuffer = '';
53
+
54
+ ptyProcess = pty.spawn(claudePath, args, {
55
+ name: 'xterm-256color',
56
+ cols: 120,
57
+ rows: 30,
58
+ cwd: cwd || process.cwd(),
59
+ env,
60
+ });
61
+
62
+ ptyProcess.onData((data) => {
63
+ outputBuffer += data;
64
+ if (outputBuffer.length > MAX_BUFFER) {
65
+ outputBuffer = outputBuffer.slice(-MAX_BUFFER);
66
+ }
67
+ for (const cb of dataListeners) {
68
+ try { cb(data); } catch {}
69
+ }
70
+ });
71
+
72
+ ptyProcess.onExit(({ exitCode }) => {
73
+ lastExitCode = exitCode;
74
+ ptyProcess = null;
75
+ for (const cb of exitListeners) {
76
+ try { cb(exitCode); } catch {}
77
+ }
78
+ });
79
+
80
+ return ptyProcess;
81
+ }
82
+
83
+ export function writeToPty(data) {
84
+ if (ptyProcess) {
85
+ ptyProcess.write(data);
86
+ }
87
+ }
88
+
89
+ export function resizePty(cols, rows) {
90
+ if (ptyProcess) {
91
+ try { ptyProcess.resize(cols, rows); } catch {}
92
+ }
93
+ }
94
+
95
+ export function killPty() {
96
+ if (ptyProcess) {
97
+ try { ptyProcess.kill(); } catch {}
98
+ ptyProcess = null;
99
+ }
100
+ }
101
+
102
+ export function onPtyData(cb) {
103
+ dataListeners.push(cb);
104
+ return () => {
105
+ dataListeners = dataListeners.filter(l => l !== cb);
106
+ };
107
+ }
108
+
109
+ export function onPtyExit(cb) {
110
+ exitListeners.push(cb);
111
+ return () => {
112
+ exitListeners = exitListeners.filter(l => l !== cb);
113
+ };
114
+ }
115
+
116
+ export function getPtyState() {
117
+ return {
118
+ running: !!ptyProcess,
119
+ exitCode: lastExitCode,
120
+ };
121
+ }
122
+
123
+ export function getOutputBuffer() {
124
+ return outputBuffer;
125
+ }
package/server.js CHANGED
@@ -11,6 +11,7 @@ import { t, detectLanguage } from './i18n.js';
11
11
  import { checkAndUpdate } from './updater.js';
12
12
 
13
13
  const PREFS_FILE = join(LOG_DIR, 'preferences.json');
14
+ const isCliMode = process.env.CCV_CLI_MODE === '1';
14
15
 
15
16
 
16
17
  // macOS user profile (avatar + display name), cached once
@@ -497,6 +498,13 @@ function handleRequest(req, res) {
497
498
  return;
498
499
  }
499
500
 
501
+ // CLI 模式检测
502
+ if (url === '/api/cli-mode' && method === 'GET') {
503
+ res.writeHead(200, { 'Content-Type': 'application/json' });
504
+ res.end(JSON.stringify({ cliMode: isCliMode }));
505
+ return;
506
+ }
507
+
500
508
  // 列出本地日志文件(按项目分组,遍历项目子目录)
501
509
  if (url === '/api/local-logs' && method === 'GET') {
502
510
  try {
@@ -712,6 +720,10 @@ export async function startViewer() {
712
720
  } catch { }
713
721
  startWatching();
714
722
  startStatsWorker();
723
+ // CLI 模式下启动 WebSocket 服务
724
+ if (isCliMode) {
725
+ setupTerminalWebSocket(currentServer);
726
+ }
715
727
  resolve(server);
716
728
  });
717
729
 
@@ -728,6 +740,64 @@ export async function startViewer() {
728
740
  });
729
741
  }
730
742
 
743
+ async function setupTerminalWebSocket(httpServer) {
744
+ try {
745
+ const { WebSocketServer } = await import('ws');
746
+ const { writeToPty, resizePty, onPtyData, onPtyExit, getPtyState, getOutputBuffer } = await import('./pty-manager.js');
747
+
748
+ const wss = new WebSocketServer({ server: httpServer, path: '/ws/terminal' });
749
+
750
+ wss.on('connection', (ws) => {
751
+ // 发送当前 PTY 状态
752
+ const state = getPtyState();
753
+ ws.send(JSON.stringify({ type: 'state', ...state }));
754
+
755
+ // 发送历史输出缓冲
756
+ const buffer = getOutputBuffer();
757
+ if (buffer) {
758
+ ws.send(JSON.stringify({ type: 'data', data: buffer }));
759
+ }
760
+
761
+ // PTY 输出 → WebSocket
762
+ const removeDataListener = onPtyData((data) => {
763
+ if (ws.readyState === 1) {
764
+ ws.send(JSON.stringify({ type: 'data', data }));
765
+ }
766
+ });
767
+
768
+ // PTY 退出 → WebSocket
769
+ const removeExitListener = onPtyExit((exitCode) => {
770
+ if (ws.readyState === 1) {
771
+ ws.send(JSON.stringify({ type: 'exit', exitCode }));
772
+ }
773
+ });
774
+
775
+ // WebSocket → PTY
776
+ ws.on('message', (raw) => {
777
+ try {
778
+ const msg = JSON.parse(raw.toString());
779
+ if (msg.type === 'input') {
780
+ writeToPty(msg.data);
781
+ } else if (msg.type === 'resize') {
782
+ resizePty(msg.cols, msg.rows);
783
+ }
784
+ } catch {}
785
+ });
786
+
787
+ ws.on('close', () => {
788
+ removeDataListener();
789
+ removeExitListener();
790
+ });
791
+ });
792
+ } catch (err) {
793
+ console.error('[CC Viewer] Failed to setup terminal WebSocket:', err.message);
794
+ }
795
+ }
796
+
797
+ export function getPort() {
798
+ return actualPort;
799
+ }
800
+
731
801
  export function stopViewer() {
732
802
  // 如果用户未做选择,将临时文件转为正式文件
733
803
  if (_resumeState && _resumeState.tempFile) {