cc-viewer 1.5.21 → 1.5.23

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.
@@ -0,0 +1,230 @@
1
+ import { appendFileSync, existsSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ const SUBAGENT_SYSTEM_RE = /(?:command execution|file search|planning) specialist|general-purpose agent/i;
5
+
6
+ export function getSystemText(body) {
7
+ const system = body?.system;
8
+ if (typeof system === 'string') return system;
9
+ if (Array.isArray(system)) {
10
+ return system.map(s => (s && s.text) || '').join('');
11
+ }
12
+ return '';
13
+ }
14
+
15
+ export function isMainAgentRequest(body) {
16
+ if (!body?.system || !Array.isArray(body?.tools)) return false;
17
+
18
+ const sysText = getSystemText(body);
19
+ if (!sysText.includes('You are Claude Code')) return false;
20
+ if (SUBAGENT_SYSTEM_RE.test(sysText)) return false;
21
+
22
+ const isSystemArray = Array.isArray(body.system);
23
+ const hasToolSearch = body.tools.some(t => t.name === 'ToolSearch');
24
+
25
+ if (isSystemArray && hasToolSearch) {
26
+ const messages = body.messages || [];
27
+ const firstMsgContent = messages.length > 0 ?
28
+ (typeof messages[0].content === 'string' ? messages[0].content :
29
+ Array.isArray(messages[0].content) ? messages[0].content.map(c => c.text || '').join('') : '') : '';
30
+ if (firstMsgContent.includes('<available-deferred-tools>')) {
31
+ return true;
32
+ }
33
+ }
34
+
35
+ if (body.tools.length > 10) {
36
+ const hasEdit = body.tools.some(t => t.name === 'Edit');
37
+ const hasBash = body.tools.some(t => t.name === 'Bash');
38
+ const hasTaskOrAgent = body.tools.some(t => t.name === 'Task' || t.name === 'Agent');
39
+ if (hasEdit && hasBash && hasTaskOrAgent) {
40
+ return true;
41
+ }
42
+ }
43
+
44
+ return false;
45
+ }
46
+
47
+ export function isPreflightEntry(entry) {
48
+ if (entry.mainAgent || entry.isHeartbeat || entry.isCountTokens) return false;
49
+ const body = entry.body || {};
50
+ if (Array.isArray(body.tools) && body.tools.length > 0) return false;
51
+ const msgs = body.messages || [];
52
+ if (msgs.length !== 1 || msgs[0].role !== 'user') return false;
53
+ const sysText = typeof body.system === 'string' ? body.system :
54
+ Array.isArray(body.system) ? body.system.map(s => s?.text || '').join('') : '';
55
+ return sysText.includes('Claude Code');
56
+ }
57
+
58
+ export function isAnthropicApiPath(urlStr) {
59
+ try {
60
+ const pathname = new URL(urlStr).pathname;
61
+ return /^\/v1\/messages(\/count_tokens|\/batches(\/.*)?)?$/.test(pathname)
62
+ || /^\/api\/eval\/sdk-/.test(pathname);
63
+ } catch {
64
+ return /\/v1\/messages/.test(urlStr);
65
+ }
66
+ }
67
+
68
+ export function assembleStreamMessage(events) {
69
+ let message = null;
70
+ const contentBlocks = [];
71
+ let currentBlockIndex = -1;
72
+
73
+ for (const event of events) {
74
+ if (!event || typeof event !== 'object' || !event.type) continue;
75
+
76
+ switch (event.type) {
77
+ case 'message_start':
78
+ message = { ...event.message };
79
+ message.content = [];
80
+ break;
81
+
82
+ case 'content_block_start':
83
+ currentBlockIndex = event.index;
84
+ contentBlocks[currentBlockIndex] = { ...event.content_block };
85
+ if (contentBlocks[currentBlockIndex].type === 'text') {
86
+ contentBlocks[currentBlockIndex].text = '';
87
+ } else if (contentBlocks[currentBlockIndex].type === 'thinking') {
88
+ contentBlocks[currentBlockIndex].thinking = '';
89
+ }
90
+ break;
91
+
92
+ case 'content_block_delta':
93
+ if (event.index >= 0 && contentBlocks[event.index] && event.delta) {
94
+ if (event.delta.type === 'text_delta' && event.delta.text) {
95
+ contentBlocks[event.index].text += event.delta.text;
96
+ } else if (event.delta.type === 'input_json_delta' && event.delta.partial_json) {
97
+ if (typeof contentBlocks[event.index]._inputJson !== 'string') {
98
+ contentBlocks[event.index]._inputJson = '';
99
+ }
100
+ contentBlocks[event.index]._inputJson += event.delta.partial_json;
101
+ } else if (event.delta.type === 'thinking_delta' && event.delta.thinking) {
102
+ contentBlocks[event.index].thinking += event.delta.thinking;
103
+ } else if (event.delta.type === 'signature_delta' && event.delta.signature) {
104
+ contentBlocks[event.index].signature = event.delta.signature;
105
+ }
106
+ }
107
+ break;
108
+
109
+ case 'content_block_stop':
110
+ if (event.index >= 0 && contentBlocks[event.index]) {
111
+ if (contentBlocks[event.index].type === 'tool_use' && typeof contentBlocks[event.index]._inputJson === 'string') {
112
+ try {
113
+ contentBlocks[event.index].input = JSON.parse(contentBlocks[event.index]._inputJson);
114
+ } catch {
115
+ contentBlocks[event.index].input = contentBlocks[event.index]._inputJson;
116
+ }
117
+ delete contentBlocks[event.index]._inputJson;
118
+ }
119
+ }
120
+ break;
121
+
122
+ case 'message_delta':
123
+ if (message && event.delta) {
124
+ if (event.delta.stop_reason) {
125
+ message.stop_reason = event.delta.stop_reason;
126
+ }
127
+ if (event.delta.stop_sequence !== undefined) {
128
+ message.stop_sequence = event.delta.stop_sequence;
129
+ }
130
+ }
131
+ if (message && event.usage) {
132
+ message.usage = { ...message.usage, ...event.usage };
133
+ }
134
+ break;
135
+
136
+ case 'message_stop':
137
+ break;
138
+ }
139
+ }
140
+
141
+ if (message) {
142
+ message.content = contentBlocks.filter(block => block !== undefined);
143
+ }
144
+
145
+ return message;
146
+ }
147
+
148
+ export function findRecentLog(dir, projectName) {
149
+ try {
150
+ const files = readdirSync(dir)
151
+ .filter(f => f.startsWith(projectName + '_') && f.endsWith('.jsonl'))
152
+ .sort()
153
+ .reverse();
154
+ if (files.length === 0) return null;
155
+ return join(dir, files[0]);
156
+ } catch { }
157
+ return null;
158
+ }
159
+
160
+ export function cleanupTempFiles(dir, projectName) {
161
+ try {
162
+ const tempFiles = readdirSync(dir)
163
+ .filter(f => f.startsWith(projectName + '_') && f.endsWith('_temp.jsonl'));
164
+ for (const f of tempFiles) {
165
+ try {
166
+ const tempPath = join(dir, f);
167
+ const newPath = tempPath.replace('_temp.jsonl', '.jsonl');
168
+ if (existsSync(newPath)) {
169
+ const tempContent = readFileSync(tempPath, 'utf-8');
170
+ if (tempContent.trim()) {
171
+ appendFileSync(newPath, tempContent);
172
+ }
173
+ unlinkSync(tempPath);
174
+ } else {
175
+ renameSync(tempPath, newPath);
176
+ }
177
+ } catch { }
178
+ }
179
+ } catch { }
180
+ }
181
+
182
+ export function migrateConversationContext(oldFile, newFile) {
183
+ try {
184
+ const content = readFileSync(oldFile, 'utf-8');
185
+ if (!content.trim()) return;
186
+
187
+ const parts = content.split('\n---\n').filter(p => p.trim());
188
+ if (parts.length === 0) return;
189
+
190
+ let originIndex = -1;
191
+ for (let i = parts.length - 1; i >= 0; i--) {
192
+ if (!/"mainAgent"\s*:\s*true/.test(parts[i])) continue;
193
+ try {
194
+ const entry = JSON.parse(parts[i]);
195
+ if (entry.mainAgent) {
196
+ const msgs = entry.body?.messages;
197
+ if (Array.isArray(msgs) && msgs.length === 1) {
198
+ originIndex = i;
199
+ break;
200
+ }
201
+ }
202
+ } catch { }
203
+ }
204
+
205
+ if (originIndex < 0) return;
206
+
207
+ let migrationStart = originIndex;
208
+ if (originIndex > 0) {
209
+ try {
210
+ const prevContent = parts[originIndex - 1];
211
+ if (prevContent.trim().startsWith('{')) {
212
+ const prev = JSON.parse(prevContent);
213
+ if (isPreflightEntry(prev)) {
214
+ migrationStart = originIndex - 1;
215
+ }
216
+ }
217
+ } catch { }
218
+ }
219
+
220
+ const migratedParts = parts.slice(migrationStart);
221
+ writeFileSync(newFile, migratedParts.join('\n---\n') + '\n---\n');
222
+
223
+ const remainingParts = parts.slice(0, migrationStart);
224
+ if (remainingParts.length > 0) {
225
+ writeFileSync(oldFile, remainingParts.join('\n---\n') + '\n---\n');
226
+ } else {
227
+ writeFileSync(oldFile, '');
228
+ }
229
+ } catch { }
230
+ }
@@ -4,6 +4,7 @@ import { LOG_DIR } from '../findcc.js';
4
4
 
5
5
  export const PLUGINS_DIR = join(LOG_DIR, 'plugins');
6
6
  const PREFS_FILE = join(LOG_DIR, 'preferences.json');
7
+ const SHOULD_LOG = process.env.CCV_DEBUG_PLUGINS === '1';
7
8
 
8
9
  // Hook 类型定义
9
10
  const HOOK_TYPES = {
@@ -33,7 +34,7 @@ export async function loadPlugins() {
33
34
  disabledPlugins = prefs.disabledPlugins;
34
35
  }
35
36
  }
36
- } catch {}
37
+ } catch { }
37
38
 
38
39
  let files;
39
40
  try {
@@ -52,16 +53,16 @@ export async function loadPlugins() {
52
53
  const name = plugin.name || file;
53
54
 
54
55
  if (disabledPlugins.includes(name)) {
55
- console.error(`[CC Viewer] Plugin "${name}" is disabled, skipping.`);
56
+ if (SHOULD_LOG) console.error(`[CC Viewer] Plugin "${name}" is disabled, skipping.`);
56
57
  continue;
57
58
  }
58
59
 
59
60
  if (plugin.hooks && typeof plugin.hooks === 'object') {
60
61
  _plugins.push({ name, hooks: plugin.hooks, file });
61
- console.error(`[CC Viewer] Plugin loaded: ${name} (${file})`);
62
+ if (SHOULD_LOG) console.error(`[CC Viewer] Plugin loaded: ${name} (${file})`);
62
63
  }
63
64
  } catch (err) {
64
- console.error(`[CC Viewer] Failed to load plugin "${file}":`, err.message);
65
+ if (SHOULD_LOG) console.error(`[CC Viewer] Failed to load plugin "${file}":`, err.message);
65
66
  }
66
67
  }
67
68
  }
@@ -80,7 +81,7 @@ export async function runWaterfallHook(name, initialValue) {
80
81
  value = { ...value, ...result };
81
82
  }
82
83
  } catch (err) {
83
- console.error(`[CC Viewer] Plugin "${plugin.name}" hook "${name}" error:`, err.message);
84
+ if (SHOULD_LOG) console.error(`[CC Viewer] Plugin "${plugin.name}" hook "${name}" error:`, err.message);
84
85
  }
85
86
  }
86
87
  return value;
@@ -98,7 +99,7 @@ export async function runParallelHook(name, context = {}) {
98
99
  Promise.resolve()
99
100
  .then(() => hookFn(context))
100
101
  .catch(err => {
101
- console.error(`[CC Viewer] Plugin "${plugin.name}" hook "${name}" error:`, err.message);
102
+ if (SHOULD_LOG) console.error(`[CC Viewer] Plugin "${plugin.name}" hook "${name}" error:`, err.message);
102
103
  })
103
104
  );
104
105
  }
@@ -119,7 +120,7 @@ export function getPluginsInfo() {
119
120
  disabledPlugins = prefs.disabledPlugins;
120
121
  }
121
122
  }
122
- } catch {}
123
+ } catch { }
123
124
 
124
125
  let files;
125
126
  try {
package/lib/updater.js CHANGED
@@ -64,7 +64,10 @@ function saveCheckTime() {
64
64
  } catch { }
65
65
  }
66
66
 
67
- export async function checkAndUpdate() {
67
+ export async function checkAndUpdate(options = {}) {
68
+ const fetchImpl = options.fetchImpl || fetch;
69
+ const execImpl = options.execImpl || execSync;
70
+ const dryRun = options.dryRun === true;
68
71
  const currentVersion = getCurrentVersion();
69
72
 
70
73
  // 跟随 Claude Code 全局配置
@@ -77,7 +80,7 @@ export async function checkAndUpdate() {
77
80
  }
78
81
 
79
82
  try {
80
- const res = await fetch('https://registry.npmjs.org/cc-viewer');
83
+ const res = await fetchImpl('https://registry.npmjs.org/cc-viewer');
81
84
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
82
85
  const data = await res.json();
83
86
  const remoteVersion = data['dist-tags']?.latest;
@@ -104,7 +107,9 @@ export async function checkAndUpdate() {
104
107
  // 同大版本:自动更新
105
108
  console.error(`[CC Viewer] ${t('update.updating', { version: remoteVersion })}`);
106
109
  try {
107
- execSync(`npm install -g cc-viewer@${remoteVersion}`, { stdio: 'pipe', timeout: 60000 });
110
+ if (!dryRun) {
111
+ execImpl(`npm install -g cc-viewer@${remoteVersion}`, { stdio: 'pipe', timeout: 60000 });
112
+ }
108
113
  console.error(`[CC Viewer] ${t('update.completed', { version: remoteVersion })}`);
109
114
  return { status: 'updated', currentVersion, remoteVersion };
110
115
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-viewer",
3
- "version": "1.5.21",
3
+ "version": "1.5.23",
4
4
  "description": "Claude Code Logger visualization management tool",
5
5
  "license": "MIT",
6
6
  "main": "server.js",
@@ -18,7 +18,7 @@
18
18
  "dev": "vite",
19
19
  "build": "node build.js",
20
20
  "start": "node server.js",
21
- "test": "node --test",
21
+ "test": "CCV_LOG_DIR=tmp node --test",
22
22
  "prepublishOnly": "npm run build"
23
23
  },
24
24
  "keywords": [
package/pty-manager.js CHANGED
@@ -19,6 +19,19 @@ let lastPtyRows = 30;
19
19
  const MAX_BUFFER = 200000;
20
20
  let batchBuffer = '';
21
21
  let batchScheduled = false;
22
+ let _ptyImportForTests = null;
23
+
24
+ export function _setPtyImportForTests(fn) {
25
+ _ptyImportForTests = fn;
26
+ }
27
+
28
+ async function getPty() {
29
+ if (typeof _ptyImportForTests === 'function') {
30
+ return _ptyImportForTests();
31
+ }
32
+ const ptyMod = await import('node-pty');
33
+ return ptyMod.default || ptyMod;
34
+ }
22
35
 
23
36
  /**
24
37
  * 在 outputBuffer 截断时,找到安全的截断位置,
@@ -65,7 +78,7 @@ function flushBatch() {
65
78
  const chunk = batchBuffer;
66
79
  batchBuffer = '';
67
80
  for (const cb of dataListeners) {
68
- try { cb(chunk); } catch {}
81
+ try { cb(chunk); } catch { }
69
82
  }
70
83
  }
71
84
 
@@ -78,7 +91,7 @@ function fixSpawnHelperPermissions() {
78
91
  if (!(stat.mode & 0o111)) {
79
92
  chmodSync(helperPath, stat.mode | 0o755);
80
93
  }
81
- } catch {}
94
+ } catch { }
82
95
  }
83
96
 
84
97
  export async function spawnClaude(proxyPort, cwd, extraArgs = [], claudePath = null, isNpmVersion = false, serverPort = null) {
@@ -86,8 +99,7 @@ export async function spawnClaude(proxyPort, cwd, extraArgs = [], claudePath = n
86
99
  killPty();
87
100
  }
88
101
 
89
- const ptyMod = await import('node-pty');
90
- const pty = ptyMod.default || ptyMod;
102
+ const pty = await getPty();
91
103
 
92
104
  fixSpawnHelperPermissions();
93
105
 
@@ -162,7 +174,7 @@ export async function spawnClaude(proxyPort, cwd, extraArgs = [], claudePath = n
162
174
  // 保留 lastWorkspacePath,不清除,用于 respawn
163
175
  currentWorkspacePath = null;
164
176
  for (const cb of exitListeners) {
165
- try { cb(exitCode); } catch {}
177
+ try { cb(exitCode); } catch { }
166
178
  }
167
179
  });
168
180
 
@@ -183,8 +195,7 @@ export async function spawnShell() {
183
195
  if (ptyProcess) return false; // 已有进程在运行
184
196
  const cwd = lastWorkspacePath || process.cwd();
185
197
 
186
- const ptyMod = await import('node-pty');
187
- const pty = ptyMod.default || ptyMod;
198
+ const pty = await getPty();
188
199
 
189
200
  fixSpawnHelperPermissions();
190
201
 
@@ -221,7 +232,7 @@ export async function spawnShell() {
221
232
  ptyProcess = null;
222
233
  currentWorkspacePath = null;
223
234
  for (const cb of exitListeners) {
224
- try { cb(exitCode); } catch {}
235
+ try { cb(exitCode); } catch { }
225
236
  }
226
237
  });
227
238
 
@@ -232,7 +243,7 @@ export function resizePty(cols, rows) {
232
243
  lastPtyCols = cols;
233
244
  lastPtyRows = rows;
234
245
  if (ptyProcess) {
235
- try { ptyProcess.resize(cols, rows); } catch {}
246
+ try { ptyProcess.resize(cols, rows); } catch { }
236
247
  }
237
248
  }
238
249
 
@@ -241,7 +252,7 @@ export function killPty() {
241
252
  flushBatch();
242
253
  batchBuffer = '';
243
254
  batchScheduled = false;
244
- try { ptyProcess.kill(); } catch {}
255
+ try { ptyProcess.kill(); } catch { }
245
256
  ptyProcess = null;
246
257
  }
247
258
  }
@@ -1,10 +1,56 @@
1
1
  // Workspace Registry - 工作区持久化管理
2
- import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, readdirSync } from 'node:fs';
2
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, statSync, readdirSync, openSync, closeSync, renameSync, unlinkSync } from 'node:fs';
3
3
  import { join, basename, resolve } from 'node:path';
4
4
  import { randomBytes } from 'node:crypto';
5
5
  import { LOG_DIR } from './findcc.js';
6
6
 
7
7
  const WORKSPACES_FILE = join(LOG_DIR, 'workspaces.json');
8
+ const LOCK_FILE = join(LOG_DIR, 'workspaces.lock');
9
+
10
+ function sleep(ms) {
11
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
12
+ }
13
+
14
+ function withLock(fn) {
15
+ mkdirSync(LOG_DIR, { recursive: true });
16
+ const deadline = Date.now() + 2000;
17
+ // 如果锁文件超过 5 秒未更新,认为它是死锁(前一个进程崩溃)
18
+ const STALE_THRESHOLD = 5000;
19
+
20
+ while (true) {
21
+ try {
22
+ const fd = openSync(LOCK_FILE, 'wx');
23
+ closeSync(fd);
24
+ break;
25
+ } catch (err) {
26
+ if (err?.code === 'EEXIST') {
27
+ if (Date.now() < deadline) {
28
+ // 检查是否为陈旧锁
29
+ try {
30
+ const stats = statSync(LOCK_FILE);
31
+ if (Date.now() - stats.mtimeMs > STALE_THRESHOLD) {
32
+ // 尝试强制移除锁
33
+ try { unlinkSync(LOCK_FILE); } catch { }
34
+ // 立即重试获取
35
+ continue;
36
+ }
37
+ } catch {
38
+ // stat 失败可能意味着锁刚被释放,继续循环尝试获取
39
+ }
40
+ sleep(25);
41
+ continue;
42
+ }
43
+ }
44
+ throw err;
45
+ }
46
+ }
47
+
48
+ try {
49
+ return fn();
50
+ } finally {
51
+ try { unlinkSync(LOCK_FILE); } catch { }
52
+ }
53
+ }
8
54
 
9
55
  export function loadWorkspaces() {
10
56
  try {
@@ -17,45 +63,67 @@ export function loadWorkspaces() {
17
63
  }
18
64
 
19
65
  export function saveWorkspaces(list) {
66
+ const tmpFile = `${WORKSPACES_FILE}.tmp-${process.pid}-${randomBytes(4).toString('hex')}`;
20
67
  try {
21
68
  mkdirSync(LOG_DIR, { recursive: true });
22
- writeFileSync(WORKSPACES_FILE, JSON.stringify({ workspaces: list }, null, 2));
69
+ writeFileSync(tmpFile, JSON.stringify({ workspaces: list }, null, 2));
70
+
71
+ // Windows 上 renameSync 可能会因为目标文件存在或被占用而失败
72
+ // 简单的重试机制
73
+ let retries = 3;
74
+ while (retries > 0) {
75
+ try {
76
+ renameSync(tmpFile, WORKSPACES_FILE);
77
+ break;
78
+ } catch (err) {
79
+ if (retries === 1) throw err;
80
+ retries--;
81
+ sleep(20);
82
+ }
83
+ }
23
84
  } catch (err) {
24
85
  console.error('[CC Viewer] Failed to save workspaces:', err.message);
86
+ // 尝试清理临时文件
87
+ try { unlinkSync(tmpFile); } catch { }
25
88
  }
26
89
  }
27
90
 
28
91
  export function registerWorkspace(absolutePath) {
29
- const resolvedPath = resolve(absolutePath);
30
- const projectName = basename(resolvedPath).replace(/[^a-zA-Z0-9_\-\.]/g, '_');
31
- const list = loadWorkspaces();
32
- const existing = list.find(w => w.path === resolvedPath);
33
- if (existing) {
34
- existing.lastUsed = new Date().toISOString();
35
- existing.projectName = projectName;
92
+ return withLock(() => {
93
+ const resolvedPath = resolve(absolutePath);
94
+ const projectName = basename(resolvedPath).replace(/[^a-zA-Z0-9_\-\.]/g, '_');
95
+ const list = loadWorkspaces();
96
+ const existing = list.find(w => w.path === resolvedPath);
97
+ if (existing) {
98
+ existing.lastUsed = new Date().toISOString();
99
+ existing.projectName = projectName;
100
+ saveWorkspaces(list);
101
+ return existing;
102
+ }
103
+ const now = new Date().toISOString();
104
+ const entry = {
105
+ id: randomBytes(6).toString('hex'),
106
+ path: resolvedPath,
107
+ projectName,
108
+ lastUsed: now,
109
+ createdAt: now,
110
+ };
111
+ list.push(entry);
36
112
  saveWorkspaces(list);
37
- return existing;
38
- }
39
- const entry = {
40
- id: randomBytes(6).toString('hex'),
41
- path: resolvedPath,
42
- projectName,
43
- lastUsed: new Date().toISOString(),
44
- createdAt: new Date().toISOString(),
45
- };
46
- list.push(entry);
47
- saveWorkspaces(list);
48
- return entry;
113
+ return entry;
114
+ });
49
115
  }
50
116
 
51
117
  export function removeWorkspace(id) {
52
- const list = loadWorkspaces();
53
- const filtered = list.filter(w => w.id !== id);
54
- if (filtered.length !== list.length) {
55
- saveWorkspaces(filtered);
56
- return true;
57
- }
58
- return false;
118
+ return withLock(() => {
119
+ const list = loadWorkspaces();
120
+ const filtered = list.filter(w => w.id !== id);
121
+ if (filtered.length !== list.length) {
122
+ saveWorkspaces(filtered);
123
+ return true;
124
+ }
125
+ return false;
126
+ });
59
127
  }
60
128
 
61
129
  export function getWorkspaces() {
@@ -71,11 +139,11 @@ export function getWorkspaces() {
71
139
  for (const f of files) {
72
140
  if (f.endsWith('.jsonl')) {
73
141
  logCount++;
74
- try { totalSize += statSync(join(logDir, f)).size; } catch {}
142
+ try { totalSize += statSync(join(logDir, f)).size; } catch { }
75
143
  }
76
144
  }
77
145
  }
78
- } catch {}
146
+ } catch { }
79
147
  return { ...w, logCount, totalSize };
80
148
  })
81
149
  .sort((a, b) => new Date(b.lastUsed) - new Date(a.lastUsed));