coding-tool-x 3.7.13 → 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 CHANGED
@@ -3,7 +3,7 @@
3
3
  > 面向 Claude Code、Codex CLI、Gemini CLI、OpenCode、OMP 的统一增强控制台
4
4
  > Web UI + CLI + 多平台代理 + 配置托管 + 工作区编排 + 分析面板
5
5
 
6
- ![Node.js](https://img.shields.io/badge/Node.js-%3E%3D14.0.0-43853d?style=flat-square)
6
+ ![Node.js](https://img.shields.io/badge/Node.js-%3E%3D22.13.0-43853d?style=flat-square)
7
7
  ![License](https://img.shields.io/badge/License-MIT-blue?style=flat-square)
8
8
  ![Platforms](https://img.shields.io/badge/Platforms-Claude%20%7C%20Codex%20%7C%20Gemini%20%7C%20OpenCode%20%7C%20OMP-1f6feb?style=flat-square)
9
9
 
@@ -86,7 +86,7 @@
86
86
  补充说明:
87
87
 
88
88
  - Codex Agents 目前仅支持用户级代理
89
- - OpenCode 会话读取依赖本机 `sqlite3`
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
- - 如需读取 OpenCode 会话,请确保系统里有可用的 `sqlite3`
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.7.13",
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": ">=14.0.0"
40
+ "node": ">=22.13.0"
41
41
  },
42
42
  "files": [
43
43
  "bin/",
@@ -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
- uniqueSessionIds.forEach((sessionId) => {
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