@mmmbuto/nexuscli 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +172 -0
  3. package/bin/nexuscli.js +117 -0
  4. package/frontend/dist/apple-touch-icon.png +0 -0
  5. package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  6. package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  7. package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  8. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  9. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  10. package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  11. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  12. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  13. package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  14. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  15. package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  16. package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  17. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  18. package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  19. package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  20. package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  21. package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  22. package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  23. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  24. package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  25. package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  26. package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  27. package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  28. package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  29. package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  30. package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  31. package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  32. package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  33. package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  34. package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  35. package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  36. package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  37. package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  38. package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  39. package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  40. package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  41. package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  42. package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  43. package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  44. package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  45. package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  46. package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  47. package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  48. package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  49. package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  50. package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  51. package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  52. package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  53. package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  54. package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  55. package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  56. package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  57. package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  58. package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  59. package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  60. package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  61. package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  62. package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  63. package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  64. package/frontend/dist/assets/index-Bn_l1e6e.css +1 -0
  65. package/frontend/dist/assets/index-CikJbUR5.js +8617 -0
  66. package/frontend/dist/browserconfig.xml +12 -0
  67. package/frontend/dist/favicon-16x16.png +0 -0
  68. package/frontend/dist/favicon-32x32.png +0 -0
  69. package/frontend/dist/favicon-48x48.png +0 -0
  70. package/frontend/dist/favicon.ico +0 -0
  71. package/frontend/dist/icon-192.png +0 -0
  72. package/frontend/dist/icon-512.png +0 -0
  73. package/frontend/dist/icon-maskable-192.png +0 -0
  74. package/frontend/dist/icon-maskable-512.png +0 -0
  75. package/frontend/dist/index.html +79 -0
  76. package/frontend/dist/manifest.json +75 -0
  77. package/frontend/dist/sw.js +122 -0
  78. package/frontend/package.json +28 -0
  79. package/lib/cli/api.js +156 -0
  80. package/lib/cli/boot.js +172 -0
  81. package/lib/cli/config.js +185 -0
  82. package/lib/cli/engines.js +257 -0
  83. package/lib/cli/init.js +660 -0
  84. package/lib/cli/logs.js +72 -0
  85. package/lib/cli/start.js +220 -0
  86. package/lib/cli/status.js +187 -0
  87. package/lib/cli/stop.js +64 -0
  88. package/lib/cli/uninstall.js +194 -0
  89. package/lib/cli/users.js +295 -0
  90. package/lib/cli/workspaces.js +337 -0
  91. package/lib/config/manager.js +233 -0
  92. package/lib/server/.env.example +20 -0
  93. package/lib/server/db/adapter.js +314 -0
  94. package/lib/server/db/drivers/better-sqlite3.js +38 -0
  95. package/lib/server/db/drivers/sql-js.js +75 -0
  96. package/lib/server/db/migrate.js +174 -0
  97. package/lib/server/db/migrations/001_ultra_light_schema.sql +96 -0
  98. package/lib/server/db/migrations/002_session_conversation_mapping.sql +19 -0
  99. package/lib/server/db/migrations/003_message_engine_tracking.sql +18 -0
  100. package/lib/server/db/migrations/004_performance_indexes.sql +16 -0
  101. package/lib/server/db.js +2 -0
  102. package/lib/server/lib/cli-wrapper.js +164 -0
  103. package/lib/server/lib/output-parser.js +132 -0
  104. package/lib/server/lib/pty-adapter.js +57 -0
  105. package/lib/server/middleware/auth.js +103 -0
  106. package/lib/server/models/Conversation.js +259 -0
  107. package/lib/server/models/Message.js +228 -0
  108. package/lib/server/models/User.js +115 -0
  109. package/lib/server/package-lock.json +5895 -0
  110. package/lib/server/routes/auth.js +168 -0
  111. package/lib/server/routes/chat.js +206 -0
  112. package/lib/server/routes/codex.js +205 -0
  113. package/lib/server/routes/conversations.js +224 -0
  114. package/lib/server/routes/gemini.js +228 -0
  115. package/lib/server/routes/jobs.js +317 -0
  116. package/lib/server/routes/messages.js +60 -0
  117. package/lib/server/routes/models.js +198 -0
  118. package/lib/server/routes/sessions.js +285 -0
  119. package/lib/server/routes/upload.js +134 -0
  120. package/lib/server/routes/wake-lock.js +95 -0
  121. package/lib/server/routes/workspace.js +80 -0
  122. package/lib/server/routes/workspaces.js +142 -0
  123. package/lib/server/scripts/cleanup-ghost-sessions.js +71 -0
  124. package/lib/server/scripts/seed-users.js +37 -0
  125. package/lib/server/scripts/test-history-access.js +50 -0
  126. package/lib/server/server.js +227 -0
  127. package/lib/server/services/cache.js +85 -0
  128. package/lib/server/services/claude-wrapper.js +312 -0
  129. package/lib/server/services/cli-loader.js +384 -0
  130. package/lib/server/services/codex-output-parser.js +277 -0
  131. package/lib/server/services/codex-wrapper.js +224 -0
  132. package/lib/server/services/context-bridge.js +289 -0
  133. package/lib/server/services/gemini-output-parser.js +398 -0
  134. package/lib/server/services/gemini-wrapper.js +249 -0
  135. package/lib/server/services/history-sync.js +407 -0
  136. package/lib/server/services/output-parser.js +415 -0
  137. package/lib/server/services/session-manager.js +465 -0
  138. package/lib/server/services/summary-generator.js +259 -0
  139. package/lib/server/services/workspace-manager.js +516 -0
  140. package/lib/server/tests/history-sync.test.js +90 -0
  141. package/lib/server/tests/integration-session-sync.test.js +151 -0
  142. package/lib/server/tests/integration.test.js +76 -0
  143. package/lib/server/tests/performance.test.js +118 -0
  144. package/lib/server/tests/services.test.js +160 -0
  145. package/lib/setup/postinstall.js +216 -0
  146. package/lib/utils/paths.js +107 -0
  147. package/lib/utils/termux.js +145 -0
  148. package/package.json +82 -0
@@ -0,0 +1,224 @@
1
+ const express = require('express');
2
+ const Conversation = require('../models/Conversation');
3
+ const Message = require('../models/Message');
4
+ const HistorySync = require('../services/history-sync');
5
+ const sessionManager = require('../services/session-manager');
6
+ const { getOrSet, invalidateConversations, KEYS, getStats } = require('../services/cache');
7
+
8
+ const router = express.Router();
9
+ const historySync = new HistorySync();
10
+
11
+ /**
12
+ * GET /api/v1/conversations
13
+ * List conversations (optionally grouped by date)
14
+ *
15
+ * Query params:
16
+ * - groupBy=date: Group conversations by date
17
+ * - sync=true: Force sync with Claude Code history.jsonl (default: auto)
18
+ * - limit=N: Limit per group (default: 20, 0 = unlimited)
19
+ * - workspace=path: Filter by workspace path
20
+ */
21
+ router.get('/', async (req, res) => {
22
+ try {
23
+ const startTime = Date.now();
24
+ const groupBy = req.query.groupBy;
25
+ const syncParam = req.query.sync;
26
+ const workspace = req.query.workspace;
27
+ const limit = parseInt(req.query.limit) || 20;
28
+ const noCache = req.query.nocache === 'true' || req.query.nocache === '1';
29
+ const forceSync = syncParam === 'true' || syncParam === '1';
30
+
31
+ // Sync with Claude Code history.jsonl
32
+ // Only sync if explicitly requested (removed auto-sync for performance)
33
+ if (forceSync && historySync.exists()) {
34
+ try {
35
+ await historySync.sync(true);
36
+ invalidateConversations(); // Clear cache after sync
37
+ } catch (syncError) {
38
+ console.error('[Conversations] Sync error:', syncError);
39
+ // Continue even if sync fails (use cached data)
40
+ }
41
+ }
42
+
43
+ // Filter by workspace if requested (returns grouped data)
44
+ if (workspace) {
45
+ const cacheKey = KEYS.CONVERSATIONS_WORKSPACE(workspace);
46
+ const grouped = noCache
47
+ ? await historySync.getWorkspaceSessions(workspace, limit)
48
+ : await getOrSet(cacheKey, () => historySync.getWorkspaceSessions(workspace, limit), 30);
49
+
50
+ console.log(`[Conversations] Workspace query took ${Date.now() - startTime}ms (cached: ${!noCache})`);
51
+ return res.json(grouped);
52
+ }
53
+
54
+ // Return conversations grouped by date
55
+ if (groupBy === 'date') {
56
+ const grouped = noCache
57
+ ? Conversation.listGroupedByDate(limit)
58
+ : await getOrSet(KEYS.CONVERSATIONS_GROUPED, () => Conversation.listGroupedByDate(limit), 30);
59
+
60
+ console.log(`[Conversations] Grouped query took ${Date.now() - startTime}ms (cached: ${!noCache})`);
61
+ return res.json(grouped);
62
+ }
63
+
64
+ // Return flat list (not cached - less frequent)
65
+ const conversations = Conversation.listRecent(limit);
66
+ console.log(`[Conversations] List query took ${Date.now() - startTime}ms`);
67
+
68
+ res.json({ conversations });
69
+ } catch (error) {
70
+ console.error('[Conversations] List error:', error);
71
+ res.status(500).json({ error: 'Failed to list conversations' });
72
+ }
73
+ });
74
+
75
+ /**
76
+ * GET /api/v1/conversations/:id
77
+ * Get conversation with messages
78
+ *
79
+ * Query params:
80
+ * - limit=N: Max messages to return (default: 50)
81
+ * - offset=N: Skip first N messages (default: 0)
82
+ * - all=true: Return all messages (ignores limit/offset)
83
+ */
84
+ router.get('/:id', (req, res) => {
85
+ try {
86
+ const conversation = Conversation.getById(req.params.id);
87
+
88
+ if (!conversation) {
89
+ return res.status(404).json({ error: 'Conversation not found' });
90
+ }
91
+
92
+ // Get total message count
93
+ const totalMessages = Message.countByConversation(req.params.id);
94
+
95
+ // Parse query params
96
+ const all = req.query.all === 'true';
97
+ const limit = all ? totalMessages : (parseInt(req.query.limit) || 50);
98
+ const offset = all ? 0 : (parseInt(req.query.offset) || 0);
99
+
100
+ // Get messages for this conversation
101
+ const messages = Message.getByConversation(req.params.id, limit, offset);
102
+
103
+ res.json({
104
+ ...conversation,
105
+ messages,
106
+ pagination: {
107
+ total: totalMessages,
108
+ limit,
109
+ offset,
110
+ hasMore: (offset + messages.length) < totalMessages
111
+ }
112
+ });
113
+ } catch (error) {
114
+ console.error('[Conversations] Get error:', error);
115
+ res.status(500).json({ error: 'Failed to get conversation' });
116
+ }
117
+ });
118
+
119
+ /**
120
+ * PATCH /api/v1/conversations/:id
121
+ * Update conversation title
122
+ */
123
+ router.patch('/:id', (req, res) => {
124
+ try {
125
+ const { title } = req.body;
126
+
127
+ if (!title) {
128
+ return res.status(400).json({ error: 'Title is required' });
129
+ }
130
+
131
+ const updated = Conversation.updateTitle(req.params.id, title);
132
+
133
+ if (!updated) {
134
+ return res.status(404).json({ error: 'Conversation not found' });
135
+ }
136
+
137
+ invalidateConversations(); // Clear cache after update
138
+ res.json({ success: true });
139
+ } catch (error) {
140
+ console.error('[Conversations] Update error:', error);
141
+ res.status(500).json({ error: 'Failed to update conversation' });
142
+ }
143
+ });
144
+
145
+ /**
146
+ * DELETE /api/v1/conversations/:id
147
+ * Delete conversation (cascade deletes messages and sessions)
148
+ */
149
+ router.delete('/:id', (req, res) => {
150
+ try {
151
+ const conversationId = req.params.id;
152
+
153
+ // First, cleanup sessions for this conversation
154
+ const sessionsDeleted = sessionManager.deleteConversationSessions(conversationId);
155
+ console.log(`[Conversations] Deleted ${sessionsDeleted} sessions for ${conversationId}`);
156
+
157
+ // Then delete the conversation (cascade deletes messages)
158
+ const deleted = Conversation.delete(conversationId);
159
+
160
+ if (!deleted) {
161
+ return res.status(404).json({ error: 'Conversation not found' });
162
+ }
163
+
164
+ invalidateConversations(); // Clear cache after delete
165
+ res.json({ success: true, sessionsDeleted });
166
+ } catch (error) {
167
+ console.error('[Conversations] Delete error:', error);
168
+ res.status(500).json({ error: 'Failed to delete conversation' });
169
+ }
170
+ });
171
+
172
+ /**
173
+ * POST /api/v1/conversations/:id/bookmark
174
+ * Toggle bookmark status
175
+ */
176
+ router.post('/:id/bookmark', (req, res) => {
177
+ try {
178
+ const bookmarked = Conversation.toggleBookmark(req.params.id);
179
+
180
+ if (bookmarked === null) {
181
+ return res.status(404).json({ error: 'Conversation not found' });
182
+ }
183
+
184
+ invalidateConversations(); // Clear cache after bookmark change
185
+ res.json({ bookmarked });
186
+ } catch (error) {
187
+ console.error('[Conversations] Bookmark error:', error);
188
+ res.status(500).json({ error: 'Failed to toggle bookmark' });
189
+ }
190
+ });
191
+
192
+ /**
193
+ * POST /api/v1/conversations/:id/pin
194
+ * Toggle pin status (alias for bookmark for UI consistency)
195
+ */
196
+ router.post('/:id/pin', (req, res) => {
197
+ try {
198
+ const pinned = Conversation.toggleBookmark(req.params.id);
199
+
200
+ if (pinned === null) {
201
+ return res.status(404).json({ error: 'Conversation not found' });
202
+ }
203
+
204
+ invalidateConversations(); // Clear cache after pin change
205
+ res.json({ pinned });
206
+ } catch (error) {
207
+ console.error('[Conversations] Pin error:', error);
208
+ res.status(500).json({ error: 'Failed to toggle pin' });
209
+ }
210
+ });
211
+
212
+ /**
213
+ * GET /api/v1/conversations/cache/stats
214
+ * Get cache statistics (for debugging)
215
+ */
216
+ router.get('/cache/stats', (req, res) => {
217
+ try {
218
+ res.json(getStats());
219
+ } catch (error) {
220
+ res.status(500).json({ error: error.message });
221
+ }
222
+ });
223
+
224
+ module.exports = router;
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Gemini Route - /api/v1/gemini
3
+ *
4
+ * Send messages to Gemini CLI with SSE streaming.
5
+ * Part of TRI CLI v0.4.0
6
+ */
7
+
8
+ const express = require('express');
9
+ const GeminiWrapper = require('../services/gemini-wrapper');
10
+ const Message = require('../models/Message');
11
+ const { v4: uuidv4 } = require('uuid');
12
+ const sessionManager = require('../services/session-manager');
13
+ const contextBridge = require('../services/context-bridge');
14
+
15
+ const router = express.Router();
16
+ const geminiWrapper = new GeminiWrapper();
17
+
18
+ /**
19
+ * POST /api/v1/gemini
20
+ * Send message to Gemini CLI with SSE streaming
21
+ *
22
+ * Request body:
23
+ * {
24
+ * "conversationId": "uuid" (optional for new chat)
25
+ * "message": "user prompt",
26
+ * "model": "gemini-3-pro-preview" (optional),
27
+ * "workspace": "/path" (optional)
28
+ * }
29
+ *
30
+ * Response: SSE stream
31
+ * - status: Tool use, system events
32
+ * - response_chunk: Streaming text
33
+ * - message_done: Final response and usage
34
+ */
35
+ router.post('/', async (req, res) => {
36
+ try {
37
+ console.log('[Gemini] === NEW GEMINI REQUEST ===');
38
+ console.log('[Gemini] Body:', JSON.stringify(req.body, null, 2));
39
+
40
+ const { conversationId, message, model = 'gemini-3-pro-preview', workspace } = req.body;
41
+
42
+ console.log(`[Gemini] conversationId: ${conversationId}`);
43
+ console.log(`[Gemini] message: ${message?.substring(0, 100)}`);
44
+ console.log(`[Gemini] model: ${model}`);
45
+ console.log(`[Gemini] workspace: ${workspace}`);
46
+
47
+ if (!message) {
48
+ console.log('[Gemini] ERROR: message required');
49
+ return res.status(400).json({ error: 'message required' });
50
+ }
51
+
52
+ // Check if Gemini CLI is available
53
+ const isAvailable = await geminiWrapper.isAvailable();
54
+ if (!isAvailable) {
55
+ console.log('[Gemini] ERROR: Gemini CLI not available');
56
+ return res.status(503).json({
57
+ error: 'Gemini CLI not available',
58
+ details: 'Please install Gemini CLI: npm install -g @anthropic/gemini-cli'
59
+ });
60
+ }
61
+
62
+ // Resolve workspace path
63
+ const workspacePath = workspace || process.cwd();
64
+
65
+ // Use SessionManager for session sync pattern
66
+ const frontendConversationId = conversationId || uuidv4();
67
+ const { sessionId, isNew: isNewSession } = await sessionManager.getOrCreateSession(
68
+ frontendConversationId,
69
+ 'gemini',
70
+ workspacePath
71
+ );
72
+
73
+ console.log(`[Gemini] Session resolved: ${sessionId} (new: ${isNewSession})`);
74
+
75
+ // Set up SSE
76
+ res.setHeader('Content-Type', 'text/event-stream');
77
+ res.setHeader('Cache-Control', 'no-cache');
78
+ res.setHeader('Connection', 'keep-alive');
79
+ res.setHeader('X-Accel-Buffering', 'no'); // Disable nginx buffering
80
+
81
+ // Send initial event
82
+ res.write(`data: ${JSON.stringify({
83
+ type: 'message_start',
84
+ messageId: `user-${Date.now()}`,
85
+ sessionId,
86
+ engine: 'gemini'
87
+ })}\n\n`);
88
+
89
+ // Use optimized ContextBridge for token-aware context building
90
+ // Note: Gemini has larger context window, uses preferSummary: false config
91
+ const lastEngine = Message.getLastEngine(sessionId);
92
+ const contextResult = await contextBridge.buildContext({
93
+ sessionId,
94
+ fromEngine: lastEngine,
95
+ toEngine: 'gemini',
96
+ userMessage: message
97
+ });
98
+
99
+ const promptWithContext = contextResult.prompt;
100
+ const isEngineBridge = contextResult.isEngineBridge;
101
+
102
+ console.log(`[Gemini] Context: ${contextResult.contextTokens} tokens from ${contextResult.contextSource}, total: ${contextResult.totalTokens}`);
103
+
104
+ // Notify frontend about engine switch
105
+ if (isEngineBridge) {
106
+ res.write(`data: ${JSON.stringify({
107
+ type: 'status',
108
+ category: 'system',
109
+ message: `Context bridged from ${lastEngine}`,
110
+ icon: '🔄'
111
+ })}\n\n`);
112
+ }
113
+
114
+ // Save user message to DB
115
+ try {
116
+ Message.create(
117
+ sessionId,
118
+ 'user',
119
+ message,
120
+ { workspace: workspacePath, model },
121
+ Date.now(),
122
+ 'gemini'
123
+ );
124
+ } catch (dbErr) {
125
+ console.warn('[Gemini] Failed to save user message:', dbErr.message);
126
+ }
127
+
128
+ // Update session title if new chat
129
+ if (isNewSession) {
130
+ const title = sessionManager.extractTitle(message);
131
+ sessionManager.updateSessionTitle(sessionId, title);
132
+ }
133
+
134
+ console.log('[Gemini] Calling Gemini CLI...');
135
+
136
+ // Call Gemini wrapper with SSE streaming
137
+ const result = await geminiWrapper.sendMessage({
138
+ prompt: promptWithContext,
139
+ sessionId,
140
+ model,
141
+ workspacePath,
142
+ onStatus: (event) => {
143
+ // Forward all events to SSE
144
+ res.write(`data: ${JSON.stringify(event)}\n\n`);
145
+ }
146
+ });
147
+
148
+ console.log(`[Gemini] Response received: ${result.text?.length || 0} chars`);
149
+
150
+ // Save assistant response to DB
151
+ try {
152
+ Message.create(
153
+ sessionId,
154
+ 'assistant',
155
+ result.text,
156
+ { model, usage: result.usage },
157
+ Date.now(),
158
+ 'gemini'
159
+ );
160
+ } catch (dbErr) {
161
+ console.warn('[Gemini] Failed to save assistant message:', dbErr.message);
162
+ }
163
+
164
+ // Smart auto-summary: trigger based on message count and engine bridging
165
+ if (contextBridge.shouldTriggerSummary(sessionId, isEngineBridge)) {
166
+ contextBridge.triggerSummaryGeneration(sessionId, '[Gemini]');
167
+ }
168
+
169
+ // Send final message with full content
170
+ res.write(`data: ${JSON.stringify({
171
+ type: 'message_done',
172
+ content: result.text,
173
+ usage: result.usage,
174
+ sessionId,
175
+ conversationId: frontendConversationId,
176
+ engine: 'gemini',
177
+ model
178
+ })}\n\n`);
179
+
180
+ res.end();
181
+ console.log('[Gemini] === REQUEST COMPLETE ===');
182
+
183
+ } catch (error) {
184
+ console.error('[Gemini] Error:', error);
185
+
186
+ if (!res.headersSent) {
187
+ res.status(500).json({ error: error.message });
188
+ } else {
189
+ // Send error via SSE if headers already sent
190
+ res.write(`data: ${JSON.stringify({
191
+ type: 'error',
192
+ error: error.message
193
+ })}\n\n`);
194
+ res.end();
195
+ }
196
+ }
197
+ });
198
+
199
+ /**
200
+ * GET /api/v1/gemini/status
201
+ * Check if Gemini CLI is available
202
+ */
203
+ router.get('/status', async (req, res) => {
204
+ try {
205
+ const isAvailable = await geminiWrapper.isAvailable();
206
+ res.json({
207
+ available: isAvailable,
208
+ defaultModel: geminiWrapper.getDefaultModel(),
209
+ models: geminiWrapper.getAvailableModels()
210
+ });
211
+ } catch (error) {
212
+ console.error('[Gemini] Status check error:', error);
213
+ res.status(500).json({ error: error.message });
214
+ }
215
+ });
216
+
217
+ /**
218
+ * GET /api/v1/gemini/models
219
+ * List available Gemini models
220
+ */
221
+ router.get('/models', (req, res) => {
222
+ res.json({
223
+ models: geminiWrapper.getAvailableModels(),
224
+ default: geminiWrapper.getDefaultModel()
225
+ });
226
+ });
227
+
228
+ module.exports = router;