@mmmbuto/nexuscli 0.7.6 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -27
- package/frontend/dist/assets/{index-CikJbUR5.js → index-BbBoc8w4.js} +1704 -1704
- package/frontend/dist/assets/{index-Bn_l1e6e.css → index-WfmfixF4.css} +1 -1
- package/frontend/dist/index.html +2 -2
- package/lib/server/routes/chat.js +70 -8
- package/lib/server/routes/codex.js +70 -9
- package/lib/server/routes/gemini.js +75 -15
- package/lib/server/routes/sessions.js +22 -2
- package/lib/server/server.js +11 -0
- package/lib/server/services/codex-wrapper.js +14 -5
- package/lib/server/services/context-bridge.js +165 -42
- package/lib/server/services/gemini-wrapper.js +28 -9
- package/lib/server/services/session-importer.js +155 -0
- package/lib/server/services/session-manager.js +20 -0
- package/lib/server/services/workspace-manager.js +3 -8
- package/package.json +1 -1
|
@@ -9,6 +9,20 @@ const contextBridge = require('../services/context-bridge');
|
|
|
9
9
|
const router = express.Router();
|
|
10
10
|
const codexWrapper = new CodexWrapper();
|
|
11
11
|
|
|
12
|
+
function ensureConversation(conversationId, workspacePath) {
|
|
13
|
+
try {
|
|
14
|
+
const stmt = prepare(`
|
|
15
|
+
INSERT OR IGNORE INTO conversations (id, title, created_at, updated_at, metadata)
|
|
16
|
+
VALUES (?, ?, ?, ?, ?)
|
|
17
|
+
`);
|
|
18
|
+
const now = Date.now();
|
|
19
|
+
const metadata = workspacePath ? JSON.stringify({ workspace: workspacePath }) : null;
|
|
20
|
+
stmt.run(conversationId, 'New Chat', now, now, metadata);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.warn('[Codex] Failed to ensure conversation exists:', err.message);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
12
26
|
/**
|
|
13
27
|
* POST /api/v1/codex
|
|
14
28
|
* Send message to Codex CLI with SSE streaming
|
|
@@ -61,6 +75,8 @@ router.post('/', async (req, res) => {
|
|
|
61
75
|
// Use SessionManager for session sync pattern
|
|
62
76
|
// conversationId → sessionId (per engine)
|
|
63
77
|
const frontendConversationId = conversationId || uuidv4();
|
|
78
|
+
ensureConversation(frontendConversationId, workspacePath);
|
|
79
|
+
|
|
64
80
|
const { sessionId, isNew: isNewSession } = await sessionManager.getOrCreateSession(
|
|
65
81
|
frontendConversationId,
|
|
66
82
|
'codex',
|
|
@@ -80,10 +96,10 @@ router.post('/', async (req, res) => {
|
|
|
80
96
|
res.setHeader('Connection', 'keep-alive');
|
|
81
97
|
|
|
82
98
|
// Send initial event
|
|
83
|
-
res.write(`data: ${JSON.stringify({ type: 'message_start', messageId: `user-${Date.now()}`, sessionId })}\n\n`);
|
|
99
|
+
res.write(`data: ${JSON.stringify({ type: 'message_start', messageId: `user-${Date.now()}`, sessionId, conversationId: frontendConversationId })}\n\n`);
|
|
84
100
|
|
|
85
101
|
// Check if this is an engine switch (requires context bridging)
|
|
86
|
-
const lastEngine = Message.getLastEngine(
|
|
102
|
+
const lastEngine = Message.getLastEngine(frontendConversationId);
|
|
87
103
|
const isEngineBridge = lastEngine && lastEngine !== 'codex';
|
|
88
104
|
|
|
89
105
|
// IMPORTANT: Skip contextBridge for Codex native resume!
|
|
@@ -94,6 +110,7 @@ router.post('/', async (req, res) => {
|
|
|
94
110
|
if (isEngineBridge) {
|
|
95
111
|
// Engine switch: need context from previous engine
|
|
96
112
|
const contextResult = await contextBridge.buildContext({
|
|
113
|
+
conversationId: frontendConversationId,
|
|
97
114
|
sessionId,
|
|
98
115
|
fromEngine: lastEngine,
|
|
99
116
|
toEngine: 'codex',
|
|
@@ -119,7 +136,7 @@ router.post('/', async (req, res) => {
|
|
|
119
136
|
// Save user message to database with engine tracking
|
|
120
137
|
try {
|
|
121
138
|
const userMessage = Message.create(
|
|
122
|
-
|
|
139
|
+
frontendConversationId,
|
|
123
140
|
'user',
|
|
124
141
|
message,
|
|
125
142
|
{ workspace: workspacePath },
|
|
@@ -127,6 +144,7 @@ router.post('/', async (req, res) => {
|
|
|
127
144
|
'codex' // Engine tracking for context bridging
|
|
128
145
|
);
|
|
129
146
|
console.log(`[Codex] Saved user message: ${userMessage.id} (engine: codex)`);
|
|
147
|
+
sessionManager.bumpSessionActivity(sessionId, 1);
|
|
130
148
|
} catch (msgErr) {
|
|
131
149
|
console.warn('[Codex] Failed to save user message:', msgErr.message);
|
|
132
150
|
}
|
|
@@ -138,21 +156,28 @@ router.post('/', async (req, res) => {
|
|
|
138
156
|
threadId: nativeThreadId, // Native Codex thread ID for resume
|
|
139
157
|
reasoningEffort,
|
|
140
158
|
workspacePath,
|
|
159
|
+
processId: sessionId, // Use Nexus sessionId for interrupt tracking
|
|
141
160
|
onStatus: (event) => {
|
|
142
161
|
// Stream status events to client
|
|
143
162
|
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
144
163
|
}
|
|
145
164
|
});
|
|
146
165
|
|
|
147
|
-
//
|
|
148
|
-
if (result.threadId
|
|
166
|
+
// Persist native threadId for future resume (always, even if unchanged)
|
|
167
|
+
if (result.threadId) {
|
|
149
168
|
sessionManager.setNativeThreadId(sessionId, result.threadId);
|
|
169
|
+
const unchanged = nativeThreadId && nativeThreadId === result.threadId ? ' (unchanged)' : '';
|
|
170
|
+
console.log(`[Codex] Saved native threadId: ${result.threadId}${unchanged}`);
|
|
171
|
+
} else if (nativeThreadId) {
|
|
172
|
+
console.log(`[Codex] Reused existing native threadId: ${nativeThreadId}`);
|
|
173
|
+
} else {
|
|
174
|
+
console.warn('[Codex] No threadId returned; resume may not work for next turn');
|
|
150
175
|
}
|
|
151
176
|
|
|
152
177
|
// Save assistant response to database with engine tracking
|
|
153
178
|
try {
|
|
154
179
|
const assistantMessage = Message.create(
|
|
155
|
-
|
|
180
|
+
frontendConversationId,
|
|
156
181
|
'assistant',
|
|
157
182
|
result.text,
|
|
158
183
|
{ usage: result.usage, model },
|
|
@@ -160,13 +185,14 @@ router.post('/', async (req, res) => {
|
|
|
160
185
|
'codex' // Engine tracking for context bridging
|
|
161
186
|
);
|
|
162
187
|
console.log(`[Codex] Saved assistant message: ${assistantMessage.id} (engine: codex)`);
|
|
188
|
+
sessionManager.bumpSessionActivity(sessionId, 1);
|
|
163
189
|
} catch (msgErr) {
|
|
164
190
|
console.warn('[Codex] Failed to save assistant message:', msgErr.message);
|
|
165
191
|
}
|
|
166
192
|
|
|
167
193
|
// Smart auto-summary: trigger based on message count and engine bridging
|
|
168
|
-
if (contextBridge.shouldTriggerSummary(
|
|
169
|
-
contextBridge.triggerSummaryGeneration(
|
|
194
|
+
if (contextBridge.shouldTriggerSummary(frontendConversationId, isEngineBridge)) {
|
|
195
|
+
contextBridge.triggerSummaryGeneration(frontendConversationId, '[Codex]');
|
|
170
196
|
}
|
|
171
197
|
|
|
172
198
|
// Send completion event
|
|
@@ -175,7 +201,8 @@ router.post('/', async (req, res) => {
|
|
|
175
201
|
messageId: `assistant-${Date.now()}`,
|
|
176
202
|
content: result.text,
|
|
177
203
|
usage: result.usage,
|
|
178
|
-
sessionId
|
|
204
|
+
sessionId,
|
|
205
|
+
conversationId: frontendConversationId
|
|
179
206
|
})}\n\n`);
|
|
180
207
|
|
|
181
208
|
res.end();
|
|
@@ -220,4 +247,38 @@ router.get('/status', async (req, res) => {
|
|
|
220
247
|
}
|
|
221
248
|
});
|
|
222
249
|
|
|
250
|
+
/**
|
|
251
|
+
* POST /api/v1/codex/interrupt
|
|
252
|
+
* Interrupt running Codex CLI process
|
|
253
|
+
*
|
|
254
|
+
* Request body:
|
|
255
|
+
* {
|
|
256
|
+
* "sessionId": "uuid" - Session to interrupt
|
|
257
|
+
* }
|
|
258
|
+
*/
|
|
259
|
+
router.post('/interrupt', async (req, res) => {
|
|
260
|
+
try {
|
|
261
|
+
const { sessionId } = req.body;
|
|
262
|
+
|
|
263
|
+
if (!sessionId) {
|
|
264
|
+
return res.status(400).json({ error: 'sessionId required' });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
console.log(`[Codex] Interrupt request for session: ${sessionId}`);
|
|
268
|
+
|
|
269
|
+
const result = codexWrapper.interrupt(sessionId);
|
|
270
|
+
|
|
271
|
+
if (result.success) {
|
|
272
|
+
console.log(`[Codex] Interrupted session ${sessionId} via ${result.method}`);
|
|
273
|
+
} else {
|
|
274
|
+
console.log(`[Codex] Failed to interrupt session ${sessionId}: ${result.reason}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
res.json(result);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
console.error('[Codex] Interrupt error:', error);
|
|
280
|
+
res.status(500).json({ error: error.message });
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
223
284
|
module.exports = router;
|
|
@@ -15,6 +15,20 @@ const contextBridge = require('../services/context-bridge');
|
|
|
15
15
|
const router = express.Router();
|
|
16
16
|
const geminiWrapper = new GeminiWrapper();
|
|
17
17
|
|
|
18
|
+
function ensureConversation(conversationId, workspacePath) {
|
|
19
|
+
try {
|
|
20
|
+
const stmt = require('../db').prepare(`
|
|
21
|
+
INSERT OR IGNORE INTO conversations (id, title, created_at, updated_at, metadata)
|
|
22
|
+
VALUES (?, ?, ?, ?, ?)
|
|
23
|
+
`);
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
const metadata = workspacePath ? JSON.stringify({ workspace: workspacePath }) : null;
|
|
26
|
+
stmt.run(conversationId, 'New Chat', now, now, metadata);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.warn('[Gemini] Failed to ensure conversation exists:', err.message);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
18
32
|
/**
|
|
19
33
|
* POST /api/v1/gemini
|
|
20
34
|
* Send message to Gemini CLI with SSE streaming
|
|
@@ -64,6 +78,8 @@ router.post('/', async (req, res) => {
|
|
|
64
78
|
|
|
65
79
|
// Use SessionManager for session sync pattern
|
|
66
80
|
const frontendConversationId = conversationId || uuidv4();
|
|
81
|
+
ensureConversation(frontendConversationId, workspacePath);
|
|
82
|
+
|
|
67
83
|
const { sessionId, isNew: isNewSession } = await sessionManager.getOrCreateSession(
|
|
68
84
|
frontendConversationId,
|
|
69
85
|
'gemini',
|
|
@@ -87,11 +103,12 @@ router.post('/', async (req, res) => {
|
|
|
87
103
|
type: 'message_start',
|
|
88
104
|
messageId: `user-${Date.now()}`,
|
|
89
105
|
sessionId,
|
|
106
|
+
conversationId: frontendConversationId,
|
|
90
107
|
engine: 'gemini'
|
|
91
108
|
})}\n\n`);
|
|
92
109
|
|
|
93
110
|
// Check if this is an engine switch (requires context bridging)
|
|
94
|
-
const lastEngine = Message.getLastEngine(
|
|
111
|
+
const lastEngine = Message.getLastEngine(frontendConversationId);
|
|
95
112
|
const isEngineBridge = lastEngine && lastEngine !== 'gemini';
|
|
96
113
|
|
|
97
114
|
// IMPORTANT: Skip contextBridge for Gemini native resume!
|
|
@@ -102,6 +119,7 @@ router.post('/', async (req, res) => {
|
|
|
102
119
|
if (isEngineBridge) {
|
|
103
120
|
// Engine switch: need context from previous engine
|
|
104
121
|
const contextResult = await contextBridge.buildContext({
|
|
122
|
+
conversationId: frontendConversationId,
|
|
105
123
|
sessionId,
|
|
106
124
|
fromEngine: lastEngine,
|
|
107
125
|
toEngine: 'gemini',
|
|
@@ -127,13 +145,14 @@ router.post('/', async (req, res) => {
|
|
|
127
145
|
// Save user message to DB
|
|
128
146
|
try {
|
|
129
147
|
Message.create(
|
|
130
|
-
|
|
148
|
+
frontendConversationId,
|
|
131
149
|
'user',
|
|
132
150
|
message,
|
|
133
151
|
{ workspace: workspacePath, model },
|
|
134
152
|
Date.now(),
|
|
135
153
|
'gemini'
|
|
136
154
|
);
|
|
155
|
+
sessionManager.bumpSessionActivity(sessionId, 1);
|
|
137
156
|
} catch (dbErr) {
|
|
138
157
|
console.warn('[Gemini] Failed to save user message:', dbErr.message);
|
|
139
158
|
}
|
|
@@ -152,6 +171,7 @@ router.post('/', async (req, res) => {
|
|
|
152
171
|
threadId: nativeSessionId, // Native Gemini session ID for resume
|
|
153
172
|
model,
|
|
154
173
|
workspacePath,
|
|
174
|
+
processId: sessionId, // Use Nexus sessionId for interrupt tracking
|
|
155
175
|
onStatus: (event) => {
|
|
156
176
|
// Forward all events to SSE
|
|
157
177
|
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
@@ -160,29 +180,35 @@ router.post('/', async (req, res) => {
|
|
|
160
180
|
|
|
161
181
|
console.log(`[Gemini] Response received: ${result.text?.length || 0} chars`);
|
|
162
182
|
|
|
163
|
-
// Save native sessionId for future resume (if
|
|
164
|
-
if (result.sessionId
|
|
183
|
+
// Save native sessionId for future resume (always, even if unchanged)
|
|
184
|
+
if (result.sessionId) {
|
|
165
185
|
sessionManager.setNativeThreadId(sessionId, result.sessionId);
|
|
166
|
-
|
|
186
|
+
const unchanged = nativeSessionId && nativeSessionId === result.sessionId ? ' (unchanged)' : '';
|
|
187
|
+
console.log(`[Gemini] Saved native sessionId: ${result.sessionId}${unchanged}`);
|
|
188
|
+
} else if (nativeSessionId) {
|
|
189
|
+
console.log(`[Gemini] Reused existing native sessionId: ${nativeSessionId}`);
|
|
190
|
+
} else {
|
|
191
|
+
console.warn('[Gemini] No sessionId returned; resume may not work for next turn');
|
|
167
192
|
}
|
|
168
193
|
|
|
169
194
|
// Save assistant response to DB
|
|
170
195
|
try {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
196
|
+
Message.create(
|
|
197
|
+
frontendConversationId,
|
|
198
|
+
'assistant',
|
|
199
|
+
result.text,
|
|
200
|
+
{ model, usage: result.usage },
|
|
201
|
+
Date.now(),
|
|
202
|
+
'gemini'
|
|
203
|
+
);
|
|
204
|
+
sessionManager.bumpSessionActivity(sessionId, 1);
|
|
179
205
|
} catch (dbErr) {
|
|
180
206
|
console.warn('[Gemini] Failed to save assistant message:', dbErr.message);
|
|
181
207
|
}
|
|
182
208
|
|
|
183
209
|
// Smart auto-summary: trigger based on message count and engine bridging
|
|
184
|
-
if (contextBridge.shouldTriggerSummary(
|
|
185
|
-
contextBridge.triggerSummaryGeneration(
|
|
210
|
+
if (contextBridge.shouldTriggerSummary(frontendConversationId, isEngineBridge)) {
|
|
211
|
+
contextBridge.triggerSummaryGeneration(frontendConversationId, '[Gemini]');
|
|
186
212
|
}
|
|
187
213
|
|
|
188
214
|
// Send final message with full content
|
|
@@ -244,4 +270,38 @@ router.get('/models', (req, res) => {
|
|
|
244
270
|
});
|
|
245
271
|
});
|
|
246
272
|
|
|
273
|
+
/**
|
|
274
|
+
* POST /api/v1/gemini/interrupt
|
|
275
|
+
* Interrupt running Gemini CLI process
|
|
276
|
+
*
|
|
277
|
+
* Request body:
|
|
278
|
+
* {
|
|
279
|
+
* "sessionId": "uuid" - Session to interrupt
|
|
280
|
+
* }
|
|
281
|
+
*/
|
|
282
|
+
router.post('/interrupt', async (req, res) => {
|
|
283
|
+
try {
|
|
284
|
+
const { sessionId } = req.body;
|
|
285
|
+
|
|
286
|
+
if (!sessionId) {
|
|
287
|
+
return res.status(400).json({ error: 'sessionId required' });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
console.log(`[Gemini] Interrupt request for session: ${sessionId}`);
|
|
291
|
+
|
|
292
|
+
const result = geminiWrapper.interrupt(sessionId);
|
|
293
|
+
|
|
294
|
+
if (result.success) {
|
|
295
|
+
console.log(`[Gemini] Interrupted session ${sessionId} via ${result.method}`);
|
|
296
|
+
} else {
|
|
297
|
+
console.log(`[Gemini] Failed to interrupt session ${sessionId}: ${result.reason}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
res.json(result);
|
|
301
|
+
} catch (error) {
|
|
302
|
+
console.error('[Gemini] Interrupt error:', error);
|
|
303
|
+
res.status(500).json({ error: error.message });
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
247
307
|
module.exports = router;
|
|
@@ -4,6 +4,7 @@ const path = require('path');
|
|
|
4
4
|
const { prepare, saveDb } = require('../db');
|
|
5
5
|
const CliLoader = require('../services/cli-loader');
|
|
6
6
|
const SummaryGenerator = require('../services/summary-generator');
|
|
7
|
+
const sessionImporter = require('../services/session-importer');
|
|
7
8
|
|
|
8
9
|
const router = express.Router();
|
|
9
10
|
const cliLoader = new CliLoader();
|
|
@@ -16,6 +17,20 @@ const SESSION_DIRS = {
|
|
|
16
17
|
gemini: path.join(process.env.HOME || '', '.gemini', 'sessions'),
|
|
17
18
|
};
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* POST /api/v1/sessions/import
|
|
22
|
+
* Importa tutte le sessioni native (Claude/Codex/Gemini) nel DB
|
|
23
|
+
*/
|
|
24
|
+
router.post('/import', async (_req, res) => {
|
|
25
|
+
try {
|
|
26
|
+
const imported = sessionImporter.importAll();
|
|
27
|
+
res.json({ success: true, imported });
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.error('[Sessions] Import error:', error);
|
|
30
|
+
res.status(500).json({ success: false, error: error.message });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
19
34
|
/**
|
|
20
35
|
* GET /api/v1/sessions/:id
|
|
21
36
|
* Return session metadata from DB (sessions table)
|
|
@@ -66,8 +81,8 @@ router.get('/:id/messages', async (req, res) => {
|
|
|
66
81
|
|
|
67
82
|
const { messages, pagination } = await cliLoader.loadMessagesFromCLI({
|
|
68
83
|
sessionId,
|
|
69
|
-
threadId: session.session_path, // native thread id
|
|
70
|
-
sessionPath: session.session_path,
|
|
84
|
+
threadId: session.session_path, // native Codex/Gemini thread id
|
|
85
|
+
sessionPath: session.session_path, // alias for compatibility
|
|
71
86
|
engine: session.engine || 'claude-code',
|
|
72
87
|
workspacePath: session.workspace_path,
|
|
73
88
|
limit,
|
|
@@ -81,6 +96,7 @@ router.get('/:id/messages', async (req, res) => {
|
|
|
81
96
|
workspace_path: session.workspace_path,
|
|
82
97
|
title: session.title,
|
|
83
98
|
engine: session.engine,
|
|
99
|
+
conversation_id: session.conversation_id,
|
|
84
100
|
last_used_at: session.last_used_at,
|
|
85
101
|
created_at: session.created_at,
|
|
86
102
|
message_count: session.message_count
|
|
@@ -283,6 +299,7 @@ function getSessionFilePath(sessionId, engine, workspacePath, sessionPath) {
|
|
|
283
299
|
const baseDir = SESSION_DIRS.codex;
|
|
284
300
|
const flatPath = path.join(baseDir, `${nativeId}.jsonl`);
|
|
285
301
|
if (fs.existsSync(flatPath)) return flatPath;
|
|
302
|
+
// Search nested rollout-*.jsonl files containing the threadId
|
|
286
303
|
return findCodexSessionFile(baseDir, nativeId);
|
|
287
304
|
case 'gemini':
|
|
288
305
|
return path.join(SESSION_DIRS.gemini, `${sessionId}.jsonl`);
|
|
@@ -291,6 +308,9 @@ function getSessionFilePath(sessionId, engine, workspacePath, sessionPath) {
|
|
|
291
308
|
}
|
|
292
309
|
}
|
|
293
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Find Codex rollout file by threadId within YYYY/MM/DD directories
|
|
313
|
+
*/
|
|
294
314
|
function findCodexSessionFile(baseDir, threadId) {
|
|
295
315
|
if (!threadId || !fs.existsSync(baseDir)) return null;
|
|
296
316
|
try {
|
package/lib/server/server.js
CHANGED
|
@@ -7,6 +7,7 @@ const http = require('http');
|
|
|
7
7
|
const { initDb, prepare } = require('./db');
|
|
8
8
|
const User = require('./models/User');
|
|
9
9
|
const WorkspaceManager = require('./services/workspace-manager');
|
|
10
|
+
const sessionImporter = require('./services/session-importer');
|
|
10
11
|
const pkg = require('../../package.json');
|
|
11
12
|
|
|
12
13
|
// Import middleware
|
|
@@ -28,6 +29,7 @@ const wakeLockRouter = require('./routes/wake-lock');
|
|
|
28
29
|
const uploadRouter = require('./routes/upload');
|
|
29
30
|
const keysRouter = require('./routes/keys');
|
|
30
31
|
const speechRouter = require('./routes/speech');
|
|
32
|
+
const configRouter = require('./routes/config');
|
|
31
33
|
|
|
32
34
|
const app = express();
|
|
33
35
|
const PORT = process.env.PORT || 41800;
|
|
@@ -62,6 +64,7 @@ app.use(express.static(frontendDist));
|
|
|
62
64
|
// Public routes
|
|
63
65
|
app.use('/api/v1/auth', authRouter);
|
|
64
66
|
app.use('/api/v1/models', modelsRouter);
|
|
67
|
+
app.use('/api/v1/config', configRouter);
|
|
65
68
|
app.use('/api/v1/workspace', workspaceRouter);
|
|
66
69
|
app.use('/api/v1', wakeLockRouter); // Wake lock endpoints (public for app visibility handling)
|
|
67
70
|
app.use('/api/v1/workspaces', authMiddleware, workspacesRouter);
|
|
@@ -150,6 +153,14 @@ async function start() {
|
|
|
150
153
|
// Continue anyway - database will sync on first workspace mount
|
|
151
154
|
}
|
|
152
155
|
|
|
156
|
+
// Import native sessions from all engines (Claude/Codex/Gemini)
|
|
157
|
+
try {
|
|
158
|
+
const imported = sessionImporter.importAll();
|
|
159
|
+
console.log(`[Startup] Imported sessions → Claude:${imported.claude} Codex:${imported.codex} Gemini:${imported.gemini}`);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error('[Startup] ⚠️ Session import failed:', error.message);
|
|
162
|
+
}
|
|
163
|
+
|
|
153
164
|
// Auto-seed dev user on demand (Termux/local) if no users exist
|
|
154
165
|
const autoSeed = process.env.NEXUSCLI_AUTO_SEED === '1';
|
|
155
166
|
if (autoSeed) {
|
|
@@ -4,19 +4,20 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Based on NexusChat codex-cli-wrapper.js pattern
|
|
6
6
|
* Requires: codex-cli 0.62.1+ with exec subcommand
|
|
7
|
+
*
|
|
8
|
+
* @version 0.5.0 - Extended BaseCliWrapper for interrupt support
|
|
7
9
|
*/
|
|
8
10
|
|
|
9
11
|
const { spawn, exec } = require('child_process');
|
|
10
12
|
const CodexOutputParser = require('./codex-output-parser');
|
|
13
|
+
const BaseCliWrapper = require('./base-cli-wrapper');
|
|
11
14
|
|
|
12
|
-
class CodexWrapper {
|
|
15
|
+
class CodexWrapper extends BaseCliWrapper {
|
|
13
16
|
constructor(options = {}) {
|
|
17
|
+
super(); // Initialize activeProcesses from BaseCliWrapper
|
|
14
18
|
this.workspaceDir = options.workspaceDir || process.cwd();
|
|
15
19
|
this.codexBin = options.codexBin || 'codex';
|
|
16
20
|
|
|
17
|
-
// Track active sessions
|
|
18
|
-
this.activeSessions = new Set();
|
|
19
|
-
|
|
20
21
|
console.log('[CodexWrapper] Initialized');
|
|
21
22
|
console.log('[CodexWrapper] Workspace:', this.workspaceDir);
|
|
22
23
|
console.log('[CodexWrapper] Binary:', this.codexBin);
|
|
@@ -34,7 +35,7 @@ class CodexWrapper {
|
|
|
34
35
|
* @param {Function} options.onStatus - Callback for status events
|
|
35
36
|
* @returns {Promise<Object>} Response with text, usage, threadId
|
|
36
37
|
*/
|
|
37
|
-
async sendMessage({ prompt, model, threadId, reasoningEffort, workspacePath, imageFiles = [], onStatus }) {
|
|
38
|
+
async sendMessage({ prompt, model, threadId, reasoningEffort, workspacePath, imageFiles = [], onStatus, processId: processIdOverride }) {
|
|
38
39
|
return new Promise((resolve, reject) => {
|
|
39
40
|
const parser = new CodexOutputParser();
|
|
40
41
|
const cwd = workspacePath || this.workspaceDir;
|
|
@@ -91,6 +92,11 @@ class CodexWrapper {
|
|
|
91
92
|
},
|
|
92
93
|
});
|
|
93
94
|
|
|
95
|
+
// Register process for interrupt capability
|
|
96
|
+
// Prefer external processId (Nexus sessionId), else threadId, else temp
|
|
97
|
+
const processId = processIdOverride || threadId || `codex-${Date.now()}`;
|
|
98
|
+
this.registerProcess(processId, proc, 'spawn');
|
|
99
|
+
|
|
94
100
|
proc.stdout.on('data', (data) => {
|
|
95
101
|
const str = data.toString();
|
|
96
102
|
stdout += str;
|
|
@@ -102,6 +108,9 @@ class CodexWrapper {
|
|
|
102
108
|
});
|
|
103
109
|
|
|
104
110
|
proc.on('close', (exitCode) => {
|
|
111
|
+
// Unregister process on exit
|
|
112
|
+
this.unregisterProcess(processId);
|
|
113
|
+
|
|
105
114
|
clearTimeout(timeout);
|
|
106
115
|
this.handleExit(exitCode, stdout, parser, prompt, resolve, reject);
|
|
107
116
|
});
|