cc-viewer 1.6.1 → 1.6.2
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/assets/{index-LsDPtdaY.js → index-CzJcswn8.js} +158 -158
- package/dist/index.html +1 -1
- package/lib/log-watcher.js +49 -13
- package/package.json +1 -1
package/dist/index.html
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
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-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-CzJcswn8.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="/assets/index-7ty6PCA6.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
package/lib/log-watcher.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, existsSync, watchFile } from 'node:fs';
|
|
1
|
+
import { readFileSync, existsSync, watchFile, openSync, readSync, closeSync, statSync } from 'node:fs';
|
|
2
2
|
import { isMainAgentEntry, extractCachedContent } from './kv-cache-analyzer.js';
|
|
3
3
|
import { buildContextWindowEvent, getContextSizeForModel } from './context-watcher.js';
|
|
4
4
|
|
|
@@ -80,25 +80,64 @@ export function sendEventToClients(clients, eventName, data) {
|
|
|
80
80
|
export function watchLogFile(opts) {
|
|
81
81
|
const { logFile, clients, getClaudePid, runParallelHook, notifyStatsWorker, getLogFile } = opts;
|
|
82
82
|
if (watchedFiles.has(logFile)) return;
|
|
83
|
-
|
|
83
|
+
|
|
84
|
+
// Track byte offset instead of string length — avoids full-file re-read on every poll
|
|
85
|
+
let lastByteOffset = 0;
|
|
86
|
+
let pendingTail = ''; // incomplete entry carried across polls
|
|
84
87
|
try {
|
|
85
88
|
if (existsSync(logFile)) {
|
|
86
|
-
|
|
89
|
+
lastByteOffset = statSync(logFile).size;
|
|
87
90
|
}
|
|
88
91
|
} catch {}
|
|
92
|
+
|
|
89
93
|
watchedFiles.set(logFile, true);
|
|
90
94
|
watchFile(logFile, { interval: 500 }, () => {
|
|
91
95
|
try {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
96
|
+
const currentSize = statSync(logFile).size;
|
|
97
|
+
|
|
98
|
+
// File truncated (rotation or clear) — reset offset
|
|
99
|
+
if (currentSize < lastByteOffset) {
|
|
100
|
+
lastByteOffset = 0;
|
|
101
|
+
pendingTail = '';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (currentSize <= lastByteOffset) return;
|
|
105
|
+
|
|
106
|
+
// Read only the new bytes
|
|
107
|
+
const bytesToRead = currentSize - lastByteOffset;
|
|
108
|
+
const buf = Buffer.alloc(bytesToRead);
|
|
109
|
+
const fd = openSync(logFile, 'r');
|
|
110
|
+
try {
|
|
111
|
+
readSync(fd, buf, 0, bytesToRead, lastByteOffset);
|
|
112
|
+
} finally {
|
|
113
|
+
closeSync(fd);
|
|
114
|
+
}
|
|
115
|
+
lastByteOffset = currentSize;
|
|
116
|
+
|
|
117
|
+
const raw = pendingTail + buf.toString('utf-8');
|
|
118
|
+
const parts = raw.split('\n---\n');
|
|
119
|
+
|
|
120
|
+
// Last part may be incomplete — keep it for next poll
|
|
121
|
+
pendingTail = parts.pop() || '';
|
|
122
|
+
|
|
123
|
+
// If there's only the tail and no complete entries, check if tail is a complete entry
|
|
124
|
+
// (happens when the file ends without a trailing \n---\n)
|
|
125
|
+
if (parts.length === 0 && pendingTail.trim()) {
|
|
126
|
+
try {
|
|
127
|
+
JSON.parse(pendingTail);
|
|
128
|
+
// Valid JSON — treat as complete entry
|
|
129
|
+
parts.push(pendingTail);
|
|
130
|
+
pendingTail = '';
|
|
131
|
+
} catch {
|
|
132
|
+
// Incomplete — keep in pendingTail for next poll
|
|
133
|
+
}
|
|
134
|
+
}
|
|
95
135
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
136
|
+
const validParts = parts.filter(p => p.trim());
|
|
137
|
+
if (validParts.length > 0) {
|
|
138
|
+
validParts.forEach(entry => {
|
|
99
139
|
try {
|
|
100
140
|
const parsed = JSON.parse(entry);
|
|
101
|
-
// 注入 Claude 进程 PID:CLI 模式从 PTY 获取,非 CLI 模式使用当前进程 PID
|
|
102
141
|
if (!parsed.pid) {
|
|
103
142
|
parsed.pid = getClaudePid();
|
|
104
143
|
}
|
|
@@ -109,7 +148,6 @@ export function watchLogFile(opts) {
|
|
|
109
148
|
if (cached) {
|
|
110
149
|
sendEventToClients(clients, 'kv_cache_content', cached);
|
|
111
150
|
}
|
|
112
|
-
// Push context_window from usage data
|
|
113
151
|
const usage = parsed.response?.body?.usage;
|
|
114
152
|
if (usage) {
|
|
115
153
|
const contextSize = getContextSizeForModel(parsed.body?.model);
|
|
@@ -123,14 +161,12 @@ export function watchLogFile(opts) {
|
|
|
123
161
|
// Skip invalid entries
|
|
124
162
|
}
|
|
125
163
|
});
|
|
126
|
-
// 通知 stats worker 更新统计
|
|
127
164
|
notifyStatsWorker(logFile);
|
|
128
165
|
}
|
|
129
166
|
|
|
130
167
|
// 检测日志文件是否已轮转到新文件
|
|
131
168
|
const currentLogFile = getLogFile();
|
|
132
169
|
if (currentLogFile !== logFile && !watchedFiles.has(currentLogFile)) {
|
|
133
|
-
// 轮转发生,发送 full_reload 让客户端重新加载新文件
|
|
134
170
|
const newEntries = readLogFile(currentLogFile);
|
|
135
171
|
clients.forEach(client => {
|
|
136
172
|
try {
|