@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.
Files changed (73) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/db/schema.d.ts +63 -6
  3. package/dist/db/schema.d.ts.map +1 -1
  4. package/dist/db/schema.js +17 -2
  5. package/dist/db/schema.js.map +1 -1
  6. package/dist/lib/__tests__/cache-manager.test.js +146 -83
  7. package/dist/lib/__tests__/cache-manager.test.js.map +1 -1
  8. package/dist/lib/__tests__/connection-manager.test.js +2 -2
  9. package/dist/lib/__tests__/connection-manager.test.js.map +1 -1
  10. package/dist/lib/__tests__/proxy-server.test.js +16 -44
  11. package/dist/lib/__tests__/proxy-server.test.js.map +1 -1
  12. package/dist/lib/__tests__/qwickbrain-client.test.js +3 -1
  13. package/dist/lib/__tests__/qwickbrain-client.test.js.map +1 -1
  14. package/dist/lib/__tests__/sse-invalidation-listener.test.d.ts +2 -0
  15. package/dist/lib/__tests__/sse-invalidation-listener.test.d.ts.map +1 -0
  16. package/dist/lib/__tests__/sse-invalidation-listener.test.js +245 -0
  17. package/dist/lib/__tests__/sse-invalidation-listener.test.js.map +1 -0
  18. package/dist/lib/__tests__/write-queue-manager.test.d.ts +2 -0
  19. package/dist/lib/__tests__/write-queue-manager.test.d.ts.map +1 -0
  20. package/dist/lib/__tests__/write-queue-manager.test.js +291 -0
  21. package/dist/lib/__tests__/write-queue-manager.test.js.map +1 -0
  22. package/dist/lib/cache-manager.d.ts +35 -6
  23. package/dist/lib/cache-manager.d.ts.map +1 -1
  24. package/dist/lib/cache-manager.js +154 -41
  25. package/dist/lib/cache-manager.js.map +1 -1
  26. package/dist/lib/connection-manager.d.ts +7 -0
  27. package/dist/lib/connection-manager.d.ts.map +1 -1
  28. package/dist/lib/connection-manager.js +57 -8
  29. package/dist/lib/connection-manager.js.map +1 -1
  30. package/dist/lib/proxy-server.d.ts +12 -0
  31. package/dist/lib/proxy-server.d.ts.map +1 -1
  32. package/dist/lib/proxy-server.js +184 -87
  33. package/dist/lib/proxy-server.js.map +1 -1
  34. package/dist/lib/qwickbrain-client.d.ts +4 -0
  35. package/dist/lib/qwickbrain-client.d.ts.map +1 -1
  36. package/dist/lib/qwickbrain-client.js +152 -13
  37. package/dist/lib/qwickbrain-client.js.map +1 -1
  38. package/dist/lib/sse-invalidation-listener.d.ts +31 -0
  39. package/dist/lib/sse-invalidation-listener.d.ts.map +1 -0
  40. package/dist/lib/sse-invalidation-listener.js +151 -0
  41. package/dist/lib/sse-invalidation-listener.js.map +1 -0
  42. package/dist/lib/tools.d.ts +21 -0
  43. package/dist/lib/tools.d.ts.map +1 -0
  44. package/dist/lib/tools.js +513 -0
  45. package/dist/lib/tools.js.map +1 -0
  46. package/dist/lib/write-queue-manager.d.ts +88 -0
  47. package/dist/lib/write-queue-manager.d.ts.map +1 -0
  48. package/dist/lib/write-queue-manager.js +191 -0
  49. package/dist/lib/write-queue-manager.js.map +1 -0
  50. package/dist/types/config.d.ts +7 -42
  51. package/dist/types/config.d.ts.map +1 -1
  52. package/dist/types/config.js +1 -6
  53. package/dist/types/config.js.map +1 -1
  54. package/drizzle/0002_lru_cache_migration.sql +94 -0
  55. package/drizzle/meta/_journal.json +7 -0
  56. package/package.json +6 -2
  57. package/scripts/rebuild-sqlite.sh +26 -0
  58. package/src/db/schema.ts +17 -2
  59. package/src/lib/__tests__/cache-manager.test.ts +180 -90
  60. package/src/lib/__tests__/connection-manager.test.ts +2 -2
  61. package/src/lib/__tests__/proxy-server.test.ts +16 -51
  62. package/src/lib/__tests__/qwickbrain-client.test.ts +3 -1
  63. package/src/lib/__tests__/sse-invalidation-listener.test.ts +326 -0
  64. package/src/lib/__tests__/write-queue-manager.test.ts +383 -0
  65. package/src/lib/cache-manager.ts +198 -46
  66. package/src/lib/connection-manager.ts +67 -8
  67. package/src/lib/proxy-server.ts +231 -90
  68. package/src/lib/qwickbrain-client.ts +166 -12
  69. package/src/lib/sse-invalidation-listener.ts +185 -0
  70. package/src/lib/tools.ts +525 -0
  71. package/src/lib/write-queue-manager.ts +271 -0
  72. package/src/types/config.ts +1 -6
  73. package/.github/workflows/publish.yml +0 -92
@@ -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
- // Try to fetch tools from upstream if connected
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 && !cached.isExpired) {
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
- // Try remote if connected
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 stale cache
331
+ // Fall through to error
262
332
  }
263
333
  }
264
334
 
265
- // Try stale cache
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
- // Similar logic to handleGetDocument but for memories
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 && !cached.isExpired) {
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
- if (cached) {
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
- // Clean up expired cache entries on startup
392
- const { documentsDeleted, memoriesDeleted } = await this.cacheManager.cleanupExpiredEntries();
393
- if (documentsDeleted > 0 || memoriesDeleted > 0) {
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 === 'mcp' || this.mode === 'sse') {
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();