memorix 1.9.4 → 1.9.5

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 (60) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/README.md +6 -5
  3. package/README.zh-CN.md +6 -5
  4. package/dist/cli/index.js +890 -325
  5. package/dist/index.d.ts +3 -1
  6. package/dist/index.js +251 -104
  7. package/dist/maintenance-runner.js +144 -113
  8. package/dist/media-audio-runner.js +57 -26
  9. package/dist/media-video-runner.js +57 -26
  10. package/dist/memcode-runtime/CHANGELOG.md +33 -1
  11. package/dist/sdk.d.ts +1 -0
  12. package/dist/sdk.js +1012 -842
  13. package/dist/types.d.ts +1 -1
  14. package/dist/vector-backfill-runner.js +57 -26
  15. package/docs/AGENT_OPERATOR_PLAYBOOK.md +47 -3
  16. package/docs/API_REFERENCE.md +3 -1
  17. package/docs/CONFIGURATION.md +15 -1
  18. package/docs/DOCKER.md +23 -3
  19. package/docs/INTEGRATIONS.md +1 -1
  20. package/docs/KNOWN_ISSUES_AND_ROADMAP.md +26 -1
  21. package/docs/MULTI-DEVICE-SYNC-RESEARCH.md +4 -0
  22. package/docs/SETUP.md +55 -0
  23. package/package.json +2 -2
  24. package/plugins/antigravity/memorix/plugin.json +1 -1
  25. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  26. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  27. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  28. package/plugins/copilot/memorix/plugin.json +1 -1
  29. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  30. package/plugins/hermes/memorix/plugin.yaml +1 -1
  31. package/plugins/omp/memorix/package.json +1 -1
  32. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  33. package/plugins/pi/memorix/package.json +1 -1
  34. package/server.json +2 -2
  35. package/src/cli/command-guide.ts +2 -2
  36. package/src/cli/commands/agent-integrations.ts +57 -0
  37. package/src/cli/commands/background.ts +11 -2
  38. package/src/cli/commands/serve-http.ts +193 -42
  39. package/src/cli/commands/setup.ts +77 -10
  40. package/src/cli/commands/sync-store.ts +47 -12
  41. package/src/cli/commands/sync.ts +26 -0
  42. package/src/dashboard/server.ts +10 -12
  43. package/src/integrations/registry.ts +1 -1
  44. package/src/memory/graph.ts +10 -8
  45. package/src/memory/session.ts +1 -1
  46. package/src/sdk.ts +24 -0
  47. package/src/server/mcp-binding-store.ts +14 -3
  48. package/src/server.ts +2 -2
  49. package/src/store/file-lock.ts +28 -3
  50. package/src/store/graph-store.ts +178 -46
  51. package/src/store/persistence.ts +14 -9
  52. package/src/store/sqlite-db.ts +6 -3
  53. package/src/store/sqlite-store.ts +32 -27
  54. package/src/sync/engine.ts +9 -7
  55. package/src/sync/namespace.ts +23 -0
  56. package/src/sync/policy.ts +19 -4
  57. package/src/sync/store-port.ts +43 -13
  58. package/src/sync/types.ts +2 -0
  59. package/src/types.ts +1 -1
  60. package/src/workspace/mcp-adapters/grok.ts +32 -0
@@ -33,6 +33,7 @@ import { TraeMCPAdapter } from '../../workspace/mcp-adapters/trae.js';
33
33
  import { DshMCPAdapter } from '../../workspace/mcp-adapters/dsh.js';
34
34
  import { WorkbuddyMCPAdapter } from '../../workspace/mcp-adapters/workbuddy.js';
35
35
  import { CodeBuddyMCPAdapter } from '../../workspace/mcp-adapters/codebuddy.js';
36
+ import { GrokMCPAdapter } from '../../workspace/mcp-adapters/grok.js';
36
37
  import { getSetupIntegrationRows as readSetupIntegrationRows } from '../../integrations/registry.js';
37
38
  import { OFFICIAL_MEMORIX_SKILLS } from '../../hooks/official-skills.js';
38
39
  import { getCliVersion } from '../version.js';
@@ -212,9 +213,11 @@ export function buildSetupPlan(options: {
212
213
  plugin?: boolean;
213
214
  global?: boolean;
214
215
  }): SetupPlan {
215
- // Grok owns its MCP configuration through `grok mcp`; Memorix setup only
216
- // installs the independent hooks/rules integration for this target.
217
- const mcp = options.agent === 'grok' ? 'none' : options.mcp ?? 'stdio';
216
+ // Grok MCP stays host-owned by default (`grok mcp`). An explicit
217
+ // `--mcp http` is the exception: write the shared control-plane URL.
218
+ const mcp = options.agent === 'grok'
219
+ ? (options.mcp === 'http' ? 'http' : 'none')
220
+ : options.mcp ?? 'stdio';
218
221
  const actions: SetupAction[] = [];
219
222
  const isPackageOwnedAgent = options.agent !== 'all' && PACKAGE_OWNED_INTEGRATION_AGENTS.has(options.agent);
220
223
  const includeHooks = options.hooks ?? true;
@@ -391,7 +394,7 @@ function relativePosix(from: string, to: string): string {
391
394
  return rel;
392
395
  }
393
396
 
394
- export type McpConfigAgent = Exclude<AgentName, 'pi' | 'grok'>;
397
+ export type McpConfigAgent = Exclude<AgentName, 'pi'>;
395
398
 
396
399
  export function getMcpAdapter(agent: McpConfigAgent): MCPConfigAdapter {
397
400
  const adapters: Record<McpConfigAgent, MCPConfigAdapter> = {
@@ -411,17 +414,20 @@ export function getMcpAdapter(agent: McpConfigAgent): MCPConfigAdapter {
411
414
  trae: new TraeMCPAdapter(),
412
415
  dsh: new DshMCPAdapter(),
413
416
  workbuddy: new WorkbuddyMCPAdapter(),
417
+ grok: new GrokMCPAdapter(),
414
418
  };
415
419
  return adapters[agent];
416
420
  }
417
421
 
422
+ export const MEMORIX_HTTP_MCP_URL = 'http://localhost:3211/mcp';
423
+
418
424
  export function buildMemorixServer(mcp: Exclude<SetupMcpTransport, 'none'>): MCPServerEntry {
419
425
  if (mcp === 'http') {
420
426
  return {
421
427
  name: 'memorix',
422
428
  command: '',
423
429
  args: [],
424
- url: 'http://localhost:3211/mcp',
430
+ url: MEMORIX_HTTP_MCP_URL,
425
431
  };
426
432
  }
427
433
 
@@ -434,6 +440,45 @@ export function buildMemorixServer(mcp: Exclude<SetupMcpTransport, 'none'>): MCP
434
440
  };
435
441
  }
436
442
 
443
+ function toHttpMemorixServer(existing: Record<string, unknown>): Record<string, unknown> {
444
+ const next: Record<string, unknown> = { url: MEMORIX_HTTP_MCP_URL };
445
+ if (existing.alwaysLoad === true) next.alwaysLoad = true;
446
+ if (typeof existing.type === 'string') next.type = 'http';
447
+ return next;
448
+ }
449
+
450
+ /**
451
+ * Rewrite a copied plugin/extension MCP manifest from stdio `memorix serve`
452
+ * to the shared HTTP control plane. Leaves unrelated keys intact.
453
+ */
454
+ export async function rewriteBundledMemorixMcpToHttp(root: string): Promise<string[]> {
455
+ const candidates = ['.mcp.json', 'mcp_config.json', 'gemini-extension.json']
456
+ .map((name) => path.join(root, name));
457
+ const rewritten: string[] = [];
458
+
459
+ for (const filePath of candidates) {
460
+ if (!existsSync(filePath)) continue;
461
+ let doc: unknown;
462
+ try {
463
+ doc = JSON.parse(await readFile(filePath, 'utf-8'));
464
+ } catch {
465
+ continue;
466
+ }
467
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc)) continue;
468
+ const record = doc as Record<string, unknown>;
469
+ const servers = record.mcpServers;
470
+ if (!servers || typeof servers !== 'object' || Array.isArray(servers)) continue;
471
+ const serverMap = servers as Record<string, unknown>;
472
+ const existing = serverMap.memorix;
473
+ if (!existing || typeof existing !== 'object' || Array.isArray(existing)) continue;
474
+ serverMap.memorix = toHttpMemorixServer(existing as Record<string, unknown>);
475
+ await writeFile(filePath, JSON.stringify(record, null, 2) + '\n', 'utf-8');
476
+ rewritten.push(filePath);
477
+ }
478
+
479
+ return rewritten;
480
+ }
481
+
437
482
  function mergeJsonMcpConfig(existingContent: string | null, generatedContent: string): string {
438
483
  let generated: Record<string, unknown>;
439
484
  try {
@@ -781,7 +826,7 @@ export async function installMcpConfig(options: {
781
826
  }
782
827
 
783
828
  const generated = adapter.generate([server]);
784
- const content = options.agent === 'codex'
829
+ const content = options.agent === 'codex' || options.agent === 'grok'
785
830
  ? mergeTomlMcpConfig(existingContent, generated)
786
831
  : options.agent === 'hermes'
787
832
  ? mergeYamlMcpConfig(existingContent, generated)
@@ -1456,7 +1501,12 @@ export async function installAgentSetup(agent: AgentName, plan: SetupPlan, globa
1456
1501
  const hasCodeBuddyPlugin = plan.actions.includes('codebuddy-plugin') && agent === 'codebuddy';
1457
1502
  // Codex's supported default is the user-level plugin. Never create a
1458
1503
  // project-local .codex/config.toml as an implicit fallback.
1459
- const wantsMcpConfig = plan.mcp !== 'none' && agent !== 'pi' && agent !== 'grok' && !(agent === 'codex' && !global);
1504
+ // Grok MCP stays host-owned unless the caller passed --mcp http.
1505
+ const wantsMcpConfig = plan.mcp !== 'none'
1506
+ && agent !== 'pi'
1507
+ && !(agent === 'codex' && !global)
1508
+ && !(agent === 'grok' && plan.mcp !== 'http');
1509
+ const bundledMcpRoots: string[] = [];
1460
1510
 
1461
1511
  if (PLUGIN_PACKAGE_AGENTS.has(agent) && plan.includePlugin && !global) {
1462
1512
  p.log.info(`${agent}: project setup leaves user-level plugins untouched. Run \`memorix setup --agent ${agent} --global\` to install its plugin, skills, and lifecycle hooks.`);
@@ -1467,6 +1517,7 @@ export async function installAgentSetup(agent: AgentName, plan: SetupPlan, globa
1467
1517
  agent: agent as PluginInstallOptions['agent'],
1468
1518
  includeHooks: plan.includeHooks,
1469
1519
  });
1520
+ bundledMcpRoots.push(result.pluginPath);
1470
1521
  p.log.success(`${agent}: plugin package -> ${result.pluginPath}`);
1471
1522
  if (result.marketplacePath) {
1472
1523
  p.log.info(`${agent}: marketplace -> ${result.marketplacePath}`);
@@ -1498,6 +1549,7 @@ export async function installAgentSetup(agent: AgentName, plan: SetupPlan, globa
1498
1549
 
1499
1550
  if (hasCodeBuddyPlugin) {
1500
1551
  const result = await installCodeBuddyPluginPackage({ includeHooks: plan.includeHooks });
1552
+ bundledMcpRoots.push(result.pluginPath);
1501
1553
  p.log.success(`${agent}: plugin -> ${result.pluginPath}`);
1502
1554
  p.log.info(`${agent}: marketplace -> ${result.marketplacePath}`);
1503
1555
  const install = tryInstallCodeBuddyPlugin(result.marketplaceRoot);
@@ -1509,6 +1561,7 @@ export async function installAgentSetup(agent: AgentName, plan: SetupPlan, globa
1509
1561
 
1510
1562
  if (hasExtensionPackage) {
1511
1563
  const result = await installGeminiExtensionPackage();
1564
+ bundledMcpRoots.push(result.extensionPath);
1512
1565
  p.log.success(`${agent}: extension package -> ${result.extensionPath}`);
1513
1566
  p.log.info(result.installHint);
1514
1567
  }
@@ -1524,6 +1577,7 @@ export async function installAgentSetup(agent: AgentName, plan: SetupPlan, globa
1524
1577
 
1525
1578
  if (hasOpenClawBundle) {
1526
1579
  const result = await installOpenClawBundlePackage({ includeHooks: plan.includeHooks });
1580
+ bundledMcpRoots.push(result.bundlePath);
1527
1581
  p.log.success(`${agent}: compatible bundle -> ${result.bundlePath}`);
1528
1582
  if (plan.includeHooks) {
1529
1583
  const install = tryInstallOpenClawBundle(result.bundlePath);
@@ -1537,6 +1591,7 @@ export async function installAgentSetup(agent: AgentName, plan: SetupPlan, globa
1537
1591
 
1538
1592
  if (hasAntigravityPlugin) {
1539
1593
  const result = await installAntigravityPluginPackage({ global, projectRoot: targetRoot, includeHooks: plan.includeHooks });
1594
+ bundledMcpRoots.push(result.pluginPath);
1540
1595
  p.log.success(`${agent}: plugin -> ${result.pluginPath}`);
1541
1596
  if (!plan.includeHooks) {
1542
1597
  p.log.info(`${agent}: hooks.json skipped because --noHooks was selected`);
@@ -1570,7 +1625,8 @@ export async function installAgentSetup(agent: AgentName, plan: SetupPlan, globa
1570
1625
  }
1571
1626
 
1572
1627
  if (wantsMcpConfig) {
1573
- if ((hasPluginPackage || hasCodeBuddyPlugin || hasExtensionPackage || hasOpenClawBundle || hasAntigravityPlugin) && plan.mcp === 'stdio') {
1628
+ const mcpOwnedByPackage = hasPluginPackage || hasCodeBuddyPlugin || hasExtensionPackage || hasOpenClawBundle || hasAntigravityPlugin;
1629
+ if (mcpOwnedByPackage && plan.mcp === 'stdio') {
1574
1630
  const packageLabel = hasExtensionPackage
1575
1631
  ? 'extension package'
1576
1632
  : hasCodeBuddyPlugin
@@ -1581,6 +1637,13 @@ export async function installAgentSetup(agent: AgentName, plan: SetupPlan, globa
1581
1637
  ? 'Antigravity plugin'
1582
1638
  : 'plugin package';
1583
1639
  p.log.info(`${agent}: stdio MCP is bundled in the ${packageLabel}`);
1640
+ } else if (mcpOwnedByPackage && plan.mcp === 'http') {
1641
+ for (const root of bundledMcpRoots) {
1642
+ const rewritten = await rewriteBundledMemorixMcpToHttp(root);
1643
+ for (const filePath of rewritten) {
1644
+ p.log.success(`${agent}: plugin MCP -> HTTP ${filePath}`);
1645
+ }
1646
+ }
1584
1647
  } else {
1585
1648
  const mcp = plan.mcp === 'http' ? 'http' : 'stdio';
1586
1649
  const result = await installMcpConfig({ agent: agent as McpConfigAgent, projectRoot: targetRoot, global, mcp });
@@ -1637,7 +1700,11 @@ export async function installAgentSetup(agent: AgentName, plan: SetupPlan, globa
1637
1700
  if (agent === 'pi') {
1638
1701
  p.log.info('pi: Pi has no MCP config lane in the current CLI; use the installed package extension and skill.');
1639
1702
  } else if (agent === 'grok') {
1640
- p.log.info('grok: lifecycle hooks write ~/.grok/hooks/memorix.json. MCP stays host-owned (do not write config.toml).');
1703
+ if (plan.mcp === 'http') {
1704
+ p.log.info('grok: wrote [mcp_servers.memorix] HTTP URL in Grok config.toml. Start the control plane with `memorix background start`.');
1705
+ } else {
1706
+ p.log.info('grok: lifecycle hooks write ~/.grok/hooks/memorix.json. MCP stays host-owned unless you pass `--mcp http`.');
1707
+ }
1641
1708
  if (!global) p.log.info('grok: project hooks require `/hooks-trust` or launching Grok with `--trust`.');
1642
1709
  } else if (plan.mcp === 'stdio') {
1643
1710
  p.log.info(`${agent}: MCP server command is \`memorix serve\``);
@@ -1744,7 +1811,7 @@ export default defineCommand({
1744
1811
  { value: 'codex', label: 'Codex', hint: 'plugin package + marketplace + MCP' },
1745
1812
  { value: 'copilot', label: 'GitHub Copilot CLI', hint: 'plugin package + hooks + MCP' },
1746
1813
  { value: 'cursor', label: 'Cursor', hint: 'MCP + rules + skills' },
1747
- { value: 'grok', label: 'Grok Build', hint: 'hooks file; MCP stays host-owned' },
1814
+ { value: 'grok', label: 'Grok Build', hint: 'hooks file; MCP host-owned unless --mcp http' },
1748
1815
  { value: 'gemini-cli', label: 'Gemini CLI', hint: 'extension + MCP' },
1749
1816
  { value: 'openclaw', label: 'OpenClaw', hint: 'bundle + hook pack + skills + MCP' },
1750
1817
  { value: 'hermes', label: 'Hermes Agent', hint: 'plugin + hooks + commands + skills + MCP' },
@@ -10,14 +10,40 @@ import { defineCommand } from 'citty';
10
10
 
11
11
  import { getCliProjectContext, emitError, emitResult } from './operator-shared.js';
12
12
  import { getObservationStore, initObservationStore } from '../../store/obs-store.js';
13
+ import { getProjectDataDir } from '../../store/persistence.js';
13
14
  import { runSync } from '../../sync/engine.js';
14
15
  import { createSqliteSyncStore } from '../../sync/store-port.js';
15
16
  import { resolveSyncConfig, createRemote } from '../../sync/remote-factory.js';
17
+ import { USER_SYNC_SCOPE_ID } from '../../sync/namespace.js';
18
+ import type { SyncScope } from '../../sync/policy.js';
16
19
  import type { SyncReport } from '../../sync/types.js';
17
20
 
21
+ function parseScope(raw: unknown): SyncScope {
22
+ const value = String(raw || 'project').toLowerCase();
23
+ if (value === 'project' || value === 'user') return value;
24
+ throw new Error('--scope must be one of: project, user');
25
+ }
26
+
27
+ async function openSyncStore(scope: SyncScope) {
28
+ if (scope === 'user') {
29
+ const dataDir = await getProjectDataDir(USER_SYNC_SCOPE_ID);
30
+ await initObservationStore(dataDir);
31
+ return {
32
+ projectId: USER_SYNC_SCOPE_ID,
33
+ syncStore: createSqliteSyncStore(dataDir, getObservationStore(), USER_SYNC_SCOPE_ID, { scope: 'user' }),
34
+ };
35
+ }
36
+ const { project, dataDir } = await getCliProjectContext();
37
+ await initObservationStore(dataDir);
38
+ return {
39
+ projectId: project.id,
40
+ syncStore: createSqliteSyncStore(dataDir, getObservationStore(), project.id),
41
+ };
42
+ }
43
+
18
44
  function renderReport(report: SyncReport): string {
19
45
  const lines = [
20
- `Store sync (${report.remote}) — device ${report.deviceId}${report.dryRun ? ' [dry-run]' : ''}`,
46
+ `Store sync (${report.remote}, scope ${report.scope}) — device ${report.deviceId}${report.dryRun ? ' [dry-run]' : ''}`,
21
47
  `- Pushed changes: ${report.pushed}`,
22
48
  `- Pulled batches: ${report.pulledBatches}`,
23
49
  `- Applied upserts: ${report.applied}`,
@@ -48,22 +74,34 @@ export default defineCommand({
48
74
  'pull-only': { type: 'boolean', description: 'Only pull remote changes', default: false },
49
75
  through: { type: 'string', description: 'Compaction cutoff: device=sequence,device=sequence' },
50
76
  yes: { type: 'boolean', description: 'Confirm destructive remote compaction', default: false },
77
+ scope: {
78
+ type: 'string',
79
+ description: 'project (default: current Git project) or user (all local projects)',
80
+ default: 'project',
81
+ },
51
82
  },
52
83
  run: async ({ args }) => {
53
84
  const asJson = Boolean(args.json);
54
85
  const action = (args._ as string[])?.[0] || 'status';
86
+ let scope: SyncScope;
87
+ try {
88
+ scope = parseScope(args.scope);
89
+ } catch (err) {
90
+ emitError((err as Error).message, asJson);
91
+ process.exitCode = 2;
92
+ return;
93
+ }
55
94
  if (action === 'device') {
56
- const { project, dataDir } = await getCliProjectContext();
57
- await initObservationStore(dataDir);
58
- const syncStore = createSqliteSyncStore(dataDir, getObservationStore(), project.id);
59
95
  const subaction = (args._ as string[])?.[1] || '';
60
96
  if (subaction !== 'rotate') {
61
97
  emitError('expected "memorix sync store device rotate"', asJson);
62
98
  process.exitCode = 2;
63
99
  return;
64
100
  }
101
+ // Device identity is per data directory, not per Git project.
102
+ const { projectId, syncStore } = await openSyncStore('user');
65
103
  const deviceId = syncStore.rotateDevice();
66
- emitResult({ project: project.id, deviceId, rotated: true }, `Sync device identity rotated: ${deviceId}`, asJson);
104
+ emitResult({ project: projectId, scope: 'user', deviceId, rotated: true }, `Sync device identity rotated: ${deviceId}`, asJson);
67
105
  return;
68
106
  }
69
107
  if (action !== 'push' && action !== 'pull' && action !== 'status' && action !== 'compact') {
@@ -90,10 +128,7 @@ export default defineCommand({
90
128
  return;
91
129
  }
92
130
 
93
- const { project, dataDir } = await getCliProjectContext();
94
- await initObservationStore(dataDir);
95
- const obsStore = getObservationStore();
96
- const syncStore = createSqliteSyncStore(dataDir, obsStore, project.id);
131
+ const { projectId, syncStore } = await openSyncStore(scope);
97
132
 
98
133
  if (action === 'compact') {
99
134
  if (!args.yes && !args.dry) {
@@ -129,7 +164,7 @@ export default defineCommand({
129
164
  try {
130
165
  await remote.init({ create: false });
131
166
  const result = await remote.compact(through, { dryRun: Boolean(args.dry) });
132
- emitResult({ project: project.id, through, ...result, dryRun: Boolean(args.dry) }, `Compaction candidates: ${result.candidates}; deleted: ${result.deleted}`, asJson);
167
+ emitResult({ project: projectId, scope, through, ...result, dryRun: Boolean(args.dry) }, `Compaction candidates: ${result.candidates}; deleted: ${result.deleted}`, asJson);
133
168
  } catch (err) {
134
169
  emitError((err as Error).message, asJson);
135
170
  process.exitCode = 1;
@@ -155,7 +190,7 @@ export default defineCommand({
155
190
  push: true,
156
191
  pull: true,
157
192
  });
158
- emitResult({ project: project.id, ...report }, renderReport(report), asJson);
193
+ emitResult({ project: projectId, ...report }, renderReport(report), asJson);
159
194
  } catch (err) {
160
195
  emitError((err as Error).message, asJson);
161
196
  process.exitCode = 1;
@@ -182,7 +217,7 @@ export default defineCommand({
182
217
  push: doPush,
183
218
  pull: doPull,
184
219
  });
185
- emitResult({ project: project.id, ...report }, renderReport(report), asJson);
220
+ emitResult({ project: projectId, ...report }, renderReport(report), asJson);
186
221
  } catch (err) {
187
222
  emitError((err as Error).message, asJson);
188
223
  process.exitCode = 1;
@@ -48,6 +48,32 @@ export default defineCommand({
48
48
  description: 'Emit machine-readable JSON output',
49
49
  default: false,
50
50
  },
51
+ scope: {
52
+ type: 'string',
53
+ description: 'Store sync scope: project (default) or user',
54
+ required: false,
55
+ default: 'project',
56
+ },
57
+ 'push-only': {
58
+ type: 'boolean',
59
+ description: 'Store sync: only push local changes',
60
+ required: false,
61
+ },
62
+ 'pull-only': {
63
+ type: 'boolean',
64
+ description: 'Store sync: only pull remote changes',
65
+ required: false,
66
+ },
67
+ through: {
68
+ type: 'string',
69
+ description: 'Store sync compaction cutoff: device=sequence[,device=sequence]',
70
+ required: false,
71
+ },
72
+ yes: {
73
+ type: 'boolean',
74
+ description: 'Store sync: confirm destructive remote compaction',
75
+ required: false,
76
+ },
51
77
  },
52
78
  run: async ({ args }) => {
53
79
  const section = (args._ as string[])?.[0] || '';
@@ -16,7 +16,7 @@ import { exec } from 'node:child_process';
16
16
  import { getBaseDataDir } from '../store/persistence.js';
17
17
  import { getObservationStore, initObservationStore } from '../store/obs-store.js';
18
18
  import { getSessionStore, initSessionStore } from '../store/session-store.js';
19
- import { initGraphStore, getGraphStore } from '../store/graph-store.js';
19
+ import { initGraphStore } from '../store/graph-store.js';
20
20
  import type { TeamStore } from '../team/team-store.js';
21
21
  import { loadDotenv } from '../config/dotenv-loader.js';
22
22
  import { resetDotenv } from '../config/dotenv-loader.js';
@@ -309,8 +309,7 @@ async function handleApi(
309
309
  }
310
310
 
311
311
  case '/graph': {
312
- await initGraphStore(effectiveDataDir);
313
- const gStore = getGraphStore();
312
+ const gStore = await initGraphStore(effectiveDataDir, effectiveProjectId);
314
313
  const graph = { entities: gStore.loadEntities(), relations: gStore.loadRelations() };
315
314
  const graphObs = filterDashboardObservations(
316
315
  await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }),
@@ -360,8 +359,8 @@ async function handleApi(
360
359
  }
361
360
 
362
361
  case '/stats': {
363
- await initGraphStore(effectiveDataDir);
364
- const graph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
362
+ const gStore = await initGraphStore(effectiveDataDir, effectiveProjectId);
363
+ const graph = { entities: gStore.loadEntities(), relations: gStore.loadRelations() };
365
364
  const observations = filterDashboardObservations(
366
365
  await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }),
367
366
  effectiveProjectId,
@@ -584,10 +583,10 @@ async function handleApi(
584
583
  case '/knowledge-graph': {
585
584
  const { generateKnowledgeGraph } = await import('../wiki/knowledge-graph.js');
586
585
  const { initMiniSkillStore, getMiniSkillStore } = await import('../store/mini-skill-store.js');
587
- const { initGraphStore, getGraphStore } = await import('../store/graph-store.js');
586
+ const { initGraphStore } = await import('../store/graph-store.js');
588
587
 
589
588
  await initMiniSkillStore(effectiveDataDir);
590
- await initGraphStore(effectiveDataDir);
589
+ const gStore = await initGraphStore(effectiveDataDir, effectiveProjectId);
591
590
 
592
591
  const allObs = filterDashboardObservations(
593
592
  await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }),
@@ -595,7 +594,7 @@ async function handleApi(
595
594
  );
596
595
  const skills = await getMiniSkillStore().loadByProject(effectiveProjectId);
597
596
 
598
- const fullGraph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
597
+ const fullGraph = { entities: gStore.loadEntities(), relations: gStore.loadRelations() };
599
598
  const scoped = scopeKnowledgeGraphToProject(fullGraph, allObs);
600
599
 
601
600
  const graph = generateKnowledgeGraph({
@@ -848,8 +847,7 @@ async function handleApi(
848
847
 
849
848
  // Sync: clean up graph entity references for this observation
850
849
  try {
851
- await initGraphStore(effectiveDataDir);
852
- const gStore = getGraphStore();
850
+ const gStore = await initGraphStore(effectiveDataDir, effectiveProjectId);
853
851
  const prefix = `[#${obsId}] `;
854
852
  const deletions: { entityName: string; observations: string[] }[] = [];
855
853
  for (const entity of gStore.loadEntities()) {
@@ -865,8 +863,8 @@ async function handleApi(
865
863
  }
866
864
 
867
865
  if (apiPath === '/export') {
868
- await initGraphStore(effectiveDataDir);
869
- const fullGraph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
866
+ const gStore = await initGraphStore(effectiveDataDir, effectiveProjectId);
867
+ const fullGraph = { entities: gStore.loadEntities(), relations: gStore.loadRelations() };
870
868
  const observations = filterDashboardObservations(
871
869
  await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }),
872
870
  effectiveProjectId,
@@ -72,7 +72,7 @@ export const MEMORIX_INTEGRATIONS: MemorixIntegration[] = [
72
72
  status: 'ready',
73
73
  install: 'memorix setup --agent grok --global',
74
74
  surfaces: ['hooks', 'AGENTS.md'],
75
- note: 'Writes ~/.grok/hooks/memorix.json and leaves Grok MCP host-owned; setup does not write config.toml.',
75
+ note: 'Writes ~/.grok/hooks/memorix.json and leaves Grok MCP host-owned by default. Pass --mcp http to write [mcp_servers.memorix] in Grok config.toml.',
76
76
  },
77
77
  {
78
78
  agent: 'cursor',
@@ -11,18 +11,21 @@
11
11
  */
12
12
 
13
13
  import type { Entity, Relation, KnowledgeGraph } from '../types.js';
14
- import { initGraphStore, getGraphStore } from '../store/graph-store.js';
14
+ import { initGraphStore, type GraphSqliteStore } from '../store/graph-store.js';
15
15
 
16
16
  export class KnowledgeGraphManager {
17
17
  private entities: Entity[] = [];
18
18
  private relations: Relation[] = [];
19
19
  private projectDir: string;
20
+ private projectId: string;
21
+ private store: GraphSqliteStore | null = null;
20
22
  private initialized = false;
21
23
  /** Index: lowercase entity name → Entity for O(1) lookups */
22
24
  private entityIndex = new Map<string, Entity>();
23
25
 
24
- constructor(projectDir: string) {
26
+ constructor(projectDir: string, projectId = '__default__') {
25
27
  this.projectDir = projectDir;
28
+ this.projectId = projectId;
26
29
  }
27
30
 
28
31
  /** Rebuild the entity name index */
@@ -36,10 +39,9 @@ export class KnowledgeGraphManager {
36
39
  /** Load graph from SQLite on first access */
37
40
  async init(): Promise<void> {
38
41
  if (this.initialized) return;
39
- await initGraphStore(this.projectDir);
40
- const store = getGraphStore();
41
- this.entities = store.loadEntities();
42
- this.relations = store.loadRelations();
42
+ this.store = await initGraphStore(this.projectDir, this.projectId);
43
+ this.entities = this.store.loadEntities();
44
+ this.relations = this.store.loadRelations();
43
45
  this.rebuildIndex();
44
46
  this.initialized = true;
45
47
  }
@@ -56,8 +58,8 @@ export class KnowledgeGraphManager {
56
58
 
57
59
  /** Persist current state to SQLite */
58
60
  private async save(): Promise<void> {
59
- const store = getGraphStore();
60
- store.replaceAll(this.entities, this.relations);
61
+ if (!this.store) throw new Error('[memorix] KnowledgeGraphManager not initialized');
62
+ this.store.replaceAll(this.entities, this.relations);
61
63
  }
62
64
 
63
65
  /** Create new entities (skip duplicates by name) */
@@ -616,7 +616,7 @@ export async function getSessionContext(
616
616
  let graphNeighbors: string[] = [];
617
617
  if (!reader && activeEntities.length > 0) {
618
618
  try {
619
- const graphMgr = new KnowledgeGraphManager(projectDir);
619
+ const graphMgr = new KnowledgeGraphManager(projectDir, projectId);
620
620
  await graphMgr.init();
621
621
  const { relations } = await graphMgr.readGraph();
622
622
  const activeSet = new Set(activeEntities.map((n) => n.toLowerCase()));
package/src/sdk.ts CHANGED
@@ -28,6 +28,7 @@ import type {
28
28
  ObservationReader,
29
29
  RetrievalQuality,
30
30
  } from './types.js';
31
+ import path from 'node:path';
31
32
  import {
32
33
  canManageObservation,
33
34
  canReadObservation,
@@ -101,6 +102,9 @@ export interface ResolveResult {
101
102
  notFound: number[];
102
103
  }
103
104
 
105
+ const activeSdkClients = new Map<string, number>();
106
+ let activeSdkDataDir: string | null = null;
107
+
104
108
  /**
105
109
  * A lightweight, self-contained memory client for reading and writing
106
110
  * Memorix observations without MCP overhead.
@@ -124,6 +128,7 @@ export class MemoryClient {
124
128
  private _oramaStore!: typeof import('./store/orama-store.js');
125
129
  private _obsStore!: typeof import('./store/obs-store.js');
126
130
  private _freshness!: typeof import('./memory/freshness.js');
131
+ private _sdkRefRegistered = false;
127
132
 
128
133
  /** @internal — use createMemoryClient() instead */
129
134
  constructor(projectId: string, projectRoot: string, dataDir: string) {
@@ -147,6 +152,12 @@ export class MemoryClient {
147
152
  * Called by createMemoryClient(). Do not call directly.
148
153
  */
149
154
  async _init(silent: boolean): Promise<void> {
155
+ const normalizedDataDir = path.resolve(this._dataDir);
156
+ if (activeSdkDataDir && activeSdkDataDir !== normalizedDataDir) {
157
+ throw new Error(
158
+ '[memorix-sdk] Multiple active clients with different data directories are not supported in one process; use separate processes.',
159
+ );
160
+ }
150
161
  // Suppress logs if requested
151
162
  const originalError = console.error;
152
163
  if (silent) {
@@ -167,6 +178,9 @@ export class MemoryClient {
167
178
 
168
179
  // Prepare search index (hydrate Orama from SQLite)
169
180
  await this._observations.prepareSearchIndex();
181
+ activeSdkDataDir = normalizedDataDir;
182
+ activeSdkClients.set(normalizedDataDir, (activeSdkClients.get(normalizedDataDir) ?? 0) + 1);
183
+ this._sdkRefRegistered = true;
170
184
  } finally {
171
185
  if (silent) {
172
186
  console.error = originalError;
@@ -298,6 +312,16 @@ export class MemoryClient {
298
312
  async close(): Promise<void> {
299
313
  if (this._closed) return;
300
314
  this._closed = true;
315
+ const normalizedDataDir = path.resolve(this._dataDir);
316
+ const currentRefs = activeSdkClients.get(normalizedDataDir) ?? 0;
317
+ const remainingRefs = Math.max(0, currentRefs - (this._sdkRefRegistered ? 1 : 0));
318
+ if (remainingRefs > 0) {
319
+ activeSdkClients.set(normalizedDataDir, remainingRefs);
320
+ this._sdkRefRegistered = false;
321
+ return;
322
+ }
323
+ activeSdkClients.delete(normalizedDataDir);
324
+ if (activeSdkDataDir === normalizedDataDir) activeSdkDataDir = null;
301
325
  try {
302
326
  this._obsStore.resetObservationStore();
303
327
  await this._oramaStore.resetDb();
@@ -1,6 +1,8 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { getDatabase } from '../store/sqlite-db.js';
3
3
 
4
+ export const DEFAULT_MCP_BINDING_TTL_MS = 30 * 24 * 60 * 60 * 1000;
5
+
4
6
  export interface McpProjectBinding {
5
7
  handleId: string;
6
8
  projectId: string;
@@ -38,6 +40,7 @@ export class McpBindingStore {
38
40
 
39
41
  create(input: { projectId: string; projectRoot: string; dataDir: string; ttlMs?: number }): McpProjectBinding {
40
42
  const now = new Date();
43
+ const ttlMs = input.ttlMs ?? DEFAULT_MCP_BINDING_TTL_MS;
41
44
  const binding: McpProjectBinding = {
42
45
  handleId: `mxh_${randomUUID().replace(/-/g, '')}`,
43
46
  projectId: input.projectId,
@@ -45,7 +48,7 @@ export class McpBindingStore {
45
48
  dataDir: input.dataDir,
46
49
  createdAt: now.toISOString(),
47
50
  lastUsedAt: now.toISOString(),
48
- ...(input.ttlMs && input.ttlMs > 0 ? { expiresAt: new Date(now.getTime() + input.ttlMs).toISOString() } : {}),
51
+ ...(ttlMs > 0 ? { expiresAt: new Date(now.getTime() + ttlMs).toISOString() } : {}),
49
52
  };
50
53
  this.requireDb().prepare(`
51
54
  INSERT INTO mcp_bindings (handle_id, project_id, project_root, data_dir, created_at, last_used_at, expires_at)
@@ -69,8 +72,16 @@ export class McpBindingStore {
69
72
  const binding = this.get(handleId);
70
73
  if (!binding) return undefined;
71
74
  const lastUsedAt = new Date().toISOString();
72
- this.requireDb().prepare(`UPDATE mcp_bindings SET last_used_at = ? WHERE handle_id = ?`).run(lastUsedAt, handleId);
73
- return { ...binding, lastUsedAt };
75
+ const expiresAt = new Date(Date.now() + DEFAULT_MCP_BINDING_TTL_MS).toISOString();
76
+ this.requireDb().prepare(`UPDATE mcp_bindings SET last_used_at = ?, expires_at = ? WHERE handle_id = ?`).run(lastUsedAt, expiresAt, handleId);
77
+ return { ...binding, lastUsedAt, expiresAt };
78
+ }
79
+
80
+ cleanupExpired(now = Date.now()): number {
81
+ const result = this.requireDb().prepare(
82
+ `DELETE FROM mcp_bindings WHERE expires_at IS NOT NULL AND expires_at <= ?`,
83
+ ).run(new Date(now).toISOString());
84
+ return Number(result.changes ?? 0);
74
85
  }
75
86
 
76
87
  /** Reuse the most recent valid extension handle for a verified project root. */
package/src/server.ts CHANGED
@@ -364,7 +364,7 @@ export async function createMemorixServer(
364
364
  const store = getObservationStore();
365
365
  console.error(`[memorix] ObservationStore backend: ${store.getBackendName()}, generation: ${store.getGeneration()}`);
366
366
  }
367
- let graphManager = new KnowledgeGraphManager(projectDir);
367
+ let graphManager = new KnowledgeGraphManager(projectDir, project.id);
368
368
  if (!deferProjectRuntimeInit) {
369
369
  await graphManager.init();
370
370
  await initObservations(projectDir);
@@ -378,7 +378,7 @@ export async function createMemorixServer(
378
378
  await initObservationStore(projectDir);
379
379
  await initMiniSkillStore(projectDir);
380
380
  await initSessionStore(projectDir);
381
- graphManager = new KnowledgeGraphManager(projectDir);
381
+ graphManager = new KnowledgeGraphManager(projectDir, project.id);
382
382
  await graphManager.init();
383
383
  await initObservations(projectDir);
384
384