coding-tool-x 3.5.4 → 3.5.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +8 -4
  3. package/dist/web/assets/{Analytics-CmN09J9U.js → Analytics-CRNCHeui.js} +1 -1
  4. package/dist/web/assets/{ConfigTemplates-CeTAPmep.js → ConfigTemplates-C0erJdo2.js} +1 -1
  5. package/dist/web/assets/{Home-BYtCM3rK.js → Home-CL5z6Q4d.js} +1 -1
  6. package/dist/web/assets/{PluginManager-OAH1eMO0.js → PluginManager-hDx0XMO_.js} +1 -1
  7. package/dist/web/assets/{ProjectList-B0pIy1cv.js → ProjectList-BNsz96av.js} +1 -1
  8. package/dist/web/assets/{SessionList-DbB6ASiA.js → SessionList-CG1UhFo3.js} +1 -1
  9. package/dist/web/assets/{SkillManager-wp1dhL1z.js → SkillManager-D6Vwpajh.js} +1 -1
  10. package/dist/web/assets/{WorkspaceManager-Ce6wQoKb.js → WorkspaceManager-C3TjeOPy.js} +1 -1
  11. package/dist/web/assets/{icons-DlxD2wZJ.js → icons-CQuif85v.js} +1 -1
  12. package/dist/web/assets/index-GuER-BmS.js +2 -0
  13. package/dist/web/assets/{index-B02wDWNC.css → index-VGAxnLqi.css} +1 -1
  14. package/dist/web/index.html +3 -3
  15. package/package.json +1 -1
  16. package/src/commands/stats.js +41 -4
  17. package/src/index.js +1 -0
  18. package/src/server/api/codex-sessions.js +6 -3
  19. package/src/server/api/dashboard.js +25 -1
  20. package/src/server/api/gemini-sessions.js +6 -3
  21. package/src/server/api/hooks.js +17 -1
  22. package/src/server/api/opencode-sessions.js +6 -3
  23. package/src/server/api/plugins.js +24 -33
  24. package/src/server/api/sessions.js +6 -3
  25. package/src/server/codex-proxy-server.js +24 -59
  26. package/src/server/gemini-proxy-server.js +25 -66
  27. package/src/server/index.js +6 -4
  28. package/src/server/opencode-proxy-server.js +24 -59
  29. package/src/server/proxy-server.js +18 -30
  30. package/src/server/services/base/response-usage-parser.js +187 -0
  31. package/src/server/services/codex-sessions.js +107 -9
  32. package/src/server/services/network-access.js +14 -0
  33. package/src/server/services/notification-hooks.js +175 -16
  34. package/src/server/services/plugins-service.js +502 -44
  35. package/src/server/services/proxy-log-helper.js +21 -3
  36. package/src/server/services/session-launch-command.js +81 -0
  37. package/src/server/services/sessions.js +103 -33
  38. package/src/server/services/statistics-service.js +7 -0
  39. package/src/server/websocket-server.js +25 -1
  40. package/dist/web/assets/index-CHwVofQH.js +0 -2
@@ -0,0 +1,81 @@
1
+ const { isWindowsLikePlatform } = require('../../utils/home-dir');
2
+
3
+ function escapeForDoubleQuotes(value) {
4
+ return String(value).replace(/"/g, '\\"');
5
+ }
6
+
7
+ function escapeForPowerShellSingleQuotes(value) {
8
+ return String(value).replace(/'/g, "''");
9
+ }
10
+
11
+ function buildDisplayCommand(executable, args = []) {
12
+ return [String(executable || ''), ...args.map(arg => String(arg))].filter(Boolean).join(' ').trim();
13
+ }
14
+
15
+ function buildWindowsCopyCommand(cwd, executable, args = []) {
16
+ const quotedCwd = `'${escapeForPowerShellSingleQuotes(cwd)}'`;
17
+ const quotedExecutable = `'${escapeForPowerShellSingleQuotes(executable)}'`;
18
+ const quotedArgs = args.map(arg => `'${escapeForPowerShellSingleQuotes(arg)}'`).join(' ');
19
+ const invokeCommand = quotedArgs
20
+ ? `& ${quotedExecutable} ${quotedArgs}`
21
+ : `& ${quotedExecutable}`;
22
+
23
+ return `powershell -NoProfile -ExecutionPolicy Bypass -Command "& { Set-Location -LiteralPath ${quotedCwd}; ${invokeCommand} }"`;
24
+ }
25
+
26
+ function buildPosixCopyCommand(cwd, command) {
27
+ const quotedCwd = `"${escapeForDoubleQuotes(cwd)}"`;
28
+ return `cd ${quotedCwd} && ${command}`;
29
+ }
30
+
31
+ function buildCopyCommand({
32
+ cwd,
33
+ command,
34
+ executable,
35
+ args = [],
36
+ runtimePlatform = process.platform,
37
+ runtimeEnv = process.env
38
+ }) {
39
+ const resolvedCommand = command || buildDisplayCommand(executable, args);
40
+ if (!cwd) {
41
+ return resolvedCommand;
42
+ }
43
+
44
+ if (isWindowsLikePlatform(runtimePlatform, runtimeEnv)) {
45
+ return buildWindowsCopyCommand(cwd, executable, args);
46
+ }
47
+
48
+ return buildPosixCopyCommand(cwd, resolvedCommand);
49
+ }
50
+
51
+ function buildLaunchCommand({
52
+ cwd,
53
+ executable,
54
+ args = [],
55
+ runtimePlatform = process.platform,
56
+ runtimeEnv = process.env
57
+ }) {
58
+ const command = buildDisplayCommand(executable, args);
59
+ return {
60
+ command,
61
+ copyCommand: buildCopyCommand({
62
+ cwd,
63
+ command,
64
+ executable,
65
+ args,
66
+ runtimePlatform,
67
+ runtimeEnv
68
+ })
69
+ };
70
+ }
71
+
72
+ module.exports = {
73
+ buildLaunchCommand,
74
+ _test: {
75
+ buildDisplayCommand,
76
+ buildCopyCommand,
77
+ buildWindowsCopyCommand,
78
+ buildPosixCopyCommand,
79
+ escapeForPowerShellSingleQuotes
80
+ }
81
+ };
@@ -16,6 +16,9 @@ const { PATHS, NATIVE_PATHS } = require('../../config/paths');
16
16
  const CLAUDE_PROJECTS_DIR = NATIVE_PATHS.claude.projects;
17
17
  const CODEX_PROJECTS_DIR = path.join(path.dirname(NATIVE_PATHS.codex.config), 'projects');
18
18
  const GEMINI_PROJECTS_DIR = path.join(path.dirname(NATIVE_PATHS.gemini.env), 'projects');
19
+ const PROJECT_PATH_CACHE_TTL_MS = 5 * 60 * 1000;
20
+ const MAX_PROJECT_PATH_CACHE_ENTRIES = 500;
21
+ let projectPathResolutionCache = new Map();
19
22
 
20
23
  // Base directory for cc-tool data
21
24
  function getCcToolDir() {
@@ -100,15 +103,82 @@ async function getProjects(config) {
100
103
  .map(entry => entry.name);
101
104
  }
102
105
 
106
+ function getProjectPathCacheKey(encodedName) {
107
+ return `${process.platform}:${encodedName}`;
108
+ }
109
+
110
+ function cloneProjectPathResolution(value) {
111
+ return {
112
+ fullPath: value?.fullPath || '',
113
+ projectName: value?.projectName || ''
114
+ };
115
+ }
116
+
117
+ function getCachedProjectPathResolution(encodedName) {
118
+ const cacheKey = getProjectPathCacheKey(encodedName);
119
+ const cached = projectPathResolutionCache.get(cacheKey);
120
+ if (!cached) {
121
+ return null;
122
+ }
123
+
124
+ if (cached.expiresAt <= Date.now()) {
125
+ projectPathResolutionCache.delete(cacheKey);
126
+ return null;
127
+ }
128
+
129
+ return cloneProjectPathResolution(cached.value);
130
+ }
131
+
132
+ function setCachedProjectPathResolution(encodedName, value) {
133
+ const cacheKey = getProjectPathCacheKey(encodedName);
134
+ projectPathResolutionCache.delete(cacheKey);
135
+ projectPathResolutionCache.set(cacheKey, {
136
+ expiresAt: Date.now() + PROJECT_PATH_CACHE_TTL_MS,
137
+ value: cloneProjectPathResolution(value)
138
+ });
139
+
140
+ while (projectPathResolutionCache.size > MAX_PROJECT_PATH_CACHE_ENTRIES) {
141
+ const oldestKey = projectPathResolutionCache.keys().next().value;
142
+ if (!oldestKey) {
143
+ break;
144
+ }
145
+ projectPathResolutionCache.delete(oldestKey);
146
+ }
147
+ }
148
+
149
+ function clearProjectPathResolutionCache(encodedName) {
150
+ if (!encodedName) {
151
+ projectPathResolutionCache.clear();
152
+ return;
153
+ }
154
+
155
+ projectPathResolutionCache.delete(getProjectPathCacheKey(encodedName));
156
+ }
157
+
103
158
  // Parse real project path from encoded name
104
159
  // macOS/Linux: "-Users-lilithgames-work-project" -> "/Users/lilithgames/work/project"
105
160
  // Windows: "C--Users-admin-Desktop-project" -> "C:\Users\admin\Desktop\project"
106
161
  function parseRealProjectPath(encodedName) {
162
+ const normalizedEncodedName = String(encodedName || '').trim();
163
+ const cached = getCachedProjectPathResolution(normalizedEncodedName);
164
+ if (cached) {
165
+ return cached;
166
+ }
167
+
107
168
  const isWindows = process.platform === 'win32';
108
- const fallbackFromSessions = tryResolvePathFromSessions(encodedName);
169
+ const fallbackFromSessions = tryResolvePathFromSessions(normalizedEncodedName);
170
+
171
+ if (fallbackFromSessions?.fullPath) {
172
+ const resolved = {
173
+ fullPath: fallbackFromSessions.fullPath,
174
+ projectName: fallbackFromSessions.projectName || path.basename(fallbackFromSessions.fullPath) || normalizedEncodedName
175
+ };
176
+ setCachedProjectPathResolution(normalizedEncodedName, resolved);
177
+ return resolved;
178
+ }
109
179
 
110
180
  // Detect Windows drive letter (e.g., "C--Users-admin")
111
- const windowsDriveMatch = encodedName.match(/^([A-Z])--(.+)$/);
181
+ const windowsDriveMatch = normalizedEncodedName.match(/^([A-Z])--(.+)$/);
112
182
 
113
183
  if (isWindows && windowsDriveMatch) {
114
184
  // Windows path with drive letter
@@ -167,13 +237,15 @@ function parseRealProjectPath(encodedName) {
167
237
  currentPath = driveLetter + ':\\' + realSegments.join('\\');
168
238
  }
169
239
 
170
- return {
171
- fullPath: validateProjectPath(currentPath) || fallbackFromSessions?.fullPath || (driveLetter + ':\\' + restPath.replace(/-/g, '\\')),
172
- projectName: fallbackFromSessions?.projectName || realSegments[realSegments.length - 1] || encodedName
240
+ const resolved = {
241
+ fullPath: validateProjectPath(currentPath) || (driveLetter + ':\\' + restPath.replace(/-/g, '\\')),
242
+ projectName: realSegments[realSegments.length - 1] || normalizedEncodedName
173
243
  };
244
+ setCachedProjectPathResolution(normalizedEncodedName, resolved);
245
+ return resolved;
174
246
  } else {
175
247
  // Unix-like path (macOS/Linux) or fallback
176
- const pathStr = encodedName.replace(/^-/, '/').replace(/-/g, '/');
248
+ const pathStr = normalizedEncodedName.replace(/^-/, '/').replace(/-/g, '/');
177
249
  const segments = pathStr.split('/').filter(s => s);
178
250
 
179
251
  // Build path from left to right, checking existence
@@ -225,10 +297,12 @@ function parseRealProjectPath(encodedName) {
225
297
  currentPath = '/' + realSegments.join('/');
226
298
  }
227
299
 
228
- return {
229
- fullPath: validateProjectPath(currentPath) || fallbackFromSessions?.fullPath || pathStr,
230
- projectName: fallbackFromSessions?.projectName || realSegments[realSegments.length - 1] || encodedName
300
+ const resolved = {
301
+ fullPath: validateProjectPath(currentPath) || pathStr,
302
+ projectName: realSegments[realSegments.length - 1] || normalizedEncodedName
231
303
  };
304
+ setCachedProjectPathResolution(normalizedEncodedName, resolved);
305
+ return resolved;
232
306
  }
233
307
  }
234
308
 
@@ -347,27 +421,20 @@ async function buildProjectsWithStats(config) {
347
421
  const files = await fs.promises.readdir(projectPath);
348
422
  const jsonlFiles = files.filter(f => f.endsWith('.jsonl') && !f.startsWith('agent-'));
349
423
 
350
- // Filter: only count sessions that have actual messages (in parallel)
351
424
  const sessionChecks = await Promise.all(
352
- jsonlFiles.map(async (f) => {
353
- const filePath = path.join(projectPath, f);
354
- const hasMessages = await hasActualMessages(filePath);
355
- return hasMessages ? f : null;
425
+ jsonlFiles.map(async (fileName) => {
426
+ const filePath = path.join(projectPath, fileName);
427
+ const stats = await fs.promises.stat(filePath);
428
+ const hasMessages = await hasActualMessages(filePath, stats);
429
+ return hasMessages ? stats.mtime.getTime() : null;
356
430
  })
357
431
  );
358
432
 
359
- const sessionFilesWithMessages = sessionChecks.filter(f => f !== null);
360
- sessionCount = sessionFilesWithMessages.length;
433
+ const activeSessionTimes = sessionChecks.filter((mtimeMs) => Number.isFinite(mtimeMs));
434
+ sessionCount = activeSessionTimes.length;
361
435
 
362
- // Find most recent session (only from sessions with messages)
363
- if (sessionFilesWithMessages.length > 0) {
364
- const statPromises = sessionFilesWithMessages.map(async (f) => {
365
- const filePath = path.join(projectPath, f);
366
- const stat = await fs.promises.stat(filePath);
367
- return stat.mtime.getTime();
368
- });
369
- const stats = await Promise.all(statPromises);
370
- lastUsed = Math.max(...stats);
436
+ if (activeSessionTimes.length > 0) {
437
+ lastUsed = Math.max(...activeSessionTimes);
371
438
  }
372
439
  } catch (err) {
373
440
  // Ignore errors
@@ -415,9 +482,9 @@ function getProjectAndSessionCounts(config) {
415
482
  }
416
483
 
417
484
  // Check if a session file has actual messages (async with enhanced caching)
418
- async function hasActualMessages(filePath) {
485
+ async function hasActualMessages(filePath, statsOverride = null) {
419
486
  try {
420
- const stats = await fs.promises.stat(filePath);
487
+ const stats = statsOverride || await fs.promises.stat(filePath);
421
488
 
422
489
  // Check enhanced cache first
423
490
  const cacheKey = `${CacheKeys.HAS_MESSAGES}${filePath}:${stats.mtime.getTime()}`;
@@ -658,14 +725,13 @@ function saveSessionOrder(projectName, order) {
658
725
  // Delete a project (remove the entire project directory)
659
726
  function deleteProject(config, projectName) {
660
727
  const projectDir = path.join(config.projectsDir, projectName);
728
+ const existed = fs.existsSync(projectDir);
661
729
 
662
- if (!fs.existsSync(projectDir)) {
663
- throw new Error('Project not found');
730
+ if (existed) {
731
+ // Recursively delete the directory
732
+ fs.rmSync(projectDir, { recursive: true, force: true });
664
733
  }
665
734
 
666
- // Recursively delete the directory
667
- fs.rmSync(projectDir, { recursive: true, force: true });
668
-
669
735
  // Remove from order file if exists
670
736
  const order = getProjectOrder(config);
671
737
  const newOrder = order.filter(name => name !== projectName);
@@ -674,7 +740,11 @@ function deleteProject(config, projectName) {
674
740
  }
675
741
 
676
742
  invalidateProjectsCache(config);
677
- return { success: true };
743
+ clearProjectPathResolutionCache(projectName);
744
+ return {
745
+ success: true,
746
+ alreadyDeleted: !existed
747
+ };
678
748
  }
679
749
 
680
750
  // Search sessions for keyword
@@ -242,6 +242,13 @@ function recordRequest(requestData) {
242
242
  session,
243
243
  project
244
244
  };
245
+ // 如果有模型重定向信息,记录到日志中
246
+ if (requestData.originalModel) {
247
+ logEntry.originalModel = requestData.originalModel;
248
+ }
249
+ if (requestData.redirectedModel) {
250
+ logEntry.redirectedModel = requestData.redirectedModel;
251
+ }
245
252
  appendRequestLog(logEntry);
246
253
 
247
254
  // 2. 更新总体统计
@@ -504,11 +504,35 @@ function broadcastSchedulerState(source, schedulerState) {
504
504
  }
505
505
  }
506
506
 
507
+ function broadcastBrowserNotification(event = {}) {
508
+ const notification = {
509
+ type: 'browser-notification',
510
+ title: String(event.title || '').trim() || 'Coding Tool',
511
+ source: String(event.source || '').trim() || 'claude',
512
+ sourceLabel: String(event.sourceLabel || '').trim() || 'Claude Code',
513
+ mode: String(event.mode || '').trim() || 'notification',
514
+ eventType: String(event.eventType || '').trim(),
515
+ message: String(event.message || '').trim() || '任务已完成 | 等待交互',
516
+ timestamp: typeof event.timestamp === 'number' ? event.timestamp : Date.now()
517
+ };
518
+
519
+ if (wss && wsClients.size > 0) {
520
+ const message = JSON.stringify(notification);
521
+
522
+ wsClients.forEach(client => {
523
+ if (client.readyState === WebSocket.OPEN) {
524
+ client.send(message);
525
+ }
526
+ });
527
+ }
528
+ }
529
+
507
530
  module.exports = {
508
531
  startWebSocketServer,
509
532
  stopWebSocketServer,
510
533
  broadcastLog,
511
534
  clearAllLogs,
512
535
  broadcastProxyState,
513
- broadcastSchedulerState
536
+ broadcastSchedulerState,
537
+ broadcastBrowserNotification
514
538
  };