@mmmbuto/nexuscli 0.6.0 → 0.6.2

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.
@@ -71,27 +71,39 @@ router.post('/', async (req, res) => {
71
71
  // Send initial event
72
72
  res.write(`data: ${JSON.stringify({ type: 'message_start', messageId: `user-${Date.now()}`, sessionId })}\n\n`);
73
73
 
74
- // Use optimized ContextBridge for token-aware context building
74
+ // Check if this is an engine switch (requires context bridging)
75
75
  const lastEngine = Message.getLastEngine(sessionId);
76
- const contextResult = await contextBridge.buildContext({
77
- sessionId,
78
- fromEngine: lastEngine,
79
- toEngine: 'claude',
80
- userMessage: message
81
- });
76
+ const isEngineBridge = lastEngine && lastEngine !== 'claude';
82
77
 
83
- const promptWithContext = contextResult.prompt;
84
- const isEngineBridge = contextResult.isEngineBridge;
78
+ // IMPORTANT: Skip contextBridge for Claude native resume!
79
+ // When Claude uses `-r sessionId`, it loads full history from .jsonl file.
80
+ // ContextBridge is ONLY needed for:
81
+ // 1. Engine switches (e.g., Codex → Claude)
82
+ // 2. New sessions without history
83
+ let promptToSend = message;
85
84
 
86
- console.log(`[Chat] Context: ${contextResult.contextTokens} tokens from ${contextResult.contextSource}, total: ${contextResult.totalTokens}`);
87
-
88
- // Notify frontend about engine switch
89
85
  if (isEngineBridge) {
86
+ // Engine switch: need context from previous engine
87
+ const contextResult = await contextBridge.buildContext({
88
+ sessionId,
89
+ fromEngine: lastEngine,
90
+ toEngine: 'claude',
91
+ userMessage: message
92
+ });
93
+ promptToSend = contextResult.prompt;
94
+ console.log(`[Chat] Engine bridge: ${contextResult.contextTokens} tokens from ${lastEngine}`);
95
+
90
96
  res.write(`data: ${JSON.stringify({
91
97
  type: 'status',
92
98
  category: 'engine_switch',
93
99
  message: `Continuing conversation from ${lastEngine}`
94
100
  })}\n\n`);
101
+ } else if (!isNewSession) {
102
+ // Native resume: Claude will load history from session file
103
+ console.log(`[Chat] Native resume: Claude will use -r ${sessionId} (full history in .jsonl)`);
104
+ } else {
105
+ // New session: no context needed
106
+ console.log(`[Chat] New session: starting fresh`);
95
107
  }
96
108
 
97
109
  try {
@@ -113,7 +125,7 @@ router.post('/', async (req, res) => {
113
125
 
114
126
  // Call Claude Code wrapper with workspace path for --cwd
115
127
  const result = await claudeWrapper.sendMessage({
116
- prompt: promptWithContext,
128
+ prompt: promptToSend,
117
129
  conversationId: sessionId,
118
130
  model,
119
131
  workspacePath, // Pass workspace for Claude CLI --cwd
@@ -82,28 +82,37 @@ router.post('/', async (req, res) => {
82
82
  // Send initial event
83
83
  res.write(`data: ${JSON.stringify({ type: 'message_start', messageId: `user-${Date.now()}`, sessionId })}\n\n`);
84
84
 
85
- // Use optimized ContextBridge for token-aware context building
86
- // Note: Codex uses codeOnly mode for better code-focused context
85
+ // Check if this is an engine switch (requires context bridging)
87
86
  const lastEngine = Message.getLastEngine(sessionId);
88
- const contextResult = await contextBridge.buildContext({
89
- sessionId,
90
- fromEngine: lastEngine,
91
- toEngine: 'codex',
92
- userMessage: message
93
- });
94
-
95
- const promptWithContext = contextResult.prompt;
96
- const isEngineBridge = contextResult.isEngineBridge;
87
+ const isEngineBridge = lastEngine && lastEngine !== 'codex';
97
88
 
98
- console.log(`[Codex] Context: ${contextResult.contextTokens} tokens from ${contextResult.contextSource}, total: ${contextResult.totalTokens}`);
89
+ // IMPORTANT: Skip contextBridge for Codex native resume!
90
+ // When Codex uses `exec resume <threadId>`, it loads full history from native thread.
91
+ // ContextBridge is ONLY needed for engine switches.
92
+ let promptToSend = message;
99
93
 
100
- // Notify frontend about engine switch
101
94
  if (isEngineBridge) {
95
+ // Engine switch: need context from previous engine
96
+ const contextResult = await contextBridge.buildContext({
97
+ sessionId,
98
+ fromEngine: lastEngine,
99
+ toEngine: 'codex',
100
+ userMessage: message
101
+ });
102
+ promptToSend = contextResult.prompt;
103
+ console.log(`[Codex] Engine bridge: ${contextResult.contextTokens} tokens from ${lastEngine}`);
104
+
102
105
  res.write(`data: ${JSON.stringify({
103
106
  type: 'status',
104
107
  category: 'engine_switch',
105
108
  message: `Continuing conversation from ${lastEngine}`
106
109
  })}\n\n`);
110
+ } else if (nativeThreadId) {
111
+ // Native resume: Codex will load history from thread
112
+ console.log(`[Codex] Native resume: Codex will use threadId ${nativeThreadId}`);
113
+ } else {
114
+ // New session: no context needed
115
+ console.log(`[Codex] New session: starting fresh`);
107
116
  }
108
117
 
109
118
  try {
@@ -124,7 +133,7 @@ router.post('/', async (req, res) => {
124
133
 
125
134
  // Call Codex wrapper with native threadId for session resume
126
135
  const result = await codexWrapper.sendMessage({
127
- prompt: nativeThreadId ? message : promptWithContext, // Use raw message if resuming native session
136
+ prompt: promptToSend,
128
137
  model,
129
138
  threadId: nativeThreadId, // Native Codex thread ID for resume
130
139
  reasoningEffort,
@@ -90,29 +90,38 @@ router.post('/', async (req, res) => {
90
90
  engine: 'gemini'
91
91
  })}\n\n`);
92
92
 
93
- // Use optimized ContextBridge for token-aware context building
94
- // Note: Gemini has larger context window, uses preferSummary: false config
93
+ // Check if this is an engine switch (requires context bridging)
95
94
  const lastEngine = Message.getLastEngine(sessionId);
96
- const contextResult = await contextBridge.buildContext({
97
- sessionId,
98
- fromEngine: lastEngine,
99
- toEngine: 'gemini',
100
- userMessage: message
101
- });
95
+ const isEngineBridge = lastEngine && lastEngine !== 'gemini';
102
96
 
103
- const promptWithContext = contextResult.prompt;
104
- const isEngineBridge = contextResult.isEngineBridge;
97
+ // IMPORTANT: Skip contextBridge for Gemini native resume!
98
+ // When Gemini uses `--resume <sessionId>`, it loads full history from native session.
99
+ // ContextBridge is ONLY needed for engine switches.
100
+ let promptToSend = message;
105
101
 
106
- console.log(`[Gemini] Context: ${contextResult.contextTokens} tokens from ${contextResult.contextSource}, total: ${contextResult.totalTokens}`);
107
-
108
- // Notify frontend about engine switch
109
102
  if (isEngineBridge) {
103
+ // Engine switch: need context from previous engine
104
+ const contextResult = await contextBridge.buildContext({
105
+ sessionId,
106
+ fromEngine: lastEngine,
107
+ toEngine: 'gemini',
108
+ userMessage: message
109
+ });
110
+ promptToSend = contextResult.prompt;
111
+ console.log(`[Gemini] Engine bridge: ${contextResult.contextTokens} tokens from ${lastEngine}`);
112
+
110
113
  res.write(`data: ${JSON.stringify({
111
114
  type: 'status',
112
115
  category: 'system',
113
116
  message: `Context bridged from ${lastEngine}`,
114
117
  icon: '🔄'
115
118
  })}\n\n`);
119
+ } else if (nativeSessionId) {
120
+ // Native resume: Gemini will load history from session
121
+ console.log(`[Gemini] Native resume: Gemini will use --resume ${nativeSessionId}`);
122
+ } else {
123
+ // New session: no context needed
124
+ console.log(`[Gemini] New session: starting fresh`);
116
125
  }
117
126
 
118
127
  // Save user message to DB
@@ -139,7 +148,7 @@ router.post('/', async (req, res) => {
139
148
 
140
149
  // Call Gemini wrapper with native sessionId for session resume
141
150
  const result = await geminiWrapper.sendMessage({
142
- prompt: nativeSessionId ? message : promptWithContext, // Use raw message if resuming native session
151
+ prompt: promptToSend,
143
152
  threadId: nativeSessionId, // Native Gemini session ID for resume
144
153
  model,
145
154
  workspacePath,
@@ -147,6 +147,15 @@ class SessionManager {
147
147
  return crypto.createHash('md5').update(workspacePath).digest('hex').substring(0, 8);
148
148
  }
149
149
 
150
+ /**
151
+ * Normalize workspace path (remove trailing slashes)
152
+ * Prevents duplicate entries like /path and /path/
153
+ */
154
+ _normalizePath(workspacePath) {
155
+ if (!workspacePath) return '';
156
+ return workspacePath.replace(/\/+$/, '');
157
+ }
158
+
150
159
  /**
151
160
  * Get or create session for conversation + engine
152
161
  *
@@ -164,16 +173,17 @@ class SessionManager {
164
173
  */
165
174
  async getOrCreateSession(conversationId, engine, workspacePath) {
166
175
  const normalizedEngine = this._normalizeEngine(engine);
176
+ const normalizedPath = this._normalizePath(workspacePath);
167
177
  const cacheKey = this._getCacheKey(conversationId, normalizedEngine);
168
178
 
169
- console.log(`[SessionManager] getOrCreateSession(${conversationId}, ${normalizedEngine}, ${workspacePath})`);
179
+ console.log(`[SessionManager] getOrCreateSession(${conversationId}, ${normalizedEngine}, ${normalizedPath})`);
170
180
 
171
181
  // 1. Check RAM cache first (fastest path)
172
182
  if (this.sessionMap.has(cacheKey)) {
173
183
  const cachedId = this.sessionMap.get(cacheKey);
174
184
 
175
185
  // Verify it still exists on filesystem
176
- if (this.sessionFileExists(cachedId, normalizedEngine, workspacePath)) {
186
+ if (this.sessionFileExists(cachedId, normalizedEngine, normalizedPath)) {
177
187
  this.lastAccess.set(cacheKey, Date.now());
178
188
  console.log(`[SessionManager] Cache hit: ${cachedId}`);
179
189
  return { sessionId: cachedId, isNew: false };
@@ -195,7 +205,7 @@ class SessionManager {
195
205
 
196
206
  if (row) {
197
207
  // 3. Verify session file exists on filesystem
198
- if (this.sessionFileExists(row.id, normalizedEngine, row.workspace_path || workspacePath)) {
208
+ if (this.sessionFileExists(row.id, normalizedEngine, row.workspace_path || normalizedPath)) {
199
209
  // Valid session - update cache and return
200
210
  this.sessionMap.set(cacheKey, row.id);
201
211
  this.lastAccess.set(cacheKey, Date.now());
@@ -223,7 +233,7 @@ class SessionManager {
223
233
  VALUES (?, ?, ?, ?, ?, ?, ?)
224
234
  `);
225
235
  const title = 'New Chat'; // Will be updated after first response
226
- insertStmt.run(sessionId, workspacePath, normalizedEngine, conversationId, title, now, now);
236
+ insertStmt.run(sessionId, normalizedPath, normalizedEngine, conversationId, title, now, now);
227
237
  saveDb();
228
238
  console.log(`[SessionManager] Saved to DB: ${sessionId}`);
229
239
  } catch (dbErr) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscli",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "NexusCLI - TRI CLI Control Plane (Claude/Codex/Gemini)",
5
5
  "main": "lib/server/server.js",
6
6
  "bin": {