@mmmbuto/nexuscli 0.7.7 → 0.7.9

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.
Files changed (33) hide show
  1. package/README.md +18 -30
  2. package/bin/nexuscli.js +6 -6
  3. package/frontend/dist/assets/{index-CHOlrfA0.css → index-WfmfixF4.css} +1 -1
  4. package/frontend/dist/index.html +2 -2
  5. package/lib/server/.env.example +1 -1
  6. package/lib/server/lib/pty-adapter.js +1 -15
  7. package/lib/server/routes/codex.js +9 -2
  8. package/lib/server/routes/gemini.js +9 -3
  9. package/lib/server/routes/sessions.js +15 -0
  10. package/lib/server/server.js +9 -0
  11. package/lib/server/services/claude-wrapper.js +1 -11
  12. package/lib/server/services/codex-output-parser.js +0 -8
  13. package/lib/server/services/codex-wrapper.js +3 -3
  14. package/lib/server/services/context-bridge.js +143 -24
  15. package/lib/server/services/gemini-wrapper.js +4 -3
  16. package/lib/server/services/session-importer.js +155 -0
  17. package/lib/server/services/workspace-manager.js +2 -7
  18. package/lib/server/tests/performance.test.js +1 -1
  19. package/lib/server/tests/services.test.js +2 -2
  20. package/lib/setup/postinstall.js +4 -1
  21. package/package.json +1 -1
  22. package/lib/server/db.js.old +0 -225
  23. package/lib/server/docs/API_WRAPPER_CONTRACT.md +0 -682
  24. package/lib/server/docs/ARCHITECTURE.md +0 -441
  25. package/lib/server/docs/DATABASE_SCHEMA.md +0 -783
  26. package/lib/server/docs/DESIGN_PRINCIPLES.md +0 -598
  27. package/lib/server/docs/NEXUSCHAT_ANALYSIS.md +0 -488
  28. package/lib/server/docs/PIPELINE_INTEGRATION.md +0 -636
  29. package/lib/server/docs/README.md +0 -272
  30. package/lib/server/docs/UI_DESIGN.md +0 -916
  31. package/lib/server/services/base-cli-wrapper.js +0 -137
  32. package/lib/server/services/cli-loader.js.backup +0 -446
  33. /package/frontend/dist/assets/{index-BAY_sRAu.js → index-BbBoc8w4.js} +0 -0
@@ -1,137 +0,0 @@
1
- /**
2
- * Base CLI Wrapper - Common interrupt logic for all CLI wrappers
3
- *
4
- * Provides unified process tracking and interrupt capability for:
5
- * - ClaudeWrapper (PTY)
6
- * - GeminiWrapper (PTY)
7
- * - CodexWrapper (child_process.spawn)
8
- *
9
- * @version 0.5.0 - Interrupt Support
10
- */
11
-
12
- class BaseCliWrapper {
13
- constructor() {
14
- // Map: sessionId → { process, type, startTime }
15
- this.activeProcesses = new Map();
16
- }
17
-
18
- /**
19
- * Register active process for interrupt capability
20
- * @param {string} sessionId - Session/conversation ID
21
- * @param {Object} process - PTY process or child_process
22
- * @param {string} type - 'pty' or 'spawn'
23
- */
24
- registerProcess(sessionId, process, type = 'pty') {
25
- this.activeProcesses.set(sessionId, {
26
- process,
27
- type,
28
- startTime: Date.now()
29
- });
30
- console.log(`[BaseWrapper] Registered ${type} process for session ${sessionId}`);
31
- }
32
-
33
- /**
34
- * Unregister process on completion
35
- * @param {string} sessionId
36
- */
37
- unregisterProcess(sessionId) {
38
- if (this.activeProcesses.has(sessionId)) {
39
- this.activeProcesses.delete(sessionId);
40
- console.log(`[BaseWrapper] Unregistered process for session ${sessionId}`);
41
- }
42
- }
43
-
44
- /**
45
- * Interrupt running process
46
- *
47
- * Strategy:
48
- * - PTY: Try ESC (0x1B) first, then SIGINT
49
- * - Spawn: SIGINT directly
50
- *
51
- * @param {string} sessionId
52
- * @returns {{ success: boolean, method?: string, reason?: string }}
53
- */
54
- interrupt(sessionId) {
55
- const entry = this.activeProcesses.get(sessionId);
56
-
57
- if (!entry) {
58
- console.log(`[BaseWrapper] No active process for session ${sessionId}`);
59
- return { success: false, reason: 'no_active_process' };
60
- }
61
-
62
- const { process, type } = entry;
63
-
64
- try {
65
- if (type === 'pty') {
66
- // PTY process: Try ESC first (gentler), then SIGINT
67
- if (typeof process.sendEsc === 'function' && process.sendEsc()) {
68
- console.log(`[BaseWrapper] Sent ESC to PTY session ${sessionId}`);
69
- // Don't remove yet - let onExit handle cleanup
70
- return { success: true, method: 'esc' };
71
- }
72
-
73
- // ESC failed or not available, use SIGINT
74
- if (typeof process.kill === 'function') {
75
- process.kill('SIGINT');
76
- console.log(`[BaseWrapper] Sent SIGINT to PTY session ${sessionId}`);
77
- return { success: true, method: 'sigint' };
78
- }
79
- } else if (type === 'spawn') {
80
- // child_process.spawn: SIGINT directly
81
- if (typeof process.kill === 'function') {
82
- process.kill('SIGINT');
83
- console.log(`[BaseWrapper] Sent SIGINT to spawn session ${sessionId}`);
84
- return { success: true, method: 'sigint' };
85
- }
86
- }
87
-
88
- return { success: false, reason: 'kill_not_available' };
89
- } catch (err) {
90
- console.error(`[BaseWrapper] Interrupt error for session ${sessionId}:`, err.message);
91
- return { success: false, reason: err.message };
92
- }
93
- }
94
-
95
- /**
96
- * Check if session has active process
97
- * @param {string} sessionId
98
- * @returns {boolean}
99
- */
100
- isActive(sessionId) {
101
- return this.activeProcesses.has(sessionId);
102
- }
103
-
104
- /**
105
- * Get count of active processes
106
- * @returns {number}
107
- */
108
- getActiveCount() {
109
- return this.activeProcesses.size;
110
- }
111
-
112
- /**
113
- * Get all active session IDs
114
- * @returns {string[]}
115
- */
116
- getActiveSessions() {
117
- return Array.from(this.activeProcesses.keys());
118
- }
119
-
120
- /**
121
- * Get process info for session
122
- * @param {string} sessionId
123
- * @returns {{ type: string, startTime: number, duration: number } | null}
124
- */
125
- getProcessInfo(sessionId) {
126
- const entry = this.activeProcesses.get(sessionId);
127
- if (!entry) return null;
128
-
129
- return {
130
- type: entry.type,
131
- startTime: entry.startTime,
132
- duration: Date.now() - entry.startTime
133
- };
134
- }
135
- }
136
-
137
- module.exports = BaseCliWrapper;
@@ -1,446 +0,0 @@
1
- /**
2
- * CliLoader - Unified message loader for TRI CLI (Claude/Codex/Gemini)
3
- *
4
- * Loads messages on-demand from CLI history files (lazy loading).
5
- * Filesystem is the source of truth - no DB caching of messages.
6
- *
7
- * Session file locations:
8
- * - Claude: ~/.claude/projects/<workspace-slug>/<sessionId>.jsonl
9
- * - Codex: ~/.codex/sessions/<sessionId>.jsonl (if available)
10
- * - Gemini: ~/.gemini/sessions/<sessionId>.jsonl (if available)
11
- *
12
- * @version 0.4.0 - TRI CLI Support
13
- */
14
-
15
- const fs = require('fs');
16
- const path = require('path');
17
- const readline = require('readline');
18
-
19
- const DEFAULT_LIMIT = 30;
20
-
21
- // Engine-specific paths
22
- const ENGINE_PATHS = {
23
- claude: path.join(process.env.HOME || '', '.claude'),
24
- codex: path.join(process.env.HOME || '', '.codex'),
25
- gemini: path.join(process.env.HOME || '', '.gemini'),
26
- };
27
-
28
- class CliLoader {
29
- constructor() {
30
- this.claudePath = ENGINE_PATHS.claude;
31
- this.codexPath = ENGINE_PATHS.codex;
32
- this.geminiPath = ENGINE_PATHS.gemini;
33
- }
34
-
35
- /**
36
- * Load messages from CLI history by session.
37
- * Supports all three engines: Claude, Codex, Gemini.
38
- *
39
- * @param {Object} params
40
- * @param {string} params.sessionId - Session UUID
41
- * @param {string} params.engine - 'claude'|'claude-code'|'codex'|'gemini'
42
- * @param {string} params.workspacePath - Workspace directory (required for Claude)
43
- * @param {number} [params.limit=30] - Max messages to return
44
- * @param {number} [params.before] - Timestamp cursor for pagination (ms)
45
- * @param {string} [params.mode='asc'] - Return order ('asc'|'desc')
46
- * @returns {Promise<{messages: Array, pagination: Object}>}
47
- */
48
- async loadMessagesFromCLI({
49
- sessionId,
50
- threadId, // optional native thread id (e.g., Codex exec thread)
51
- sessionPath, // alias kept for compatibility
52
- engine = 'claude',
53
- workspacePath,
54
- limit = DEFAULT_LIMIT,
55
- before,
56
- mode = 'asc'
57
- }) {
58
- if (!sessionId) {
59
- throw new Error('sessionId is required');
60
- }
61
-
62
- const startedAt = Date.now();
63
- const normalizedEngine = this._normalizeEngine(engine);
64
- const nativeId = threadId || sessionPath || sessionId;
65
-
66
- let result;
67
- switch (normalizedEngine) {
68
- case 'claude':
69
- result = await this.loadClaudeMessages({ sessionId, workspacePath, limit, before, mode });
70
- break;
71
-
72
- case 'codex':
73
- result = await this.loadCodexMessages({ sessionId, nativeId, limit, before, mode });
74
- break;
75
-
76
- case 'gemini':
77
- result = await this.loadGeminiMessages({ sessionId, limit, before, mode });
78
- break;
79
-
80
- default:
81
- throw new Error(`Unsupported engine: ${engine}`);
82
- }
83
-
84
- console.log(`[CliLoader] ${normalizedEngine} messages loaded in ${Date.now() - startedAt}ms (session ${sessionId}, ${result.messages.length} msgs)`);
85
- return result;
86
- }
87
-
88
- /**
89
- * Normalize engine name variants
90
- */
91
- _normalizeEngine(engine) {
92
- if (!engine) return 'claude';
93
- const lower = engine.toLowerCase();
94
- if (lower.includes('claude')) return 'claude';
95
- if (lower.includes('codex') || lower.includes('openai')) return 'codex';
96
- if (lower.includes('gemini') || lower.includes('google')) return 'gemini';
97
- return lower;
98
- }
99
-
100
- /**
101
- * Convert workspace path to slug (for .claude/projects/ directory)
102
- * Same as Claude Code behavior: /path/to/dir → -path-to-dir
103
- * Also converts dots to dashes (e.g., com.termux → com-termux)
104
- */
105
- pathToSlug(workspacePath) {
106
- if (!workspacePath) return '-default';
107
- // Replace slashes AND dots with dashes (matches Claude Code behavior)
108
- return workspacePath.replace(/[\/\.]/g, '-');
109
- }
110
-
111
- // ============================================================
112
- // CLAUDE - Load from ~/.claude/projects/<slug>/<sessionId>.jsonl
113
- // ============================================================
114
-
115
- async loadClaudeMessages({ sessionId, workspacePath, limit, before, mode }) {
116
- if (!workspacePath) {
117
- console.warn('[CliLoader] No workspacePath for Claude, using cwd');
118
- workspacePath = process.cwd();
119
- }
120
-
121
- const slug = this.pathToSlug(workspacePath);
122
- const sessionFile = path.join(this.claudePath, 'projects', slug, `${sessionId}.jsonl`);
123
-
124
- if (!fs.existsSync(sessionFile)) {
125
- console.warn(`[CliLoader] Claude session file not found: ${sessionFile}`);
126
- return this._emptyResult();
127
- }
128
-
129
- const rawMessages = await this._parseJsonlFile(sessionFile);
130
-
131
- // Filter and normalize
132
- const messages = rawMessages
133
- .filter(entry => entry.type === 'user' || entry.type === 'assistant')
134
- .map(entry => this._normalizeClaudeEntry(entry));
135
-
136
- return this._paginateMessages(messages, limit, before, mode);
137
- }
138
-
139
- /**
140
- * Normalize Claude Code session entry to message shape
141
- */
142
- _normalizeClaudeEntry(entry) {
143
- // Extract content - handle both string and array of content blocks
144
- let content = '';
145
- const rawContent = entry.message?.content;
146
-
147
- if (typeof rawContent === 'string') {
148
- content = rawContent;
149
- } else if (Array.isArray(rawContent)) {
150
- // Claude Code uses array of content blocks: [{type: 'text', text: '...'}, ...]
151
- content = rawContent
152
- .filter(block => block.type === 'text' && block.text)
153
- .map(block => block.text)
154
- .join('\n');
155
- } else if (entry.display || entry.text) {
156
- // Fallback for older formats
157
- content = entry.display || entry.text || '';
158
- }
159
-
160
- const role = entry.message?.role || entry.type || 'assistant';
161
- const created_at = new Date(entry.timestamp).getTime() || Date.now();
162
-
163
- return {
164
- id: entry.message?.id || `claude-${created_at}`,
165
- role,
166
- content,
167
- engine: 'claude',
168
- created_at,
169
- metadata: {
170
- model: entry.message?.model,
171
- stop_reason: entry.message?.stop_reason
172
- }
173
- };
174
- }
175
-
176
- // ============================================================
177
- // CODEX - Load from ~/.codex/sessions/<sessionId>.jsonl
178
- // ============================================================
179
-
180
- async loadCodexMessages({ sessionId, nativeId, limit, before, mode }) {
181
- const baseDir = path.join(this.codexPath, 'sessions');
182
- let sessionFile = path.join(baseDir, `${nativeId || sessionId}.jsonl`);
183
-
184
- // If flat file missing, search nested rollout-* files by threadId
185
- if (!fs.existsSync(sessionFile)) {
186
- sessionFile = this.findCodexSessionFile(baseDir, nativeId || sessionId);
187
- }
188
-
189
- // Codex exec may not persist sessions; handle gracefully
190
- if (!sessionFile || !fs.existsSync(sessionFile)) {
191
- console.log(`[CliLoader] Codex session file not found (id=${nativeId || sessionId})`);
192
- return this._emptyResult();
193
- }
194
-
195
- const rawMessages = await this._parseJsonlFile(sessionFile);
196
-
197
- // Normalize then filter only chat messages
198
- const messages = rawMessages
199
- .map(entry => this._normalizeCodexEntry(entry))
200
- .filter(msg => msg && (msg.role === 'user' || msg.role === 'assistant'));
201
-
202
- return this._paginateMessages(messages, limit, before, mode);
203
- }
204
-
205
- /**
206
- * Normalize Codex session entry to message shape
207
- */
208
- _normalizeCodexEntry(entry) {
209
- // Skip non-chat bookkeeping events
210
- const skipTypes = ['session_meta', 'turn_context', 'event_msg', 'token_count'];
211
- if (skipTypes.includes(entry.type)) return null;
212
-
213
- const role =
214
- entry.role ||
215
- entry.payload?.role ||
216
- (entry.payload?.type === 'message' ? entry.payload.role : null) ||
217
- entry.message?.role ||
218
- 'assistant';
219
-
220
- const created_at = entry.timestamp
221
- ? new Date(entry.timestamp).getTime()
222
- : (entry.payload?.timestamp ? new Date(entry.payload.timestamp).getTime() : Date.now());
223
-
224
- // Codex may store content in multiple shapes
225
- let content = '';
226
- if (typeof entry.content === 'string') {
227
- content = entry.content;
228
- } else if (typeof entry.payload?.content === 'string') {
229
- content = entry.payload.content;
230
- } else if (Array.isArray(entry.payload?.content)) {
231
- content = entry.payload.content
232
- .map(block => block.text || block.message || block.title || '')
233
- .filter(Boolean)
234
- .join('\n');
235
- } else if (entry.payload?.text) {
236
- content = entry.payload.text;
237
- } else if (entry.message) {
238
- content = typeof entry.message === 'string' ? entry.message : JSON.stringify(entry.message);
239
- }
240
-
241
- return {
242
- id: entry.id || `codex-${created_at}`,
243
- role,
244
- content,
245
- engine: 'codex',
246
- created_at,
247
- metadata: {
248
- model: entry.model,
249
- reasoning_effort: entry.reasoning_effort
250
- }
251
- };
252
- }
253
-
254
- /**
255
- * Find Codex rollout file by threadId within YYYY/MM/DD directories
256
- */
257
- findCodexSessionFile(baseDir, threadId) {
258
- if (!threadId || !fs.existsSync(baseDir)) return null;
259
- try {
260
- const years = fs.readdirSync(baseDir);
261
- for (const year of years) {
262
- const yearPath = path.join(baseDir, year);
263
- if (!fs.statSync(yearPath).isDirectory()) continue;
264
- const months = fs.readdirSync(yearPath);
265
- for (const month of months) {
266
- const monthPath = path.join(yearPath, month);
267
- if (!fs.statSync(monthPath).isDirectory()) continue;
268
- const days = fs.readdirSync(monthPath);
269
- for (const day of days) {
270
- const dayPath = path.join(monthPath, day);
271
- if (!fs.statSync(dayPath).isDirectory()) continue;
272
- const files = fs.readdirSync(dayPath);
273
- for (const file of files) {
274
- if (file.endsWith('.jsonl') && file.includes(threadId)) {
275
- return path.join(dayPath, file);
276
- }
277
- }
278
- }
279
- }
280
- }
281
- } catch (err) {
282
- console.warn(`[CliLoader] Failed to search Codex session file: ${err.message}`);
283
- }
284
- return null;
285
- }
286
-
287
- // ============================================================
288
- // GEMINI - Load from ~/.gemini/sessions/<sessionId>.jsonl
289
- // ============================================================
290
-
291
- async loadGeminiMessages({ sessionId, limit, before, mode }) {
292
- const sessionFile = path.join(this.geminiPath, 'sessions', `${sessionId}.jsonl`);
293
-
294
- // Gemini CLI may not save sessions - check if file exists
295
- if (!fs.existsSync(sessionFile)) {
296
- console.log(`[CliLoader] Gemini session file not found: ${sessionFile}`);
297
- return this._emptyResult();
298
- }
299
-
300
- const rawMessages = await this._parseJsonlFile(sessionFile);
301
-
302
- // Filter and normalize
303
- const messages = rawMessages
304
- .filter(entry => entry.role === 'user' || entry.role === 'model' || entry.role === 'assistant')
305
- .map(entry => this._normalizeGeminiEntry(entry));
306
-
307
- return this._paginateMessages(messages, limit, before, mode);
308
- }
309
-
310
- /**
311
- * Normalize Gemini session entry to message shape
312
- */
313
- _normalizeGeminiEntry(entry) {
314
- // Gemini uses 'model' instead of 'assistant'
315
- const role = entry.role === 'model' ? 'assistant' : (entry.role || 'assistant');
316
- const created_at = entry.timestamp ? new Date(entry.timestamp).getTime() : Date.now();
317
-
318
- // Gemini content format
319
- let content = '';
320
- if (typeof entry.content === 'string') {
321
- content = entry.content;
322
- } else if (Array.isArray(entry.parts)) {
323
- // Gemini uses parts array: [{text: '...'}]
324
- content = entry.parts
325
- .filter(p => p.text)
326
- .map(p => p.text)
327
- .join('\n');
328
- } else if (entry.text) {
329
- content = entry.text;
330
- }
331
-
332
- return {
333
- id: entry.id || `gemini-${created_at}`,
334
- role,
335
- content,
336
- engine: 'gemini',
337
- created_at,
338
- metadata: {
339
- model: entry.model
340
- }
341
- };
342
- }
343
-
344
- // ============================================================
345
- // UTILITY METHODS
346
- // ============================================================
347
-
348
- /**
349
- * Parse JSONL file line by line (memory efficient)
350
- */
351
- async _parseJsonlFile(filePath) {
352
- const entries = [];
353
-
354
- const fileStream = fs.createReadStream(filePath);
355
- const rl = readline.createInterface({
356
- input: fileStream,
357
- crlfDelay: Infinity
358
- });
359
-
360
- for await (const line of rl) {
361
- if (!line.trim()) continue;
362
-
363
- try {
364
- const entry = JSON.parse(line);
365
- entries.push(entry);
366
- } catch (e) {
367
- // Skip malformed lines
368
- console.warn(`[CliLoader] Skipping malformed JSON line in ${filePath}`);
369
- }
370
- }
371
-
372
- return entries;
373
- }
374
-
375
- /**
376
- * Apply pagination to messages array
377
- */
378
- _paginateMessages(messages, limit, before, mode) {
379
- // Filter by timestamp if 'before' cursor provided
380
- let filtered = messages;
381
- if (before) {
382
- filtered = messages.filter(m => m.created_at < Number(before));
383
- }
384
-
385
- // Sort newest first for pagination slicing
386
- filtered.sort((a, b) => b.created_at - a.created_at);
387
-
388
- // Apply limit
389
- const page = filtered.slice(0, limit);
390
- const hasMore = filtered.length > limit;
391
- const oldestTimestamp = page.length ? page[page.length - 1].created_at : null;
392
-
393
- // Return in requested order (default asc for UI rendering)
394
- const ordered = mode === 'desc'
395
- ? page
396
- : [...page].sort((a, b) => a.created_at - b.created_at);
397
-
398
- return {
399
- messages: ordered,
400
- pagination: {
401
- hasMore,
402
- oldestTimestamp,
403
- total: messages.length
404
- }
405
- };
406
- }
407
-
408
- /**
409
- * Return empty result structure
410
- */
411
- _emptyResult() {
412
- return {
413
- messages: [],
414
- pagination: {
415
- hasMore: false,
416
- oldestTimestamp: null,
417
- total: 0
418
- }
419
- };
420
- }
421
-
422
- /**
423
- * Get session file path for an engine
424
- * Useful for external checks
425
- */
426
- getSessionFilePath(sessionId, engine, workspacePath) {
427
- const normalizedEngine = this._normalizeEngine(engine);
428
-
429
- switch (normalizedEngine) {
430
- case 'claude':
431
- const slug = this.pathToSlug(workspacePath);
432
- return path.join(this.claudePath, 'projects', slug, `${sessionId}.jsonl`);
433
-
434
- case 'codex':
435
- return path.join(this.codexPath, 'sessions', `${sessionId}.jsonl`);
436
-
437
- case 'gemini':
438
- return path.join(this.geminiPath, 'sessions', `${sessionId}.jsonl`);
439
-
440
- default:
441
- return null;
442
- }
443
- }
444
- }
445
-
446
- module.exports = CliLoader;