memorix 1.8.4 → 1.8.6
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 +52 -0
- package/CLAUDE.md +3 -0
- package/README.md +8 -6
- package/README.zh-CN.md +5 -3
- package/assets/star-history-dark.svg +5 -5
- package/assets/star-history-light.svg +5 -5
- package/assets/star-history.json +35 -5
- package/dist/cli/index.js +1848 -754
- package/dist/dashboard/static/app.js +29 -885
- package/dist/dashboard/static/index.html +1 -5
- package/dist/dashboard/static/style.css +36 -503
- package/dist/index.d.ts +4 -4
- package/dist/index.js +286 -79
- package/dist/memcode/index.js +28035 -169
- package/dist/memcode-runtime/CHANGELOG.md +52 -0
- package/dist/sdk.js +103 -21
- package/dist/types.d.ts +1 -1
- package/docs/1.8.0-RELEASE-CANDIDATE-SPEC.md +22 -16
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +2 -2
- package/docs/API_REFERENCE.md +10 -8
- package/docs/DASHBOARD.md +23 -0
- package/docs/DEVELOPMENT.md +2 -2
- package/docs/DOCKER.md +6 -3
- package/docs/INTEGRATIONS.md +2 -1
- package/docs/MODULES.md +46 -20
- package/docs/PERFORMANCE.md +1 -1
- package/docs/README.md +8 -7
- package/docs/SETUP.md +4 -3
- package/package.json +11 -11
- package/plugins/antigravity/memorix/plugin.json +1 -1
- package/plugins/antigravity/memorix/rules/memorix.md +5 -0
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/memorix/skills/memorix-memory/SKILL.md +3 -0
- package/plugins/claude/memorix/skills/memorix-mini-skills/SKILL.md +2 -0
- package/plugins/claude/memorix/skills/memorix-orchestrate/SKILL.md +2 -0
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/skills/memorix-memory/SKILL.md +3 -0
- package/plugins/codebuddy/memorix/skills/memorix-mini-skills/SKILL.md +2 -0
- package/plugins/codebuddy/memorix/skills/memorix-orchestrate/SKILL.md +2 -0
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/skills/memorix-memory/SKILL.md +3 -0
- package/plugins/codex/memorix/skills/memorix-mini-skills/SKILL.md +2 -0
- package/plugins/codex/memorix/skills/memorix-orchestrate/SKILL.md +2 -0
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/copilot/memorix/skills/memorix-memory/SKILL.md +3 -0
- package/plugins/copilot/memorix/skills/memorix-mini-skills/SKILL.md +2 -0
- package/plugins/copilot/memorix/skills/memorix-orchestrate/SKILL.md +2 -0
- package/plugins/cursor/memorix/skills/memorix-memory/SKILL.md +3 -0
- package/plugins/cursor/memorix/skills/memorix-mini-skills/SKILL.md +2 -0
- package/plugins/cursor/memorix/skills/memorix-orchestrate/SKILL.md +2 -0
- package/plugins/gemini/memorix/GEMINI.md +5 -0
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/hermes/memorix/plugin.yaml +7 -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/plugins/pi/memorix/skills/memorix-memory/SKILL.md +3 -0
- package/plugins/pi/memorix/skills/memorix-mini-skills/SKILL.md +2 -0
- package/plugins/pi/memorix/skills/memorix-orchestrate/SKILL.md +2 -0
- package/server.json +2 -2
- package/src/cli/command-guide.ts +52 -1
- package/src/cli/commands/agent-integrations.ts +1 -0
- package/src/cli/commands/background.ts +166 -6
- package/src/cli/commands/hooks-install.ts +9 -2
- package/src/cli/commands/hooks-preview.ts +5 -0
- package/src/cli/commands/integrate.ts +2 -0
- package/src/cli/commands/serve-http.ts +73 -80
- package/src/cli/commands/serve.ts +95 -93
- package/src/cli/commands/setup.ts +11 -1
- package/src/cli/commands/uninstall.ts +24 -2
- package/src/cli/index.ts +1 -1
- package/src/codegraph/task-lens.ts +72 -7
- package/src/dashboard/static/index.html +1 -5
- package/src/dashboard/static/style.css +36 -503
- package/src/hooks/installers/index.ts +58 -0
- package/src/hooks/official-skills.ts +13 -6
- package/src/hooks/types.ts +2 -0
- package/src/integrations/registry.ts +9 -0
- package/src/memory/graph.ts +2 -2
- package/src/server/mcp-diagnostics.ts +9 -7
- package/src/server/modern-mcp-bridge.ts +108 -0
- package/src/server/stdio-startup-gate.ts +41 -2
- package/src/server/tool-profile.ts +7 -6
- package/src/server.ts +1 -1
- package/src/store/orama-store.ts +2 -2
- package/src/types.ts +1 -1
- package/src/wiki/types.ts +2 -2
- package/src/workspace/mcp-adapters/workbuddy.ts +71 -0
package/src/cli/command-guide.ts
CHANGED
|
@@ -61,6 +61,10 @@ const GUIDES: Record<string, CliCommandGuide> = {
|
|
|
61
61
|
usage: [
|
|
62
62
|
'memorix codegraph status',
|
|
63
63
|
'memorix codegraph refresh',
|
|
64
|
+
'memorix codegraph init',
|
|
65
|
+
'memorix codegraph sync',
|
|
66
|
+
'memorix codegraph diff [--from <snapshot>] [--to <snapshot>]',
|
|
67
|
+
'memorix codegraph impact --path <relative-source-path>',
|
|
64
68
|
'memorix codegraph context-pack --task "trace the auth flow" [--limit 20]',
|
|
65
69
|
],
|
|
66
70
|
},
|
|
@@ -79,10 +83,57 @@ const GUIDES: Record<string, CliCommandGuide> = {
|
|
|
79
83
|
usage: [
|
|
80
84
|
'memorix knowledge init [--mode local|versioned]',
|
|
81
85
|
'memorix knowledge status',
|
|
86
|
+
'memorix knowledge claims',
|
|
87
|
+
'memorix knowledge review --id <claim-id> --review approved|rejected --detail "..."',
|
|
82
88
|
'memorix knowledge compile',
|
|
83
89
|
'memorix knowledge lint',
|
|
84
|
-
'memorix knowledge
|
|
90
|
+
'memorix knowledge apply --proposal <id> [--force]',
|
|
91
|
+
'memorix knowledge workflow import|list',
|
|
92
|
+
'memorix knowledge workflow select --task "prepare a release"',
|
|
93
|
+
'memorix knowledge workflow preview|apply --id <workflow-id> --agent codex',
|
|
94
|
+
'memorix knowledge workflow run --id <workflow-id> --task "..." --outcome passed',
|
|
85
95
|
],
|
|
96
|
+
notes: ['Claims need source review before they can become maintained Knowledge Workspace pages.'],
|
|
97
|
+
},
|
|
98
|
+
evidence: {
|
|
99
|
+
summary: 'Inspect the source, freshness, and verification state behind project memories.',
|
|
100
|
+
usage: [
|
|
101
|
+
'memorix evidence list [--query "auth"] [--limit 20]',
|
|
102
|
+
'memorix evidence get --id 42',
|
|
103
|
+
'memorix evidence sync',
|
|
104
|
+
'memorix evidence stale --paths src/server.ts,tests/server.test.ts --reason "source changed"',
|
|
105
|
+
'memorix evidence events --id 42',
|
|
106
|
+
],
|
|
107
|
+
notes: ['Evidence cards point back to observations and source files; they are not a second memory store.'],
|
|
108
|
+
},
|
|
109
|
+
feedback: {
|
|
110
|
+
summary: 'Record and inspect whether a memory helped, conflicted, or was corrected.',
|
|
111
|
+
usage: [
|
|
112
|
+
'memorix feedback record --id 42 --signal verification-success --source test:run-1',
|
|
113
|
+
'memorix feedback show --id 42',
|
|
114
|
+
'memorix feedback audit --id 42 [--limit 100]',
|
|
115
|
+
],
|
|
116
|
+
notes: ['Feedback changes the candidate score through an auditable event; it does not rewrite the original memory.'],
|
|
117
|
+
},
|
|
118
|
+
formation: {
|
|
119
|
+
summary: 'Inspect Memory Formation quality metrics collected by the runtime.',
|
|
120
|
+
usage: ['memorix formation metrics'],
|
|
121
|
+
notes: ['Metrics are empty until the formation pipeline has processed observations.'],
|
|
122
|
+
},
|
|
123
|
+
media: {
|
|
124
|
+
summary: 'Import, inspect, derive, embed, and clean controlled local media assets.',
|
|
125
|
+
usage: [
|
|
126
|
+
'memorix media import --path ./diagram.png',
|
|
127
|
+
'memorix media attach --asset <asset-id> --title "Architecture diagram"',
|
|
128
|
+
'memorix media list --kind image',
|
|
129
|
+
'memorix media show --asset <asset-id>',
|
|
130
|
+
'memorix media derive-pdf|derive-audio --asset <asset-id> [--attach]',
|
|
131
|
+
'memorix media embed|similar --asset <asset-id>',
|
|
132
|
+
'memorix media generate image|video --prompt "..."',
|
|
133
|
+
'memorix media status|cancel --job <media-job-id>',
|
|
134
|
+
'memorix media cleanup [--max-bytes <limit>]',
|
|
135
|
+
],
|
|
136
|
+
notes: ['Media generation is explicit and provider-gated; importing a file does not automatically send it to a model.'],
|
|
86
137
|
},
|
|
87
138
|
reasoning: {
|
|
88
139
|
summary: 'Capture and retrieve engineering decision rationale.',
|
|
@@ -42,6 +42,10 @@ interface BackgroundState {
|
|
|
42
42
|
startCwd?: string;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
const BACKGROUND_LOCK_STALE_MS = 15_000;
|
|
46
|
+
const BACKGROUND_LOCK_ACQUIRE_TIMEOUT_MS = 10_000;
|
|
47
|
+
const BACKGROUND_LOCK_HEARTBEAT_MS = 2_000;
|
|
48
|
+
|
|
45
49
|
export function resolveBackgroundCwd(cwd: string, homeDir: string = os.homedir()): string {
|
|
46
50
|
if (!isHomeDirectory(cwd, homeDir)) return cwd;
|
|
47
51
|
const inherited = inheritProjectRootFromHome({
|
|
@@ -71,6 +75,10 @@ function getLogFilePath(): string {
|
|
|
71
75
|
return getMemorixDir() + '/background.log';
|
|
72
76
|
}
|
|
73
77
|
|
|
78
|
+
function getBackgroundLockPath(): string {
|
|
79
|
+
return getMemorixDir() + '/background.lock';
|
|
80
|
+
}
|
|
81
|
+
|
|
74
82
|
function parseBoundedPositiveInteger(value: string | undefined, fallback: number, label: string, maximum: number): number {
|
|
75
83
|
const raw = value ?? String(fallback);
|
|
76
84
|
if (!/^[0-9]+$/u.test(raw)) {
|
|
@@ -276,20 +284,21 @@ async function startWindowsBackgroundService(options: WindowsBackgroundLaunchOpt
|
|
|
276
284
|
}
|
|
277
285
|
|
|
278
286
|
async function healthCheck(port: number, timeoutMs = 3000): Promise<{ ok: boolean; data?: any; error?: string }> {
|
|
287
|
+
const controller = new AbortController();
|
|
288
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
279
289
|
try {
|
|
280
|
-
const controller = new AbortController();
|
|
281
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
282
290
|
// Use /health endpoint — lightweight, no TeamStore or heavy init required.
|
|
283
291
|
// Responds immediately once the HTTP server has bound the port.
|
|
284
292
|
const res = await fetch(`http://127.0.0.1:${port}/health`, {
|
|
285
293
|
signal: controller.signal,
|
|
286
294
|
});
|
|
287
|
-
clearTimeout(timer);
|
|
288
295
|
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
|
|
289
296
|
const data = await res.json();
|
|
290
297
|
return { ok: true, data };
|
|
291
298
|
} catch (err) {
|
|
292
299
|
return { ok: false, error: err instanceof Error ? err.message : 'Unknown error' };
|
|
300
|
+
} finally {
|
|
301
|
+
clearTimeout(timer);
|
|
293
302
|
}
|
|
294
303
|
}
|
|
295
304
|
|
|
@@ -299,10 +308,95 @@ async function isPortInUse(port: number): Promise<boolean> {
|
|
|
299
308
|
}
|
|
300
309
|
|
|
301
310
|
// ============================================================
|
|
311
|
+
// Background start lock
|
|
312
|
+
// ============================================================
|
|
313
|
+
|
|
314
|
+
interface BackgroundLock {
|
|
315
|
+
fd: number | null;
|
|
316
|
+
acquired: boolean;
|
|
317
|
+
path: string;
|
|
318
|
+
token: string;
|
|
319
|
+
heartbeat?: ReturnType<typeof setInterval>;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Hold an exclusive lock across the complete check/spawn/readiness sequence.
|
|
324
|
+
* The heartbeat prevents a normal slow startup from being mistaken for a
|
|
325
|
+
* crashed caller; an old lock without a live heartbeat can still be reclaimed.
|
|
326
|
+
*/
|
|
327
|
+
async function acquireBackgroundLock(
|
|
328
|
+
lockPath: string,
|
|
329
|
+
staleMs: number = BACKGROUND_LOCK_STALE_MS,
|
|
330
|
+
timeoutMs: number = BACKGROUND_LOCK_ACQUIRE_TIMEOUT_MS,
|
|
331
|
+
): Promise<BackgroundLock> {
|
|
332
|
+
const lockDir = path.dirname(lockPath);
|
|
333
|
+
if (!fs.existsSync(lockDir)) fs.mkdirSync(lockDir, { recursive: true });
|
|
334
|
+
|
|
335
|
+
const deadline = Date.now() + timeoutMs;
|
|
336
|
+
while (Date.now() < deadline) {
|
|
337
|
+
try {
|
|
338
|
+
const fd = fs.openSync(lockPath, 'wx');
|
|
339
|
+
const token = `${process.pid}:${randomBytes(12).toString('hex')}`;
|
|
340
|
+
fs.writeSync(fd, JSON.stringify({ pid: process.pid, acquiredAt: Date.now(), token }));
|
|
341
|
+
const heartbeat = setInterval(() => {
|
|
342
|
+
try {
|
|
343
|
+
const now = new Date();
|
|
344
|
+
// Update the inode held by this descriptor. If an operator or a
|
|
345
|
+
// recovering caller replaced the path, an old owner must not touch
|
|
346
|
+
// the replacement lock's mtime and keep it alive forever.
|
|
347
|
+
fs.futimesSync(fd, now, now);
|
|
348
|
+
} catch {
|
|
349
|
+
// The owner will fail naturally if its lock was removed by an operator.
|
|
350
|
+
}
|
|
351
|
+
}, BACKGROUND_LOCK_HEARTBEAT_MS);
|
|
352
|
+
heartbeat.unref();
|
|
353
|
+
return { fd, acquired: true, path: lockPath, token, heartbeat };
|
|
354
|
+
} catch (err) {
|
|
355
|
+
if (!(err && (err as NodeJS.ErrnoException).code === 'EEXIST')) throw err;
|
|
356
|
+
try {
|
|
357
|
+
if (Date.now() - fs.statSync(lockPath).mtimeMs > staleMs) {
|
|
358
|
+
let ownerAlive = false;
|
|
359
|
+
try {
|
|
360
|
+
const raw = fs.readFileSync(lockPath, 'utf8');
|
|
361
|
+
const owner = JSON.parse(raw) as { pid?: unknown };
|
|
362
|
+
ownerAlive = typeof owner.pid === 'number'
|
|
363
|
+
&& owner.pid > 0
|
|
364
|
+
&& isProcessRunning(owner.pid);
|
|
365
|
+
} catch {
|
|
366
|
+
// A malformed old lock has no trustworthy owner and can be reclaimed.
|
|
367
|
+
}
|
|
368
|
+
if (!ownerAlive) {
|
|
369
|
+
fs.unlinkSync(lockPath);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
} catch {
|
|
374
|
+
// Another caller may have released or reclaimed it; retry the exclusive open.
|
|
375
|
+
}
|
|
376
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
throw new Error(
|
|
381
|
+
`Another Memorix background start is still in progress (${lockPath}). `
|
|
382
|
+
+ 'Wait a few seconds and retry; no second control plane was started.',
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function releaseBackgroundLock(lock: BackgroundLock): void {
|
|
387
|
+
if (!lock.acquired) return;
|
|
388
|
+
if (lock.heartbeat) clearInterval(lock.heartbeat);
|
|
389
|
+
try { if (lock.fd !== null) fs.closeSync(lock.fd); } catch { /* already closed */ }
|
|
390
|
+
try {
|
|
391
|
+
const current = JSON.parse(fs.readFileSync(lock.path, 'utf8')) as { token?: unknown };
|
|
392
|
+
if (current.token === lock.token) fs.unlinkSync(lock.path);
|
|
393
|
+
} catch { /* already released, reclaimed, or malformed */ }
|
|
394
|
+
}
|
|
395
|
+
|
|
302
396
|
// Subcommands
|
|
303
397
|
// ============================================================
|
|
304
398
|
|
|
305
|
-
|
|
399
|
+
async function doStartUnlocked(port: number): Promise<void> {
|
|
306
400
|
// 1. Check if already running — validate both PID existence AND HTTP health
|
|
307
401
|
const state = loadState();
|
|
308
402
|
if (state) {
|
|
@@ -491,6 +585,15 @@ export async function doStart(port: number): Promise<void> {
|
|
|
491
585
|
process.stderr.write(footer + '\n');
|
|
492
586
|
}
|
|
493
587
|
|
|
588
|
+
export async function doStart(port: number): Promise<void> {
|
|
589
|
+
const lock = await acquireBackgroundLock(getBackgroundLockPath());
|
|
590
|
+
try {
|
|
591
|
+
await doStartUnlocked(port);
|
|
592
|
+
} finally {
|
|
593
|
+
releaseBackgroundLock(lock);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
494
597
|
export async function doStop(): Promise<void> {
|
|
495
598
|
const state = loadState();
|
|
496
599
|
|
|
@@ -559,13 +662,24 @@ export async function doStop(): Promise<void> {
|
|
|
559
662
|
console.log('[OK] Control plane stopped.');
|
|
560
663
|
}
|
|
561
664
|
|
|
562
|
-
async function doStatus(): Promise<void> {
|
|
665
|
+
async function doStatus(asJson = false): Promise<void> {
|
|
563
666
|
const state = loadState();
|
|
564
667
|
|
|
565
668
|
if (!state) {
|
|
566
669
|
// No background.json — but check if port has an unmanaged foreground instance
|
|
567
670
|
const portHealth = await healthCheck(3211, 2000);
|
|
568
671
|
if (portHealth.ok) {
|
|
672
|
+
if (asJson) {
|
|
673
|
+
console.log(JSON.stringify({
|
|
674
|
+
status: 'unmanaged',
|
|
675
|
+
managed: false,
|
|
676
|
+
port: 3211,
|
|
677
|
+
dashboard: 'http://127.0.0.1:3211/',
|
|
678
|
+
mcp: 'http://127.0.0.1:3211/mcp',
|
|
679
|
+
health: portHealth.data ?? null,
|
|
680
|
+
}, null, 2));
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
569
683
|
console.log('');
|
|
570
684
|
console.log('No background control plane is registered,');
|
|
571
685
|
console.log('but a Memorix instance IS running on port 3211 (likely a foreground "memorix serve-http").');
|
|
@@ -585,6 +699,10 @@ async function doStatus(): Promise<void> {
|
|
|
585
699
|
console.log(' 2. Run "memorix background start"');
|
|
586
700
|
console.log('');
|
|
587
701
|
} else {
|
|
702
|
+
if (asJson) {
|
|
703
|
+
console.log(JSON.stringify({ status: 'stopped', managed: false, port: 3211 }, null, 2));
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
588
706
|
console.log('No background control plane is registered.');
|
|
589
707
|
console.log('');
|
|
590
708
|
console.log('Start one with: memorix background start');
|
|
@@ -600,6 +718,36 @@ async function doStatus(): Promise<void> {
|
|
|
600
718
|
// PID reuse detection: PID alive but /health fails or returns different PID
|
|
601
719
|
const pidMismatch = health.ok && health.data?.pid && health.data.pid !== state.pid;
|
|
602
720
|
const probablyReused = running && !health.ok;
|
|
721
|
+
const status = health.ok && !pidMismatch
|
|
722
|
+
? 'running'
|
|
723
|
+
: pidMismatch
|
|
724
|
+
? 'pid-mismatch'
|
|
725
|
+
: probablyReused
|
|
726
|
+
? 'pid-reused'
|
|
727
|
+
: running
|
|
728
|
+
? 'starting'
|
|
729
|
+
: 'stopped';
|
|
730
|
+
|
|
731
|
+
if (asJson) {
|
|
732
|
+
if (pidMismatch || probablyReused || !running) {
|
|
733
|
+
clearState();
|
|
734
|
+
try { fs.unlinkSync(getMemorixDir() + '/background.ready'); } catch { /* best effort */ }
|
|
735
|
+
}
|
|
736
|
+
console.log(JSON.stringify({
|
|
737
|
+
status,
|
|
738
|
+
managed: true,
|
|
739
|
+
pid: state.pid,
|
|
740
|
+
port: state.port,
|
|
741
|
+
startedAt: state.startedAt,
|
|
742
|
+
instance: state.instanceToken ? `${state.instanceToken.slice(0, 8)}…` : null,
|
|
743
|
+
dashboard: `http://127.0.0.1:${state.port}/`,
|
|
744
|
+
mcp: `http://127.0.0.1:${state.port}/mcp`,
|
|
745
|
+
logFile: normalizePath(state.logFile),
|
|
746
|
+
health: health.ok ? health.data ?? null : null,
|
|
747
|
+
error: health.ok ? null : health.error ?? (running ? 'Health check failed' : 'Process not running'),
|
|
748
|
+
}, null, 2));
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
603
751
|
|
|
604
752
|
console.log('');
|
|
605
753
|
console.log('Memorix Background Control Plane');
|
|
@@ -629,6 +777,9 @@ async function doStatus(): Promise<void> {
|
|
|
629
777
|
console.log(` PID: ${d.pid ?? 'unknown'}`);
|
|
630
778
|
console.log(` Uptime: ${d.uptime ?? 0}s`);
|
|
631
779
|
console.log(` Mode: ${d.mode ?? 'unknown'}`);
|
|
780
|
+
console.log(` Profile: ${d.profile ?? 'unknown'}`);
|
|
781
|
+
console.log(` Project: ${d.projectId ?? 'unresolved'}${d.projectName ? ` (${d.projectName})` : ''}`);
|
|
782
|
+
console.log(` Ready at: ${d.readyAt ?? 'unknown'}`);
|
|
632
783
|
}
|
|
633
784
|
|
|
634
785
|
if (pidMismatch) {
|
|
@@ -770,6 +921,11 @@ export default defineCommand({
|
|
|
770
921
|
description: 'Number of log lines to show (default: 50)',
|
|
771
922
|
required: false,
|
|
772
923
|
},
|
|
924
|
+
json: {
|
|
925
|
+
type: 'boolean',
|
|
926
|
+
description: 'Emit a machine-readable status receipt',
|
|
927
|
+
required: false,
|
|
928
|
+
},
|
|
773
929
|
},
|
|
774
930
|
run: async ({ args }) => {
|
|
775
931
|
try {
|
|
@@ -784,7 +940,7 @@ export default defineCommand({
|
|
|
784
940
|
await doStop();
|
|
785
941
|
break;
|
|
786
942
|
case 'status':
|
|
787
|
-
await doStatus();
|
|
943
|
+
await doStatus(!!args.json);
|
|
788
944
|
break;
|
|
789
945
|
case 'restart':
|
|
790
946
|
await doRestart(port);
|
|
@@ -813,6 +969,7 @@ export default defineCommand({
|
|
|
813
969
|
console.log(' --port <port> HTTP port (default: 3211)');
|
|
814
970
|
console.log(' --follow, -f Follow log output in real-time (for "logs")');
|
|
815
971
|
console.log(' --lines, -n Number of log lines to show (default: 50)');
|
|
972
|
+
console.log(' --json Emit a machine-readable status receipt (for "status")');
|
|
816
973
|
console.log('');
|
|
817
974
|
console.log('Mode distinction:');
|
|
818
975
|
console.log(' Quick Mode = stdio / single project / zero friction');
|
|
@@ -838,4 +995,7 @@ export const _testing = {
|
|
|
838
995
|
resolveBackgroundCliEntry,
|
|
839
996
|
resolveBackgroundCliEntryFrom,
|
|
840
997
|
resolveBackgroundCwd,
|
|
998
|
+
acquireBackgroundLock,
|
|
999
|
+
releaseBackgroundLock,
|
|
1000
|
+
getBackgroundLockPath,
|
|
841
1001
|
};
|
|
@@ -115,8 +115,14 @@ async function installSingleAgent(agent: string, cwd: string, global: boolean):
|
|
|
115
115
|
cwd,
|
|
116
116
|
global,
|
|
117
117
|
);
|
|
118
|
-
|
|
118
|
+
const note = (config.generated as Record<string, unknown>)?.note;
|
|
119
|
+
if (config.configPath) {
|
|
120
|
+
console.log(`[OK] ${agent}: hooks installed -> ${config.configPath}`);
|
|
121
|
+
} else {
|
|
122
|
+
console.log(`[OK] ${agent}: no hooks installed at this scope (MCP-only integration)`);
|
|
123
|
+
}
|
|
119
124
|
console.log(` Events: ${config.events.join(', ')}`);
|
|
125
|
+
if (!config.configPath && note) console.log(` Note: ${note}`);
|
|
120
126
|
|
|
121
127
|
// Copilot on Windows without pwsh: warn about runtime compatibility
|
|
122
128
|
if (agent === 'copilot' && process.platform === 'win32') {
|
|
@@ -136,7 +142,6 @@ async function installSingleAgent(agent: string, cwd: string, global: boolean):
|
|
|
136
142
|
|
|
137
143
|
// Copilot global not supported
|
|
138
144
|
if (agent === 'copilot' && global) {
|
|
139
|
-
const note = (config.generated as Record<string, unknown>)?.note;
|
|
140
145
|
if (note) console.warn(`[WARN] ${note}`);
|
|
141
146
|
}
|
|
142
147
|
} catch (err) {
|
|
@@ -160,6 +165,7 @@ function getAgentLabel(agent: string): string {
|
|
|
160
165
|
omp: 'Oh-my-Pi',
|
|
161
166
|
trae: 'Trae',
|
|
162
167
|
dsh: 'DeepSeek Harness',
|
|
168
|
+
workbuddy: 'WorkBuddy',
|
|
163
169
|
};
|
|
164
170
|
return labels[agent] || agent;
|
|
165
171
|
}
|
|
@@ -179,6 +185,7 @@ function getAgentHint(agent: string): string {
|
|
|
179
185
|
omp: 'Oh-my-Pi package hooks; use `memorix setup --agent omp`',
|
|
180
186
|
trae: '.trae/rules/project_rules.md (rules only, no hooks system)',
|
|
181
187
|
dsh: '~/.dsh/cordis.patch.yml MCP row via `memorix setup --agent dsh` (no hooks system)',
|
|
188
|
+
workbuddy: '~/.workbuddy/mcp.json MCP row via `memorix setup --agent workbuddy` (no hooks system)',
|
|
182
189
|
codex: 'Codex plugin hooks; use `memorix setup --agent codex`',
|
|
183
190
|
};
|
|
184
191
|
return hints[agent] || '';
|
|
@@ -153,6 +153,7 @@ function estimateFileSize(agent: string, configPath: string): string {
|
|
|
153
153
|
opencode: '~3KB',
|
|
154
154
|
trae: '~1KB',
|
|
155
155
|
dsh: '~1KB',
|
|
156
|
+
workbuddy: '~1KB',
|
|
156
157
|
};
|
|
157
158
|
return sizes[agent] || '~2KB';
|
|
158
159
|
}
|
|
@@ -174,6 +175,8 @@ function getRulesPath(agent: string, cwd: string, global: boolean): string {
|
|
|
174
175
|
return path.join(home, '.trae', 'rules', 'project_rules.md');
|
|
175
176
|
case 'dsh':
|
|
176
177
|
return path.join(process.env.DSH_HOME?.trim() || path.join(home, '.dsh'), 'AGENTS.md');
|
|
178
|
+
case 'workbuddy':
|
|
179
|
+
return '';
|
|
177
180
|
default:
|
|
178
181
|
return '';
|
|
179
182
|
}
|
|
@@ -191,6 +194,8 @@ function getRulesPath(agent: string, cwd: string, global: boolean): string {
|
|
|
191
194
|
return path.join(cwd, '.trae', 'rules', 'project_rules.md');
|
|
192
195
|
case 'dsh':
|
|
193
196
|
return path.join(cwd, 'AGENTS.md');
|
|
197
|
+
case 'workbuddy':
|
|
198
|
+
return path.join(cwd, 'AGENTS.md');
|
|
194
199
|
default:
|
|
195
200
|
return '';
|
|
196
201
|
}
|
|
@@ -115,6 +115,7 @@ function getAgentLabel(agent: string): string {
|
|
|
115
115
|
'gemini-cli': 'Gemini CLI',
|
|
116
116
|
trae: 'Trae',
|
|
117
117
|
dsh: 'DeepSeek Harness',
|
|
118
|
+
workbuddy: 'WorkBuddy',
|
|
118
119
|
};
|
|
119
120
|
return labels[agent] || agent;
|
|
120
121
|
}
|
|
@@ -132,6 +133,7 @@ function getAgentHint(agent: string): string {
|
|
|
132
133
|
'gemini-cli': 'settings + GEMINI.md',
|
|
133
134
|
trae: 'rules',
|
|
134
135
|
dsh: 'MCP patch + AGENTS.md + skills',
|
|
136
|
+
workbuddy: 'MCP + AGENTS.md',
|
|
135
137
|
};
|
|
136
138
|
return hints[agent] || 'integration files';
|
|
137
139
|
}
|
|
@@ -27,6 +27,7 @@ import { scopeKnowledgeGraphToProject } from '../../memory/graph-scope.js';
|
|
|
27
27
|
import { projectObservationRetention, summarizeRetentionProjections } from '../../memory/retention.js';
|
|
28
28
|
import { canManageObservation, filterReadableObservations } from '../../memory/visibility.js';
|
|
29
29
|
import { CURRENT_MCP_PROTOCOL_VERSION, MCP_SERVER_NAME, getMcpDiagnostics } from '../../server/mcp-diagnostics.js';
|
|
30
|
+
import { createMcpStartupDiscoverResult, getMcpServerInfo } from '../../server/mcp-discovery.js';
|
|
30
31
|
import { parseTcpPortOrReport } from '../port.js';
|
|
31
32
|
import { mcpFileUriToPath } from '../mcp-root-path.js';
|
|
32
33
|
|
|
@@ -130,6 +131,9 @@ export default defineCommand({
|
|
|
130
131
|
// node:fs from being evaluated before their mock factories initialize.
|
|
131
132
|
const { McpBindingStore } = await import('../../server/mcp-binding-store.js');
|
|
132
133
|
const { MemoryFeedbackStore } = await import('../../memory/feedback.js');
|
|
134
|
+
const { createModernMcpBridge } = await import('../../server/modern-mcp-bridge.js');
|
|
135
|
+
const { createMcpHandler } = await import('@modelcontextprotocol/server');
|
|
136
|
+
const { toNodeHandler } = await import('@modelcontextprotocol/node');
|
|
133
137
|
|
|
134
138
|
// The [server] config section feeds startup defaults; CLI flags win.
|
|
135
139
|
let configuredDefaults: ServeHttpConfiguredDefaults = {};
|
|
@@ -197,6 +201,32 @@ export default defineCommand({
|
|
|
197
201
|
const statelessBindingStore = new McpBindingStore();
|
|
198
202
|
await statelessBindingStore.init(persistMod.getBaseDataDir());
|
|
199
203
|
|
|
204
|
+
// Modern MCP (2026-07-28) is stateless at the protocol layer. The official
|
|
205
|
+
// v2 handler owns discovery, per-request envelopes, result metadata,
|
|
206
|
+
// subscriptions, and version errors. Product tools are bridged to the
|
|
207
|
+
// existing v1 implementation so business semantics stay in one place.
|
|
208
|
+
const modernMcpHandler = createMcpHandler(async ({ requestInfo }) => {
|
|
209
|
+
const handleId = requestInfo?.headers.get('mcp-project-handle') ?? undefined;
|
|
210
|
+
const persisted = handleId ? statelessBindingStore.touch(handleId) : undefined;
|
|
211
|
+
const requestedRoot = requestInfo?.headers.get('mcp-project-root') ?? undefined;
|
|
212
|
+
const root = persisted?.projectRoot ?? requestedRoot ?? projectRoot;
|
|
213
|
+
return createModernMcpBridge({
|
|
214
|
+
projectRoot: root,
|
|
215
|
+
allowUntrackedFallback: false,
|
|
216
|
+
deferProjectInitUntilBound: true,
|
|
217
|
+
deferProjectRuntimeInit: true,
|
|
218
|
+
dashboardMode: 'control-plane',
|
|
219
|
+
dashboardPort: port,
|
|
220
|
+
toolProfile,
|
|
221
|
+
});
|
|
222
|
+
}, {
|
|
223
|
+
// Legacy requests are routed to the existing sessionful implementation
|
|
224
|
+
// below; this handler is strict modern-only by construction.
|
|
225
|
+
legacy: 'reject',
|
|
226
|
+
responseMode: 'auto',
|
|
227
|
+
});
|
|
228
|
+
const modernNodeHandler = toNodeHandler(modernMcpHandler);
|
|
229
|
+
|
|
200
230
|
type SessionState = {
|
|
201
231
|
transport: InstanceType<typeof StreamableHTTPServerTransport>;
|
|
202
232
|
server: Awaited<ReturnType<typeof createMemorixServer>>['server'];
|
|
@@ -418,85 +448,6 @@ export default defineCommand({
|
|
|
418
448
|
res.setHeader('Mcp-Name', MCP_SERVER_NAME);
|
|
419
449
|
}
|
|
420
450
|
|
|
421
|
-
/** Handle one 2026 stateless MCP request with a fresh server/transport. */
|
|
422
|
-
async function handleStatelessPost(req: IncomingMessage, res: ServerResponse, body: unknown): Promise<void> {
|
|
423
|
-
const handleId = typeof req.headers['mcp-project-handle'] === 'string'
|
|
424
|
-
? req.headers['mcp-project-handle']
|
|
425
|
-
: undefined;
|
|
426
|
-
const persisted = handleId ? statelessBindingStore.touch(handleId) : undefined;
|
|
427
|
-
if (handleId && !persisted) {
|
|
428
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
429
|
-
res.end(JSON.stringify({ jsonrpc: '2.0', error: { code: -32001, message: 'MCP project handle not found or expired; initialize again.' }, id: null }));
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
const requestedRoot = typeof req.headers['mcp-project-root'] === 'string'
|
|
433
|
-
? req.headers['mcp-project-root']
|
|
434
|
-
: undefined;
|
|
435
|
-
const root = persisted?.projectRoot ?? requestedRoot ?? projectRoot;
|
|
436
|
-
|
|
437
|
-
const { createMemorixServer } = await import('../../server.js');
|
|
438
|
-
const { createProjectBindingController } = await import('../../server/request-context.js');
|
|
439
|
-
const binding = createProjectBindingController(root);
|
|
440
|
-
const { server, projectId, handleTransportClose } = await createMemorixServer(
|
|
441
|
-
root,
|
|
442
|
-
undefined,
|
|
443
|
-
undefined,
|
|
444
|
-
{
|
|
445
|
-
allowUntrackedFallback: false,
|
|
446
|
-
deferProjectInitUntilBound: true,
|
|
447
|
-
deferProjectRuntimeInit: true,
|
|
448
|
-
dashboardMode: 'control-plane',
|
|
449
|
-
dashboardPort: port,
|
|
450
|
-
toolProfile,
|
|
451
|
-
projectBinding: binding,
|
|
452
|
-
},
|
|
453
|
-
);
|
|
454
|
-
const transport = new StreamableHTTPServerTransport({
|
|
455
|
-
// No Mcp-Session-Id: the protocol state is carried by our explicit
|
|
456
|
-
// durable project handle and reconstructed on every request.
|
|
457
|
-
sessionIdGenerator: undefined,
|
|
458
|
-
enableJsonResponse: true,
|
|
459
|
-
});
|
|
460
|
-
try {
|
|
461
|
-
await server.connect(transport);
|
|
462
|
-
let effectiveHandle = persisted;
|
|
463
|
-
if (!effectiveHandle && projectId !== '__unresolved__' && isInitializeRequest(body)) {
|
|
464
|
-
effectiveHandle = statelessBindingStore.create({
|
|
465
|
-
projectId,
|
|
466
|
-
projectRoot: root,
|
|
467
|
-
dataDir: await persistMod.getProjectDataDir(projectId),
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
if (effectiveHandle) res.setHeader('Mcp-Project-Handle', effectiveHandle.handleId);
|
|
471
|
-
// The 1.30 SDK predates the 2026-07-28 version string. Keep the
|
|
472
|
-
// external stateless headers at the current contract, but normalize
|
|
473
|
-
// only the SDK-facing request and initialize params to its newest
|
|
474
|
-
// accepted wire version. This is a real compatibility adapter, not a
|
|
475
|
-
// claim that the pinned SDK implements newer optional extensions.
|
|
476
|
-
const sdkBody = body && typeof body === 'object'
|
|
477
|
-
? JSON.parse(JSON.stringify(body)) as Record<string, any>
|
|
478
|
-
: body;
|
|
479
|
-
const sdkRequest = sdkBody as Record<string, any> | undefined;
|
|
480
|
-
if (sdkRequest && typeof sdkRequest === 'object' && sdkRequest.method === 'initialize' && sdkRequest.params) {
|
|
481
|
-
sdkRequest.params.protocolVersion = MCP_SDK_COMPAT_PROTOCOL_VERSION;
|
|
482
|
-
}
|
|
483
|
-
if (req.headers['mcp-protocol-version']) {
|
|
484
|
-
req.headers['mcp-protocol-version'] = MCP_SDK_COMPAT_PROTOCOL_VERSION;
|
|
485
|
-
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
|
486
|
-
if (req.rawHeaders[i].toLowerCase() === 'mcp-protocol-version') {
|
|
487
|
-
req.rawHeaders[i + 1] = MCP_SDK_COMPAT_PROTOCOL_VERSION;
|
|
488
|
-
break;
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
patchMcpResponse(res);
|
|
493
|
-
await transport.handleRequest(req, res, sdkBody);
|
|
494
|
-
} finally {
|
|
495
|
-
handleTransportClose();
|
|
496
|
-
await transport.close().catch(() => undefined);
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
451
|
/**
|
|
501
452
|
* Handle POST /mcp — JSON-RPC requests from agents
|
|
502
453
|
*/
|
|
@@ -504,13 +455,49 @@ export default defineCommand({
|
|
|
504
455
|
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
505
456
|
const body = await parseBody(req);
|
|
506
457
|
|
|
458
|
+
// Keep the pre-2026 startup probe compatible. Older MCP clients sent a
|
|
459
|
+
// claim-less server/discover before initialize and expect a plain JSON
|
|
460
|
+
// response; modern requests carry the _meta envelope and go to v2.
|
|
461
|
+
const params = body && typeof body === 'object' && !Array.isArray(body)
|
|
462
|
+
? (body as { params?: unknown }).params
|
|
463
|
+
: undefined;
|
|
464
|
+
const claimlessDiscovery = !sessionId
|
|
465
|
+
&& isServerDiscoverRequest(body)
|
|
466
|
+
&& !req.headers['mcp-protocol-version']
|
|
467
|
+
&& !req.headers['mcp-method']
|
|
468
|
+
&& req.headers['mcp-stateless'] !== 'true'
|
|
469
|
+
&& (params === undefined
|
|
470
|
+
|| (typeof params === 'object' && params !== null && !Array.isArray(params)
|
|
471
|
+
&& !Object.prototype.hasOwnProperty.call(params, '_meta')));
|
|
472
|
+
if (claimlessDiscovery) {
|
|
473
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
474
|
+
res.end(JSON.stringify({
|
|
475
|
+
jsonrpc: '2.0',
|
|
476
|
+
id: (body as { id?: string | number | null }).id ?? null,
|
|
477
|
+
result: createMcpStartupDiscoverResult(getMcpServerInfo()),
|
|
478
|
+
}));
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
507
482
|
const requestedProtocol = req.headers['mcp-protocol-version'];
|
|
508
483
|
const statelessRequested = req.headers['mcp-stateless'] === 'true'
|
|
509
484
|
|| requestedProtocol === CURRENT_MCP_PROTOCOL_VERSION
|
|
510
485
|
|| typeof req.headers['mcp-project-handle'] === 'string'
|
|
511
486
|
|| isServerDiscoverRequest(body);
|
|
512
487
|
if (!sessionId && statelessRequested) {
|
|
513
|
-
|
|
488
|
+
const handleId = typeof req.headers['mcp-project-handle'] === 'string'
|
|
489
|
+
? req.headers['mcp-project-handle']
|
|
490
|
+
: undefined;
|
|
491
|
+
if (handleId && !statelessBindingStore.touch(handleId)) {
|
|
492
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
493
|
+
res.end(JSON.stringify({
|
|
494
|
+
jsonrpc: '2.0',
|
|
495
|
+
error: { code: -32001, message: 'MCP project handle not found or expired; initialize again.' },
|
|
496
|
+
id: null,
|
|
497
|
+
}));
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
await modernNodeHandler(req, res, body);
|
|
514
501
|
return;
|
|
515
502
|
}
|
|
516
503
|
|
|
@@ -1684,6 +1671,7 @@ export default defineCommand({
|
|
|
1684
1671
|
}
|
|
1685
1672
|
}
|
|
1686
1673
|
|
|
1674
|
+
let httpReadyAt: string | null = null;
|
|
1687
1675
|
const httpServer = createServer(async (req, res) => {
|
|
1688
1676
|
setCorsHeaders(req, res);
|
|
1689
1677
|
|
|
@@ -1704,9 +1692,13 @@ export default defineCommand({
|
|
|
1704
1692
|
res.end(JSON.stringify({
|
|
1705
1693
|
status: 'ok',
|
|
1706
1694
|
mode: 'control-plane',
|
|
1695
|
+
profile: toolProfile,
|
|
1707
1696
|
port,
|
|
1708
1697
|
uptime: Math.round(process.uptime()),
|
|
1709
1698
|
pid: process.pid,
|
|
1699
|
+
projectId: defaultProject?.id ?? '__unresolved__',
|
|
1700
|
+
projectName: defaultProject?.name ?? null,
|
|
1701
|
+
readyAt: httpReadyAt,
|
|
1710
1702
|
embedding: getEmbeddingRuntimeHealth(),
|
|
1711
1703
|
}));
|
|
1712
1704
|
return;
|
|
@@ -1791,6 +1783,7 @@ export default defineCommand({
|
|
|
1791
1783
|
httpServer.headersTimeout = 65_000;
|
|
1792
1784
|
|
|
1793
1785
|
httpServer.listen(port, host, () => {
|
|
1786
|
+
httpReadyAt = new Date().toISOString();
|
|
1794
1787
|
// Write readiness file — background start polls this for out-of-band readiness detection
|
|
1795
1788
|
try {
|
|
1796
1789
|
const memorixDir = (process.env.HOME || process.env.USERPROFILE || '').replace(/\\/g, '/') + '/.memorix';
|