coding-tool-x 3.8.0 → 3.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -6
- package/package.json +2 -2
- 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/services/codex-sessions.js +41 -233
- package/src/server/services/dashboard-snapshot-worker.js +5 -5
- package/src/server/services/gemini-sessions.js +67 -157
- package/src/server/services/omp-sessions.js +102 -105
- 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 +98 -0
- package/src/server/services/session-snapshot-worker.js +7 -7
- package/src/server/services/sessions.js +167 -775
- package/src/server/services/sqlite-connection.js +78 -0
- package/src/server/services/workspace-service.js +3 -3
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> 面向 Claude Code、Codex CLI、Gemini CLI、OpenCode、OMP 的统一增强控制台
|
|
4
4
|
> Web UI + CLI + 多平台代理 + 配置托管 + 工作区编排 + 分析面板
|
|
5
5
|
|
|
6
|
-

|
|
7
7
|

|
|
8
8
|

|
|
9
9
|
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
补充说明:
|
|
87
87
|
|
|
88
88
|
- Codex Agents 目前仅支持用户级代理
|
|
89
|
-
- OpenCode
|
|
89
|
+
- OpenCode 会话读取使用内置 `node:sqlite`,不再依赖本机 sqlite3 可执行文件
|
|
90
90
|
- OMP 的原生资源轴包含 skills、commands、prompt templates、packages / extensions 和 MCP `mcp.json`;Agents 暂不作为独立可写配置文件管理
|
|
91
91
|
- OMP 平台键统一为 `omp`,不再保留 `omp` 平台键兼容
|
|
92
92
|
- OMP OAuth 不在 OAuth Credentials 抽屉中直接增删改;`auth.json` 仍随 Config Export / Import 的 native config 快照迁移
|
|
@@ -115,11 +115,9 @@ npm run build:web
|
|
|
115
115
|
npm link
|
|
116
116
|
```
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
- Node.js `>= 14.0.0`
|
|
118
|
+
- Node.js `>= 22.13.0`
|
|
121
119
|
- 建议至少运行过一次目标 CLI,以便生成原生配置目录
|
|
122
|
-
-
|
|
120
|
+
- OpenCode 会话使用内置 `node:sqlite`,不再需要系统 sqlite3
|
|
123
121
|
|
|
124
122
|
## 快速开始
|
|
125
123
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "coding-tool-x",
|
|
3
|
-
"version": "3.8.
|
|
3
|
+
"version": "3.8.1",
|
|
4
4
|
"description": "Vibe Coding 增强工作助手 - 智能会话管理、动态渠道切换、全局搜索、实时监控",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"author": "CooperJiang",
|
|
38
38
|
"license": "MIT",
|
|
39
39
|
"engines": {
|
|
40
|
-
"node": ">=
|
|
40
|
+
"node": ">=22.13.0"
|
|
41
41
|
},
|
|
42
42
|
"files": [
|
|
43
43
|
"bin/",
|
package/src/config/paths.js
CHANGED
|
@@ -439,6 +439,7 @@ const PATHS = {
|
|
|
439
439
|
sessionHasCache: path.join(CACHE_DIR, 'session-has-cache.json'),
|
|
440
440
|
channelModels: path.join(CACHE_DIR, 'channel-models.json'),
|
|
441
441
|
channelBalanceStrategies: path.join(CACHE_DIR, 'channel-balance-strategies.json'),
|
|
442
|
+
sessionHistoryIndex: path.join(CACHE_DIR, 'session-history.sqlite'),
|
|
442
443
|
envBackups: path.join(BACKUPS_DIR, 'env'),
|
|
443
444
|
proxyRuntime: {
|
|
444
445
|
claude: path.join(RUNTIME_DIR, 'claude-proxy.json'),
|
|
@@ -79,7 +79,7 @@ module.exports = (config) => {
|
|
|
79
79
|
* GET /api/codex/sessions/search/global?keyword=xxx
|
|
80
80
|
* 全局搜索
|
|
81
81
|
*/
|
|
82
|
-
router.get('/search/global', (req, res) => {
|
|
82
|
+
router.get('/search/global', async (req, res) => {
|
|
83
83
|
try {
|
|
84
84
|
if (!isCodexInstalled()) {
|
|
85
85
|
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
@@ -91,7 +91,7 @@ module.exports = (config) => {
|
|
|
91
91
|
return res.status(400).json({ error: 'Keyword is required' });
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
const results = searchSessions(keyword);
|
|
94
|
+
const results = await searchSessions(keyword);
|
|
95
95
|
|
|
96
96
|
// 按会话分组,统计每个会话的匹配数
|
|
97
97
|
const sessionMap = new Map();
|
|
@@ -132,14 +132,14 @@ module.exports = (config) => {
|
|
|
132
132
|
* GET /api/codex/sessions/recent/list?limit=10
|
|
133
133
|
* 获取最近会话
|
|
134
134
|
*/
|
|
135
|
-
router.get('/recent/list', (req, res) => {
|
|
135
|
+
router.get('/recent/list', async (req, res) => {
|
|
136
136
|
try {
|
|
137
137
|
if (!isCodexInstalled()) {
|
|
138
138
|
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
const limit = parseInt(req.query.limit) || 5;
|
|
142
|
-
const sessions = getRecentSessions(limit);
|
|
142
|
+
const sessions = await getRecentSessions(limit);
|
|
143
143
|
|
|
144
144
|
res.json({
|
|
145
145
|
sessions,
|
|
@@ -191,7 +191,7 @@ module.exports = (config) => {
|
|
|
191
191
|
* GET /api/codex/sessions/:projectName/search
|
|
192
192
|
* 项目级搜索
|
|
193
193
|
*/
|
|
194
|
-
router.get('/:projectName/search', (req, res) => {
|
|
194
|
+
router.get('/:projectName/search', async (req, res) => {
|
|
195
195
|
try {
|
|
196
196
|
if (!isCodexInstalled()) {
|
|
197
197
|
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
@@ -205,7 +205,7 @@ module.exports = (config) => {
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
// 使用全局搜索,然后过滤项目
|
|
208
|
-
const allResults = searchSessions(keyword);
|
|
208
|
+
const allResults = await searchSessions(keyword);
|
|
209
209
|
const filteredResults = allResults.filter(r => r.projectName === projectName);
|
|
210
210
|
|
|
211
211
|
// 按会话分组
|
|
@@ -242,14 +242,14 @@ module.exports = (config) => {
|
|
|
242
242
|
}
|
|
243
243
|
});
|
|
244
244
|
|
|
245
|
-
router.get('/:projectName/:sessionId/status', (req, res) => {
|
|
245
|
+
router.get('/:projectName/:sessionId/status', async (req, res) => {
|
|
246
246
|
try {
|
|
247
247
|
if (!isCodexInstalled()) {
|
|
248
248
|
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
249
249
|
}
|
|
250
250
|
|
|
251
251
|
const { sessionId } = req.params;
|
|
252
|
-
const session = getSessionById(sessionId);
|
|
252
|
+
const session = await getSessionById(sessionId);
|
|
253
253
|
if (!session || !session.filePath) {
|
|
254
254
|
return res.status(404).json({ error: 'Session not found' });
|
|
255
255
|
}
|
|
@@ -267,14 +267,14 @@ module.exports = (config) => {
|
|
|
267
267
|
}
|
|
268
268
|
});
|
|
269
269
|
|
|
270
|
-
router.get('/:projectName/:sessionId/outline', (req, res) => {
|
|
270
|
+
router.get('/:projectName/:sessionId/outline', async (req, res) => {
|
|
271
271
|
try {
|
|
272
272
|
if (!isCodexInstalled()) {
|
|
273
273
|
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
274
274
|
}
|
|
275
275
|
|
|
276
276
|
const { sessionId } = req.params;
|
|
277
|
-
const session = getSessionById(sessionId);
|
|
277
|
+
const session = await getSessionById(sessionId);
|
|
278
278
|
if (!session) {
|
|
279
279
|
return res.status(404).json({ error: 'Session not found' });
|
|
280
280
|
}
|
|
@@ -307,7 +307,7 @@ module.exports = (config) => {
|
|
|
307
307
|
* GET /api/codex/sessions/:projectName/:sessionId/messages
|
|
308
308
|
* 获取会话的消息列表
|
|
309
309
|
*/
|
|
310
|
-
router.get('/:projectName/:sessionId/messages', (req, res) => {
|
|
310
|
+
router.get('/:projectName/:sessionId/messages', async (req, res) => {
|
|
311
311
|
const startMs = Date.now();
|
|
312
312
|
try {
|
|
313
313
|
if (!isCodexInstalled()) {
|
|
@@ -318,7 +318,7 @@ module.exports = (config) => {
|
|
|
318
318
|
const { sessionId } = req.params;
|
|
319
319
|
const { page = 1, limit = 20, order = 'desc' } = req.query;
|
|
320
320
|
|
|
321
|
-
const session = getSessionById(sessionId);
|
|
321
|
+
const session = await getSessionById(sessionId);
|
|
322
322
|
|
|
323
323
|
if (!session) {
|
|
324
324
|
logPerf('GET /api/codex/sessions/:projectName/:sessionId/messages', startMs, `session=${sessionId}, not found`);
|
|
@@ -574,7 +574,7 @@ module.exports = (config) => {
|
|
|
574
574
|
* POST /api/codex/sessions/:projectName/:sessionId/launch
|
|
575
575
|
* 获取会话启动命令(用于复制)
|
|
576
576
|
*/
|
|
577
|
-
router.post('/:projectName/:sessionId/launch', (req, res) => {
|
|
577
|
+
router.post('/:projectName/:sessionId/launch', async (req, res) => {
|
|
578
578
|
try {
|
|
579
579
|
if (!isCodexInstalled()) {
|
|
580
580
|
return res.status(404).json({ error: 'Codex CLI not installed' });
|
|
@@ -584,7 +584,7 @@ module.exports = (config) => {
|
|
|
584
584
|
const fs = require('fs');
|
|
585
585
|
|
|
586
586
|
// 获取会话详情
|
|
587
|
-
const session = getSessionById(sessionId);
|
|
587
|
+
const session = await getSessionById(sessionId);
|
|
588
588
|
|
|
589
589
|
if (!session) {
|
|
590
590
|
return res.status(404).json({ error: 'Session not found' });
|
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
getProjectPath,
|
|
12
12
|
getAllSessions
|
|
13
13
|
} = require('../services/gemini-sessions');
|
|
14
|
+
const { getSessionStatus, getSessionOutline, getMessagePage } = require('../services/session-history-index');
|
|
14
15
|
const { isGeminiInstalled } = require('../services/gemini-config');
|
|
15
16
|
const { loadAliases } = require('../services/alias');
|
|
16
17
|
const {
|
|
@@ -45,7 +46,7 @@ module.exports = (config) => {
|
|
|
45
46
|
* GET /api/gemini/sessions/search/global?keyword=xxx
|
|
46
47
|
* 全局搜索
|
|
47
48
|
*/
|
|
48
|
-
router.get('/search/global', (req, res) => {
|
|
49
|
+
router.get('/search/global', async (req, res) => {
|
|
49
50
|
try {
|
|
50
51
|
if (!isGeminiInstalled()) {
|
|
51
52
|
return res.status(404).json({ error: 'Gemini CLI not installed' });
|
|
@@ -57,7 +58,7 @@ module.exports = (config) => {
|
|
|
57
58
|
return res.status(400).json({ error: 'Keyword is required' });
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
const results = searchSessions(keyword);
|
|
61
|
+
const results = await searchSessions(keyword);
|
|
61
62
|
|
|
62
63
|
res.json({
|
|
63
64
|
keyword,
|
|
@@ -75,14 +76,14 @@ module.exports = (config) => {
|
|
|
75
76
|
* GET /api/gemini/sessions/recent/list?limit=10
|
|
76
77
|
* 获取最近会话
|
|
77
78
|
*/
|
|
78
|
-
router.get('/recent/list', (req, res) => {
|
|
79
|
+
router.get('/recent/list', async (req, res) => {
|
|
79
80
|
try {
|
|
80
81
|
if (!isGeminiInstalled()) {
|
|
81
82
|
return res.status(404).json({ error: 'Gemini CLI not installed' });
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
const limit = parseInt(req.query.limit) || 5;
|
|
85
|
-
const sessions = getRecentSessions(limit);
|
|
86
|
+
const sessions = await getRecentSessions(limit);
|
|
86
87
|
|
|
87
88
|
res.json({
|
|
88
89
|
sessions,
|
|
@@ -125,7 +126,7 @@ module.exports = (config) => {
|
|
|
125
126
|
* GET /api/gemini/sessions/:projectHash/search
|
|
126
127
|
* 搜索项目内会话内容
|
|
127
128
|
*/
|
|
128
|
-
router.get('/:projectHash/search', (req, res) => {
|
|
129
|
+
router.get('/:projectHash/search', async (req, res) => {
|
|
129
130
|
try {
|
|
130
131
|
if (!isGeminiInstalled()) {
|
|
131
132
|
return res.status(404).json({ error: 'Gemini CLI not installed' });
|
|
@@ -141,7 +142,7 @@ module.exports = (config) => {
|
|
|
141
142
|
const contextLength = context ? parseInt(context) : 35;
|
|
142
143
|
|
|
143
144
|
// 搜索所有会话,然后过滤该项目的会话
|
|
144
|
-
const allResults = searchSessions(keyword, contextLength);
|
|
145
|
+
const allResults = await searchSessions(keyword, contextLength);
|
|
145
146
|
const results = allResults.filter(r => r.projectHash === projectHash);
|
|
146
147
|
|
|
147
148
|
res.json({
|
|
@@ -155,14 +156,26 @@ module.exports = (config) => {
|
|
|
155
156
|
}
|
|
156
157
|
});
|
|
157
158
|
|
|
158
|
-
router.get('/:projectHash/:sessionId/status', (req, res) => {
|
|
159
|
+
router.get('/:projectHash/:sessionId/status', async (req, res) => {
|
|
160
|
+
// Try session-history-index first for fast lookup
|
|
161
|
+
try {
|
|
162
|
+
const idxStatus = await getSessionStatus('gemini', req.params.sessionId, { consistency: 'stale-ok' });
|
|
163
|
+
if (idxStatus) {
|
|
164
|
+
return res.json({
|
|
165
|
+
sessionId: idxStatus.sessionId,
|
|
166
|
+
lastModified: new Date(idxStatus.lastModified).toISOString(),
|
|
167
|
+
size: idxStatus.size
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
} catch (_) { /* fall through to direct lookup */ }
|
|
171
|
+
|
|
159
172
|
try {
|
|
160
173
|
if (!isGeminiInstalled()) {
|
|
161
174
|
return res.status(404).json({ error: 'Gemini CLI not installed' });
|
|
162
175
|
}
|
|
163
176
|
|
|
164
177
|
const { sessionId } = req.params;
|
|
165
|
-
const session = getSessionById(sessionId);
|
|
178
|
+
const session = await getSessionById(sessionId);
|
|
166
179
|
if (!session) {
|
|
167
180
|
return res.status(404).json({ error: 'Session not found' });
|
|
168
181
|
}
|
|
@@ -178,14 +191,25 @@ module.exports = (config) => {
|
|
|
178
191
|
}
|
|
179
192
|
});
|
|
180
193
|
|
|
181
|
-
router.get('/:projectHash/:sessionId/outline', (req, res) => {
|
|
194
|
+
router.get('/:projectHash/:sessionId/outline', async (req, res) => {
|
|
195
|
+
// Try session-history-index first for fast lookup
|
|
196
|
+
try {
|
|
197
|
+
const idxOutline = await getSessionOutline('gemini', req.params.sessionId, { consistency: 'stale-ok' });
|
|
198
|
+
if (idxOutline) {
|
|
199
|
+
return res.json({
|
|
200
|
+
sessionId: idxOutline.sessionId,
|
|
201
|
+
items: idxOutline.items
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
} catch (_) { /* fall through to direct read */ }
|
|
205
|
+
|
|
182
206
|
try {
|
|
183
207
|
if (!isGeminiInstalled()) {
|
|
184
208
|
return res.status(404).json({ error: 'Gemini CLI not installed' });
|
|
185
209
|
}
|
|
186
210
|
|
|
187
211
|
const { sessionId } = req.params;
|
|
188
|
-
const session = getSessionById(sessionId);
|
|
212
|
+
const session = await getSessionById(sessionId);
|
|
189
213
|
if (!session) {
|
|
190
214
|
return res.status(404).json({ error: 'Session not found' });
|
|
191
215
|
}
|
|
@@ -218,7 +242,21 @@ module.exports = (config) => {
|
|
|
218
242
|
* GET /api/gemini/sessions/:projectHash/:sessionId/messages
|
|
219
243
|
* 获取会话的消息列表
|
|
220
244
|
*/
|
|
221
|
-
router.get('/:projectHash/:sessionId/messages', (req, res) => {
|
|
245
|
+
router.get('/:projectHash/:sessionId/messages', async (req, res) => {
|
|
246
|
+
const startMs = Date.now();
|
|
247
|
+
// Try session-history-index first for fast lookup
|
|
248
|
+
try {
|
|
249
|
+
const { page = 1, limit = 20, order = 'desc' } = req.query;
|
|
250
|
+
const pageNum = parseInt(page) || 1;
|
|
251
|
+
const limitNum = parseInt(limit) || 20;
|
|
252
|
+
const idxPage = await getMessagePage('gemini', req.params.sessionId, {
|
|
253
|
+
page: pageNum, limit: limitNum, order
|
|
254
|
+
});
|
|
255
|
+
if (idxPage) {
|
|
256
|
+
return res.json(idxPage);
|
|
257
|
+
}
|
|
258
|
+
} catch (_) { /* fall through to direct file read */ }
|
|
259
|
+
|
|
222
260
|
try {
|
|
223
261
|
if (!isGeminiInstalled()) {
|
|
224
262
|
return res.status(404).json({ error: 'Gemini CLI not installed' });
|
|
@@ -227,7 +265,7 @@ module.exports = (config) => {
|
|
|
227
265
|
const { sessionId } = req.params;
|
|
228
266
|
const { page = 1, limit = 20, order = 'desc' } = req.query;
|
|
229
267
|
|
|
230
|
-
const session = getSessionById(sessionId);
|
|
268
|
+
const session = await getSessionById(sessionId);
|
|
231
269
|
|
|
232
270
|
if (!session) {
|
|
233
271
|
return res.status(404).json({ error: 'Session not found' });
|
|
@@ -442,7 +480,7 @@ module.exports = (config) => {
|
|
|
442
480
|
* POST /api/gemini/sessions/:projectHash/:sessionId/launch
|
|
443
481
|
* 获取会话启动命令(用于复制)
|
|
444
482
|
*/
|
|
445
|
-
router.post('/:projectHash/:sessionId/launch', (req, res) => {
|
|
483
|
+
router.post('/:projectHash/:sessionId/launch', async (req, res) => {
|
|
446
484
|
try {
|
|
447
485
|
if (!isGeminiInstalled()) {
|
|
448
486
|
return res.status(404).json({ error: 'Gemini CLI not installed' });
|
|
@@ -451,7 +489,7 @@ module.exports = (config) => {
|
|
|
451
489
|
const { projectHash, sessionId } = req.params;
|
|
452
490
|
|
|
453
491
|
// 获取会话详情
|
|
454
|
-
const session = getSessionById(sessionId);
|
|
492
|
+
const session = await getSessionById(sessionId);
|
|
455
493
|
|
|
456
494
|
if (!session) {
|
|
457
495
|
return res.status(404).json({ error: 'Session not found' });
|
|
@@ -66,12 +66,12 @@ module.exports = () => {
|
|
|
66
66
|
}
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
router.delete('/:projectName', (req, res) => {
|
|
69
|
+
router.delete('/:projectName', async (req, res) => {
|
|
70
70
|
try {
|
|
71
71
|
if (!isOmpInstalled()) {
|
|
72
72
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
73
73
|
}
|
|
74
|
-
const result = deleteProject(req.params.projectName);
|
|
74
|
+
const result = await deleteProject(req.params.projectName);
|
|
75
75
|
invalidateProjectSnapshots('omp');
|
|
76
76
|
invalidateSessionSnapshots('omp', req.params.projectName);
|
|
77
77
|
res.json(result);
|
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
isOmpInstalled,
|
|
15
15
|
HOME_DIR
|
|
16
16
|
} = require('../services/omp-sessions');
|
|
17
|
+
const { getSessionStatus, getSessionOutline, getMessagePage } = require('../services/session-history-index');
|
|
17
18
|
const { loadAliases } = require('../services/alias');
|
|
18
19
|
const { broadcastLog } = require('../websocket-server');
|
|
19
20
|
const {
|
|
@@ -35,7 +36,7 @@ function quoteForShell(value) {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
module.exports = () => {
|
|
38
|
-
router.get('/search/global', (req, res) => {
|
|
39
|
+
router.get('/search/global', async (req, res) => {
|
|
39
40
|
try {
|
|
40
41
|
if (!isOmpInstalled()) {
|
|
41
42
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
@@ -46,7 +47,7 @@ module.exports = () => {
|
|
|
46
47
|
if (!keyword) {
|
|
47
48
|
return res.status(400).json({ error: 'Keyword is required' });
|
|
48
49
|
}
|
|
49
|
-
const sessions = searchSessions(keyword, contextLength);
|
|
50
|
+
const sessions = await searchSessions(keyword, contextLength);
|
|
50
51
|
const totalMatches = sessions.reduce((sum, session) => sum + (session.matchCount || 0), 0);
|
|
51
52
|
res.json({ keyword, totalMatches, sessions, source: 'omp' });
|
|
52
53
|
} catch (err) {
|
|
@@ -55,20 +56,20 @@ module.exports = () => {
|
|
|
55
56
|
}
|
|
56
57
|
});
|
|
57
58
|
|
|
58
|
-
router.get('/recent/list', (req, res) => {
|
|
59
|
+
router.get('/recent/list', async (req, res) => {
|
|
59
60
|
try {
|
|
60
61
|
if (!isOmpInstalled()) {
|
|
61
62
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
62
63
|
}
|
|
63
64
|
const limit = parseInt(req.query.limit, 10) || 5;
|
|
64
|
-
res.json({ sessions: getRecentSessions(limit), source: 'omp' });
|
|
65
|
+
res.json({ sessions: await getRecentSessions(limit), source: 'omp' });
|
|
65
66
|
} catch (err) {
|
|
66
67
|
console.error('[OMP API] Failed to get recent sessions:', err);
|
|
67
68
|
res.status(500).json({ error: err.message });
|
|
68
69
|
}
|
|
69
70
|
});
|
|
70
71
|
|
|
71
|
-
router.get('/:projectName/search', (req, res) => {
|
|
72
|
+
router.get('/:projectName/search', async (req, res) => {
|
|
72
73
|
try {
|
|
73
74
|
if (!isOmpInstalled()) {
|
|
74
75
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
@@ -80,7 +81,7 @@ module.exports = () => {
|
|
|
80
81
|
if (!keyword) {
|
|
81
82
|
return res.status(400).json({ error: 'Keyword is required' });
|
|
82
83
|
}
|
|
83
|
-
const sessions = searchSessions(keyword, contextLength, projectName);
|
|
84
|
+
const sessions = await searchSessions(keyword, contextLength, projectName);
|
|
84
85
|
const totalMatches = sessions.reduce((sum, session) => sum + (session.matchCount || 0), 0);
|
|
85
86
|
res.json({ keyword, totalMatches, sessions });
|
|
86
87
|
} catch (err) {
|
|
@@ -111,12 +112,24 @@ module.exports = () => {
|
|
|
111
112
|
}
|
|
112
113
|
});
|
|
113
114
|
|
|
114
|
-
router.get('/:projectName/:sessionId/status', (req, res) => {
|
|
115
|
+
router.get('/:projectName/:sessionId/status', async (req, res) => {
|
|
116
|
+
// Try session-history-index first for fast lookup
|
|
117
|
+
try {
|
|
118
|
+
const idxStatus = await getSessionStatus('omp', req.params.sessionId, { consistency: 'stale-ok' });
|
|
119
|
+
if (idxStatus) {
|
|
120
|
+
return res.json({
|
|
121
|
+
sessionId: idxStatus.sessionId,
|
|
122
|
+
lastModified: new Date(idxStatus.lastModified).toISOString(),
|
|
123
|
+
size: idxStatus.size
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
} catch (_) { /* fall through to direct lookup */ }
|
|
127
|
+
|
|
115
128
|
try {
|
|
116
129
|
if (!isOmpInstalled()) {
|
|
117
130
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
118
131
|
}
|
|
119
|
-
const session = getSessionById(req.params.sessionId);
|
|
132
|
+
const session = await getSessionById(req.params.sessionId);
|
|
120
133
|
if (!session) {
|
|
121
134
|
return res.status(404).json({ error: 'Session not found' });
|
|
122
135
|
}
|
|
@@ -131,12 +144,23 @@ module.exports = () => {
|
|
|
131
144
|
}
|
|
132
145
|
});
|
|
133
146
|
|
|
134
|
-
router.get('/:projectName/:sessionId/outline', (req, res) => {
|
|
147
|
+
router.get('/:projectName/:sessionId/outline', async (req, res) => {
|
|
148
|
+
// Try session-history-index first for fast lookup
|
|
149
|
+
try {
|
|
150
|
+
const idxOutline = await getSessionOutline('omp', req.params.sessionId, { consistency: 'stale-ok' });
|
|
151
|
+
if (idxOutline) {
|
|
152
|
+
return res.json({
|
|
153
|
+
sessionId: idxOutline.sessionId,
|
|
154
|
+
items: idxOutline.items
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
} catch (_) { /* fall through to direct read */ }
|
|
158
|
+
|
|
135
159
|
try {
|
|
136
160
|
if (!isOmpInstalled()) {
|
|
137
161
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
138
162
|
}
|
|
139
|
-
const session = getSessionById(req.params.sessionId);
|
|
163
|
+
const session = await getSessionById(req.params.sessionId);
|
|
140
164
|
if (!session) {
|
|
141
165
|
return res.status(404).json({ error: 'Session not found' });
|
|
142
166
|
}
|
|
@@ -160,14 +184,28 @@ module.exports = () => {
|
|
|
160
184
|
}
|
|
161
185
|
});
|
|
162
186
|
|
|
163
|
-
router.get('/:projectName/:sessionId/messages', (req, res) => {
|
|
187
|
+
router.get('/:projectName/:sessionId/messages', async (req, res) => {
|
|
188
|
+
const startMs = Date.now();
|
|
189
|
+
// Try session-history-index first for fast lookup
|
|
190
|
+
try {
|
|
191
|
+
const { page = 1, limit = 20, order = 'desc' } = req.query;
|
|
192
|
+
const pageNum = parseInt(page) || 1;
|
|
193
|
+
const limitNum = parseInt(limit) || 20;
|
|
194
|
+
const idxPage = await getMessagePage('omp', req.params.sessionId, {
|
|
195
|
+
page: pageNum, limit: limitNum, order
|
|
196
|
+
});
|
|
197
|
+
if (idxPage) {
|
|
198
|
+
return res.json(idxPage);
|
|
199
|
+
}
|
|
200
|
+
} catch (_) { /* fall through to direct file read */ }
|
|
201
|
+
|
|
164
202
|
try {
|
|
165
203
|
if (!isOmpInstalled()) {
|
|
166
204
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
167
205
|
}
|
|
168
206
|
const { sessionId } = req.params;
|
|
169
207
|
const { page = 1, limit = 20, order = 'desc' } = req.query;
|
|
170
|
-
const session = getSessionById(sessionId);
|
|
208
|
+
const session = await getSessionById(sessionId);
|
|
171
209
|
if (!session) {
|
|
172
210
|
return res.status(404).json({ error: 'Session not found' });
|
|
173
211
|
}
|
|
@@ -204,12 +242,12 @@ module.exports = () => {
|
|
|
204
242
|
}
|
|
205
243
|
});
|
|
206
244
|
|
|
207
|
-
router.delete('/:projectName/:sessionId', (req, res) => {
|
|
245
|
+
router.delete('/:projectName/:sessionId', async (req, res) => {
|
|
208
246
|
try {
|
|
209
247
|
if (!isOmpInstalled()) {
|
|
210
248
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
211
249
|
}
|
|
212
|
-
const result = deleteSession(req.params.sessionId);
|
|
250
|
+
const result = await deleteSession(req.params.sessionId);
|
|
213
251
|
invalidateSessionSnapshots('omp', req.params.projectName);
|
|
214
252
|
invalidateProjectSnapshots('omp');
|
|
215
253
|
res.json(result);
|
|
@@ -222,7 +260,7 @@ module.exports = () => {
|
|
|
222
260
|
}
|
|
223
261
|
});
|
|
224
262
|
|
|
225
|
-
router.post('/:projectName/batch-delete', (req, res) => {
|
|
263
|
+
router.post('/:projectName/batch-delete', async (req, res) => {
|
|
226
264
|
try {
|
|
227
265
|
if (!isOmpInstalled()) {
|
|
228
266
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
@@ -242,14 +280,14 @@ module.exports = () => {
|
|
|
242
280
|
}
|
|
243
281
|
const deletedSessionIds = [];
|
|
244
282
|
const failed = [];
|
|
245
|
-
|
|
283
|
+
for (const sessionId of uniqueSessionIds) {
|
|
246
284
|
try {
|
|
247
|
-
deleteSession(sessionId);
|
|
285
|
+
await deleteSession(sessionId);
|
|
248
286
|
deletedSessionIds.push(sessionId);
|
|
249
287
|
} catch (err) {
|
|
250
288
|
failed.push({ sessionId, error: err.message });
|
|
251
289
|
}
|
|
252
|
-
}
|
|
290
|
+
}
|
|
253
291
|
if (deletedSessionIds.length > 0) {
|
|
254
292
|
invalidateSessionSnapshots('omp', req.params.projectName);
|
|
255
293
|
invalidateProjectSnapshots('omp');
|
|
@@ -267,12 +305,12 @@ module.exports = () => {
|
|
|
267
305
|
}
|
|
268
306
|
});
|
|
269
307
|
|
|
270
|
-
router.post('/:projectName/:sessionId/fork', (req, res) => {
|
|
308
|
+
router.post('/:projectName/:sessionId/fork', async (req, res) => {
|
|
271
309
|
try {
|
|
272
310
|
if (!isOmpInstalled()) {
|
|
273
311
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
274
312
|
}
|
|
275
|
-
const result = forkSession(req.params.sessionId, req.body || {});
|
|
313
|
+
const result = await forkSession(req.params.sessionId, req.body || {});
|
|
276
314
|
invalidateSessionSnapshots('omp', req.params.projectName);
|
|
277
315
|
invalidateProjectSnapshots('omp');
|
|
278
316
|
res.json(result);
|
|
@@ -303,17 +341,17 @@ module.exports = () => {
|
|
|
303
341
|
}
|
|
304
342
|
});
|
|
305
343
|
|
|
306
|
-
router.post('/:projectName/:sessionId/launch', (req, res) => {
|
|
344
|
+
router.post('/:projectName/:sessionId/launch', async (req, res) => {
|
|
307
345
|
try {
|
|
308
346
|
if (!isOmpInstalled()) {
|
|
309
347
|
return res.status(404).json({ error: 'OMP CLI not installed' });
|
|
310
348
|
}
|
|
311
349
|
const { projectName, sessionId } = req.params;
|
|
312
|
-
const session = getSessionById(sessionId);
|
|
350
|
+
const session = await getSessionById(sessionId);
|
|
313
351
|
if (!session) {
|
|
314
352
|
return res.status(404).json({ error: 'Session not found' });
|
|
315
353
|
}
|
|
316
|
-
const project = getProjects().find(p => p.name === projectName);
|
|
354
|
+
const project = (await getProjects()).find(p => p.name === projectName);
|
|
317
355
|
const cwd = session.directory || project?.fullPath || HOME_DIR;
|
|
318
356
|
const command = buildLaunchCommand(sessionId, cwd, req.body || {});
|
|
319
357
|
const copyCommand = `cd ${quoteForShell(cwd)} && ${command}`;
|
|
@@ -4,6 +4,7 @@ const path = require('path');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const readline = require('readline');
|
|
6
6
|
const { deleteSession, forkSession, saveSessionOrder, parseRealProjectPath, searchSessions, getRecentSessions, searchSessionsAcrossProjects, hasActualMessages } = require('../services/sessions');
|
|
7
|
+
const { getSessionStatus, getSessionOutline, getMessagePage } = require('../services/session-history-index');
|
|
7
8
|
const { loadAliases } = require('../services/alias');
|
|
8
9
|
const { broadcastLog } = require('../websocket-server');
|
|
9
10
|
const { NATIVE_PATHS } = require('../../config/paths');
|
|
@@ -376,7 +377,7 @@ module.exports = (config) => {
|
|
|
376
377
|
});
|
|
377
378
|
|
|
378
379
|
// GET /api/sessions/:projectName/search - Search sessions content
|
|
379
|
-
router.get('/:projectName/search', (req, res) => {
|
|
380
|
+
router.get('/:projectName/search', async (req, res) => {
|
|
380
381
|
try {
|
|
381
382
|
const { projectName } = req.params;
|
|
382
383
|
const { keyword, context } = req.query;
|
|
@@ -386,7 +387,7 @@ module.exports = (config) => {
|
|
|
386
387
|
}
|
|
387
388
|
|
|
388
389
|
const contextLength = context ? parseInt(context) : 15;
|
|
389
|
-
const results = searchSessions(config, projectName, keyword, contextLength);
|
|
390
|
+
const results = await searchSessions(config, projectName, keyword, contextLength);
|
|
390
391
|
|
|
391
392
|
res.json({
|
|
392
393
|
keyword,
|
|
@@ -402,6 +403,19 @@ module.exports = (config) => {
|
|
|
402
403
|
// GET /api/sessions/:projectName/:sessionId/status - Lightweight status for live sync
|
|
403
404
|
router.get('/:projectName/:sessionId/status', async (req, res) => {
|
|
404
405
|
try {
|
|
406
|
+
|
|
407
|
+
// Try session-history-index first for fast lookup
|
|
408
|
+
try {
|
|
409
|
+
const idxStatus = await getSessionStatus('claude', req.params.sessionId, { consistency: 'stale-ok' });
|
|
410
|
+
if (idxStatus) {
|
|
411
|
+
return res.json({
|
|
412
|
+
sessionId: idxStatus.sessionId,
|
|
413
|
+
lastModified: new Date(idxStatus.lastModified).toISOString(),
|
|
414
|
+
size: idxStatus.size
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
} catch (_) { /* fall through to direct stat */ }
|
|
418
|
+
|
|
405
419
|
const { projectName, sessionId } = req.params;
|
|
406
420
|
const { fullPath } = parseRealProjectPath(projectName);
|
|
407
421
|
const { sessionFile, triedPaths } = resolveClaudeSessionFile(projectName, sessionId, fullPath);
|
|
@@ -428,6 +442,18 @@ module.exports = (config) => {
|
|
|
428
442
|
// GET /api/sessions/:projectName/:sessionId/outline - Lightweight user outline for TOC and forking
|
|
429
443
|
router.get('/:projectName/:sessionId/outline', async (req, res) => {
|
|
430
444
|
try {
|
|
445
|
+
|
|
446
|
+
// Try session-history-index first for fast lookup
|
|
447
|
+
try {
|
|
448
|
+
const idxOutline = await getSessionOutline('claude', req.params.sessionId, { consistency: 'stale-ok' });
|
|
449
|
+
if (idxOutline) {
|
|
450
|
+
return res.json({
|
|
451
|
+
sessionId: idxOutline.sessionId,
|
|
452
|
+
items: idxOutline.items
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
} catch (_) { /* fall through to direct read */ }
|
|
456
|
+
|
|
431
457
|
const { projectName, sessionId } = req.params;
|
|
432
458
|
const { fullPath } = parseRealProjectPath(projectName);
|
|
433
459
|
const { sessionFile, triedPaths } = resolveClaudeSessionFile(projectName, sessionId, fullPath);
|
|
@@ -453,6 +479,22 @@ module.exports = (config) => {
|
|
|
453
479
|
// GET /api/sessions/:projectName/:sessionId/messages - Get session messages with pagination
|
|
454
480
|
router.get('/:projectName/:sessionId/messages', async (req, res) => {
|
|
455
481
|
try {
|
|
482
|
+
|
|
483
|
+
// Try session-history-index first for fast lookup
|
|
484
|
+
try {
|
|
485
|
+
const { page = 1, limit = 20, order = 'desc' } = req.query;
|
|
486
|
+
const pageNum = parseInt(page) || 1;
|
|
487
|
+
const limitNum = parseInt(limit) || 20;
|
|
488
|
+
const idxPage = await getMessagePage('claude', req.params.sessionId, {
|
|
489
|
+
page: pageNum,
|
|
490
|
+
limit: limitNum,
|
|
491
|
+
order
|
|
492
|
+
});
|
|
493
|
+
if (idxPage) {
|
|
494
|
+
return res.json(idxPage);
|
|
495
|
+
}
|
|
496
|
+
} catch (_) { /* fall through to direct file read */ }
|
|
497
|
+
|
|
456
498
|
const { projectName, sessionId } = req.params;
|
|
457
499
|
const { page = 1, limit = 20, order = 'desc' } = req.query;
|
|
458
500
|
|