@qwickapps/qwickbrain-proxy 1.0.2 → 1.1.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/CHANGELOG.md +17 -0
- package/dist/db/schema.d.ts +63 -6
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +17 -2
- package/dist/db/schema.js.map +1 -1
- package/dist/lib/__tests__/cache-manager.test.js +146 -83
- package/dist/lib/__tests__/cache-manager.test.js.map +1 -1
- package/dist/lib/__tests__/connection-manager.test.js +2 -2
- package/dist/lib/__tests__/connection-manager.test.js.map +1 -1
- package/dist/lib/__tests__/proxy-server.test.js +16 -44
- package/dist/lib/__tests__/proxy-server.test.js.map +1 -1
- package/dist/lib/__tests__/qwickbrain-client.test.js +3 -1
- package/dist/lib/__tests__/qwickbrain-client.test.js.map +1 -1
- package/dist/lib/__tests__/sse-invalidation-listener.test.d.ts +2 -0
- package/dist/lib/__tests__/sse-invalidation-listener.test.d.ts.map +1 -0
- package/dist/lib/__tests__/sse-invalidation-listener.test.js +245 -0
- package/dist/lib/__tests__/sse-invalidation-listener.test.js.map +1 -0
- package/dist/lib/__tests__/write-queue-manager.test.d.ts +2 -0
- package/dist/lib/__tests__/write-queue-manager.test.d.ts.map +1 -0
- package/dist/lib/__tests__/write-queue-manager.test.js +291 -0
- package/dist/lib/__tests__/write-queue-manager.test.js.map +1 -0
- package/dist/lib/cache-manager.d.ts +35 -6
- package/dist/lib/cache-manager.d.ts.map +1 -1
- package/dist/lib/cache-manager.js +154 -41
- package/dist/lib/cache-manager.js.map +1 -1
- package/dist/lib/connection-manager.d.ts +7 -0
- package/dist/lib/connection-manager.d.ts.map +1 -1
- package/dist/lib/connection-manager.js +57 -8
- package/dist/lib/connection-manager.js.map +1 -1
- package/dist/lib/proxy-server.d.ts +12 -0
- package/dist/lib/proxy-server.d.ts.map +1 -1
- package/dist/lib/proxy-server.js +184 -87
- package/dist/lib/proxy-server.js.map +1 -1
- package/dist/lib/qwickbrain-client.d.ts +4 -0
- package/dist/lib/qwickbrain-client.d.ts.map +1 -1
- package/dist/lib/qwickbrain-client.js +152 -13
- package/dist/lib/qwickbrain-client.js.map +1 -1
- package/dist/lib/sse-invalidation-listener.d.ts +31 -0
- package/dist/lib/sse-invalidation-listener.d.ts.map +1 -0
- package/dist/lib/sse-invalidation-listener.js +151 -0
- package/dist/lib/sse-invalidation-listener.js.map +1 -0
- package/dist/lib/tools.d.ts +21 -0
- package/dist/lib/tools.d.ts.map +1 -0
- package/dist/lib/tools.js +513 -0
- package/dist/lib/tools.js.map +1 -0
- package/dist/lib/write-queue-manager.d.ts +88 -0
- package/dist/lib/write-queue-manager.d.ts.map +1 -0
- package/dist/lib/write-queue-manager.js +191 -0
- package/dist/lib/write-queue-manager.js.map +1 -0
- package/dist/types/config.d.ts +7 -42
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +1 -6
- package/dist/types/config.js.map +1 -1
- package/drizzle/0002_lru_cache_migration.sql +94 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +6 -2
- package/scripts/rebuild-sqlite.sh +26 -0
- package/src/db/schema.ts +17 -2
- package/src/lib/__tests__/cache-manager.test.ts +180 -90
- package/src/lib/__tests__/connection-manager.test.ts +2 -2
- package/src/lib/__tests__/proxy-server.test.ts +16 -51
- package/src/lib/__tests__/qwickbrain-client.test.ts +3 -1
- package/src/lib/__tests__/sse-invalidation-listener.test.ts +326 -0
- package/src/lib/__tests__/write-queue-manager.test.ts +383 -0
- package/src/lib/cache-manager.ts +198 -46
- package/src/lib/connection-manager.ts +67 -8
- package/src/lib/proxy-server.ts +231 -90
- package/src/lib/qwickbrain-client.ts +166 -12
- package/src/lib/sse-invalidation-listener.ts +185 -0
- package/src/lib/tools.ts +525 -0
- package/src/lib/write-queue-manager.ts +271 -0
- package/src/types/config.ts +1 -6
- package/.github/workflows/publish.yml +0 -92
package/src/lib/proxy-server.ts
CHANGED
|
@@ -7,6 +7,9 @@ import {
|
|
|
7
7
|
import { ConnectionManager } from './connection-manager.js';
|
|
8
8
|
import { CacheManager } from './cache-manager.js';
|
|
9
9
|
import { QwickBrainClient } from './qwickbrain-client.js';
|
|
10
|
+
import { WriteQueueManager } from './write-queue-manager.js';
|
|
11
|
+
import { SSEInvalidationListener } from './sse-invalidation-listener.js';
|
|
12
|
+
import { QWICKBRAIN_TOOLS, requiresConnection } from './tools.js';
|
|
10
13
|
import type { Config } from '../types/config.js';
|
|
11
14
|
import type { DB } from '../db/client.js';
|
|
12
15
|
import type { MCPResponse, MCPResponseMetadata } from '../types/mcp.js';
|
|
@@ -17,7 +20,10 @@ export class ProxyServer {
|
|
|
17
20
|
private connectionManager: ConnectionManager;
|
|
18
21
|
private cacheManager: CacheManager;
|
|
19
22
|
private qwickbrainClient: QwickBrainClient;
|
|
23
|
+
private writeQueueManager: WriteQueueManager;
|
|
24
|
+
private sseInvalidationListener: SSEInvalidationListener | null = null;
|
|
20
25
|
private config: Config;
|
|
26
|
+
private connectionInitialized = false;
|
|
21
27
|
|
|
22
28
|
constructor(db: DB, config: Config) {
|
|
23
29
|
this.config = config;
|
|
@@ -41,11 +47,42 @@ export class ProxyServer {
|
|
|
41
47
|
);
|
|
42
48
|
|
|
43
49
|
this.cacheManager = new CacheManager(db, config.cache);
|
|
50
|
+
this.writeQueueManager = new WriteQueueManager(db, this.qwickbrainClient);
|
|
51
|
+
|
|
52
|
+
// Initialize SSE invalidation listener if in SSE mode
|
|
53
|
+
if (config.qwickbrain.mode === 'sse' && config.qwickbrain.url) {
|
|
54
|
+
this.sseInvalidationListener = new SSEInvalidationListener(
|
|
55
|
+
config.qwickbrain.url,
|
|
56
|
+
this.cacheManager,
|
|
57
|
+
config.qwickbrain.apiKey
|
|
58
|
+
);
|
|
59
|
+
}
|
|
44
60
|
|
|
45
61
|
this.setupHandlers();
|
|
46
62
|
this.setupConnectionListeners();
|
|
47
63
|
}
|
|
48
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Lazy initialization: start connection manager on first tool call
|
|
67
|
+
* instead of eagerly on server startup. Saves CPU when QwickBrain
|
|
68
|
+
* tools are never used in a session.
|
|
69
|
+
*/
|
|
70
|
+
private async ensureConnectionInitialized(): Promise<void> {
|
|
71
|
+
if (this.connectionInitialized) {
|
|
72
|
+
// If dormant, wake up on tool call
|
|
73
|
+
this.connectionManager.wakeUp();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
this.connectionInitialized = true;
|
|
78
|
+
console.error('First tool call received, initializing connection...');
|
|
79
|
+
|
|
80
|
+
await this.connectionManager.start();
|
|
81
|
+
|
|
82
|
+
// SSE listener starts only when connection is established (see setupConnectionListeners)
|
|
83
|
+
// Not here - avoid starting SSE reconnection loops before we know the server is reachable
|
|
84
|
+
}
|
|
85
|
+
|
|
49
86
|
private setupConnectionListeners(): void {
|
|
50
87
|
this.connectionManager.on('stateChange', ({ from, to }) => {
|
|
51
88
|
console.error(`Connection state: ${from} → ${to}`);
|
|
@@ -57,14 +94,38 @@ export class ProxyServer {
|
|
|
57
94
|
|
|
58
95
|
this.connectionManager.on('connected', ({ latencyMs }) => {
|
|
59
96
|
console.error(`Connected to QwickBrain (latency: ${latencyMs}ms)`);
|
|
97
|
+
|
|
98
|
+
// Start SSE listener when connection is established
|
|
99
|
+
if (this.sseInvalidationListener && !this.sseInvalidationListener.isListening()) {
|
|
100
|
+
this.sseInvalidationListener.start().catch(err => {
|
|
101
|
+
console.error('SSE listener start error:', err);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
60
105
|
// Event-driven: trigger background sync when connection restored
|
|
61
106
|
this.onConnectionRestored().catch(err => {
|
|
62
107
|
console.error('Background sync error:', err);
|
|
63
108
|
});
|
|
109
|
+
// Sync pending write operations
|
|
110
|
+
this.syncWriteQueue().catch(err => {
|
|
111
|
+
console.error('Write queue sync error:', err);
|
|
112
|
+
});
|
|
64
113
|
});
|
|
65
114
|
|
|
66
115
|
this.connectionManager.on('disconnected', ({ error }) => {
|
|
67
116
|
console.error(`Disconnected from QwickBrain: ${error}`);
|
|
117
|
+
// Stop SSE listener when disconnected to avoid independent reconnection loops
|
|
118
|
+
if (this.sseInvalidationListener) {
|
|
119
|
+
this.sseInvalidationListener.stop();
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
this.connectionManager.on('dormant', () => {
|
|
124
|
+
console.error('Connection manager entered dormant mode');
|
|
125
|
+
// Stop SSE listener in dormant mode
|
|
126
|
+
if (this.sseInvalidationListener) {
|
|
127
|
+
this.sseInvalidationListener.stop();
|
|
128
|
+
}
|
|
68
129
|
});
|
|
69
130
|
}
|
|
70
131
|
|
|
@@ -90,66 +151,29 @@ export class ProxyServer {
|
|
|
90
151
|
console.error('Background cache sync complete');
|
|
91
152
|
}
|
|
92
153
|
|
|
154
|
+
private async syncWriteQueue(): Promise<void> {
|
|
155
|
+
const pendingCount = await this.writeQueueManager.getPendingCount();
|
|
156
|
+
if (pendingCount === 0) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.error(`Syncing ${pendingCount} pending write operations...`);
|
|
161
|
+
const { synced, failed } = await this.writeQueueManager.syncPendingOperations();
|
|
162
|
+
console.error(`Write queue sync complete: ${synced} synced, ${failed} failed`);
|
|
163
|
+
}
|
|
164
|
+
|
|
93
165
|
private setupHandlers(): void {
|
|
166
|
+
// Static tool listing - always returns all tools regardless of connection state
|
|
94
167
|
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
95
|
-
|
|
96
|
-
if (this.connectionManager.getState() === 'connected') {
|
|
97
|
-
try {
|
|
98
|
-
const tools = await this.connectionManager.execute(async () => {
|
|
99
|
-
return await this.qwickbrainClient.listTools();
|
|
100
|
-
});
|
|
101
|
-
return { tools };
|
|
102
|
-
} catch (error) {
|
|
103
|
-
console.error('Failed to list tools from upstream:', error);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Fallback to minimal tool set when offline or error
|
|
108
|
-
return {
|
|
109
|
-
tools: [
|
|
110
|
-
{
|
|
111
|
-
name: 'get_workflow',
|
|
112
|
-
description: 'Get a workflow definition by name (cached)',
|
|
113
|
-
inputSchema: {
|
|
114
|
-
type: 'object',
|
|
115
|
-
properties: {
|
|
116
|
-
name: { type: 'string', description: 'Workflow name' },
|
|
117
|
-
},
|
|
118
|
-
required: ['name'],
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
{
|
|
122
|
-
name: 'get_document',
|
|
123
|
-
description: 'Get a document by name and type (cached)',
|
|
124
|
-
inputSchema: {
|
|
125
|
-
type: 'object',
|
|
126
|
-
properties: {
|
|
127
|
-
name: { type: 'string', description: 'Document name' },
|
|
128
|
-
doc_type: { type: 'string', description: 'Document type (rule, frd, design, etc.)' },
|
|
129
|
-
project: { type: 'string', description: 'Project name (optional)' },
|
|
130
|
-
},
|
|
131
|
-
required: ['name', 'doc_type'],
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
{
|
|
135
|
-
name: 'get_memory',
|
|
136
|
-
description: 'Get a memory/context document by name (cached)',
|
|
137
|
-
inputSchema: {
|
|
138
|
-
type: 'object',
|
|
139
|
-
properties: {
|
|
140
|
-
name: { type: 'string', description: 'Memory name' },
|
|
141
|
-
project: { type: 'string', description: 'Project name (optional)' },
|
|
142
|
-
},
|
|
143
|
-
required: ['name'],
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
],
|
|
147
|
-
};
|
|
168
|
+
return { tools: QWICKBRAIN_TOOLS };
|
|
148
169
|
});
|
|
149
170
|
|
|
150
171
|
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
151
172
|
const { name, arguments: args } = request.params;
|
|
152
173
|
|
|
174
|
+
// Lazy init: start connection on first tool call
|
|
175
|
+
await this.ensureConnectionInitialized();
|
|
176
|
+
|
|
153
177
|
try {
|
|
154
178
|
let result: MCPResponse;
|
|
155
179
|
|
|
@@ -171,8 +195,54 @@ export class ProxyServer {
|
|
|
171
195
|
args?.project as string | undefined
|
|
172
196
|
);
|
|
173
197
|
break;
|
|
198
|
+
case 'create_document':
|
|
199
|
+
case 'update_document':
|
|
200
|
+
result = await this.handleCreateDocument(
|
|
201
|
+
args?.doc_type as string,
|
|
202
|
+
args?.name as string,
|
|
203
|
+
args?.content as string,
|
|
204
|
+
args?.project as string | undefined,
|
|
205
|
+
args?.metadata as Record<string, unknown> | undefined
|
|
206
|
+
);
|
|
207
|
+
break;
|
|
208
|
+
case 'set_memory':
|
|
209
|
+
case 'update_memory':
|
|
210
|
+
result = await this.handleSetMemory(
|
|
211
|
+
args?.name as string,
|
|
212
|
+
args?.content as string,
|
|
213
|
+
args?.project as string | undefined,
|
|
214
|
+
args?.metadata as Record<string, unknown> | undefined
|
|
215
|
+
);
|
|
216
|
+
break;
|
|
174
217
|
default:
|
|
175
218
|
// Generic forwarding for all other tools (analyze_repository, search_codebase, etc.)
|
|
219
|
+
// Check if tool requires connection
|
|
220
|
+
if (requiresConnection(name) && this.connectionManager.getState() !== 'connected') {
|
|
221
|
+
// Return offline error for non-cacheable tools
|
|
222
|
+
return {
|
|
223
|
+
content: [
|
|
224
|
+
{
|
|
225
|
+
type: 'text',
|
|
226
|
+
text: JSON.stringify({
|
|
227
|
+
error: {
|
|
228
|
+
code: 'OFFLINE',
|
|
229
|
+
message: `QwickBrain offline - "${name}" requires active connection`,
|
|
230
|
+
suggestions: [
|
|
231
|
+
'Check internet connection',
|
|
232
|
+
'Wait for automatic reconnection',
|
|
233
|
+
'Cached tools (get_workflow, get_document, get_memory) work offline',
|
|
234
|
+
],
|
|
235
|
+
},
|
|
236
|
+
_metadata: {
|
|
237
|
+
source: 'cache',
|
|
238
|
+
status: this.connectionManager.getState(),
|
|
239
|
+
},
|
|
240
|
+
}, null, 2),
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
isError: true,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
176
246
|
result = await this.handleGenericTool(name, args || {});
|
|
177
247
|
break;
|
|
178
248
|
}
|
|
@@ -226,17 +296,17 @@ export class ProxyServer {
|
|
|
226
296
|
name: string,
|
|
227
297
|
project?: string
|
|
228
298
|
): Promise<MCPResponse> {
|
|
229
|
-
// Try cache first
|
|
299
|
+
// Try cache first (LRU cache never expires, always valid if present)
|
|
230
300
|
const cached = await this.cacheManager.getDocument(docType, name, project);
|
|
231
301
|
|
|
232
|
-
if (cached
|
|
302
|
+
if (cached) {
|
|
233
303
|
return {
|
|
234
304
|
data: cached.data,
|
|
235
305
|
_metadata: this.createMetadata('cache', cached.age),
|
|
236
306
|
};
|
|
237
307
|
}
|
|
238
308
|
|
|
239
|
-
//
|
|
309
|
+
// Not cached - try remote if connected
|
|
240
310
|
if (this.connectionManager.getState() === 'connected') {
|
|
241
311
|
try {
|
|
242
312
|
const result = await this.connectionManager.execute(async () => {
|
|
@@ -258,22 +328,11 @@ export class ProxyServer {
|
|
|
258
328
|
};
|
|
259
329
|
} catch (error) {
|
|
260
330
|
console.error('Failed to fetch from QwickBrain:', error);
|
|
261
|
-
// Fall through to
|
|
331
|
+
// Fall through to error
|
|
262
332
|
}
|
|
263
333
|
}
|
|
264
334
|
|
|
265
|
-
//
|
|
266
|
-
if (cached) {
|
|
267
|
-
return {
|
|
268
|
-
data: cached.data,
|
|
269
|
-
_metadata: {
|
|
270
|
-
...this.createMetadata('stale_cache', cached.age),
|
|
271
|
-
warning: `QwickBrain unavailable - serving cached data (${cached.age}s old)`,
|
|
272
|
-
},
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// No cache, no connection
|
|
335
|
+
// No cache and remote failed/unavailable
|
|
277
336
|
return {
|
|
278
337
|
error: {
|
|
279
338
|
code: 'UNAVAILABLE',
|
|
@@ -289,16 +348,17 @@ export class ProxyServer {
|
|
|
289
348
|
}
|
|
290
349
|
|
|
291
350
|
private async handleGetMemory(name: string, project?: string): Promise<MCPResponse> {
|
|
292
|
-
//
|
|
351
|
+
// Try cache first (LRU cache never expires, always valid if present)
|
|
293
352
|
const cached = await this.cacheManager.getMemory(name, project);
|
|
294
353
|
|
|
295
|
-
if (cached
|
|
354
|
+
if (cached) {
|
|
296
355
|
return {
|
|
297
356
|
data: cached.data,
|
|
298
357
|
_metadata: this.createMetadata('cache', cached.age),
|
|
299
358
|
};
|
|
300
359
|
}
|
|
301
360
|
|
|
361
|
+
// Not cached - try remote if connected
|
|
302
362
|
if (this.connectionManager.getState() === 'connected') {
|
|
303
363
|
try {
|
|
304
364
|
const result = await this.connectionManager.execute(async () => {
|
|
@@ -313,19 +373,11 @@ export class ProxyServer {
|
|
|
313
373
|
};
|
|
314
374
|
} catch (error) {
|
|
315
375
|
console.error('Failed to fetch memory from QwickBrain:', error);
|
|
376
|
+
// Fall through to error
|
|
316
377
|
}
|
|
317
378
|
}
|
|
318
379
|
|
|
319
|
-
|
|
320
|
-
return {
|
|
321
|
-
data: cached.data,
|
|
322
|
-
_metadata: {
|
|
323
|
-
...this.createMetadata('stale_cache', cached.age),
|
|
324
|
-
warning: `QwickBrain unavailable - serving cached memory (${cached.age}s old)`,
|
|
325
|
-
},
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
|
|
380
|
+
// No cache and remote failed/unavailable
|
|
329
381
|
return {
|
|
330
382
|
error: {
|
|
331
383
|
code: 'UNAVAILABLE',
|
|
@@ -336,6 +388,94 @@ export class ProxyServer {
|
|
|
336
388
|
};
|
|
337
389
|
}
|
|
338
390
|
|
|
391
|
+
private async handleCreateDocument(
|
|
392
|
+
docType: string,
|
|
393
|
+
name: string,
|
|
394
|
+
content: string,
|
|
395
|
+
project?: string,
|
|
396
|
+
metadata?: Record<string, unknown>
|
|
397
|
+
): Promise<MCPResponse> {
|
|
398
|
+
// Always update local cache first
|
|
399
|
+
await this.cacheManager.setDocument(docType, name, content, project, metadata);
|
|
400
|
+
|
|
401
|
+
// If connected, sync immediately
|
|
402
|
+
if (this.connectionManager.getState() === 'connected') {
|
|
403
|
+
try {
|
|
404
|
+
await this.connectionManager.execute(async () => {
|
|
405
|
+
await this.qwickbrainClient.createDocument(docType, name, content, project, metadata);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
data: { success: true },
|
|
410
|
+
_metadata: this.createMetadata('live'),
|
|
411
|
+
};
|
|
412
|
+
} catch (error) {
|
|
413
|
+
console.error('Failed to create document on QwickBrain:', error);
|
|
414
|
+
// Fall through to queue
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// If offline or sync failed, queue for later
|
|
419
|
+
await this.writeQueueManager.queueOperation('create_document', {
|
|
420
|
+
docType,
|
|
421
|
+
name,
|
|
422
|
+
content,
|
|
423
|
+
project,
|
|
424
|
+
metadata,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
data: { success: true, queued: true },
|
|
429
|
+
_metadata: {
|
|
430
|
+
...this.createMetadata('cache'),
|
|
431
|
+
warning: 'Operation queued - will sync when connection restored',
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private async handleSetMemory(
|
|
437
|
+
name: string,
|
|
438
|
+
content: string,
|
|
439
|
+
project?: string,
|
|
440
|
+
metadata?: Record<string, unknown>
|
|
441
|
+
): Promise<MCPResponse> {
|
|
442
|
+
// Always update local cache first
|
|
443
|
+
await this.cacheManager.setMemory(name, content, project, metadata);
|
|
444
|
+
|
|
445
|
+
// If connected, sync immediately
|
|
446
|
+
if (this.connectionManager.getState() === 'connected') {
|
|
447
|
+
try {
|
|
448
|
+
await this.connectionManager.execute(async () => {
|
|
449
|
+
await this.qwickbrainClient.setMemory(name, content, project, metadata);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
data: { success: true },
|
|
454
|
+
_metadata: this.createMetadata('live'),
|
|
455
|
+
};
|
|
456
|
+
} catch (error) {
|
|
457
|
+
console.error('Failed to set memory on QwickBrain:', error);
|
|
458
|
+
// Fall through to queue
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// If offline or sync failed, queue for later
|
|
463
|
+
await this.writeQueueManager.queueOperation('set_memory', {
|
|
464
|
+
name,
|
|
465
|
+
content,
|
|
466
|
+
project,
|
|
467
|
+
metadata,
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
data: { success: true, queued: true },
|
|
472
|
+
_metadata: {
|
|
473
|
+
...this.createMetadata('cache'),
|
|
474
|
+
warning: 'Operation queued - will sync when connection restored',
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
339
479
|
private async handleGenericTool(name: string, args: Record<string, unknown>): Promise<MCPResponse> {
|
|
340
480
|
// Generic tool forwarding - no caching for non-document tools
|
|
341
481
|
if (this.connectionManager.getState() !== 'connected') {
|
|
@@ -388,24 +528,25 @@ export class ProxyServer {
|
|
|
388
528
|
}
|
|
389
529
|
|
|
390
530
|
async start(): Promise<void> {
|
|
391
|
-
//
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
console.error(`Cache cleanup: removed ${documentsDeleted} documents, ${memoriesDeleted} memories`);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
// Start connection manager (handles connection gracefully, doesn't throw)
|
|
398
|
-
await this.connectionManager.start();
|
|
531
|
+
// Lazy init: do NOT start connection manager here.
|
|
532
|
+
// Connection is initialized on first tool call (see ensureConnectionInitialized).
|
|
533
|
+
// This means idle proxy instances use zero network resources.
|
|
399
534
|
|
|
400
535
|
// Start MCP server
|
|
401
536
|
const transport = new StdioServerTransport();
|
|
402
537
|
await this.server.connect(transport);
|
|
403
538
|
|
|
404
|
-
console.error('QwickBrain Proxy started');
|
|
539
|
+
console.error('QwickBrain Proxy started (lazy mode - connection starts on first tool call)');
|
|
405
540
|
}
|
|
406
541
|
|
|
407
542
|
async stop(): Promise<void> {
|
|
408
543
|
this.connectionManager.stop();
|
|
544
|
+
|
|
545
|
+
// Stop SSE invalidation listener
|
|
546
|
+
if (this.sseInvalidationListener) {
|
|
547
|
+
this.sseInvalidationListener.stop();
|
|
548
|
+
}
|
|
549
|
+
|
|
409
550
|
try {
|
|
410
551
|
await this.qwickbrainClient.disconnect();
|
|
411
552
|
} catch (error) {
|
|
@@ -229,25 +229,42 @@ export class QwickBrainClient {
|
|
|
229
229
|
|
|
230
230
|
async healthCheck(): Promise<boolean> {
|
|
231
231
|
try {
|
|
232
|
-
if (this.mode === '
|
|
233
|
-
// For MCP/SSE mode, check if client is connected
|
|
234
|
-
if (!this.client) {
|
|
235
|
-
// Don't create new connection in health check - just return false
|
|
236
|
-
return false;
|
|
237
|
-
}
|
|
238
|
-
// Try listing tools as health check
|
|
239
|
-
await this.client.listTools();
|
|
240
|
-
return true;
|
|
241
|
-
} else {
|
|
232
|
+
if (this.mode === 'http') {
|
|
242
233
|
// For HTTP mode, ping health endpoint
|
|
243
234
|
if (!this.config.url) {
|
|
244
235
|
return false;
|
|
245
236
|
}
|
|
246
|
-
const response = await fetch(`${this.config.url}/health
|
|
237
|
+
const response = await fetch(`${this.config.url}/health`, {
|
|
238
|
+
signal: AbortSignal.timeout(5000),
|
|
239
|
+
});
|
|
247
240
|
return response.ok;
|
|
248
241
|
}
|
|
242
|
+
|
|
243
|
+
// For MCP/SSE mode: use a lightweight HTTP ping to the base URL
|
|
244
|
+
// instead of creating expensive MCP Client+Transport objects each time
|
|
245
|
+
if (this.config.url) {
|
|
246
|
+
const baseUrl = this.config.url.replace(/\/sse$/, '');
|
|
247
|
+
const response = await fetch(`${baseUrl}/health`, {
|
|
248
|
+
signal: AbortSignal.timeout(5000),
|
|
249
|
+
});
|
|
250
|
+
if (!response.ok) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Server is reachable; ensure MCP client is connected
|
|
256
|
+
if (!this.client) {
|
|
257
|
+
await this.connect();
|
|
258
|
+
if (!this.client) {
|
|
259
|
+
return false;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Verify the MCP connection is alive
|
|
264
|
+
await this.client.listTools();
|
|
265
|
+
return true;
|
|
249
266
|
} catch (error) {
|
|
250
|
-
// Connection dropped - clear client to force reconnection
|
|
267
|
+
// Connection dropped - clear client to force reconnection on next successful health check
|
|
251
268
|
this.client = null;
|
|
252
269
|
this.transport = null;
|
|
253
270
|
return false;
|
|
@@ -310,6 +327,143 @@ export class QwickBrainClient {
|
|
|
310
327
|
}
|
|
311
328
|
}
|
|
312
329
|
|
|
330
|
+
async createDocument(
|
|
331
|
+
docType: string,
|
|
332
|
+
name: string,
|
|
333
|
+
content: string,
|
|
334
|
+
project?: string,
|
|
335
|
+
metadata?: Record<string, unknown>
|
|
336
|
+
): Promise<void> {
|
|
337
|
+
if (this.mode === 'mcp' || this.mode === 'sse') {
|
|
338
|
+
if (!this.client) {
|
|
339
|
+
throw new Error('MCP client not connected');
|
|
340
|
+
}
|
|
341
|
+
await this.client.callTool({
|
|
342
|
+
name: 'create_document',
|
|
343
|
+
arguments: {
|
|
344
|
+
doc_type: docType,
|
|
345
|
+
name,
|
|
346
|
+
content,
|
|
347
|
+
project,
|
|
348
|
+
metadata,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
} else {
|
|
352
|
+
if (!this.config.url) {
|
|
353
|
+
throw new Error('HTTP mode requires url to be configured');
|
|
354
|
+
}
|
|
355
|
+
const response = await fetch(`${this.config.url}/mcp/document/create`, {
|
|
356
|
+
method: 'POST',
|
|
357
|
+
headers: {
|
|
358
|
+
'Content-Type': 'application/json',
|
|
359
|
+
...(this.config.apiKey && { 'Authorization': `Bearer ${this.config.apiKey}` }),
|
|
360
|
+
},
|
|
361
|
+
body: JSON.stringify({ doc_type: docType, name, content, project, metadata }),
|
|
362
|
+
});
|
|
363
|
+
if (!response.ok) {
|
|
364
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async setMemory(
|
|
370
|
+
name: string,
|
|
371
|
+
content: string,
|
|
372
|
+
project?: string,
|
|
373
|
+
metadata?: Record<string, unknown>
|
|
374
|
+
): Promise<void> {
|
|
375
|
+
if (this.mode === 'mcp' || this.mode === 'sse') {
|
|
376
|
+
if (!this.client) {
|
|
377
|
+
throw new Error('MCP client not connected');
|
|
378
|
+
}
|
|
379
|
+
await this.client.callTool({
|
|
380
|
+
name: 'set_memory',
|
|
381
|
+
arguments: {
|
|
382
|
+
name,
|
|
383
|
+
content,
|
|
384
|
+
project,
|
|
385
|
+
metadata,
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
} else {
|
|
389
|
+
if (!this.config.url) {
|
|
390
|
+
throw new Error('HTTP mode requires url to be configured');
|
|
391
|
+
}
|
|
392
|
+
const response = await fetch(`${this.config.url}/mcp/memory/set`, {
|
|
393
|
+
method: 'POST',
|
|
394
|
+
headers: {
|
|
395
|
+
'Content-Type': 'application/json',
|
|
396
|
+
...(this.config.apiKey && { 'Authorization': `Bearer ${this.config.apiKey}` }),
|
|
397
|
+
},
|
|
398
|
+
body: JSON.stringify({ name, content, project, metadata }),
|
|
399
|
+
});
|
|
400
|
+
if (!response.ok) {
|
|
401
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async deleteDocument(docType: string, name: string, project?: string): Promise<void> {
|
|
407
|
+
if (this.mode === 'mcp' || this.mode === 'sse') {
|
|
408
|
+
if (!this.client) {
|
|
409
|
+
throw new Error('MCP client not connected');
|
|
410
|
+
}
|
|
411
|
+
await this.client.callTool({
|
|
412
|
+
name: 'delete_document',
|
|
413
|
+
arguments: {
|
|
414
|
+
doc_type: docType,
|
|
415
|
+
name,
|
|
416
|
+
project,
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
} else {
|
|
420
|
+
if (!this.config.url) {
|
|
421
|
+
throw new Error('HTTP mode requires url to be configured');
|
|
422
|
+
}
|
|
423
|
+
const response = await fetch(`${this.config.url}/mcp/document/delete`, {
|
|
424
|
+
method: 'POST',
|
|
425
|
+
headers: {
|
|
426
|
+
'Content-Type': 'application/json',
|
|
427
|
+
...(this.config.apiKey && { 'Authorization': `Bearer ${this.config.apiKey}` }),
|
|
428
|
+
},
|
|
429
|
+
body: JSON.stringify({ doc_type: docType, name, project }),
|
|
430
|
+
});
|
|
431
|
+
if (!response.ok) {
|
|
432
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async deleteMemory(name: string, project?: string): Promise<void> {
|
|
438
|
+
if (this.mode === 'mcp' || this.mode === 'sse') {
|
|
439
|
+
if (!this.client) {
|
|
440
|
+
throw new Error('MCP client not connected');
|
|
441
|
+
}
|
|
442
|
+
await this.client.callTool({
|
|
443
|
+
name: 'delete_memory',
|
|
444
|
+
arguments: {
|
|
445
|
+
name,
|
|
446
|
+
project,
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
} else {
|
|
450
|
+
if (!this.config.url) {
|
|
451
|
+
throw new Error('HTTP mode requires url to be configured');
|
|
452
|
+
}
|
|
453
|
+
const response = await fetch(`${this.config.url}/mcp/memory/delete`, {
|
|
454
|
+
method: 'POST',
|
|
455
|
+
headers: {
|
|
456
|
+
'Content-Type': 'application/json',
|
|
457
|
+
...(this.config.apiKey && { 'Authorization': `Bearer ${this.config.apiKey}` }),
|
|
458
|
+
},
|
|
459
|
+
body: JSON.stringify({ name, project }),
|
|
460
|
+
});
|
|
461
|
+
if (!response.ok) {
|
|
462
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
313
467
|
async disconnect(): Promise<void> {
|
|
314
468
|
if ((this.mode === 'mcp' || this.mode === 'sse') && this.client && this.transport) {
|
|
315
469
|
await this.client.close();
|