@mmmbuto/nexuscli 0.5.3 → 0.5.5

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.
@@ -70,6 +70,10 @@ router.post('/', async (req, res) => {
70
70
  console.log(`[Codex] Session resolved: ${sessionId} (new: ${isNewSession})`);
71
71
  const isNewChat = isNewSession;
72
72
 
73
+ // Get native Codex threadId for session resume (if exists)
74
+ const nativeThreadId = isNewSession ? null : sessionManager.getNativeThreadId(sessionId);
75
+ console.log(`[Codex] Native threadId: ${nativeThreadId || '(new thread)'}`);
76
+
73
77
  // Set up SSE
74
78
  res.setHeader('Content-Type', 'text/event-stream');
75
79
  res.setHeader('Cache-Control', 'no-cache');
@@ -118,11 +122,11 @@ router.post('/', async (req, res) => {
118
122
  console.warn('[Codex] Failed to save user message:', msgErr.message);
119
123
  }
120
124
 
121
- // Call Codex wrapper with workspace path
125
+ // Call Codex wrapper with native threadId for session resume
122
126
  const result = await codexWrapper.sendMessage({
123
- prompt: promptWithContext,
127
+ prompt: nativeThreadId ? message : promptWithContext, // Use raw message if resuming native session
124
128
  model,
125
- sessionId,
129
+ threadId: nativeThreadId, // Native Codex thread ID for resume
126
130
  reasoningEffort,
127
131
  workspacePath,
128
132
  onStatus: (event) => {
@@ -131,6 +135,11 @@ router.post('/', async (req, res) => {
131
135
  }
132
136
  });
133
137
 
138
+ // Save native threadId for future resume (if new)
139
+ if (result.threadId && result.threadId !== nativeThreadId) {
140
+ sessionManager.setNativeThreadId(sessionId, result.threadId);
141
+ }
142
+
134
143
  // Save assistant response to database with engine tracking
135
144
  try {
136
145
  const assistantMessage = Message.create(
@@ -72,6 +72,10 @@ router.post('/', async (req, res) => {
72
72
 
73
73
  console.log(`[Gemini] Session resolved: ${sessionId} (new: ${isNewSession})`);
74
74
 
75
+ // Get native Gemini sessionId for session resume (if exists)
76
+ const nativeSessionId = isNewSession ? null : sessionManager.getNativeThreadId(sessionId);
77
+ console.log(`[Gemini] Native sessionId: ${nativeSessionId || '(new session)'}`);
78
+
75
79
  // Set up SSE
76
80
  res.setHeader('Content-Type', 'text/event-stream');
77
81
  res.setHeader('Cache-Control', 'no-cache');
@@ -133,10 +137,10 @@ router.post('/', async (req, res) => {
133
137
 
134
138
  console.log('[Gemini] Calling Gemini CLI...');
135
139
 
136
- // Call Gemini wrapper with SSE streaming
140
+ // Call Gemini wrapper with native sessionId for session resume
137
141
  const result = await geminiWrapper.sendMessage({
138
- prompt: promptWithContext,
139
- sessionId,
142
+ prompt: nativeSessionId ? message : promptWithContext, // Use raw message if resuming native session
143
+ threadId: nativeSessionId, // Native Gemini session ID for resume
140
144
  model,
141
145
  workspacePath,
142
146
  onStatus: (event) => {
@@ -147,6 +151,12 @@ router.post('/', async (req, res) => {
147
151
 
148
152
  console.log(`[Gemini] Response received: ${result.text?.length || 0} chars`);
149
153
 
154
+ // Save native sessionId for future resume (if new)
155
+ if (result.sessionId && result.sessionId !== nativeSessionId) {
156
+ sessionManager.setNativeThreadId(sessionId, result.sessionId);
157
+ console.log(`[Gemini] Saved native sessionId: ${result.sessionId}`);
158
+ }
159
+
150
160
  // Save assistant response to DB
151
161
  try {
152
162
  Message.create(
@@ -169,50 +169,62 @@ async function start() {
169
169
  const certDir = path.join(process.env.HOME || '', '.nexuscli', 'certs');
170
170
  const certPath = path.join(certDir, 'cert.pem');
171
171
  const keyPath = path.join(certDir, 'key.pem');
172
- const useHttps = fs.existsSync(certPath) && fs.existsSync(keyPath);
172
+ const hasHttpsCerts = fs.existsSync(certPath) && fs.existsSync(keyPath);
173
173
 
174
- let server;
175
- let protocol = 'http';
174
+ // Always start HTTP server on main port
175
+ const httpServer = http.createServer(app);
176
176
 
177
- if (useHttps) {
177
+ // Start HTTPS server on PORT+1 if certificates exist (for remote mic access)
178
+ let httpsServer = null;
179
+ const HTTPS_PORT = parseInt(PORT) + 1;
180
+
181
+ if (hasHttpsCerts) {
178
182
  try {
179
183
  const httpsOptions = {
180
184
  key: fs.readFileSync(keyPath),
181
185
  cert: fs.readFileSync(certPath)
182
186
  };
183
- server = https.createServer(httpsOptions, app);
184
- protocol = 'https';
185
- console.log('[Startup] HTTPS enabled - certificates found');
187
+ httpsServer = https.createServer(httpsOptions, app);
188
+ console.log('[Startup] HTTPS certificates found');
186
189
  } catch (err) {
187
- console.warn('[Startup] Failed to load certificates, falling back to HTTP:', err.message);
188
- server = http.createServer(app);
190
+ console.warn('[Startup] Failed to load certificates:', err.message);
189
191
  }
190
- } else {
191
- server = http.createServer(app);
192
- console.log('[Startup] HTTP mode - no certificates found');
193
- console.log('[Startup] Run: ./scripts/setup-https.sh to enable HTTPS');
194
192
  }
195
193
 
196
- server.listen(PORT, () => {
194
+ httpServer.listen(PORT, () => {
197
195
  console.log('');
198
196
  console.log('╔══════════════════════════════════════════════╗');
199
197
  console.log('║ 🚀 NexusCLI Backend ║');
200
198
  console.log('╚══════════════════════════════════════════════╝');
201
199
  console.log('');
202
- console.log(`✅ Server running on ${protocol}://localhost:${PORT}`);
203
- if (useHttps) {
204
- console.log(`🔒 HTTPS enabled - secure connection`);
200
+ console.log(`✅ HTTP: http://localhost:${PORT}`);
201
+
202
+ if (httpsServer) {
203
+ httpsServer.listen(HTTPS_PORT, () => {
204
+ console.log(`🔒 HTTPS: https://localhost:${HTTPS_PORT} (for remote mic)`);
205
+ console.log('');
206
+ console.log('Endpoints:');
207
+ console.log(` GET /health (public)`);
208
+ console.log(` POST /api/v1/auth/login (public)`);
209
+ console.log(` GET /api/v1/auth/me (protected)`);
210
+ console.log(` GET /api/v1/conversations (protected)`);
211
+ console.log(` POST /api/v1/conversations (protected)`);
212
+ console.log(` POST /api/v1/jobs (protected)`);
213
+ console.log(` GET /api/v1/jobs/:id/stream (protected, SSE)`);
214
+ console.log('');
215
+ });
216
+ } else {
217
+ console.log('');
218
+ console.log('Endpoints:');
219
+ console.log(` GET /health (public)`);
220
+ console.log(` POST /api/v1/auth/login (public)`);
221
+ console.log(` GET /api/v1/auth/me (protected)`);
222
+ console.log(` GET /api/v1/conversations (protected)`);
223
+ console.log(` POST /api/v1/conversations (protected)`);
224
+ console.log(` POST /api/v1/jobs (protected)`);
225
+ console.log(` GET /api/v1/jobs/:id/stream (protected, SSE)`);
226
+ console.log('');
205
227
  }
206
- console.log('');
207
- console.log('Endpoints:');
208
- console.log(` GET /health (public)`);
209
- console.log(` POST /api/v1/auth/login (public)`);
210
- console.log(` GET /api/v1/auth/me (protected)`);
211
- console.log(` GET /api/v1/conversations (protected)`);
212
- console.log(` POST /api/v1/conversations (protected)`);
213
- console.log(` POST /api/v1/jobs (protected)`);
214
- console.log(` GET /api/v1/jobs/:id/stream (protected, SSE)`);
215
- console.log('');
216
228
  });
217
229
  }
218
230
 
@@ -263,6 +263,13 @@ class CodexOutputParser {
263
263
  return this.usage;
264
264
  }
265
265
 
266
+ /**
267
+ * Get thread ID (native Codex session ID)
268
+ */
269
+ getThreadId() {
270
+ return this.threadId;
271
+ }
272
+
266
273
  /**
267
274
  * Reset parser state for new request
268
275
  */
@@ -270,6 +277,7 @@ class CodexOutputParser {
270
277
  this.buffer = '';
271
278
  this.finalResponse = '';
272
279
  this.usage = null;
280
+ this.threadId = null;
273
281
  this.pendingCommands.clear();
274
282
  }
275
283
  }
@@ -27,29 +27,27 @@ class CodexWrapper {
27
27
  * @param {Object} options - Message options
28
28
  * @param {string} options.prompt - User prompt
29
29
  * @param {string} options.model - Model name (e.g., gpt-5.1-codex-max)
30
- * @param {string} options.sessionId - Session ID for conversation continuity
30
+ * @param {string} options.threadId - Native Codex thread ID for session resume
31
31
  * @param {string} options.reasoningEffort - Reasoning level (low, medium, high, xhigh)
32
32
  * @param {string} options.workspacePath - Working directory override
33
33
  * @param {string[]} options.imageFiles - Array of image file paths for multimodal
34
34
  * @param {Function} options.onStatus - Callback for status events
35
- * @returns {Promise<Object>} Response with text, usage
35
+ * @returns {Promise<Object>} Response with text, usage, threadId
36
36
  */
37
- async sendMessage({ prompt, model, sessionId, reasoningEffort, workspacePath, imageFiles = [], onStatus }) {
37
+ async sendMessage({ prompt, model, threadId, reasoningEffort, workspacePath, imageFiles = [], onStatus }) {
38
38
  return new Promise((resolve, reject) => {
39
39
  const parser = new CodexOutputParser();
40
40
  const cwd = workspacePath || this.workspaceDir;
41
41
 
42
42
  // Build CLI arguments
43
- const args = [
44
- 'exec',
45
- '--json', // JSONL output for parsing
46
- '--skip-git-repo-check', // Allow non-git directories
47
- '--dangerously-bypass-approvals-and-sandbox', // Full access (safety via CLAUDE.md policy)
48
- '-C', cwd, // Working directory
49
- ];
50
-
51
- // Add model if specified
52
- if (model) {
43
+ // If threadId exists, use 'exec --json resume <threadId>' to continue session
44
+ // Otherwise use 'exec --json' for new session
45
+ const args = threadId
46
+ ? ['exec', '--json', 'resume', threadId]
47
+ : ['exec', '--json', '--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox', '-C', cwd];
48
+
49
+ // Add model if specified (only for new sessions)
50
+ if (model && !threadId) {
53
51
  const baseModel = this.extractBaseModel(model);
54
52
  args.push('-m', baseModel);
55
53
  }
@@ -72,7 +70,7 @@ class CodexWrapper {
72
70
 
73
71
  console.log('[CodexWrapper] Model:', model);
74
72
  console.log('[CodexWrapper] Reasoning:', reasoningEffort);
75
- console.log('[CodexWrapper] Session:', sessionId);
73
+ console.log('[CodexWrapper] ThreadId:', threadId || '(new session)');
76
74
  console.log('[CodexWrapper] CWD:', cwd);
77
75
  console.log('[CodexWrapper] Args:', args.slice(0, 6).join(' ') + '...');
78
76
 
@@ -151,8 +149,10 @@ class CodexWrapper {
151
149
 
152
150
  const finalResponse = parser.getFinalResponse();
153
151
  const usage = parser.getUsage();
152
+ const threadId = parser.getThreadId();
154
153
 
155
154
  console.log('[CodexWrapper] Final response length:', finalResponse.length);
155
+ console.log('[CodexWrapper] ThreadId:', threadId);
156
156
 
157
157
  // Calculate token counts (fallback)
158
158
  const promptTokens = usage?.input_tokens || Math.ceil(prompt.length / 4);
@@ -160,6 +160,7 @@ class CodexWrapper {
160
160
 
161
161
  resolve({
162
162
  text: finalResponse,
163
+ threadId, // Native Codex session ID for resume
163
164
  usage: {
164
165
  prompt_tokens: promptTokens,
165
166
  completion_tokens: completionTokens,
@@ -139,11 +139,13 @@ class ContextBridge {
139
139
  for (let i = messages.length - 1; i >= 0; i--) {
140
140
  const msg = messages[i];
141
141
 
142
- // For code-focused engines, filter out non-code content
142
+ // For code-focused engines, compress assistant responses to code only
143
+ // BUT always keep user messages for context continuity
143
144
  let content = msg.content;
144
- if (config.codeOnly) {
145
- content = this.extractCodeContent(content);
146
- if (!content) continue; // Skip if no code
145
+ if (config.codeOnly && msg.role === 'assistant') {
146
+ const codeContent = this.extractCodeContent(content);
147
+ // Only use code-only if there's actual code, otherwise keep truncated original
148
+ content = codeContent || (content.length > 500 ? content.substring(0, 500) + '...' : content);
147
149
  }
148
150
 
149
151
  // Truncate long messages
@@ -67,15 +67,15 @@ class GeminiWrapper {
67
67
  *
68
68
  * @param {Object} params
69
69
  * @param {string} params.prompt - User message/prompt
70
- * @param {string} params.sessionId - Session UUID (for logging)
70
+ * @param {string} params.threadId - Native Gemini session ID for resume
71
71
  * @param {string} [params.model='gemini-3-pro-preview'] - Model name
72
72
  * @param {string} [params.workspacePath] - Workspace directory
73
73
  * @param {Function} [params.onStatus] - Callback for status events (SSE streaming)
74
- * @returns {Promise<{text: string, usage: Object}>}
74
+ * @returns {Promise<{text: string, usage: Object, sessionId: string}>}
75
75
  */
76
76
  async sendMessage({
77
77
  prompt,
78
- sessionId,
78
+ threadId,
79
79
  model = DEFAULT_MODEL,
80
80
  workspacePath,
81
81
  onStatus
@@ -87,16 +87,23 @@ class GeminiWrapper {
87
87
  const cwd = workspacePath || this.workspaceDir;
88
88
 
89
89
  // Build CLI arguments
90
- // Note: cwd is set in pty.spawn() options, no need for --include-directories
90
+ // If threadId exists, use --resume to continue native session
91
91
  const args = [
92
92
  '-y', // YOLO mode - auto-approve all actions
93
93
  '-m', model, // Model selection
94
94
  '-o', 'stream-json', // JSON streaming for structured events
95
- prompt // Prompt as positional argument
96
95
  ];
97
96
 
97
+ // Add resume flag if continuing existing session
98
+ if (threadId) {
99
+ args.push('--resume', threadId);
100
+ }
101
+
102
+ // Add prompt as positional argument
103
+ args.push(prompt);
104
+
98
105
  console.log(`[GeminiWrapper] Model: ${model}`);
99
- console.log(`[GeminiWrapper] Session: ${sessionId}`);
106
+ console.log(`[GeminiWrapper] ThreadId: ${threadId || '(new session)'}`);
100
107
  console.log(`[GeminiWrapper] CWD: ${cwd}`);
101
108
  console.log(`[GeminiWrapper] Prompt length: ${prompt.length}`);
102
109
 
@@ -170,6 +177,7 @@ class GeminiWrapper {
170
177
 
171
178
  resolve({
172
179
  text: finalResponse,
180
+ sessionId: parser.getSessionId(), // Native Gemini session ID for resume
173
181
  usage: {
174
182
  prompt_tokens: promptTokens,
175
183
  completion_tokens: completionTokens,
@@ -46,10 +46,18 @@ class SessionManager {
46
46
  /**
47
47
  * Check if session file exists on disk
48
48
  * Claude: ~/.claude/projects/<workspace-slug>/<sessionId>.jsonl
49
- * Codex: ~/.codex/sessions/<sessionId>.jsonl (if available)
49
+ * Codex: Uses exec mode (no session files) - DB is source of truth
50
50
  * Gemini: ~/.gemini/sessions/<sessionId>.jsonl (if available)
51
51
  */
52
52
  sessionFileExists(sessionId, engine, workspacePath) {
53
+ const normalizedEngine = this._normalizeEngine(engine);
54
+
55
+ // Codex/Gemini exec mode doesn't create session files - trust DB mapping
56
+ // Session continuity is managed via NexusCLI's message DB + contextBridge
57
+ if (normalizedEngine === 'codex' || normalizedEngine === 'gemini') {
58
+ return true; // Always trust DB for exec-mode CLI sessions
59
+ }
60
+
53
61
  try {
54
62
  const sessionPath = this.getSessionFilePath(sessionId, engine, workspacePath);
55
63
  if (!sessionPath) return false;
@@ -452,6 +460,40 @@ class SessionManager {
452
460
  timestamp: new Date().toISOString()
453
461
  };
454
462
  }
463
+
464
+ /**
465
+ * Get native thread ID for Codex/Gemini sessions
466
+ * Uses session_path column to store native CLI thread ID
467
+ * @param {string} sessionId - NexusCLI session ID
468
+ * @returns {string|null} Native thread ID or null
469
+ */
470
+ getNativeThreadId(sessionId) {
471
+ try {
472
+ const stmt = prepare('SELECT session_path FROM sessions WHERE id = ?');
473
+ const row = stmt.get(sessionId);
474
+ return row?.session_path || null;
475
+ } catch (error) {
476
+ console.warn(`[SessionManager] Failed to get native threadId:`, error.message);
477
+ return null;
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Set native thread ID for Codex/Gemini sessions
483
+ * @param {string} sessionId - NexusCLI session ID
484
+ * @param {string} threadId - Native CLI thread ID
485
+ */
486
+ setNativeThreadId(sessionId, threadId) {
487
+ if (!threadId) return;
488
+ try {
489
+ const stmt = prepare('UPDATE sessions SET session_path = ? WHERE id = ?');
490
+ stmt.run(threadId, sessionId);
491
+ saveDb();
492
+ console.log(`[SessionManager] Set native threadId: ${sessionId} → ${threadId}`);
493
+ } catch (error) {
494
+ console.warn(`[SessionManager] Failed to set native threadId:`, error.message);
495
+ }
496
+ }
455
497
  }
456
498
 
457
499
  // Singleton instance
@@ -396,7 +396,7 @@ class WorkspaceManager {
396
396
  * @returns {string}
397
397
  */
398
398
  getSessionPath(workspacePath) {
399
- // Convert /home/user/myproject → -home-user-myproject
399
+ // Convert /var/www/cli.wellanet.dev → -var-www-cli-wellanet-dev
400
400
  const projectDir = workspacePath.replace(/\//g, '-').replace(/^-/, '');
401
401
  return path.join(this.claudePath, 'projects', projectDir);
402
402
  }
@@ -45,7 +45,7 @@ describe('Performance Benchmarks', () => {
45
45
 
46
46
  test('Workspace validation should be fast', async () => {
47
47
  const manager = new WorkspaceManager();
48
- const testPath = '/home/user/myproject';
48
+ const testPath = '/var/www/cli.wellanet.dev';
49
49
 
50
50
  const start = Date.now();
51
51
  const validated = await manager.validateWorkspace(testPath);
@@ -16,7 +16,7 @@ describe('WorkspaceManager', () => {
16
16
 
17
17
  test('should validate workspace path', async () => {
18
18
  // Test with allowed path
19
- const validPath = '/home/user/myproject';
19
+ const validPath = '/var/www/cli.wellanet.dev';
20
20
  const result = await manager.validateWorkspace(validPath);
21
21
  expect(result).toBe(validPath);
22
22
  });
@@ -147,7 +147,7 @@ describe('SummaryGenerator', () => {
147
147
  describe('Integration - Service Interactions', () => {
148
148
  test('WorkspaceManager should use consistent path resolution', async () => {
149
149
  const manager = new WorkspaceManager();
150
- const testPath = '/home/user/myproject';
150
+ const testPath = '/var/www/cli.wellanet.dev';
151
151
  const validated = await manager.validateWorkspace(testPath);
152
152
  expect(validated).toBe(testPath);
153
153
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscli",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "NexusCLI - TRI CLI Control Plane (Claude/Codex/Gemini)",
5
5
  "main": "lib/server/server.js",
6
6
  "bin": {