cc-viewer 0.1.5 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -23
- package/cli.js +142 -16
- package/i18n.js +85 -0
- package/interceptor.js +3 -1
- package/lib/assets/index-BX8W4MxH.js +551 -0
- package/lib/index.html +1 -1
- package/lib/server.js +191 -42
- package/locales/ar.json +62 -0
- package/locales/da.json +62 -0
- package/locales/de.json +62 -0
- package/locales/en.json +62 -0
- package/locales/es.json +62 -0
- package/locales/fr.json +62 -0
- package/locales/it.json +62 -0
- package/locales/ja.json +62 -0
- package/locales/ko.json +62 -0
- package/locales/no.json +62 -0
- package/locales/pl.json +62 -0
- package/locales/pt-BR.json +62 -0
- package/locales/ru.json +62 -0
- package/locales/th.json +62 -0
- package/locales/tr.json +62 -0
- package/locales/uk.json +62 -0
- package/locales/zh-TW.json +62 -0
- package/locales/zh.json +62 -0
- package/package.json +4 -2
- package/lib/assets/index-W-UhJiNi.js +0 -401
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
# CC
|
|
1
|
+
# CC-Viewer
|
|
2
2
|
|
|
3
|
-
Claude Code
|
|
3
|
+
Claude Code 请求监控系统,实时捕获并可视化展示 Claude Code 的所有 API 请求与响应。方便开发者监控自己的 Context,以便于 Vibe Coding 过程中回顾和排查问题。
|
|
4
|
+
|
|
5
|
+
[English](./docs/README.en.md) | [繁體中文](./docs/README.zh-TW.md) | [한국어](./docs/README.ko.md) | [日本語](./docs/README.ja.md) | [Deutsch](./docs/README.de.md) | [Español](./docs/README.es.md) | [Français](./docs/README.fr.md) | [Italiano](./docs/README.it.md) | [Dansk](./docs/README.da.md) | [Polski](./docs/README.pl.md) | [Русский](./docs/README.ru.md) | [العربية](./docs/README.ar.md) | [Norsk](./docs/README.no.md) | [Português (Brasil)](./docs/README.pt-BR.md) | [ไทย](./docs/README.th.md) | [Türkçe](./docs/README.tr.md) | [Українська](./docs/README.uk.md)
|
|
4
6
|
|
|
5
7
|
## 使用方法
|
|
6
8
|
|
|
@@ -14,7 +16,17 @@ npm install -g cc-viewer
|
|
|
14
16
|
ccv
|
|
15
17
|
```
|
|
16
18
|
|
|
17
|
-
该命令会自动将监控脚本注入到本地安装的 Claude Code
|
|
19
|
+
该命令会自动将监控脚本注入到本地安装的 Claude Code 中,并在 shell 配置文件(`~/.zshrc` 或 `~/.bashrc`)中添加自动重注入 hook。之后正常使用 Claude Code,打开浏览器访问 `http://localhost:7008` 即可查看监控界面。
|
|
20
|
+
|
|
21
|
+
Claude Code 更新后无需手动操作,下次运行 `claude` 时会自动检测并重新注入。
|
|
22
|
+
|
|
23
|
+
### 卸载
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
ccv --uninstall
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
一键清理 cli.js 中的注入代码和 shell 配置文件中的 hook。
|
|
18
30
|
|
|
19
31
|
## 功能
|
|
20
32
|
|
|
@@ -31,35 +43,40 @@ ccv
|
|
|
31
43
|
|
|
32
44
|
### 对话模式
|
|
33
45
|
|
|
34
|
-
|
|
46
|
+
点击右上角「对话模式」按钮,将 Main Agent 的完整对话历史解析为聊天界面:
|
|
35
47
|
|
|
36
|
-
-
|
|
48
|
+
- 用户消息左对齐(蓝色气泡)
|
|
37
49
|
- Main Agent 回复左对齐(深灰气泡),支持 Markdown 渲染
|
|
38
|
-
-
|
|
50
|
+
- 工具调用结果内联显示在对应的 Assistant 消息内部
|
|
39
51
|
- `thinking` 块默认折叠,点击展开查看思考过程
|
|
40
|
-
- `tool_use`
|
|
41
|
-
-
|
|
52
|
+
- `tool_use` 显示为紧凑的工具调用卡片(Bash、Read、Edit、Write、Glob、Grep、Task 等均有专属展示)
|
|
53
|
+
- 用户选择型消息(AskUserQuestion)以问答形式展示
|
|
54
|
+
- 系统注入标签(`<system-reminder>`、`<project-reminder>` 等)自动折叠
|
|
42
55
|
- 自动过滤系统注入文本,只展示用户的真实输入
|
|
56
|
+
- 支持多 session 分段展示(`/compact`、`/clear` 等操作后自动分段)
|
|
57
|
+
- 每条消息显示精确到秒的时间戳
|
|
58
|
+
|
|
59
|
+
### Token 消耗统计
|
|
60
|
+
|
|
61
|
+
Header 区域的「Token 消耗统计」悬浮面板:
|
|
62
|
+
|
|
63
|
+
- 按模型分组统计 input/output token 数量
|
|
64
|
+
- 显示 cache creation/read 数量及缓存命中率
|
|
65
|
+
- Main Agent 缓存失效倒计时
|
|
66
|
+
|
|
67
|
+
### 日志管理
|
|
43
68
|
|
|
44
|
-
|
|
69
|
+
通过左上角 CC-Viewer 下拉菜单:
|
|
45
70
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
| Agent 文本回复 | `role: "assistant"` + `type: "text"` | 左对齐 Markdown 渲染 |
|
|
50
|
-
| Agent 工具调用 | `role: "assistant"` + `type: "tool_use"` | 工具调用卡片 |
|
|
51
|
-
| Agent 思考 | `role: "assistant"` + `type: "thinking"` | 可折叠思考块 |
|
|
52
|
-
| 工具返回 | `role: "user"` + `type: "tool_result"` | 关联到对应 tool_use |
|
|
71
|
+
- 导入本地日志:浏览历史日志文件,按项目分组,在新窗口打开
|
|
72
|
+
- 当前日志另存为:下载当前监控的 JSONL 日志文件
|
|
73
|
+
- 导出用户 Prompt:提取并展示所有用户输入,支持 system-reminder 折叠查看
|
|
53
74
|
|
|
54
|
-
|
|
75
|
+
### 多语言支持
|
|
55
76
|
|
|
56
|
-
|
|
77
|
+
CC-Viewer 支持 18 种语言,根据系统语言环境自动切换:
|
|
57
78
|
|
|
58
|
-
|
|
59
|
-
- JSON 渲染:[@alenaksu/json-viewer](https://github.com/nicolo-ribaudo/json-viewer)
|
|
60
|
-
- Markdown 渲染:[marked](https://github.com/markedjs/marked)
|
|
61
|
-
- 后端:Node.js 原生 HTTP 服务 + SSE 实时推送
|
|
62
|
-
- 请求拦截:通过 `globalThis.fetch` 拦截,支持流式响应组装
|
|
79
|
+
简体中文 | English | 繁體中文 | 한국어 | Deutsch | Español | Français | Italiano | Dansk | 日本語 | Polski | Русский | العربية | Norsk | Português (Brasil) | ไทย | Türkçe | Українська
|
|
63
80
|
|
|
64
81
|
## License
|
|
65
82
|
|
package/cli.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import { readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'node:fs';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { t } from './i18n.js';
|
|
6
8
|
|
|
7
9
|
const __dirname = fileURLToPath(new URL('.', import.meta.url));
|
|
8
10
|
|
|
@@ -10,30 +12,154 @@ const INJECT_START = '// >>> Start CC Viewer Web Service >>>';
|
|
|
10
12
|
const INJECT_END = '// <<< Start CC Viewer Web Service <<<';
|
|
11
13
|
const INJECT_IMPORT = "import '../../cc-viewer/interceptor.js';";
|
|
12
14
|
const INJECT_BLOCK = `${INJECT_START}\n${INJECT_IMPORT}\n${INJECT_END}`;
|
|
15
|
+
const SHOW_ALL_FILE = '/tmp/cc-viewer-show-all';
|
|
16
|
+
|
|
17
|
+
const SHELL_HOOK_START = '# >>> CC-Viewer Auto-Inject >>>';
|
|
18
|
+
const SHELL_HOOK_END = '# <<< CC-Viewer Auto-Inject <<<';
|
|
13
19
|
|
|
14
|
-
// Claude Code cli.js 的路径
|
|
15
20
|
const cliPath = resolve(__dirname, '../@anthropic-ai/claude-code/cli.js');
|
|
16
21
|
|
|
17
|
-
|
|
18
|
-
const
|
|
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() {
|
|
34
|
+
return `${SHELL_HOOK_START}
|
|
35
|
+
claude() {
|
|
36
|
+
local cli_js="$HOME/.npm-global/lib/node_modules/@anthropic-ai/claude-code/cli.js"
|
|
37
|
+
if [ -f "$cli_js" ] && ! grep -q "CC Viewer" "$cli_js" 2>/dev/null; then
|
|
38
|
+
ccv 2>/dev/null
|
|
39
|
+
fi
|
|
40
|
+
command claude "$@"
|
|
41
|
+
}
|
|
42
|
+
${SHELL_HOOK_END}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function installShellHook() {
|
|
46
|
+
const configPath = getShellConfigPath();
|
|
47
|
+
try {
|
|
48
|
+
const content = existsSync(configPath) ? readFileSync(configPath, 'utf-8') : '';
|
|
49
|
+
if (content.includes(SHELL_HOOK_START)) {
|
|
50
|
+
return { path: configPath, status: 'exists' };
|
|
51
|
+
}
|
|
52
|
+
const hook = buildShellHook();
|
|
53
|
+
const newContent = content.endsWith('\n') ? content + '\n' + hook + '\n' : content + '\n\n' + hook + '\n';
|
|
54
|
+
writeFileSync(configPath, newContent);
|
|
55
|
+
return { path: configPath, status: 'installed' };
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return { path: configPath, status: 'error', error: err.message };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
19
60
|
|
|
61
|
+
function removeShellHook() {
|
|
62
|
+
const configPath = getShellConfigPath();
|
|
63
|
+
try {
|
|
64
|
+
if (!existsSync(configPath)) return { path: configPath, status: 'not_found' };
|
|
65
|
+
const content = readFileSync(configPath, 'utf-8');
|
|
66
|
+
if (!content.includes(SHELL_HOOK_START)) return { path: configPath, status: 'clean' };
|
|
67
|
+
const regex = new RegExp(`\\n?${SHELL_HOOK_START}[\\s\\S]*?${SHELL_HOOK_END}\\n?`, 'g');
|
|
68
|
+
const newContent = content.replace(regex, '\n');
|
|
69
|
+
writeFileSync(configPath, newContent);
|
|
70
|
+
return { path: configPath, status: 'removed' };
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return { path: configPath, status: 'error', error: err.message };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function injectCliJs() {
|
|
77
|
+
const content = readFileSync(cliPath, 'utf-8');
|
|
20
78
|
if (content.includes(INJECT_START)) {
|
|
21
|
-
|
|
79
|
+
return 'exists';
|
|
80
|
+
}
|
|
81
|
+
const lines = content.split('\n');
|
|
82
|
+
lines.splice(2, 0, INJECT_BLOCK);
|
|
83
|
+
writeFileSync(cliPath, lines.join('\n'));
|
|
84
|
+
return 'injected';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function removeCliJsInjection() {
|
|
88
|
+
try {
|
|
89
|
+
if (!existsSync(cliPath)) return 'not_found';
|
|
90
|
+
const content = readFileSync(cliPath, 'utf-8');
|
|
91
|
+
if (!content.includes(INJECT_START)) return 'clean';
|
|
92
|
+
const regex = new RegExp(`${INJECT_START}\\n${INJECT_IMPORT}\\n${INJECT_END}\\n?`, 'g');
|
|
93
|
+
writeFileSync(cliPath, content.replace(regex, ''));
|
|
94
|
+
return 'removed';
|
|
95
|
+
} catch {
|
|
96
|
+
return 'error';
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// === 主逻辑 ===
|
|
101
|
+
|
|
102
|
+
const isUninstall = process.argv.includes('--uninstall');
|
|
103
|
+
|
|
104
|
+
if (isUninstall) {
|
|
105
|
+
const cliResult = removeCliJsInjection();
|
|
106
|
+
const shellResult = removeShellHook();
|
|
107
|
+
|
|
108
|
+
if (cliResult === 'removed' || cliResult === 'clean') {
|
|
109
|
+
console.log(t('cli.uninstall.cliCleaned'));
|
|
110
|
+
} else if (cliResult === 'not_found') {
|
|
111
|
+
console.log(t('cli.uninstall.cliNotFound'));
|
|
112
|
+
} else {
|
|
113
|
+
console.log(t('cli.uninstall.cliFail'));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (shellResult.status === 'removed') {
|
|
117
|
+
console.log(t('cli.uninstall.hookRemoved', { path: shellResult.path }));
|
|
118
|
+
} else if (shellResult.status === 'clean' || shellResult.status === 'not_found') {
|
|
119
|
+
console.log(t('cli.uninstall.hookClean', { path: shellResult.path }));
|
|
120
|
+
} else {
|
|
121
|
+
console.log(t('cli.uninstall.hookFail', { error: shellResult.error }));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
try { unlinkSync(SHOW_ALL_FILE); } catch {}
|
|
125
|
+
|
|
126
|
+
console.log(t('cli.uninstall.done'));
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 正常安装流程
|
|
131
|
+
const showAll = process.argv.includes('--all');
|
|
132
|
+
if (showAll) {
|
|
133
|
+
try { writeFileSync(SHOW_ALL_FILE, '1'); } catch {}
|
|
134
|
+
} else {
|
|
135
|
+
try { unlinkSync(SHOW_ALL_FILE); } catch {}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const cliResult = injectCliJs();
|
|
140
|
+
if (cliResult === 'exists') {
|
|
141
|
+
console.log(t('cli.inject.exists'));
|
|
22
142
|
} else {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
143
|
+
console.log(t('cli.inject.success'));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const shellResult = installShellHook();
|
|
147
|
+
if (shellResult.status === 'installed') {
|
|
148
|
+
console.log(t('cli.hook.installed', { path: shellResult.path }));
|
|
149
|
+
} else if (shellResult.status === 'exists') {
|
|
150
|
+
console.log(t('cli.hook.exists', { path: shellResult.path }));
|
|
151
|
+
} else {
|
|
152
|
+
console.log(t('cli.hook.fail', { error: shellResult.error }));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
console.log(t('cli.usage.hint'));
|
|
156
|
+
console.log(t('cli.usage.uninstallHint'));
|
|
31
157
|
} catch (err) {
|
|
32
158
|
if (err.code === 'ENOENT') {
|
|
33
|
-
console.error('
|
|
34
|
-
console.error('
|
|
159
|
+
console.error(t('cli.inject.notFound', { path: cliPath }));
|
|
160
|
+
console.error(t('cli.inject.notFoundHint'));
|
|
35
161
|
} else {
|
|
36
|
-
console.error('
|
|
162
|
+
console.error(t('cli.inject.fail', { error: err.message }));
|
|
37
163
|
}
|
|
38
164
|
process.exit(1);
|
|
39
165
|
}
|
package/i18n.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
const SUPPORTED_LANGS = [
|
|
8
|
+
'zh', 'en', 'zh-TW', 'ko', 'de', 'es', 'fr', 'it', 'da', 'ja',
|
|
9
|
+
'pl', 'ru', 'ar', 'no', 'pt-BR', 'th', 'tr', 'uk',
|
|
10
|
+
];
|
|
11
|
+
|
|
12
|
+
function loadLocale(lang) {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(join(__dirname, 'locales', `${lang}.json`), 'utf-8'));
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const locales = {};
|
|
21
|
+
for (const lang of SUPPORTED_LANGS) {
|
|
22
|
+
locales[lang] = loadLocale(lang);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let currentLang = 'zh';
|
|
26
|
+
|
|
27
|
+
// 语言代码 → locale key 映射
|
|
28
|
+
const LANG_MAP = {
|
|
29
|
+
zh: 'zh', 'zh-cn': 'zh', 'zh-hans': 'zh',
|
|
30
|
+
'zh-tw': 'zh-TW', 'zh-hk': 'zh-TW', 'zh-hant': 'zh-TW',
|
|
31
|
+
en: 'en',
|
|
32
|
+
ko: 'ko',
|
|
33
|
+
de: 'de',
|
|
34
|
+
es: 'es',
|
|
35
|
+
fr: 'fr',
|
|
36
|
+
it: 'it',
|
|
37
|
+
da: 'da',
|
|
38
|
+
ja: 'ja',
|
|
39
|
+
pl: 'pl',
|
|
40
|
+
ru: 'ru',
|
|
41
|
+
ar: 'ar',
|
|
42
|
+
no: 'no', nb: 'no', nn: 'no',
|
|
43
|
+
pt: 'pt-BR', 'pt-br': 'pt-BR',
|
|
44
|
+
th: 'th',
|
|
45
|
+
tr: 'tr',
|
|
46
|
+
uk: 'uk',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function resolveLocale(raw) {
|
|
50
|
+
if (!raw) return 'en';
|
|
51
|
+
// 去掉 .UTF-8 等后缀,统一小写
|
|
52
|
+
const cleaned = raw.split('.')[0].replace(/_/g, '-').toLowerCase();
|
|
53
|
+
// 精确匹配
|
|
54
|
+
if (LANG_MAP[cleaned]) return LANG_MAP[cleaned];
|
|
55
|
+
// 取主语言
|
|
56
|
+
const primary = cleaned.split('-')[0];
|
|
57
|
+
if (LANG_MAP[primary]) return LANG_MAP[primary];
|
|
58
|
+
return 'en';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function detectLanguage() {
|
|
62
|
+
const lang = process.env.LANG || process.env.LANGUAGE || process.env.LC_ALL || '';
|
|
63
|
+
return resolveLocale(lang);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function setLang(lang) {
|
|
67
|
+
currentLang = locales[lang] ? lang : 'en';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getLang() {
|
|
71
|
+
return currentLang;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function t(key, params) {
|
|
75
|
+
let text = locales[currentLang]?.[key] || locales['en'][key] || key;
|
|
76
|
+
if (params) {
|
|
77
|
+
for (const [k, v] of Object.entries(params)) {
|
|
78
|
+
text = text.replaceAll(`{${k}}`, v);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return text;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 自动检测并设置语言
|
|
85
|
+
setLang(detectLanguage());
|
package/interceptor.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// LLM Request Interceptor
|
|
2
2
|
// 拦截并记录所有Claude API请求
|
|
3
3
|
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
4
5
|
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { dirname, join, basename } from 'node:path';
|
|
6
7
|
|
|
@@ -18,7 +19,7 @@ function generateLogFilePath() {
|
|
|
18
19
|
+ String(now.getMinutes()).padStart(2, '0')
|
|
19
20
|
+ String(now.getSeconds()).padStart(2, '0');
|
|
20
21
|
const projectName = basename(process.cwd()).replace(/[^a-zA-Z0-9_\-\.]/g, '_');
|
|
21
|
-
const dir = '
|
|
22
|
+
const dir = join(homedir(), '.claude', 'cc-viewer');
|
|
22
23
|
try { mkdirSync(dir, { recursive: true }); } catch {}
|
|
23
24
|
return join(dir, `${projectName}_${ts}.jsonl`);
|
|
24
25
|
}
|
|
@@ -215,6 +216,7 @@ export function setupInterceptor() {
|
|
|
215
216
|
response: null,
|
|
216
217
|
duration: 0,
|
|
217
218
|
isStream: body?.stream === true,
|
|
219
|
+
isHeartbeat: /\/api\/eval\/sdk-/.test(urlStr),
|
|
218
220
|
mainAgent: !!body?.system && Array.isArray(body?.tools) && body.tools.length > 10 &&
|
|
219
221
|
['Task', 'Edit', 'Bash'].every(n => body.tools.some(t => t.name === n))
|
|
220
222
|
};
|