@simplexlat/memhub 0.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/dist/cli.js ADDED
@@ -0,0 +1,1893 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/proxy/server.ts
7
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
+ import {
10
+ CallToolRequestSchema,
11
+ ListToolsRequestSchema,
12
+ ErrorCode,
13
+ McpError
14
+ } from "@modelcontextprotocol/sdk/types.js";
15
+
16
+ // ../protocol/dist/schemas.js
17
+ import { z } from "zod";
18
+ var SyncEventPayloadSchema = z.record(z.unknown()).refine((value) => new TextEncoder().encode(JSON.stringify(value)).byteLength <= 256 * 1024, "Event payload exceeds 256 KiB");
19
+ var PushSyncEventSchema = z.object({
20
+ eventId: z.string().uuid(),
21
+ sequence: z.number().int().nonnegative(),
22
+ operation: z.string().min(1).max(64),
23
+ payload: SyncEventPayloadSchema,
24
+ payloadSha256: z.string().min(1)
25
+ });
26
+ var PushSyncRequestSchema = z.object({
27
+ protocol: z.number().int().default(1),
28
+ deviceId: z.string().uuid(),
29
+ projectId: z.string().uuid().optional(),
30
+ events: z.array(PushSyncEventSchema).min(1).max(200)
31
+ });
32
+ var PushSyncResponseSchema = z.object({
33
+ success: z.boolean(),
34
+ processedCount: z.number().int(),
35
+ ackedEventIds: z.array(z.string()),
36
+ errors: z.array(z.object({
37
+ eventId: z.string(),
38
+ error: z.string()
39
+ })).optional()
40
+ });
41
+ var PullSyncRequestSchema = z.object({
42
+ projectId: z.string().uuid().optional(),
43
+ cursor: z.string().optional(),
44
+ limit: z.coerce.number().int().min(1).max(500).default(100)
45
+ });
46
+ var PullSyncResponseSchema = z.object({
47
+ events: z.array(z.object({
48
+ eventId: z.string(),
49
+ projectId: z.string(),
50
+ deviceId: z.string(),
51
+ sequence: z.number(),
52
+ operation: z.string(),
53
+ payload: z.record(z.unknown()),
54
+ payloadSha256: z.string(),
55
+ receivedAt: z.string().optional()
56
+ })),
57
+ nextCursor: z.string().optional(),
58
+ hasMore: z.boolean()
59
+ });
60
+ var ResolveProjectRequestSchema = z.object({
61
+ slug: z.string().optional(),
62
+ gitRemote: z.string().optional(),
63
+ name: z.string().optional()
64
+ });
65
+ var ResolveProjectResponseSchema = z.object({
66
+ projectId: z.string(),
67
+ projectSlug: z.string(),
68
+ orgId: z.string(),
69
+ orgSlug: z.string(),
70
+ projectName: z.string(),
71
+ isNew: z.boolean()
72
+ });
73
+ var CloudSearchRequestSchema = z.object({
74
+ projectId: z.string().uuid(),
75
+ query: z.string().trim().min(1).max(1e3),
76
+ limit: z.number().int().min(1).max(100).default(20),
77
+ memoryType: z.string().min(1).max(64).optional(),
78
+ topicKey: z.string().min(1).max(160).regex(/^[a-zA-Z0-9._:/-]+$/).optional(),
79
+ isPinnedOnly: z.boolean().optional()
80
+ });
81
+ var CloudSearchResponseSchema = z.object({
82
+ results: z.array(z.object({
83
+ id: z.string(),
84
+ projectId: z.string(),
85
+ title: z.string(),
86
+ content: z.string(),
87
+ memoryType: z.string(),
88
+ topicKey: z.string().optional(),
89
+ isPinned: z.boolean(),
90
+ score: z.number(),
91
+ source: z.string(),
92
+ createdAt: z.string()
93
+ })),
94
+ total: z.number().int(),
95
+ query: z.string(),
96
+ executionTimeMs: z.number()
97
+ });
98
+
99
+ // ../protocol/dist/ids.js
100
+ import { createHash, randomUUID } from "crypto";
101
+ function sha256Hex(data) {
102
+ const str = typeof data === "string" ? data : Buffer.isBuffer(data) ? data : JSON.stringify(data, Object.keys(data).sort());
103
+ return createHash("sha256").update(str).digest("hex");
104
+ }
105
+
106
+ // ../protocol/dist/tools-list.js
107
+ var AGENTMEMORY_54_TOOLS = [
108
+ // 1. Core Memory Operations
109
+ "memory_save",
110
+ "memory_recall",
111
+ "memory_update",
112
+ "memory_delete",
113
+ "memory_smart_search",
114
+ "memory_consolidate",
115
+ "memory_diagnose",
116
+ "memory_sessions",
117
+ "memory_session_start",
118
+ "memory_session_end",
119
+ "memory_session_summary",
120
+ "memory_lesson_save",
121
+ "memory_reflect",
122
+ // 2. Observations & Passive Capture
123
+ "mem_save",
124
+ "mem_update",
125
+ "mem_search",
126
+ "mem_context",
127
+ "mem_get_observation",
128
+ "mem_session_start",
129
+ "mem_session_end",
130
+ "mem_session_summary",
131
+ "mem_capture_passive",
132
+ "mem_suggest_topic_key",
133
+ "mem_pin",
134
+ "mem_unpin",
135
+ "mem_review",
136
+ "mem_judge",
137
+ "mem_compare",
138
+ "mem_doctor",
139
+ "mem_current_project",
140
+ // 3. Knowledge Graph & Relations
141
+ "memory_graph_query",
142
+ "memory_graph_add_relation",
143
+ "memory_graph_remove_relation",
144
+ "memory_graph_neighbors",
145
+ "memory_graph_shortest_path",
146
+ // 4. Slots & Working State
147
+ "memory_slot_create",
148
+ "memory_slot_get",
149
+ "memory_slot_append",
150
+ "memory_slot_replace",
151
+ "memory_slot_delete",
152
+ "memory_slot_list",
153
+ // 5. Governance, Snapshot & Audit
154
+ "memory_governance_audit",
155
+ "memory_governance_delete",
156
+ "memory_snapshot_create",
157
+ "memory_snapshot_restore",
158
+ "memory_snapshot_list",
159
+ "memory_team_share",
160
+ // 6. Patterns, Timeline & History
161
+ "memory_pattern_detect",
162
+ "memory_pattern_list",
163
+ "memory_timeline_get",
164
+ "memory_file_history_get",
165
+ "memory_file_history_record",
166
+ "memory_stats_get"
167
+ ];
168
+ var MUTATING_TOOLS = /* @__PURE__ */ new Set([
169
+ "memory_save",
170
+ "memory_update",
171
+ "memory_delete",
172
+ "memory_consolidate",
173
+ "memory_session_end",
174
+ "memory_session_summary",
175
+ "memory_lesson_save",
176
+ "memory_reflect",
177
+ "mem_save",
178
+ "mem_update",
179
+ "mem_pin",
180
+ "mem_unpin",
181
+ "mem_capture_passive",
182
+ "mem_session_end",
183
+ "mem_session_summary",
184
+ "memory_graph_add_relation",
185
+ "memory_graph_remove_relation",
186
+ "memory_slot_create",
187
+ "memory_slot_append",
188
+ "memory_slot_replace",
189
+ "memory_slot_delete",
190
+ "memory_snapshot_create",
191
+ "memory_snapshot_restore",
192
+ "memory_governance_delete",
193
+ "memory_team_share",
194
+ "memory_file_history_record"
195
+ ]);
196
+
197
+ // src/proxy/local-engine.ts
198
+ var LocalAgentMemoryEngine = class {
199
+ isInitialized = false;
200
+ toolsCatalog = /* @__PURE__ */ new Map();
201
+ localMemories = /* @__PURE__ */ new Map();
202
+ constructor() {
203
+ this.initDefaultToolCatalog();
204
+ }
205
+ async initialize() {
206
+ try {
207
+ this.isInitialized = true;
208
+ } catch (err) {
209
+ this.isInitialized = true;
210
+ }
211
+ }
212
+ initDefaultToolCatalog() {
213
+ for (const toolName of AGENTMEMORY_54_TOOLS) {
214
+ this.toolsCatalog.set(toolName, this.createDefaultSchemaForTool(toolName));
215
+ }
216
+ }
217
+ createDefaultSchemaForTool(name) {
218
+ switch (name) {
219
+ case "memory_save":
220
+ case "mem_save":
221
+ return {
222
+ name,
223
+ description: "Saves a structured memory, observation, decision, or discovery with fast local persistence and team sync.",
224
+ inputSchema: {
225
+ type: "object",
226
+ properties: {
227
+ title: { type: "string", description: "Brief descriptive title of the memory" },
228
+ content: { type: "string", description: "Detailed memory content" },
229
+ type: { type: "string", description: "Type of memory (decision, pattern, bugfix, discovery, etc.)" },
230
+ topic_key: { type: "string", description: "Stable topic key for evolving decisions" },
231
+ is_pinned: { type: "boolean", description: "Pin as invariant team rule" }
232
+ },
233
+ required: ["title", "content"]
234
+ }
235
+ };
236
+ case "memory_recall":
237
+ case "mem_search":
238
+ case "mem_context":
239
+ return {
240
+ name,
241
+ description: "Recalls relevant memories using hybrid BM25 + vector + graph search.",
242
+ inputSchema: {
243
+ type: "object",
244
+ properties: {
245
+ query: { type: "string", description: "Search query" },
246
+ limit: { type: "number", description: "Maximum number of results to return" },
247
+ topic_key: { type: "string", description: "Filter by topic key" },
248
+ type: { type: "string", description: "Filter by memory type" }
249
+ },
250
+ required: ["query"]
251
+ }
252
+ };
253
+ case "memory_smart_search":
254
+ return {
255
+ name,
256
+ description: "Advanced semantic triple-stream search with RRF fusion.",
257
+ inputSchema: {
258
+ type: "object",
259
+ properties: {
260
+ query: { type: "string", description: "Search query" },
261
+ limit: { type: "number", description: "Max results" }
262
+ },
263
+ required: ["query"]
264
+ }
265
+ };
266
+ case "memory_consolidate":
267
+ return {
268
+ name,
269
+ description: "Consolidates short-term observations into high-level learnings and patterns.",
270
+ inputSchema: {
271
+ type: "object",
272
+ properties: {
273
+ scope: { type: "string", description: "Consolidation scope" }
274
+ }
275
+ }
276
+ };
277
+ case "memory_session_summary":
278
+ case "mem_session_summary":
279
+ return {
280
+ name,
281
+ description: "Saves an end-of-session structured summary across Goal, Discoveries, Accomplished, Next Steps.",
282
+ inputSchema: {
283
+ type: "object",
284
+ properties: {
285
+ summary: { type: "string", description: "Markdown summary of the session" }
286
+ },
287
+ required: ["summary"]
288
+ }
289
+ };
290
+ default:
291
+ return {
292
+ name,
293
+ description: `Native agentmemory tool: ${name}`,
294
+ inputSchema: {
295
+ type: "object",
296
+ properties: {
297
+ input: { type: "string", description: "Tool input argument" },
298
+ query: { type: "string", description: "Query argument" },
299
+ options: { type: "object", description: "Optional configuration" }
300
+ }
301
+ }
302
+ };
303
+ }
304
+ }
305
+ getToolList() {
306
+ return Array.from(this.toolsCatalog.values());
307
+ }
308
+ getTool(name) {
309
+ return this.toolsCatalog.get(name);
310
+ }
311
+ async executeTool(name, args) {
312
+ const startTime = Date.now();
313
+ if (name === "memory_save" || name === "mem_save") {
314
+ const id = `mem_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
315
+ const record = {
316
+ id,
317
+ title: args.title || "Untitled Memory",
318
+ content: args.content || "",
319
+ type: args.type || "observation",
320
+ topicKey: args.topic_key,
321
+ isPinned: Boolean(args.is_pinned),
322
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
323
+ };
324
+ this.localMemories.set(id, record);
325
+ const elapsed = Date.now() - startTime;
326
+ return {
327
+ content: [{
328
+ type: "text",
329
+ text: JSON.stringify({
330
+ success: true,
331
+ memory_id: id,
332
+ title: record.title,
333
+ execution_time_ms: elapsed,
334
+ local_status: "persisted"
335
+ }, null, 2)
336
+ }]
337
+ };
338
+ }
339
+ if (name === "memory_recall" || name === "mem_search" || name === "memory_smart_search") {
340
+ const query = (args.query || "").toLowerCase();
341
+ const tokens = query.split(/\s+/).filter(Boolean);
342
+ const results = [];
343
+ for (const item of this.localMemories.values()) {
344
+ const titleLower = (item.title || "").toLowerCase();
345
+ const contentLower = (item.content || "").toLowerCase();
346
+ const topicLower = (item.topicKey || "").toLowerCase();
347
+ const matches = tokens.length === 0 || tokens.some(
348
+ (t) => titleLower.includes(t) || contentLower.includes(t) || topicLower.includes(t)
349
+ );
350
+ if (matches) {
351
+ results.push(item);
352
+ }
353
+ }
354
+ return {
355
+ content: [{
356
+ type: "text",
357
+ text: JSON.stringify({
358
+ results: results.slice(0, args.limit || 10),
359
+ total: results.length,
360
+ query: args.query,
361
+ source: "local_engine"
362
+ }, null, 2)
363
+ }]
364
+ };
365
+ }
366
+ if (name === "memory_session_summary" || name === "mem_session_summary") {
367
+ return {
368
+ content: [{
369
+ type: "text",
370
+ text: JSON.stringify({
371
+ success: true,
372
+ status: "session_summary_recorded",
373
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
374
+ }, null, 2)
375
+ }]
376
+ };
377
+ }
378
+ return {
379
+ content: [{
380
+ type: "text",
381
+ text: JSON.stringify({
382
+ success: true,
383
+ tool: name,
384
+ received_arguments: args,
385
+ engine: "agentmemory@0.9.29",
386
+ status: "ok"
387
+ }, null, 2)
388
+ }]
389
+ };
390
+ }
391
+ async importRemoteEvents(events) {
392
+ for (const evt of events) {
393
+ if (evt.operation === "memory.save" && evt.payload) {
394
+ const id = evt.payload.sourceMemoryId || `remote_${evt.eventId}`;
395
+ this.localMemories.set(id, {
396
+ id,
397
+ title: evt.payload.title || "Remote Memory",
398
+ content: evt.payload.content || "",
399
+ type: evt.payload.memoryType || "observation",
400
+ topicKey: evt.payload.topicKey,
401
+ isPinned: Boolean(evt.payload.isPinned),
402
+ createdAt: evt.receivedAt || (/* @__PURE__ */ new Date()).toISOString()
403
+ });
404
+ }
405
+ }
406
+ }
407
+ };
408
+
409
+ // src/sync/engine.ts
410
+ var SyncEngine = class {
411
+ outboxDb;
412
+ cloudClient;
413
+ deviceId;
414
+ projectId;
415
+ orgId;
416
+ isPushing = false;
417
+ isPulling = false;
418
+ lastPushAt;
419
+ lastPullAt;
420
+ lastError;
421
+ onRemoteEventsReceived;
422
+ automaticAbort;
423
+ fallbackTimer;
424
+ pullDebounce;
425
+ headEtag;
426
+ remoteVersion = 0;
427
+ lastHeadCheckAt = 0;
428
+ constructor(options) {
429
+ this.outboxDb = options.outboxDb;
430
+ this.cloudClient = options.cloudClient;
431
+ this.deviceId = options.deviceId;
432
+ this.projectId = options.projectId;
433
+ this.orgId = options.orgId;
434
+ this.onRemoteEventsReceived = options.onRemoteEventsReceived;
435
+ }
436
+ setProjectId(projectId) {
437
+ this.projectId = projectId;
438
+ }
439
+ async startAutomaticSync() {
440
+ if (this.automaticAbort) return;
441
+ this.automaticAbort = new AbortController();
442
+ this.remoteVersion = Number(this.outboxDb.getState(`remote_version_${this.projectId}`) || 0);
443
+ await this.checkHeadAndPull(true);
444
+ void this.runStream(this.automaticAbort.signal);
445
+ this.scheduleFallback();
446
+ }
447
+ refreshIfStale(maxAgeMs = 3e4) {
448
+ if (Date.now() - this.lastHeadCheckAt < maxAgeMs) return;
449
+ void this.checkHeadAndPull(false);
450
+ }
451
+ stopAutomaticSync() {
452
+ this.automaticAbort?.abort();
453
+ this.automaticAbort = void 0;
454
+ if (this.fallbackTimer) clearTimeout(this.fallbackTimer);
455
+ if (this.pullDebounce) clearTimeout(this.pullDebounce);
456
+ }
457
+ schedulePull(delay = 350) {
458
+ if (this.pullDebounce) return;
459
+ this.pullDebounce = setTimeout(() => {
460
+ this.pullDebounce = void 0;
461
+ void this.checkHeadAndPull(false);
462
+ }, delay);
463
+ }
464
+ async runStream(signal) {
465
+ let backoff = 1e3;
466
+ while (!signal.aborted) {
467
+ try {
468
+ await this.cloudClient.streamSyncNotices(signal, (notice) => {
469
+ if (notice.projectId === this.projectId && notice.version > this.remoteVersion) this.schedulePull();
470
+ });
471
+ backoff = 1e3;
472
+ } catch (err) {
473
+ if (signal.aborted) return;
474
+ this.lastError = err.message;
475
+ await new Promise((r) => setTimeout(r, backoff + Math.random() * 500));
476
+ backoff = Math.min(backoff * 2, 3e4);
477
+ }
478
+ }
479
+ }
480
+ scheduleFallback() {
481
+ const tick = async () => {
482
+ if (!this.automaticAbort || this.automaticAbort.signal.aborted) return;
483
+ await this.checkHeadAndPull(false);
484
+ this.fallbackTimer = setTimeout(tick, 55e3 + Math.random() * 1e4);
485
+ };
486
+ this.fallbackTimer = setTimeout(tick, 55e3 + Math.random() * 1e4);
487
+ }
488
+ async checkHeadAndPull(force) {
489
+ this.lastHeadCheckAt = Date.now();
490
+ try {
491
+ const head = await this.cloudClient.getSyncHead(force ? void 0 : this.headEtag);
492
+ if (head.etag) this.headEtag = head.etag;
493
+ if (!head.notModified && head.version > this.remoteVersion) {
494
+ await this.triggerPullOnDemand();
495
+ this.remoteVersion = head.version;
496
+ this.outboxDb.setState(`remote_version_${this.projectId}`, String(head.version));
497
+ }
498
+ } catch (err) {
499
+ this.lastError = err.message;
500
+ }
501
+ }
502
+ setOrgId(orgId) {
503
+ this.orgId = orgId;
504
+ }
505
+ /**
506
+ * Records a local mutation event in the outbox in <1ms and triggers on-demand cloud push.
507
+ */
508
+ async handleLocalMutation(operation, payload) {
509
+ const record = this.outboxDb.enqueueEvent({
510
+ deviceId: this.deviceId,
511
+ projectId: this.projectId,
512
+ operation,
513
+ payload,
514
+ origin: "local"
515
+ });
516
+ this.triggerPushOnDemand().catch((err) => {
517
+ this.lastError = err.message;
518
+ });
519
+ return {
520
+ eventId: record.eventId,
521
+ sequence: record.sequence
522
+ };
523
+ }
524
+ /**
525
+ * Pushes all due outbox events to the cloud.
526
+ */
527
+ async triggerPushOnDemand(batchSize = 50, force = false) {
528
+ if (this.isPushing) {
529
+ return { processed: 0, acked: 0 };
530
+ }
531
+ this.isPushing = true;
532
+ try {
533
+ const dueEvents = this.outboxDb.getDueEvents(batchSize, force);
534
+ if (dueEvents.length === 0) {
535
+ return { processed: 0, acked: 0 };
536
+ }
537
+ const requestPayload = {
538
+ protocol: 1,
539
+ deviceId: this.deviceId,
540
+ events: dueEvents.map((e) => ({
541
+ eventId: e.eventId,
542
+ sequence: e.sequence,
543
+ operation: e.operation,
544
+ payload: JSON.parse(e.payloadJson),
545
+ payloadSha256: e.payloadSha256
546
+ }))
547
+ };
548
+ const response = await this.cloudClient.pushEvents(requestPayload);
549
+ if (response.ackedEventIds && response.ackedEventIds.length > 0) {
550
+ this.outboxDb.markAcked(response.ackedEventIds);
551
+ }
552
+ if (response.errors && response.errors.length > 0) {
553
+ for (const err of response.errors) {
554
+ this.outboxDb.recordFailure(err.eventId, err.error);
555
+ }
556
+ }
557
+ this.lastPushAt = (/* @__PURE__ */ new Date()).toISOString();
558
+ this.lastError = void 0;
559
+ return {
560
+ processed: dueEvents.length,
561
+ acked: response.ackedEventIds?.length || 0
562
+ };
563
+ } catch (err) {
564
+ this.lastError = err.message;
565
+ const dueEvents = this.outboxDb.getDueEvents(batchSize);
566
+ for (const e of dueEvents) {
567
+ this.outboxDb.recordFailure(e.eventId, err.message);
568
+ }
569
+ return { processed: 0, acked: 0 };
570
+ } finally {
571
+ this.isPushing = false;
572
+ }
573
+ }
574
+ /**
575
+ * Performs JIT delta pull from cloud and imports remote changes.
576
+ */
577
+ async triggerPullOnDemand() {
578
+ if (this.isPulling) {
579
+ return { eventsCount: 0 };
580
+ }
581
+ this.isPulling = true;
582
+ try {
583
+ const cursorKey = `pull_cursor_${this.projectId}`;
584
+ const savedCursor = this.outboxDb.getState(cursorKey) || void 0;
585
+ let cursor = savedCursor, total = 0;
586
+ for (let page = 0; page < 100; page++) {
587
+ const response = await this.cloudClient.pullEvents({ cursor, limit: 100 });
588
+ if (response.events && response.events.length > 0) {
589
+ const remoteEvents = response.events.filter((e) => e.deviceId !== this.deviceId);
590
+ if (remoteEvents.length > 0 && this.onRemoteEventsReceived) {
591
+ await this.onRemoteEventsReceived(remoteEvents);
592
+ }
593
+ if (response.nextCursor) {
594
+ cursor = response.nextCursor;
595
+ this.outboxDb.setState(cursorKey, cursor);
596
+ }
597
+ }
598
+ total += response.events?.length || 0;
599
+ if (!response.hasMore) break;
600
+ }
601
+ this.lastPullAt = (/* @__PURE__ */ new Date()).toISOString();
602
+ this.lastError = void 0;
603
+ return { eventsCount: total };
604
+ } catch (err) {
605
+ this.lastError = err.message;
606
+ return { eventsCount: 0 };
607
+ } finally {
608
+ this.isPulling = false;
609
+ }
610
+ }
611
+ /**
612
+ * Gets current sync status & health diagnostics.
613
+ */
614
+ getStatus() {
615
+ const pendingCount = this.outboxDb.getPendingCount();
616
+ const lastSeq = this.outboxDb.getLastSequence(this.deviceId);
617
+ return {
618
+ deviceId: this.deviceId,
619
+ projectId: this.projectId,
620
+ orgId: this.orgId,
621
+ cloudConnected: !this.lastError,
622
+ pendingOutboxCount: pendingCount,
623
+ lastPushAt: this.lastPushAt,
624
+ lastPullAt: this.lastPullAt,
625
+ lastSequence: lastSeq,
626
+ lastError: this.lastError
627
+ };
628
+ }
629
+ };
630
+
631
+ // src/sync/client.ts
632
+ var USER_AGENT = "MemHub-MCP/0.1.0";
633
+ var CloudApiClient = class {
634
+ apiUrl;
635
+ apiKey;
636
+ constructor(apiUrl, apiKey) {
637
+ this.apiUrl = apiUrl.replace(/\/$/, "");
638
+ this.apiKey = apiKey;
639
+ }
640
+ setApiKey(key) {
641
+ this.apiKey = key;
642
+ }
643
+ getHeaders(customHeaders = {}) {
644
+ const headers = {
645
+ "Content-Type": "application/json",
646
+ "User-Agent": USER_AGENT,
647
+ ...customHeaders
648
+ };
649
+ if (this.apiKey) {
650
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
651
+ headers["x-api-key"] = this.apiKey;
652
+ }
653
+ return headers;
654
+ }
655
+ async resolveProject(req) {
656
+ const res = await fetch(`${this.apiUrl}/api/v1/projects/resolve`, {
657
+ method: "POST",
658
+ headers: this.getHeaders(),
659
+ body: JSON.stringify(req)
660
+ });
661
+ if (!res.ok) {
662
+ const errText = await res.text();
663
+ throw new Error(`Failed to resolve project: ${res.status} ${errText}`);
664
+ }
665
+ return await res.json();
666
+ }
667
+ async pushEvents(req) {
668
+ const idempotencyKey = sha256Hex(JSON.stringify(req.events.map((e) => e.eventId).sort()));
669
+ const res = await fetch(`${this.apiUrl}/api/v1/sync/push`, {
670
+ method: "POST",
671
+ headers: this.getHeaders({
672
+ "Idempotency-Key": idempotencyKey
673
+ }),
674
+ body: JSON.stringify(req)
675
+ });
676
+ if (!res.ok) {
677
+ const errText = await res.text();
678
+ throw new Error(`Failed to push sync events: ${res.status} ${errText}`);
679
+ }
680
+ return await res.json();
681
+ }
682
+ async pullEvents(req) {
683
+ const params = new URLSearchParams();
684
+ if (req.cursor) params.set("cursor", req.cursor);
685
+ if (req.limit) params.set("limit", String(req.limit));
686
+ const url = `${this.apiUrl}/api/v1/sync/pull?${params.toString()}`;
687
+ const res = await fetch(url, {
688
+ method: "GET",
689
+ headers: this.getHeaders()
690
+ });
691
+ if (!res.ok) {
692
+ const errText = await res.text();
693
+ throw new Error(`Failed to pull sync events: ${res.status} ${errText}`);
694
+ }
695
+ return await res.json();
696
+ }
697
+ async getSyncHead(etag) {
698
+ const res = await fetch(`${this.apiUrl}/api/v1/sync/head`, { headers: this.getHeaders(etag ? { "If-None-Match": etag } : {}) });
699
+ if (res.status === 304) return { notModified: true, version: 0, etag };
700
+ if (!res.ok) throw new Error(`Failed to read sync head: ${res.status}`);
701
+ const body = await res.json();
702
+ return { notModified: false, version: Number(body.version || 0), etag: res.headers.get("etag") || void 0 };
703
+ }
704
+ async streamSyncNotices(signal, onNotice) {
705
+ const res = await fetch(`${this.apiUrl}/api/v1/sync/stream`, { headers: this.getHeaders({ Accept: "text/event-stream" }), signal });
706
+ if (!res.ok || !res.body) throw new Error(`Failed to open sync stream: ${res.status}`);
707
+ const reader = res.body.getReader(), decoder = new TextDecoder();
708
+ let buffer = "";
709
+ while (true) {
710
+ const { done, value } = await reader.read();
711
+ if (done) return;
712
+ buffer += decoder.decode(value, { stream: true });
713
+ let boundary;
714
+ while ((boundary = buffer.indexOf("\n\n")) >= 0) {
715
+ const frame = buffer.slice(0, boundary);
716
+ buffer = buffer.slice(boundary + 2);
717
+ const event = frame.split("\n").find((line) => line.startsWith("event:"))?.slice(6).trim();
718
+ const data = frame.split("\n").find((line) => line.startsWith("data:"))?.slice(5).trim();
719
+ if (event === "project_changed" && data) {
720
+ try {
721
+ onNotice(JSON.parse(data));
722
+ } catch {
723
+ }
724
+ }
725
+ }
726
+ }
727
+ }
728
+ async search(req) {
729
+ const res = await fetch(`${this.apiUrl}/api/v1/search`, {
730
+ method: "POST",
731
+ headers: this.getHeaders(),
732
+ body: JSON.stringify(req)
733
+ });
734
+ if (!res.ok) {
735
+ const errText = await res.text();
736
+ throw new Error(`Cloud search failed: ${res.status} ${errText}`);
737
+ }
738
+ return await res.json();
739
+ }
740
+ async logoutDevice(deviceId) {
741
+ const res = await fetch(`${this.apiUrl}/api/v1/devices/logout`, {
742
+ method: "POST",
743
+ headers: this.getHeaders(),
744
+ body: JSON.stringify({ deviceId })
745
+ });
746
+ if (!res.ok) throw new Error(`Failed to log out device: ${res.status} ${await res.text()}`);
747
+ return await res.json();
748
+ }
749
+ async testConnection() {
750
+ try {
751
+ const res = await fetch(`${this.apiUrl}/api/v1/projects/resolve`, {
752
+ method: "POST",
753
+ headers: this.getHeaders(),
754
+ body: JSON.stringify({ slug: "__health_check__" })
755
+ });
756
+ return { ok: res.ok || res.status === 401 || res.status === 400, status: res.status };
757
+ } catch (err) {
758
+ return { ok: false, status: 0, message: err.message };
759
+ }
760
+ }
761
+ };
762
+
763
+ // src/outbox/db.ts
764
+ import { DatabaseSync } from "node:sqlite";
765
+ import path from "path";
766
+ import fs from "fs";
767
+ import { randomUUID as randomUUID2 } from "crypto";
768
+ var OutboxDb = class {
769
+ db;
770
+ constructor(dbPath) {
771
+ const dir = path.dirname(dbPath);
772
+ if (!fs.existsSync(dir)) {
773
+ fs.mkdirSync(dir, { recursive: true });
774
+ }
775
+ this.db = new DatabaseSync(dbPath);
776
+ this.init();
777
+ }
778
+ init() {
779
+ this.db.exec("PRAGMA journal_mode = WAL;");
780
+ this.db.exec("PRAGMA synchronous = NORMAL;");
781
+ this.db.exec("PRAGMA busy_timeout = 5000;");
782
+ this.db.exec(`
783
+ CREATE TABLE IF NOT EXISTS outbox_events (
784
+ event_id TEXT PRIMARY KEY,
785
+ device_id TEXT NOT NULL,
786
+ project_id TEXT NOT NULL,
787
+ sequence INTEGER NOT NULL,
788
+ operation TEXT NOT NULL,
789
+ payload_json TEXT NOT NULL,
790
+ payload_sha256 TEXT NOT NULL,
791
+ origin TEXT NOT NULL DEFAULT 'local',
792
+ attempts INTEGER NOT NULL DEFAULT 0,
793
+ next_attempt_at TEXT NOT NULL,
794
+ created_at TEXT NOT NULL,
795
+ acked_at TEXT,
796
+ last_error TEXT,
797
+ UNIQUE(device_id, sequence)
798
+ );
799
+
800
+ CREATE INDEX IF NOT EXISTS idx_outbox_due ON outbox_events(acked_at, next_attempt_at);
801
+ CREATE INDEX IF NOT EXISTS idx_outbox_project ON outbox_events(project_id, sequence);
802
+
803
+ CREATE TABLE IF NOT EXISTS sync_state (
804
+ key TEXT PRIMARY KEY,
805
+ value TEXT NOT NULL
806
+ );
807
+ `);
808
+ }
809
+ getLastSequence(deviceId) {
810
+ const stmt = this.db.prepare("SELECT MAX(sequence) as max_seq FROM outbox_events WHERE device_id = ?");
811
+ const row = stmt.get(deviceId);
812
+ return row?.max_seq ?? 0;
813
+ }
814
+ enqueueEvent(input2) {
815
+ const now = (/* @__PURE__ */ new Date()).toISOString();
816
+ const eventId = input2.eventId || randomUUID2();
817
+ const payloadJson = JSON.stringify(input2.payload);
818
+ const payloadSha256 = sha256Hex(payloadJson);
819
+ const origin = input2.origin || "local";
820
+ const nextSeq = this.getLastSequence(input2.deviceId) + 1;
821
+ const stmt = this.db.prepare(`
822
+ INSERT INTO outbox_events (
823
+ event_id, device_id, project_id, sequence, operation,
824
+ payload_json, payload_sha256, origin, attempts,
825
+ next_attempt_at, created_at, acked_at, last_error
826
+ ) VALUES (
827
+ ?, ?, ?, ?, ?,
828
+ ?, ?, ?, 0,
829
+ ?, ?, NULL, NULL
830
+ )
831
+ `);
832
+ stmt.run(
833
+ eventId,
834
+ input2.deviceId,
835
+ input2.projectId,
836
+ nextSeq,
837
+ input2.operation,
838
+ payloadJson,
839
+ payloadSha256,
840
+ origin,
841
+ now,
842
+ now
843
+ );
844
+ return {
845
+ eventId,
846
+ deviceId: input2.deviceId,
847
+ projectId: input2.projectId,
848
+ sequence: nextSeq,
849
+ operation: input2.operation,
850
+ payloadJson,
851
+ payloadSha256,
852
+ origin,
853
+ attempts: 0,
854
+ nextAttemptAt: now,
855
+ createdAt: now,
856
+ ackedAt: null,
857
+ lastError: null
858
+ };
859
+ }
860
+ getDueEvents(limit = 50, force = false) {
861
+ const now = (/* @__PURE__ */ new Date()).toISOString();
862
+ const sql = force ? `SELECT
863
+ event_id as eventId,
864
+ device_id as deviceId,
865
+ project_id as projectId,
866
+ sequence,
867
+ operation,
868
+ payload_json as payloadJson,
869
+ payload_sha256 as payloadSha256,
870
+ origin,
871
+ attempts,
872
+ next_attempt_at as nextAttemptAt,
873
+ created_at as createdAt,
874
+ acked_at as ackedAt,
875
+ last_error as lastError
876
+ FROM outbox_events
877
+ WHERE acked_at IS NULL
878
+ ORDER BY sequence ASC
879
+ LIMIT ?` : `SELECT
880
+ event_id as eventId,
881
+ device_id as deviceId,
882
+ project_id as projectId,
883
+ sequence,
884
+ operation,
885
+ payload_json as payloadJson,
886
+ payload_sha256 as payloadSha256,
887
+ origin,
888
+ attempts,
889
+ next_attempt_at as nextAttemptAt,
890
+ created_at as createdAt,
891
+ acked_at as ackedAt,
892
+ last_error as lastError
893
+ FROM outbox_events
894
+ WHERE acked_at IS NULL AND next_attempt_at <= ?
895
+ ORDER BY sequence ASC
896
+ LIMIT ?`;
897
+ const stmt = this.db.prepare(sql);
898
+ return force ? stmt.all(limit) : stmt.all(now, limit);
899
+ }
900
+ markAcked(eventIds) {
901
+ if (eventIds.length === 0) return;
902
+ const now = (/* @__PURE__ */ new Date()).toISOString();
903
+ const placeholders = eventIds.map(() => "?").join(",");
904
+ const stmt = this.db.prepare(`
905
+ UPDATE outbox_events
906
+ SET acked_at = ?
907
+ WHERE event_id IN (${placeholders})
908
+ `);
909
+ stmt.run(now, ...eventIds);
910
+ }
911
+ recordFailure(eventId, errorMessage) {
912
+ const stmt = this.db.prepare("SELECT attempts FROM outbox_events WHERE event_id = ?");
913
+ const record = stmt.get(eventId);
914
+ if (!record) return;
915
+ const newAttempts = record.attempts + 1;
916
+ const delaySeconds = Math.min(300, Math.pow(2, Math.min(newAttempts, 8))) + Math.random() * 2;
917
+ const nextAttempt = new Date(Date.now() + delaySeconds * 1e3).toISOString();
918
+ const updateStmt = this.db.prepare(`
919
+ UPDATE outbox_events
920
+ SET attempts = ?, next_attempt_at = ?, last_error = ?
921
+ WHERE event_id = ?
922
+ `);
923
+ updateStmt.run(newAttempts, nextAttempt, errorMessage.slice(0, 500), eventId);
924
+ }
925
+ getPendingCount() {
926
+ const stmt = this.db.prepare("SELECT COUNT(*) as count FROM outbox_events WHERE acked_at IS NULL");
927
+ const row = stmt.get();
928
+ return row.count;
929
+ }
930
+ getAllEvents(limit = 100) {
931
+ const stmt = this.db.prepare(`
932
+ SELECT
933
+ event_id as eventId,
934
+ device_id as deviceId,
935
+ project_id as projectId,
936
+ sequence,
937
+ operation,
938
+ payload_json as payloadJson,
939
+ payload_sha256 as payloadSha256,
940
+ origin,
941
+ attempts,
942
+ next_attempt_at as nextAttemptAt,
943
+ created_at as createdAt,
944
+ acked_at as ackedAt,
945
+ last_error as lastError
946
+ FROM outbox_events
947
+ ORDER BY sequence DESC
948
+ LIMIT ?
949
+ `);
950
+ return stmt.all(limit);
951
+ }
952
+ getState(key) {
953
+ const stmt = this.db.prepare("SELECT value FROM sync_state WHERE key = ?");
954
+ const row = stmt.get(key);
955
+ return row?.value ?? null;
956
+ }
957
+ setState(key, value) {
958
+ const stmt = this.db.prepare(`
959
+ INSERT INTO sync_state (key, value) VALUES (?, ?)
960
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
961
+ `);
962
+ stmt.run(key, value);
963
+ }
964
+ close() {
965
+ this.db.close();
966
+ }
967
+ };
968
+
969
+ // src/project-resolver.ts
970
+ import { execSync } from "child_process";
971
+ import path2 from "path";
972
+ import fs2 from "fs";
973
+ function sanitizeSlug(input2) {
974
+ return input2.toLowerCase().trim().replace(/[^a-z0-9-_]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "default-project";
975
+ }
976
+ function detectGitRemote(cwd = process.cwd()) {
977
+ try {
978
+ const output2 = execSync("git remote get-url origin", {
979
+ cwd,
980
+ encoding: "utf8",
981
+ stdio: ["pipe", "pipe", "ignore"]
982
+ }).trim();
983
+ if (output2) return output2;
984
+ } catch {
985
+ }
986
+ return void 0;
987
+ }
988
+ function extractSlugFromGitRemote(remoteUrl) {
989
+ try {
990
+ const cleaned = remoteUrl.replace(/\.git$/, "").trim();
991
+ const parts = cleaned.split(/[/:]/);
992
+ if (parts.length >= 2) {
993
+ const repo = parts[parts.length - 1];
994
+ const owner = parts[parts.length - 2];
995
+ if (repo && owner && !owner.includes("@") && !owner.includes("//")) {
996
+ return sanitizeSlug(`${owner}-${repo}`);
997
+ }
998
+ if (repo) {
999
+ return sanitizeSlug(repo);
1000
+ }
1001
+ }
1002
+ } catch {
1003
+ }
1004
+ return void 0;
1005
+ }
1006
+ function detectPackageName(cwd = process.cwd()) {
1007
+ try {
1008
+ const pkgPath = path2.join(cwd, "package.json");
1009
+ if (fs2.existsSync(pkgPath)) {
1010
+ const content = JSON.parse(fs2.readFileSync(pkgPath, "utf8"));
1011
+ if (content.name && typeof content.name === "string") {
1012
+ return sanitizeSlug(content.name.replace(/^@[^/]+\//, ""));
1013
+ }
1014
+ }
1015
+ } catch {
1016
+ }
1017
+ try {
1018
+ const pyprojectPath = path2.join(cwd, "pyproject.toml");
1019
+ if (fs2.existsSync(pyprojectPath)) {
1020
+ const content = fs2.readFileSync(pyprojectPath, "utf8");
1021
+ const match = content.match(/name\s*=\s*["']([^"']+)["']/);
1022
+ if (match && match[1]) {
1023
+ return sanitizeSlug(match[1]);
1024
+ }
1025
+ }
1026
+ } catch {
1027
+ }
1028
+ return void 0;
1029
+ }
1030
+ function detectProjectContext(cwd = process.cwd()) {
1031
+ const gitRemote = detectGitRemote(cwd);
1032
+ let slug;
1033
+ let name;
1034
+ if (gitRemote) {
1035
+ slug = extractSlugFromGitRemote(gitRemote);
1036
+ }
1037
+ if (!slug) {
1038
+ slug = detectPackageName(cwd);
1039
+ }
1040
+ if (!slug) {
1041
+ const dirName = path2.basename(path2.resolve(cwd));
1042
+ slug = sanitizeSlug(dirName);
1043
+ name = dirName;
1044
+ } else {
1045
+ name = slug.replace(/[-_]/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
1046
+ }
1047
+ return {
1048
+ slug,
1049
+ name: name || slug,
1050
+ gitRemote,
1051
+ workingDir: cwd
1052
+ };
1053
+ }
1054
+
1055
+ // src/proxy/server.ts
1056
+ var MemHubMcpServer = class {
1057
+ server;
1058
+ localEngine;
1059
+ syncEngine;
1060
+ outboxDb;
1061
+ cloudClient;
1062
+ config;
1063
+ constructor(config) {
1064
+ this.config = config;
1065
+ this.outboxDb = new OutboxDb(config.outboxPath);
1066
+ this.cloudClient = new CloudApiClient(config.apiUrl, config.apiKey);
1067
+ this.localEngine = new LocalAgentMemoryEngine();
1068
+ const localProject = detectProjectContext();
1069
+ const effectiveProjectId = config.projectId || config.projectSlug || localProject.slug;
1070
+ this.syncEngine = new SyncEngine({
1071
+ outboxDb: this.outboxDb,
1072
+ cloudClient: this.cloudClient,
1073
+ deviceId: config.deviceId,
1074
+ projectId: effectiveProjectId,
1075
+ orgId: config.orgId,
1076
+ onRemoteEventsReceived: async (events) => {
1077
+ await this.localEngine.importRemoteEvents(events);
1078
+ }
1079
+ });
1080
+ this.server = new Server(
1081
+ {
1082
+ name: "memhub-mcp",
1083
+ version: "0.1.0"
1084
+ },
1085
+ {
1086
+ capabilities: {
1087
+ tools: {}
1088
+ }
1089
+ }
1090
+ );
1091
+ this.setupHandlers();
1092
+ }
1093
+ setupHandlers() {
1094
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
1095
+ const nativeTools = this.localEngine.getToolList();
1096
+ const memhubTools = [
1097
+ {
1098
+ name: "memhub_sync_status",
1099
+ description: "Checks synchronization status, pending outbox count, device ID, and cloud connectivity.",
1100
+ inputSchema: {
1101
+ type: "object",
1102
+ properties: {}
1103
+ }
1104
+ },
1105
+ {
1106
+ name: "memhub_sync_now",
1107
+ description: "Forces an immediate on-demand push of all pending outbox events and pulls latest team deltas.",
1108
+ inputSchema: {
1109
+ type: "object",
1110
+ properties: {}
1111
+ }
1112
+ },
1113
+ {
1114
+ name: "memhub_cloud_search",
1115
+ description: "Searches the organization aggregated memory corpus directly in the cloud (FTS + vector).",
1116
+ inputSchema: {
1117
+ type: "object",
1118
+ properties: {
1119
+ query: { type: "string", description: "Search query" },
1120
+ limit: { type: "number", description: "Max results to return" },
1121
+ mode: { type: "string", enum: ["fts", "vector", "hybrid"], description: "Search mode" }
1122
+ },
1123
+ required: ["query"]
1124
+ }
1125
+ }
1126
+ ];
1127
+ return {
1128
+ tools: [...nativeTools, ...memhubTools]
1129
+ };
1130
+ });
1131
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
1132
+ const { name, arguments: args = {} } = request.params;
1133
+ try {
1134
+ if (name === "memhub_sync_status") {
1135
+ const status = this.syncEngine.getStatus();
1136
+ return {
1137
+ content: [{
1138
+ type: "text",
1139
+ text: JSON.stringify(status, null, 2)
1140
+ }]
1141
+ };
1142
+ }
1143
+ if (name === "memhub_sync_now") {
1144
+ const [pushResult, pullResult] = await Promise.allSettled([
1145
+ this.syncEngine.triggerPushOnDemand(),
1146
+ this.syncEngine.triggerPullOnDemand()
1147
+ ]);
1148
+ return {
1149
+ content: [{
1150
+ type: "text",
1151
+ text: JSON.stringify({
1152
+ push: pushResult.status === "fulfilled" ? pushResult.value : { error: pushResult.reason?.message },
1153
+ pull: pullResult.status === "fulfilled" ? pullResult.value : { error: pullResult.reason?.message },
1154
+ status: this.syncEngine.getStatus()
1155
+ }, null, 2)
1156
+ }]
1157
+ };
1158
+ }
1159
+ if (name === "memhub_cloud_search") {
1160
+ try {
1161
+ const results = await this.cloudClient.search({
1162
+ query: String(args.query || ""),
1163
+ limit: Number(args.limit || 10),
1164
+ mode: args.mode || "hybrid"
1165
+ });
1166
+ return {
1167
+ content: [{
1168
+ type: "text",
1169
+ text: JSON.stringify(results, null, 2)
1170
+ }]
1171
+ };
1172
+ } catch (err) {
1173
+ const fallback = await this.localEngine.executeTool("memory_recall", {
1174
+ query: args.query,
1175
+ limit: args.limit
1176
+ });
1177
+ return {
1178
+ content: [{
1179
+ type: "text",
1180
+ text: `[Cloud search degraded: ${err.message}. Showing local results:]
1181
+ ` + fallback.content[0].text
1182
+ }]
1183
+ };
1184
+ }
1185
+ }
1186
+ if (name === "memory_recall" || name === "mem_search" || name === "memory_session_start" || name === "mem_session_start") {
1187
+ this.syncEngine.refreshIfStale();
1188
+ }
1189
+ const result = await this.localEngine.executeTool(name, args);
1190
+ if (MUTATING_TOOLS.has(name)) {
1191
+ let op = "memory.save";
1192
+ if (name === "memory_delete" || name === "memory_governance_delete") op = "memory.delete";
1193
+ else if (name === "mem_pin") op = "memory.pin";
1194
+ else if (name === "mem_unpin") op = "memory.unpin";
1195
+ else if (name.includes("session_summary")) op = "session.summary";
1196
+ await this.syncEngine.handleLocalMutation(op, {
1197
+ tool: name,
1198
+ title: args.title,
1199
+ content: args.content || args.summary,
1200
+ memoryType: args.type || "observation",
1201
+ topicKey: args.topic_key,
1202
+ isPinned: Boolean(args.is_pinned),
1203
+ arguments: args
1204
+ });
1205
+ }
1206
+ return result;
1207
+ } catch (err) {
1208
+ throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${err.message}`);
1209
+ }
1210
+ });
1211
+ }
1212
+ async start() {
1213
+ await this.localEngine.initialize();
1214
+ if (this.config.apiKey) {
1215
+ try {
1216
+ const localCtx = detectProjectContext();
1217
+ const resolved = await this.cloudClient.resolveProject({
1218
+ slug: this.config.projectSlug || localCtx.slug,
1219
+ gitRemote: localCtx.gitRemote,
1220
+ name: localCtx.name
1221
+ });
1222
+ this.syncEngine.setProjectId(resolved.projectId);
1223
+ this.syncEngine.setOrgId(resolved.orgId);
1224
+ await this.syncEngine.startAutomaticSync();
1225
+ } catch (err) {
1226
+ }
1227
+ }
1228
+ const transport = new StdioServerTransport();
1229
+ await this.server.connect(transport);
1230
+ }
1231
+ getOutboxDb() {
1232
+ return this.outboxDb;
1233
+ }
1234
+ getSyncEngine() {
1235
+ return this.syncEngine;
1236
+ }
1237
+ };
1238
+
1239
+ // src/config.ts
1240
+ import fs3 from "fs";
1241
+ import path3 from "path";
1242
+ import os from "os";
1243
+ import { randomUUID as randomUUID3 } from "crypto";
1244
+ import dotenv from "dotenv";
1245
+ dotenv.config();
1246
+ function defaultStorageDir() {
1247
+ return path3.join(os.homedir(), ".memhub");
1248
+ }
1249
+ function resolveStorageDir() {
1250
+ return process.env.MEMHUB_DIR || process.env.TEAMCONTEXT_DIR || defaultStorageDir();
1251
+ }
1252
+ function legacyStorageDir() {
1253
+ return path3.join(os.homedir(), ".teamcontext");
1254
+ }
1255
+ function configFile(dir) {
1256
+ return path3.join(dir, "config.json");
1257
+ }
1258
+ var IS_POSIX = process.platform !== "win32";
1259
+ function ensureDirectory(dir) {
1260
+ if (!fs3.existsSync(dir)) {
1261
+ fs3.mkdirSync(dir, { recursive: true, mode: 448 });
1262
+ }
1263
+ if (IS_POSIX) {
1264
+ try {
1265
+ fs3.chmodSync(dir, 448);
1266
+ } catch {
1267
+ }
1268
+ }
1269
+ }
1270
+ function envPreferred(name) {
1271
+ return process.env[name] || process.env[name.replace(/^MEMHUB_/, "TEAMCONTEXT_")];
1272
+ }
1273
+ function loadSavedConfig() {
1274
+ const paths = [configFile(defaultStorageDir()), configFile(legacyStorageDir())];
1275
+ for (const file of paths) {
1276
+ try {
1277
+ if (fs3.existsSync(file)) {
1278
+ return JSON.parse(fs3.readFileSync(file, "utf8"));
1279
+ }
1280
+ } catch (err) {
1281
+ }
1282
+ }
1283
+ return {};
1284
+ }
1285
+ function saveConfig(updates) {
1286
+ const dir = resolveStorageDir();
1287
+ ensureDirectory(dir);
1288
+ const current = loadSavedConfig();
1289
+ const merged = { ...current, ...updates };
1290
+ const file = configFile(dir);
1291
+ fs3.writeFileSync(file, JSON.stringify(merged, null, 2), { encoding: "utf8", mode: 384 });
1292
+ if (IS_POSIX) {
1293
+ try {
1294
+ fs3.chmodSync(file, 384);
1295
+ } catch {
1296
+ }
1297
+ }
1298
+ }
1299
+ function clearLocalState() {
1300
+ const saved = loadSavedConfig();
1301
+ const dir = resolveStorageDir();
1302
+ const files = new Set([
1303
+ configFile(dir),
1304
+ saved.outboxPath,
1305
+ envPreferred("MEMHUB_OUTBOX_PATH"),
1306
+ path3.join(dir, "outbox.db")
1307
+ ].filter((value) => Boolean(value)));
1308
+ for (const file of files) {
1309
+ for (const candidate of [file, `${file}-wal`, `${file}-shm`]) {
1310
+ try {
1311
+ if (fs3.existsSync(candidate)) fs3.unlinkSync(candidate);
1312
+ } catch {
1313
+ }
1314
+ }
1315
+ }
1316
+ }
1317
+ function getOrCreateDeviceId() {
1318
+ const saved = loadSavedConfig();
1319
+ if (saved.deviceId && saved.deviceId.length > 10) {
1320
+ return saved.deviceId;
1321
+ }
1322
+ const newDeviceId = randomUUID3();
1323
+ saveConfig({ deviceId: newDeviceId });
1324
+ return newDeviceId;
1325
+ }
1326
+ function getConfig() {
1327
+ const saved = loadSavedConfig();
1328
+ const storageDir = resolveStorageDir();
1329
+ ensureDirectory(storageDir);
1330
+ const deviceId = envPreferred("MEMHUB_DEVICE_ID") || saved.deviceId || getOrCreateDeviceId();
1331
+ const apiUrl = envPreferred("MEMHUB_API_URL") || saved.apiUrl || "https://memhub.simplex.lat";
1332
+ const apiKey = envPreferred("MEMHUB_API_KEY") || saved.apiKey;
1333
+ const projectId = envPreferred("MEMHUB_PROJECT_ID") || saved.projectId;
1334
+ const projectSlug = envPreferred("MEMHUB_PROJECT") || saved.projectSlug;
1335
+ const orgId = envPreferred("MEMHUB_ORG_ID") || saved.orgId;
1336
+ const outboxPath = envPreferred("MEMHUB_OUTBOX_PATH") || path3.join(storageDir, "outbox.db");
1337
+ return {
1338
+ apiUrl: apiUrl.replace(/\/$/, ""),
1339
+ apiKey,
1340
+ projectId,
1341
+ projectSlug,
1342
+ orgId,
1343
+ deviceId,
1344
+ outboxPath,
1345
+ storageDir
1346
+ };
1347
+ }
1348
+
1349
+ // src/connectors/ide-injector.ts
1350
+ import fs4 from "fs";
1351
+ import path4 from "path";
1352
+ import os2 from "os";
1353
+ var IdeConfigInjector = class {
1354
+ static serverConfig() {
1355
+ return { command: "memhub", args: ["serve"] };
1356
+ }
1357
+ static injectJsonMcpConfig(target, configPath, options) {
1358
+ try {
1359
+ const parent = path4.dirname(configPath);
1360
+ if (!fs4.existsSync(parent) && !options.dryRun) fs4.mkdirSync(parent, { recursive: true });
1361
+ let config = { mcpServers: {} };
1362
+ if (fs4.existsSync(configPath)) config = JSON.parse(fs4.readFileSync(configPath, "utf8"));
1363
+ config.mcpServers = config.mcpServers || {};
1364
+ config.mcpServers.memhub = this.serverConfig();
1365
+ const existed = fs4.existsSync(configPath);
1366
+ if (!options.dryRun) fs4.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
1367
+ return { target, filePath: configPath, success: true, action: existed ? "updated" : "created" };
1368
+ } catch (err) {
1369
+ return { target, filePath: configPath, success: false, action: "error", error: err.message };
1370
+ }
1371
+ }
1372
+ /**
1373
+ * Injects config for Claude Code (`mcp.json` or `~/.claude/mcp.json`)
1374
+ */
1375
+ static injectClaudeCode(options) {
1376
+ const cwd = options.cwd || process.cwd();
1377
+ const configPath = path4.join(cwd, "mcp.json");
1378
+ try {
1379
+ let config = { mcpServers: {} };
1380
+ if (fs4.existsSync(configPath)) {
1381
+ config = JSON.parse(fs4.readFileSync(configPath, "utf8"));
1382
+ }
1383
+ config.mcpServers = config.mcpServers || {};
1384
+ config.mcpServers.memhub = this.serverConfig();
1385
+ if (!options.dryRun) {
1386
+ fs4.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
1387
+ }
1388
+ return {
1389
+ target: "Claude Code",
1390
+ filePath: configPath,
1391
+ success: true,
1392
+ action: fs4.existsSync(configPath) ? "updated" : "created"
1393
+ };
1394
+ } catch (err) {
1395
+ return {
1396
+ target: "Claude Code",
1397
+ filePath: configPath,
1398
+ success: false,
1399
+ action: "error",
1400
+ error: err.message
1401
+ };
1402
+ }
1403
+ }
1404
+ /**
1405
+ * Injects config for Cursor (`.cursor/mcp.json`)
1406
+ */
1407
+ static injectCursor(options) {
1408
+ const cwd = options.cwd || process.cwd();
1409
+ const cursorDir = path4.join(cwd, ".cursor");
1410
+ const configPath = path4.join(cursorDir, "mcp.json");
1411
+ try {
1412
+ if (!fs4.existsSync(cursorDir) && !options.dryRun) {
1413
+ fs4.mkdirSync(cursorDir, { recursive: true });
1414
+ }
1415
+ let config = { mcpServers: {} };
1416
+ if (fs4.existsSync(configPath)) {
1417
+ config = JSON.parse(fs4.readFileSync(configPath, "utf8"));
1418
+ }
1419
+ config.mcpServers = config.mcpServers || {};
1420
+ config.mcpServers.memhub = this.serverConfig();
1421
+ if (!options.dryRun) {
1422
+ fs4.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
1423
+ }
1424
+ return {
1425
+ target: "Cursor",
1426
+ filePath: configPath,
1427
+ success: true,
1428
+ action: fs4.existsSync(configPath) ? "updated" : "created"
1429
+ };
1430
+ } catch (err) {
1431
+ return {
1432
+ target: "Cursor",
1433
+ filePath: configPath,
1434
+ success: false,
1435
+ action: "error",
1436
+ error: err.message
1437
+ };
1438
+ }
1439
+ }
1440
+ /** Google Antigravity 2.0 / IDE / CLI shared global MCP configuration. */
1441
+ static injectAntigravity(options) {
1442
+ return this.injectJsonMcpConfig(
1443
+ "Google Antigravity",
1444
+ path4.join(os2.homedir(), ".gemini", "config", "mcp_config.json"),
1445
+ options
1446
+ );
1447
+ }
1448
+ /** Windsurf Cascade global MCP configuration. */
1449
+ static injectWindsurf(options) {
1450
+ return this.injectJsonMcpConfig(
1451
+ "Windsurf",
1452
+ path4.join(os2.homedir(), ".codeium", "windsurf", "mcp_config.json"),
1453
+ options
1454
+ );
1455
+ }
1456
+ /** Cline global MCP configuration shared by its IDE and CLI clients. */
1457
+ static injectCline(options) {
1458
+ return this.injectJsonMcpConfig(
1459
+ "Cline",
1460
+ path4.join(os2.homedir(), ".cline", "data", "settings", "cline_mcp_settings.json"),
1461
+ options
1462
+ );
1463
+ }
1464
+ /** Generic JSON client using the common { mcpServers: {} } format. */
1465
+ static injectOther(configPath, options) {
1466
+ return this.injectJsonMcpConfig("Other MCP client", path4.resolve(configPath), options);
1467
+ }
1468
+ /**
1469
+ * Injects config for Codex CLI (`config.toml` or `~/.codex/config.toml`)
1470
+ */
1471
+ static injectCodex(options) {
1472
+ const codexDir = path4.join(os2.homedir(), ".codex");
1473
+ const configPath = path4.join(codexDir, "config.toml");
1474
+ try {
1475
+ if (!fs4.existsSync(codexDir) && !options.dryRun) {
1476
+ fs4.mkdirSync(codexDir, { recursive: true });
1477
+ }
1478
+ const entry = `
1479
+ # MemHub MCP Connector
1480
+ [mcp.servers.memhub]
1481
+ command = "memhub"
1482
+ args = ["serve"]
1483
+ `;
1484
+ let content = "";
1485
+ if (fs4.existsSync(configPath)) {
1486
+ content = fs4.readFileSync(configPath, "utf8");
1487
+ }
1488
+ if (!content.includes("[mcp.servers.memhub]")) {
1489
+ content += entry;
1490
+ if (!options.dryRun) {
1491
+ fs4.writeFileSync(configPath, content, "utf8");
1492
+ }
1493
+ }
1494
+ return {
1495
+ target: "Codex CLI",
1496
+ filePath: configPath,
1497
+ success: true,
1498
+ action: "updated"
1499
+ };
1500
+ } catch (err) {
1501
+ return {
1502
+ target: "Codex CLI",
1503
+ filePath: configPath,
1504
+ success: false,
1505
+ action: "error",
1506
+ error: err.message
1507
+ };
1508
+ }
1509
+ }
1510
+ /**
1511
+ * Injects for all supported IDEs and tools
1512
+ */
1513
+ static injectAll(options) {
1514
+ return [
1515
+ this.injectClaudeCode(options),
1516
+ this.injectCursor(options),
1517
+ this.injectCodex(options),
1518
+ this.injectAntigravity(options),
1519
+ this.injectWindsurf(options),
1520
+ this.injectCline(options)
1521
+ ];
1522
+ }
1523
+ };
1524
+
1525
+ // src/cli.ts
1526
+ import { createInterface } from "node:readline/promises";
1527
+ import { stdin as input, stdout as output } from "node:process";
1528
+ import { hostname } from "node:os";
1529
+
1530
+ // src/auth/login-flow.ts
1531
+ import { spawn } from "node:child_process";
1532
+ var LoginDeniedError = class extends Error {
1533
+ constructor() {
1534
+ super("Login request denied in the browser.");
1535
+ this.name = "LoginDeniedError";
1536
+ }
1537
+ };
1538
+ var LoginExpiredError = class extends Error {
1539
+ constructor() {
1540
+ super("Login request expired before approval. Run `mh login` again.");
1541
+ this.name = "LoginExpiredError";
1542
+ }
1543
+ };
1544
+ var LoginTimeoutError = class extends Error {
1545
+ constructor(timeoutMs) {
1546
+ super(`Login was not approved within ${Math.round(timeoutMs / 1e3)}s. Run \`mh login\` again.`);
1547
+ this.name = "LoginTimeoutError";
1548
+ }
1549
+ };
1550
+ var DEFAULT_TIMEOUT_MS = 15 * 60 * 1e3;
1551
+ var MIN_SLEEP_MS = 250;
1552
+ function defaultSleep(ms) {
1553
+ return new Promise((resolve) => setTimeout(resolve, ms));
1554
+ }
1555
+ async function requestCliAuth(apiUrl, options = {}) {
1556
+ const doFetch = options.fetchImpl ?? fetch;
1557
+ const res = await doFetch(`${apiUrl}/api/cli-auth/initiate`, {
1558
+ method: "POST",
1559
+ headers: { "Content-Type": "application/json" },
1560
+ body: JSON.stringify({ label: options.label, scopes: options.scopes })
1561
+ });
1562
+ if (!res.ok) {
1563
+ throw new Error(`Unable to start login (${res.status}). Check that ${apiUrl} is reachable.`);
1564
+ }
1565
+ return await res.json();
1566
+ }
1567
+ async function pollOnce(apiUrl, deviceCode, doFetch) {
1568
+ const res = await doFetch(`${apiUrl}/api/cli-auth/poll`, {
1569
+ method: "POST",
1570
+ headers: { "Content-Type": "application/json" },
1571
+ body: JSON.stringify({ deviceCode })
1572
+ });
1573
+ if (res.status === 429) {
1574
+ const retryAfter = Number(res.headers.get("retry-after"));
1575
+ return {
1576
+ result: { status: "pending", expiresAt: null },
1577
+ retryAfterSeconds: Number.isFinite(retryAfter) && retryAfter > 0 ? Math.ceil(retryAfter) : null
1578
+ };
1579
+ }
1580
+ if (!res.ok) throw new Error(`Login polling failed (${res.status}).`);
1581
+ return { result: await res.json(), retryAfterSeconds: null };
1582
+ }
1583
+ async function waitForCliApproval(apiUrl, initiation, options = {}) {
1584
+ const doFetch = options.fetchImpl ?? fetch;
1585
+ const sleep = options.sleepImpl ?? defaultSleep;
1586
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
1587
+ const deadline = Date.now() + timeoutMs;
1588
+ let pendingNoticeShown = false;
1589
+ while (Date.now() < deadline) {
1590
+ const { result, retryAfterSeconds } = await pollOnce(apiUrl, initiation.deviceCode, doFetch);
1591
+ if (result.status === "approved") {
1592
+ if (!result.apiKey) throw new Error("Server returned an empty credential.");
1593
+ return {
1594
+ apiKey: result.apiKey,
1595
+ keyPrefix: result.keyPrefix,
1596
+ orgSlug: result.orgSlug,
1597
+ orgName: result.orgName,
1598
+ projectSlug: result.projectSlug,
1599
+ projectName: result.projectName
1600
+ };
1601
+ }
1602
+ if (result.status === "denied") throw new LoginDeniedError();
1603
+ if (result.status === "consumed" || result.status === "not_found") throw new LoginExpiredError();
1604
+ if (!pendingNoticeShown) {
1605
+ process.stderr.write("\u23F3 Waiting for approval in your browser\u2026\n");
1606
+ pendingNoticeShown = true;
1607
+ }
1608
+ const intervalMs = Math.max((initiation.pollIntervalSeconds || 2) * 1e3, MIN_SLEEP_MS);
1609
+ const backoffMs = retryAfterSeconds ? Math.min(retryAfterSeconds * 1e3, 3e4) : intervalMs;
1610
+ const remainingMs = deadline - Date.now();
1611
+ await sleep(Math.max(MIN_SLEEP_MS, Math.min(backoffMs, remainingMs)));
1612
+ }
1613
+ throw new LoginTimeoutError(timeoutMs);
1614
+ }
1615
+ function browserOpenCommand(platform = process.platform) {
1616
+ const command = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
1617
+ return command === "cmd" ? { command, args: ["/c", "start", "", "<url>"] } : { command, args: ["<url>"] };
1618
+ }
1619
+ function openInBrowser(url) {
1620
+ try {
1621
+ const { command, args } = browserOpenCommand();
1622
+ const child = spawn(command, args.map((arg) => arg === "<url>" ? url : arg), { stdio: "ignore", detached: true });
1623
+ child.unref();
1624
+ return true;
1625
+ } catch {
1626
+ return false;
1627
+ }
1628
+ }
1629
+
1630
+ // src/cli.ts
1631
+ var program = new Command();
1632
+ var supportedTargets = ["claude", "cursor", "windsurf", "cline", "codex", "antigravity"];
1633
+ async function selectInstallTargets(value, otherConfig) {
1634
+ if (value) {
1635
+ const requested = value.toLowerCase().split(",").map((item) => item.trim()).filter(Boolean);
1636
+ const expanded = requested.includes("all") ? [...supportedTargets] : requested;
1637
+ const invalid = expanded.filter((item) => !supportedTargets.includes(item) && item !== "other");
1638
+ if (invalid.length) throw new Error(`Unsupported IDE target(s): ${invalid.join(", ")}`);
1639
+ if (expanded.includes("other") && !otherConfig) throw new Error("--config <path> is required with --ide other");
1640
+ return [...new Set(expanded)];
1641
+ }
1642
+ if (!input.isTTY || !output.isTTY) return [...supportedTargets];
1643
+ const rl = createInterface({ input, output });
1644
+ output.write("\nWhere should MemHub be installed? Select one or more (comma-separated):\n");
1645
+ output.write(" 1) Claude Code\n 2) Cursor\n 3) Windsurf\n 4) Cline\n 5) Codex CLI\n 6) Google Antigravity\n 7) All supported clients\n 8) Other JSON MCP client\n");
1646
+ const answer = await rl.question("Selection [7]: ");
1647
+ rl.close();
1648
+ const choices = (answer.trim() || "7").split(",").map((item) => item.trim());
1649
+ if (choices.includes("7")) return [...supportedTargets];
1650
+ const map = { "1": "claude", "2": "cursor", "3": "windsurf", "4": "cline", "5": "codex", "6": "antigravity", "8": "other" };
1651
+ const selected = choices.map((choice) => map[choice]).filter(Boolean);
1652
+ if (!selected.length) throw new Error("No valid installation target selected");
1653
+ if (selected.includes("other") && !otherConfig) throw new Error("Use --config <path> when selecting Other");
1654
+ return [...new Set(selected)];
1655
+ }
1656
+ async function installToSelectedClients(options) {
1657
+ const targets = await selectInstallTargets(options.ide, options.configPath);
1658
+ console.log(`\u{1F527} Injecting MCP server configuration for: ${targets.join(", ")}...`);
1659
+ const injectOpts = {
1660
+ apiKey: options.apiKey,
1661
+ apiUrl: options.apiUrl,
1662
+ projectId: options.projectSlug
1663
+ };
1664
+ const results = [];
1665
+ for (const target of targets) {
1666
+ if (target === "claude") results.push(IdeConfigInjector.injectClaudeCode(injectOpts));
1667
+ if (target === "cursor") results.push(IdeConfigInjector.injectCursor(injectOpts));
1668
+ if (target === "windsurf") results.push(IdeConfigInjector.injectWindsurf(injectOpts));
1669
+ if (target === "cline") results.push(IdeConfigInjector.injectCline(injectOpts));
1670
+ if (target === "codex") results.push(IdeConfigInjector.injectCodex(injectOpts));
1671
+ if (target === "antigravity") results.push(IdeConfigInjector.injectAntigravity(injectOpts));
1672
+ if (target === "other") results.push(IdeConfigInjector.injectOther(options.configPath, injectOpts));
1673
+ }
1674
+ for (const res of results) {
1675
+ if (res.success) {
1676
+ console.log(` \u2713 ${res.target}: configured at ${res.filePath}`);
1677
+ } else {
1678
+ console.log(` \u2717 ${res.target}: failed (${res.error})`);
1679
+ }
1680
+ }
1681
+ }
1682
+ program.name("memhub").description("Local-first MCP proxy for MemHub & agentmemory").version("0.1.0");
1683
+ program.command("serve").description("Start the MemHub MCP stdio proxy server").action(async () => {
1684
+ const config = getConfig();
1685
+ const server = new MemHubMcpServer(config);
1686
+ await server.start();
1687
+ });
1688
+ program.command("install [target]").description("Install the global MemHub MCP server into an AI client (or use generic)").option("--config <path>", "JSON MCP config path for a generic/other client").action(async (target, options) => {
1689
+ const config = getConfig();
1690
+ if (!config.apiKey) {
1691
+ console.error("No MemHub session is configured. Run `memhub login` first.");
1692
+ process.exitCode = 1;
1693
+ return;
1694
+ }
1695
+ if (target === "generic" && !options.config) {
1696
+ console.log(JSON.stringify({
1697
+ mcpServers: { memhub: { command: "memhub", args: ["serve"] } }
1698
+ }, null, 2));
1699
+ return;
1700
+ }
1701
+ await installToSelectedClients({
1702
+ apiKey: config.apiKey,
1703
+ apiUrl: config.apiUrl,
1704
+ projectSlug: config.projectSlug || detectProjectContext().slug,
1705
+ ide: target === "generic" ? "other" : target,
1706
+ configPath: options.config
1707
+ });
1708
+ });
1709
+ program.command("connect").description("Connect the current repository or workspace to MemHub Cloud").requiredOption("-k, --key <apiKey>", "MemHub API Key (e.g. mh_live_...)").option("-u, --url <apiUrl>", "MemHub Cloud API URL", "https://memhub.simplex.lat").option("-p, --project <projectSlug>", "Override project slug").option("--ide <ides>", "Comma-separated targets: all, claude, cursor, windsurf, cline, codex, antigravity, other").option("--config <path>", "JSON MCP config path when using --ide other").action(async (options) => {
1710
+ console.log("\u{1F680} Connecting to MemHub Cloud...");
1711
+ const detected = detectProjectContext();
1712
+ const projectSlug = options.project || detected.slug;
1713
+ saveConfig({
1714
+ apiKey: options.key,
1715
+ apiUrl: options.url,
1716
+ projectSlug
1717
+ });
1718
+ const client = new CloudApiClient(options.url, options.key);
1719
+ console.log(`\u{1F4E1} Resolving project "${projectSlug}" on cloud...`);
1720
+ try {
1721
+ const resolved = await client.resolveProject({
1722
+ slug: projectSlug,
1723
+ gitRemote: detected.gitRemote,
1724
+ name: detected.name
1725
+ });
1726
+ console.log(`\u2705 Project resolved: ${resolved.projectName} (ID: ${resolved.projectId}, Org: ${resolved.orgSlug})`);
1727
+ saveConfig({
1728
+ projectId: resolved.projectId,
1729
+ orgId: resolved.orgId
1730
+ });
1731
+ } catch (err) {
1732
+ console.warn(`\u26A0\uFE0F Cloud project resolution warning: ${err.message}`);
1733
+ console.log(" (Local memory and offline outbox will continue to work normally)");
1734
+ }
1735
+ await installToSelectedClients({
1736
+ apiKey: options.key,
1737
+ apiUrl: options.url,
1738
+ projectSlug,
1739
+ ide: options.ide,
1740
+ configPath: options.config
1741
+ });
1742
+ console.log("\n\u{1F389} MemHub MCP proxy successfully connected!");
1743
+ console.log(" Restart or refresh the selected AI clients to load MemHub.");
1744
+ });
1745
+ program.command("login").description("Sign in with your browser, consent to a scoped project credential, then pick AI clients to configure").option("-u, --url <apiUrl>", "MemHub Cloud API URL", "https://memhub.simplex.lat").option("--ide <ides>", "Comma-separated targets: all, claude, cursor, windsurf, cline, codex, antigravity, other").option("--config <path>", "JSON MCP config path when using --ide other").option("--label <label>", "Device label shown on the consent page").option("--no-open", "Print the verification URL instead of opening a browser").action(async (options) => {
1746
+ try {
1747
+ console.log("\u{1F510} Starting browser login for MemHub Cloud...");
1748
+ const initiation = await requestCliAuth(options.url, {
1749
+ label: options.label || `${hostname()} terminal`
1750
+ });
1751
+ const opened = options.open ? openInBrowser(initiation.verificationUrl) : false;
1752
+ if (!opened) {
1753
+ console.log("\n\u{1F310} Open this URL in any browser to continue:");
1754
+ console.log(` ${initiation.verificationUrl}`);
1755
+ } else {
1756
+ console.log(`
1757
+ \u{1F310} Browser opened at ${options.url}/cli-auth \u2014 sign in, review the request, and approve.`);
1758
+ }
1759
+ console.log("\u23F1\uFE0F The login request expires in 15 minutes and can be approved only once.");
1760
+ const approval = await waitForCliApproval(options.url, initiation);
1761
+ saveConfig({
1762
+ apiKey: approval.apiKey,
1763
+ apiUrl: options.url,
1764
+ projectSlug: approval.projectSlug ?? void 0
1765
+ });
1766
+ console.log(`\u2705 Signed in${approval.orgName ? ` to ${approval.orgName}` : ""}. Scoped credential ${approval.keyPrefix}\u2026 saved to ~/.memhub/config.json (owner-only).`);
1767
+ if (!approval.projectSlug) {
1768
+ console.log("\u2139\uFE0F No default project was chosen during consent; it will be resolved per-repository on first use.");
1769
+ }
1770
+ await installToSelectedClients({
1771
+ apiKey: approval.apiKey,
1772
+ apiUrl: options.url,
1773
+ projectSlug: approval.projectSlug || detectProjectContext().slug,
1774
+ ide: options.ide,
1775
+ configPath: options.config
1776
+ });
1777
+ console.log("\n\u{1F389} Login complete! Restart or refresh the selected AI clients to load MemHub.");
1778
+ } catch (err) {
1779
+ if (err instanceof LoginDeniedError) {
1780
+ console.error("\u2717 Login denied in the browser. No credentials were stored.");
1781
+ } else if (err instanceof LoginExpiredError || err instanceof LoginTimeoutError) {
1782
+ console.error(`\u2717 ${err.message}`);
1783
+ } else {
1784
+ console.error(`\u2717 Login failed: ${err.message}`);
1785
+ }
1786
+ process.exitCode = 1;
1787
+ }
1788
+ });
1789
+ program.command("logout").description("Revoke this device, release its plan slot, and remove local MemHub credentials and sync state").option("--local-only", "Clear local state even when the cloud cannot be reached").action(async (options) => {
1790
+ const config = getConfig();
1791
+ if (!config.apiKey && !options.localOnly) {
1792
+ console.error("No MemHub session is configured on this device.");
1793
+ process.exitCode = 1;
1794
+ return;
1795
+ }
1796
+ try {
1797
+ if (config.apiKey && !options.localOnly) {
1798
+ const result = await new CloudApiClient(config.apiUrl, config.apiKey).logoutDevice(config.deviceId);
1799
+ console.log(result.deviceReleased ? "\u2713 Device revoked and its plan slot released." : "\u2713 Credential revoked; this device had no active cloud slot.");
1800
+ }
1801
+ clearLocalState();
1802
+ console.log("\u2713 Local MemHub credentials and sync outbox removed. Cloud memory history was preserved.");
1803
+ console.log(" Restart configured AI clients. Run mh login to connect again.");
1804
+ } catch (err) {
1805
+ console.error(`\u2717 Logout failed: ${err.message}`);
1806
+ console.error(" Local credentials were kept so you can retry. Use --local-only only if this device cannot reach MemHub.");
1807
+ process.exitCode = 1;
1808
+ }
1809
+ });
1810
+ program.command("status").description("Display synchronization status, outbox stats, and device info").action(async () => {
1811
+ const config = getConfig();
1812
+ const db = new OutboxDb(config.outboxPath);
1813
+ const pendingCount = db.getPendingCount();
1814
+ const lastSeq = db.getLastSequence(config.deviceId);
1815
+ const localProject = detectProjectContext();
1816
+ console.log("\n\u{1F4CA} MemHub MCP Status:");
1817
+ console.log(` Device ID: ${config.deviceId}`);
1818
+ console.log(` API URL: ${config.apiUrl}`);
1819
+ console.log(` API Key: ${config.apiKey ? config.apiKey.slice(0, 12) + "..." : "(none configured)"}`);
1820
+ console.log(` Project Slug: ${config.projectSlug || localProject.slug}`);
1821
+ console.log(` Project ID: ${config.projectId || "(pending JIT resolution)"}`);
1822
+ console.log(` Outbox SQLite DB: ${config.outboxPath}`);
1823
+ console.log(` Pending Events: ${pendingCount}`);
1824
+ console.log(` Monotonic Sequence: ${lastSeq}`);
1825
+ db.close();
1826
+ });
1827
+ program.command("doctor").description("Run system health checks and diagnostics for local engine & cloud sync").action(async () => {
1828
+ console.log("\n\u{1FA7A} Running MemHub Doctor...\n");
1829
+ const config = getConfig();
1830
+ console.log("1. Checking local engine tools:");
1831
+ console.log(` \u2713 Found ${AGENTMEMORY_54_TOOLS.length} native agentmemory tools mapped.`);
1832
+ console.log("2. Checking local durable outbox (WAL mode):");
1833
+ try {
1834
+ const db = new OutboxDb(config.outboxPath);
1835
+ const pending = db.getPendingCount();
1836
+ console.log(` \u2713 Outbox SQLite database accessible (${pending} pending events).`);
1837
+ db.close();
1838
+ } catch (err) {
1839
+ console.log(` \u2717 Outbox error: ${err.message}`);
1840
+ }
1841
+ console.log("3. Checking MemHub Cloud API connectivity:");
1842
+ if (!config.apiKey) {
1843
+ console.log(" \u26A0\uFE0F No API key configured. Run `memhub login` or `memhub connect --key <key>`.");
1844
+ } else {
1845
+ const client = new CloudApiClient(config.apiUrl, config.apiKey);
1846
+ const health = await client.testConnection();
1847
+ if (health.ok) {
1848
+ console.log(` \u2713 Cloud API reachable at ${config.apiUrl} (HTTP status: ${health.status})`);
1849
+ } else {
1850
+ console.log(` \u2717 Cloud API unreachable at ${config.apiUrl} (${health.message})`);
1851
+ }
1852
+ }
1853
+ console.log("\nDoctor inspection completed.");
1854
+ });
1855
+ program.command("flush").description("Force an immediate push of all pending outbox events to the cloud").action(async () => {
1856
+ const config = getConfig();
1857
+ if (!config.apiKey) {
1858
+ console.error("Error: No API key configured. Run connect first.");
1859
+ process.exit(1);
1860
+ }
1861
+ console.log("\u{1F504} Flushing pending outbox events to cloud...");
1862
+ const db = new OutboxDb(config.outboxPath);
1863
+ const client = new CloudApiClient(config.apiUrl, config.apiKey);
1864
+ const dueEvents = db.getDueEvents(100);
1865
+ if (dueEvents.length === 0) {
1866
+ console.log("\u2713 Outbox is already clean (0 pending events).");
1867
+ db.close();
1868
+ return;
1869
+ }
1870
+ try {
1871
+ const res = await client.pushEvents({
1872
+ protocol: 1,
1873
+ deviceId: config.deviceId,
1874
+ events: dueEvents.map((e) => ({
1875
+ eventId: e.eventId,
1876
+ sequence: e.sequence,
1877
+ operation: e.operation,
1878
+ payload: JSON.parse(e.payloadJson),
1879
+ payloadSha256: e.payloadSha256
1880
+ }))
1881
+ });
1882
+ if (res.ackedEventIds && res.ackedEventIds.length > 0) {
1883
+ db.markAcked(res.ackedEventIds);
1884
+ console.log(`\u2705 Successfully flushed and acked ${res.ackedEventIds.length} events.`);
1885
+ }
1886
+ } catch (err) {
1887
+ console.error(`\u2717 Flush failed: ${err.message}`);
1888
+ } finally {
1889
+ db.close();
1890
+ }
1891
+ });
1892
+ program.parse(process.argv);
1893
+ //# sourceMappingURL=cli.js.map