@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,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;
|