@mmmbuto/nexuscli 0.5.4 → 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.
- package/README.md +20 -106
- package/bin/nexuscli.js +6 -6
- package/lib/server/.env.example +1 -1
- package/lib/server/db.js.old +225 -0
- package/lib/server/docs/API_WRAPPER_CONTRACT.md +682 -0
- package/lib/server/docs/ARCHITECTURE.md +441 -0
- package/lib/server/docs/DATABASE_SCHEMA.md +783 -0
- package/lib/server/docs/DESIGN_PRINCIPLES.md +598 -0
- package/lib/server/docs/NEXUSCHAT_ANALYSIS.md +488 -0
- package/lib/server/docs/PIPELINE_INTEGRATION.md +636 -0
- package/lib/server/docs/README.md +272 -0
- package/lib/server/docs/UI_DESIGN.md +916 -0
- package/lib/server/routes/codex.js +12 -3
- package/lib/server/routes/gemini.js +13 -3
- package/lib/server/services/codex-output-parser.js +8 -0
- package/lib/server/services/codex-wrapper.js +15 -14
- package/lib/server/services/context-bridge.js +6 -4
- package/lib/server/services/gemini-wrapper.js +14 -6
- package/lib/server/services/session-manager.js +43 -1
- package/lib/server/services/workspace-manager.js +1 -1
- package/lib/server/tests/performance.test.js +1 -1
- package/lib/server/tests/services.test.js +2 -2
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
|
|
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
|
|
140
|
+
// Call Gemini wrapper with native sessionId for session resume
|
|
137
141
|
const result = await geminiWrapper.sendMessage({
|
|
138
|
-
prompt: promptWithContext,
|
|
139
|
-
|
|
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(
|
|
@@ -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.
|
|
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,
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
'--
|
|
47
|
-
'--dangerously-bypass-approvals-and-sandbox',
|
|
48
|
-
|
|
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]
|
|
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,
|
|
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
|
-
|
|
146
|
-
if
|
|
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
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]
|
|
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:
|
|
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 /
|
|
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 = '/
|
|
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 = '/
|
|
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 = '/
|
|
150
|
+
const testPath = '/var/www/cli.wellanet.dev';
|
|
151
151
|
const validated = await manager.validateWorkspace(testPath);
|
|
152
152
|
expect(validated).toBe(testPath);
|
|
153
153
|
});
|