@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.
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/bin/nexuscli.js +117 -0
- package/frontend/dist/apple-touch-icon.png +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/frontend/dist/assets/index-Bn_l1e6e.css +1 -0
- package/frontend/dist/assets/index-CikJbUR5.js +8617 -0
- package/frontend/dist/browserconfig.xml +12 -0
- package/frontend/dist/favicon-16x16.png +0 -0
- package/frontend/dist/favicon-32x32.png +0 -0
- package/frontend/dist/favicon-48x48.png +0 -0
- package/frontend/dist/favicon.ico +0 -0
- package/frontend/dist/icon-192.png +0 -0
- package/frontend/dist/icon-512.png +0 -0
- package/frontend/dist/icon-maskable-192.png +0 -0
- package/frontend/dist/icon-maskable-512.png +0 -0
- package/frontend/dist/index.html +79 -0
- package/frontend/dist/manifest.json +75 -0
- package/frontend/dist/sw.js +122 -0
- package/frontend/package.json +28 -0
- package/lib/cli/api.js +156 -0
- package/lib/cli/boot.js +172 -0
- package/lib/cli/config.js +185 -0
- package/lib/cli/engines.js +257 -0
- package/lib/cli/init.js +660 -0
- package/lib/cli/logs.js +72 -0
- package/lib/cli/start.js +220 -0
- package/lib/cli/status.js +187 -0
- package/lib/cli/stop.js +64 -0
- package/lib/cli/uninstall.js +194 -0
- package/lib/cli/users.js +295 -0
- package/lib/cli/workspaces.js +337 -0
- package/lib/config/manager.js +233 -0
- package/lib/server/.env.example +20 -0
- package/lib/server/db/adapter.js +314 -0
- package/lib/server/db/drivers/better-sqlite3.js +38 -0
- package/lib/server/db/drivers/sql-js.js +75 -0
- package/lib/server/db/migrate.js +174 -0
- package/lib/server/db/migrations/001_ultra_light_schema.sql +96 -0
- package/lib/server/db/migrations/002_session_conversation_mapping.sql +19 -0
- package/lib/server/db/migrations/003_message_engine_tracking.sql +18 -0
- package/lib/server/db/migrations/004_performance_indexes.sql +16 -0
- package/lib/server/db.js +2 -0
- package/lib/server/lib/cli-wrapper.js +164 -0
- package/lib/server/lib/output-parser.js +132 -0
- package/lib/server/lib/pty-adapter.js +57 -0
- package/lib/server/middleware/auth.js +103 -0
- package/lib/server/models/Conversation.js +259 -0
- package/lib/server/models/Message.js +228 -0
- package/lib/server/models/User.js +115 -0
- package/lib/server/package-lock.json +5895 -0
- package/lib/server/routes/auth.js +168 -0
- package/lib/server/routes/chat.js +206 -0
- package/lib/server/routes/codex.js +205 -0
- package/lib/server/routes/conversations.js +224 -0
- package/lib/server/routes/gemini.js +228 -0
- package/lib/server/routes/jobs.js +317 -0
- package/lib/server/routes/messages.js +60 -0
- package/lib/server/routes/models.js +198 -0
- package/lib/server/routes/sessions.js +285 -0
- package/lib/server/routes/upload.js +134 -0
- package/lib/server/routes/wake-lock.js +95 -0
- package/lib/server/routes/workspace.js +80 -0
- package/lib/server/routes/workspaces.js +142 -0
- package/lib/server/scripts/cleanup-ghost-sessions.js +71 -0
- package/lib/server/scripts/seed-users.js +37 -0
- package/lib/server/scripts/test-history-access.js +50 -0
- package/lib/server/server.js +227 -0
- package/lib/server/services/cache.js +85 -0
- package/lib/server/services/claude-wrapper.js +312 -0
- package/lib/server/services/cli-loader.js +384 -0
- package/lib/server/services/codex-output-parser.js +277 -0
- package/lib/server/services/codex-wrapper.js +224 -0
- package/lib/server/services/context-bridge.js +289 -0
- package/lib/server/services/gemini-output-parser.js +398 -0
- package/lib/server/services/gemini-wrapper.js +249 -0
- package/lib/server/services/history-sync.js +407 -0
- package/lib/server/services/output-parser.js +415 -0
- package/lib/server/services/session-manager.js +465 -0
- package/lib/server/services/summary-generator.js +259 -0
- package/lib/server/services/workspace-manager.js +516 -0
- package/lib/server/tests/history-sync.test.js +90 -0
- package/lib/server/tests/integration-session-sync.test.js +151 -0
- package/lib/server/tests/integration.test.js +76 -0
- package/lib/server/tests/performance.test.js +118 -0
- package/lib/server/tests/services.test.js +160 -0
- package/lib/setup/postinstall.js +216 -0
- package/lib/utils/paths.js +107 -0
- package/lib/utils/termux.js +145 -0
- 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;
|