cc-viewer 1.6.4 → 1.6.6

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,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-3VL83o1N.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-DS1ml1GF.js"></script>
10
10
  <link rel="stylesheet" crossorigin href="/assets/index-7ty6PCA6.css">
11
11
  </head>
12
12
  <body>
package/interceptor.js CHANGED
@@ -440,11 +440,16 @@ export function setupInterceptor() {
440
440
  delete requestEntry.inProgress;
441
441
  delete requestEntry.requestId;
442
442
  appendFileSync(LOG_FILE, JSON.stringify(requestEntry) + '\n---\n');
443
+ // Release memory: clear large objects after disk write
444
+ streamedContent = '';
445
+ requestEntry.response = null;
443
446
  } catch (err) {
444
447
  requestEntry.response.body = streamedContent.slice(0, 1000);
445
448
  delete requestEntry.inProgress;
446
449
  delete requestEntry.requestId;
447
450
  appendFileSync(LOG_FILE, JSON.stringify(requestEntry) + '\n---\n');
451
+ streamedContent = '';
452
+ requestEntry.response = null;
448
453
  }
449
454
  controller.close();
450
455
  break;
@@ -1,4 +1,4 @@
1
- import { readFileSync, existsSync, watchFile, openSync, readSync, closeSync, statSync } from 'node:fs';
1
+ import { readFileSync, existsSync, watchFile, unwatchFile, 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
 
@@ -167,6 +167,10 @@ export function watchLogFile(opts) {
167
167
  // 检测日志文件是否已轮转到新文件
168
168
  const currentLogFile = getLogFile();
169
169
  if (currentLogFile !== logFile && !watchedFiles.has(currentLogFile)) {
170
+ // Unwatch old file to prevent watcher leak on rotation
171
+ unwatchFile(logFile);
172
+ watchedFiles.delete(logFile);
173
+
170
174
  const newEntries = readLogFile(currentLogFile);
171
175
  clients.forEach(client => {
172
176
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-viewer",
3
- "version": "1.6.4",
3
+ "version": "1.6.6",
4
4
  "description": "Claude Code Logger visualization management tool",
5
5
  "license": "MIT",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -77,7 +77,15 @@ let _workspaceIsNpmVersion = false;
77
77
  let _workspaceLaunched = false; // 工作区是否已经启动了会话
78
78
 
79
79
  // Editor session state (for $EDITOR intercept)
80
- const editorSessions = new Map(); // sessionId → { filePath, done }
80
+ const editorSessions = new Map(); // sessionId → { filePath, done, createdAt }
81
+ // Periodically clean up abandoned editor sessions (older than 1 hour)
82
+ const _editorCleanupTimer = setInterval(() => {
83
+ const now = Date.now();
84
+ for (const [id, session] of editorSessions) {
85
+ if (now - (session.createdAt || 0) > 3600000) editorSessions.delete(id);
86
+ }
87
+ }, 60000);
88
+ _editorCleanupTimer.unref(); // Don't keep process alive for cleanup
81
89
  let terminalWss = null; // WebSocketServer reference for broadcasting
82
90
  export function setWorkspaceClaudeArgs(args) {
83
91
  _workspaceClaudeArgs = args;
@@ -87,6 +95,9 @@ export function setWorkspaceClaudePath(path, isNpm) {
87
95
  _workspaceIsNpmVersion = isNpm;
88
96
  }
89
97
 
98
+ // Global POST body size limit (10MB) to prevent OOM from malicious/buggy clients
99
+ const MAX_POST_BODY = 10 * 1024 * 1024;
100
+
90
101
 
91
102
 
92
103
  const __filename = fileURLToPath(import.meta.url);
@@ -279,7 +290,7 @@ async function handleRequest(req, res) {
279
290
 
280
291
  if (url === '/api/preferences' && method === 'POST') {
281
292
  let body = '';
282
- req.on('data', chunk => { body += chunk; });
293
+ req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
283
294
  req.on('end', () => {
284
295
  try {
285
296
  const incoming = JSON.parse(body);
@@ -300,7 +311,7 @@ async function handleRequest(req, res) {
300
311
  // 注册新的日志文件进行 watch(供新进程复用旧服务时调用)
301
312
  if (url === '/api/register-log' && method === 'POST') {
302
313
  let body = '';
303
- req.on('data', chunk => { body += chunk; });
314
+ req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
304
315
  req.on('end', () => {
305
316
  try {
306
317
  const { logFile } = JSON.parse(body);
@@ -323,7 +334,7 @@ async function handleRequest(req, res) {
323
334
  // 用户选择继续/新开日志
324
335
  if (url === '/api/resume-choice' && method === 'POST') {
325
336
  let body = '';
326
- req.on('data', chunk => { body += chunk; });
337
+ req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
327
338
  req.on('end', () => {
328
339
  try {
329
340
  const { choice } = JSON.parse(body);
@@ -367,7 +378,7 @@ async function handleRequest(req, res) {
367
378
  // 翻译 API
368
379
  if (url === '/api/translate' && method === 'POST') {
369
380
  let body = '';
370
- req.on('data', chunk => { body += chunk; });
381
+ req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
371
382
  req.on('end', async () => {
372
383
  try {
373
384
  const { text, from = 'en', to } = JSON.parse(body);
@@ -511,7 +522,7 @@ async function handleRequest(req, res) {
511
522
 
512
523
  if (url === '/api/workspaces/launch' && method === 'POST') {
513
524
  let body = '';
514
- req.on('data', chunk => { body += chunk; });
525
+ req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
515
526
  req.on('end', async () => {
516
527
  try {
517
528
  const { path: wsPath } = JSON.parse(body);
@@ -570,7 +581,7 @@ async function handleRequest(req, res) {
570
581
 
571
582
  if (url === '/api/workspaces/add' && method === 'POST') {
572
583
  let body = '';
573
- req.on('data', chunk => { body += chunk; });
584
+ req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
574
585
  req.on('end', async () => {
575
586
  try {
576
587
  const { path: wsPath } = JSON.parse(body);
@@ -902,7 +913,7 @@ async function handleRequest(req, res) {
902
913
 
903
914
  if (url === '/api/editor-open' && method === 'POST') {
904
915
  let body = '';
905
- req.on('data', chunk => { body += chunk; });
916
+ req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
906
917
  req.on('end', () => {
907
918
  try {
908
919
  const { sessionId, filePath } = JSON.parse(body);
@@ -911,7 +922,7 @@ async function handleRequest(req, res) {
911
922
  res.end(JSON.stringify({ error: 'Missing sessionId or filePath' }));
912
923
  return;
913
924
  }
914
- editorSessions.set(sessionId, { filePath, done: false });
925
+ editorSessions.set(sessionId, { filePath, done: false, createdAt: Date.now() });
915
926
  // Broadcast to all terminal WebSocket clients
916
927
  if (terminalWss) {
917
928
  const msg = JSON.stringify({ type: 'editor-open', sessionId, filePath });
@@ -946,7 +957,7 @@ async function handleRequest(req, res) {
946
957
 
947
958
  if (url === '/api/editor-done' && method === 'POST') {
948
959
  let body = '';
949
- req.on('data', chunk => { body += chunk; });
960
+ req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
950
961
  req.on('end', () => {
951
962
  try {
952
963
  const { sessionId } = JSON.parse(body);
@@ -1211,7 +1222,7 @@ async function handleRequest(req, res) {
1211
1222
 
1212
1223
  if (url === '/api/plugins/upload' && method === 'POST') {
1213
1224
  let body = '';
1214
- req.on('data', chunk => { body += chunk; });
1225
+ req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
1215
1226
  req.on('end', async () => {
1216
1227
  try {
1217
1228
  const { files: fileList } = JSON.parse(body);
@@ -1396,7 +1407,7 @@ async function handleRequest(req, res) {
1396
1407
  // 删除日志文件
1397
1408
  if (url === '/api/delete-logs' && method === 'POST') {
1398
1409
  let body = '';
1399
- req.on('data', chunk => { body += chunk; });
1410
+ req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
1400
1411
  req.on('end', () => {
1401
1412
  try {
1402
1413
  const { files } = JSON.parse(body);
@@ -1442,7 +1453,7 @@ async function handleRequest(req, res) {
1442
1453
  // 合并日志文件
1443
1454
  if (url === '/api/merge-logs' && method === 'POST') {
1444
1455
  let body = '';
1445
- req.on('data', chunk => { body += chunk; });
1456
+ req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
1446
1457
  req.on('end', () => {
1447
1458
  try {
1448
1459
  const { files } = JSON.parse(body);
@@ -1601,7 +1612,7 @@ async function handleRequest(req, res) {
1601
1612
  // CCV 进程关闭
1602
1613
  if (url === '/api/ccv-processes/kill' && method === 'POST') {
1603
1614
  let body = '';
1604
- req.on('data', chunk => { body += chunk; });
1615
+ req.on('data', chunk => { body += chunk; if (body.length > MAX_POST_BODY) req.destroy(); });
1605
1616
  req.on('end', async () => {
1606
1617
  try {
1607
1618
  const { pid } = JSON.parse(body);