@mmmbuto/nexuscli 0.7.7 → 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 +20 -32
- package/bin/nexuscli.js +6 -6
- package/frontend/dist/assets/{index-CHOlrfA0.css → index-WfmfixF4.css} +1 -1
- package/frontend/dist/index.html +2 -2
- package/lib/server/.env.example +1 -1
- package/lib/server/lib/pty-adapter.js +1 -15
- package/lib/server/routes/codex.js +9 -2
- package/lib/server/routes/gemini.js +9 -3
- package/lib/server/routes/sessions.js +15 -0
- package/lib/server/server.js +9 -0
- package/lib/server/services/claude-wrapper.js +1 -11
- package/lib/server/services/codex-output-parser.js +0 -8
- package/lib/server/services/codex-wrapper.js +3 -3
- package/lib/server/services/context-bridge.js +143 -24
- package/lib/server/services/gemini-wrapper.js +4 -3
- package/lib/server/services/session-importer.js +155 -0
- package/lib/server/services/workspace-manager.js +2 -7
- package/lib/server/tests/performance.test.js +1 -1
- package/lib/server/tests/services.test.js +2 -2
- package/package.json +1 -1
- package/lib/server/db.js.old +0 -225
- package/lib/server/docs/API_WRAPPER_CONTRACT.md +0 -682
- package/lib/server/docs/ARCHITECTURE.md +0 -441
- package/lib/server/docs/DATABASE_SCHEMA.md +0 -783
- package/lib/server/docs/DESIGN_PRINCIPLES.md +0 -598
- package/lib/server/docs/NEXUSCHAT_ANALYSIS.md +0 -488
- package/lib/server/docs/PIPELINE_INTEGRATION.md +0 -636
- package/lib/server/docs/README.md +0 -272
- package/lib/server/docs/UI_DESIGN.md +0 -916
- package/lib/server/services/base-cli-wrapper.js +0 -137
- package/lib/server/services/cli-loader.js.backup +0 -446
- /package/frontend/dist/assets/{index-BAY_sRAu.js → index-BbBoc8w4.js} +0 -0
|
@@ -263,13 +263,6 @@ 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
|
-
|
|
273
266
|
/**
|
|
274
267
|
* Reset parser state for new request
|
|
275
268
|
*/
|
|
@@ -277,7 +270,6 @@ class CodexOutputParser {
|
|
|
277
270
|
this.buffer = '';
|
|
278
271
|
this.finalResponse = '';
|
|
279
272
|
this.usage = null;
|
|
280
|
-
this.threadId = null;
|
|
281
273
|
this.pendingCommands.clear();
|
|
282
274
|
}
|
|
283
275
|
}
|
|
@@ -35,7 +35,7 @@ class CodexWrapper extends BaseCliWrapper {
|
|
|
35
35
|
* @param {Function} options.onStatus - Callback for status events
|
|
36
36
|
* @returns {Promise<Object>} Response with text, usage, threadId
|
|
37
37
|
*/
|
|
38
|
-
async sendMessage({ prompt, model, threadId, reasoningEffort, workspacePath, imageFiles = [], onStatus }) {
|
|
38
|
+
async sendMessage({ prompt, model, threadId, reasoningEffort, workspacePath, imageFiles = [], onStatus, processId: processIdOverride }) {
|
|
39
39
|
return new Promise((resolve, reject) => {
|
|
40
40
|
const parser = new CodexOutputParser();
|
|
41
41
|
const cwd = workspacePath || this.workspaceDir;
|
|
@@ -93,8 +93,8 @@ class CodexWrapper extends BaseCliWrapper {
|
|
|
93
93
|
});
|
|
94
94
|
|
|
95
95
|
// Register process for interrupt capability
|
|
96
|
-
//
|
|
97
|
-
const processId = threadId || `codex-${Date.now()}`;
|
|
96
|
+
// Prefer external processId (Nexus sessionId), else threadId, else temp
|
|
97
|
+
const processId = processIdOverride || threadId || `codex-${Date.now()}`;
|
|
98
98
|
this.registerProcess(processId, proc, 'spawn');
|
|
99
99
|
|
|
100
100
|
proc.stdout.on('data', (data) => {
|
|
@@ -67,47 +67,66 @@ class ContextBridge {
|
|
|
67
67
|
|
|
68
68
|
// Reserve tokens for user message
|
|
69
69
|
const userTokens = this.estimateTokens(userMessage);
|
|
70
|
-
const availableTokens = config.maxTokens - userTokens - 200; // 200 token buffer
|
|
70
|
+
const availableTokens = Math.max(0, config.maxTokens - userTokens - 200); // 200 token buffer, never negative
|
|
71
71
|
|
|
72
72
|
let contextText = '';
|
|
73
73
|
let contextTokens = 0;
|
|
74
74
|
let contextSource = 'none';
|
|
75
75
|
|
|
76
|
-
//
|
|
77
|
-
if (
|
|
78
|
-
const
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
+
}
|
|
85
104
|
}
|
|
86
105
|
}
|
|
87
|
-
}
|
|
88
106
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
+
}
|
|
96
115
|
}
|
|
97
116
|
}
|
|
98
117
|
|
|
99
118
|
// Build final prompt
|
|
100
119
|
let prompt = userMessage;
|
|
101
120
|
|
|
121
|
+
if (availableTokens === 0) {
|
|
122
|
+
console.log(`[ContextBridge] Budget exhausted before context; sending raw message (engine=${toEngine})`);
|
|
123
|
+
}
|
|
124
|
+
|
|
102
125
|
if (contextText) {
|
|
103
|
-
|
|
104
|
-
prompt = `${contextText}\n\n[Switching from ${fromEngine} to ${toEngine}]\n\nUser message:\n${userMessage}`;
|
|
105
|
-
} else {
|
|
106
|
-
prompt = `${contextText}\n\nUser message:\n${userMessage}`;
|
|
107
|
-
}
|
|
126
|
+
prompt = `${contextText}\n\n${userMessage}`;
|
|
108
127
|
}
|
|
109
128
|
|
|
110
|
-
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}`);
|
|
111
130
|
|
|
112
131
|
return {
|
|
113
132
|
prompt,
|
|
@@ -118,6 +137,106 @@ class ContextBridge {
|
|
|
118
137
|
};
|
|
119
138
|
}
|
|
120
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
|
+
|
|
121
240
|
/**
|
|
122
241
|
* Build token-aware history context
|
|
123
242
|
* @param {string} sessionId - Session ID
|
|
@@ -80,7 +80,8 @@ class GeminiWrapper extends BaseCliWrapper {
|
|
|
80
80
|
threadId,
|
|
81
81
|
model = DEFAULT_MODEL,
|
|
82
82
|
workspacePath,
|
|
83
|
-
onStatus
|
|
83
|
+
onStatus,
|
|
84
|
+
processId: processIdOverride
|
|
84
85
|
}) {
|
|
85
86
|
return new Promise((resolve, reject) => {
|
|
86
87
|
const parser = new GeminiOutputParser();
|
|
@@ -127,8 +128,8 @@ class GeminiWrapper extends BaseCliWrapper {
|
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
// Register process for interrupt capability
|
|
130
|
-
//
|
|
131
|
-
const processId = threadId || `gemini-${Date.now()}`;
|
|
131
|
+
// Prefer external processId (Nexus sessionId), else threadId, else temp
|
|
132
|
+
const processId = processIdOverride || threadId || `gemini-${Date.now()}`;
|
|
132
133
|
this.registerProcess(processId, ptyProcess, 'pty');
|
|
133
134
|
|
|
134
135
|
let stdout = '';
|
|
@@ -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();
|
|
@@ -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
|
-
|
|
332
|
-
|
|
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
|
/**
|
|
@@ -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 = '/home/user/myproject';
|
|
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 = '/home/user/myproject';
|
|
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 = '/home/user/myproject';
|
|
151
151
|
const validated = await manager.validateWorkspace(testPath);
|
|
152
152
|
expect(validated).toBe(testPath);
|
|
153
153
|
});
|