@adversity/coding-tool-x 3.1.0 → 3.1.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.
Files changed (142) hide show
  1. package/CHANGELOG.md +39 -18
  2. package/README.md +8 -8
  3. package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
  4. package/dist/web/assets/ConfigTemplates-DvcbKKdS.js +1 -0
  5. package/dist/web/assets/Home-BJKPCBuk.css +1 -0
  6. package/dist/web/assets/Home-Cw-F_Wnu.js +1 -0
  7. package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
  8. package/dist/web/assets/PluginManager-jy_4GVxI.js +1 -0
  9. package/dist/web/assets/ProjectList-C1fQb9OW.css +1 -0
  10. package/dist/web/assets/ProjectList-Df1-NcNr.js +1 -0
  11. package/dist/web/assets/SessionList-BGJWyneI.css +1 -0
  12. package/dist/web/assets/SessionList-UWcZtC2r.js +1 -0
  13. package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
  14. package/dist/web/assets/SkillManager-IRdseMKB.js +1 -0
  15. package/dist/web/assets/Terminal-BasTyDut.js +1 -0
  16. package/dist/web/assets/Terminal-DGNJeVtc.css +1 -0
  17. package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
  18. package/dist/web/assets/WorkspaceManager-D-D2kK1V.js +1 -0
  19. package/dist/web/assets/icons-kcfLIMBB.js +1 -0
  20. package/dist/web/assets/index-CoB3zF0K.css +1 -0
  21. package/dist/web/assets/index-CryrSLv8.js +2 -0
  22. package/dist/web/assets/markdown-BfC0goYb.css +10 -0
  23. package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
  24. package/dist/web/assets/naive-ui-CSrLusZZ.js +1 -0
  25. package/dist/web/assets/{vendors-D2HHw_aW.js → vendors-CO3Upi1d.js} +2 -2
  26. package/dist/web/assets/vue-vendor-DqyWIXEb.js +45 -0
  27. package/dist/web/assets/xterm-6GBZ9nXN.css +32 -0
  28. package/dist/web/assets/xterm-BJzAjXCH.js +13 -0
  29. package/dist/web/index.html +8 -6
  30. package/package.json +4 -2
  31. package/src/commands/channels.js +48 -1
  32. package/src/commands/cli-type.js +4 -2
  33. package/src/commands/daemon.js +81 -12
  34. package/src/commands/doctor.js +10 -9
  35. package/src/commands/list.js +1 -1
  36. package/src/commands/logs.js +6 -4
  37. package/src/commands/port-config.js +24 -4
  38. package/src/commands/proxy-control.js +12 -6
  39. package/src/commands/search.js +1 -1
  40. package/src/commands/security.js +3 -2
  41. package/src/commands/stats.js +226 -52
  42. package/src/commands/switch.js +1 -1
  43. package/src/commands/toggle-proxy.js +31 -6
  44. package/src/commands/update.js +97 -0
  45. package/src/commands/workspace.js +1 -1
  46. package/src/config/default.js +41 -2
  47. package/src/config/loader.js +74 -8
  48. package/src/config/model-metadata.js +415 -0
  49. package/src/config/model-pricing.js +23 -93
  50. package/src/config/paths.js +105 -33
  51. package/src/index.js +64 -3
  52. package/src/plugins/constants.js +3 -2
  53. package/src/plugins/plugin-api.js +1 -1
  54. package/src/reset-config.js +4 -2
  55. package/src/server/api/agents.js +57 -14
  56. package/src/server/api/channels.js +112 -33
  57. package/src/server/api/codex-channels.js +111 -18
  58. package/src/server/api/codex-proxy.js +14 -8
  59. package/src/server/api/commands.js +71 -18
  60. package/src/server/api/config-export.js +0 -6
  61. package/src/server/api/config-registry.js +11 -3
  62. package/src/server/api/config.js +376 -5
  63. package/src/server/api/convert.js +133 -0
  64. package/src/server/api/dashboard.js +22 -6
  65. package/src/server/api/gemini-channels.js +107 -18
  66. package/src/server/api/gemini-proxy.js +14 -8
  67. package/src/server/api/gemini-sessions.js +1 -1
  68. package/src/server/api/health-check.js +4 -3
  69. package/src/server/api/mcp.js +3 -3
  70. package/src/server/api/opencode-channels.js +497 -0
  71. package/src/server/api/opencode-projects.js +99 -0
  72. package/src/server/api/opencode-proxy.js +207 -0
  73. package/src/server/api/opencode-sessions.js +345 -0
  74. package/src/server/api/opencode-statistics.js +57 -0
  75. package/src/server/api/plugins.js +66 -19
  76. package/src/server/api/prompts.js +2 -2
  77. package/src/server/api/proxy.js +7 -4
  78. package/src/server/api/sessions.js +3 -0
  79. package/src/server/api/settings.js +111 -0
  80. package/src/server/api/skills.js +69 -18
  81. package/src/server/api/workspaces.js +78 -6
  82. package/src/server/codex-proxy-server.js +36 -22
  83. package/src/server/dev-server.js +1 -1
  84. package/src/server/gemini-proxy-server.js +21 -7
  85. package/src/server/index.js +174 -58
  86. package/src/server/opencode-proxy-server.js +5486 -0
  87. package/src/server/proxy-server.js +33 -22
  88. package/src/server/services/agents-service.js +61 -24
  89. package/src/server/services/channel-scheduler.js +9 -5
  90. package/src/server/services/channels.js +64 -37
  91. package/src/server/services/codex-channels.js +56 -43
  92. package/src/server/services/codex-sessions.js +105 -6
  93. package/src/server/services/codex-settings-manager.js +271 -49
  94. package/src/server/services/codex-statistics-service.js +2 -2
  95. package/src/server/services/commands-service.js +84 -25
  96. package/src/server/services/config-export-service.js +7 -45
  97. package/src/server/services/config-registry-service.js +63 -17
  98. package/src/server/services/config-sync-manager.js +160 -7
  99. package/src/server/services/config-templates-service.js +204 -51
  100. package/src/server/services/env-checker.js +50 -13
  101. package/src/server/services/env-manager.js +155 -19
  102. package/src/server/services/favorites.js +5 -3
  103. package/src/server/services/gemini-channels.js +33 -44
  104. package/src/server/services/gemini-statistics-service.js +2 -2
  105. package/src/server/services/mcp-service.js +350 -9
  106. package/src/server/services/model-detector.js +707 -221
  107. package/src/server/services/network-access.js +80 -0
  108. package/src/server/services/opencode-channels.js +208 -0
  109. package/src/server/services/opencode-gateway-converter.js +639 -0
  110. package/src/server/services/opencode-sessions.js +931 -0
  111. package/src/server/services/opencode-settings-manager.js +478 -0
  112. package/src/server/services/opencode-statistics-service.js +255 -0
  113. package/src/server/services/plugins-service.js +479 -22
  114. package/src/server/services/prompts-service.js +53 -11
  115. package/src/server/services/proxy-runtime.js +1 -1
  116. package/src/server/services/repo-scanner-base.js +1 -1
  117. package/src/server/services/response-decoder.js +21 -0
  118. package/src/server/services/security-config.js +1 -1
  119. package/src/server/services/session-cache.js +1 -1
  120. package/src/server/services/skill-service.js +300 -46
  121. package/src/server/services/speed-test.js +464 -186
  122. package/src/server/services/statistics-service.js +2 -2
  123. package/src/server/services/terminal-commands.js +10 -3
  124. package/src/server/services/terminal-config.js +1 -1
  125. package/src/server/services/ui-config.js +1 -1
  126. package/src/server/services/workspace-service.js +57 -100
  127. package/src/server/websocket-server.js +156 -8
  128. package/src/ui/menu.js +49 -40
  129. package/src/utils/port-helper.js +22 -8
  130. package/src/utils/session.js +5 -4
  131. package/dist/web/assets/icons-CO_2OFES.js +0 -1
  132. package/dist/web/assets/index-DI8QOi-E.js +0 -14
  133. package/dist/web/assets/index-uLHGdeZh.css +0 -41
  134. package/dist/web/assets/naive-ui-B1re3c-e.js +0 -1
  135. package/dist/web/assets/vue-vendor-6JaYHOiI.js +0 -44
  136. package/src/server/api/oauth.js +0 -294
  137. package/src/server/api/permissions.js +0 -385
  138. package/src/server/config/oauth-providers.js +0 -68
  139. package/src/server/services/oauth-callback-server.js +0 -284
  140. package/src/server/services/oauth-service.js +0 -378
  141. package/src/server/services/oauth-token-storage.js +0 -135
  142. package/src/server/services/permission-templates-service.js +0 -308
@@ -20,9 +20,17 @@ const { injectEnvToShell, removeEnvFromShell, isProxyConfig } = require('./codex
20
20
  * - 使用 weight 和 maxConcurrency 控制负载均衡
21
21
  */
22
22
 
23
+ function normalizeGatewaySourceType(value, fallback = 'codex') {
24
+ const normalized = String(value || '').trim().toLowerCase();
25
+ if (normalized === 'claude') return 'claude';
26
+ if (normalized === 'codex') return 'codex';
27
+ if (normalized === 'gemini') return 'gemini';
28
+ return fallback;
29
+ }
30
+
23
31
  // 获取渠道存储文件路径
24
32
  function getChannelsFilePath() {
25
- const ccToolDir = path.join(os.homedir(), '.claude', 'cc-tool');
33
+ const ccToolDir = path.join(os.homedir(), '.cc-tool');
26
34
  if (!fs.existsSync(ccToolDir)) {
27
35
  fs.mkdirSync(ccToolDir, { recursive: true });
28
36
  }
@@ -43,15 +51,17 @@ function loadChannels() {
43
51
  const data = JSON.parse(content);
44
52
  // 确保渠道有 enabled 字段(兼容旧数据)
45
53
  if (data.channels) {
46
- data.channels = data.channels.map(ch => ({
47
- ...ch,
48
- enabled: ch.enabled !== false, // 默认启用
49
- weight: ch.weight || 1,
50
- maxConcurrency: ch.maxConcurrency || null,
51
- modelRedirects: ch.modelRedirects || [],
52
- speedTestModel: ch.speedTestModel || null,
53
- authType: ch.authType || 'apiKey' // 默认 API Key 认证
54
- }));
54
+ data.channels = data.channels.map(ch => {
55
+ return {
56
+ ...ch,
57
+ enabled: ch.enabled !== false, // 默认启用
58
+ weight: ch.weight || 1,
59
+ maxConcurrency: ch.maxConcurrency || null,
60
+ modelRedirects: ch.modelRedirects || [],
61
+ speedTestModel: ch.speedTestModel || null,
62
+ gatewaySourceType: normalizeGatewaySourceType(ch.gatewaySourceType, 'codex')
63
+ };
64
+ });
55
65
  }
56
66
  return data;
57
67
  } catch (err) {
@@ -111,6 +121,7 @@ function initializeFromConfig() {
111
121
  enabled: config.model_provider === providerKey, // 当前激活的渠道启用
112
122
  weight: 1,
113
123
  maxConcurrency: null,
124
+ gatewaySourceType: 'codex',
114
125
  createdAt: Date.now(),
115
126
  updatedAt: Date.now()
116
127
  });
@@ -180,9 +191,7 @@ function createChannel(name, providerKey, baseUrl, apiKey, wireApi = 'responses'
180
191
  maxConcurrency: extraConfig.maxConcurrency || null,
181
192
  modelRedirects: extraConfig.modelRedirects || [],
182
193
  speedTestModel: extraConfig.speedTestModel || null,
183
- authType: extraConfig.authType || 'apiKey',
184
- oauthProvider: extraConfig.oauthProvider || null,
185
- oauthTokenId: extraConfig.oauthTokenId || null,
194
+ gatewaySourceType: normalizeGatewaySourceType(extraConfig.gatewaySourceType, 'codex'),
186
195
  createdAt: Date.now(),
187
196
  updatedAt: Date.now()
188
197
  };
@@ -191,8 +200,8 @@ function createChannel(name, providerKey, baseUrl, apiKey, wireApi = 'responses'
191
200
  saveChannels(data);
192
201
 
193
202
  // 注入该渠道的环境变量(用于直接使用 codex 命令)
194
- if (apiKey && envKey) {
195
- const injectResult = injectEnvToShell(envKey, apiKey);
203
+ if (newChannel.enabled !== false && newChannel.apiKey && envKey) {
204
+ const injectResult = injectEnvToShell(envKey, newChannel.apiKey);
196
205
  if (injectResult.success) {
197
206
  console.log(`[Codex Channels] Environment variable ${envKey} injected for new channel`);
198
207
  } else {
@@ -232,6 +241,7 @@ function updateChannel(channelId, updates) {
232
241
  createdAt: oldChannel.createdAt, // 保持创建时间
233
242
  modelRedirects: merged.modelRedirects || [],
234
243
  speedTestModel: merged.speedTestModel !== undefined ? merged.speedTestModel : (oldChannel.speedTestModel || null),
244
+ gatewaySourceType: normalizeGatewaySourceType(merged.gatewaySourceType, 'codex'),
235
245
  updatedAt: Date.now()
236
246
  };
237
247
 
@@ -273,19 +283,23 @@ function updateChannel(channelId, updates) {
273
283
  // 如果 envKey 或 apiKey 变化,需要更新环境变量
274
284
  const oldEnvKey = oldChannel.envKey;
275
285
  const newEnvKey = newChannel.envKey;
276
- const oldApiKey = oldChannel.apiKey;
277
286
  const newApiKey = newChannel.apiKey;
278
-
279
- // 如果 envKey 改变,删除旧的,注入新的
280
- if (oldEnvKey !== newEnvKey && oldEnvKey) {
287
+ const shouldRemoveOldEnv =
288
+ !!oldEnvKey && (
289
+ oldEnvKey !== newEnvKey ||
290
+ !newApiKey ||
291
+ newChannel.enabled === false
292
+ );
293
+
294
+ // 禁用或 key 变化时都要清理旧环境变量,避免残留
295
+ if (shouldRemoveOldEnv) {
281
296
  const removeResult = removeEnvFromShell(oldEnvKey);
282
297
  if (removeResult.success) {
283
298
  console.log(`[Codex Channels] Old environment variable ${oldEnvKey} removed`);
284
299
  }
285
300
  }
286
301
 
287
- // 如果有新的 API Key,注入到环境变量
288
- if (newApiKey && newEnvKey) {
302
+ if (newChannel.enabled !== false && newApiKey && newEnvKey) {
289
303
  const injectResult = injectEnvToShell(newEnvKey, newApiKey);
290
304
  if (injectResult.success) {
291
305
  console.log(`[Codex Channels] Environment variable ${newEnvKey} updated`);
@@ -299,7 +313,7 @@ function updateChannel(channelId, updates) {
299
313
  }
300
314
 
301
315
  // 删除渠道
302
- function deleteChannel(channelId) {
316
+ async function deleteChannel(channelId) {
303
317
  const data = loadChannels();
304
318
 
305
319
  const index = data.channels.findIndex(c => c.id === channelId);
@@ -401,7 +415,7 @@ function writeCodexConfigForMultiChannel(allChannels) {
401
415
  // 回退默认的代理配置(使用默认端口),确保 provider 存在
402
416
  config.model_providers['cc-proxy'] = {
403
417
  name: 'cc-proxy',
404
- base_url: 'http://127.0.0.1:10089/v1',
418
+ base_url: 'http://127.0.0.1:20089/v1',
405
419
  wire_api: 'responses',
406
420
  env_key: 'CC_PROXY_KEY'
407
421
  };
@@ -452,6 +466,12 @@ ${tomlContent}`;
452
466
  }
453
467
 
454
468
  // 更新所有渠道的 API Key
469
+ for (const channel of allChannels) {
470
+ if (channel.envKey && !channel.apiKey) {
471
+ delete auth[channel.envKey];
472
+ }
473
+ }
474
+
455
475
  for (const channel of allChannels) {
456
476
  if (channel.apiKey) {
457
477
  auth[channel.envKey] = channel.apiKey;
@@ -512,7 +532,10 @@ function syncAllChannelEnvVars() {
512
532
  const results = [];
513
533
 
514
534
  for (const channel of channels) {
515
- if (channel.apiKey && channel.envKey) {
535
+ if (!channel.envKey) continue;
536
+
537
+ const shouldInject = channel.enabled !== false && !!channel.apiKey;
538
+ if (shouldInject) {
516
539
  const injectResult = injectEnvToShell(channel.envKey, channel.apiKey);
517
540
  if (injectResult.success) {
518
541
  syncedCount++;
@@ -520,7 +543,11 @@ function syncAllChannelEnvVars() {
520
543
  } else {
521
544
  results.push({ envKey: channel.envKey, success: false, error: injectResult.error });
522
545
  }
546
+ continue;
523
547
  }
548
+
549
+ // 清理已停用或缺失 key 的渠道环境变量,避免残留
550
+ removeEnvFromShell(channel.envKey);
524
551
  }
525
552
 
526
553
  console.log(`[Codex Channels] Synced ${syncedCount} environment variables`);
@@ -624,19 +651,21 @@ ${tomlContent}`;
624
651
  }
625
652
  }
626
653
 
627
- // 添加当前渠道的 API Key
628
654
  if (channel.apiKey && channel.envKey) {
629
655
  auth[channel.envKey] = channel.apiKey;
656
+ } else if (channel.envKey) {
657
+ delete auth[channel.envKey];
630
658
  }
631
659
 
632
660
  fs.writeFileSync(authPath, JSON.stringify(auth, null, 2), 'utf8');
633
661
 
634
- // 注入环境变量到 shell 配置文件
635
662
  if (channel.apiKey && channel.envKey) {
636
663
  const injectResult = injectEnvToShell(channel.envKey, channel.apiKey);
637
664
  if (injectResult.success) {
638
665
  console.log(`[Codex Channels] Environment variable ${channel.envKey} injected`);
639
666
  }
667
+ } else if (channel.envKey) {
668
+ removeEnvFromShell(channel.envKey);
640
669
  }
641
670
 
642
671
  return channel;
@@ -653,24 +682,8 @@ try {
653
682
  console.warn('[Codex Channels] Auto sync env vars failed:', err.message);
654
683
  }
655
684
 
656
- /**
657
- * 获取渠道的有效 API Key
658
- * 如果渠道使用 OAuth 认证,返回 OAuth 令牌;否则返回静态 API Key
659
- *
660
- * @param {Object} channel - 渠道对象
661
- * @returns {string|null} 有效的 API Key
662
- */
663
685
  function getEffectiveApiKey(channel) {
664
- if (channel.authType === 'oauth' && channel.oauthTokenId) {
665
- const { getToken, isTokenExpired } = require('./oauth-token-storage');
666
- const token = getToken(channel.oauthTokenId);
667
- if (token && !isTokenExpired(token)) {
668
- return token.accessToken;
669
- }
670
- // OAuth 令牌无效或已过期,返回 null
671
- return null;
672
- }
673
- return channel.apiKey;
686
+ return channel.apiKey || null;
674
687
  }
675
688
 
676
689
  module.exports = {
@@ -3,6 +3,15 @@ const path = require('path');
3
3
  const { getCodexDir } = require('./codex-config');
4
4
  const { parseSession, parseSessionMeta, extractSessionMeta, readJSONL } = require('./codex-parser');
5
5
 
6
+ const COUNTS_CACHE_TTL_MS = 30 * 1000;
7
+ const FAST_META_READ_BYTES = 64 * 1024;
8
+ const EMPTY_COUNTS = Object.freeze({ projectCount: 0, sessionCount: 0 });
9
+
10
+ let countsCache = {
11
+ expiresAt: 0,
12
+ value: EMPTY_COUNTS
13
+ };
14
+
6
15
  /**
7
16
  * 获取会话目录
8
17
  */
@@ -437,6 +446,7 @@ function deleteProject(projectName) {
437
446
  console.error('[Codex] Failed to clean session order:', err.message);
438
447
  }
439
448
 
449
+ invalidateProjectAndSessionCountsCache();
440
450
  return { success: true, deletedCount };
441
451
  }
442
452
 
@@ -524,6 +534,7 @@ function deleteSession(sessionId) {
524
534
  // 忽略别名不存在的错误
525
535
  }
526
536
 
537
+ invalidateProjectAndSessionCountsCache();
527
538
  return { success: true };
528
539
  }
529
540
 
@@ -577,6 +588,7 @@ function forkSession(sessionId) {
577
588
  forkRelations[newSessionId] = sessionId;
578
589
  saveForkRelations(forkRelations);
579
590
 
591
+ invalidateProjectAndSessionCountsCache();
580
592
  return {
581
593
  newSessionId,
582
594
  forkedFrom: sessionId,
@@ -628,19 +640,106 @@ function saveProjectOrder(order) {
628
640
  saveClaudeProjectOrder({ projectsDir: getCodexDir() }, order);
629
641
  }
630
642
 
643
+ function invalidateProjectAndSessionCountsCache() {
644
+ countsCache.expiresAt = 0;
645
+ }
646
+
647
+ function extractCodexProjectNameFromMeta(metaPayload = {}) {
648
+ const repoUrl = metaPayload?.git?.repository_url || metaPayload?.git?.repositoryUrl;
649
+ if (typeof repoUrl === 'string' && repoUrl.trim()) {
650
+ const parsedName = repoUrl.split('/').pop();
651
+ if (parsedName) {
652
+ const normalized = parsedName.replace(/\.git$/i, '').trim();
653
+ if (normalized) return normalized;
654
+ }
655
+ }
656
+
657
+ const cwd = metaPayload?.cwd;
658
+ if (typeof cwd === 'string' && cwd.trim()) {
659
+ return path.basename(cwd.trim());
660
+ }
661
+
662
+ return '';
663
+ }
664
+
665
+ function readSessionMetaPayloadFast(filePath) {
666
+ let fd;
667
+ try {
668
+ fd = fs.openSync(filePath, 'r');
669
+ const buffer = Buffer.alloc(FAST_META_READ_BYTES);
670
+ const bytesRead = fs.readSync(fd, buffer, 0, FAST_META_READ_BYTES, 0);
671
+ if (bytesRead <= 0) return null;
672
+
673
+ const chunk = buffer.toString('utf8', 0, bytesRead);
674
+ const lines = chunk.split('\n');
675
+
676
+ for (const line of lines) {
677
+ const trimmed = line.trim();
678
+ if (!trimmed) continue;
679
+ let parsed;
680
+ try {
681
+ parsed = JSON.parse(trimmed);
682
+ } catch (err) {
683
+ continue;
684
+ }
685
+ if (parsed?.type === 'session_meta' && parsed?.payload && typeof parsed.payload === 'object') {
686
+ return parsed.payload;
687
+ }
688
+ }
689
+ } catch (err) {
690
+ return null;
691
+ } finally {
692
+ if (fd !== undefined) {
693
+ try {
694
+ fs.closeSync(fd);
695
+ } catch (err) {
696
+ // ignore close errors
697
+ }
698
+ }
699
+ }
700
+
701
+ return null;
702
+ }
703
+
704
+ function calculateProjectAndSessionCounts() {
705
+ const sessions = scanSessionFiles();
706
+ if (sessions.length === 0) {
707
+ return EMPTY_COUNTS;
708
+ }
709
+
710
+ const projectNames = new Set();
711
+ sessions.forEach((session) => {
712
+ const payload = readSessionMetaPayloadFast(session.filePath);
713
+ const projectName = extractCodexProjectNameFromMeta(payload || {});
714
+ if (projectName) {
715
+ projectNames.add(projectName);
716
+ }
717
+ });
718
+
719
+ return {
720
+ projectCount: projectNames.size,
721
+ sessionCount: sessions.length
722
+ };
723
+ }
724
+
631
725
  /**
632
726
  * 获取 Codex 项目与会话数量(用于仪表盘轻量统计)
633
727
  */
634
728
  function getProjectAndSessionCounts() {
729
+ const now = Date.now();
730
+ if (countsCache.expiresAt > now) {
731
+ return countsCache.value;
732
+ }
733
+
635
734
  try {
636
- const projects = getProjects();
637
- const sessions = scanSessionFiles();
638
- return {
639
- projectCount: projects.length,
640
- sessionCount: sessions.length
735
+ const counts = calculateProjectAndSessionCounts();
736
+ countsCache = {
737
+ value: counts,
738
+ expiresAt: now + COUNTS_CACHE_TTL_MS
641
739
  };
740
+ return counts;
642
741
  } catch (err) {
643
- return { projectCount: 0, sessionCount: 0 };
742
+ return countsCache.value || EMPTY_COUNTS;
644
743
  }
645
744
  }
646
745