cc-viewer 1.0.2 → 1.0.3
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
CHANGED
|
@@ -16,9 +16,9 @@ npm install -g cc-viewer
|
|
|
16
16
|
ccv
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
该命令会自动配置本地安装的 Claude Code 以启用监控,并在 shell 配置文件(`~/.zshrc` 或 `~/.bashrc`)中添加自动修复 hook。之后正常使用 Claude Code,打开浏览器访问 `http://localhost:7008` 即可查看监控界面。
|
|
20
20
|
|
|
21
|
-
Claude Code 更新后无需手动操作,下次运行 `claude`
|
|
21
|
+
Claude Code 更新后无需手动操作,下次运行 `claude` 时会自动检测并重新配置。
|
|
22
22
|
|
|
23
23
|
### 卸载
|
|
24
24
|
|
|
@@ -26,7 +26,7 @@ Claude Code 更新后无需手动操作,下次运行 `claude` 时会自动检
|
|
|
26
26
|
ccv --uninstall
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
一键清理 cli.js
|
|
29
|
+
一键清理 cli.js 中的配置和 shell 配置文件中的 hook。
|
|
30
30
|
|
|
31
31
|
## 功能
|
|
32
32
|
|
|
@@ -52,8 +52,8 @@ ccv --uninstall
|
|
|
52
52
|
- `thinking` 块默认折叠,点击展开查看思考过程
|
|
53
53
|
- `tool_use` 显示为紧凑的工具调用卡片(Bash、Read、Edit、Write、Glob、Grep、Task 等均有专属展示)
|
|
54
54
|
- 用户选择型消息(AskUserQuestion)以问答形式展示
|
|
55
|
-
-
|
|
56
|
-
-
|
|
55
|
+
- 系统标签(`<system-reminder>`、`<project-reminder>` 等)自动折叠
|
|
56
|
+
- 自动过滤系统文本,只展示用户的真实输入
|
|
57
57
|
- 支持多 session 分段展示(`/compact`、`/clear` 等操作后自动分段)
|
|
58
58
|
- 每条消息显示精确到秒的时间戳
|
|
59
59
|
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-viewer",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Claude Code Logger visualization management tool",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"main": "
|
|
6
|
+
"main": "server.js",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"exports": {
|
|
9
|
-
".": "./
|
|
10
|
-
"./server.js": "./
|
|
9
|
+
".": "./server.js",
|
|
10
|
+
"./server.js": "./server.js",
|
|
11
11
|
"./interceptor.js": "./interceptor.js"
|
|
12
12
|
},
|
|
13
13
|
"author": "weiesky",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"scripts": {
|
|
18
18
|
"dev": "vite",
|
|
19
19
|
"build": "node build.js",
|
|
20
|
-
"start": "node
|
|
20
|
+
"start": "node server.js",
|
|
21
21
|
"prepublishOnly": "npm run build"
|
|
22
22
|
},
|
|
23
23
|
"keywords": [
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
"node": ">=18"
|
|
42
42
|
},
|
|
43
43
|
"files": [
|
|
44
|
-
"
|
|
44
|
+
"dist/",
|
|
45
|
+
"server.js",
|
|
45
46
|
"cli.js",
|
|
46
47
|
"interceptor.js",
|
|
47
48
|
"i18n.js",
|
|
@@ -3,8 +3,8 @@ import { readFileSync, existsSync, watchFile, unwatchFile, statSync, writeFileSy
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { dirname, join, extname, basename } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
|
-
import { LOG_FILE } from '
|
|
7
|
-
import { t } from '
|
|
6
|
+
import { LOG_FILE } from './interceptor.js';
|
|
7
|
+
import { t } from './i18n.js';
|
|
8
8
|
|
|
9
9
|
const LOG_DIR = join(homedir(), '.claude', 'cc-viewer');
|
|
10
10
|
const SHOW_ALL_FILE = '/tmp/cc-viewer-show-all';
|
|
@@ -56,9 +56,29 @@ function checkPortAlive(port) {
|
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function registerLogToServer(port, logFile) {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
const data = JSON.stringify({ logFile });
|
|
62
|
+
const req = httpRequest({
|
|
63
|
+
host: HOST, port, path: '/api/register-log', method: 'POST',
|
|
64
|
+
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) },
|
|
65
|
+
timeout: 2000,
|
|
66
|
+
}, (res) => {
|
|
67
|
+
res.resume();
|
|
68
|
+
resolve(true);
|
|
69
|
+
});
|
|
70
|
+
req.on('error', () => resolve(false));
|
|
71
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
72
|
+
req.write(data);
|
|
73
|
+
req.end();
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
59
77
|
let clients = [];
|
|
60
78
|
let server;
|
|
61
79
|
let actualPort = START_PORT;
|
|
80
|
+
// 跟踪所有被 watch 的日志文件
|
|
81
|
+
const watchedFiles = new Map();
|
|
62
82
|
|
|
63
83
|
const MIME_TYPES = {
|
|
64
84
|
'.html': 'text/html; charset=utf-8',
|
|
@@ -103,11 +123,13 @@ function sendToClients(entry) {
|
|
|
103
123
|
});
|
|
104
124
|
}
|
|
105
125
|
|
|
106
|
-
function
|
|
126
|
+
function watchLogFile(logFile) {
|
|
127
|
+
if (watchedFiles.has(logFile)) return;
|
|
107
128
|
let lastSize = 0;
|
|
108
|
-
|
|
129
|
+
watchedFiles.set(logFile, true);
|
|
130
|
+
watchFile(logFile, { interval: 500 }, () => {
|
|
109
131
|
try {
|
|
110
|
-
const content = readFileSync(
|
|
132
|
+
const content = readFileSync(logFile, 'utf-8');
|
|
111
133
|
const newContent = content.slice(lastSize);
|
|
112
134
|
lastSize = content.length;
|
|
113
135
|
|
|
@@ -128,6 +150,10 @@ function startWatching() {
|
|
|
128
150
|
});
|
|
129
151
|
}
|
|
130
152
|
|
|
153
|
+
function startWatching() {
|
|
154
|
+
watchLogFile(LOG_FILE);
|
|
155
|
+
}
|
|
156
|
+
|
|
131
157
|
function handleRequest(req, res) {
|
|
132
158
|
const { url, method } = req;
|
|
133
159
|
|
|
@@ -142,6 +168,29 @@ function handleRequest(req, res) {
|
|
|
142
168
|
return;
|
|
143
169
|
}
|
|
144
170
|
|
|
171
|
+
// 注册新的日志文件进行 watch(供新进程复用旧服务时调用)
|
|
172
|
+
if (url === '/api/register-log' && method === 'POST') {
|
|
173
|
+
let body = '';
|
|
174
|
+
req.on('data', chunk => { body += chunk; });
|
|
175
|
+
req.on('end', () => {
|
|
176
|
+
try {
|
|
177
|
+
const { logFile } = JSON.parse(body);
|
|
178
|
+
if (logFile && typeof logFile === 'string' && logFile.startsWith(LOG_DIR) && existsSync(logFile)) {
|
|
179
|
+
watchLogFile(logFile);
|
|
180
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
181
|
+
res.end(JSON.stringify({ ok: true }));
|
|
182
|
+
} else {
|
|
183
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
184
|
+
res.end(JSON.stringify({ error: 'Invalid log file path' }));
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
188
|
+
res.end(JSON.stringify({ error: 'Invalid request body' }));
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
145
194
|
// SSE endpoint
|
|
146
195
|
if (url === '/events' && method === 'GET') {
|
|
147
196
|
res.writeHead(200, {
|
|
@@ -308,6 +357,7 @@ export async function startViewer() {
|
|
|
308
357
|
const alive = await checkPortAlive(existingPort);
|
|
309
358
|
if (alive) {
|
|
310
359
|
actualPort = existingPort;
|
|
360
|
+
await registerLogToServer(existingPort, LOG_FILE);
|
|
311
361
|
return null;
|
|
312
362
|
}
|
|
313
363
|
}
|
|
@@ -326,6 +376,7 @@ export async function startViewer() {
|
|
|
326
376
|
const alive = await checkPortAlive(existingPort);
|
|
327
377
|
if (alive) {
|
|
328
378
|
actualPort = existingPort;
|
|
379
|
+
await registerLogToServer(existingPort, LOG_FILE);
|
|
329
380
|
releaseLock();
|
|
330
381
|
console.log(t('server.reuse', { host: HOST, port: existingPort }));
|
|
331
382
|
return null;
|
|
@@ -378,7 +429,10 @@ export async function startViewer() {
|
|
|
378
429
|
}
|
|
379
430
|
|
|
380
431
|
export function stopViewer() {
|
|
381
|
-
|
|
432
|
+
for (const logFile of watchedFiles.keys()) {
|
|
433
|
+
unwatchFile(logFile);
|
|
434
|
+
}
|
|
435
|
+
watchedFiles.clear();
|
|
382
436
|
clients.forEach(client => client.end());
|
|
383
437
|
clients = [];
|
|
384
438
|
if (server) {
|
|
File without changes
|
|
File without changes
|
/package/{lib → dist}/index.html
RENAMED
|
File without changes
|