@adversity/coding-tool-x 2.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 +333 -0
- package/LICENSE +21 -0
- package/README.md +404 -0
- package/bin/ctx.js +8 -0
- package/dist/web/assets/index-D1AYlFLZ.js +3220 -0
- package/dist/web/assets/index-aL3cKxSK.css +41 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +14 -0
- package/dist/web/logo.png +0 -0
- package/docs/CHANGELOG.md +582 -0
- package/docs/DIRECTORY_MIGRATION.md +112 -0
- package/docs/PROJECT_STRUCTURE.md +396 -0
- package/docs/bannel.png +0 -0
- package/docs/home.png +0 -0
- package/docs/logo.png +0 -0
- package/docs/multi-channel-load-balancing.md +249 -0
- package/package.json +73 -0
- package/src/commands/channels.js +504 -0
- package/src/commands/cli-type.js +99 -0
- package/src/commands/daemon.js +286 -0
- package/src/commands/doctor.js +332 -0
- package/src/commands/list.js +222 -0
- package/src/commands/logs.js +259 -0
- package/src/commands/port-config.js +115 -0
- package/src/commands/proxy-control.js +258 -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/stats.js +224 -0
- package/src/commands/switch.js +48 -0
- package/src/commands/toggle-proxy.js +222 -0
- package/src/commands/ui.js +92 -0
- package/src/commands/workspace.js +454 -0
- package/src/config/default.js +40 -0
- package/src/config/loader.js +75 -0
- package/src/config/paths.js +121 -0
- package/src/index.js +373 -0
- package/src/reset-config.js +92 -0
- package/src/server/api/agents.js +248 -0
- package/src/server/api/aliases.js +36 -0
- package/src/server/api/channels.js +258 -0
- package/src/server/api/claude-hooks.js +480 -0
- package/src/server/api/codex-channels.js +312 -0
- package/src/server/api/codex-projects.js +91 -0
- package/src/server/api/codex-proxy.js +182 -0
- package/src/server/api/codex-sessions.js +491 -0
- package/src/server/api/codex-statistics.js +57 -0
- package/src/server/api/commands.js +245 -0
- package/src/server/api/config-templates.js +182 -0
- package/src/server/api/config.js +147 -0
- package/src/server/api/convert.js +127 -0
- package/src/server/api/dashboard.js +125 -0
- package/src/server/api/env.js +144 -0
- package/src/server/api/favorites.js +77 -0
- package/src/server/api/gemini-channels.js +261 -0
- package/src/server/api/gemini-projects.js +91 -0
- package/src/server/api/gemini-proxy.js +160 -0
- package/src/server/api/gemini-sessions.js +397 -0
- package/src/server/api/gemini-statistics.js +57 -0
- package/src/server/api/health-check.js +118 -0
- package/src/server/api/mcp.js +336 -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 +235 -0
- package/src/server/api/rules.js +271 -0
- package/src/server/api/sessions.js +595 -0
- package/src/server/api/settings.js +61 -0
- package/src/server/api/skills.js +305 -0
- package/src/server/api/statistics.js +91 -0
- package/src/server/api/terminal.js +202 -0
- package/src/server/api/ui-config.js +64 -0
- package/src/server/api/workspaces.js +407 -0
- package/src/server/codex-proxy-server.js +538 -0
- package/src/server/dev-server.js +26 -0
- package/src/server/gemini-proxy-server.js +518 -0
- package/src/server/index.js +305 -0
- package/src/server/proxy-server.js +469 -0
- package/src/server/services/agents-service.js +354 -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 +234 -0
- package/src/server/services/channels.js +347 -0
- package/src/server/services/codex-channels.js +625 -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 +665 -0
- package/src/server/services/codex-settings-manager.js +397 -0
- package/src/server/services/codex-speed-test-template.json +24 -0
- package/src/server/services/codex-statistics-service.js +255 -0
- package/src/server/services/commands-service.js +360 -0
- package/src/server/services/config-templates-service.js +732 -0
- package/src/server/services/env-checker.js +307 -0
- package/src/server/services/env-manager.js +300 -0
- package/src/server/services/favorites.js +163 -0
- package/src/server/services/gemini-channels.js +333 -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 +253 -0
- package/src/server/services/health-check.js +399 -0
- package/src/server/services/mcp-service.js +1188 -0
- package/src/server/services/prompts-service.js +492 -0
- package/src/server/services/proxy-runtime.js +79 -0
- package/src/server/services/pty-manager.js +435 -0
- package/src/server/services/rules-service.js +401 -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 +757 -0
- package/src/server/services/settings-manager.js +163 -0
- package/src/server/services/skill-service.js +965 -0
- package/src/server/services/speed-test.js +545 -0
- package/src/server/services/statistics-service.js +386 -0
- package/src/server/services/terminal-commands.js +155 -0
- package/src/server/services/terminal-config.js +140 -0
- package/src/server/services/terminal-detector.js +306 -0
- package/src/server/services/ui-config.js +130 -0
- package/src/server/services/workspace-service.js +662 -0
- package/src/server/utils/pricing.js +41 -0
- package/src/server/websocket-server.js +557 -0
- package/src/ui/menu.js +129 -0
- package/src/ui/prompts.js +100 -0
- package/src/utils/format.js +43 -0
- package/src/utils/port-helper.js +94 -0
- package/src/utils/session.js +239 -0
|
@@ -0,0 +1,757 @@
|
|
|
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 { PATHS } = require('../../config/paths');
|
|
15
|
+
|
|
16
|
+
// Base directory for cc-tool data
|
|
17
|
+
function getCcToolDir() {
|
|
18
|
+
return PATHS.base;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Get path for storing project order
|
|
22
|
+
function getOrderFilePath() {
|
|
23
|
+
return PATHS.projectOrder;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Get path for storing fork relations
|
|
27
|
+
function getForkRelationsFilePath() {
|
|
28
|
+
return path.join(PATHS.base, 'fork-relations.json');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Get path for storing session order
|
|
32
|
+
function getSessionOrderFilePath() {
|
|
33
|
+
return path.join(getCcToolDir(), 'session-order.json');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Get saved project order
|
|
37
|
+
function getProjectOrder(config) {
|
|
38
|
+
const orderFile = getOrderFilePath();
|
|
39
|
+
try {
|
|
40
|
+
if (fs.existsSync(orderFile)) {
|
|
41
|
+
const data = fs.readFileSync(orderFile, 'utf8');
|
|
42
|
+
return JSON.parse(data);
|
|
43
|
+
}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
// Ignore errors
|
|
46
|
+
}
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Save project order
|
|
51
|
+
function saveProjectOrder(config, order) {
|
|
52
|
+
const orderFile = getOrderFilePath();
|
|
53
|
+
const dir = path.dirname(orderFile);
|
|
54
|
+
if (!fs.existsSync(dir)) {
|
|
55
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
fs.writeFileSync(orderFile, JSON.stringify(order, null, 2), 'utf8');
|
|
58
|
+
invalidateProjectsCache(config);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Get fork relations
|
|
62
|
+
function getForkRelations() {
|
|
63
|
+
const relationsFile = getForkRelationsFilePath();
|
|
64
|
+
try {
|
|
65
|
+
if (fs.existsSync(relationsFile)) {
|
|
66
|
+
const data = fs.readFileSync(relationsFile, 'utf8');
|
|
67
|
+
return JSON.parse(data);
|
|
68
|
+
}
|
|
69
|
+
} catch (err) {
|
|
70
|
+
// Ignore errors
|
|
71
|
+
}
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Save fork relations
|
|
76
|
+
function saveForkRelations(relations) {
|
|
77
|
+
const relationsFile = getForkRelationsFilePath();
|
|
78
|
+
const dir = path.dirname(relationsFile);
|
|
79
|
+
if (!fs.existsSync(dir)) {
|
|
80
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
81
|
+
}
|
|
82
|
+
fs.writeFileSync(relationsFile, JSON.stringify(relations, null, 2), 'utf8');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Get all projects with stats
|
|
86
|
+
function getProjects(config) {
|
|
87
|
+
const projectsDir = config.projectsDir;
|
|
88
|
+
|
|
89
|
+
if (!fs.existsSync(projectsDir)) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const entries = fs.readdirSync(projectsDir, { withFileTypes: true });
|
|
94
|
+
return entries
|
|
95
|
+
.filter(entry => entry.isDirectory())
|
|
96
|
+
.map(entry => entry.name);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Parse real project path from encoded name
|
|
100
|
+
// macOS/Linux: "-Users-lilithgames-work-project" -> "/Users/lilithgames/work/project"
|
|
101
|
+
// Windows: "C--Users-admin-Desktop-project" -> "C:\Users\admin\Desktop\project"
|
|
102
|
+
function parseRealProjectPath(encodedName) {
|
|
103
|
+
const isWindows = process.platform === 'win32';
|
|
104
|
+
const fallbackFromSessions = tryResolvePathFromSessions(encodedName);
|
|
105
|
+
|
|
106
|
+
// Detect Windows drive letter (e.g., "C--Users-admin")
|
|
107
|
+
const windowsDriveMatch = encodedName.match(/^([A-Z])--(.+)$/);
|
|
108
|
+
|
|
109
|
+
if (isWindows && windowsDriveMatch) {
|
|
110
|
+
// Windows path with drive letter
|
|
111
|
+
const driveLetter = windowsDriveMatch[1];
|
|
112
|
+
const restPath = windowsDriveMatch[2];
|
|
113
|
+
|
|
114
|
+
// Split by '-' to get segments
|
|
115
|
+
const segments = restPath.split('-').filter(s => s);
|
|
116
|
+
|
|
117
|
+
// Build path from left to right, checking existence
|
|
118
|
+
let realSegments = [];
|
|
119
|
+
let accumulated = '';
|
|
120
|
+
let currentPath = '';
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < segments.length; i++) {
|
|
123
|
+
if (accumulated) {
|
|
124
|
+
accumulated += '-' + segments[i];
|
|
125
|
+
} else {
|
|
126
|
+
accumulated = segments[i];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const testPath = driveLetter + ':\\' + realSegments.concat(accumulated).join('\\');
|
|
130
|
+
|
|
131
|
+
// Check if this path exists
|
|
132
|
+
let found = fs.existsSync(testPath);
|
|
133
|
+
let finalAccumulated = accumulated;
|
|
134
|
+
|
|
135
|
+
// If not found with dash, try with underscore
|
|
136
|
+
if (!found && accumulated.includes('-')) {
|
|
137
|
+
const withUnderscore = accumulated.replace(/-/g, '_');
|
|
138
|
+
const testPathUnderscore = driveLetter + ':\\' + realSegments.concat(withUnderscore).join('\\');
|
|
139
|
+
if (fs.existsSync(testPathUnderscore)) {
|
|
140
|
+
finalAccumulated = withUnderscore;
|
|
141
|
+
found = true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (found) {
|
|
146
|
+
realSegments.push(finalAccumulated);
|
|
147
|
+
accumulated = '';
|
|
148
|
+
currentPath = driveLetter + ':\\' + realSegments.join('\\');
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// If there's remaining accumulated segment, try underscore variant
|
|
153
|
+
if (accumulated) {
|
|
154
|
+
let finalAccumulated = accumulated;
|
|
155
|
+
if (accumulated.includes('-')) {
|
|
156
|
+
const withUnderscore = accumulated.replace(/-/g, '_');
|
|
157
|
+
const testPath = driveLetter + ':\\' + realSegments.concat(withUnderscore).join('\\');
|
|
158
|
+
if (fs.existsSync(testPath)) {
|
|
159
|
+
finalAccumulated = withUnderscore;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
realSegments.push(finalAccumulated);
|
|
163
|
+
currentPath = driveLetter + ':\\' + realSegments.join('\\');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
fullPath: validateProjectPath(currentPath) || fallbackFromSessions?.fullPath || (driveLetter + ':\\' + restPath.replace(/-/g, '\\')),
|
|
168
|
+
projectName: fallbackFromSessions?.projectName || realSegments[realSegments.length - 1] || encodedName
|
|
169
|
+
};
|
|
170
|
+
} else {
|
|
171
|
+
// Unix-like path (macOS/Linux) or fallback
|
|
172
|
+
const pathStr = encodedName.replace(/^-/, '/').replace(/-/g, '/');
|
|
173
|
+
const segments = pathStr.split('/').filter(s => s);
|
|
174
|
+
|
|
175
|
+
// Build path from left to right, checking existence
|
|
176
|
+
let currentPath = '';
|
|
177
|
+
const realSegments = [];
|
|
178
|
+
let accumulated = '';
|
|
179
|
+
|
|
180
|
+
for (let i = 0; i < segments.length; i++) {
|
|
181
|
+
if (accumulated) {
|
|
182
|
+
accumulated += '-' + segments[i];
|
|
183
|
+
} else {
|
|
184
|
+
accumulated = segments[i];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const testPath = '/' + realSegments.concat(accumulated).join('/');
|
|
188
|
+
|
|
189
|
+
// Check if this path exists
|
|
190
|
+
let found = fs.existsSync(testPath);
|
|
191
|
+
let finalAccumulated = accumulated;
|
|
192
|
+
|
|
193
|
+
// If not found with dash, try with underscore
|
|
194
|
+
if (!found && accumulated.includes('-')) {
|
|
195
|
+
const withUnderscore = accumulated.replace(/-/g, '_');
|
|
196
|
+
const testPathUnderscore = '/' + realSegments.concat(withUnderscore).join('/');
|
|
197
|
+
if (fs.existsSync(testPathUnderscore)) {
|
|
198
|
+
finalAccumulated = withUnderscore;
|
|
199
|
+
found = true;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (found) {
|
|
204
|
+
realSegments.push(finalAccumulated);
|
|
205
|
+
accumulated = '';
|
|
206
|
+
currentPath = '/' + realSegments.join('/');
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// If there's remaining accumulated segment, try underscore variant
|
|
211
|
+
if (accumulated) {
|
|
212
|
+
let finalAccumulated = accumulated;
|
|
213
|
+
if (accumulated.includes('-')) {
|
|
214
|
+
const withUnderscore = accumulated.replace(/-/g, '_');
|
|
215
|
+
const testPath = '/' + realSegments.concat(withUnderscore).join('/');
|
|
216
|
+
if (fs.existsSync(testPath)) {
|
|
217
|
+
finalAccumulated = withUnderscore;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
realSegments.push(finalAccumulated);
|
|
221
|
+
currentPath = '/' + realSegments.join('/');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
fullPath: validateProjectPath(currentPath) || fallbackFromSessions?.fullPath || pathStr,
|
|
226
|
+
projectName: fallbackFromSessions?.projectName || realSegments[realSegments.length - 1] || encodedName
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function validateProjectPath(candidatePath) {
|
|
232
|
+
if (candidatePath && fs.existsSync(candidatePath)) {
|
|
233
|
+
return candidatePath;
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function tryResolvePathFromSessions(encodedName) {
|
|
239
|
+
try {
|
|
240
|
+
const projectDir = path.join(os.homedir(), '.claude', 'projects', encodedName);
|
|
241
|
+
if (!fs.existsSync(projectDir)) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
const files = fs.readdirSync(projectDir).filter(f => f.endsWith('.jsonl'));
|
|
245
|
+
for (const file of files) {
|
|
246
|
+
const sessionFile = path.join(projectDir, file);
|
|
247
|
+
const cwd = extractCwdFromSessionHeader(sessionFile);
|
|
248
|
+
if (cwd && fs.existsSync(cwd)) {
|
|
249
|
+
return {
|
|
250
|
+
fullPath: cwd,
|
|
251
|
+
projectName: path.basename(cwd)
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
} catch (err) {
|
|
256
|
+
// ignore fallback errors
|
|
257
|
+
}
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function extractCwdFromSessionHeader(sessionFile) {
|
|
262
|
+
try {
|
|
263
|
+
const fd = fs.openSync(sessionFile, 'r');
|
|
264
|
+
const buffer = Buffer.alloc(4096);
|
|
265
|
+
const bytesRead = fs.readSync(fd, buffer, 0, 4096, 0);
|
|
266
|
+
fs.closeSync(fd);
|
|
267
|
+
const content = buffer.slice(0, bytesRead).toString('utf8');
|
|
268
|
+
const lines = content.split('\n');
|
|
269
|
+
for (const line of lines) {
|
|
270
|
+
if (!line.trim()) continue;
|
|
271
|
+
try {
|
|
272
|
+
const json = JSON.parse(line);
|
|
273
|
+
if (json.cwd && typeof json.cwd === 'string') {
|
|
274
|
+
return json.cwd;
|
|
275
|
+
}
|
|
276
|
+
} catch (e) {
|
|
277
|
+
// ignore
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} catch (err) {
|
|
281
|
+
// ignore
|
|
282
|
+
}
|
|
283
|
+
return null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Get projects with detailed stats (with caching)
|
|
287
|
+
function getProjectsWithStats(config, options = {}) {
|
|
288
|
+
if (!options.force) {
|
|
289
|
+
const cached = getCachedProjects(config);
|
|
290
|
+
if (cached) {
|
|
291
|
+
return cached;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const data = buildProjectsWithStats(config);
|
|
296
|
+
setCachedProjects(config, data);
|
|
297
|
+
return data;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function buildProjectsWithStats(config) {
|
|
301
|
+
const projectsDir = config.projectsDir;
|
|
302
|
+
|
|
303
|
+
if (!fs.existsSync(projectsDir)) {
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const entries = fs.readdirSync(projectsDir, { withFileTypes: true });
|
|
308
|
+
|
|
309
|
+
return entries
|
|
310
|
+
.filter(entry => entry.isDirectory())
|
|
311
|
+
.map(entry => {
|
|
312
|
+
const projectName = entry.name;
|
|
313
|
+
const projectPath = path.join(projectsDir, projectName);
|
|
314
|
+
|
|
315
|
+
// Parse real project path
|
|
316
|
+
const { fullPath, projectName: displayName } = parseRealProjectPath(projectName);
|
|
317
|
+
|
|
318
|
+
// Get session files (only count sessions with actual messages)
|
|
319
|
+
let sessionCount = 0;
|
|
320
|
+
let lastUsed = null;
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const files = fs.readdirSync(projectPath);
|
|
324
|
+
const jsonlFiles = files.filter(f => f.endsWith('.jsonl') && !f.startsWith('agent-'));
|
|
325
|
+
|
|
326
|
+
// Filter: only count sessions that have actual messages (not just file-history-snapshots)
|
|
327
|
+
const sessionFilesWithMessages = jsonlFiles.filter(f => {
|
|
328
|
+
const filePath = path.join(projectPath, f);
|
|
329
|
+
return hasActualMessages(filePath);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
sessionCount = sessionFilesWithMessages.length;
|
|
333
|
+
|
|
334
|
+
// Find most recent session (only from sessions with messages)
|
|
335
|
+
if (sessionFilesWithMessages.length > 0) {
|
|
336
|
+
const stats = sessionFilesWithMessages.map(f => {
|
|
337
|
+
const filePath = path.join(projectPath, f);
|
|
338
|
+
const stat = fs.statSync(filePath);
|
|
339
|
+
return stat.mtime.getTime();
|
|
340
|
+
});
|
|
341
|
+
lastUsed = Math.max(...stats);
|
|
342
|
+
}
|
|
343
|
+
} catch (err) {
|
|
344
|
+
// Ignore errors
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
name: projectName, // Keep encoded name for API operations
|
|
349
|
+
displayName, // Project name for display
|
|
350
|
+
fullPath, // Real full path for display
|
|
351
|
+
sessionCount,
|
|
352
|
+
lastUsed
|
|
353
|
+
};
|
|
354
|
+
})
|
|
355
|
+
.sort((a, b) => (b.lastUsed || 0) - (a.lastUsed || 0)); // Sort by last used
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 获取 Claude 项目/会话数量(轻量统计)
|
|
359
|
+
function getProjectAndSessionCounts(config) {
|
|
360
|
+
const projectsDir = config.projectsDir;
|
|
361
|
+
if (!fs.existsSync(projectsDir)) {
|
|
362
|
+
return { projectCount: 0, sessionCount: 0 };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
let projectCount = 0;
|
|
366
|
+
let sessionCount = 0;
|
|
367
|
+
|
|
368
|
+
const entries = fs.readdirSync(projectsDir, { withFileTypes: true });
|
|
369
|
+
entries.forEach((entry) => {
|
|
370
|
+
if (!entry.isDirectory()) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
projectCount += 1;
|
|
374
|
+
const projectPath = path.join(projectsDir, entry.name);
|
|
375
|
+
try {
|
|
376
|
+
const files = fs.readdirSync(projectPath);
|
|
377
|
+
sessionCount += files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-')).length;
|
|
378
|
+
} catch (err) {
|
|
379
|
+
// 忽略单个项目的读取错误
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
return { projectCount, sessionCount };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Check if a session file has actual messages (not just file-history-snapshots)
|
|
387
|
+
function hasActualMessages(filePath) {
|
|
388
|
+
try {
|
|
389
|
+
const stats = fs.statSync(filePath);
|
|
390
|
+
const cached = checkHasMessagesCache(filePath, stats);
|
|
391
|
+
if (typeof cached === 'boolean') {
|
|
392
|
+
return cached;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const result = scanSessionFileForMessages(filePath);
|
|
396
|
+
rememberHasMessages(filePath, stats, result);
|
|
397
|
+
return result;
|
|
398
|
+
} catch (err) {
|
|
399
|
+
return false;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function scanSessionFileForMessages(filePath) {
|
|
404
|
+
let fd = null;
|
|
405
|
+
try {
|
|
406
|
+
fd = fs.openSync(filePath, 'r');
|
|
407
|
+
const bufferSize = 64 * 1024;
|
|
408
|
+
const buffer = Buffer.alloc(bufferSize);
|
|
409
|
+
const pattern = /"type"\s*:\s*"(user|assistant|summary)"/;
|
|
410
|
+
let leftover = '';
|
|
411
|
+
let bytesRead;
|
|
412
|
+
|
|
413
|
+
while ((bytesRead = fs.readSync(fd, buffer, 0, bufferSize, null)) > 0) {
|
|
414
|
+
const chunk = buffer.toString('utf8', 0, bytesRead);
|
|
415
|
+
const combined = leftover + chunk;
|
|
416
|
+
if (pattern.test(combined)) {
|
|
417
|
+
fs.closeSync(fd);
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
leftover = combined.slice(-64);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
fs.closeSync(fd);
|
|
424
|
+
return false;
|
|
425
|
+
} catch (err) {
|
|
426
|
+
if (fd) {
|
|
427
|
+
try {
|
|
428
|
+
fs.closeSync(fd);
|
|
429
|
+
} catch (e) {
|
|
430
|
+
// ignore
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Get sessions for a project
|
|
438
|
+
function getSessionsForProject(config, projectName) {
|
|
439
|
+
const projectConfig = { ...config, currentProject: projectName };
|
|
440
|
+
const sessions = getAllSessions(projectConfig);
|
|
441
|
+
const forkRelations = getForkRelations();
|
|
442
|
+
const savedOrder = getSessionOrder(projectName);
|
|
443
|
+
|
|
444
|
+
// Parse session info and calculate total size, filter out sessions with no messages
|
|
445
|
+
let totalSize = 0;
|
|
446
|
+
const sessionsWithInfo = sessions
|
|
447
|
+
.filter(session => hasActualMessages(session.filePath))
|
|
448
|
+
.map(session => {
|
|
449
|
+
const info = parseSessionInfoFast(session.filePath);
|
|
450
|
+
totalSize += session.size || 0;
|
|
451
|
+
return {
|
|
452
|
+
sessionId: session.sessionId,
|
|
453
|
+
mtime: session.mtime,
|
|
454
|
+
size: session.size,
|
|
455
|
+
filePath: session.filePath,
|
|
456
|
+
gitBranch: info.gitBranch || null,
|
|
457
|
+
firstMessage: info.firstMessage || null,
|
|
458
|
+
forkedFrom: forkRelations[session.sessionId] || null
|
|
459
|
+
};
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// Apply saved order if exists
|
|
463
|
+
let orderedSessions = sessionsWithInfo;
|
|
464
|
+
if (savedOrder.length > 0) {
|
|
465
|
+
const ordered = [];
|
|
466
|
+
const sessionMap = new Map(sessionsWithInfo.map(s => [s.sessionId, s]));
|
|
467
|
+
|
|
468
|
+
// Add sessions in saved order
|
|
469
|
+
for (const sessionId of savedOrder) {
|
|
470
|
+
if (sessionMap.has(sessionId)) {
|
|
471
|
+
ordered.push(sessionMap.get(sessionId));
|
|
472
|
+
sessionMap.delete(sessionId);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Add remaining sessions (new ones not in saved order)
|
|
477
|
+
ordered.push(...sessionMap.values());
|
|
478
|
+
orderedSessions = ordered;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return {
|
|
482
|
+
sessions: orderedSessions,
|
|
483
|
+
totalSize
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Delete a session
|
|
488
|
+
function deleteSession(config, projectName, sessionId) {
|
|
489
|
+
const projectDir = path.join(config.projectsDir, projectName);
|
|
490
|
+
const sessionFile = path.join(projectDir, sessionId + '.jsonl');
|
|
491
|
+
|
|
492
|
+
if (!fs.existsSync(sessionFile)) {
|
|
493
|
+
throw new Error('Session not found');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
fs.unlinkSync(sessionFile);
|
|
497
|
+
invalidateProjectsCache(config);
|
|
498
|
+
return { success: true };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Fork a session
|
|
502
|
+
function forkSession(config, projectName, sessionId) {
|
|
503
|
+
const projectDir = path.join(config.projectsDir, projectName);
|
|
504
|
+
const sessionFile = path.join(projectDir, sessionId + '.jsonl');
|
|
505
|
+
|
|
506
|
+
if (!fs.existsSync(sessionFile)) {
|
|
507
|
+
throw new Error('Session not found');
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Read the original session
|
|
511
|
+
const content = fs.readFileSync(sessionFile, 'utf8');
|
|
512
|
+
|
|
513
|
+
// Generate new session ID (UUID v4)
|
|
514
|
+
const newSessionId = crypto.randomUUID();
|
|
515
|
+
const newSessionFile = path.join(projectDir, newSessionId + '.jsonl');
|
|
516
|
+
|
|
517
|
+
// Write to new file
|
|
518
|
+
fs.writeFileSync(newSessionFile, content, 'utf8');
|
|
519
|
+
|
|
520
|
+
// Save fork relation
|
|
521
|
+
const forkRelations = getForkRelations();
|
|
522
|
+
forkRelations[newSessionId] = sessionId;
|
|
523
|
+
saveForkRelations(forkRelations);
|
|
524
|
+
invalidateProjectsCache(config);
|
|
525
|
+
|
|
526
|
+
return { newSessionId, forkedFrom: sessionId };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Get session order for a project
|
|
530
|
+
function getSessionOrder(projectName) {
|
|
531
|
+
const orderFile = getSessionOrderFilePath();
|
|
532
|
+
try {
|
|
533
|
+
if (fs.existsSync(orderFile)) {
|
|
534
|
+
const data = fs.readFileSync(orderFile, 'utf8');
|
|
535
|
+
const allOrders = JSON.parse(data);
|
|
536
|
+
return allOrders[projectName] || [];
|
|
537
|
+
}
|
|
538
|
+
} catch (err) {
|
|
539
|
+
// Ignore errors
|
|
540
|
+
}
|
|
541
|
+
return [];
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Save session order for a project
|
|
545
|
+
function saveSessionOrder(projectName, order) {
|
|
546
|
+
const orderFile = getSessionOrderFilePath();
|
|
547
|
+
const dir = path.dirname(orderFile);
|
|
548
|
+
if (!fs.existsSync(dir)) {
|
|
549
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Read existing orders
|
|
553
|
+
let allOrders = {};
|
|
554
|
+
try {
|
|
555
|
+
if (fs.existsSync(orderFile)) {
|
|
556
|
+
const data = fs.readFileSync(orderFile, 'utf8');
|
|
557
|
+
allOrders = JSON.parse(data);
|
|
558
|
+
}
|
|
559
|
+
} catch (err) {
|
|
560
|
+
// Ignore errors
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Update order for this project
|
|
564
|
+
allOrders[projectName] = order;
|
|
565
|
+
fs.writeFileSync(orderFile, JSON.stringify(allOrders, null, 2), 'utf8');
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// Delete a project (remove the entire project directory)
|
|
569
|
+
function deleteProject(config, projectName) {
|
|
570
|
+
const projectDir = path.join(config.projectsDir, projectName);
|
|
571
|
+
|
|
572
|
+
if (!fs.existsSync(projectDir)) {
|
|
573
|
+
throw new Error('Project not found');
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Recursively delete the directory
|
|
577
|
+
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
578
|
+
|
|
579
|
+
// Remove from order file if exists
|
|
580
|
+
const order = getProjectOrder(config);
|
|
581
|
+
const newOrder = order.filter(name => name !== projectName);
|
|
582
|
+
if (newOrder.length !== order.length) {
|
|
583
|
+
saveProjectOrder(config, newOrder);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
invalidateProjectsCache(config);
|
|
587
|
+
return { success: true };
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// Search sessions for keyword
|
|
591
|
+
function searchSessions(config, projectName, keyword, contextLength = 15) {
|
|
592
|
+
const projectDir = path.join(config.projectsDir, projectName);
|
|
593
|
+
|
|
594
|
+
if (!fs.existsSync(projectDir)) {
|
|
595
|
+
return [];
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const results = [];
|
|
599
|
+
const files = fs.readdirSync(projectDir);
|
|
600
|
+
const jsonlFiles = files.filter(f => f.endsWith('.jsonl') && !f.startsWith('agent-'));
|
|
601
|
+
const aliases = loadAliases();
|
|
602
|
+
|
|
603
|
+
for (const file of jsonlFiles) {
|
|
604
|
+
const sessionId = file.replace('.jsonl', '');
|
|
605
|
+
const filePath = path.join(projectDir, file);
|
|
606
|
+
|
|
607
|
+
// Skip sessions with no actual messages
|
|
608
|
+
if (!hasActualMessages(filePath)) {
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
try {
|
|
613
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
614
|
+
const lines = content.split('\n');
|
|
615
|
+
const matches = [];
|
|
616
|
+
|
|
617
|
+
for (const line of lines) {
|
|
618
|
+
if (!line.trim()) continue;
|
|
619
|
+
|
|
620
|
+
try {
|
|
621
|
+
const json = JSON.parse(line);
|
|
622
|
+
|
|
623
|
+
// Search in message content
|
|
624
|
+
if (json.message && json.message.content) {
|
|
625
|
+
const text = json.message.content;
|
|
626
|
+
const lowerText = text.toLowerCase();
|
|
627
|
+
const lowerKeyword = keyword.toLowerCase();
|
|
628
|
+
let index = 0;
|
|
629
|
+
|
|
630
|
+
while ((index = lowerText.indexOf(lowerKeyword, index)) !== -1) {
|
|
631
|
+
// Extract context
|
|
632
|
+
const start = Math.max(0, index - contextLength);
|
|
633
|
+
const end = Math.min(text.length, index + keyword.length + contextLength);
|
|
634
|
+
const context = text.substring(start, end);
|
|
635
|
+
|
|
636
|
+
matches.push({
|
|
637
|
+
role: json.message.role || 'unknown',
|
|
638
|
+
context: (start > 0 ? '...' : '') + context + (end < text.length ? '...' : ''),
|
|
639
|
+
position: index
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
index += keyword.length;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
} catch (e) {
|
|
646
|
+
// Skip invalid JSON lines
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (matches.length > 0) {
|
|
651
|
+
results.push({
|
|
652
|
+
sessionId,
|
|
653
|
+
alias: aliases[sessionId] || null,
|
|
654
|
+
matchCount: matches.length,
|
|
655
|
+
matches: matches.slice(0, 5) // Limit to 5 matches per session
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
} catch (e) {
|
|
659
|
+
// Skip files that can't be read
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Sort by match count
|
|
664
|
+
results.sort((a, b) => b.matchCount - a.matchCount);
|
|
665
|
+
|
|
666
|
+
return results;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Get recent sessions across all projects
|
|
670
|
+
function getRecentSessions(config, limit = 5) {
|
|
671
|
+
const projects = getProjects(config);
|
|
672
|
+
const allSessions = [];
|
|
673
|
+
const forkRelations = getForkRelations();
|
|
674
|
+
const aliases = loadAliases();
|
|
675
|
+
|
|
676
|
+
// Collect all sessions from all projects
|
|
677
|
+
projects.forEach(projectName => {
|
|
678
|
+
const projectConfig = { ...config, currentProject: projectName };
|
|
679
|
+
const sessions = getAllSessions(projectConfig);
|
|
680
|
+
const { projectName: displayName, fullPath } = parseRealProjectPath(projectName);
|
|
681
|
+
|
|
682
|
+
sessions.forEach(session => {
|
|
683
|
+
// Skip sessions with no actual messages
|
|
684
|
+
if (!hasActualMessages(session.filePath)) {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
const info = parseSessionInfoFast(session.filePath);
|
|
689
|
+
allSessions.push({
|
|
690
|
+
sessionId: session.sessionId,
|
|
691
|
+
projectName: projectName,
|
|
692
|
+
projectDisplayName: displayName,
|
|
693
|
+
projectFullPath: fullPath,
|
|
694
|
+
mtime: session.mtime,
|
|
695
|
+
size: session.size,
|
|
696
|
+
filePath: session.filePath,
|
|
697
|
+
gitBranch: info.gitBranch || null,
|
|
698
|
+
firstMessage: info.firstMessage || null,
|
|
699
|
+
forkedFrom: forkRelations[session.sessionId] || null,
|
|
700
|
+
alias: aliases[session.sessionId] || null
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
// Sort by mtime descending (most recent first)
|
|
706
|
+
allSessions.sort((a, b) => b.mtime - a.mtime);
|
|
707
|
+
|
|
708
|
+
// Return top N sessions
|
|
709
|
+
return allSessions.slice(0, limit);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// Search sessions across all projects
|
|
713
|
+
function searchSessionsAcrossProjects(config, keyword, contextLength = 35) {
|
|
714
|
+
const projects = getProjects(config);
|
|
715
|
+
const allResults = [];
|
|
716
|
+
|
|
717
|
+
projects.forEach(projectName => {
|
|
718
|
+
const projectResults = searchSessions(config, projectName, keyword, contextLength);
|
|
719
|
+
const { projectName: displayName, fullPath } = parseRealProjectPath(projectName);
|
|
720
|
+
|
|
721
|
+
// Add project info to each result
|
|
722
|
+
projectResults.forEach(result => {
|
|
723
|
+
allResults.push({
|
|
724
|
+
...result,
|
|
725
|
+
projectName: projectName,
|
|
726
|
+
projectDisplayName: displayName,
|
|
727
|
+
projectFullPath: fullPath
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
// Sort by match count
|
|
733
|
+
allResults.sort((a, b) => b.matchCount - a.matchCount);
|
|
734
|
+
|
|
735
|
+
return allResults;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
module.exports = {
|
|
739
|
+
getProjects,
|
|
740
|
+
getProjectsWithStats,
|
|
741
|
+
getSessionsForProject,
|
|
742
|
+
deleteSession,
|
|
743
|
+
forkSession,
|
|
744
|
+
getRecentSessions,
|
|
745
|
+
getProjectOrder,
|
|
746
|
+
saveProjectOrder,
|
|
747
|
+
getSessionOrder,
|
|
748
|
+
saveSessionOrder,
|
|
749
|
+
deleteProject,
|
|
750
|
+
parseRealProjectPath,
|
|
751
|
+
searchSessions,
|
|
752
|
+
searchSessionsAcrossProjects,
|
|
753
|
+
getForkRelations,
|
|
754
|
+
saveForkRelations,
|
|
755
|
+
hasActualMessages,
|
|
756
|
+
getProjectAndSessionCounts
|
|
757
|
+
};
|