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.
- package/CHANGELOG.md +33 -1
- package/README.md +6 -5
- package/README.zh-CN.md +6 -5
- package/dist/cli/index.js +890 -325
- package/dist/index.d.ts +3 -1
- package/dist/index.js +251 -104
- package/dist/maintenance-runner.js +144 -113
- package/dist/media-audio-runner.js +57 -26
- package/dist/media-video-runner.js +57 -26
- package/dist/memcode-runtime/CHANGELOG.md +33 -1
- package/dist/sdk.d.ts +1 -0
- package/dist/sdk.js +1012 -842
- package/dist/types.d.ts +1 -1
- package/dist/vector-backfill-runner.js +57 -26
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +47 -3
- package/docs/API_REFERENCE.md +3 -1
- package/docs/CONFIGURATION.md +15 -1
- package/docs/DOCKER.md +23 -3
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/KNOWN_ISSUES_AND_ROADMAP.md +26 -1
- package/docs/MULTI-DEVICE-SYNC-RESEARCH.md +4 -0
- package/docs/SETUP.md +55 -0
- package/package.json +2 -2
- package/plugins/antigravity/memorix/plugin.json +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/hermes/memorix/plugin.yaml +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/cli/command-guide.ts +2 -2
- package/src/cli/commands/agent-integrations.ts +57 -0
- package/src/cli/commands/background.ts +11 -2
- package/src/cli/commands/serve-http.ts +193 -42
- package/src/cli/commands/setup.ts +77 -10
- package/src/cli/commands/sync-store.ts +47 -12
- package/src/cli/commands/sync.ts +26 -0
- package/src/dashboard/server.ts +10 -12
- package/src/integrations/registry.ts +1 -1
- package/src/memory/graph.ts +10 -8
- package/src/memory/session.ts +1 -1
- package/src/sdk.ts +24 -0
- package/src/server/mcp-binding-store.ts +14 -3
- package/src/server.ts +2 -2
- package/src/store/file-lock.ts +28 -3
- package/src/store/graph-store.ts +178 -46
- package/src/store/persistence.ts +14 -9
- package/src/store/sqlite-db.ts +6 -3
- package/src/store/sqlite-store.ts +32 -27
- package/src/sync/engine.ts +9 -7
- package/src/sync/namespace.ts +23 -0
- package/src/sync/policy.ts +19 -4
- package/src/sync/store-port.ts +43 -13
- package/src/sync/types.ts +2 -0
- package/src/types.ts +1 -1
- 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
|
|
216
|
-
//
|
|
217
|
-
const mcp = options.agent === 'grok'
|
|
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'
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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 {
|
|
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:
|
|
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:
|
|
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:
|
|
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;
|
package/src/cli/commands/sync.ts
CHANGED
|
@@ -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] || '';
|
package/src/dashboard/server.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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',
|
package/src/memory/graph.ts
CHANGED
|
@@ -11,18 +11,21 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { Entity, Relation, KnowledgeGraph } from '../types.js';
|
|
14
|
-
import { initGraphStore,
|
|
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
|
-
|
|
41
|
-
this.
|
|
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
|
-
|
|
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) */
|
package/src/memory/session.ts
CHANGED
|
@@ -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
|
-
...(
|
|
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
|
-
|
|
73
|
-
|
|
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
|
|