cc-team-viewer 1.4.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # CC-Viewer
2
+
3
+ Claude Code 请求监控系统,实时捕获并可视化展示 Claude Code 的所有 API 请求与响应(原始文本,不做阉割)。方便开发者监控自己的 Context,以便于 Vibe Coding 过程中回顾和排查问题。
4
+
5
+ ## 使用方法
6
+
7
+ ### 安装
8
+
9
+ ```bash
10
+ npm install -g cc-viewer
11
+ ```
12
+
13
+ ### 运行与自动配置
14
+
15
+ ```bash
16
+ ccv
17
+ ```
18
+
19
+ 该命令会自动检测本地 Claude Code 的安装方式(NPM 或 Native Install)并进行适配。
20
+
21
+ - **NPM 安装**:自动向 Claude Code 的 `cli.js` 中注入拦截脚本。
22
+ - **Native Install**:自动检测 `claude` 二进制文件,配置本地透明代理,并设置 Zsh Shell Hook 自动转发流量。
23
+
24
+ ### 配置覆盖 (Configuration Override)
25
+
26
+ 如果您需要使用自定义 API 端点(例如企业代理),只需在 `~/.claude/settings.json` 中配置或设置 `ANTHROPIC_BASE_URL` 环境变量。`ccv` 会自动识别并正确转发请求。
27
+
28
+ ### 静默模式 (Silent Mode)
29
+
30
+ 默认情况下,`ccv` 在包裹 `claude` 运行时处于静默模式,确保您的终端输出保持整洁,与原生体验一致。所有日志都在后台捕获,并可通过 `http://localhost:7008` 查看。
31
+
32
+ 配置完成后,正常使用 `claude` 命令即可。访问 `http://localhost:7008` 查看监控界面。
33
+
34
+ ### 常见问题排查 (Troubleshooting)
35
+
36
+ 如果你遇到无法启动的问题,有一个终极排查方案:
37
+ 第一步:任意目录打开 claude code;
38
+ 第二步:给claude code下指令,内容如下:
39
+ ```
40
+ 我已经安装了cc-viewer这个npm包,但是执行ccv以后仍然无法有效运行。查看cc-viewer的cli.js 和 findcc.js,根据具体的环境,适配本地的claude code的部署方式。适配的时候修改范围尽量约束在findcc.js中。
41
+ ```
42
+ 让Claude Code自己检查错误是比咨询任何人以及看任何文档更有效的手段!
43
+
44
+ 以上指令完成后,会更新findcc.js。如果你的项目工程经常需要本地部署。或者fork出去的代码要经常解决安装问题,保留这个文件就可以。下次直接copy 文件。现阶段很多项目和公司用claude code都不是mac部署,而是服务端托管部署,所以作者剥离了findcc.js 这个文件,方便后续跟踪cc-viewer的源代码更新。
45
+
46
+ ### 卸载
47
+
48
+ ```bash
49
+ ccv --uninstall
50
+ ```
51
+
52
+ ### 检查版本
53
+
54
+ ```bash
55
+ ccv --version
56
+ ```
57
+
58
+ ## 功能
59
+
60
+ ### 请求监控(原文模式)
61
+ <img width="1500" height="720" alt="image" src="https://github.com/user-attachments/assets/519dd496-68bd-4e76-84d7-2a3d14ae3f61" />
62
+ - 实时捕获 Claude Code 发出的所有 API 请求,确保是原文,而不是被阉割之后的日志(这很重要!!!)
63
+ - 自动识别并标记 Main Agent 和 Sub Agent 请求(子类型:Bash、Task、Plan、General)
64
+ - MainAgent 请求支持 Body Diff JSON,折叠展示与上一次 MainAgent 请求的差异(仅显示变更/新增字段)
65
+ - 每个请求内联显示 Token 用量统计(输入/输出 Token、缓存创建/读取、命中率)
66
+ - 兼容 Claude Code Router(CCR)及其他代理场景 — 通过 API 路径模式兜底匹配请求
67
+
68
+ ### 对话模式
69
+
70
+ 点击右上角「对话模式」按钮,将 Main Agent 的完整对话历史解析为聊天界面:
71
+ <img width="1500" height="730" alt="image" src="https://github.com/user-attachments/assets/c973f142-748b-403f-b2b7-31a5d81e33e6" />
72
+
73
+
74
+ - 暂不支持Agent Team的展示
75
+ - 用户消息右对齐(蓝色气泡),Main Agent 回复左对齐(深色气泡)
76
+ - `thinking` 块默认折叠,以 Markdown 渲染,点击展开查看思考过程;支持一键翻译(功能还不稳定)
77
+ - 用户选择型消息(AskUserQuestion)以问答形式展示
78
+ - 双向模式同步:切换到对话模式时自动定位到选中请求对应的对话;切回原文模式时自动定位到选中的请求
79
+ - 设置面板:可切换工具结果和思考块的默认折叠状态
80
+ - 手机端对话浏览:在手机端 CLI 模式下,点击顶部栏的「对话浏览」按钮,即可滑出只读对话视图,在手机上浏览完整对话历史
81
+
82
+
83
+ ### 统计工具
84
+
85
+ Header 区域的「数据统计」悬浮面板:
86
+ <img width="1500" height="729" alt="image" src="https://github.com/user-attachments/assets/b23f9a81-fc3d-4937-9700-e70d84e4e5ce" />
87
+
88
+ - 显示 cache creation/read 数量及缓存命中率
89
+ - 缓存重建统计:按原因分组(TTL、system/tools/model 变更、消息截断/修改、key 变更)显示次数和 cache_creation tokens
90
+ - 工具使用统计:按调用次数排序展示各工具的调用频率
91
+ - Skill 使用统计:按调用次数排序展示各 Skill 的调用频率
92
+ - 概念帮助 (?) 图标:点击可查看 MainAgent、CacheRebuild 及各工具的内置文档
93
+
94
+ ### 日志管理
95
+
96
+ 通过左上角 CC-Viewer 下拉菜单:
97
+ <img width="1200" height="672" alt="image" src="https://github.com/user-attachments/assets/8cf24f5b-9450-4790-b781-0cd074cd3b39" />
98
+
99
+ - 导入本地日志:浏览历史日志文件,按项目分组,在新窗口打开
100
+ - 加载本地 JSONL 文件:直接选择本地 `.jsonl` 文件加载查看(支持最大 500MB)
101
+ - 当前日志另存为:下载当前监控的 JSONL 日志文件
102
+ - 合并日志:将多个 JSONL 日志文件合并为一个会话,统一分析
103
+ - 查看用户 Prompt:提取并展示所有用户输入,支持三种查看模式 — 原文模式(原始内容)、上下文模式(系统标签可折叠)、Text 模式(纯文本);斜杠命令(`/model`、`/context` 等)作为独立条目展示;命令相关标签自动从 Prompt 内容中隐藏
104
+ - 导出 Prompt 为 TXT:将用户 Prompt(纯文本,不含系统标签)导出为本地 `.txt` 文件
105
+
106
+ ### 自动更新
107
+
108
+ CC-Viewer 启动时自动检查更新(每 4 小时最多一次)。同一大版本内(如 1.x.x → 1.y.z)自动更新,下次启动生效。跨大版本仅显示通知提示。
109
+
110
+ 自动更新跟随 Claude Code 全局配置 `~/.claude/settings.json`。如果 Claude Code 禁用了自动更新(`autoUpdates: false`),CC-Viewer 也会跳过自动更新。
111
+
112
+ ## License
113
+
114
+ MIT
package/build.js ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from 'node:child_process';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { dirname } from 'node:path';
6
+
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+
9
+ // 执行 vite build,输出到 dist/
10
+ console.log('🔨 正在执行 Vite 构建...');
11
+ execSync('npx vite build --config src/vite.config.js', { cwd: __dirname, stdio: 'inherit' });
12
+
13
+ console.log('✅ Build 完成,输出目录: dist/');
14
+ console.log(' - dist/index.html');
15
+ console.log(' - dist/assets/');
package/cli/cli.js ADDED
@@ -0,0 +1,446 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync, writeFileSync, existsSync, realpathSync } from 'node:fs';
4
+ import { resolve } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { homedir } from 'node:os';
7
+ import { spawn } from 'node:child_process';
8
+ import { INJECT_IMPORT, resolveCliPath, resolveNativePath, buildShellCandidates } from './findcc.js';
9
+
10
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
11
+
12
+ const INJECT_START = '// >>> Start CC Viewer Web Service >>>';
13
+ const INJECT_END = '// <<< Start CC Viewer Web Service <<<';
14
+ const INJECT_BLOCK = `${INJECT_START}\n${INJECT_IMPORT}\n${INJECT_END}`;
15
+
16
+
17
+ const SHELL_HOOK_START = '# >>> CC-Team-Viewer Auto-Inject >>>';
18
+ const SHELL_HOOK_END = '# <<< CC-Team-Viewer Auto-Inject <<<';
19
+
20
+ const cliPath = resolveCliPath();
21
+
22
+ function getShellConfigPath() {
23
+ const shell = process.env.SHELL || '';
24
+ if (shell.includes('zsh')) return resolve(homedir(), '.zshrc');
25
+ if (shell.includes('bash')) {
26
+ const bashProfile = resolve(homedir(), '.bash_profile');
27
+ if (process.platform === 'darwin' && existsSync(bashProfile)) return bashProfile;
28
+ return resolve(homedir(), '.bashrc');
29
+ }
30
+ return resolve(homedir(), '.zshrc');
31
+ }
32
+
33
+ function buildShellHook(isNative) {
34
+ // Commands/flags that should pass through directly without cctv interception
35
+ // These are non-interactive commands that don't involve API calls
36
+ const passthroughCommands = [
37
+ // Subcommands (no API calls)
38
+ 'doctor', // health check for auto-updater
39
+ 'install', // install native build
40
+ 'update', // self-update
41
+ 'upgrade', // alias for update
42
+ 'auth', // authentication management
43
+ 'setup-token', // token setup
44
+ 'agents', // list configured agents
45
+ 'mcp', // MCP server configuration
46
+ ];
47
+
48
+ const passthroughFlags = [
49
+ // Version/help info
50
+ '--version', '-v', '--v',
51
+ '--help', '-h',
52
+ ];
53
+
54
+ if (isNative) {
55
+ return `${SHELL_HOOK_START}
56
+ claude() {
57
+ # Avoid recursion if cctv invokes claude
58
+ if [ "$1" = "--cctv-internal" ]; then
59
+ shift
60
+ command claude "$@"
61
+ return
62
+ fi
63
+ # Pass through certain commands directly without cctv interception
64
+ case "$1" in
65
+ ${passthroughCommands.join('|')})
66
+ command claude "$@"
67
+ return
68
+ ;;
69
+ ${passthroughFlags.join('|')})
70
+ command claude "$@"
71
+ return
72
+ ;;
73
+ esac
74
+ cctv run -- claude --cctv-internal "$@"
75
+ }
76
+ ${SHELL_HOOK_END}`;
77
+ }
78
+
79
+ const candidates = buildShellCandidates();
80
+ return `${SHELL_HOOK_START}
81
+ claude() {
82
+ local cli_js=""
83
+ for candidate in ${candidates}; do
84
+ if [ -f "$candidate" ]; then
85
+ cli_js="$candidate"
86
+ break
87
+ fi
88
+ done
89
+ if [ -n "$cli_js" ] && ! grep -q "CC Viewer" "$cli_js" 2>/dev/null; then
90
+ cctv 2>/dev/null
91
+ fi
92
+ command claude "$@"
93
+ }
94
+ ${SHELL_HOOK_END}`;
95
+ }
96
+
97
+ function installShellHook(isNative) {
98
+ const configPath = getShellConfigPath();
99
+ try {
100
+ let content = existsSync(configPath) ? readFileSync(configPath, 'utf-8') : '';
101
+
102
+ if (content.includes(SHELL_HOOK_START)) {
103
+ // Check if existing hook matches desired mode
104
+ const isNativeHook = content.includes('cctv run -- claude');
105
+ if (!!isNative === !!isNativeHook) {
106
+ return { path: configPath, status: 'exists' };
107
+ }
108
+ // Mismatch: remove old hook first
109
+ removeShellHook();
110
+ content = existsSync(configPath) ? readFileSync(configPath, 'utf-8') : '';
111
+ }
112
+
113
+ const hook = buildShellHook(isNative);
114
+ const newContent = content.endsWith('\n') ? content + '\n' + hook + '\n' : content + '\n\n' + hook + '\n';
115
+ writeFileSync(configPath, newContent);
116
+ return { path: configPath, status: 'installed' };
117
+ } catch (err) {
118
+ return { path: configPath, status: 'error', error: err.message };
119
+ }
120
+ }
121
+
122
+ function removeShellHook() {
123
+ const configPath = getShellConfigPath();
124
+ try {
125
+ if (!existsSync(configPath)) return { path: configPath, status: 'not_found' };
126
+ const content = readFileSync(configPath, 'utf-8');
127
+ if (!content.includes(SHELL_HOOK_START)) return { path: configPath, status: 'clean' };
128
+ const regex = new RegExp(`\\n?${SHELL_HOOK_START}[\\s\\S]*?${SHELL_HOOK_END}\\n?`, 'g');
129
+ const newContent = content.replace(regex, '\n');
130
+ writeFileSync(configPath, newContent);
131
+ return { path: configPath, status: 'removed' };
132
+ } catch (err) {
133
+ return { path: configPath, status: 'error', error: err.message };
134
+ }
135
+ }
136
+
137
+ function injectCliJs() {
138
+ const content = readFileSync(cliPath, 'utf-8');
139
+ if (content.includes(INJECT_START)) {
140
+ return 'exists';
141
+ }
142
+ const lines = content.split('\n');
143
+ lines.splice(2, 0, INJECT_BLOCK);
144
+ writeFileSync(cliPath, lines.join('\n'));
145
+ return 'injected';
146
+ }
147
+
148
+ function removeCliJsInjection() {
149
+ try {
150
+ if (!existsSync(cliPath)) return 'not_found';
151
+ const content = readFileSync(cliPath, 'utf-8');
152
+ if (!content.includes(INJECT_START)) return 'clean';
153
+ const regex = new RegExp(`${INJECT_START}\\n${INJECT_IMPORT}\\n${INJECT_END}\\n?`, 'g');
154
+ writeFileSync(cliPath, content.replace(regex, ''));
155
+ return 'removed';
156
+ } catch {
157
+ return 'error';
158
+ }
159
+ }
160
+
161
+ async function runProxyCommand(args) {
162
+ try {
163
+ // Dynamic import to avoid side effects when just installing
164
+ const { startProxy } = await import('../proxy/proxy.js');
165
+ const proxyPort = await startProxy();
166
+
167
+ // args = ['run', '--', 'command', 'claude', ...] or ['run', 'claude', ...]
168
+ // Our hook uses: cctv run -- claude --cctv-internal "$@"
169
+ // args[0] is 'run'.
170
+ // If args[1] is '--', then command starts at args[2].
171
+
172
+ let cmdStartIndex = 1;
173
+ if (args[1] === '--') {
174
+ cmdStartIndex = 2;
175
+ }
176
+
177
+ let cmd = args[cmdStartIndex];
178
+ if (!cmd) {
179
+ console.error('No command provided to run.');
180
+ process.exit(1);
181
+ }
182
+ let cmdArgs = args.slice(cmdStartIndex + 1);
183
+
184
+ // If cmd is 'claude' and next arg is '--cctv-internal', remove it
185
+ // and we must use 'command claude' to avoid infinite recursion of the shell function?
186
+ // Node spawn doesn't use shell functions, so 'claude' should resolve to the binary in PATH.
187
+ // BUT, if 'claude' is a function in the current shell, spawn won't see it unless we use shell:true.
188
+ // We are using shell:false (default).
189
+ // So spawn('claude') should find /usr/local/bin/claude (the binary).
190
+ // The issue might be that cctv itself is running in a way that PATH is weird?
191
+
192
+ // Wait, the shell hook adds '--cctv-internal'. We should strip it before spawning.
193
+ if (cmdArgs[0] === '--cctv-internal') {
194
+ cmdArgs.shift();
195
+ }
196
+
197
+ const env = { ...process.env };
198
+ // Determine the path to the native 'claude' executable
199
+ if (cmd === 'claude') {
200
+ const nativePath = resolveNativePath();
201
+ if (nativePath) {
202
+ cmd = nativePath;
203
+ }
204
+ }
205
+
206
+ // 保存原始 API URL,供 proxy.js 使用
207
+ // 如果 ANTHROPIC_BASE_URL 未设置或指向 localhost,使用默认值
208
+ const originalApiUrl = process.env.ANTHROPIC_BASE_URL && !process.env.ANTHROPIC_BASE_URL.includes('127.0.0.1') && !process.env.ANTHROPIC_BASE_URL.includes('localhost')
209
+ ? process.env.ANTHROPIC_BASE_URL
210
+ : 'https://api.anthropic.com';
211
+
212
+ env.CC_ORIGINAL_API_URL = originalApiUrl;
213
+ env.ANTHROPIC_BASE_URL = `http://127.0.0.1:${proxyPort}`;
214
+ env.CCV_PROXY_MODE = '1'; // 告诉 interceptor.js 不要再启动 server
215
+
216
+ const settingsJson = JSON.stringify({
217
+ env: {
218
+ ANTHROPIC_BASE_URL: env.ANTHROPIC_BASE_URL,
219
+ CC_ORIGINAL_API_URL: env.CC_ORIGINAL_API_URL
220
+ }
221
+ });
222
+
223
+ cmdArgs.unshift(settingsJson);
224
+ cmdArgs.unshift('--settings');
225
+
226
+ const child = spawn(cmd, cmdArgs, { stdio: 'inherit', env });
227
+
228
+ child.on('exit', (code) => {
229
+ process.exit(code);
230
+ });
231
+
232
+ child.on('error', (err) => {
233
+ console.error('Failed to start command:', err);
234
+ process.exit(1);
235
+ });
236
+ } catch (err) {
237
+ console.error('Proxy error:', err);
238
+ process.exit(1);
239
+ }
240
+ }
241
+
242
+ async function runCliMode(extraClaudeArgs = []) {
243
+ const nativePath = resolveNativePath();
244
+ if (!nativePath) {
245
+ console.error('错误: 未找到 claude 命令,请确认已安装 Claude Code');
246
+ process.exit(1);
247
+ }
248
+
249
+ console.log('正在启动 CC Viewer CLI 模式...');
250
+
251
+ // 2. 设置 CLI 模式标记(必须在 import proxy.js 之前,
252
+ // 因为 proxy.js → interceptor.js 可能触发 server.js 加载,
253
+ // server.js 的 isCliMode 在模块顶层求值且只执行一次)
254
+ process.env.CCV_CLI_MODE = '1';
255
+
256
+ // 1. 启动代理
257
+ const { startProxy } = await import('../proxy/proxy.js');
258
+ const proxyPort = await startProxy();
259
+
260
+ // 3. 启动 HTTP 服务器
261
+ const { getPort, stopViewer } = await import('../server/server.js');
262
+
263
+ // 等待服务器启动完成
264
+ await new Promise(resolve => {
265
+ const check = () => {
266
+ const port = getPort();
267
+ if (port) resolve(port);
268
+ else setTimeout(check, 100);
269
+ };
270
+ setTimeout(check, 200);
271
+ });
272
+
273
+ const port = getPort();
274
+
275
+ // 3. 自动打开浏览器
276
+ const url = `http://127.0.0.1:${port}`;
277
+ try {
278
+ const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
279
+ const { execSync } = await import('node:child_process');
280
+ execSync(`${cmd} ${url}`, { stdio: 'ignore', timeout: 5000 });
281
+ } catch {}
282
+
283
+ console.log(`CC Viewer: ${url}`);
284
+
285
+ // 4. 注册退出处理
286
+ const cleanup = () => {
287
+ stopViewer();
288
+ process.exit();
289
+ };
290
+ process.on('SIGINT', cleanup);
291
+ process.on('SIGTERM', cleanup);
292
+ }
293
+
294
+ // === 主逻辑 ===
295
+
296
+ const args = process.argv.slice(2);
297
+ const isUninstall = args.includes('--uninstall');
298
+ const isHelp = args.includes('--help') || args.includes('-h') || args[0] === 'help';
299
+ const isVersion = args.includes('--v') || args.includes('--version') || args.includes('-v');
300
+ const isCliMode = args.includes('--c') || args.includes('-c');
301
+ const isDangerousMode = args.includes('-d') || args.includes('--d');
302
+
303
+ if (isHelp) {
304
+ console.log(`CC Viewer CLI\n\n用法:\n cctv [options]\n cctv run -- <command> [args...]\n\n选项:\n -h, --help 显示帮助\n -v, --version 显示版本\n -c, --c CLI 模式:自动打开浏览器\n -d, --d Dangerous 模式:CLI 模式 + --dangerously-skip-permissions\n --uninstall 移除 CC Viewer 集成\n\n说明:\n 直接运行 cctv 将安装/修复 Claude Code 的集成 Hook。`);
305
+ process.exit(0);
306
+ }
307
+
308
+ if (isVersion) {
309
+ try {
310
+ const pkg = JSON.parse(readFileSync(resolve(__dirname, 'package.json'), 'utf-8'));
311
+ console.log(`cctv v${pkg.version}`);
312
+ } catch (e) {
313
+ console.error('Failed to read version:', e.message);
314
+ }
315
+ process.exit(0);
316
+ }
317
+
318
+ if (isCliMode || isDangerousMode) {
319
+ const extraArgs = isDangerousMode ? ['--dangerously-skip-permissions'] : [];
320
+ runCliMode(extraArgs).catch(err => {
321
+ console.error('CLI mode error:', err);
322
+ process.exit(1);
323
+ });
324
+ } else if (args[0] === 'run') {
325
+ runProxyCommand(args);
326
+ } else if (isUninstall) {
327
+ const cliResult = removeCliJsInjection();
328
+ const shellResult = removeShellHook();
329
+
330
+ if (cliResult === 'removed' || cliResult === 'clean') {
331
+ console.log('cli.js 已清理');
332
+ } else if (cliResult === 'not_found') {
333
+ // Silent is better for mixed mode uninstall
334
+ } else {
335
+ console.log('cli.js 清理失败');
336
+ }
337
+
338
+ if (shellResult.status === 'removed') {
339
+ console.log(`shell hook 已从 ${shellResult.path} 移除`);
340
+ } else if (shellResult.status === 'clean' || shellResult.status === 'not_found') {
341
+ console.log(`${shellResult.path} 中无需清理`);
342
+ } else {
343
+ console.log(`shell hook 清理失败: ${shellResult.error}`);
344
+ }
345
+
346
+ console.log('CC Viewer 集成已移除 (运行 \'npm uninstall -g cctv\' 以彻底删除命令)');
347
+ process.exit(0);
348
+ } else {
349
+ // Installation Logic
350
+ let mode = 'unknown';
351
+
352
+ // Check PATH to determine priority
353
+ let prefersNative = true; // default to native if not found in PATH
354
+ const paths = (process.env.PATH || '').split(':');
355
+ for (const dir of paths) {
356
+ if (!dir) continue;
357
+ const exePath = resolve(dir, 'claude');
358
+ if (existsSync(exePath)) {
359
+ try {
360
+ const real = realpathSync(exePath);
361
+ if (real.includes('node_modules')) {
362
+ prefersNative = false;
363
+ } else {
364
+ prefersNative = true;
365
+ }
366
+ break;
367
+ } catch (e) {
368
+ // ignore
369
+ }
370
+ }
371
+ }
372
+
373
+ const nativePath = resolveNativePath();
374
+ const hasNpm = existsSync(cliPath);
375
+
376
+ if (prefersNative) {
377
+ if (nativePath) {
378
+ mode = 'native';
379
+ } else if (hasNpm) {
380
+ mode = 'npm';
381
+ }
382
+ } else {
383
+ if (hasNpm) {
384
+ mode = 'npm';
385
+ } else if (nativePath) {
386
+ mode = 'native';
387
+ }
388
+ }
389
+
390
+ if (mode === 'unknown') {
391
+ console.error(`找不到 Claude Code cli.js: ${cliPath}`);
392
+ console.error('Also could not find native "claude" command in PATH.');
393
+ console.error('Please make sure @anthropic-ai/claude-code is installed.');
394
+ process.exit(1);
395
+ }
396
+
397
+ if (mode === 'npm') {
398
+ try {
399
+ const cliResult = injectCliJs();
400
+ const shellResult = installShellHook(false);
401
+
402
+ if (cliResult === 'exists' && shellResult.status === 'exists') {
403
+ console.log('CC Viewer 已经能工作了');
404
+ } else {
405
+ if (cliResult === 'exists') {
406
+ console.log('CC Viewer 已安装,无需重复操作');
407
+ } else {
408
+ console.log('CC Viewer 安装成功');
409
+ }
410
+
411
+ if (shellResult.status === 'installed') {
412
+ console.log('All READY!');
413
+ } else if (shellResult.status !== 'exists') {
414
+ console.log(`shell hook 写入失败: ${shellResult.error}`);
415
+ }
416
+ }
417
+ console.log('如需卸载,请运行: cctv --uninstall');
418
+ } catch (err) {
419
+ if (err.code === 'ENOENT') {
420
+ console.error(`找不到 Claude Code cli.js: ${cliPath}`);
421
+ console.error('请确认 @anthropic-ai/claude-code 已安装');
422
+ } else {
423
+ console.error(`安装失败: ${err.message}`);
424
+ }
425
+ process.exit(1);
426
+ }
427
+ } else {
428
+ // Native Mode
429
+ try {
430
+ console.log('Detected Claude Code Native Install.');
431
+ const shellResult = installShellHook(true);
432
+
433
+ if (shellResult.status === 'exists') {
434
+ console.log('CC Viewer 已经能工作了');
435
+ } else if (shellResult.status === 'installed') {
436
+ console.log('Native Hook Installed! All READY!');
437
+ } else {
438
+ console.log(`shell hook 写入失败: ${shellResult.error}`);
439
+ }
440
+ console.log('如需卸载,请运行: cctv --uninstall');
441
+ } catch (err) {
442
+ console.error('Failed to install native hook:', err);
443
+ process.exit(1);
444
+ }
445
+ }
446
+ }
package/cli/findcc.js ADDED
@@ -0,0 +1,107 @@
1
+ import { resolve, join } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { existsSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { execSync } from 'node:child_process';
6
+
7
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
8
+
9
+ // ============ 配置区(第三方适配只需修改此处)============
10
+
11
+ // 日志存储根目录(所有项目日志、偏好设置均存放于此)
12
+ export const LOG_DIR = join(homedir(), '.claude', 'cc-team-viewer');
13
+
14
+ // npm 包名候选列表(按优先级排列)
15
+ export const PACKAGES = ['@anthropic-ai/claude-code', '@ali/claude-code'];
16
+
17
+ // npm 包内的入口文件(相对于包根目录)
18
+ export const CLI_ENTRY = 'cli.js';
19
+
20
+ // native 二进制候选路径(~ 会在运行时展开为 homedir())
21
+ const NATIVE_CANDIDATES = [
22
+ '~/.claude/local/claude',
23
+ '/usr/local/bin/claude',
24
+ '~/.local/bin/claude',
25
+ '/opt/homebrew/bin/claude',
26
+ ];
27
+
28
+ // 用于 which/command -v 查找的命令名
29
+ export const BINARY_NAME = 'claude';
30
+
31
+ // 注入到 cli.js 的 import 语句(相对路径,基于 cli.js 所在位置)
32
+ export const INJECT_IMPORT = "import '../../cc-viewer/proxy/interceptor.js';";
33
+
34
+ // ============ 导出函数 ============
35
+
36
+ export function getGlobalNodeModulesDir() {
37
+ try {
38
+ return execSync('npm root -g', { encoding: 'utf-8' }).trim();
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ export function resolveCliPath() {
45
+ // 候选基础目录:__dirname 的上级(适用于常规 npm 安装)+ 全局 node_modules(适用于符号链接安装)
46
+ const baseDirs = [resolve(__dirname, '..')];
47
+ const globalRoot = getGlobalNodeModulesDir();
48
+ if (globalRoot && globalRoot !== resolve(__dirname, '..')) {
49
+ baseDirs.push(globalRoot);
50
+ }
51
+
52
+ for (const baseDir of baseDirs) {
53
+ for (const packageName of PACKAGES) {
54
+ const candidate = join(baseDir, packageName, CLI_ENTRY);
55
+ if (existsSync(candidate)) {
56
+ return candidate;
57
+ }
58
+ }
59
+ }
60
+ // 兜底:返回全局目录下的默认路径,便于错误提示
61
+ return join(globalRoot || resolve(__dirname, '..'), PACKAGES[0], CLI_ENTRY);
62
+ }
63
+
64
+ export function resolveNativePath() {
65
+ // 1. 尝试 which/command -v(继承当前 process.env PATH)
66
+ for (const cmd of [`which ${BINARY_NAME}`, `command -v ${BINARY_NAME}`]) {
67
+ try {
68
+ const result = execSync(cmd, { encoding: 'utf-8', shell: true, env: process.env }).trim();
69
+ // 排除 shell function 的输出(多行说明不是路径)
70
+ if (result && !result.includes('\n') && existsSync(result)) {
71
+ return result;
72
+ }
73
+ } catch {
74
+ // ignore
75
+ }
76
+ }
77
+
78
+ // 2. 检查常见 native 安装路径
79
+ const home = homedir();
80
+ const candidates = NATIVE_CANDIDATES.map(p =>
81
+ p.startsWith('~') ? join(home, p.slice(2)) : p
82
+ );
83
+ for (const p of candidates) {
84
+ if (existsSync(p)) {
85
+ return p;
86
+ }
87
+ }
88
+
89
+ return null;
90
+ }
91
+
92
+ export function buildShellCandidates() {
93
+ const globalRoot = getGlobalNodeModulesDir();
94
+ // 使用 $HOME 而非硬编码绝对路径,保证 shell 可移植性
95
+ const dirs = [];
96
+ if (globalRoot) {
97
+ // 将绝对路径中的 homedir 替换为 $HOME
98
+ const home = homedir();
99
+ const shellRoot = globalRoot.startsWith(home)
100
+ ? '$HOME' + globalRoot.slice(home.length)
101
+ : globalRoot;
102
+ for (const pkg of PACKAGES) {
103
+ dirs.push(`"${shellRoot}/${pkg}/${CLI_ENTRY}"`);
104
+ }
105
+ }
106
+ return dirs.join(' ');
107
+ }