@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,168 @@
1
+ const express = require('express');
2
+ const rateLimit = require('express-rate-limit');
3
+ const bcrypt = require('bcryptjs');
4
+ const User = require('../models/User');
5
+ const { generateToken, authMiddleware } = require('../middleware/auth');
6
+ const { getConfig } = require('../../config/manager');
7
+
8
+ const router = express.Router();
9
+
10
+ /**
11
+ * Check config user (admin from init)
12
+ * Returns user object compatible with DB user structure
13
+ */
14
+ function findConfigUser(username) {
15
+ const config = getConfig();
16
+ if (config.auth && config.auth.user === username) {
17
+ return {
18
+ id: 'config-admin',
19
+ username: config.auth.user,
20
+ password_hash: config.auth.pass_hash,
21
+ role: 'admin',
22
+ is_locked: false,
23
+ failed_attempts: 0,
24
+ created_at: Date.now()
25
+ };
26
+ }
27
+ return null;
28
+ }
29
+
30
+ /**
31
+ * Verify password for config user
32
+ */
33
+ function verifyConfigPassword(passHash, password) {
34
+ return bcrypt.compareSync(password, passHash);
35
+ }
36
+
37
+ // Rate limiter: max 5 login attempts per 15 minutes per IP
38
+ const loginLimiter = rateLimit({
39
+ windowMs: 15 * 60 * 1000, // 15 minutes
40
+ max: 10,
41
+ message: {
42
+ error: 'Too many login attempts, please try again later',
43
+ retry_after: 15 * 60
44
+ },
45
+ standardHeaders: true,
46
+ legacyHeaders: false
47
+ });
48
+
49
+ // POST /api/v1/auth/login
50
+ router.post('/login', loginLimiter, async (req, res) => {
51
+ try {
52
+ const { username, password } = req.body;
53
+ const ipAddress = req.ip || req.connection.remoteAddress;
54
+
55
+ if (!username || !password) {
56
+ return res.status(400).json({ error: 'Username and password required' });
57
+ }
58
+
59
+ // Check IP rate limiting (additional layer)
60
+ const recentAttempts = User.getRecentLoginAttempts(ipAddress);
61
+ if (recentAttempts > 20) {
62
+ return res.status(429).json({
63
+ error: 'Too many failed attempts from this IP',
64
+ retry_after: 15 * 60
65
+ });
66
+ }
67
+
68
+ // First check config user (admin from init)
69
+ let user = findConfigUser(username);
70
+ let isConfigUser = !!user;
71
+
72
+ // If not config user, check database
73
+ if (!user) {
74
+ user = User.findByUsername(username);
75
+ }
76
+
77
+ if (!user) {
78
+ // Log failed attempt even for non-existent user
79
+ User.logLoginAttempt(ipAddress, username, false);
80
+ return res.status(401).json({ error: 'Invalid credentials' });
81
+ }
82
+
83
+ // Check if account is locked (only for DB users)
84
+ if (!isConfigUser && User.isAccountLocked(user)) {
85
+ User.logLoginAttempt(ipAddress, username, false);
86
+ const remainingMs = user.locked_until - Date.now();
87
+ return res.status(403).json({
88
+ error: 'Account locked due to failed login attempts',
89
+ locked_until: user.locked_until,
90
+ retry_after: Math.ceil(remainingMs / 1000)
91
+ });
92
+ }
93
+
94
+ // Verify password
95
+ let isValid;
96
+ if (isConfigUser) {
97
+ isValid = verifyConfigPassword(user.password_hash, password);
98
+ } else {
99
+ isValid = User.verifyPassword(user, password);
100
+ }
101
+
102
+ if (!isValid) {
103
+ User.logLoginAttempt(ipAddress, username, false);
104
+ if (!isConfigUser) {
105
+ User.incrementFailedAttempts(user.id);
106
+ }
107
+ return res.status(401).json({ error: 'Invalid credentials' });
108
+ }
109
+
110
+ // Success
111
+ User.logLoginAttempt(ipAddress, username, true);
112
+ if (!isConfigUser) {
113
+ User.resetFailedAttempts(user.id);
114
+ User.updateLastLogin(user.id);
115
+ }
116
+
117
+ const token = generateToken(user);
118
+
119
+ res.json({
120
+ token,
121
+ user: {
122
+ id: user.id,
123
+ username: user.username,
124
+ role: user.role
125
+ }
126
+ });
127
+ } catch (error) {
128
+ console.error('Login error:', error);
129
+ res.status(500).json({ error: 'Internal server error' });
130
+ }
131
+ });
132
+
133
+ // GET /api/v1/auth/me
134
+ router.get('/me', authMiddleware, (req, res) => {
135
+ // Config user is already validated by authMiddleware
136
+ // Just return the user info from req.user
137
+ if (req.user.id === 'config-admin') {
138
+ const config = getConfig();
139
+ return res.json({
140
+ id: req.user.id,
141
+ username: req.user.username,
142
+ role: req.user.role,
143
+ created_at: Date.now(),
144
+ last_login: null
145
+ });
146
+ }
147
+
148
+ const user = User.findById(req.user.id);
149
+ if (!user) {
150
+ return res.status(404).json({ error: 'User not found' });
151
+ }
152
+
153
+ res.json({
154
+ id: user.id,
155
+ username: user.username,
156
+ role: user.role,
157
+ created_at: user.created_at,
158
+ last_login: user.last_login
159
+ });
160
+ });
161
+
162
+ // POST /api/v1/auth/logout
163
+ router.post('/logout', authMiddleware, (req, res) => {
164
+ // With JWT, logout is handled client-side by removing token
165
+ res.json({ message: 'Logged out successfully' });
166
+ });
167
+
168
+ module.exports = router;
@@ -0,0 +1,206 @@
1
+ const express = require('express');
2
+ const ClaudeWrapper = require('../services/claude-wrapper');
3
+ const Message = require('../models/Message');
4
+ const Conversation = require('../models/Conversation');
5
+ const { prepare } = require('../db');
6
+ const { v4: uuidv4 } = require('uuid');
7
+ const HistorySync = require('../services/history-sync');
8
+ const sessionManager = require('../services/session-manager');
9
+ const SummaryGenerator = require('../services/summary-generator');
10
+ const contextBridge = require('../services/context-bridge');
11
+ const { invalidateConversations } = require('../services/cache');
12
+
13
+ const router = express.Router();
14
+ const claudeWrapper = new ClaudeWrapper();
15
+ const historySync = new HistorySync();
16
+ const summaryGenerator = new SummaryGenerator();
17
+
18
+ /**
19
+ * POST /api/v1/chat
20
+ * Send message to Claude Code CLI with SSE streaming
21
+ *
22
+ * Request body:
23
+ * {
24
+ * "conversationId": "uuid" (optional for new chat)
25
+ * "message": "user prompt",
26
+ * "model": "sonnet" (optional),
27
+ * "workspace": "/path" (optional for new chat)
28
+ * }
29
+ *
30
+ * Response: SSE stream
31
+ * - Status events (tool use, file ops, thinking)
32
+ * - Final message text and sessionId
33
+ */
34
+ router.post('/', async (req, res) => {
35
+ try {
36
+ console.log('[Chat] === NEW CHAT REQUEST ===');
37
+ console.log('[Chat] Body:', JSON.stringify(req.body, null, 2));
38
+
39
+ const { conversationId, message, model = 'sonnet', workspace } = req.body;
40
+
41
+ console.log(`[Chat] conversationId: ${conversationId}`);
42
+ console.log(`[Chat] message: ${message?.substring(0, 100)}`);
43
+ console.log(`[Chat] model: ${model}`);
44
+ console.log(`[Chat] workspace: ${workspace}`);
45
+
46
+ if (!message) {
47
+ console.log('[Chat] ERROR: message required');
48
+ return res.status(400).json({ error: 'message required' });
49
+ }
50
+
51
+ // Resolve workspace path
52
+ const workspacePath = workspace || process.cwd();
53
+
54
+ // Use SessionManager for session sync pattern
55
+ // conversationId → sessionId (per engine)
56
+ const frontendConversationId = conversationId || uuidv4();
57
+ const { sessionId, isNew: isNewSession } = await sessionManager.getOrCreateSession(
58
+ frontendConversationId,
59
+ 'claude',
60
+ workspacePath
61
+ );
62
+
63
+ console.log(`[Chat] Session resolved: ${sessionId} (new: ${isNewSession})`);
64
+ const isNewChat = isNewSession;
65
+
66
+ // Set up SSE
67
+ res.setHeader('Content-Type', 'text/event-stream');
68
+ res.setHeader('Cache-Control', 'no-cache');
69
+ res.setHeader('Connection', 'keep-alive');
70
+
71
+ // Send initial event
72
+ res.write(`data: ${JSON.stringify({ type: 'message_start', messageId: `user-${Date.now()}`, sessionId })}\n\n`);
73
+
74
+ // Use optimized ContextBridge for token-aware context building
75
+ const lastEngine = Message.getLastEngine(sessionId);
76
+ const contextResult = await contextBridge.buildContext({
77
+ sessionId,
78
+ fromEngine: lastEngine,
79
+ toEngine: 'claude',
80
+ userMessage: message
81
+ });
82
+
83
+ const promptWithContext = contextResult.prompt;
84
+ const isEngineBridge = contextResult.isEngineBridge;
85
+
86
+ console.log(`[Chat] Context: ${contextResult.contextTokens} tokens from ${contextResult.contextSource}, total: ${contextResult.totalTokens}`);
87
+
88
+ // Notify frontend about engine switch
89
+ if (isEngineBridge) {
90
+ res.write(`data: ${JSON.stringify({
91
+ type: 'status',
92
+ category: 'engine_switch',
93
+ message: `Continuing conversation from ${lastEngine}`
94
+ })}\n\n`);
95
+ }
96
+
97
+ try {
98
+ // Save user message to database with engine tracking
99
+ try {
100
+ const userMessage = Message.create(
101
+ sessionId,
102
+ 'user',
103
+ message,
104
+ { workspace: workspacePath },
105
+ Date.now(),
106
+ 'claude' // Engine tracking for context bridging
107
+ );
108
+ console.log(`[Chat] Saved user message: ${userMessage.id} (engine: claude)`);
109
+
110
+ } catch (msgErr) {
111
+ console.warn('[Chat] Failed to save user message:', msgErr.message);
112
+ }
113
+
114
+ // Call Claude Code wrapper with workspace path for --cwd
115
+ const result = await claudeWrapper.sendMessage({
116
+ prompt: promptWithContext,
117
+ conversationId: sessionId,
118
+ model,
119
+ workspacePath, // Pass workspace for Claude CLI --cwd
120
+ onStatus: (event) => {
121
+ // Stream status events to client
122
+ res.write(`data: ${JSON.stringify(event)}\n\n`);
123
+ }
124
+ });
125
+
126
+ // Save assistant response to database with engine tracking
127
+ try {
128
+ const assistantMessage = Message.create(
129
+ sessionId,
130
+ 'assistant',
131
+ result.text,
132
+ { usage: result.usage, model },
133
+ Date.now(),
134
+ 'claude' // Engine tracking for context bridging
135
+ );
136
+ console.log(`[Chat] Saved assistant message: ${assistantMessage.id} (engine: claude)`);
137
+ } catch (msgErr) {
138
+ console.warn('[Chat] Failed to save assistant message:', msgErr.message);
139
+ }
140
+
141
+ // Sync from history after new session to persist in DB
142
+ if (isNewChat) {
143
+ try {
144
+ await historySync.sync(true);
145
+ invalidateConversations(); // Clear cache for fresh sidebar
146
+ res.write(`data: ${JSON.stringify({
147
+ type: 'session_created',
148
+ sessionId
149
+ })}\n\n`);
150
+ } catch (syncErr) {
151
+ console.warn('[Chat] History sync failed after new chat:', syncErr.message);
152
+ }
153
+
154
+ // Generate AI title in background (fire-and-forget)
155
+ // Don't await - user shouldn't wait for title generation
156
+ summaryGenerator.generateTitle(message, result.text)
157
+ .then(title => {
158
+ sessionManager.updateSessionTitle(sessionId, title);
159
+ console.log(`[Chat] AI-generated title: ${title}`);
160
+ })
161
+ .catch(err => {
162
+ console.warn('[Chat] Title generation failed, using fallback:', err.message);
163
+ // Fallback: use truncated first message
164
+ const fallbackTitle = sessionManager.extractTitle(message);
165
+ sessionManager.updateSessionTitle(sessionId, fallbackTitle);
166
+ });
167
+ }
168
+
169
+ // Smart auto-summary: trigger based on message count and engine bridging
170
+ if (contextBridge.shouldTriggerSummary(sessionId, isEngineBridge)) {
171
+ contextBridge.triggerSummaryGeneration(sessionId, '[Chat]');
172
+ }
173
+
174
+ // Send completion event
175
+ res.write(`data: ${JSON.stringify({
176
+ type: 'message_done',
177
+ messageId: `assistant-${Date.now()}`,
178
+ content: result.text,
179
+ usage: result.usage,
180
+ sessionId
181
+ })}\n\n`);
182
+
183
+ res.end();
184
+
185
+ } catch (error) {
186
+ console.error('[Chat] Error:', error);
187
+
188
+ // Send error event
189
+ res.write(`data: ${JSON.stringify({
190
+ type: 'error',
191
+ error: error.message
192
+ })}\n\n`);
193
+
194
+ res.end();
195
+ }
196
+
197
+ } catch (error) {
198
+ console.error('[Chat] Request error:', error);
199
+
200
+ if (!res.headersSent) {
201
+ res.status(500).json({ error: error.message });
202
+ }
203
+ }
204
+ });
205
+
206
+ module.exports = router;
@@ -0,0 +1,205 @@
1
+ const express = require('express');
2
+ const CodexWrapper = require('../services/codex-wrapper');
3
+ const Message = require('../models/Message');
4
+ const { prepare } = require('../db');
5
+ const { v4: uuidv4 } = require('uuid');
6
+ const sessionManager = require('../services/session-manager');
7
+ const contextBridge = require('../services/context-bridge');
8
+
9
+ const router = express.Router();
10
+ const codexWrapper = new CodexWrapper();
11
+
12
+ /**
13
+ * POST /api/v1/codex
14
+ * Send message to Codex CLI with SSE streaming
15
+ *
16
+ * Request body:
17
+ * {
18
+ * "conversationId": "uuid" (optional for new chat)
19
+ * "message": "user prompt",
20
+ * "model": "gpt-5.1-codex-max" (optional),
21
+ * "reasoningEffort": "medium" (optional: low, medium, high, xhigh)
22
+ * "workspace": "/path" (optional for new chat)
23
+ * }
24
+ *
25
+ * Response: SSE stream
26
+ * - Status events (tool use, reasoning)
27
+ * - Final message text and threadId
28
+ */
29
+ router.post('/', async (req, res) => {
30
+ try {
31
+ console.log('[Codex] === NEW CODEX REQUEST ===');
32
+ console.log('[Codex] Body:', JSON.stringify(req.body, null, 2));
33
+
34
+ const { conversationId, message, model = 'gpt-5.1-codex-max', reasoningEffort, workspace } = req.body;
35
+
36
+ console.log(`[Codex] conversationId: ${conversationId}`);
37
+ console.log(`[Codex] message: ${message?.substring(0, 100)}`);
38
+ console.log(`[Codex] model: ${model}`);
39
+ console.log(`[Codex] reasoningEffort: ${reasoningEffort}`);
40
+ console.log(`[Codex] workspace: ${workspace}`);
41
+
42
+ if (!message) {
43
+ console.log('[Codex] ERROR: message required');
44
+ return res.status(400).json({ error: 'message required' });
45
+ }
46
+
47
+ // Check if Codex CLI is available
48
+ const isAvailable = await codexWrapper.isAvailable();
49
+ if (!isAvailable) {
50
+ return res.status(503).json({ error: 'Codex CLI not available' });
51
+ }
52
+
53
+ const hasExec = await codexWrapper.hasExecSupport();
54
+ if (!hasExec) {
55
+ return res.status(503).json({ error: 'Codex CLI does not support exec subcommand. Please update to 0.62.1+' });
56
+ }
57
+
58
+ // Resolve workspace path
59
+ const workspacePath = workspace || process.cwd();
60
+
61
+ // Use SessionManager for session sync pattern
62
+ // conversationId → sessionId (per engine)
63
+ const frontendConversationId = conversationId || uuidv4();
64
+ const { sessionId, isNew: isNewSession } = await sessionManager.getOrCreateSession(
65
+ frontendConversationId,
66
+ 'codex',
67
+ workspacePath
68
+ );
69
+
70
+ console.log(`[Codex] Session resolved: ${sessionId} (new: ${isNewSession})`);
71
+ const isNewChat = isNewSession;
72
+
73
+ // Set up SSE
74
+ res.setHeader('Content-Type', 'text/event-stream');
75
+ res.setHeader('Cache-Control', 'no-cache');
76
+ res.setHeader('Connection', 'keep-alive');
77
+
78
+ // Send initial event
79
+ res.write(`data: ${JSON.stringify({ type: 'message_start', messageId: `user-${Date.now()}`, sessionId })}\n\n`);
80
+
81
+ // Use optimized ContextBridge for token-aware context building
82
+ // Note: Codex uses codeOnly mode for better code-focused context
83
+ const lastEngine = Message.getLastEngine(sessionId);
84
+ const contextResult = await contextBridge.buildContext({
85
+ sessionId,
86
+ fromEngine: lastEngine,
87
+ toEngine: 'codex',
88
+ userMessage: message
89
+ });
90
+
91
+ const promptWithContext = contextResult.prompt;
92
+ const isEngineBridge = contextResult.isEngineBridge;
93
+
94
+ console.log(`[Codex] Context: ${contextResult.contextTokens} tokens from ${contextResult.contextSource}, total: ${contextResult.totalTokens}`);
95
+
96
+ // Notify frontend about engine switch
97
+ if (isEngineBridge) {
98
+ res.write(`data: ${JSON.stringify({
99
+ type: 'status',
100
+ category: 'engine_switch',
101
+ message: `Continuing conversation from ${lastEngine}`
102
+ })}\n\n`);
103
+ }
104
+
105
+ try {
106
+ // Save user message to database with engine tracking
107
+ try {
108
+ const userMessage = Message.create(
109
+ sessionId,
110
+ 'user',
111
+ message,
112
+ { workspace: workspacePath },
113
+ Date.now(),
114
+ 'codex' // Engine tracking for context bridging
115
+ );
116
+ console.log(`[Codex] Saved user message: ${userMessage.id} (engine: codex)`);
117
+ } catch (msgErr) {
118
+ console.warn('[Codex] Failed to save user message:', msgErr.message);
119
+ }
120
+
121
+ // Call Codex wrapper with workspace path
122
+ const result = await codexWrapper.sendMessage({
123
+ prompt: promptWithContext,
124
+ model,
125
+ sessionId,
126
+ reasoningEffort,
127
+ workspacePath,
128
+ onStatus: (event) => {
129
+ // Stream status events to client
130
+ res.write(`data: ${JSON.stringify(event)}\n\n`);
131
+ }
132
+ });
133
+
134
+ // Save assistant response to database with engine tracking
135
+ try {
136
+ const assistantMessage = Message.create(
137
+ sessionId,
138
+ 'assistant',
139
+ result.text,
140
+ { usage: result.usage, model },
141
+ Date.now(),
142
+ 'codex' // Engine tracking for context bridging
143
+ );
144
+ console.log(`[Codex] Saved assistant message: ${assistantMessage.id} (engine: codex)`);
145
+ } catch (msgErr) {
146
+ console.warn('[Codex] Failed to save assistant message:', msgErr.message);
147
+ }
148
+
149
+ // Smart auto-summary: trigger based on message count and engine bridging
150
+ if (contextBridge.shouldTriggerSummary(sessionId, isEngineBridge)) {
151
+ contextBridge.triggerSummaryGeneration(sessionId, '[Codex]');
152
+ }
153
+
154
+ // Send completion event
155
+ res.write(`data: ${JSON.stringify({
156
+ type: 'message_done',
157
+ messageId: `assistant-${Date.now()}`,
158
+ content: result.text,
159
+ usage: result.usage,
160
+ sessionId
161
+ })}\n\n`);
162
+
163
+ res.end();
164
+
165
+ } catch (error) {
166
+ console.error('[Codex] Error:', error);
167
+
168
+ // Send error event
169
+ res.write(`data: ${JSON.stringify({
170
+ type: 'error',
171
+ error: error.message
172
+ })}\n\n`);
173
+
174
+ res.end();
175
+ }
176
+
177
+ } catch (error) {
178
+ console.error('[Codex] Request error:', error);
179
+
180
+ if (!res.headersSent) {
181
+ res.status(500).json({ error: error.message });
182
+ }
183
+ }
184
+ });
185
+
186
+ /**
187
+ * GET /api/v1/codex/status
188
+ * Check Codex CLI availability
189
+ */
190
+ router.get('/status', async (req, res) => {
191
+ try {
192
+ const isAvailable = await codexWrapper.isAvailable();
193
+ const hasExec = isAvailable ? await codexWrapper.hasExecSupport() : false;
194
+
195
+ res.json({
196
+ available: isAvailable,
197
+ execSupport: hasExec,
198
+ timestamp: new Date().toISOString()
199
+ });
200
+ } catch (error) {
201
+ res.status(500).json({ error: error.message });
202
+ }
203
+ });
204
+
205
+ module.exports = router;