@mmmbuto/nexuscrew 0.1.0-beta.1 → 0.2.0

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 (35) hide show
  1. package/README.md +54 -120
  2. package/bin/nexuscrew.js +9 -8
  3. package/frontend/dist/assets/{index-C7bAndew.js → index-8pw4eMB-.js} +1715 -1702
  4. package/frontend/dist/assets/index-BF0tdvNT.css +1 -0
  5. package/frontend/dist/index.html +2 -2
  6. package/lib/config/manager.js +362 -0
  7. package/lib/config/models.js +408 -0
  8. package/lib/server/db/adapter.js +274 -0
  9. package/lib/server/db/drivers/sql-js.js +75 -0
  10. package/lib/server/db/migrate.js +174 -0
  11. package/lib/server/db/migrations/001_base_schema.sql +70 -0
  12. package/lib/server/middleware/auth.js +134 -0
  13. package/lib/server/middleware/rate-limit.js +63 -0
  14. package/lib/server/models/User.js +128 -0
  15. package/lib/server/routes/auth.js +168 -0
  16. package/lib/server/routes/keys.js +15 -0
  17. package/lib/server/routes/models.js +59 -4
  18. package/lib/server/routes/runtimes.js +34 -0
  19. package/lib/server/routes/send.js +28 -4
  20. package/lib/server/routes/sessions.js +34 -2
  21. package/lib/server/routes/speech.js +75 -0
  22. package/lib/server/routes/upload.js +134 -0
  23. package/lib/server/routes/wake-lock.js +95 -0
  24. package/lib/server/routes/workspaces.js +101 -0
  25. package/lib/server/server.js +48 -16
  26. package/lib/server/services/context-bridge.js +425 -0
  27. package/lib/server/services/runtime-manager.js +462 -0
  28. package/lib/server/services/summary-generator.js +309 -0
  29. package/lib/server/services/workspace-manager.js +79 -0
  30. package/lib/services/engine-discovery.js +198 -13
  31. package/lib/services/session-store.js +57 -61
  32. package/lib/utils/paths.js +107 -0
  33. package/lib/utils/termux.js +145 -0
  34. package/package.json +6 -2
  35. package/frontend/dist/assets/index-OENqI1_9.css +0 -1
@@ -0,0 +1,425 @@
1
+ /**
2
+ * ContextBridge - Unified context management with token optimization
3
+ *
4
+ * Features:
5
+ * - Token-aware context truncation
6
+ * - Engine-specific context compression
7
+ * - Auto-summary triggering
8
+ * - Unified bridging logic for all engines
9
+ */
10
+
11
+ const { prepare } = require('../db/adapter');
12
+ const SummaryGenerator = require('./summary-generator');
13
+
14
+ // Token estimation constants (approximate)
15
+ const CHARS_PER_TOKEN = 4; // GPT/Claude average
16
+ const DEFAULT_MAX_TOKENS = 4000; // Context budget for bridging
17
+ const SUMMARY_TRIGGER_THRESHOLD = 15; // Messages before auto-summary
18
+
19
+ // Engine-specific context limits
20
+ const ENGINE_LIMITS = {
21
+ 'claude': { maxTokens: 4000, preferSummary: true },
22
+ 'codex': { maxTokens: 3000, preferSummary: true, codeOnly: true },
23
+ 'deepseek': { maxTokens: 3000, preferSummary: true },
24
+ 'gemini': { maxTokens: 6000, preferSummary: false }, // Gemini has large context
25
+ 'qwen': { maxTokens: 6000, preferSummary: false } // Qwen Coder large context
26
+ };
27
+
28
+ class ContextBridge {
29
+ constructor() {
30
+ this.summaryGenerator = new SummaryGenerator();
31
+ }
32
+
33
+ /**
34
+ * Estimate token count for text
35
+ * @param {string} text - Text to estimate
36
+ * @returns {number} Estimated token count
37
+ */
38
+ estimateTokens(text) {
39
+ if (!text) return 0;
40
+ // Simple estimation: ~4 chars per token for English/code
41
+ // More accurate would use tiktoken, but this is faster
42
+ return Math.ceil(text.length / CHARS_PER_TOKEN);
43
+ }
44
+
45
+ /**
46
+ * Get engine-specific configuration
47
+ * @param {string} engine - Engine name
48
+ * @returns {Object} Engine config
49
+ */
50
+ getEngineConfig(engine) {
51
+ return ENGINE_LIMITS[engine] || ENGINE_LIMITS['claude'];
52
+ }
53
+
54
+ /**
55
+ * Build optimized context for engine switch
56
+ * @param {Object} params
57
+ * @param {string} params.conversationId - Stable conversation ID (cross-engine)
58
+ * @param {string} params.sessionId - Legacy session ID (fallback)
59
+ * @param {string} params.fromEngine - Previous engine
60
+ * @param {string} params.toEngine - Target engine
61
+ * @param {string} params.userMessage - Current user message
62
+ * @returns {Object} { prompt, isEngineBridge, contextTokens }
63
+ */
64
+ async buildContext({ conversationId, sessionId, fromEngine, toEngine, userMessage }) {
65
+ const config = this.getEngineConfig(toEngine);
66
+ const isEngineBridge = fromEngine && fromEngine !== toEngine;
67
+ const convoId = conversationId || sessionId; // backward compat
68
+
69
+ // Reserve tokens for user message
70
+ const userTokens = this.estimateTokens(userMessage);
71
+ const availableTokens = Math.max(0, config.maxTokens - userTokens - 200); // 200 token buffer, never negative
72
+
73
+ let contextText = '';
74
+ let contextTokens = 0;
75
+ let contextSource = 'none';
76
+
77
+ // For engine bridge, use structured handoff template
78
+ if (isEngineBridge) {
79
+ const handoffContext = this.buildEngineHandoffContext(convoId, fromEngine, toEngine, availableTokens, config);
80
+ if (handoffContext.text) {
81
+ contextText = handoffContext.text;
82
+ contextTokens = handoffContext.tokens;
83
+ contextSource = handoffContext.source;
84
+ }
85
+ // If handoff couldn't fit, fall back to history-only context
86
+ if (!contextText && availableTokens > 200) {
87
+ const historyContext = this.buildTokenAwareHistory(convoId, availableTokens, config);
88
+ if (historyContext.text) {
89
+ contextText = historyContext.text;
90
+ contextTokens = historyContext.tokens;
91
+ contextSource = 'history_fallback';
92
+ }
93
+ }
94
+ } else {
95
+ // Try summary first (most efficient)
96
+ if (config.preferSummary) {
97
+ const summaryContext = this.summaryGenerator.getBridgeContext(convoId);
98
+ if (summaryContext) {
99
+ const summaryTokens = this.estimateTokens(summaryContext);
100
+ if (summaryTokens <= availableTokens) {
101
+ contextText = summaryContext;
102
+ contextTokens = summaryTokens;
103
+ contextSource = 'summary';
104
+ }
105
+ }
106
+ }
107
+
108
+ // Fallback to token-aware message history
109
+ if (!contextText && availableTokens > 200) {
110
+ const historyContext = this.buildTokenAwareHistory(convoId, availableTokens, config);
111
+ if (historyContext.text) {
112
+ contextText = historyContext.text;
113
+ contextTokens = historyContext.tokens;
114
+ contextSource = 'history';
115
+ }
116
+ }
117
+ }
118
+
119
+ // Build final prompt
120
+ let prompt = userMessage;
121
+
122
+ if (availableTokens === 0) {
123
+ console.log(`[ContextBridge] Budget exhausted before context; sending raw message (engine=${toEngine})`);
124
+ }
125
+
126
+ if (contextText) {
127
+ prompt = `${contextText}\n\n${userMessage}`;
128
+ }
129
+
130
+ console.log(`[ContextBridge] Built context: ${contextTokens} tokens from ${contextSource}, bridge: ${isEngineBridge}, avail=${availableTokens}, total=${contextTokens + userTokens}`);
131
+
132
+ return {
133
+ prompt,
134
+ isEngineBridge,
135
+ contextTokens,
136
+ contextSource,
137
+ totalTokens: contextTokens + userTokens
138
+ };
139
+ }
140
+
141
+ /**
142
+ * Build structured context for engine handoff
143
+ * Uses a clear template that helps the new engine understand previous context
144
+ * @param {string} conversationId
145
+ * @param {string} fromEngine
146
+ * @param {string} toEngine
147
+ * @param {number} maxTokens
148
+ * @param {Object} config
149
+ * @returns {Object} { text, tokens, source }
150
+ */
151
+ buildEngineHandoffContext(conversationId, fromEngine, toEngine, maxTokens, config = {}) {
152
+ const engineNames = {
153
+ 'claude': 'Claude Code (Anthropic)',
154
+ 'codex': 'Codex (OpenAI)',
155
+ 'gemini': 'Gemini (Google)',
156
+ 'deepseek': 'DeepSeek',
157
+ 'qwen': 'Qwen Code (Alibaba)'
158
+ };
159
+
160
+ const fromName = engineNames[fromEngine] || fromEngine;
161
+ const toName = engineNames[toEngine] || toEngine;
162
+
163
+ // Get summary if available
164
+ const summary = this.summaryGenerator.getSummary(conversationId);
165
+
166
+ // Get recent messages (last 5) and message count
167
+ const stmt = prepare('SELECT role, content, engine FROM messages WHERE conversation_id = ? ORDER BY created_at DESC LIMIT 5');
168
+ const messages = stmt.all(conversationId);
169
+
170
+ const countStmt = prepare('SELECT COUNT(*) as count FROM messages WHERE conversation_id = ?');
171
+ const { count: messageCount } = countStmt.get(conversationId);
172
+
173
+ // Build structured template
174
+ const sections = [];
175
+
176
+ // Header
177
+ sections.push(`<previous_session_context engine="${fromEngine}" total_messages="${messageCount}">`);
178
+ sections.push(`This conversation was previously handled by ${fromName}.`);
179
+ sections.push(`You are now continuing as ${toName}.`);
180
+ sections.push('');
181
+
182
+ // Summary section (if available)
183
+ if (summary && summary.summary_short) {
184
+ sections.push('## Summary');
185
+ sections.push(summary.summary_short);
186
+ sections.push('');
187
+
188
+ // Key decisions
189
+ if (summary.key_decisions && summary.key_decisions.length > 0) {
190
+ sections.push('## Key Decisions');
191
+ summary.key_decisions.slice(0, 5).forEach(d => sections.push(`- ${d}`));
192
+ sections.push('');
193
+ }
194
+
195
+ // Files modified
196
+ if (summary.files_modified && summary.files_modified.length > 0) {
197
+ sections.push('## Files Modified');
198
+ summary.files_modified.slice(0, 10).forEach(f => sections.push(`- ${f}`));
199
+ sections.push('');
200
+ }
201
+ }
202
+
203
+ // Recent messages (always include for continuity)
204
+ if (messages.length > 0) {
205
+ sections.push('## Recent Messages');
206
+ for (const msg of messages) {
207
+ const role = msg.role === 'user' ? 'User' : 'Assistant';
208
+ const engine = msg.engine ? ` [${msg.engine}]` : '';
209
+ // Truncate long messages
210
+ let content = msg.content || '';
211
+ if (content.length > 500) {
212
+ content = content.substring(0, 500) + '...';
213
+ }
214
+ sections.push(`${role}${engine}: ${content}`);
215
+ sections.push('');
216
+ }
217
+ }
218
+
219
+ sections.push('</previous_session_context>');
220
+ sections.push('');
221
+ sections.push('Continue assisting with the following request:');
222
+
223
+ const text = sections.join('\n');
224
+ const tokens = this.estimateTokens(text);
225
+
226
+ // Check token budget
227
+ if (tokens > maxTokens) {
228
+ // Fallback to simpler context if too long
229
+ console.log(`[ContextBridge] Handoff template too long (${tokens} > ${maxTokens}), using fallback`);
230
+ const fallback = this.buildTokenAwareHistory(conversationId, maxTokens, config);
231
+ return {
232
+ text: fallback.text,
233
+ tokens: fallback.tokens,
234
+ source: 'handoff_fallback_history'
235
+ };
236
+ }
237
+
238
+ return {
239
+ text,
240
+ tokens,
241
+ source: summary ? 'handoff+summary' : 'handoff+history'
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Build token-aware history context
247
+ * @param {string} conversationId - Conversation ID
248
+ * @param {number} maxTokens - Token budget
249
+ * @param {Object} config - Engine config
250
+ * @returns {Object} { text, tokens, messageCount }
251
+ */
252
+ buildTokenAwareHistory(conversationId, maxTokens, config = {}) {
253
+ // Get more messages than we need, we'll trim
254
+ const stmt = prepare('SELECT role, content, engine FROM messages WHERE conversation_id = ? ORDER BY created_at DESC LIMIT 20');
255
+ const messages = stmt.all(conversationId);
256
+
257
+ if (messages.length === 0) {
258
+ return { text: '', tokens: 0, messageCount: 0 };
259
+ }
260
+
261
+ const lines = [];
262
+ let tokenCount = 0;
263
+ let includedCount = 0;
264
+
265
+ // Process from newest to oldest
266
+ for (let i = 0; i < messages.length; i++) {
267
+ const msg = messages[i];
268
+
269
+ // For code-focused engines, compress assistant responses to code only
270
+ // BUT always keep user messages for context continuity
271
+ let content = msg.content;
272
+ if (config.codeOnly && msg.role === 'assistant') {
273
+ const codeContent = this.extractCodeContent(content);
274
+ // Only use code-only if there's actual code, otherwise keep truncated original
275
+ content = codeContent || (content.length > 500 ? content.substring(0, 500) + '...' : content);
276
+ }
277
+
278
+ // Truncate long messages
279
+ if (content.length > 2000) {
280
+ content = content.substring(0, 2000) + '...';
281
+ }
282
+
283
+ const role = msg.role === 'user' ? 'User' : 'Assistant';
284
+ const engineTag = msg.engine ? ` [${msg.engine}]` : '';
285
+ const line = `${role}${engineTag}: ${content}`;
286
+
287
+ const lineTokens = this.estimateTokens(line);
288
+
289
+ // Check if adding this would exceed budget
290
+ if (tokenCount + lineTokens > maxTokens) {
291
+ break;
292
+ }
293
+
294
+ lines.unshift(line); // Add to beginning (chronological)
295
+ tokenCount += lineTokens;
296
+ includedCount++;
297
+ }
298
+
299
+ if (lines.length === 0) {
300
+ return { text: '', tokens: 0, messageCount: 0 };
301
+ }
302
+
303
+ const text = `[Context from recent messages]\n${lines.join('\n\n')}`;
304
+
305
+ return {
306
+ text,
307
+ tokens: tokenCount,
308
+ messageCount: includedCount
309
+ };
310
+ }
311
+
312
+ /**
313
+ * Extract code blocks and technical content from text
314
+ * @param {string} text - Full text
315
+ * @returns {string} Code-focused content
316
+ */
317
+ extractCodeContent(text) {
318
+ if (!text) return '';
319
+
320
+ // Extract code blocks
321
+ const codeBlocks = [];
322
+ const codeBlockRegex = /```[\s\S]*?```/g;
323
+ let match;
324
+
325
+ while ((match = codeBlockRegex.exec(text)) !== null) {
326
+ codeBlocks.push(match[0]);
327
+ }
328
+
329
+ if (codeBlocks.length > 0) {
330
+ return codeBlocks.join('\n\n');
331
+ }
332
+
333
+ // If no code blocks, check if text contains technical content
334
+ const technicalPatterns = [
335
+ /function\s+\w+/,
336
+ /const\s+\w+\s*=/,
337
+ /class\s+\w+/,
338
+ /import\s+.*from/,
339
+ /require\s*\(/,
340
+ /\/\/|\/\*|\*\//,
341
+ /\.(js|ts|py|java|cpp|go|rs)\b/,
342
+ /npm|git|docker|curl/
343
+ ];
344
+
345
+ for (const pattern of technicalPatterns) {
346
+ if (pattern.test(text)) {
347
+ return text;
348
+ }
349
+ }
350
+
351
+ return ''; // Not code-relevant
352
+ }
353
+
354
+ /**
355
+ * Check if auto-summary should be triggered
356
+ * @param {string} conversationId - Conversation ID
357
+ * @param {boolean} isEngineBridge - Was this an engine switch
358
+ * @returns {boolean} Should generate summary
359
+ */
360
+ shouldTriggerSummary(conversationId, isEngineBridge = false) {
361
+ // Always trigger on engine bridge
362
+ if (isEngineBridge) return true;
363
+
364
+ const countStmt = prepare('SELECT COUNT(*) as count FROM messages WHERE conversation_id = ?');
365
+ const { count: messageCount } = countStmt.get(conversationId);
366
+
367
+ // Trigger every 10 messages after threshold
368
+ if (messageCount >= SUMMARY_TRIGGER_THRESHOLD && messageCount % 10 === 0) {
369
+ return true;
370
+ }
371
+
372
+ // Check if we have a stale summary (older than 20 messages)
373
+ const existingSummary = this.summaryGenerator.getSummary(conversationId);
374
+ if (!existingSummary && messageCount > SUMMARY_TRIGGER_THRESHOLD) {
375
+ return true;
376
+ }
377
+
378
+ return false;
379
+ }
380
+
381
+ /**
382
+ * Trigger summary generation (async, non-blocking)
383
+ * @param {string} conversationId - Conversation ID
384
+ * @param {string} logPrefix - Log prefix for debugging
385
+ */
386
+ triggerSummaryGeneration(conversationId, logPrefix = '[ContextBridge]') {
387
+ const stmt = prepare('SELECT role, content, created_at FROM messages WHERE conversation_id = ? ORDER BY created_at DESC LIMIT 40');
388
+ const messages = stmt.all(conversationId);
389
+
390
+ this.summaryGenerator.generateAndSave(conversationId, messages)
391
+ .then(summary => {
392
+ if (summary) {
393
+ console.log(`${logPrefix} Summary updated: ${summary.summary_short?.substring(0, 50)}...`);
394
+ }
395
+ })
396
+ .catch(err => {
397
+ console.warn(`${logPrefix} Summary generation failed:`, err.message);
398
+ });
399
+ }
400
+
401
+ /**
402
+ * Get context stats for debugging
403
+ * @param {string} conversationId - Conversation ID
404
+ * @returns {Object} Stats
405
+ */
406
+ getContextStats(conversationId) {
407
+ const countStmt = prepare('SELECT COUNT(*) as count FROM messages WHERE conversation_id = ?');
408
+ const { count: messageCount } = countStmt.get(conversationId);
409
+
410
+ const lastMsgStmt = prepare('SELECT engine FROM messages WHERE conversation_id = ? ORDER BY created_at DESC LIMIT 1');
411
+ const lastMsg = lastMsgStmt.get(conversationId);
412
+ const lastEngine = lastMsg?.engine || null;
413
+
414
+ const hasSummary = !!this.summaryGenerator.getSummary(conversationId);
415
+
416
+ return {
417
+ messageCount,
418
+ lastEngine,
419
+ hasSummary,
420
+ summaryThreshold: SUMMARY_TRIGGER_THRESHOLD
421
+ };
422
+ }
423
+ }
424
+
425
+ module.exports = new ContextBridge();