coding-tool-x 3.5.5 → 3.5.7
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/README.md +25 -4
- package/bin/ctx.js +6 -1
- package/dist/web/assets/{Analytics-gvYu5sCM.js → Analytics-C6DEmD3D.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-CPlH8Ehd.js → ConfigTemplates-Cf_iTpC4.js} +1 -1
- package/dist/web/assets/{Home-B-qbu3uk.js → Home-BtBmYLJ1.js} +1 -1
- package/dist/web/assets/{PluginManager-B2tQ_YUq.js → PluginManager-DEk8vSw5.js} +1 -1
- package/dist/web/assets/{ProjectList-kDadoXXs.js → ProjectList-BMVhA_Kh.js} +1 -1
- package/dist/web/assets/{SessionList-eLgITwTV.js → SessionList-B5ioAXxg.js} +1 -1
- package/dist/web/assets/{SkillManager-B7zEB5Op.js → SkillManager-DcZOiiSf.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-C-RzB3ud.js → WorkspaceManager-BHqI8aGV.js} +1 -1
- package/dist/web/assets/{icons-DlxD2wZJ.js → icons-CQuif85v.js} +1 -1
- package/dist/web/assets/index-CtByKdkA.js +2 -0
- package/dist/web/assets/{index-BHeh2z0i.css → index-VGAxnLqi.css} +1 -1
- package/dist/web/index.html +3 -3
- package/docs/Caddyfile.example +19 -0
- package/docs/reverse-proxy-https.md +57 -0
- package/package.json +2 -1
- package/src/commands/daemon.js +33 -5
- package/src/commands/stats.js +41 -4
- package/src/commands/ui.js +12 -3
- package/src/config/paths.js +6 -0
- package/src/index.js +125 -34
- 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/index.js +31 -9
- package/src/server/services/codex-sessions.js +107 -9
- package/src/server/services/https-cert.js +171 -0
- package/src/server/services/network-access.js +61 -2
- package/src/server/services/notification-hooks.js +181 -16
- package/src/server/services/plugins-service.js +502 -44
- package/src/server/services/session-launch-command.js +81 -0
- package/src/server/services/sessions.js +103 -33
- package/src/server/services/web-ui-runtime.js +54 -0
- package/src/server/websocket-server.js +35 -4
- package/dist/web/assets/index-DG00t-zy.js +0 -2
|
@@ -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
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
function normalizeWebUiProtocol(value) {
|
|
2
|
+
return String(value || '').trim().toLowerCase() === 'https' ? 'https' : 'http';
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function hasHttpsFlag(argv = process.argv) {
|
|
6
|
+
return Array.isArray(argv) && argv.includes('--https');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function getWebUiProtocol(options = {}) {
|
|
10
|
+
if (typeof options.https === 'boolean') {
|
|
11
|
+
return options.https ? 'https' : 'http';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (typeof options.protocol === 'string' && options.protocol.trim()) {
|
|
15
|
+
return normalizeWebUiProtocol(options.protocol);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const env = options.env || process.env;
|
|
19
|
+
if (env && typeof env.CC_TOOL_WEB_UI_PROTOCOL === 'string' && env.CC_TOOL_WEB_UI_PROTOCOL.trim()) {
|
|
20
|
+
return normalizeWebUiProtocol(env.CC_TOOL_WEB_UI_PROTOCOL);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return hasHttpsFlag(options.argv || process.argv) ? 'https' : 'http';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isHttpsEnabled(options = {}) {
|
|
27
|
+
return getWebUiProtocol(options) === 'https';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getDefaultLoopbackHost(protocol = 'http') {
|
|
31
|
+
return normalizeWebUiProtocol(protocol) === 'https' ? '127.0.0.1' : 'localhost';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getWebUiBaseUrl(port, options = {}) {
|
|
35
|
+
const protocol = getWebUiProtocol(options);
|
|
36
|
+
const hostname = String(options.hostname || '').trim() || getDefaultLoopbackHost(protocol);
|
|
37
|
+
return `${protocol}://${hostname}:${port}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getWebSocketBaseUrl(port, options = {}) {
|
|
41
|
+
const protocol = getWebUiProtocol(options) === 'https' ? 'wss' : 'ws';
|
|
42
|
+
const hostname = String(options.hostname || '').trim() || getDefaultLoopbackHost(protocol === 'wss' ? 'https' : 'http');
|
|
43
|
+
return `${protocol}://${hostname}:${port}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = {
|
|
47
|
+
normalizeWebUiProtocol,
|
|
48
|
+
hasHttpsFlag,
|
|
49
|
+
getWebUiProtocol,
|
|
50
|
+
isHttpsEnabled,
|
|
51
|
+
getDefaultLoopbackHost,
|
|
52
|
+
getWebUiBaseUrl,
|
|
53
|
+
getWebSocketBaseUrl
|
|
54
|
+
};
|
|
@@ -7,7 +7,9 @@ const { PATHS } = require('../config/paths');
|
|
|
7
7
|
const {
|
|
8
8
|
normalizeAddress,
|
|
9
9
|
isLoopbackAddress,
|
|
10
|
-
isLoopbackRequest
|
|
10
|
+
isLoopbackRequest,
|
|
11
|
+
getRequestProtocol,
|
|
12
|
+
getRequestHost
|
|
11
13
|
} = require('./services/network-access');
|
|
12
14
|
|
|
13
15
|
const MAX_PERSISTED_LOGS = 500;
|
|
@@ -111,8 +113,8 @@ function isAllowedWebSocketOrigin(req) {
|
|
|
111
113
|
return false;
|
|
112
114
|
}
|
|
113
115
|
|
|
114
|
-
const requestHost = parseHostHeader(req
|
|
115
|
-
const requestProtocol = req
|
|
116
|
+
const requestHost = parseHostHeader(getRequestHost(req));
|
|
117
|
+
const requestProtocol = getRequestProtocol(req);
|
|
116
118
|
const requestHostname = normalizeAddress(requestHost.hostname).toLowerCase();
|
|
117
119
|
const requestPort = requestHost.port || defaultPortForProtocol(requestProtocol);
|
|
118
120
|
|
|
@@ -504,11 +506,40 @@ function broadcastSchedulerState(source, schedulerState) {
|
|
|
504
506
|
}
|
|
505
507
|
}
|
|
506
508
|
|
|
509
|
+
function broadcastBrowserNotification(event = {}) {
|
|
510
|
+
const notification = {
|
|
511
|
+
type: 'browser-notification',
|
|
512
|
+
title: String(event.title || '').trim() || 'Coding Tool',
|
|
513
|
+
source: String(event.source || '').trim() || 'claude',
|
|
514
|
+
sourceLabel: String(event.sourceLabel || '').trim() || 'Claude Code',
|
|
515
|
+
mode: String(event.mode || '').trim() || 'notification',
|
|
516
|
+
eventType: String(event.eventType || '').trim(),
|
|
517
|
+
message: String(event.message || '').trim() || '任务已完成 | 等待交互',
|
|
518
|
+
timestamp: typeof event.timestamp === 'number' ? event.timestamp : Date.now()
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
if (wss && wsClients.size > 0) {
|
|
522
|
+
const message = JSON.stringify(notification);
|
|
523
|
+
|
|
524
|
+
wsClients.forEach(client => {
|
|
525
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
526
|
+
client.send(message);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
507
532
|
module.exports = {
|
|
508
533
|
startWebSocketServer,
|
|
509
534
|
stopWebSocketServer,
|
|
510
535
|
broadcastLog,
|
|
511
536
|
clearAllLogs,
|
|
512
537
|
broadcastProxyState,
|
|
513
|
-
broadcastSchedulerState
|
|
538
|
+
broadcastSchedulerState,
|
|
539
|
+
broadcastBrowserNotification,
|
|
540
|
+
_test: {
|
|
541
|
+
parseHostHeader,
|
|
542
|
+
defaultPortForProtocol,
|
|
543
|
+
isAllowedWebSocketOrigin
|
|
544
|
+
}
|
|
514
545
|
};
|