coding-tool-x 3.8.0 → 3.8.2
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 +2 -0
- package/README.md +4 -6
- package/dist/web/assets/{Analytics-8dRv1aYW.js → Analytics-COEUXmd4.js} +1 -1
- package/dist/web/assets/{ConfigTemplates-BQRXSICO.js → ConfigTemplates-DLLkS53H.js} +1 -1
- package/dist/web/assets/{Home-BtzWhKIa.js → Home-DCEyLzwX.js} +1 -1
- package/dist/web/assets/{PluginManager-BTa-Ygf6.js → PluginManager-Oe1ZHoMO.js} +1 -1
- package/dist/web/assets/{ProjectList-F_cB8ldA.js → ProjectList-CWjuAFof.js} +1 -1
- package/dist/web/assets/{SessionList-LOrbPQvu.js → SessionList-v3ehGg9U.js} +1 -1
- package/dist/web/assets/{SkillManager-BzI_t-yV.js → SkillManager-DUF_qP5S.js} +1 -1
- package/dist/web/assets/{WorkspaceManager-Db9RGiz7.js → WorkspaceManager-CjAWBosO.js} +1 -1
- package/dist/web/assets/index-D5hyHPj7.js +2 -0
- package/dist/web/index.html +1 -1
- package/docs/superpowers/plans/2026-08-10-codex-conversion-compatibility.md +251 -0
- package/docs/superpowers/plans/2026-08-11-windows-command-window-hiding.md +90 -0
- package/docs/superpowers/specs/2026-08-10-codex-conversion-compatibility-design.md +61 -0
- package/docs/superpowers/specs/2026-08-11-windows-command-window-hiding-design.md +44 -0
- package/package.json +2 -2
- package/src/commands/update.js +1 -1
- package/src/config/paths.js +1 -0
- package/src/server/api/codex-sessions.js +14 -14
- package/src/server/api/gemini-sessions.js +52 -14
- package/src/server/api/omp-projects.js +2 -2
- package/src/server/api/omp-sessions.js +61 -23
- package/src/server/api/sessions.js +44 -2
- package/src/server/opencode-proxy-server.js +55 -1037
- package/src/server/services/claude-wire.js +694 -0
- package/src/server/services/codex-sessions.js +41 -233
- package/src/server/services/codex-wire.js +219 -0
- package/src/server/services/dashboard-snapshot-worker.js +5 -5
- package/src/server/services/gemini-sessions.js +67 -157
- package/src/server/services/gemini-wire.js +759 -0
- package/src/server/services/model-detector.js +49 -131
- package/src/server/services/omp-channels.js +23 -4
- package/src/server/services/omp-gateway.js +29 -1
- package/src/server/services/omp-sessions.js +102 -105
- package/src/server/services/omp-settings-manager.js +27 -3
- package/src/server/services/opencode-gateway-adapters.js +22 -1147
- package/src/server/services/opencode-normalization.js +546 -0
- package/src/server/services/opencode-sessions.js +339 -546
- package/src/server/services/session-converter.js +4 -4
- package/src/server/services/session-history-adapters/claude.js +235 -0
- package/src/server/services/session-history-adapters/codex.js +158 -0
- package/src/server/services/session-history-adapters/gemini.js +292 -0
- package/src/server/services/session-history-adapters/index.js +20 -0
- package/src/server/services/session-history-adapters/omp.js +256 -0
- package/src/server/services/session-history-index.js +930 -0
- package/src/server/services/session-history-worker.js +99 -0
- package/src/server/services/session-snapshot-worker.js +7 -7
- package/src/server/services/sessions.js +167 -775
- package/src/server/services/speed-test.js +85 -303
- package/src/server/services/sqlite-connection.js +78 -0
- package/src/server/services/workspace-service.js +3 -3
- package/dist/web/assets/index-DIWJmyL6.js +0 -2
|
@@ -2,15 +2,9 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const { getAllSessions, parseSessionInfoFast } = require('../../utils/session');
|
|
5
|
-
const { loadAliases } = require('./alias');
|
|
6
|
-
const {
|
|
7
|
-
getCachedProjects,
|
|
8
|
-
setCachedProjects,
|
|
9
|
-
invalidateProjectsCache,
|
|
10
|
-
checkHasMessagesCache,
|
|
11
|
-
rememberHasMessages
|
|
12
|
-
} = require('./session-cache');
|
|
5
|
+
const { loadAliases, setAlias } = require('./alias');
|
|
13
6
|
const { globalCache, CacheKeys } = require('./enhanced-cache');
|
|
7
|
+
const { listProjects: idxListProjects, listSessions: idxListSessions, getRecentSessions: idxGetRecent, searchSessions: idxSearch } = require('./session-history-index');
|
|
14
8
|
const { PATHS, NATIVE_PATHS } = require('../../config/paths');
|
|
15
9
|
|
|
16
10
|
const CLAUDE_PROJECTS_DIR = NATIVE_PATHS.claude.projects;
|
|
@@ -22,78 +16,37 @@ function resolveProjectsDir(config = {}) {
|
|
|
22
16
|
}
|
|
23
17
|
|
|
24
18
|
function withResolvedProjectsDir(config = {}) {
|
|
25
|
-
return {
|
|
26
|
-
...config,
|
|
27
|
-
projectsDir: resolveProjectsDir(config)
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Base directory for cc-tool data
|
|
32
|
-
function getCcToolDir() {
|
|
33
|
-
return PATHS.base;
|
|
19
|
+
return { ...config, projectsDir: resolveProjectsDir(config) };
|
|
34
20
|
}
|
|
35
21
|
|
|
36
|
-
|
|
37
|
-
function getOrderFilePath() {
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Get path for storing fork relations
|
|
42
|
-
function getForkRelationsFilePath() {
|
|
43
|
-
return PATHS.forkRelations;
|
|
44
|
-
}
|
|
22
|
+
function getCcToolDir() { return PATHS.base; }
|
|
23
|
+
function getOrderFilePath() { return PATHS.projectOrder; }
|
|
24
|
+
function getForkRelationsFilePath() { return PATHS.forkRelations; }
|
|
25
|
+
function getSessionOrderFilePath() { return PATHS.sessionOrder; }
|
|
45
26
|
|
|
46
|
-
// Get path for storing session order
|
|
47
|
-
function getSessionOrderFilePath() {
|
|
48
|
-
return PATHS.sessionOrder;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Get saved project order
|
|
52
27
|
function getProjectOrder(config) {
|
|
53
28
|
const orderFile = getOrderFilePath();
|
|
54
|
-
try {
|
|
55
|
-
if (fs.existsSync(orderFile)) {
|
|
56
|
-
const data = fs.readFileSync(orderFile, 'utf8');
|
|
57
|
-
return JSON.parse(data);
|
|
58
|
-
}
|
|
59
|
-
} catch (err) {
|
|
60
|
-
// Ignore errors
|
|
61
|
-
}
|
|
29
|
+
try { if (fs.existsSync(orderFile)) return JSON.parse(fs.readFileSync(orderFile, 'utf8')); } catch (_) {}
|
|
62
30
|
return [];
|
|
63
31
|
}
|
|
64
32
|
|
|
65
|
-
// Save project order
|
|
66
33
|
function saveProjectOrder(config, order) {
|
|
67
34
|
const orderFile = getOrderFilePath();
|
|
68
35
|
const dir = path.dirname(orderFile);
|
|
69
|
-
if (!fs.existsSync(dir)) {
|
|
70
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
71
|
-
}
|
|
36
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
72
37
|
fs.writeFileSync(orderFile, JSON.stringify(order, null, 2), 'utf8');
|
|
73
|
-
invalidateProjectsCache(config);
|
|
74
38
|
}
|
|
75
39
|
|
|
76
|
-
// Get fork relations
|
|
77
40
|
function getForkRelations() {
|
|
78
41
|
const relationsFile = getForkRelationsFilePath();
|
|
79
|
-
try {
|
|
80
|
-
if (fs.existsSync(relationsFile)) {
|
|
81
|
-
const data = fs.readFileSync(relationsFile, 'utf8');
|
|
82
|
-
return JSON.parse(data);
|
|
83
|
-
}
|
|
84
|
-
} catch (err) {
|
|
85
|
-
// Ignore errors
|
|
86
|
-
}
|
|
42
|
+
try { if (fs.existsSync(relationsFile)) return JSON.parse(fs.readFileSync(relationsFile, 'utf8')); } catch (_) {}
|
|
87
43
|
return {};
|
|
88
44
|
}
|
|
89
45
|
|
|
90
|
-
// Save fork relations
|
|
91
46
|
function saveForkRelations(relations) {
|
|
92
47
|
const relationsFile = getForkRelationsFilePath();
|
|
93
48
|
const dir = path.dirname(relationsFile);
|
|
94
|
-
if (!fs.existsSync(dir)) {
|
|
95
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
96
|
-
}
|
|
49
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
97
50
|
fs.writeFileSync(relationsFile, JSON.stringify(relations, null, 2), 'utf8');
|
|
98
51
|
}
|
|
99
52
|
|
|
@@ -103,34 +56,20 @@ function invalidateSessionResultCache(projectName) {
|
|
|
103
56
|
}
|
|
104
57
|
|
|
105
58
|
function getClaudeForkMessageText(content) {
|
|
106
|
-
if (typeof content === 'string')
|
|
107
|
-
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (!Array.isArray(content)) {
|
|
111
|
-
return '';
|
|
112
|
-
}
|
|
113
|
-
|
|
59
|
+
if (typeof content === 'string') return content === 'Warmup' ? '' : content;
|
|
60
|
+
if (!Array.isArray(content)) return '';
|
|
114
61
|
const parts = [];
|
|
115
62
|
for (const item of content) {
|
|
116
|
-
if (item?.type === 'text' && item.text)
|
|
117
|
-
|
|
118
|
-
} else if (item?.type === 'image') {
|
|
119
|
-
parts.push('[图片]');
|
|
120
|
-
}
|
|
63
|
+
if (item?.type === 'text' && item.text) parts.push(item.text);
|
|
64
|
+
else if (item?.type === 'image') parts.push('[图片]');
|
|
121
65
|
}
|
|
122
|
-
|
|
123
66
|
return parts.join('\n\n').trim();
|
|
124
67
|
}
|
|
125
68
|
|
|
126
69
|
function splitTextPreserveEol(content) {
|
|
127
70
|
const eol = content.includes('\r\n') ? '\r\n' : '\n';
|
|
128
71
|
const hasTrailingEol = content.endsWith('\r\n') || content.endsWith('\n');
|
|
129
|
-
return {
|
|
130
|
-
lines: content.split(/\r?\n/),
|
|
131
|
-
eol,
|
|
132
|
-
hasTrailingEol
|
|
133
|
-
};
|
|
72
|
+
return { lines: content.split(/\r?\n/), eol, hasTrailingEol };
|
|
134
73
|
}
|
|
135
74
|
|
|
136
75
|
function joinTextPreserveEol(lines, eol, hasTrailingEol) {
|
|
@@ -139,134 +78,73 @@ function joinTextPreserveEol(lines, eol, hasTrailingEol) {
|
|
|
139
78
|
}
|
|
140
79
|
|
|
141
80
|
function sliceClaudeSessionContentByUserMessage(content, afterUserMessageNumber) {
|
|
142
|
-
if (!Number.isInteger(afterUserMessageNumber) || afterUserMessageNumber <= 0)
|
|
143
|
-
return content;
|
|
144
|
-
}
|
|
145
|
-
|
|
81
|
+
if (!Number.isInteger(afterUserMessageNumber) || afterUserMessageNumber <= 0) return content;
|
|
146
82
|
const { lines, eol, hasTrailingEol } = splitTextPreserveEol(content);
|
|
147
83
|
const keptLines = [];
|
|
148
84
|
let matchedUserMessages = 0;
|
|
149
85
|
let targetUserReached = false;
|
|
150
|
-
|
|
151
86
|
for (const line of lines) {
|
|
152
|
-
if (!line.trim()) {
|
|
153
|
-
keptLines.push(line);
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
|
|
87
|
+
if (!line.trim()) { keptLines.push(line); continue; }
|
|
157
88
|
let json;
|
|
158
|
-
try {
|
|
159
|
-
|
|
160
|
-
} catch (err) {
|
|
161
|
-
keptLines.push(line);
|
|
162
|
-
continue;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (json.type !== 'user') {
|
|
166
|
-
keptLines.push(line);
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
|
|
89
|
+
try { json = JSON.parse(line); } catch (_) { keptLines.push(line); continue; }
|
|
90
|
+
if (json.type !== 'user') { keptLines.push(line); continue; }
|
|
170
91
|
const userText = getClaudeForkMessageText(json.message?.content);
|
|
171
|
-
if (!userText) {
|
|
172
|
-
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (targetUserReached) {
|
|
177
|
-
return joinTextPreserveEol(keptLines, eol, hasTrailingEol);
|
|
178
|
-
}
|
|
179
|
-
|
|
92
|
+
if (!userText) { keptLines.push(line); continue; }
|
|
93
|
+
if (targetUserReached) return joinTextPreserveEol(keptLines, eol, hasTrailingEol);
|
|
180
94
|
matchedUserMessages += 1;
|
|
181
95
|
keptLines.push(line);
|
|
182
|
-
if (matchedUserMessages >= afterUserMessageNumber)
|
|
183
|
-
targetUserReached = true;
|
|
184
|
-
}
|
|
96
|
+
if (matchedUserMessages >= afterUserMessageNumber) targetUserReached = true;
|
|
185
97
|
}
|
|
186
|
-
|
|
187
|
-
if (targetUserReached) {
|
|
188
|
-
return joinTextPreserveEol(keptLines, eol, hasTrailingEol);
|
|
189
|
-
}
|
|
190
|
-
|
|
98
|
+
if (targetUserReached) return joinTextPreserveEol(keptLines, eol, hasTrailingEol);
|
|
191
99
|
throw new Error(`afterUserMessageNumber ${afterUserMessageNumber} exceeds available user messages (${matchedUserMessages})`);
|
|
192
100
|
}
|
|
193
101
|
|
|
194
|
-
//
|
|
195
|
-
async function getProjects(config) {
|
|
196
|
-
const projectsDir = resolveProjectsDir(config);
|
|
102
|
+
// ===== Read-side: all project/list/session/recent/search delegate to session-history-index =====
|
|
197
103
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
104
|
+
async function getProjects(config) {
|
|
105
|
+
const indexed = await idxListProjects('claude');
|
|
106
|
+
return indexed.map(p => p.name);
|
|
107
|
+
}
|
|
201
108
|
|
|
202
|
-
|
|
203
|
-
return
|
|
204
|
-
.filter(entry => entry.isDirectory())
|
|
205
|
-
.map(entry => entry.name);
|
|
109
|
+
async function getProjectsWithStats(config, options = {}) {
|
|
110
|
+
return idxListProjects('claude');
|
|
206
111
|
}
|
|
207
112
|
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
// Windows: "C--Users-admin-Desktop-project" -> "C:\Users\admin\Desktop\project"
|
|
113
|
+
// ===== Path resolution helpers (kept for mutation/conversion paths) =====
|
|
114
|
+
|
|
211
115
|
function parseRealProjectPath(encodedName) {
|
|
212
116
|
const isWindows = process.platform === 'win32';
|
|
213
117
|
const fallbackFromSessions = tryResolvePathFromSessions(encodedName);
|
|
214
|
-
|
|
215
|
-
// Detect Windows drive letter (e.g., "C--Users-admin")
|
|
216
118
|
const windowsDriveMatch = encodedName.match(/^([A-Z])--(.+)$/);
|
|
217
119
|
|
|
218
120
|
if (isWindows && windowsDriveMatch) {
|
|
219
|
-
// Windows path with drive letter
|
|
220
121
|
const driveLetter = windowsDriveMatch[1];
|
|
221
122
|
const restPath = windowsDriveMatch[2];
|
|
222
|
-
|
|
223
|
-
// Split by '-' to get segments
|
|
224
123
|
const segments = restPath.split('-').filter(s => s);
|
|
225
|
-
|
|
226
|
-
// Build path from left to right, checking existence
|
|
227
124
|
let realSegments = [];
|
|
228
125
|
let accumulated = '';
|
|
229
126
|
let currentPath = '';
|
|
230
127
|
|
|
231
128
|
for (let i = 0; i < segments.length; i++) {
|
|
232
|
-
if (accumulated)
|
|
233
|
-
|
|
234
|
-
} else {
|
|
235
|
-
accumulated = segments[i];
|
|
236
|
-
}
|
|
237
|
-
|
|
129
|
+
if (accumulated) accumulated += '-' + segments[i];
|
|
130
|
+
else accumulated = segments[i];
|
|
238
131
|
const testPath = driveLetter + ':\\' + realSegments.concat(accumulated).join('\\');
|
|
239
|
-
|
|
240
|
-
// Check if this path exists
|
|
241
132
|
let found = fs.existsSync(testPath);
|
|
242
133
|
let finalAccumulated = accumulated;
|
|
243
|
-
|
|
244
|
-
// If not found with dash, try with underscore
|
|
245
134
|
if (!found && accumulated.includes('-')) {
|
|
246
135
|
const withUnderscore = accumulated.replace(/-/g, '_');
|
|
247
136
|
const testPathUnderscore = driveLetter + ':\\' + realSegments.concat(withUnderscore).join('\\');
|
|
248
|
-
if (fs.existsSync(testPathUnderscore)) {
|
|
249
|
-
finalAccumulated = withUnderscore;
|
|
250
|
-
found = true;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (found) {
|
|
255
|
-
realSegments.push(finalAccumulated);
|
|
256
|
-
accumulated = '';
|
|
257
|
-
currentPath = driveLetter + ':\\' + realSegments.join('\\');
|
|
137
|
+
if (fs.existsSync(testPathUnderscore)) { finalAccumulated = withUnderscore; found = true; }
|
|
258
138
|
}
|
|
139
|
+
if (found) { realSegments.push(finalAccumulated); accumulated = ''; currentPath = driveLetter + ':\\' + realSegments.join('\\'); }
|
|
259
140
|
}
|
|
260
141
|
|
|
261
|
-
// If there's remaining accumulated segment, try underscore variant
|
|
262
142
|
if (accumulated) {
|
|
263
143
|
let finalAccumulated = accumulated;
|
|
264
144
|
if (accumulated.includes('-')) {
|
|
265
145
|
const withUnderscore = accumulated.replace(/-/g, '_');
|
|
266
146
|
const testPath = driveLetter + ':\\' + realSegments.concat(withUnderscore).join('\\');
|
|
267
|
-
if (fs.existsSync(testPath))
|
|
268
|
-
finalAccumulated = withUnderscore;
|
|
269
|
-
}
|
|
147
|
+
if (fs.existsSync(testPath)) finalAccumulated = withUnderscore;
|
|
270
148
|
}
|
|
271
149
|
realSegments.push(finalAccumulated);
|
|
272
150
|
currentPath = driveLetter + ':\\' + realSegments.join('\\');
|
|
@@ -276,94 +154,60 @@ function parseRealProjectPath(encodedName) {
|
|
|
276
154
|
fullPath: validateProjectPath(currentPath) || fallbackFromSessions?.fullPath || (driveLetter + ':\\' + restPath.replace(/-/g, '\\')),
|
|
277
155
|
projectName: fallbackFromSessions?.projectName || realSegments[realSegments.length - 1] || encodedName
|
|
278
156
|
};
|
|
279
|
-
}
|
|
280
|
-
// Unix-like path (macOS/Linux) or fallback
|
|
281
|
-
const pathStr = encodedName.replace(/^-/, '/').replace(/-/g, '/');
|
|
282
|
-
const segments = pathStr.split('/').filter(s => s);
|
|
283
|
-
|
|
284
|
-
// Build path from left to right, checking existence
|
|
285
|
-
let currentPath = '';
|
|
286
|
-
const realSegments = [];
|
|
287
|
-
let accumulated = '';
|
|
288
|
-
|
|
289
|
-
for (let i = 0; i < segments.length; i++) {
|
|
290
|
-
if (accumulated) {
|
|
291
|
-
accumulated += '-' + segments[i];
|
|
292
|
-
} else {
|
|
293
|
-
accumulated = segments[i];
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const testPath = '/' + realSegments.concat(accumulated).join('/');
|
|
297
|
-
|
|
298
|
-
// Check if this path exists
|
|
299
|
-
let found = fs.existsSync(testPath);
|
|
300
|
-
let finalAccumulated = accumulated;
|
|
157
|
+
}
|
|
301
158
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
finalAccumulated = withUnderscore;
|
|
308
|
-
found = true;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
159
|
+
const pathStr = encodedName.replace(/^-/, '/').replace(/-/g, '/');
|
|
160
|
+
const segments = pathStr.split('/').filter(s => s);
|
|
161
|
+
let currentPath = '';
|
|
162
|
+
const realSegments = [];
|
|
163
|
+
let accumulated = '';
|
|
311
164
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
165
|
+
for (let i = 0; i < segments.length; i++) {
|
|
166
|
+
if (accumulated) accumulated += '-' + segments[i];
|
|
167
|
+
else accumulated = segments[i];
|
|
168
|
+
const testPath = '/' + realSegments.concat(accumulated).join('/');
|
|
169
|
+
let found = fs.existsSync(testPath);
|
|
170
|
+
let finalAccumulated = accumulated;
|
|
171
|
+
if (!found && accumulated.includes('-')) {
|
|
172
|
+
const withUnderscore = accumulated.replace(/-/g, '_');
|
|
173
|
+
const testPathUnderscore = '/' + realSegments.concat(withUnderscore).join('/');
|
|
174
|
+
if (fs.existsSync(testPathUnderscore)) { finalAccumulated = withUnderscore; found = true; }
|
|
317
175
|
}
|
|
176
|
+
if (found) { realSegments.push(finalAccumulated); accumulated = ''; currentPath = '/' + realSegments.join('/'); }
|
|
177
|
+
}
|
|
318
178
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (fs.existsSync(testPath)) {
|
|
326
|
-
finalAccumulated = withUnderscore;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
realSegments.push(finalAccumulated);
|
|
330
|
-
currentPath = '/' + realSegments.join('/');
|
|
179
|
+
if (accumulated) {
|
|
180
|
+
let finalAccumulated = accumulated;
|
|
181
|
+
if (accumulated.includes('-')) {
|
|
182
|
+
const withUnderscore = accumulated.replace(/-/g, '_');
|
|
183
|
+
const testPath = '/' + realSegments.concat(withUnderscore).join('/');
|
|
184
|
+
if (fs.existsSync(testPath)) finalAccumulated = withUnderscore;
|
|
331
185
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
fullPath: validateProjectPath(currentPath) || fallbackFromSessions?.fullPath || pathStr,
|
|
335
|
-
projectName: fallbackFromSessions?.projectName || realSegments[realSegments.length - 1] || encodedName
|
|
336
|
-
};
|
|
186
|
+
realSegments.push(finalAccumulated);
|
|
187
|
+
currentPath = '/' + realSegments.join('/');
|
|
337
188
|
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
fullPath: validateProjectPath(currentPath) || fallbackFromSessions?.fullPath || pathStr,
|
|
192
|
+
projectName: fallbackFromSessions?.projectName || realSegments[realSegments.length - 1] || encodedName
|
|
193
|
+
};
|
|
338
194
|
}
|
|
339
195
|
|
|
340
196
|
function validateProjectPath(candidatePath) {
|
|
341
|
-
|
|
342
|
-
return candidatePath;
|
|
343
|
-
}
|
|
344
|
-
return null;
|
|
197
|
+
return (candidatePath && fs.existsSync(candidatePath)) ? candidatePath : null;
|
|
345
198
|
}
|
|
346
199
|
|
|
347
200
|
function tryResolvePathFromSessions(encodedName) {
|
|
348
201
|
try {
|
|
349
202
|
const projectDir = path.join(CLAUDE_PROJECTS_DIR, encodedName);
|
|
350
|
-
if (!fs.existsSync(projectDir))
|
|
351
|
-
return null;
|
|
352
|
-
}
|
|
203
|
+
if (!fs.existsSync(projectDir)) return null;
|
|
353
204
|
const files = fs.readdirSync(projectDir).filter(f => f.endsWith('.jsonl'));
|
|
354
205
|
for (const file of files) {
|
|
355
206
|
const sessionFile = path.join(projectDir, file);
|
|
356
207
|
const cwd = extractCwdFromSessionHeader(sessionFile);
|
|
357
|
-
if (cwd && fs.existsSync(cwd)) {
|
|
358
|
-
return {
|
|
359
|
-
fullPath: cwd,
|
|
360
|
-
projectName: path.basename(cwd)
|
|
361
|
-
};
|
|
362
|
-
}
|
|
208
|
+
if (cwd && fs.existsSync(cwd)) return { fullPath: cwd, projectName: path.basename(cwd) };
|
|
363
209
|
}
|
|
364
|
-
} catch (
|
|
365
|
-
// ignore fallback errors
|
|
366
|
-
}
|
|
210
|
+
} catch (_) {}
|
|
367
211
|
return null;
|
|
368
212
|
}
|
|
369
213
|
|
|
@@ -374,637 +218,186 @@ function extractCwdFromSessionHeader(sessionFile) {
|
|
|
374
218
|
const bytesRead = fs.readSync(fd, buffer, 0, 4096, 0);
|
|
375
219
|
fs.closeSync(fd);
|
|
376
220
|
const content = buffer.slice(0, bytesRead).toString('utf8');
|
|
377
|
-
const
|
|
378
|
-
for (const line of lines) {
|
|
221
|
+
for (const line of content.split('\n')) {
|
|
379
222
|
if (!line.trim()) continue;
|
|
380
|
-
try {
|
|
381
|
-
const json = JSON.parse(line);
|
|
382
|
-
if (json.cwd && typeof json.cwd === 'string') {
|
|
383
|
-
return json.cwd;
|
|
384
|
-
}
|
|
385
|
-
} catch (e) {
|
|
386
|
-
// ignore
|
|
387
|
-
}
|
|
223
|
+
try { const json = JSON.parse(line); if (json.cwd && typeof json.cwd === 'string') return json.cwd; } catch (_) {}
|
|
388
224
|
}
|
|
389
|
-
} catch (
|
|
390
|
-
// ignore
|
|
391
|
-
}
|
|
225
|
+
} catch (_) {}
|
|
392
226
|
return null;
|
|
393
227
|
}
|
|
394
228
|
|
|
395
|
-
//
|
|
396
|
-
async function getProjectsWithStats(config, options = {}) {
|
|
397
|
-
const resolvedConfig = withResolvedProjectsDir(config);
|
|
398
|
-
if (!options.force) {
|
|
399
|
-
// Check enhanced cache first
|
|
400
|
-
const cacheKey = `${CacheKeys.PROJECTS}${resolvedConfig.projectsDir}`;
|
|
401
|
-
const enhancedCached = globalCache.get(cacheKey);
|
|
402
|
-
if (enhancedCached) {
|
|
403
|
-
return enhancedCached;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// Check old cache
|
|
407
|
-
const cached = getCachedProjects(resolvedConfig);
|
|
408
|
-
if (cached) {
|
|
409
|
-
globalCache.set(cacheKey, cached, 300000); // 5分钟
|
|
410
|
-
return cached;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
229
|
+
// ===== Counts: delegates to index =====
|
|
413
230
|
|
|
231
|
+
async function getProjectAndSessionCounts(config) {
|
|
414
232
|
try {
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
setCachedProjects(resolvedConfig, data);
|
|
421
|
-
globalCache.set(`${CacheKeys.PROJECTS}${resolvedConfig.projectsDir}`, data, 300000);
|
|
422
|
-
return data;
|
|
423
|
-
} catch (err) {
|
|
424
|
-
console.error(`[getProjectsWithStats] Failed to build projects for ${resolvedConfig.projectsDir}:`, err);
|
|
425
|
-
return [];
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
async function buildProjectsWithStats(config) {
|
|
430
|
-
const projectsDir = resolveProjectsDir(config);
|
|
431
|
-
|
|
432
|
-
if (!fs.existsSync(projectsDir)) {
|
|
433
|
-
return [];
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
const entries = await fs.promises.readdir(projectsDir, { withFileTypes: true });
|
|
437
|
-
|
|
438
|
-
// Process all projects concurrently
|
|
439
|
-
const projectPromises = entries
|
|
440
|
-
.filter(entry => entry.isDirectory())
|
|
441
|
-
.map(async (entry) => {
|
|
442
|
-
const projectName = entry.name;
|
|
443
|
-
const projectPath = path.join(projectsDir, projectName);
|
|
444
|
-
|
|
445
|
-
// Parse real project path
|
|
446
|
-
const { fullPath, projectName: displayName } = parseRealProjectPath(projectName);
|
|
447
|
-
|
|
448
|
-
// Get session files (only count sessions with actual messages)
|
|
449
|
-
let sessionCount = 0;
|
|
450
|
-
let lastUsed = null;
|
|
451
|
-
|
|
452
|
-
try {
|
|
453
|
-
const files = await fs.promises.readdir(projectPath);
|
|
454
|
-
const jsonlFiles = files.filter(f => f.endsWith('.jsonl') && !f.startsWith('agent-'));
|
|
455
|
-
|
|
456
|
-
// Filter: only count sessions that have actual messages (in parallel)
|
|
457
|
-
const sessionChecks = await Promise.all(
|
|
458
|
-
jsonlFiles.map(async (f) => {
|
|
459
|
-
const filePath = path.join(projectPath, f);
|
|
460
|
-
const hasMessages = await hasActualMessages(filePath);
|
|
461
|
-
return hasMessages ? f : null;
|
|
462
|
-
})
|
|
463
|
-
);
|
|
464
|
-
|
|
465
|
-
const sessionFilesWithMessages = sessionChecks.filter(f => f !== null);
|
|
466
|
-
sessionCount = sessionFilesWithMessages.length;
|
|
467
|
-
|
|
468
|
-
// Find most recent session (only from sessions with messages)
|
|
469
|
-
if (sessionFilesWithMessages.length > 0) {
|
|
470
|
-
const statPromises = sessionFilesWithMessages.map(async (f) => {
|
|
471
|
-
const filePath = path.join(projectPath, f);
|
|
472
|
-
const stat = await fs.promises.stat(filePath);
|
|
473
|
-
return stat.mtime.getTime();
|
|
474
|
-
});
|
|
475
|
-
const stats = await Promise.all(statPromises);
|
|
476
|
-
lastUsed = Math.max(...stats);
|
|
477
|
-
}
|
|
478
|
-
} catch (err) {
|
|
479
|
-
// Ignore errors
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
return {
|
|
483
|
-
name: projectName, // Keep encoded name for API operations
|
|
484
|
-
displayName, // Project name for display
|
|
485
|
-
fullPath, // Real full path for display
|
|
486
|
-
sessionCount,
|
|
487
|
-
lastUsed
|
|
488
|
-
};
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
const projects = await Promise.all(projectPromises);
|
|
492
|
-
return projects.sort((a, b) => (b.lastUsed || 0) - (a.lastUsed || 0)); // Sort by last used
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// 获取 Claude 项目/会话数量(轻量统计)
|
|
496
|
-
function getProjectAndSessionCounts(config) {
|
|
497
|
-
const projectsDir = resolveProjectsDir(config);
|
|
498
|
-
if (!fs.existsSync(projectsDir)) {
|
|
233
|
+
const projects = await idxListProjects('claude');
|
|
234
|
+
let sessionCount = 0;
|
|
235
|
+
for (const p of projects) sessionCount += p.sessionCount || 0;
|
|
236
|
+
return { projectCount: projects.length, sessionCount };
|
|
237
|
+
} catch (_) {
|
|
499
238
|
return { projectCount: 0, sessionCount: 0 };
|
|
500
239
|
}
|
|
501
|
-
|
|
502
|
-
let projectCount = 0;
|
|
503
|
-
let sessionCount = 0;
|
|
504
|
-
|
|
505
|
-
const entries = fs.readdirSync(projectsDir, { withFileTypes: true });
|
|
506
|
-
entries.forEach((entry) => {
|
|
507
|
-
if (!entry.isDirectory()) {
|
|
508
|
-
return;
|
|
509
|
-
}
|
|
510
|
-
projectCount += 1;
|
|
511
|
-
const projectPath = path.join(projectsDir, entry.name);
|
|
512
|
-
try {
|
|
513
|
-
const files = fs.readdirSync(projectPath);
|
|
514
|
-
sessionCount += files.filter(file => file.endsWith('.jsonl') && !file.startsWith('agent-')).length;
|
|
515
|
-
} catch (err) {
|
|
516
|
-
// 忽略单个项目的读取错误
|
|
517
|
-
}
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
return { projectCount, sessionCount };
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// Check if a session file has actual messages (async with enhanced caching)
|
|
524
|
-
async function hasActualMessages(filePath) {
|
|
525
|
-
try {
|
|
526
|
-
const stats = await fs.promises.stat(filePath);
|
|
527
|
-
|
|
528
|
-
// Check enhanced cache first
|
|
529
|
-
const cacheKey = `${CacheKeys.HAS_MESSAGES}${filePath}:${stats.mtime.getTime()}`;
|
|
530
|
-
const cached = globalCache.get(cacheKey);
|
|
531
|
-
if (typeof cached === 'boolean') {
|
|
532
|
-
return cached;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
// Check old cache mechanism
|
|
536
|
-
const oldCached = checkHasMessagesCache(filePath, stats);
|
|
537
|
-
if (typeof oldCached === 'boolean') {
|
|
538
|
-
globalCache.set(cacheKey, oldCached, 600000); // 10分钟
|
|
539
|
-
return oldCached;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const result = await scanSessionFileForMessagesAsync(filePath);
|
|
543
|
-
globalCache.set(cacheKey, result, 600000);
|
|
544
|
-
rememberHasMessages(filePath, stats, result);
|
|
545
|
-
return result;
|
|
546
|
-
} catch (err) {
|
|
547
|
-
return false;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function scanSessionFileForMessages(filePath) {
|
|
552
|
-
let fd = null;
|
|
553
|
-
try {
|
|
554
|
-
fd = fs.openSync(filePath, 'r');
|
|
555
|
-
const bufferSize = 64 * 1024;
|
|
556
|
-
const buffer = Buffer.alloc(bufferSize);
|
|
557
|
-
const pattern = /"type"\s*:\s*"(user|assistant|summary)"/;
|
|
558
|
-
let leftover = '';
|
|
559
|
-
let bytesRead;
|
|
560
|
-
|
|
561
|
-
while ((bytesRead = fs.readSync(fd, buffer, 0, bufferSize, null)) > 0) {
|
|
562
|
-
const chunk = buffer.toString('utf8', 0, bytesRead);
|
|
563
|
-
const combined = leftover + chunk;
|
|
564
|
-
if (pattern.test(combined)) {
|
|
565
|
-
fs.closeSync(fd);
|
|
566
|
-
return true;
|
|
567
|
-
}
|
|
568
|
-
leftover = combined.slice(-64);
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
fs.closeSync(fd);
|
|
572
|
-
return false;
|
|
573
|
-
} catch (err) {
|
|
574
|
-
if (fd) {
|
|
575
|
-
try {
|
|
576
|
-
fs.closeSync(fd);
|
|
577
|
-
} catch (e) {
|
|
578
|
-
// ignore
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
return false;
|
|
582
|
-
}
|
|
583
240
|
}
|
|
584
241
|
|
|
585
|
-
//
|
|
586
|
-
function scanSessionFileForMessagesAsync(filePath) {
|
|
587
|
-
return new Promise((resolve) => {
|
|
588
|
-
const stream = fs.createReadStream(filePath, { encoding: 'utf8', highWaterMark: 64 * 1024 });
|
|
589
|
-
const pattern = /"type"\s*:\s*"(user|assistant|summary)"/;
|
|
590
|
-
let found = false;
|
|
591
|
-
let leftover = '';
|
|
592
|
-
|
|
593
|
-
stream.on('data', (chunk) => {
|
|
594
|
-
if (found) return;
|
|
595
|
-
const combined = leftover + chunk;
|
|
596
|
-
if (pattern.test(combined)) {
|
|
597
|
-
found = true;
|
|
598
|
-
stream.destroy();
|
|
599
|
-
resolve(true);
|
|
600
|
-
}
|
|
601
|
-
leftover = combined.slice(-64);
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
stream.on('end', () => {
|
|
605
|
-
if (!found) resolve(false);
|
|
606
|
-
});
|
|
242
|
+
// ===== Sessions for project: index + saved order + fork relations =====
|
|
607
243
|
|
|
608
|
-
stream.on('error', () => {
|
|
609
|
-
resolve(false);
|
|
610
|
-
});
|
|
611
|
-
});
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
// Get sessions for a project - async version
|
|
615
244
|
async function getSessionsForProject(config, projectName, options = {}) {
|
|
616
|
-
|
|
617
|
-
const cacheKey = `${CacheKeys.SESSIONS}${projectName}`;
|
|
618
|
-
if (!options.force) {
|
|
619
|
-
const cached = globalCache.get(cacheKey);
|
|
620
|
-
if (cached) {
|
|
621
|
-
return cached;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const projectConfig = { ...config, currentProject: projectName };
|
|
626
|
-
const sessions = getAllSessions(projectConfig);
|
|
245
|
+
const indexed = await idxListSessions('claude', projectName);
|
|
627
246
|
const forkRelations = getForkRelations();
|
|
628
247
|
const savedOrder = getSessionOrder(projectName);
|
|
629
248
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
);
|
|
639
|
-
|
|
640
|
-
const validSessions = sessionChecks.filter(s => s !== null);
|
|
249
|
+
let sessions = indexed.map(s => ({
|
|
250
|
+
sessionId: s.sessionId,
|
|
251
|
+
mtime: s.mtime,
|
|
252
|
+
size: s.size,
|
|
253
|
+
filePath: s.filePath,
|
|
254
|
+
gitBranch: s.gitBranch || null,
|
|
255
|
+
firstMessage: s.firstMessage || null,
|
|
256
|
+
forkedFrom: forkRelations[s.sessionId] || null
|
|
257
|
+
}));
|
|
641
258
|
|
|
642
|
-
const sessionsWithInfo = validSessions.map(session => {
|
|
643
|
-
const info = parseSessionInfoFast(session.filePath);
|
|
644
|
-
totalSize += session.size || 0;
|
|
645
|
-
return {
|
|
646
|
-
sessionId: session.sessionId,
|
|
647
|
-
mtime: session.mtime,
|
|
648
|
-
size: session.size,
|
|
649
|
-
filePath: session.filePath,
|
|
650
|
-
gitBranch: info.gitBranch || null,
|
|
651
|
-
firstMessage: info.firstMessage || null,
|
|
652
|
-
forkedFrom: forkRelations[session.sessionId] || null
|
|
653
|
-
};
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
// Apply saved order if exists
|
|
657
|
-
let orderedSessions = sessionsWithInfo;
|
|
658
259
|
if (savedOrder.length > 0) {
|
|
260
|
+
const sm = new Map(sessions.map(s => [s.sessionId, s]));
|
|
659
261
|
const ordered = [];
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
// Add sessions in saved order
|
|
663
|
-
for (const sessionId of savedOrder) {
|
|
664
|
-
if (sessionMap.has(sessionId)) {
|
|
665
|
-
ordered.push(sessionMap.get(sessionId));
|
|
666
|
-
sessionMap.delete(sessionId);
|
|
667
|
-
}
|
|
262
|
+
for (const sid of savedOrder) {
|
|
263
|
+
if (sm.has(sid)) { ordered.push(sm.get(sid)); sm.delete(sid); }
|
|
668
264
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
ordered.push(...sessionMap.values());
|
|
672
|
-
orderedSessions = ordered;
|
|
265
|
+
ordered.push(...sm.values());
|
|
266
|
+
sessions = ordered;
|
|
673
267
|
}
|
|
674
268
|
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
totalSize
|
|
678
|
-
};
|
|
679
|
-
|
|
680
|
-
// Cache for 2 minutes
|
|
681
|
-
globalCache.set(cacheKey, result, 120000);
|
|
682
|
-
return result;
|
|
269
|
+
const totalSize = sessions.reduce((sum, s) => sum + (s.size || 0), 0);
|
|
270
|
+
return { sessions, totalSize };
|
|
683
271
|
}
|
|
684
272
|
|
|
685
|
-
//
|
|
273
|
+
// ===== Mutations (sync) =====
|
|
274
|
+
|
|
686
275
|
function deleteSession(config, projectName, sessionId) {
|
|
687
276
|
const resolvedConfig = withResolvedProjectsDir(config);
|
|
688
277
|
const projectDir = path.join(resolvedConfig.projectsDir, projectName);
|
|
689
|
-
const
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
throw new Error('Session not found');
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
fs.unlinkSync(sessionFile);
|
|
696
|
-
invalidateProjectsCache(resolvedConfig);
|
|
278
|
+
const filePath = path.join(projectDir, sessionId + '.jsonl');
|
|
279
|
+
if (!fs.existsSync(filePath)) throw new Error('Session not found');
|
|
280
|
+
fs.unlinkSync(filePath);
|
|
697
281
|
invalidateSessionResultCache(projectName);
|
|
698
282
|
return { success: true };
|
|
699
283
|
}
|
|
700
284
|
|
|
701
|
-
// Fork a session
|
|
702
285
|
function forkSession(config, projectName, sessionId, options = {}) {
|
|
703
286
|
const resolvedConfig = withResolvedProjectsDir(config);
|
|
704
287
|
const projectDir = path.join(resolvedConfig.projectsDir, projectName);
|
|
705
|
-
const
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
288
|
+
const sourcePath = path.join(projectDir, sessionId + '.jsonl');
|
|
289
|
+
if (!fs.existsSync(sourcePath)) throw new Error('Session not found');
|
|
290
|
+
const forkId = crypto.randomUUID();
|
|
291
|
+
const targetPath = path.join(projectDir, forkId + '.jsonl');
|
|
292
|
+
let content;
|
|
293
|
+
if (options.afterUserMessageNumber && Number.isInteger(options.afterUserMessageNumber) && options.afterUserMessageNumber > 0) {
|
|
294
|
+
content = sliceClaudeSessionContentByUserMessage(fs.readFileSync(sourcePath, 'utf8'), options.afterUserMessageNumber);
|
|
295
|
+
} else {
|
|
296
|
+
content = fs.readFileSync(sourcePath, 'utf8');
|
|
709
297
|
}
|
|
710
|
-
|
|
711
|
-
// Read the original session
|
|
712
|
-
const originalContent = fs.readFileSync(sessionFile, 'utf8');
|
|
713
|
-
const content = sliceClaudeSessionContentByUserMessage(
|
|
714
|
-
originalContent,
|
|
715
|
-
options.afterUserMessageNumber
|
|
716
|
-
);
|
|
717
|
-
|
|
718
|
-
// Generate new session ID (UUID v4)
|
|
719
|
-
const newSessionId = crypto.randomUUID();
|
|
720
|
-
const newSessionFile = path.join(projectDir, newSessionId + '.jsonl');
|
|
721
|
-
|
|
722
|
-
// Write to new file
|
|
723
|
-
fs.writeFileSync(newSessionFile, content, 'utf8');
|
|
724
|
-
|
|
725
|
-
// Save fork relation
|
|
298
|
+
fs.writeFileSync(targetPath, content, 'utf8');
|
|
726
299
|
const forkRelations = getForkRelations();
|
|
727
|
-
forkRelations[
|
|
300
|
+
forkRelations[forkId] = sessionId;
|
|
728
301
|
saveForkRelations(forkRelations);
|
|
729
|
-
if (options.alias)
|
|
730
|
-
const { setAlias } = require('./alias');
|
|
731
|
-
setAlias(newSessionId, options.alias);
|
|
732
|
-
}
|
|
733
|
-
invalidateProjectsCache(resolvedConfig);
|
|
302
|
+
if (options.alias) setAlias(forkId, options.alias);
|
|
734
303
|
invalidateSessionResultCache(projectName);
|
|
735
|
-
|
|
736
304
|
return {
|
|
737
|
-
newSessionId,
|
|
305
|
+
newSessionId: forkId,
|
|
738
306
|
forkedFrom: sessionId,
|
|
739
307
|
alias: options.alias || null,
|
|
740
308
|
afterUserMessageNumber: options.afterUserMessageNumber || null
|
|
741
309
|
};
|
|
742
310
|
}
|
|
743
311
|
|
|
744
|
-
// Get session order for a project
|
|
745
312
|
function getSessionOrder(projectName) {
|
|
746
313
|
const orderFile = getSessionOrderFilePath();
|
|
747
314
|
try {
|
|
748
315
|
if (fs.existsSync(orderFile)) {
|
|
749
|
-
const data = fs.readFileSync(orderFile, 'utf8');
|
|
750
|
-
|
|
751
|
-
return allOrders[projectName] || [];
|
|
316
|
+
const data = JSON.parse(fs.readFileSync(orderFile, 'utf8'));
|
|
317
|
+
return data[projectName] || [];
|
|
752
318
|
}
|
|
753
|
-
} catch (
|
|
754
|
-
// Ignore errors
|
|
755
|
-
}
|
|
319
|
+
} catch (_) {}
|
|
756
320
|
return [];
|
|
757
321
|
}
|
|
758
322
|
|
|
759
|
-
// Save session order for a project
|
|
760
323
|
function saveSessionOrder(projectName, order) {
|
|
761
324
|
const orderFile = getSessionOrderFilePath();
|
|
762
325
|
const dir = path.dirname(orderFile);
|
|
763
|
-
if (!fs.existsSync(dir)) {
|
|
764
|
-
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
let allOrders = {};
|
|
769
|
-
try {
|
|
770
|
-
if (fs.existsSync(orderFile)) {
|
|
771
|
-
const data = fs.readFileSync(orderFile, 'utf8');
|
|
772
|
-
allOrders = JSON.parse(data);
|
|
773
|
-
}
|
|
774
|
-
} catch (err) {
|
|
775
|
-
// Ignore errors
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
// Update order for this project
|
|
779
|
-
allOrders[projectName] = order;
|
|
780
|
-
fs.writeFileSync(orderFile, JSON.stringify(allOrders, null, 2), 'utf8');
|
|
326
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
327
|
+
let data = {};
|
|
328
|
+
try { if (fs.existsSync(orderFile)) data = JSON.parse(fs.readFileSync(orderFile, 'utf8')); } catch (_) {}
|
|
329
|
+
data[projectName] = order;
|
|
330
|
+
fs.writeFileSync(orderFile, JSON.stringify(data, null, 2), 'utf8');
|
|
781
331
|
}
|
|
782
332
|
|
|
783
|
-
// Delete a project (remove the entire project directory)
|
|
784
333
|
function deleteProject(config, projectName) {
|
|
785
334
|
const resolvedConfig = withResolvedProjectsDir(config);
|
|
786
335
|
const projectDir = path.join(resolvedConfig.projectsDir, projectName);
|
|
787
|
-
|
|
788
|
-
if (!fs.existsSync(projectDir)) {
|
|
789
|
-
throw new Error('Project not found');
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// Recursively delete the directory
|
|
336
|
+
if (!fs.existsSync(projectDir)) throw new Error('Project not found');
|
|
793
337
|
fs.rmSync(projectDir, { recursive: true, force: true });
|
|
794
|
-
|
|
795
|
-
// Remove from order file if exists
|
|
796
338
|
const order = getProjectOrder(resolvedConfig);
|
|
797
339
|
const newOrder = order.filter(name => name !== projectName);
|
|
798
|
-
if (newOrder.length !== order.length)
|
|
799
|
-
saveProjectOrder(resolvedConfig, newOrder);
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
invalidateProjectsCache(resolvedConfig);
|
|
340
|
+
if (newOrder.length !== order.length) saveProjectOrder(resolvedConfig, newOrder);
|
|
803
341
|
return { success: true };
|
|
804
342
|
}
|
|
805
343
|
|
|
806
|
-
// Search
|
|
807
|
-
function searchSessions(config, projectName, keyword, contextLength = 15) {
|
|
808
|
-
const resolvedConfig = withResolvedProjectsDir(config);
|
|
809
|
-
const projectDir = path.join(resolvedConfig.projectsDir, projectName);
|
|
810
|
-
|
|
811
|
-
if (!fs.existsSync(projectDir)) {
|
|
812
|
-
return [];
|
|
813
|
-
}
|
|
344
|
+
// ===== Search: delegates to index =====
|
|
814
345
|
|
|
815
|
-
|
|
816
|
-
const
|
|
817
|
-
const jsonlFiles = files.filter(f => f.endsWith('.jsonl') && !f.startsWith('agent-'));
|
|
346
|
+
async function searchSessions(config, projectName, keyword, contextLength = 15) {
|
|
347
|
+
const results = await idxSearch('claude', keyword, { projectName, contextLength });
|
|
818
348
|
const aliases = loadAliases();
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
if (!hasActualMessages(filePath)) {
|
|
826
|
-
continue;
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
try {
|
|
830
|
-
const content = fs.readFileSync(filePath, 'utf8');
|
|
831
|
-
const lines = content.split('\n');
|
|
832
|
-
const matches = [];
|
|
833
|
-
|
|
834
|
-
for (const line of lines) {
|
|
835
|
-
if (!line.trim()) continue;
|
|
836
|
-
|
|
837
|
-
try {
|
|
838
|
-
const json = JSON.parse(line);
|
|
839
|
-
|
|
840
|
-
// Search in message content
|
|
841
|
-
if (json.message && json.message.content) {
|
|
842
|
-
const text = json.message.content;
|
|
843
|
-
const lowerText = text.toLowerCase();
|
|
844
|
-
const lowerKeyword = keyword.toLowerCase();
|
|
845
|
-
let index = 0;
|
|
846
|
-
|
|
847
|
-
while ((index = lowerText.indexOf(lowerKeyword, index)) !== -1) {
|
|
848
|
-
// Extract context
|
|
849
|
-
const start = Math.max(0, index - contextLength);
|
|
850
|
-
const end = Math.min(text.length, index + keyword.length + contextLength);
|
|
851
|
-
const context = text.substring(start, end);
|
|
852
|
-
|
|
853
|
-
matches.push({
|
|
854
|
-
role: json.message.role || 'unknown',
|
|
855
|
-
context: (start > 0 ? '...' : '') + context + (end < text.length ? '...' : ''),
|
|
856
|
-
position: index
|
|
857
|
-
});
|
|
858
|
-
|
|
859
|
-
index += keyword.length;
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
} catch (e) {
|
|
863
|
-
// Skip invalid JSON lines
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
if (matches.length > 0) {
|
|
868
|
-
results.push({
|
|
869
|
-
sessionId,
|
|
870
|
-
alias: aliases[sessionId] || null,
|
|
871
|
-
matchCount: matches.length,
|
|
872
|
-
matches: matches.slice(0, 5) // Limit to 5 matches per session
|
|
873
|
-
});
|
|
874
|
-
}
|
|
875
|
-
} catch (e) {
|
|
876
|
-
// Skip files that can't be read
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
// Sort by match count
|
|
881
|
-
results.sort((a, b) => b.matchCount - a.matchCount);
|
|
882
|
-
|
|
883
|
-
return results;
|
|
349
|
+
return results.map(r => ({
|
|
350
|
+
sessionId: r.sessionId,
|
|
351
|
+
alias: aliases[r.sessionId] || null,
|
|
352
|
+
matchCount: r.matchCount,
|
|
353
|
+
matches: r.matches
|
|
354
|
+
}));
|
|
884
355
|
}
|
|
885
356
|
|
|
886
|
-
// Get recent sessions across all projects
|
|
887
357
|
async function getRecentSessions(config, limit = 5) {
|
|
888
|
-
const
|
|
889
|
-
const allSessions = [];
|
|
890
|
-
const forkRelations = getForkRelations();
|
|
358
|
+
const indexed = await idxGetRecent('claude', limit);
|
|
891
359
|
const aliases = loadAliases();
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
allSessions.push({
|
|
907
|
-
sessionId: session.sessionId,
|
|
908
|
-
projectName: projectName,
|
|
909
|
-
projectDisplayName: displayName,
|
|
910
|
-
projectFullPath: fullPath,
|
|
911
|
-
mtime: session.mtime,
|
|
912
|
-
size: session.size,
|
|
913
|
-
filePath: session.filePath,
|
|
914
|
-
gitBranch: info.gitBranch || null,
|
|
915
|
-
firstMessage: info.firstMessage || null,
|
|
916
|
-
forkedFrom: forkRelations[session.sessionId] || null,
|
|
917
|
-
alias: aliases[session.sessionId] || null
|
|
918
|
-
});
|
|
919
|
-
});
|
|
920
|
-
});
|
|
921
|
-
|
|
922
|
-
// Sort by mtime descending (most recent first)
|
|
923
|
-
allSessions.sort((a, b) => b.mtime - a.mtime);
|
|
924
|
-
|
|
925
|
-
// Return top N sessions
|
|
926
|
-
return allSessions.slice(0, limit);
|
|
360
|
+
const forkRelations = getForkRelations();
|
|
361
|
+
return indexed.map(s => ({
|
|
362
|
+
sessionId: s.sessionId,
|
|
363
|
+
projectName: s.projectName,
|
|
364
|
+
projectDisplayName: s.projectDisplayName,
|
|
365
|
+
projectFullPath: s.projectFullPath,
|
|
366
|
+
mtime: s.mtime,
|
|
367
|
+
size: s.size,
|
|
368
|
+
filePath: s.filePath,
|
|
369
|
+
gitBranch: s.gitBranch || null,
|
|
370
|
+
firstMessage: s.firstMessage || null,
|
|
371
|
+
forkedFrom: forkRelations[s.sessionId] || null,
|
|
372
|
+
alias: aliases[s.sessionId] || null
|
|
373
|
+
}));
|
|
927
374
|
}
|
|
928
375
|
|
|
929
|
-
// Search sessions across all projects
|
|
930
376
|
async function searchSessionsAcrossProjects(config, keyword, contextLength = 35) {
|
|
931
377
|
const allResults = [];
|
|
932
378
|
|
|
933
379
|
try {
|
|
934
|
-
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
const projectResults = searchSessions(config, projectName, keyword, contextLength);
|
|
938
|
-
const { projectName: displayName, fullPath } = parseRealProjectPath(projectName);
|
|
939
|
-
|
|
940
|
-
// Add project info to each result
|
|
941
|
-
projectResults.forEach(result => {
|
|
942
|
-
allResults.push({
|
|
943
|
-
...result,
|
|
944
|
-
projectName: projectName,
|
|
945
|
-
projectDisplayName: displayName,
|
|
946
|
-
projectFullPath: fullPath,
|
|
947
|
-
channel: 'claude'
|
|
948
|
-
});
|
|
949
|
-
});
|
|
950
|
-
});
|
|
951
|
-
} catch (error) {
|
|
952
|
-
console.error('Error searching Claude projects:', error);
|
|
953
|
-
}
|
|
380
|
+
const claudeResults = await idxSearch('claude', keyword, { contextLength });
|
|
381
|
+
for (const r of claudeResults) allResults.push({ ...r, channel: 'claude' });
|
|
382
|
+
} catch (_) {}
|
|
954
383
|
|
|
955
384
|
try {
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
const
|
|
960
|
-
const codexProjects = await getProjects(codexConfig);
|
|
961
|
-
codexProjects.forEach(projectName => {
|
|
962
|
-
const projectResults = searchSessions(codexConfig, projectName, keyword, contextLength);
|
|
963
|
-
const { projectName: displayName, fullPath } = parseRealProjectPath(projectName);
|
|
964
|
-
|
|
965
|
-
projectResults.forEach(result => {
|
|
966
|
-
allResults.push({
|
|
967
|
-
...result,
|
|
968
|
-
projectName: projectName,
|
|
969
|
-
projectDisplayName: displayName,
|
|
970
|
-
projectFullPath: fullPath,
|
|
971
|
-
channel: 'codex'
|
|
972
|
-
});
|
|
973
|
-
});
|
|
974
|
-
});
|
|
385
|
+
if (fs.existsSync(CODEX_PROJECTS_DIR)) {
|
|
386
|
+
const { searchSessions: codexSearch } = require('./codex-sessions');
|
|
387
|
+
const codexResults = await codexSearch(keyword);
|
|
388
|
+
for (const r of codexResults) allResults.push({ ...r, channel: 'codex' });
|
|
975
389
|
}
|
|
976
|
-
} catch (
|
|
977
|
-
console.error('Error searching Codex projects:', error);
|
|
978
|
-
}
|
|
390
|
+
} catch (_) {}
|
|
979
391
|
|
|
980
392
|
try {
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
const
|
|
985
|
-
const geminiProjects = await getProjects(geminiConfig);
|
|
986
|
-
geminiProjects.forEach(projectName => {
|
|
987
|
-
const projectResults = searchSessions(geminiConfig, projectName, keyword, contextLength);
|
|
988
|
-
const { projectName: displayName, fullPath } = parseRealProjectPath(projectName);
|
|
989
|
-
|
|
990
|
-
projectResults.forEach(result => {
|
|
991
|
-
allResults.push({
|
|
992
|
-
...result,
|
|
993
|
-
projectName: projectName,
|
|
994
|
-
projectDisplayName: displayName,
|
|
995
|
-
projectFullPath: fullPath,
|
|
996
|
-
channel: 'gemini'
|
|
997
|
-
});
|
|
998
|
-
});
|
|
999
|
-
});
|
|
393
|
+
if (fs.existsSync(GEMINI_PROJECTS_DIR)) {
|
|
394
|
+
const { searchSessions: geminiSearch } = require('./gemini-sessions');
|
|
395
|
+
const geminiResults = await geminiSearch(keyword, contextLength);
|
|
396
|
+
for (const r of geminiResults) allResults.push({ ...r, channel: 'gemini' });
|
|
1000
397
|
}
|
|
1001
|
-
} catch (
|
|
1002
|
-
console.error('Error searching Gemini projects:', error);
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
// Sort by match count
|
|
1006
|
-
allResults.sort((a, b) => b.matchCount - a.matchCount);
|
|
398
|
+
} catch (_) {}
|
|
1007
399
|
|
|
400
|
+
allResults.sort((a, b) => (b.matchCount || 0) - (a.matchCount || 0));
|
|
1008
401
|
return allResults;
|
|
1009
402
|
}
|
|
1010
403
|
|
|
@@ -1025,6 +418,5 @@ module.exports = {
|
|
|
1025
418
|
searchSessionsAcrossProjects,
|
|
1026
419
|
getForkRelations,
|
|
1027
420
|
saveForkRelations,
|
|
1028
|
-
hasActualMessages,
|
|
1029
421
|
getProjectAndSessionCounts
|
|
1030
422
|
};
|