coding-tool-x 3.2.0
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 +599 -0
- package/LICENSE +21 -0
- package/README.md +439 -0
- package/bin/ctx.js +8 -0
- package/dist/web/assets/Analytics-DN_YsnkW.js +39 -0
- package/dist/web/assets/Analytics-DuYvId7u.css +1 -0
- package/dist/web/assets/ConfigTemplates-Bidwfdf2.css +1 -0
- package/dist/web/assets/ConfigTemplates-DpXIMy0p.js +1 -0
- package/dist/web/assets/Home-38JTUlYt.js +1 -0
- package/dist/web/assets/Home-CjupSEWE.css +1 -0
- package/dist/web/assets/PluginManager-CX2tgq2H.js +1 -0
- package/dist/web/assets/PluginManager-ROyoZ-6m.css +1 -0
- package/dist/web/assets/ProjectList-C1lDcsn6.js +1 -0
- package/dist/web/assets/ProjectList-oJIyIRkP.css +1 -0
- package/dist/web/assets/SessionList-C55tjV7i.css +1 -0
- package/dist/web/assets/SessionList-CZ7T6rVx.js +1 -0
- package/dist/web/assets/SkillManager-D7pd-d_P.css +1 -0
- package/dist/web/assets/SkillManager-DLN9f79y.js +1 -0
- package/dist/web/assets/WorkspaceManager-CrwgQgmP.css +1 -0
- package/dist/web/assets/WorkspaceManager-DxlHZkpZ.js +1 -0
- package/dist/web/assets/icons-DRrXwWZi.js +1 -0
- package/dist/web/assets/index-CetESrXw.css +1 -0
- package/dist/web/assets/index-Cfvn-2Gb.js +2 -0
- package/dist/web/assets/markdown-BfC0goYb.css +10 -0
- package/dist/web/assets/markdown-C9MYpaSi.js +1 -0
- package/dist/web/assets/naive-ui-DlpKk-8M.js +1 -0
- package/dist/web/assets/vendors-DMjSfzlv.js +7 -0
- package/dist/web/assets/vue-vendor-DET08QYg.js +45 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +20 -0
- package/dist/web/logo.png +0 -0
- package/docs/bannel.png +0 -0
- package/docs/home.png +0 -0
- package/docs/logo.png +0 -0
- package/docs/model-redirection.md +251 -0
- package/docs/multi-channel-load-balancing.md +249 -0
- package/package.json +80 -0
- package/src/commands/channels.js +551 -0
- package/src/commands/cli-type.js +101 -0
- package/src/commands/daemon.js +365 -0
- package/src/commands/doctor.js +333 -0
- package/src/commands/export-config.js +205 -0
- package/src/commands/list.js +222 -0
- package/src/commands/logs.js +261 -0
- package/src/commands/plugin.js +585 -0
- package/src/commands/port-config.js +135 -0
- package/src/commands/proxy-control.js +264 -0
- package/src/commands/proxy.js +152 -0
- package/src/commands/resume.js +137 -0
- package/src/commands/search.js +190 -0
- package/src/commands/security.js +37 -0
- package/src/commands/stats.js +398 -0
- package/src/commands/switch.js +48 -0
- package/src/commands/toggle-proxy.js +247 -0
- package/src/commands/ui.js +99 -0
- package/src/commands/update.js +97 -0
- package/src/commands/workspace.js +454 -0
- package/src/config/default.js +69 -0
- package/src/config/loader.js +149 -0
- package/src/config/model-metadata.js +167 -0
- package/src/config/model-metadata.json +125 -0
- package/src/config/model-pricing.js +35 -0
- package/src/config/paths.js +190 -0
- package/src/index.js +680 -0
- package/src/plugins/constants.js +15 -0
- package/src/plugins/event-bus.js +54 -0
- package/src/plugins/manifest-validator.js +129 -0
- package/src/plugins/plugin-api.js +128 -0
- package/src/plugins/plugin-installer.js +601 -0
- package/src/plugins/plugin-loader.js +229 -0
- package/src/plugins/plugin-manager.js +170 -0
- package/src/plugins/registry.js +152 -0
- package/src/plugins/schema/plugin-manifest.json +115 -0
- package/src/reset-config.js +94 -0
- package/src/server/api/agents.js +826 -0
- package/src/server/api/aliases.js +36 -0
- package/src/server/api/channels.js +368 -0
- package/src/server/api/claude-hooks.js +480 -0
- package/src/server/api/codex-channels.js +417 -0
- package/src/server/api/codex-projects.js +104 -0
- package/src/server/api/codex-proxy.js +195 -0
- package/src/server/api/codex-sessions.js +483 -0
- package/src/server/api/codex-statistics.js +57 -0
- package/src/server/api/commands.js +482 -0
- package/src/server/api/config-export.js +212 -0
- package/src/server/api/config-registry.js +357 -0
- package/src/server/api/config-sync.js +155 -0
- package/src/server/api/config-templates.js +248 -0
- package/src/server/api/config.js +521 -0
- package/src/server/api/convert.js +260 -0
- package/src/server/api/dashboard.js +142 -0
- package/src/server/api/env.js +144 -0
- package/src/server/api/favorites.js +77 -0
- package/src/server/api/gemini-channels.js +366 -0
- package/src/server/api/gemini-projects.js +91 -0
- package/src/server/api/gemini-proxy.js +173 -0
- package/src/server/api/gemini-sessions.js +376 -0
- package/src/server/api/gemini-statistics.js +57 -0
- package/src/server/api/health-check.js +31 -0
- package/src/server/api/mcp.js +399 -0
- package/src/server/api/opencode-channels.js +419 -0
- package/src/server/api/opencode-projects.js +99 -0
- package/src/server/api/opencode-proxy.js +207 -0
- package/src/server/api/opencode-sessions.js +327 -0
- package/src/server/api/opencode-statistics.js +57 -0
- package/src/server/api/plugins.js +463 -0
- package/src/server/api/pm2-autostart.js +269 -0
- package/src/server/api/projects.js +124 -0
- package/src/server/api/prompts.js +279 -0
- package/src/server/api/proxy.js +306 -0
- package/src/server/api/security.js +53 -0
- package/src/server/api/sessions.js +514 -0
- package/src/server/api/settings.js +142 -0
- package/src/server/api/skills.js +570 -0
- package/src/server/api/statistics.js +238 -0
- package/src/server/api/ui-config.js +64 -0
- package/src/server/api/workspaces.js +456 -0
- package/src/server/codex-proxy-server.js +681 -0
- package/src/server/dev-server.js +26 -0
- package/src/server/gemini-proxy-server.js +610 -0
- package/src/server/index.js +422 -0
- package/src/server/opencode-proxy-server.js +4771 -0
- package/src/server/proxy-server.js +669 -0
- package/src/server/services/agents-service.js +1137 -0
- package/src/server/services/alias.js +71 -0
- package/src/server/services/channel-health.js +234 -0
- package/src/server/services/channel-scheduler.js +240 -0
- package/src/server/services/channels.js +447 -0
- package/src/server/services/codex-channels.js +705 -0
- package/src/server/services/codex-config.js +90 -0
- package/src/server/services/codex-parser.js +322 -0
- package/src/server/services/codex-sessions.js +936 -0
- package/src/server/services/codex-settings-manager.js +619 -0
- package/src/server/services/codex-speed-test-template.json +24 -0
- package/src/server/services/codex-statistics-service.js +161 -0
- package/src/server/services/commands-service.js +574 -0
- package/src/server/services/config-export-service.js +1165 -0
- package/src/server/services/config-registry-service.js +828 -0
- package/src/server/services/config-sync-manager.js +941 -0
- package/src/server/services/config-sync-service.js +504 -0
- package/src/server/services/config-templates-service.js +913 -0
- package/src/server/services/enhanced-cache.js +196 -0
- package/src/server/services/env-checker.js +409 -0
- package/src/server/services/env-manager.js +436 -0
- package/src/server/services/favorites.js +165 -0
- package/src/server/services/format-converter.js +620 -0
- package/src/server/services/gemini-channels.js +459 -0
- package/src/server/services/gemini-config.js +73 -0
- package/src/server/services/gemini-sessions.js +689 -0
- package/src/server/services/gemini-settings-manager.js +263 -0
- package/src/server/services/gemini-statistics-service.js +157 -0
- package/src/server/services/health-check.js +85 -0
- package/src/server/services/mcp-client.js +790 -0
- package/src/server/services/mcp-service.js +1732 -0
- package/src/server/services/model-detector.js +1245 -0
- package/src/server/services/network-access.js +80 -0
- package/src/server/services/opencode-channels.js +366 -0
- package/src/server/services/opencode-gateway-adapters.js +1168 -0
- package/src/server/services/opencode-gateway-converter.js +639 -0
- package/src/server/services/opencode-sessions.js +931 -0
- package/src/server/services/opencode-settings-manager.js +478 -0
- package/src/server/services/opencode-statistics-service.js +161 -0
- package/src/server/services/plugins-service.js +1268 -0
- package/src/server/services/prompts-service.js +534 -0
- package/src/server/services/proxy-runtime.js +79 -0
- package/src/server/services/repo-scanner-base.js +708 -0
- package/src/server/services/request-logger.js +130 -0
- package/src/server/services/response-decoder.js +21 -0
- package/src/server/services/security-config.js +131 -0
- package/src/server/services/session-cache.js +127 -0
- package/src/server/services/session-converter.js +577 -0
- package/src/server/services/sessions.js +900 -0
- package/src/server/services/settings-manager.js +163 -0
- package/src/server/services/skill-service.js +1482 -0
- package/src/server/services/speed-test.js +1146 -0
- package/src/server/services/statistics-service.js +1043 -0
- package/src/server/services/ui-config.js +132 -0
- package/src/server/services/workspace-service.js +830 -0
- package/src/server/utils/pricing.js +73 -0
- package/src/server/websocket-server.js +513 -0
- package/src/ui/menu.js +139 -0
- package/src/ui/prompts.js +100 -0
- package/src/utils/format.js +43 -0
- package/src/utils/port-helper.js +108 -0
- package/src/utils/session.js +240 -0
|
@@ -0,0 +1,900 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const { getAllSessions, parseSessionInfoFast } = require('../../utils/session');
|
|
6
|
+
const { loadAliases } = require('./alias');
|
|
7
|
+
const {
|
|
8
|
+
getCachedProjects,
|
|
9
|
+
setCachedProjects,
|
|
10
|
+
invalidateProjectsCache,
|
|
11
|
+
checkHasMessagesCache,
|
|
12
|
+
rememberHasMessages
|
|
13
|
+
} = require('./session-cache');
|
|
14
|
+
const { globalCache, CacheKeys } = require('./enhanced-cache');
|
|
15
|
+
const { PATHS } = require('../../config/paths');
|
|
16
|
+
|
|
17
|
+
// Base directory for cc-tool data
|
|
18
|
+
function getCcToolDir() {
|
|
19
|
+
return PATHS.base;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Get path for storing project order
|
|
23
|
+
function getOrderFilePath() {
|
|
24
|
+
return PATHS.projectOrder;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Get path for storing fork relations
|
|
28
|
+
function getForkRelationsFilePath() {
|
|
29
|
+
return path.join(PATHS.base, 'fork-relations.json');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Get path for storing session order
|
|
33
|
+
function getSessionOrderFilePath() {
|
|
34
|
+
return path.join(getCcToolDir(), 'session-order.json');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Get saved project order
|
|
38
|
+
function getProjectOrder(config) {
|
|
39
|
+
const orderFile = getOrderFilePath();
|
|
40
|
+
try {
|
|
41
|
+
if (fs.existsSync(orderFile)) {
|
|
42
|
+
const data = fs.readFileSync(orderFile, 'utf8');
|
|
43
|
+
return JSON.parse(data);
|
|
44
|
+
}
|
|
45
|
+
} catch (err) {
|
|
46
|
+
// Ignore errors
|
|
47
|
+
}
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Save project order
|
|
52
|
+
function saveProjectOrder(config, order) {
|
|
53
|
+
const orderFile = getOrderFilePath();
|
|
54
|
+
const dir = path.dirname(orderFile);
|
|
55
|
+
if (!fs.existsSync(dir)) {
|
|
56
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
fs.writeFileSync(orderFile, JSON.stringify(order, null, 2), 'utf8');
|
|
59
|
+
invalidateProjectsCache(config);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Get fork relations
|
|
63
|
+
function getForkRelations() {
|
|
64
|
+
const relationsFile = getForkRelationsFilePath();
|
|
65
|
+
try {
|
|
66
|
+
if (fs.existsSync(relationsFile)) {
|
|
67
|
+
const data = fs.readFileSync(relationsFile, 'utf8');
|
|
68
|
+
return JSON.parse(data);
|
|
69
|
+
}
|
|
70
|
+
} catch (err) {
|
|
71
|
+
// Ignore errors
|
|
72
|
+
}
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Save fork relations
|
|
77
|
+
function saveForkRelations(relations) {
|
|
78
|
+
const relationsFile = getForkRelationsFilePath();
|
|
79
|
+
const dir = path.dirname(relationsFile);
|
|
80
|
+
if (!fs.existsSync(dir)) {
|
|
81
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
fs.writeFileSync(relationsFile, JSON.stringify(relations, null, 2), 'utf8');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Get all projects with stats (async version)
|
|
87
|
+
async function getProjects(config) {
|
|
88
|
+
const projectsDir = config.projectsDir;
|
|
89
|
+
|
|
90
|
+
if (!fs.existsSync(projectsDir)) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const entries = await fs.promises.readdir(projectsDir, { withFileTypes: true });
|
|
95
|
+
return entries
|
|
96
|
+
.filter(entry => entry.isDirectory())
|
|
97
|
+
.map(entry => entry.name);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Parse real project path from encoded name
|
|
101
|
+
// macOS/Linux: "-Users-lilithgames-work-project" -> "/Users/lilithgames/work/project"
|
|
102
|
+
// Windows: "C--Users-admin-Desktop-project" -> "C:\Users\admin\Desktop\project"
|
|
103
|
+
function parseRealProjectPath(encodedName) {
|
|
104
|
+
const isWindows = process.platform === 'win32';
|
|
105
|
+
const fallbackFromSessions = tryResolvePathFromSessions(encodedName);
|
|
106
|
+
|
|
107
|
+
// Detect Windows drive letter (e.g., "C--Users-admin")
|
|
108
|
+
const windowsDriveMatch = encodedName.match(/^([A-Z])--(.+)$/);
|
|
109
|
+
|
|
110
|
+
if (isWindows && windowsDriveMatch) {
|
|
111
|
+
// Windows path with drive letter
|
|
112
|
+
const driveLetter = windowsDriveMatch[1];
|
|
113
|
+
const restPath = windowsDriveMatch[2];
|
|
114
|
+
|
|
115
|
+
// Split by '-' to get segments
|
|
116
|
+
const segments = restPath.split('-').filter(s => s);
|
|
117
|
+
|
|
118
|
+
// Build path from left to right, checking existence
|
|
119
|
+
let realSegments = [];
|
|
120
|
+
let accumulated = '';
|
|
121
|
+
let currentPath = '';
|
|
122
|
+
|
|
123
|
+
for (let i = 0; i < segments.length; i++) {
|
|
124
|
+
if (accumulated) {
|
|
125
|
+
accumulated += '-' + segments[i];
|
|
126
|
+
} else {
|
|
127
|
+
accumulated = segments[i];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const testPath = driveLetter + ':\\' + realSegments.concat(accumulated).join('\\');
|
|
131
|
+
|
|
132
|
+
// Check if this path exists
|
|
133
|
+
let found = fs.existsSync(testPath);
|
|
134
|
+
let finalAccumulated = accumulated;
|
|
135
|
+
|
|
136
|
+
// If not found with dash, try with underscore
|
|
137
|
+
if (!found && accumulated.includes('-')) {
|
|
138
|
+
const withUnderscore = accumulated.replace(/-/g, '_');
|
|
139
|
+
const testPathUnderscore = driveLetter + ':\\' + realSegments.concat(withUnderscore).join('\\');
|
|
140
|
+
if (fs.existsSync(testPathUnderscore)) {
|
|
141
|
+
finalAccumulated = withUnderscore;
|
|
142
|
+
found = true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (found) {
|
|
147
|
+
realSegments.push(finalAccumulated);
|
|
148
|
+
accumulated = '';
|
|
149
|
+
currentPath = driveLetter + ':\\' + realSegments.join('\\');
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// If there's remaining accumulated segment, try underscore variant
|
|
154
|
+
if (accumulated) {
|
|
155
|
+
let finalAccumulated = accumulated;
|
|
156
|
+
if (accumulated.includes('-')) {
|
|
157
|
+
const withUnderscore = accumulated.replace(/-/g, '_');
|
|
158
|
+
const testPath = driveLetter + ':\\' + realSegments.concat(withUnderscore).join('\\');
|
|
159
|
+
if (fs.existsSync(testPath)) {
|
|
160
|
+
finalAccumulated = withUnderscore;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
realSegments.push(finalAccumulated);
|
|
164
|
+
currentPath = driveLetter + ':\\' + realSegments.join('\\');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
fullPath: validateProjectPath(currentPath) || fallbackFromSessions?.fullPath || (driveLetter + ':\\' + restPath.replace(/-/g, '\\')),
|
|
169
|
+
projectName: fallbackFromSessions?.projectName || realSegments[realSegments.length - 1] || encodedName
|
|
170
|
+
};
|
|
171
|
+
} else {
|
|
172
|
+
// Unix-like path (macOS/Linux) or fallback
|
|
173
|
+
const pathStr = encodedName.replace(/^-/, '/').replace(/-/g, '/');
|
|
174
|
+
const segments = pathStr.split('/').filter(s => s);
|
|
175
|
+
|
|
176
|
+
// Build path from left to right, checking existence
|
|
177
|
+
let currentPath = '';
|
|
178
|
+
const realSegments = [];
|
|
179
|
+
let accumulated = '';
|
|
180
|
+
|
|
181
|
+
for (let i = 0; i < segments.length; i++) {
|
|
182
|
+
if (accumulated) {
|
|
183
|
+
accumulated += '-' + segments[i];
|
|
184
|
+
} else {
|
|
185
|
+
accumulated = segments[i];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const testPath = '/' + realSegments.concat(accumulated).join('/');
|
|
189
|
+
|
|
190
|
+
// Check if this path exists
|
|
191
|
+
let found = fs.existsSync(testPath);
|
|
192
|
+
let finalAccumulated = accumulated;
|
|
193
|
+
|
|
194
|
+
// If not found with dash, try with underscore
|
|
195
|
+
if (!found && accumulated.includes('-')) {
|
|
196
|
+
const withUnderscore = accumulated.replace(/-/g, '_');
|
|
197
|
+
const testPathUnderscore = '/' + realSegments.concat(withUnderscore).join('/');
|
|
198
|
+
if (fs.existsSync(testPathUnderscore)) {
|
|
199
|
+
finalAccumulated = withUnderscore;
|
|
200
|
+
found = true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (found) {
|
|
205
|
+
realSegments.push(finalAccumulated);
|
|
206
|
+
accumulated = '';
|
|
207
|
+
currentPath = '/' + realSegments.join('/');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// If there's remaining accumulated segment, try underscore variant
|
|
212
|
+
if (accumulated) {
|
|
213
|
+
let finalAccumulated = accumulated;
|
|
214
|
+
if (accumulated.includes('-')) {
|
|
215
|
+
const withUnderscore = accumulated.replace(/-/g, '_');
|
|
216
|
+
const testPath = '/' + realSegments.concat(withUnderscore).join('/');
|
|
217
|
+
if (fs.existsSync(testPath)) {
|
|
218
|
+
finalAccumulated = withUnderscore;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
realSegments.push(finalAccumulated);
|
|
222
|
+
currentPath = '/' + realSegments.join('/');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
fullPath: validateProjectPath(currentPath) || fallbackFromSessions?.fullPath || pathStr,
|
|
227
|
+
projectName: fallbackFromSessions?.projectName || realSegments[realSegments.length - 1] || encodedName
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function validateProjectPath(candidatePath) {
|
|
233
|
+
if (candidatePath && fs.existsSync(candidatePath)) {
|
|
234
|
+
return candidatePath;
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function tryResolvePathFromSessions(encodedName) {
|
|
240
|
+
try {
|
|
241
|
+
const projectDir = path.join(os.homedir(), '.claude', 'projects', encodedName);
|
|
242
|
+
if (!fs.existsSync(projectDir)) {
|
|
243
|
+
return null;
|
|
244
|
+
}
|
|
245
|
+
const files = fs.readdirSync(projectDir).filter(f => f.endsWith('.jsonl'));
|
|
246
|
+
for (const file of files) {
|
|
247
|
+
const sessionFile = path.join(projectDir, file);
|
|
248
|
+
const cwd = extractCwdFromSessionHeader(sessionFile);
|
|
249
|
+
if (cwd && fs.existsSync(cwd)) {
|
|
250
|
+
return {
|
|
251
|
+
fullPath: cwd,
|
|
252
|
+
projectName: path.basename(cwd)
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
} catch (err) {
|
|
257
|
+
// ignore fallback errors
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function extractCwdFromSessionHeader(sessionFile) {
|
|
263
|
+
try {
|
|
264
|
+
const fd = fs.openSync(sessionFile, 'r');
|
|
265
|
+
const buffer = Buffer.alloc(4096);
|
|
266
|
+
const bytesRead = fs.readSync(fd, buffer, 0, 4096, 0);
|
|
267
|
+
fs.closeSync(fd);
|
|
268
|
+
const content = buffer.slice(0, bytesRead).toString('utf8');
|
|
269
|
+
const lines = content.split('\n');
|
|
270
|
+
for (const line of lines) {
|
|
271
|
+
if (!line.trim()) continue;
|
|
272
|
+
try {
|
|
273
|
+
const json = JSON.parse(line);
|
|
274
|
+
if (json.cwd && typeof json.cwd === 'string') {
|
|
275
|
+
return json.cwd;
|
|
276
|
+
}
|
|
277
|
+
} catch (e) {
|
|
278
|
+
// ignore
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} catch (err) {
|
|
282
|
+
// ignore
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Get projects with detailed stats (with caching) - async version
|
|
288
|
+
async function getProjectsWithStats(config, options = {}) {
|
|
289
|
+
if (!options.force) {
|
|
290
|
+
// Check enhanced cache first
|
|
291
|
+
const cacheKey = `${CacheKeys.PROJECTS}${config.projectsDir}`;
|
|
292
|
+
const enhancedCached = globalCache.get(cacheKey);
|
|
293
|
+
if (enhancedCached) {
|
|
294
|
+
return enhancedCached;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Check old cache
|
|
298
|
+
const cached = getCachedProjects(config);
|
|
299
|
+
if (cached) {
|
|
300
|
+
globalCache.set(cacheKey, cached, 300000); // 5分钟
|
|
301
|
+
return cached;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const data = await buildProjectsWithStats(config);
|
|
307
|
+
if (!Array.isArray(data)) {
|
|
308
|
+
console.warn(`[getProjectsWithStats] Unexpected non-array result for ${config.projectsDir}, returning empty array.`);
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
setCachedProjects(config, data);
|
|
312
|
+
globalCache.set(`${CacheKeys.PROJECTS}${config.projectsDir}`, data, 300000);
|
|
313
|
+
return data;
|
|
314
|
+
} catch (err) {
|
|
315
|
+
console.error(`[getProjectsWithStats] Failed to build projects for ${config.projectsDir}:`, err);
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function buildProjectsWithStats(config) {
|
|
321
|
+
const projectsDir = config.projectsDir;
|
|
322
|
+
|
|
323
|
+
if (!fs.existsSync(projectsDir)) {
|
|
324
|
+
return [];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const entries = await fs.promises.readdir(projectsDir, { withFileTypes: true });
|
|
328
|
+
|
|
329
|
+
// Process all projects concurrently
|
|
330
|
+
const projectPromises = entries
|
|
331
|
+
.filter(entry => entry.isDirectory())
|
|
332
|
+
.map(async (entry) => {
|
|
333
|
+
const projectName = entry.name;
|
|
334
|
+
const projectPath = path.join(projectsDir, projectName);
|
|
335
|
+
|
|
336
|
+
// Parse real project path
|
|
337
|
+
const { fullPath, projectName: displayName } = parseRealProjectPath(projectName);
|
|
338
|
+
|
|
339
|
+
// Get session files (only count sessions with actual messages)
|
|
340
|
+
let sessionCount = 0;
|
|
341
|
+
let lastUsed = null;
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
const files = await fs.promises.readdir(projectPath);
|
|
345
|
+
const jsonlFiles = files.filter(f => f.endsWith('.jsonl') && !f.startsWith('agent-'));
|
|
346
|
+
|
|
347
|
+
// Filter: only count sessions that have actual messages (in parallel)
|
|
348
|
+
const sessionChecks = await Promise.all(
|
|
349
|
+
jsonlFiles.map(async (f) => {
|
|
350
|
+
const filePath = path.join(projectPath, f);
|
|
351
|
+
const hasMessages = await hasActualMessages(filePath);
|
|
352
|
+
return hasMessages ? f : null;
|
|
353
|
+
})
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
const sessionFilesWithMessages = sessionChecks.filter(f => f !== null);
|
|
357
|
+
sessionCount = sessionFilesWithMessages.length;
|
|
358
|
+
|
|
359
|
+
// Find most recent session (only from sessions with messages)
|
|
360
|
+
if (sessionFilesWithMessages.length > 0) {
|
|
361
|
+
const statPromises = sessionFilesWithMessages.map(async (f) => {
|
|
362
|
+
const filePath = path.join(projectPath, f);
|
|
363
|
+
const stat = await fs.promises.stat(filePath);
|
|
364
|
+
return stat.mtime.getTime();
|
|
365
|
+
});
|
|
366
|
+
const stats = await Promise.all(statPromises);
|
|
367
|
+
lastUsed = Math.max(...stats);
|
|
368
|
+
}
|
|
369
|
+
} catch (err) {
|
|
370
|
+
// Ignore errors
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
name: projectName, // Keep encoded name for API operations
|
|
375
|
+
displayName, // Project name for display
|
|
376
|
+
fullPath, // Real full path for display
|
|
377
|
+
sessionCount,
|
|
378
|
+
lastUsed
|
|
379
|
+
};
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const projects = await Promise.all(projectPromises);
|
|
383
|
+
return projects.sort((a, b) => (b.lastUsed || 0) - (a.lastUsed || 0)); // Sort by last used
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// 获取 Claude 项目/会话数量(轻量统计)
|
|
387
|
+
function getProjectAndSessionCounts(config) {
|
|
388
|
+
const projectsDir = config.projectsDir;
|
|
389
|
+
if (!fs.existsSync(projectsDir)) {
|
|
390
|
+
return { projectCount: 0, sessionCount: 0 };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
let projectCount = 0;
|
|
394
|
+
let sessionCount = 0;
|
|
395
|
+
|
|
396
|
+
const entries = fs.readdirSync(projectsDir, { withFileTypes: true });
|
|
397
|
+
entries.forEach((entry) => {
|
|
398
|
+
if (!entry.isDirectory()) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
projectCount += 1;
|
|
402
|
+
const projectPath = path.join(projectsDir, entry.name);
|
|
403
|
+
try {
|
|
404
|
+
const files = fs.readdirSync(projectPath);
|
|
405
|
+
sessionCount += files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-')).length;
|
|
406
|
+
} catch (err) {
|
|
407
|
+
// 忽略单个项目的读取错误
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
return { projectCount, sessionCount };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Check if a session file has actual messages (async with enhanced caching)
|
|
415
|
+
async function hasActualMessages(filePath) {
|
|
416
|
+
try {
|
|
417
|
+
const stats = await fs.promises.stat(filePath);
|
|
418
|
+
|
|
419
|
+
// Check enhanced cache first
|
|
420
|
+
const cacheKey = `${CacheKeys.HAS_MESSAGES}${filePath}:${stats.mtime.getTime()}`;
|
|
421
|
+
const cached = globalCache.get(cacheKey);
|
|
422
|
+
if (typeof cached === 'boolean') {
|
|
423
|
+
return cached;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Check old cache mechanism
|
|
427
|
+
const oldCached = checkHasMessagesCache(filePath, stats);
|
|
428
|
+
if (typeof oldCached === 'boolean') {
|
|
429
|
+
globalCache.set(cacheKey, oldCached, 600000); // 10分钟
|
|
430
|
+
return oldCached;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const result = await scanSessionFileForMessagesAsync(filePath);
|
|
434
|
+
globalCache.set(cacheKey, result, 600000);
|
|
435
|
+
rememberHasMessages(filePath, stats, result);
|
|
436
|
+
return result;
|
|
437
|
+
} catch (err) {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function scanSessionFileForMessages(filePath) {
|
|
443
|
+
let fd = null;
|
|
444
|
+
try {
|
|
445
|
+
fd = fs.openSync(filePath, 'r');
|
|
446
|
+
const bufferSize = 64 * 1024;
|
|
447
|
+
const buffer = Buffer.alloc(bufferSize);
|
|
448
|
+
const pattern = /"type"\s*:\s*"(user|assistant|summary)"/;
|
|
449
|
+
let leftover = '';
|
|
450
|
+
let bytesRead;
|
|
451
|
+
|
|
452
|
+
while ((bytesRead = fs.readSync(fd, buffer, 0, bufferSize, null)) > 0) {
|
|
453
|
+
const chunk = buffer.toString('utf8', 0, bytesRead);
|
|
454
|
+
const combined = leftover + chunk;
|
|
455
|
+
if (pattern.test(combined)) {
|
|
456
|
+
fs.closeSync(fd);
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
leftover = combined.slice(-64);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
fs.closeSync(fd);
|
|
463
|
+
return false;
|
|
464
|
+
} catch (err) {
|
|
465
|
+
if (fd) {
|
|
466
|
+
try {
|
|
467
|
+
fs.closeSync(fd);
|
|
468
|
+
} catch (e) {
|
|
469
|
+
// ignore
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Async version using streams for better performance
|
|
477
|
+
function scanSessionFileForMessagesAsync(filePath) {
|
|
478
|
+
return new Promise((resolve) => {
|
|
479
|
+
const stream = fs.createReadStream(filePath, { encoding: 'utf8', highWaterMark: 64 * 1024 });
|
|
480
|
+
const pattern = /"type"\s*:\s*"(user|assistant|summary)"/;
|
|
481
|
+
let found = false;
|
|
482
|
+
let leftover = '';
|
|
483
|
+
|
|
484
|
+
stream.on('data', (chunk) => {
|
|
485
|
+
if (found) return;
|
|
486
|
+
const combined = leftover + chunk;
|
|
487
|
+
if (pattern.test(combined)) {
|
|
488
|
+
found = true;
|
|
489
|
+
stream.destroy();
|
|
490
|
+
resolve(true);
|
|
491
|
+
}
|
|
492
|
+
leftover = combined.slice(-64);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
stream.on('end', () => {
|
|
496
|
+
if (!found) resolve(false);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
stream.on('error', () => {
|
|
500
|
+
resolve(false);
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Get sessions for a project - async version
|
|
506
|
+
async function getSessionsForProject(config, projectName) {
|
|
507
|
+
// Check cache first
|
|
508
|
+
const cacheKey = `${CacheKeys.SESSIONS}${projectName}`;
|
|
509
|
+
const cached = globalCache.get(cacheKey);
|
|
510
|
+
if (cached) {
|
|
511
|
+
return cached;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const projectConfig = { ...config, currentProject: projectName };
|
|
515
|
+
const sessions = getAllSessions(projectConfig);
|
|
516
|
+
const forkRelations = getForkRelations();
|
|
517
|
+
const savedOrder = getSessionOrder(projectName);
|
|
518
|
+
|
|
519
|
+
// Parse session info and calculate total size, filter out sessions with no messages (in parallel)
|
|
520
|
+
let totalSize = 0;
|
|
521
|
+
|
|
522
|
+
const sessionChecks = await Promise.all(
|
|
523
|
+
sessions.map(async (session) => {
|
|
524
|
+
const hasMessages = await hasActualMessages(session.filePath);
|
|
525
|
+
return hasMessages ? session : null;
|
|
526
|
+
})
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
const validSessions = sessionChecks.filter(s => s !== null);
|
|
530
|
+
|
|
531
|
+
const sessionsWithInfo = validSessions.map(session => {
|
|
532
|
+
const info = parseSessionInfoFast(session.filePath);
|
|
533
|
+
totalSize += session.size || 0;
|
|
534
|
+
return {
|
|
535
|
+
sessionId: session.sessionId,
|
|
536
|
+
mtime: session.mtime,
|
|
537
|
+
size: session.size,
|
|
538
|
+
filePath: session.filePath,
|
|
539
|
+
gitBranch: info.gitBranch || null,
|
|
540
|
+
firstMessage: info.firstMessage || null,
|
|
541
|
+
forkedFrom: forkRelations[session.sessionId] || null
|
|
542
|
+
};
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// Apply saved order if exists
|
|
546
|
+
let orderedSessions = sessionsWithInfo;
|
|
547
|
+
if (savedOrder.length > 0) {
|
|
548
|
+
const ordered = [];
|
|
549
|
+
const sessionMap = new Map(sessionsWithInfo.map(s => [s.sessionId, s]));
|
|
550
|
+
|
|
551
|
+
// Add sessions in saved order
|
|
552
|
+
for (const sessionId of savedOrder) {
|
|
553
|
+
if (sessionMap.has(sessionId)) {
|
|
554
|
+
ordered.push(sessionMap.get(sessionId));
|
|
555
|
+
sessionMap.delete(sessionId);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Add remaining sessions (new ones not in saved order)
|
|
560
|
+
ordered.push(...sessionMap.values());
|
|
561
|
+
orderedSessions = ordered;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const result = {
|
|
565
|
+
sessions: orderedSessions,
|
|
566
|
+
totalSize
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
// Cache for 2 minutes
|
|
570
|
+
globalCache.set(cacheKey, result, 120000);
|
|
571
|
+
return result;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Delete a session
|
|
575
|
+
function deleteSession(config, projectName, sessionId) {
|
|
576
|
+
const projectDir = path.join(config.projectsDir, projectName);
|
|
577
|
+
const sessionFile = path.join(projectDir, sessionId + '.jsonl');
|
|
578
|
+
|
|
579
|
+
if (!fs.existsSync(sessionFile)) {
|
|
580
|
+
throw new Error('Session not found');
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
fs.unlinkSync(sessionFile);
|
|
584
|
+
invalidateProjectsCache(config);
|
|
585
|
+
return { success: true };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Fork a session
|
|
589
|
+
function forkSession(config, projectName, sessionId) {
|
|
590
|
+
const projectDir = path.join(config.projectsDir, projectName);
|
|
591
|
+
const sessionFile = path.join(projectDir, sessionId + '.jsonl');
|
|
592
|
+
|
|
593
|
+
if (!fs.existsSync(sessionFile)) {
|
|
594
|
+
throw new Error('Session not found');
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Read the original session
|
|
598
|
+
const content = fs.readFileSync(sessionFile, 'utf8');
|
|
599
|
+
|
|
600
|
+
// Generate new session ID (UUID v4)
|
|
601
|
+
const newSessionId = crypto.randomUUID();
|
|
602
|
+
const newSessionFile = path.join(projectDir, newSessionId + '.jsonl');
|
|
603
|
+
|
|
604
|
+
// Write to new file
|
|
605
|
+
fs.writeFileSync(newSessionFile, content, 'utf8');
|
|
606
|
+
|
|
607
|
+
// Save fork relation
|
|
608
|
+
const forkRelations = getForkRelations();
|
|
609
|
+
forkRelations[newSessionId] = sessionId;
|
|
610
|
+
saveForkRelations(forkRelations);
|
|
611
|
+
invalidateProjectsCache(config);
|
|
612
|
+
|
|
613
|
+
return { newSessionId, forkedFrom: sessionId };
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// Get session order for a project
|
|
617
|
+
function getSessionOrder(projectName) {
|
|
618
|
+
const orderFile = getSessionOrderFilePath();
|
|
619
|
+
try {
|
|
620
|
+
if (fs.existsSync(orderFile)) {
|
|
621
|
+
const data = fs.readFileSync(orderFile, 'utf8');
|
|
622
|
+
const allOrders = JSON.parse(data);
|
|
623
|
+
return allOrders[projectName] || [];
|
|
624
|
+
}
|
|
625
|
+
} catch (err) {
|
|
626
|
+
// Ignore errors
|
|
627
|
+
}
|
|
628
|
+
return [];
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Save session order for a project
|
|
632
|
+
function saveSessionOrder(projectName, order) {
|
|
633
|
+
const orderFile = getSessionOrderFilePath();
|
|
634
|
+
const dir = path.dirname(orderFile);
|
|
635
|
+
if (!fs.existsSync(dir)) {
|
|
636
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Read existing orders
|
|
640
|
+
let allOrders = {};
|
|
641
|
+
try {
|
|
642
|
+
if (fs.existsSync(orderFile)) {
|
|
643
|
+
const data = fs.readFileSync(orderFile, 'utf8');
|
|
644
|
+
allOrders = JSON.parse(data);
|
|
645
|
+
}
|
|
646
|
+
} catch (err) {
|
|
647
|
+
// Ignore errors
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Update order for this project
|
|
651
|
+
allOrders[projectName] = order;
|
|
652
|
+
fs.writeFileSync(orderFile, JSON.stringify(allOrders, null, 2), 'utf8');
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Delete a project (remove the entire project directory)
|
|
656
|
+
function deleteProject(config, projectName) {
|
|
657
|
+
const projectDir = path.join(config.projectsDir, projectName);
|
|
658
|
+
|
|
659
|
+
if (!fs.existsSync(projectDir)) {
|
|
660
|
+
throw new Error('Project not found');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Recursively delete the directory
|
|
664
|
+
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
665
|
+
|
|
666
|
+
// Remove from order file if exists
|
|
667
|
+
const order = getProjectOrder(config);
|
|
668
|
+
const newOrder = order.filter(name => name !== projectName);
|
|
669
|
+
if (newOrder.length !== order.length) {
|
|
670
|
+
saveProjectOrder(config, newOrder);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
invalidateProjectsCache(config);
|
|
674
|
+
return { success: true };
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Search sessions for keyword
|
|
678
|
+
function searchSessions(config, projectName, keyword, contextLength = 15) {
|
|
679
|
+
const projectDir = path.join(config.projectsDir, projectName);
|
|
680
|
+
|
|
681
|
+
if (!fs.existsSync(projectDir)) {
|
|
682
|
+
return [];
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const results = [];
|
|
686
|
+
const files = fs.readdirSync(projectDir);
|
|
687
|
+
const jsonlFiles = files.filter(f => f.endsWith('.jsonl') && !f.startsWith('agent-'));
|
|
688
|
+
const aliases = loadAliases();
|
|
689
|
+
|
|
690
|
+
for (const file of jsonlFiles) {
|
|
691
|
+
const sessionId = file.replace('.jsonl', '');
|
|
692
|
+
const filePath = path.join(projectDir, file);
|
|
693
|
+
|
|
694
|
+
// Skip sessions with no actual messages
|
|
695
|
+
if (!hasActualMessages(filePath)) {
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
try {
|
|
700
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
701
|
+
const lines = content.split('\n');
|
|
702
|
+
const matches = [];
|
|
703
|
+
|
|
704
|
+
for (const line of lines) {
|
|
705
|
+
if (!line.trim()) continue;
|
|
706
|
+
|
|
707
|
+
try {
|
|
708
|
+
const json = JSON.parse(line);
|
|
709
|
+
|
|
710
|
+
// Search in message content
|
|
711
|
+
if (json.message && json.message.content) {
|
|
712
|
+
const text = json.message.content;
|
|
713
|
+
const lowerText = text.toLowerCase();
|
|
714
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
715
|
+
let index = 0;
|
|
716
|
+
|
|
717
|
+
while ((index = lowerText.indexOf(lowerKeyword, index)) !== -1) {
|
|
718
|
+
// Extract context
|
|
719
|
+
const start = Math.max(0, index - contextLength);
|
|
720
|
+
const end = Math.min(text.length, index + keyword.length + contextLength);
|
|
721
|
+
const context = text.substring(start, end);
|
|
722
|
+
|
|
723
|
+
matches.push({
|
|
724
|
+
role: json.message.role || 'unknown',
|
|
725
|
+
context: (start > 0 ? '...' : '') + context + (end < text.length ? '...' : ''),
|
|
726
|
+
position: index
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
index += keyword.length;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
} catch (e) {
|
|
733
|
+
// Skip invalid JSON lines
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (matches.length > 0) {
|
|
738
|
+
results.push({
|
|
739
|
+
sessionId,
|
|
740
|
+
alias: aliases[sessionId] || null,
|
|
741
|
+
matchCount: matches.length,
|
|
742
|
+
matches: matches.slice(0, 5) // Limit to 5 matches per session
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
} catch (e) {
|
|
746
|
+
// Skip files that can't be read
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Sort by match count
|
|
751
|
+
results.sort((a, b) => b.matchCount - a.matchCount);
|
|
752
|
+
|
|
753
|
+
return results;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Get recent sessions across all projects
|
|
757
|
+
async function getRecentSessions(config, limit = 5) {
|
|
758
|
+
const projects = await getProjects(config);
|
|
759
|
+
const allSessions = [];
|
|
760
|
+
const forkRelations = getForkRelations();
|
|
761
|
+
const aliases = loadAliases();
|
|
762
|
+
|
|
763
|
+
// Collect all sessions from all projects
|
|
764
|
+
projects.forEach(projectName => {
|
|
765
|
+
const projectConfig = { ...config, currentProject: projectName };
|
|
766
|
+
const sessions = getAllSessions(projectConfig);
|
|
767
|
+
const { projectName: displayName, fullPath } = parseRealProjectPath(projectName);
|
|
768
|
+
|
|
769
|
+
sessions.forEach(session => {
|
|
770
|
+
// Skip sessions with no actual messages
|
|
771
|
+
if (!hasActualMessages(session.filePath)) {
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const info = parseSessionInfoFast(session.filePath);
|
|
776
|
+
allSessions.push({
|
|
777
|
+
sessionId: session.sessionId,
|
|
778
|
+
projectName: projectName,
|
|
779
|
+
projectDisplayName: displayName,
|
|
780
|
+
projectFullPath: fullPath,
|
|
781
|
+
mtime: session.mtime,
|
|
782
|
+
size: session.size,
|
|
783
|
+
filePath: session.filePath,
|
|
784
|
+
gitBranch: info.gitBranch || null,
|
|
785
|
+
firstMessage: info.firstMessage || null,
|
|
786
|
+
forkedFrom: forkRelations[session.sessionId] || null,
|
|
787
|
+
alias: aliases[session.sessionId] || null
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
});
|
|
791
|
+
|
|
792
|
+
// Sort by mtime descending (most recent first)
|
|
793
|
+
allSessions.sort((a, b) => b.mtime - a.mtime);
|
|
794
|
+
|
|
795
|
+
// Return top N sessions
|
|
796
|
+
return allSessions.slice(0, limit);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Search sessions across all projects
|
|
800
|
+
async function searchSessionsAcrossProjects(config, keyword, contextLength = 35) {
|
|
801
|
+
const allResults = [];
|
|
802
|
+
|
|
803
|
+
try {
|
|
804
|
+
// Search in Claude projects
|
|
805
|
+
const claudeProjects = await getProjects(config);
|
|
806
|
+
claudeProjects.forEach(projectName => {
|
|
807
|
+
const projectResults = searchSessions(config, projectName, keyword, contextLength);
|
|
808
|
+
const { projectName: displayName, fullPath } = parseRealProjectPath(projectName);
|
|
809
|
+
|
|
810
|
+
// Add project info to each result
|
|
811
|
+
projectResults.forEach(result => {
|
|
812
|
+
allResults.push({
|
|
813
|
+
...result,
|
|
814
|
+
projectName: projectName,
|
|
815
|
+
projectDisplayName: displayName,
|
|
816
|
+
projectFullPath: fullPath,
|
|
817
|
+
channel: 'claude'
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
});
|
|
821
|
+
} catch (error) {
|
|
822
|
+
console.error('Error searching Claude projects:', error);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
try {
|
|
826
|
+
// Search in Codex projects
|
|
827
|
+
const codexProjectsDir = path.join(os.homedir(), '.codex', 'projects');
|
|
828
|
+
if (fs.existsSync(codexProjectsDir)) {
|
|
829
|
+
const codexConfig = { ...config, projectsDir: codexProjectsDir };
|
|
830
|
+
const codexProjects = await getProjects(codexConfig);
|
|
831
|
+
codexProjects.forEach(projectName => {
|
|
832
|
+
const projectResults = searchSessions(codexConfig, projectName, keyword, contextLength);
|
|
833
|
+
const { projectName: displayName, fullPath } = parseRealProjectPath(projectName);
|
|
834
|
+
|
|
835
|
+
projectResults.forEach(result => {
|
|
836
|
+
allResults.push({
|
|
837
|
+
...result,
|
|
838
|
+
projectName: projectName,
|
|
839
|
+
projectDisplayName: displayName,
|
|
840
|
+
projectFullPath: fullPath,
|
|
841
|
+
channel: 'codex'
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
} catch (error) {
|
|
847
|
+
console.error('Error searching Codex projects:', error);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
try {
|
|
851
|
+
// Search in Gemini projects
|
|
852
|
+
const geminiProjectsDir = path.join(os.homedir(), '.gemini', 'projects');
|
|
853
|
+
if (fs.existsSync(geminiProjectsDir)) {
|
|
854
|
+
const geminiConfig = { ...config, projectsDir: geminiProjectsDir };
|
|
855
|
+
const geminiProjects = await getProjects(geminiConfig);
|
|
856
|
+
geminiProjects.forEach(projectName => {
|
|
857
|
+
const projectResults = searchSessions(geminiConfig, projectName, keyword, contextLength);
|
|
858
|
+
const { projectName: displayName, fullPath } = parseRealProjectPath(projectName);
|
|
859
|
+
|
|
860
|
+
projectResults.forEach(result => {
|
|
861
|
+
allResults.push({
|
|
862
|
+
...result,
|
|
863
|
+
projectName: projectName,
|
|
864
|
+
projectDisplayName: displayName,
|
|
865
|
+
projectFullPath: fullPath,
|
|
866
|
+
channel: 'gemini'
|
|
867
|
+
});
|
|
868
|
+
});
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
} catch (error) {
|
|
872
|
+
console.error('Error searching Gemini projects:', error);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Sort by match count
|
|
876
|
+
allResults.sort((a, b) => b.matchCount - a.matchCount);
|
|
877
|
+
|
|
878
|
+
return allResults;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
module.exports = {
|
|
882
|
+
getProjects,
|
|
883
|
+
getProjectsWithStats,
|
|
884
|
+
getSessionsForProject,
|
|
885
|
+
deleteSession,
|
|
886
|
+
forkSession,
|
|
887
|
+
getRecentSessions,
|
|
888
|
+
getProjectOrder,
|
|
889
|
+
saveProjectOrder,
|
|
890
|
+
getSessionOrder,
|
|
891
|
+
saveSessionOrder,
|
|
892
|
+
deleteProject,
|
|
893
|
+
parseRealProjectPath,
|
|
894
|
+
searchSessions,
|
|
895
|
+
searchSessionsAcrossProjects,
|
|
896
|
+
getForkRelations,
|
|
897
|
+
saveForkRelations,
|
|
898
|
+
hasActualMessages,
|
|
899
|
+
getProjectAndSessionCounts
|
|
900
|
+
};
|