@mmmbuto/nexuscli 0.5.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.
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/bin/nexuscli.js +117 -0
- package/frontend/dist/apple-touch-icon.png +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/frontend/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/frontend/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/frontend/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/frontend/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/frontend/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/frontend/dist/assets/index-Bn_l1e6e.css +1 -0
- package/frontend/dist/assets/index-CikJbUR5.js +8617 -0
- package/frontend/dist/browserconfig.xml +12 -0
- package/frontend/dist/favicon-16x16.png +0 -0
- package/frontend/dist/favicon-32x32.png +0 -0
- package/frontend/dist/favicon-48x48.png +0 -0
- package/frontend/dist/favicon.ico +0 -0
- package/frontend/dist/icon-192.png +0 -0
- package/frontend/dist/icon-512.png +0 -0
- package/frontend/dist/icon-maskable-192.png +0 -0
- package/frontend/dist/icon-maskable-512.png +0 -0
- package/frontend/dist/index.html +79 -0
- package/frontend/dist/manifest.json +75 -0
- package/frontend/dist/sw.js +122 -0
- package/frontend/package.json +28 -0
- package/lib/cli/api.js +156 -0
- package/lib/cli/boot.js +172 -0
- package/lib/cli/config.js +185 -0
- package/lib/cli/engines.js +257 -0
- package/lib/cli/init.js +660 -0
- package/lib/cli/logs.js +72 -0
- package/lib/cli/start.js +220 -0
- package/lib/cli/status.js +187 -0
- package/lib/cli/stop.js +64 -0
- package/lib/cli/uninstall.js +194 -0
- package/lib/cli/users.js +295 -0
- package/lib/cli/workspaces.js +337 -0
- package/lib/config/manager.js +233 -0
- package/lib/server/.env.example +20 -0
- package/lib/server/db/adapter.js +314 -0
- package/lib/server/db/drivers/better-sqlite3.js +38 -0
- package/lib/server/db/drivers/sql-js.js +75 -0
- package/lib/server/db/migrate.js +174 -0
- package/lib/server/db/migrations/001_ultra_light_schema.sql +96 -0
- package/lib/server/db/migrations/002_session_conversation_mapping.sql +19 -0
- package/lib/server/db/migrations/003_message_engine_tracking.sql +18 -0
- package/lib/server/db/migrations/004_performance_indexes.sql +16 -0
- package/lib/server/db.js +2 -0
- package/lib/server/lib/cli-wrapper.js +164 -0
- package/lib/server/lib/output-parser.js +132 -0
- package/lib/server/lib/pty-adapter.js +57 -0
- package/lib/server/middleware/auth.js +103 -0
- package/lib/server/models/Conversation.js +259 -0
- package/lib/server/models/Message.js +228 -0
- package/lib/server/models/User.js +115 -0
- package/lib/server/package-lock.json +5895 -0
- package/lib/server/routes/auth.js +168 -0
- package/lib/server/routes/chat.js +206 -0
- package/lib/server/routes/codex.js +205 -0
- package/lib/server/routes/conversations.js +224 -0
- package/lib/server/routes/gemini.js +228 -0
- package/lib/server/routes/jobs.js +317 -0
- package/lib/server/routes/messages.js +60 -0
- package/lib/server/routes/models.js +198 -0
- package/lib/server/routes/sessions.js +285 -0
- package/lib/server/routes/upload.js +134 -0
- package/lib/server/routes/wake-lock.js +95 -0
- package/lib/server/routes/workspace.js +80 -0
- package/lib/server/routes/workspaces.js +142 -0
- package/lib/server/scripts/cleanup-ghost-sessions.js +71 -0
- package/lib/server/scripts/seed-users.js +37 -0
- package/lib/server/scripts/test-history-access.js +50 -0
- package/lib/server/server.js +227 -0
- package/lib/server/services/cache.js +85 -0
- package/lib/server/services/claude-wrapper.js +312 -0
- package/lib/server/services/cli-loader.js +384 -0
- package/lib/server/services/codex-output-parser.js +277 -0
- package/lib/server/services/codex-wrapper.js +224 -0
- package/lib/server/services/context-bridge.js +289 -0
- package/lib/server/services/gemini-output-parser.js +398 -0
- package/lib/server/services/gemini-wrapper.js +249 -0
- package/lib/server/services/history-sync.js +407 -0
- package/lib/server/services/output-parser.js +415 -0
- package/lib/server/services/session-manager.js +465 -0
- package/lib/server/services/summary-generator.js +259 -0
- package/lib/server/services/workspace-manager.js +516 -0
- package/lib/server/tests/history-sync.test.js +90 -0
- package/lib/server/tests/integration-session-sync.test.js +151 -0
- package/lib/server/tests/integration.test.js +76 -0
- package/lib/server/tests/performance.test.js +118 -0
- package/lib/server/tests/services.test.js +160 -0
- package/lib/setup/postinstall.js +216 -0
- package/lib/utils/paths.js +107 -0
- package/lib/utils/termux.js +145 -0
- package/package.json +82 -0
|
@@ -0,0 +1,289 @@
|
|
|
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 Message = require('../models/Message');
|
|
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
|
+
};
|
|
26
|
+
|
|
27
|
+
class ContextBridge {
|
|
28
|
+
constructor() {
|
|
29
|
+
this.summaryGenerator = new SummaryGenerator();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Estimate token count for text
|
|
34
|
+
* @param {string} text - Text to estimate
|
|
35
|
+
* @returns {number} Estimated token count
|
|
36
|
+
*/
|
|
37
|
+
estimateTokens(text) {
|
|
38
|
+
if (!text) return 0;
|
|
39
|
+
// Simple estimation: ~4 chars per token for English/code
|
|
40
|
+
// More accurate would use tiktoken, but this is faster
|
|
41
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get engine-specific configuration
|
|
46
|
+
* @param {string} engine - Engine name
|
|
47
|
+
* @returns {Object} Engine config
|
|
48
|
+
*/
|
|
49
|
+
getEngineConfig(engine) {
|
|
50
|
+
return ENGINE_LIMITS[engine] || ENGINE_LIMITS['claude'];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build optimized context for engine switch
|
|
55
|
+
* @param {Object} params
|
|
56
|
+
* @param {string} params.sessionId - Session ID
|
|
57
|
+
* @param {string} params.fromEngine - Previous engine
|
|
58
|
+
* @param {string} params.toEngine - Target engine
|
|
59
|
+
* @param {string} params.userMessage - Current user message
|
|
60
|
+
* @returns {Object} { prompt, isEngineBridge, contextTokens }
|
|
61
|
+
*/
|
|
62
|
+
async buildContext({ sessionId, fromEngine, toEngine, userMessage }) {
|
|
63
|
+
const config = this.getEngineConfig(toEngine);
|
|
64
|
+
const isEngineBridge = fromEngine && fromEngine !== toEngine;
|
|
65
|
+
|
|
66
|
+
// Reserve tokens for user message
|
|
67
|
+
const userTokens = this.estimateTokens(userMessage);
|
|
68
|
+
const availableTokens = config.maxTokens - userTokens - 200; // 200 token buffer
|
|
69
|
+
|
|
70
|
+
let contextText = '';
|
|
71
|
+
let contextTokens = 0;
|
|
72
|
+
let contextSource = 'none';
|
|
73
|
+
|
|
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';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
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';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Build final prompt
|
|
98
|
+
let prompt = userMessage;
|
|
99
|
+
|
|
100
|
+
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
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log(`[ContextBridge] Built context: ${contextTokens} tokens from ${contextSource}, bridge: ${isEngineBridge}`);
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
prompt,
|
|
112
|
+
isEngineBridge,
|
|
113
|
+
contextTokens,
|
|
114
|
+
contextSource,
|
|
115
|
+
totalTokens: contextTokens + userTokens
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build token-aware history context
|
|
121
|
+
* @param {string} sessionId - Session ID
|
|
122
|
+
* @param {number} maxTokens - Token budget
|
|
123
|
+
* @param {Object} config - Engine config
|
|
124
|
+
* @returns {Object} { text, tokens, messageCount }
|
|
125
|
+
*/
|
|
126
|
+
buildTokenAwareHistory(sessionId, maxTokens, config = {}) {
|
|
127
|
+
// Get more messages than we need, we'll trim
|
|
128
|
+
const messages = Message.getContextMessages(sessionId, 20);
|
|
129
|
+
|
|
130
|
+
if (messages.length === 0) {
|
|
131
|
+
return { text: '', tokens: 0, messageCount: 0 };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const lines = [];
|
|
135
|
+
let tokenCount = 0;
|
|
136
|
+
let includedCount = 0;
|
|
137
|
+
|
|
138
|
+
// Process from newest to oldest (we got them in chronological order, reverse)
|
|
139
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
140
|
+
const msg = messages[i];
|
|
141
|
+
|
|
142
|
+
// For code-focused engines, filter out non-code content
|
|
143
|
+
let content = msg.content;
|
|
144
|
+
if (config.codeOnly) {
|
|
145
|
+
content = this.extractCodeContent(content);
|
|
146
|
+
if (!content) continue; // Skip if no code
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Truncate long messages
|
|
150
|
+
if (content.length > 2000) {
|
|
151
|
+
content = content.substring(0, 2000) + '...';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const role = msg.role === 'user' ? 'User' : 'Assistant';
|
|
155
|
+
const engineTag = msg.engine ? ` [${msg.engine}]` : '';
|
|
156
|
+
const line = `${role}${engineTag}: ${content}`;
|
|
157
|
+
|
|
158
|
+
const lineTokens = this.estimateTokens(line);
|
|
159
|
+
|
|
160
|
+
// Check if adding this would exceed budget
|
|
161
|
+
if (tokenCount + lineTokens > maxTokens) {
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
lines.unshift(line); // Add to beginning (chronological)
|
|
166
|
+
tokenCount += lineTokens;
|
|
167
|
+
includedCount++;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (lines.length === 0) {
|
|
171
|
+
return { text: '', tokens: 0, messageCount: 0 };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const text = `[Context from recent messages]\n${lines.join('\n\n')}`;
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
text,
|
|
178
|
+
tokens: tokenCount,
|
|
179
|
+
messageCount: includedCount
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Extract code blocks and technical content from text
|
|
185
|
+
* @param {string} text - Full text
|
|
186
|
+
* @returns {string} Code-focused content
|
|
187
|
+
*/
|
|
188
|
+
extractCodeContent(text) {
|
|
189
|
+
if (!text) return '';
|
|
190
|
+
|
|
191
|
+
// Extract code blocks
|
|
192
|
+
const codeBlocks = [];
|
|
193
|
+
const codeBlockRegex = /```[\s\S]*?```/g;
|
|
194
|
+
let match;
|
|
195
|
+
|
|
196
|
+
while ((match = codeBlockRegex.exec(text)) !== null) {
|
|
197
|
+
codeBlocks.push(match[0]);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (codeBlocks.length > 0) {
|
|
201
|
+
return codeBlocks.join('\n\n');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// If no code blocks, check if text contains technical content
|
|
205
|
+
const technicalPatterns = [
|
|
206
|
+
/function\s+\w+/,
|
|
207
|
+
/const\s+\w+\s*=/,
|
|
208
|
+
/class\s+\w+/,
|
|
209
|
+
/import\s+.*from/,
|
|
210
|
+
/require\s*\(/,
|
|
211
|
+
/\/\/|\/\*|\*\//,
|
|
212
|
+
/\.(js|ts|py|java|cpp|go|rs)\b/,
|
|
213
|
+
/npm|git|docker|curl/
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
for (const pattern of technicalPatterns) {
|
|
217
|
+
if (pattern.test(text)) {
|
|
218
|
+
return text;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return ''; // Not code-relevant
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Check if auto-summary should be triggered
|
|
227
|
+
* @param {string} sessionId - Session ID
|
|
228
|
+
* @param {boolean} isEngineBridge - Was this an engine switch
|
|
229
|
+
* @returns {boolean} Should generate summary
|
|
230
|
+
*/
|
|
231
|
+
shouldTriggerSummary(sessionId, isEngineBridge = false) {
|
|
232
|
+
// Always trigger on engine bridge
|
|
233
|
+
if (isEngineBridge) return true;
|
|
234
|
+
|
|
235
|
+
const messageCount = Message.countByConversation(sessionId);
|
|
236
|
+
|
|
237
|
+
// Trigger every 10 messages after threshold
|
|
238
|
+
if (messageCount >= SUMMARY_TRIGGER_THRESHOLD && messageCount % 10 === 0) {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Check if we have a stale summary (older than 20 messages)
|
|
243
|
+
const existingSummary = this.summaryGenerator.getSummary(sessionId);
|
|
244
|
+
if (!existingSummary && messageCount > SUMMARY_TRIGGER_THRESHOLD) {
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Trigger summary generation (async, non-blocking)
|
|
253
|
+
* @param {string} sessionId - Session ID
|
|
254
|
+
* @param {string} logPrefix - Log prefix for debugging
|
|
255
|
+
*/
|
|
256
|
+
triggerSummaryGeneration(sessionId, logPrefix = '[ContextBridge]') {
|
|
257
|
+
const messages = Message.getByConversation(sessionId, 40);
|
|
258
|
+
|
|
259
|
+
this.summaryGenerator.generateAndSave(sessionId, messages)
|
|
260
|
+
.then(summary => {
|
|
261
|
+
if (summary) {
|
|
262
|
+
console.log(`${logPrefix} Summary updated: ${summary.summary_short?.substring(0, 50)}...`);
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
.catch(err => {
|
|
266
|
+
console.warn(`${logPrefix} Summary generation failed:`, err.message);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Get context stats for debugging
|
|
272
|
+
* @param {string} sessionId - Session ID
|
|
273
|
+
* @returns {Object} Stats
|
|
274
|
+
*/
|
|
275
|
+
getContextStats(sessionId) {
|
|
276
|
+
const messageCount = Message.countByConversation(sessionId);
|
|
277
|
+
const lastEngine = Message.getLastEngine(sessionId);
|
|
278
|
+
const hasSummary = !!this.summaryGenerator.getSummary(sessionId);
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
messageCount,
|
|
282
|
+
lastEngine,
|
|
283
|
+
hasSummary,
|
|
284
|
+
summaryThreshold: SUMMARY_TRIGGER_THRESHOLD
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
module.exports = new ContextBridge();
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GeminiOutputParser - Parse Gemini CLI JSON stream output
|
|
3
|
+
*
|
|
4
|
+
* Parses `gemini -o stream-json` output format.
|
|
5
|
+
*
|
|
6
|
+
* JSON Event Types from Gemini CLI:
|
|
7
|
+
* - init: Session initialization { type: 'init', session_id, model }
|
|
8
|
+
* - message: Response content { type: 'message', role: 'assistant', content, delta: true }
|
|
9
|
+
* - tool_use: Tool invocation { type: 'tool_use', tool, input }
|
|
10
|
+
* - tool_result: Tool output { type: 'tool_result', tool, output, status }
|
|
11
|
+
* - result: Final stats { type: 'result', status, stats: { input_tokens, output_tokens } }
|
|
12
|
+
* - error: Error event { type: 'error', message }
|
|
13
|
+
*
|
|
14
|
+
* Emits normalized events for SSE streaming:
|
|
15
|
+
* - status: { type: 'status', category: 'tool'|'system', message, icon }
|
|
16
|
+
* - response_chunk: { type: 'response_chunk', text, isIncremental }
|
|
17
|
+
* - response_done: { type: 'response_done', fullText }
|
|
18
|
+
* - done: { type: 'done', usage, status }
|
|
19
|
+
* - error: { type: 'error', message }
|
|
20
|
+
*
|
|
21
|
+
* @version 0.4.0 - TRI CLI Support
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
class GeminiOutputParser {
|
|
25
|
+
constructor() {
|
|
26
|
+
this.buffer = '';
|
|
27
|
+
this.finalResponse = '';
|
|
28
|
+
this.usage = null;
|
|
29
|
+
this.sessionId = null;
|
|
30
|
+
this.model = null;
|
|
31
|
+
this.pendingTools = new Map();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse a chunk of stdout (may contain multiple JSON lines)
|
|
36
|
+
* @param {string} chunk - Raw stdout chunk from node-pty
|
|
37
|
+
* @returns {Array} Array of normalized event objects for SSE
|
|
38
|
+
*/
|
|
39
|
+
parse(chunk) {
|
|
40
|
+
const events = [];
|
|
41
|
+
|
|
42
|
+
// Add chunk to buffer
|
|
43
|
+
this.buffer += chunk;
|
|
44
|
+
|
|
45
|
+
// Process complete lines
|
|
46
|
+
const lines = this.buffer.split('\n');
|
|
47
|
+
this.buffer = lines.pop() || ''; // Keep incomplete last line
|
|
48
|
+
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
const trimmed = line.trim();
|
|
51
|
+
if (!trimmed) continue;
|
|
52
|
+
|
|
53
|
+
// Skip non-JSON lines (CLI status messages, retry messages, etc.)
|
|
54
|
+
if (!trimmed.startsWith('{')) {
|
|
55
|
+
// Log significant non-JSON messages
|
|
56
|
+
if (trimmed.includes('Attempt') || trimmed.includes('failed') || trimmed.includes('Error')) {
|
|
57
|
+
console.log('[GeminiOutputParser] CLI message:', trimmed.substring(0, 100));
|
|
58
|
+
}
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const json = JSON.parse(trimmed);
|
|
64
|
+
const lineEvents = this._parseJsonEvent(json);
|
|
65
|
+
events.push(...lineEvents);
|
|
66
|
+
} catch (e) {
|
|
67
|
+
console.warn('[GeminiOutputParser] JSON parse error:', e.message, '- Line:', trimmed.substring(0, 80));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return events;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse a single JSON event from Gemini CLI
|
|
76
|
+
* @param {Object} event - Parsed JSON object
|
|
77
|
+
* @returns {Array} Events to emit
|
|
78
|
+
*/
|
|
79
|
+
_parseJsonEvent(event) {
|
|
80
|
+
const events = [];
|
|
81
|
+
|
|
82
|
+
switch (event.type) {
|
|
83
|
+
case 'init':
|
|
84
|
+
// Session initialization
|
|
85
|
+
this.sessionId = event.session_id;
|
|
86
|
+
this.model = event.model;
|
|
87
|
+
console.log('[GeminiOutputParser] Session initialized:', this.sessionId, 'Model:', this.model);
|
|
88
|
+
events.push({
|
|
89
|
+
type: 'status',
|
|
90
|
+
category: 'system',
|
|
91
|
+
message: 'Session initialized',
|
|
92
|
+
icon: '🚀',
|
|
93
|
+
sessionId: this.sessionId,
|
|
94
|
+
model: this.model,
|
|
95
|
+
timestamp: event.timestamp || new Date().toISOString(),
|
|
96
|
+
});
|
|
97
|
+
break;
|
|
98
|
+
|
|
99
|
+
case 'message':
|
|
100
|
+
if (event.role === 'assistant' || event.role === 'model') {
|
|
101
|
+
// Assistant response - streaming content
|
|
102
|
+
const content = event.content || '';
|
|
103
|
+
if (content) {
|
|
104
|
+
// Accumulate response
|
|
105
|
+
if (event.delta) {
|
|
106
|
+
// Delta mode - append to existing
|
|
107
|
+
this.finalResponse += content;
|
|
108
|
+
} else {
|
|
109
|
+
// Full replacement (rare)
|
|
110
|
+
this.finalResponse = content;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
events.push({
|
|
114
|
+
type: 'response_chunk',
|
|
115
|
+
text: content,
|
|
116
|
+
isIncremental: !!event.delta,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
|
|
122
|
+
case 'tool_use':
|
|
123
|
+
// Tool being invoked
|
|
124
|
+
const toolEvent = this._formatToolUseEvent(event);
|
|
125
|
+
events.push(toolEvent);
|
|
126
|
+
this.pendingTools.set(event.tool_id || event.tool_use_id || event.tool_name, event);
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
case 'tool_result':
|
|
130
|
+
// Tool execution completed
|
|
131
|
+
// Gemini CLI uses 'tool_name'
|
|
132
|
+
const toolName = event.tool_name || event.tool || 'Tool';
|
|
133
|
+
const success = event.status !== 'error' && event.status !== 'failure';
|
|
134
|
+
|
|
135
|
+
events.push({
|
|
136
|
+
type: 'status',
|
|
137
|
+
category: 'tool',
|
|
138
|
+
message: `${toolName}: ${success ? 'completed' : 'failed'}`,
|
|
139
|
+
icon: success ? '✅' : '❌',
|
|
140
|
+
toolOutput: this._truncateOutput(event.output || event.result),
|
|
141
|
+
timestamp: event.timestamp || new Date().toISOString(),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
this.pendingTools.delete(event.tool_use_id || event.tool);
|
|
145
|
+
break;
|
|
146
|
+
|
|
147
|
+
case 'result':
|
|
148
|
+
// Final result with stats
|
|
149
|
+
this.usage = event.stats || null;
|
|
150
|
+
console.log('[GeminiOutputParser] Result received, status:', event.status, 'stats:', JSON.stringify(this.usage));
|
|
151
|
+
|
|
152
|
+
// Emit response_done
|
|
153
|
+
events.push({
|
|
154
|
+
type: 'response_done',
|
|
155
|
+
fullText: this.finalResponse,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Emit done with usage stats
|
|
159
|
+
events.push({
|
|
160
|
+
type: 'done',
|
|
161
|
+
status: event.status,
|
|
162
|
+
usage: {
|
|
163
|
+
prompt_tokens: this.usage?.input_tokens || 0,
|
|
164
|
+
completion_tokens: this.usage?.output_tokens || 0,
|
|
165
|
+
total_tokens: (this.usage?.input_tokens || 0) + (this.usage?.output_tokens || 0),
|
|
166
|
+
},
|
|
167
|
+
duration_ms: this.usage?.duration_ms || 0,
|
|
168
|
+
tool_calls: this.usage?.tool_calls || 0,
|
|
169
|
+
sessionId: this.sessionId,
|
|
170
|
+
});
|
|
171
|
+
break;
|
|
172
|
+
|
|
173
|
+
case 'error':
|
|
174
|
+
console.error('[GeminiOutputParser] Error event:', event.message || event.error);
|
|
175
|
+
events.push({
|
|
176
|
+
type: 'error',
|
|
177
|
+
message: event.message || event.error || 'Unknown error',
|
|
178
|
+
});
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
default:
|
|
182
|
+
// Unknown event type - log but don't fail
|
|
183
|
+
console.log('[GeminiOutputParser] Unknown event type:', event.type);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return events;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Format a tool_use event into a status event
|
|
191
|
+
*/
|
|
192
|
+
_formatToolUseEvent(event) {
|
|
193
|
+
// Extract tool name from various possible fields
|
|
194
|
+
// Gemini CLI uses 'tool_name' and 'parameters'
|
|
195
|
+
const tool = event.tool_name || event.tool || event.name || event.function?.name || '';
|
|
196
|
+
const input = event.parameters || event.input || event.args || event.function?.arguments || {};
|
|
197
|
+
let message = tool || 'Tool';
|
|
198
|
+
|
|
199
|
+
// Debug: log raw event to understand structure
|
|
200
|
+
if (!tool) {
|
|
201
|
+
console.log('[GeminiOutputParser] Tool event without name:', JSON.stringify(event).substring(0, 200));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Format message based on tool type
|
|
205
|
+
switch (tool) {
|
|
206
|
+
case 'shell':
|
|
207
|
+
case 'run_shell_command':
|
|
208
|
+
case 'execute_command':
|
|
209
|
+
message = `Shell: ${this._truncate(event.command || input.command || '', 60)}`;
|
|
210
|
+
break;
|
|
211
|
+
case 'read_file':
|
|
212
|
+
case 'read_many_files':
|
|
213
|
+
case 'read':
|
|
214
|
+
message = `Reading: ${this._truncate(event.path || input.path || input.file_path || '', 50)}`;
|
|
215
|
+
break;
|
|
216
|
+
case 'write_file':
|
|
217
|
+
case 'write':
|
|
218
|
+
message = `Writing: ${this._truncate(event.path || input.path || input.file_path || '', 50)}`;
|
|
219
|
+
break;
|
|
220
|
+
case 'edit_file':
|
|
221
|
+
case 'edit':
|
|
222
|
+
message = `Editing: ${this._truncate(event.path || input.path || input.file_path || '', 50)}`;
|
|
223
|
+
break;
|
|
224
|
+
case 'search_files':
|
|
225
|
+
case 'grep':
|
|
226
|
+
case 'find_files':
|
|
227
|
+
message = `Searching: ${this._truncate(event.pattern || input.pattern || input.query || '', 40)}`;
|
|
228
|
+
break;
|
|
229
|
+
case 'list_directory':
|
|
230
|
+
case 'list_dir':
|
|
231
|
+
case 'ls':
|
|
232
|
+
message = `Listing: ${this._truncate(event.path || input.path || input.dir_path || '.', 50)}`;
|
|
233
|
+
break;
|
|
234
|
+
case 'web_search':
|
|
235
|
+
case 'google_search':
|
|
236
|
+
case 'search':
|
|
237
|
+
message = `Web search: ${this._truncate(event.query || input.query || '', 40)}`;
|
|
238
|
+
break;
|
|
239
|
+
case 'web_fetch':
|
|
240
|
+
case 'fetch_url':
|
|
241
|
+
message = `Fetching: ${this._truncate(event.url || input.url || '', 50)}`;
|
|
242
|
+
break;
|
|
243
|
+
default:
|
|
244
|
+
// For unknown tools, try to create a meaningful message
|
|
245
|
+
if (tool) {
|
|
246
|
+
message = `${tool}: running...`;
|
|
247
|
+
} else {
|
|
248
|
+
// Try to extract any useful info from the event
|
|
249
|
+
const keys = Object.keys(event).filter(k => !['type', 'timestamp', 'tool_use_id'].includes(k));
|
|
250
|
+
if (keys.length > 0) {
|
|
251
|
+
message = `Tool: ${keys[0]}...`;
|
|
252
|
+
} else {
|
|
253
|
+
message = 'Tool: running...';
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
type: 'status',
|
|
260
|
+
category: 'tool',
|
|
261
|
+
message,
|
|
262
|
+
icon: this._getToolIcon(tool),
|
|
263
|
+
timestamp: event.timestamp || new Date().toISOString(),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get emoji icon for tool type
|
|
269
|
+
*/
|
|
270
|
+
_getToolIcon(tool) {
|
|
271
|
+
const icons = {
|
|
272
|
+
// Shell commands
|
|
273
|
+
'shell': '🔧',
|
|
274
|
+
'run_shell_command': '🔧',
|
|
275
|
+
'execute_command': '🔧',
|
|
276
|
+
// File operations
|
|
277
|
+
'read_file': '📖',
|
|
278
|
+
'read_many_files': '📚',
|
|
279
|
+
'read': '📖',
|
|
280
|
+
'write_file': '✍️',
|
|
281
|
+
'write': '✍️',
|
|
282
|
+
'edit_file': '📝',
|
|
283
|
+
'edit': '📝',
|
|
284
|
+
// Search
|
|
285
|
+
'search_files': '🔍',
|
|
286
|
+
'grep': '🔍',
|
|
287
|
+
'find_files': '🔍',
|
|
288
|
+
// Directory
|
|
289
|
+
'list_directory': '🗂️',
|
|
290
|
+
'list_dir': '🗂️',
|
|
291
|
+
'ls': '🗂️',
|
|
292
|
+
// Web
|
|
293
|
+
'web_search': '🔎',
|
|
294
|
+
'google_search': '🔎',
|
|
295
|
+
'search': '🔎',
|
|
296
|
+
'web_fetch': '🌐',
|
|
297
|
+
'fetch_url': '🌐',
|
|
298
|
+
};
|
|
299
|
+
return icons[tool] || '⚙️';
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Truncate string to max length
|
|
304
|
+
*/
|
|
305
|
+
_truncate(str, maxLen) {
|
|
306
|
+
if (!str) return '';
|
|
307
|
+
if (str.length <= maxLen) return str;
|
|
308
|
+
return str.substring(0, maxLen) + '...';
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Truncate tool output (can be very long)
|
|
313
|
+
*/
|
|
314
|
+
_truncateOutput(content) {
|
|
315
|
+
if (!content) return null;
|
|
316
|
+
const str = typeof content === 'string' ? content : JSON.stringify(content);
|
|
317
|
+
if (str.length > 500) {
|
|
318
|
+
return str.substring(0, 500) + '\n... (truncated)';
|
|
319
|
+
}
|
|
320
|
+
return str;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Filter out internal "thinking" patterns from model output
|
|
325
|
+
* Common in preview models that expose chain-of-thought
|
|
326
|
+
*/
|
|
327
|
+
_filterThinkingPatterns(text) {
|
|
328
|
+
if (!text) return text;
|
|
329
|
+
|
|
330
|
+
const thinkingPatterns = [
|
|
331
|
+
/^Wait,?\s+.{0,200}$/gm,
|
|
332
|
+
/^Actually,?\s+.{0,200}$/gm,
|
|
333
|
+
/^Let me\s+.{0,150}$/gm,
|
|
334
|
+
/^I will\s+.{0,150}$/gm,
|
|
335
|
+
/^I should\s+.{0,150}$/gm,
|
|
336
|
+
/^I need to\s+.{0,150}$/gm,
|
|
337
|
+
/^Ready\.?\s*$/gm,
|
|
338
|
+
/^Okay\.?\s*$/gm,
|
|
339
|
+
];
|
|
340
|
+
|
|
341
|
+
let filtered = text;
|
|
342
|
+
for (const pattern of thinkingPatterns) {
|
|
343
|
+
filtered = filtered.replace(pattern, '');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Remove multiple consecutive newlines
|
|
347
|
+
filtered = filtered.replace(/\n{3,}/g, '\n\n');
|
|
348
|
+
return filtered.trim();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Get accumulated final response (filtered)
|
|
353
|
+
*/
|
|
354
|
+
getFinalResponse() {
|
|
355
|
+
return this._filterThinkingPatterns(this.finalResponse);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Get raw unfiltered response (for debugging)
|
|
360
|
+
*/
|
|
361
|
+
getRawResponse() {
|
|
362
|
+
return this.finalResponse;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Get usage statistics
|
|
367
|
+
*/
|
|
368
|
+
getUsage() {
|
|
369
|
+
return this.usage;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Get session ID
|
|
374
|
+
*/
|
|
375
|
+
getSessionId() {
|
|
376
|
+
return this.sessionId;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Get model name
|
|
381
|
+
*/
|
|
382
|
+
getModel() {
|
|
383
|
+
return this.model;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Reset parser state for new request
|
|
388
|
+
*/
|
|
389
|
+
reset() {
|
|
390
|
+
this.buffer = '';
|
|
391
|
+
this.finalResponse = '';
|
|
392
|
+
this.usage = null;
|
|
393
|
+
this.pendingTools.clear();
|
|
394
|
+
// Keep sessionId and model for session continuity
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
module.exports = GeminiOutputParser;
|