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.
- package/CHANGELOG.md +7 -0
- package/README.md +8 -4
- package/dist/web/assets/{Analytics-CmN09J9U.js → Analytics-CRNCHeui.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-CeTAPmep.js → ConfigTemplates-C0erJdo2.js} +1 -1
- package/dist/web/assets/{Home-BYtCM3rK.js → Home-CL5z6Q4d.js} +1 -1
- package/dist/web/assets/{PluginManager-OAH1eMO0.js → PluginManager-hDx0XMO_.js} +1 -1
- package/dist/web/assets/{ProjectList-B0pIy1cv.js → ProjectList-BNsz96av.js} +1 -1
- package/dist/web/assets/{SessionList-DbB6ASiA.js → SessionList-CG1UhFo3.js} +1 -1
- package/dist/web/assets/{SkillManager-wp1dhL1z.js → SkillManager-D6Vwpajh.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Ce6wQoKb.js → WorkspaceManager-C3TjeOPy.js} +1 -1
- package/dist/web/assets/{icons-DlxD2wZJ.js → icons-CQuif85v.js} +1 -1
- package/dist/web/assets/index-GuER-BmS.js +2 -0
- package/dist/web/assets/{index-B02wDWNC.css → index-VGAxnLqi.css} +1 -1
- package/dist/web/index.html +3 -3
- package/package.json +1 -1
- package/src/commands/stats.js +41 -4
- package/src/index.js +1 -0
- package/src/server/api/codex-sessions.js +6 -3
- package/src/server/api/dashboard.js +25 -1
- package/src/server/api/gemini-sessions.js +6 -3
- package/src/server/api/hooks.js +17 -1
- package/src/server/api/opencode-sessions.js +6 -3
- package/src/server/api/plugins.js +24 -33
- package/src/server/api/sessions.js +6 -3
- package/src/server/codex-proxy-server.js +24 -59
- package/src/server/gemini-proxy-server.js +25 -66
- package/src/server/index.js +6 -4
- package/src/server/opencode-proxy-server.js +24 -59
- package/src/server/proxy-server.js +18 -30
- package/src/server/services/base/response-usage-parser.js +187 -0
- package/src/server/services/codex-sessions.js +107 -9
- package/src/server/services/network-access.js +14 -0
- package/src/server/services/notification-hooks.js +175 -16
- package/src/server/services/plugins-service.js +502 -44
- package/src/server/services/proxy-log-helper.js +21 -3
- package/src/server/services/session-launch-command.js +81 -0
- package/src/server/services/sessions.js +103 -33
- package/src/server/services/statistics-service.js +7 -0
- package/src/server/websocket-server.js +25 -1
- 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(
|
|
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 =
|
|
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
|
-
|
|
171
|
-
fullPath: validateProjectPath(currentPath) ||
|
|
172
|
-
projectName:
|
|
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 =
|
|
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
|
-
|
|
229
|
-
fullPath: validateProjectPath(currentPath) ||
|
|
230
|
-
projectName:
|
|
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 (
|
|
353
|
-
const filePath = path.join(projectPath,
|
|
354
|
-
const
|
|
355
|
-
|
|
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
|
|
360
|
-
sessionCount =
|
|
433
|
+
const activeSessionTimes = sessionChecks.filter((mtimeMs) => Number.isFinite(mtimeMs));
|
|
434
|
+
sessionCount = activeSessionTimes.length;
|
|
361
435
|
|
|
362
|
-
|
|
363
|
-
|
|
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 (
|
|
663
|
-
|
|
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
|
-
|
|
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
|
};
|