@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.
@@ -53,59 +53,80 @@ class ContextBridge {
53
53
  /**
54
54
  * Build optimized context for engine switch
55
55
  * @param {Object} params
56
- * @param {string} params.sessionId - Session ID
56
+ * @param {string} params.conversationId - Stable conversation ID (cross-engine)
57
+ * @param {string} params.sessionId - Legacy session ID (fallback)
57
58
  * @param {string} params.fromEngine - Previous engine
58
59
  * @param {string} params.toEngine - Target engine
59
60
  * @param {string} params.userMessage - Current user message
60
61
  * @returns {Object} { prompt, isEngineBridge, contextTokens }
61
62
  */
62
- async buildContext({ sessionId, fromEngine, toEngine, userMessage }) {
63
+ async buildContext({ conversationId, sessionId, fromEngine, toEngine, userMessage }) {
63
64
  const config = this.getEngineConfig(toEngine);
64
65
  const isEngineBridge = fromEngine && fromEngine !== toEngine;
66
+ const convoId = conversationId || sessionId; // backward compat
65
67
 
66
68
  // Reserve tokens for user message
67
69
  const userTokens = this.estimateTokens(userMessage);
68
- const availableTokens = config.maxTokens - userTokens - 200; // 200 token buffer
70
+ const availableTokens = Math.max(0, config.maxTokens - userTokens - 200); // 200 token buffer, never negative
69
71
 
70
72
  let contextText = '';
71
73
  let contextTokens = 0;
72
74
  let contextSource = 'none';
73
75
 
74
- // Try summary first (most efficient)
75
- if (config.preferSummary) {
76
- const summaryContext = this.summaryGenerator.getBridgeContext(sessionId);
77
- if (summaryContext) {
78
- const summaryTokens = this.estimateTokens(summaryContext);
79
- if (summaryTokens <= availableTokens) {
80
- contextText = summaryContext;
81
- contextTokens = summaryTokens;
82
- contextSource = 'summary';
76
+ // For engine bridge, use structured handoff template
77
+ if (isEngineBridge) {
78
+ const handoffContext = this.buildEngineHandoffContext(convoId, fromEngine, toEngine, availableTokens, config);
79
+ if (handoffContext.text) {
80
+ contextText = handoffContext.text;
81
+ contextTokens = handoffContext.tokens;
82
+ contextSource = handoffContext.source;
83
+ }
84
+ // If handoff couldn't fit, fall back to history-only context
85
+ if (!contextText && availableTokens > 200) {
86
+ const historyContext = this.buildTokenAwareHistory(convoId, availableTokens, config);
87
+ if (historyContext.text) {
88
+ contextText = historyContext.text;
89
+ contextTokens = historyContext.tokens;
90
+ contextSource = 'history_fallback';
91
+ }
92
+ }
93
+ } else {
94
+ // Try summary first (most efficient)
95
+ if (config.preferSummary) {
96
+ const summaryContext = this.summaryGenerator.getBridgeContext(convoId);
97
+ if (summaryContext) {
98
+ const summaryTokens = this.estimateTokens(summaryContext);
99
+ if (summaryTokens <= availableTokens) {
100
+ contextText = summaryContext;
101
+ contextTokens = summaryTokens;
102
+ contextSource = 'summary';
103
+ }
83
104
  }
84
105
  }
85
- }
86
106
 
87
- // Fallback to token-aware message history
88
- if (!contextText && availableTokens > 200) {
89
- const historyContext = this.buildTokenAwareHistory(sessionId, availableTokens, config);
90
- if (historyContext.text) {
91
- contextText = historyContext.text;
92
- contextTokens = historyContext.tokens;
93
- contextSource = 'history';
107
+ // Fallback to token-aware message history
108
+ if (!contextText && availableTokens > 200) {
109
+ const historyContext = this.buildTokenAwareHistory(convoId, availableTokens, config);
110
+ if (historyContext.text) {
111
+ contextText = historyContext.text;
112
+ contextTokens = historyContext.tokens;
113
+ contextSource = 'history';
114
+ }
94
115
  }
95
116
  }
96
117
 
97
118
  // Build final prompt
98
119
  let prompt = userMessage;
99
120
 
121
+ if (availableTokens === 0) {
122
+ console.log(`[ContextBridge] Budget exhausted before context; sending raw message (engine=${toEngine})`);
123
+ }
124
+
100
125
  if (contextText) {
101
- if (isEngineBridge) {
102
- prompt = `${contextText}\n\n[Switching from ${fromEngine} to ${toEngine}]\n\nUser message:\n${userMessage}`;
103
- } else {
104
- prompt = `${contextText}\n\nUser message:\n${userMessage}`;
105
- }
126
+ prompt = `${contextText}\n\n${userMessage}`;
106
127
  }
107
128
 
108
- console.log(`[ContextBridge] Built context: ${contextTokens} tokens from ${contextSource}, bridge: ${isEngineBridge}`);
129
+ console.log(`[ContextBridge] Built context: ${contextTokens} tokens from ${contextSource}, bridge: ${isEngineBridge}, avail=${availableTokens}, total=${contextTokens + userTokens}`);
109
130
 
110
131
  return {
111
132
  prompt,
@@ -116,6 +137,106 @@ class ContextBridge {
116
137
  };
117
138
  }
118
139
 
140
+ /**
141
+ * Build structured context for engine handoff
142
+ * Uses a clear template that helps the new engine understand previous context
143
+ * @param {string} conversationId
144
+ * @param {string} fromEngine
145
+ * @param {string} toEngine
146
+ * @param {number} maxTokens
147
+ * @param {Object} config
148
+ * @returns {Object} { text, tokens, source }
149
+ */
150
+ buildEngineHandoffContext(conversationId, fromEngine, toEngine, maxTokens, config = {}) {
151
+ const engineNames = {
152
+ 'claude': 'Claude Code (Anthropic)',
153
+ 'codex': 'Codex (OpenAI)',
154
+ 'gemini': 'Gemini (Google)',
155
+ 'deepseek': 'DeepSeek'
156
+ };
157
+
158
+ const fromName = engineNames[fromEngine] || fromEngine;
159
+ const toName = engineNames[toEngine] || toEngine;
160
+
161
+ // Get summary if available
162
+ const summary = this.summaryGenerator.getSummary(conversationId);
163
+
164
+ // Get recent messages (last 5)
165
+ const messages = Message.getContextMessages(conversationId, 5);
166
+ const messageCount = Message.countByConversation(conversationId);
167
+
168
+ // Build structured template
169
+ const sections = [];
170
+
171
+ // Header
172
+ sections.push(`<previous_session_context engine="${fromEngine}" total_messages="${messageCount}">`);
173
+ sections.push(`This conversation was previously handled by ${fromName}.`);
174
+ sections.push(`You are now continuing as ${toName}.`);
175
+ sections.push('');
176
+
177
+ // Summary section (if available)
178
+ if (summary && summary.summary_short) {
179
+ sections.push('## Summary');
180
+ sections.push(summary.summary_short);
181
+ sections.push('');
182
+
183
+ // Key decisions
184
+ if (summary.key_decisions && summary.key_decisions.length > 0) {
185
+ sections.push('## Key Decisions');
186
+ summary.key_decisions.slice(0, 5).forEach(d => sections.push(`- ${d}`));
187
+ sections.push('');
188
+ }
189
+
190
+ // Files modified
191
+ if (summary.files_modified && summary.files_modified.length > 0) {
192
+ sections.push('## Files Modified');
193
+ summary.files_modified.slice(0, 10).forEach(f => sections.push(`- ${f}`));
194
+ sections.push('');
195
+ }
196
+ }
197
+
198
+ // Recent messages (always include for continuity)
199
+ if (messages.length > 0) {
200
+ sections.push('## Recent Messages');
201
+ for (const msg of messages) {
202
+ const role = msg.role === 'user' ? 'User' : 'Assistant';
203
+ const engine = msg.engine ? ` [${msg.engine}]` : '';
204
+ // Truncate long messages
205
+ let content = msg.content || '';
206
+ if (content.length > 500) {
207
+ content = content.substring(0, 500) + '...';
208
+ }
209
+ sections.push(`${role}${engine}: ${content}`);
210
+ sections.push('');
211
+ }
212
+ }
213
+
214
+ sections.push('</previous_session_context>');
215
+ sections.push('');
216
+ sections.push('Continue assisting with the following request:');
217
+
218
+ const text = sections.join('\n');
219
+ const tokens = this.estimateTokens(text);
220
+
221
+ // Check token budget
222
+ if (tokens > maxTokens) {
223
+ // Fallback to simpler context if too long
224
+ console.log(`[ContextBridge] Handoff template too long (${tokens} > ${maxTokens}), using fallback`);
225
+ const fallback = this.buildTokenAwareHistory(conversationId, maxTokens, config);
226
+ return {
227
+ text: fallback.text,
228
+ tokens: fallback.tokens,
229
+ source: 'handoff_fallback_history'
230
+ };
231
+ }
232
+
233
+ return {
234
+ text,
235
+ tokens,
236
+ source: summary ? 'handoff+summary' : 'handoff+history'
237
+ };
238
+ }
239
+
119
240
  /**
120
241
  * Build token-aware history context
121
242
  * @param {string} sessionId - Session ID
@@ -123,9 +244,9 @@ class ContextBridge {
123
244
  * @param {Object} config - Engine config
124
245
  * @returns {Object} { text, tokens, messageCount }
125
246
  */
126
- buildTokenAwareHistory(sessionId, maxTokens, config = {}) {
247
+ buildTokenAwareHistory(conversationId, maxTokens, config = {}) {
127
248
  // Get more messages than we need, we'll trim
128
- const messages = Message.getContextMessages(sessionId, 20);
249
+ const messages = Message.getContextMessages(conversationId, 20);
129
250
 
130
251
  if (messages.length === 0) {
131
252
  return { text: '', tokens: 0, messageCount: 0 };
@@ -139,11 +260,13 @@ class ContextBridge {
139
260
  for (let i = messages.length - 1; i >= 0; i--) {
140
261
  const msg = messages[i];
141
262
 
142
- // For code-focused engines, filter out non-code content
263
+ // For code-focused engines, compress assistant responses to code only
264
+ // BUT always keep user messages for context continuity
143
265
  let content = msg.content;
144
- if (config.codeOnly) {
145
- content = this.extractCodeContent(content);
146
- if (!content) continue; // Skip if no code
266
+ if (config.codeOnly && msg.role === 'assistant') {
267
+ const codeContent = this.extractCodeContent(content);
268
+ // Only use code-only if there's actual code, otherwise keep truncated original
269
+ content = codeContent || (content.length > 500 ? content.substring(0, 500) + '...' : content);
147
270
  }
148
271
 
149
272
  // Truncate long messages
@@ -228,11 +351,11 @@ class ContextBridge {
228
351
  * @param {boolean} isEngineBridge - Was this an engine switch
229
352
  * @returns {boolean} Should generate summary
230
353
  */
231
- shouldTriggerSummary(sessionId, isEngineBridge = false) {
354
+ shouldTriggerSummary(conversationId, isEngineBridge = false) {
232
355
  // Always trigger on engine bridge
233
356
  if (isEngineBridge) return true;
234
357
 
235
- const messageCount = Message.countByConversation(sessionId);
358
+ const messageCount = Message.countByConversation(conversationId);
236
359
 
237
360
  // Trigger every 10 messages after threshold
238
361
  if (messageCount >= SUMMARY_TRIGGER_THRESHOLD && messageCount % 10 === 0) {
@@ -240,7 +363,7 @@ class ContextBridge {
240
363
  }
241
364
 
242
365
  // Check if we have a stale summary (older than 20 messages)
243
- const existingSummary = this.summaryGenerator.getSummary(sessionId);
366
+ const existingSummary = this.summaryGenerator.getSummary(conversationId);
244
367
  if (!existingSummary && messageCount > SUMMARY_TRIGGER_THRESHOLD) {
245
368
  return true;
246
369
  }
@@ -253,10 +376,10 @@ class ContextBridge {
253
376
  * @param {string} sessionId - Session ID
254
377
  * @param {string} logPrefix - Log prefix for debugging
255
378
  */
256
- triggerSummaryGeneration(sessionId, logPrefix = '[ContextBridge]') {
257
- const messages = Message.getByConversation(sessionId, 40);
379
+ triggerSummaryGeneration(conversationId, logPrefix = '[ContextBridge]') {
380
+ const messages = Message.getByConversation(conversationId, 40);
258
381
 
259
- this.summaryGenerator.generateAndSave(sessionId, messages)
382
+ this.summaryGenerator.generateAndSave(conversationId, messages)
260
383
  .then(summary => {
261
384
  if (summary) {
262
385
  console.log(`${logPrefix} Summary updated: ${summary.summary_short?.substring(0, 50)}...`);
@@ -272,10 +395,10 @@ class ContextBridge {
272
395
  * @param {string} sessionId - Session ID
273
396
  * @returns {Object} Stats
274
397
  */
275
- getContextStats(sessionId) {
276
- const messageCount = Message.countByConversation(sessionId);
277
- const lastEngine = Message.getLastEngine(sessionId);
278
- const hasSummary = !!this.summaryGenerator.getSummary(sessionId);
398
+ getContextStats(conversationId) {
399
+ const messageCount = Message.countByConversation(conversationId);
400
+ const lastEngine = Message.getLastEngine(conversationId);
401
+ const hasSummary = !!this.summaryGenerator.getSummary(conversationId);
279
402
 
280
403
  return {
281
404
  messageCount,
@@ -10,13 +10,14 @@
10
10
  * - -o stream-json: JSON streaming output
11
11
  * - --include-directories: Workspace access
12
12
  *
13
- * @version 0.4.0 - TRI CLI Support
13
+ * @version 0.5.0 - Extended BaseCliWrapper for interrupt support
14
14
  */
15
15
 
16
16
  const fs = require('fs');
17
17
  const path = require('path');
18
18
  const pty = require('../lib/pty-adapter');
19
19
  const GeminiOutputParser = require('./gemini-output-parser');
20
+ const BaseCliWrapper = require('./base-cli-wrapper');
20
21
 
21
22
  // Default model - Gemini 3 Pro Preview
22
23
  const DEFAULT_MODEL = 'gemini-3-pro-preview';
@@ -24,8 +25,9 @@ const DEFAULT_MODEL = 'gemini-3-pro-preview';
24
25
  // CLI timeout (10 minutes)
25
26
  const CLI_TIMEOUT_MS = 600000;
26
27
 
27
- class GeminiWrapper {
28
+ class GeminiWrapper extends BaseCliWrapper {
28
29
  constructor(options = {}) {
30
+ super(); // Initialize activeProcesses from BaseCliWrapper
29
31
  this.geminiPath = this._resolveGeminiPath(options.geminiPath);
30
32
  this.workspaceDir = options.workspaceDir || process.cwd();
31
33
 
@@ -67,18 +69,19 @@ class GeminiWrapper {
67
69
  *
68
70
  * @param {Object} params
69
71
  * @param {string} params.prompt - User message/prompt
70
- * @param {string} params.sessionId - Session UUID (for logging)
72
+ * @param {string} params.threadId - Native Gemini session ID for resume
71
73
  * @param {string} [params.model='gemini-3-pro-preview'] - Model name
72
74
  * @param {string} [params.workspacePath] - Workspace directory
73
75
  * @param {Function} [params.onStatus] - Callback for status events (SSE streaming)
74
- * @returns {Promise<{text: string, usage: Object}>}
76
+ * @returns {Promise<{text: string, usage: Object, sessionId: string}>}
75
77
  */
76
78
  async sendMessage({
77
79
  prompt,
78
- sessionId,
80
+ threadId,
79
81
  model = DEFAULT_MODEL,
80
82
  workspacePath,
81
- onStatus
83
+ onStatus,
84
+ processId: processIdOverride
82
85
  }) {
83
86
  return new Promise((resolve, reject) => {
84
87
  const parser = new GeminiOutputParser();
@@ -87,16 +90,23 @@ class GeminiWrapper {
87
90
  const cwd = workspacePath || this.workspaceDir;
88
91
 
89
92
  // Build CLI arguments
90
- // Note: cwd is set in pty.spawn() options, no need for --include-directories
93
+ // If threadId exists, use --resume to continue native session
91
94
  const args = [
92
95
  '-y', // YOLO mode - auto-approve all actions
93
96
  '-m', model, // Model selection
94
97
  '-o', 'stream-json', // JSON streaming for structured events
95
- prompt // Prompt as positional argument
96
98
  ];
97
99
 
100
+ // Add resume flag if continuing existing session
101
+ if (threadId) {
102
+ args.push('--resume', threadId);
103
+ }
104
+
105
+ // Add prompt as positional argument
106
+ args.push(prompt);
107
+
98
108
  console.log(`[GeminiWrapper] Model: ${model}`);
99
- console.log(`[GeminiWrapper] Session: ${sessionId}`);
109
+ console.log(`[GeminiWrapper] ThreadId: ${threadId || '(new session)'}`);
100
110
  console.log(`[GeminiWrapper] CWD: ${cwd}`);
101
111
  console.log(`[GeminiWrapper] Prompt length: ${prompt.length}`);
102
112
 
@@ -117,6 +127,11 @@ class GeminiWrapper {
117
127
  return reject(new Error(`Failed to spawn Gemini CLI: ${spawnError.message}`));
118
128
  }
119
129
 
130
+ // Register process for interrupt capability
131
+ // Prefer external processId (Nexus sessionId), else threadId, else temp
132
+ const processId = processIdOverride || threadId || `gemini-${Date.now()}`;
133
+ this.registerProcess(processId, ptyProcess, 'pty');
134
+
120
135
  let stdout = '';
121
136
 
122
137
  // Handle PTY data
@@ -145,6 +160,9 @@ class GeminiWrapper {
145
160
 
146
161
  // Handle PTY exit
147
162
  ptyProcess.onExit(({ exitCode }) => {
163
+ // Unregister process on exit
164
+ this.unregisterProcess(processId);
165
+
148
166
  if (promiseSettled) return;
149
167
  promiseSettled = true;
150
168
 
@@ -170,6 +188,7 @@ class GeminiWrapper {
170
188
 
171
189
  resolve({
172
190
  text: finalResponse,
191
+ sessionId: parser.getSessionId(), // Native Gemini session ID for resume
173
192
  usage: {
174
193
  prompt_tokens: promptTokens,
175
194
  completion_tokens: completionTokens,
@@ -0,0 +1,155 @@
1
+ /**
2
+ * SessionImporter - Importa sessioni CLI native in tabella sessions (indice NexusCLI)
3
+ *
4
+ * Scansione:
5
+ * - Claude: ~/.claude/projects/<slug>/<sessionId>.jsonl
6
+ * - Codex : ~/.codex/sessions/<sessionId>.jsonl
7
+ * - Gemini: ~/.gemini/sessions/<sessionId>.jsonl
8
+ *
9
+ * Note:
10
+ * - Usa FILESYSTEM come source of truth: non legge contenuti, solo metadati.
11
+ * - workspace_path stimato da slug (best effort, non reversibile al 100%).
12
+ * - Non sovrascrive entry esistenti in DB.
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const { prepare, saveDb } = require('../db');
18
+
19
+ const HOME = process.env.HOME || '';
20
+
21
+ const CLAUDE_PROJECTS = path.join(HOME, '.claude', 'projects');
22
+ const CODEX_SESSIONS = path.join(HOME, '.codex', 'sessions');
23
+ const GEMINI_SESSIONS = path.join(HOME, '.gemini', 'sessions');
24
+
25
+ class SessionImporter {
26
+ constructor() {}
27
+
28
+ /**
29
+ * Importa tutte le sessioni per tutti gli engine
30
+ * @returns {{claude:number, codex:number, gemini:number}}
31
+ */
32
+ importAll() {
33
+ const claude = this.importClaudeSessions();
34
+ const codex = this.importCodexSessions();
35
+ const gemini = this.importGeminiSessions();
36
+ return { claude, codex, gemini };
37
+ }
38
+
39
+ /**
40
+ * Claude: ~/.claude/projects/<slug>/<sessionId>.jsonl
41
+ */
42
+ importClaudeSessions() {
43
+ let imported = 0;
44
+ if (!fs.existsSync(CLAUDE_PROJECTS)) return imported;
45
+
46
+ const projectSlugs = fs.readdirSync(CLAUDE_PROJECTS);
47
+ for (const slug of projectSlugs) {
48
+ const projectDir = path.join(CLAUDE_PROJECTS, slug);
49
+ if (!fs.statSync(projectDir).isDirectory()) continue;
50
+
51
+ const files = fs.readdirSync(projectDir).filter(f => f.endsWith('.jsonl'));
52
+ for (const file of files) {
53
+ const sessionId = file.replace('.jsonl', '');
54
+ if (this.sessionExists(sessionId)) continue;
55
+
56
+ const workspacePath = this.slugToPath(slug);
57
+ this.insertSession(sessionId, 'claude', workspacePath, null);
58
+ imported++;
59
+ }
60
+ }
61
+
62
+ if (imported > 0) saveDb();
63
+ console.log(`[SessionImporter] Claude imported: ${imported}`);
64
+ return imported;
65
+ }
66
+
67
+ /**
68
+ * Codex: ~/.codex/sessions/<sessionId>.jsonl
69
+ */
70
+ importCodexSessions() {
71
+ let imported = 0;
72
+ if (!fs.existsSync(CODEX_SESSIONS)) return imported;
73
+
74
+ const files = fs.readdirSync(CODEX_SESSIONS)
75
+ .filter(f => f.endsWith('.jsonl') || f.endsWith('.json'));
76
+ for (const file of files) {
77
+ const sessionId = file.replace('.jsonl', '');
78
+ if (this.sessionExists(sessionId)) continue;
79
+
80
+ this.insertSession(sessionId, 'codex', '', null);
81
+ imported++;
82
+ }
83
+
84
+ if (imported > 0) saveDb();
85
+ console.log(`[SessionImporter] Codex imported: ${imported}`);
86
+ return imported;
87
+ }
88
+
89
+ /**
90
+ * Gemini: ~/.gemini/sessions/<sessionId>.jsonl
91
+ */
92
+ importGeminiSessions() {
93
+ let imported = 0;
94
+ if (!fs.existsSync(GEMINI_SESSIONS)) return imported;
95
+
96
+ const files = fs.readdirSync(GEMINI_SESSIONS)
97
+ .filter(f => f.endsWith('.jsonl') || f.endsWith('.json'));
98
+ for (const file of files) {
99
+ const sessionId = file.replace('.jsonl', '');
100
+ if (this.sessionExists(sessionId)) continue;
101
+
102
+ this.insertSession(sessionId, 'gemini', '', null);
103
+ imported++;
104
+ }
105
+
106
+ if (imported > 0) saveDb();
107
+ console.log(`[SessionImporter] Gemini imported: ${imported}`);
108
+ return imported;
109
+ }
110
+
111
+ /**
112
+ * Inserisce riga minima in sessions
113
+ */
114
+ insertSession(id, engine, workspacePath, conversationId) {
115
+ try {
116
+ const now = Date.now();
117
+ const stmt = prepare(`
118
+ INSERT INTO sessions (id, engine, workspace_path, conversation_id, title, created_at, last_used_at)
119
+ VALUES (?, ?, ?, ?, ?, ?, ?)
120
+ `);
121
+ stmt.run(id, engine, workspacePath || '', conversationId || null, 'Imported Chat', now, now);
122
+ } catch (err) {
123
+ console.warn(`[SessionImporter] insert failed for ${id}: ${err.message}`);
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Controlla se la sessione esiste già
129
+ */
130
+ sessionExists(sessionId) {
131
+ try {
132
+ const stmt = prepare('SELECT 1 FROM sessions WHERE id = ?');
133
+ return !!stmt.get(sessionId);
134
+ } catch (err) {
135
+ console.warn(`[SessionImporter] exists check failed: ${err.message}`);
136
+ return true; // default to skip to avoid duplicates
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Best-effort reverse di slug Claude in path
142
+ * -path-to-dir → /path/to/dir
143
+ * Conserva i punti.
144
+ */
145
+ slugToPath(slug) {
146
+ if (!slug) return '';
147
+ // Claude slug era workspacePath.replace(/\//g, '-'); mantiene i punti
148
+ // Invertiamo: leading '-' → '/', i restanti '-' → '/'
149
+ let tmp = slug;
150
+ if (tmp.startsWith('-')) tmp = '/' + tmp.slice(1);
151
+ return tmp.replace(/-/g, '/');
152
+ }
153
+ }
154
+
155
+ module.exports = new SessionImporter();
@@ -442,6 +442,26 @@ class SessionManager {
442
442
  }
443
443
  }
444
444
 
445
+ /**
446
+ * Bump session activity counters (message_count, last_used_at)
447
+ * @param {string} sessionId
448
+ * @param {number} increment
449
+ */
450
+ bumpSessionActivity(sessionId, increment = 1) {
451
+ if (!sessionId) return;
452
+ try {
453
+ const stmt = prepare(`
454
+ UPDATE sessions
455
+ SET message_count = COALESCE(message_count, 0) + ?, last_used_at = ?
456
+ WHERE id = ?
457
+ `);
458
+ stmt.run(increment, Date.now(), sessionId);
459
+ saveDb();
460
+ } catch (error) {
461
+ console.warn(`[SessionManager] Failed to bump activity for ${sessionId}:`, error.message);
462
+ }
463
+ }
464
+
445
465
  /**
446
466
  * Get all sessions for a conversation
447
467
  */
@@ -328,13 +328,8 @@ class WorkspaceManager {
328
328
  // Sort by most recent first
329
329
  sessions.sort((a, b) => b.last_used_at - a.last_used_at);
330
330
 
331
- // Limit to most recent 10 sessions (lazy load the rest)
332
- const limit = 10;
333
- const totalSessions = sessions.length;
334
- const limitedSessions = sessions.slice(0, limit);
335
-
336
- console.log(`[WorkspaceManager] Loaded ${limitedSessions.length} of ${totalSessions} sessions (sorted by recency)`);
337
- return limitedSessions;
331
+ console.log(`[WorkspaceManager] Loaded ${sessions.length} sessions (sorted by recency)`);
332
+ return sessions;
338
333
  }
339
334
 
340
335
  /**
@@ -396,7 +391,7 @@ class WorkspaceManager {
396
391
  * @returns {string}
397
392
  */
398
393
  getSessionPath(workspacePath) {
399
- // Convert /home/user/myproject → -home-user-myproject
394
+ // Convert /var/www/myapp → -var-www-myapp
400
395
  const projectDir = workspacePath.replace(/\//g, '-').replace(/^-/, '');
401
396
  return path.join(this.claudePath, 'projects', projectDir);
402
397
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscli",
3
- "version": "0.7.6",
3
+ "version": "0.7.8",
4
4
  "description": "NexusCLI - TRI CLI Control Plane (Claude/Codex/Gemini)",
5
5
  "main": "lib/server/server.js",
6
6
  "bin": {