memorix 1.2.7 → 1.2.9
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 +15 -0
- package/dist/cli/index.js +19531 -19017
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/app.js +7 -2
- package/dist/index.js +7870 -7497
- package/dist/index.js.map +1 -1
- package/dist/maintenance-jobs-o1rYSFcM.d.ts +36 -0
- package/dist/maintenance-runner.d.ts +1 -28
- package/dist/maintenance-runner.js +5937 -5596
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +15 -0
- package/dist/sdk.js +7866 -7493
- package/dist/sdk.js.map +1 -1
- package/dist/vector-backfill-runner.d.ts +31 -0
- package/dist/vector-backfill-runner.js +12670 -0
- package/dist/vector-backfill-runner.js.map +1 -0
- package/docs/dev-log/progress.txt +25 -0
- package/package.json +3 -3
- package/plugins/claude/memorix/.claude-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/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/src/cli/commands/agent-integrations.ts +102 -5
- package/src/cli/commands/operator-shared.ts +4 -1
- package/src/cli/commands/serve-http.ts +58 -50
- package/src/cli/commands/setup.ts +44 -0
- package/src/dashboard/server.ts +54 -63
- package/src/memory/observations.ts +175 -99
- package/src/memory/retention.ts +106 -28
- package/src/memory/session.ts +71 -12
- package/src/runtime/maintenance-jobs.ts +84 -4
- package/src/runtime/project-maintenance.ts +63 -6
- package/src/runtime/vector-backfill-runner.ts +144 -0
- package/src/search/intent-detector.ts +39 -1
- package/src/server.ts +5 -5
|
@@ -339,3 +339,28 @@
|
|
|
339
339
|
isolated Claude Code handoff/continuation smoke. `npm publish --dry-run
|
|
340
340
|
--ignore-scripts` accepted the public 1.2.7 tarball; the full prepublish
|
|
341
341
|
build/test path is covered by the root release gates above.
|
|
342
|
+
|
|
343
|
+
## 1.2.8 Vector Recovery and Codex Installation Clarity
|
|
344
|
+
- Reworked vector backfill failure handling so transient provider or local
|
|
345
|
+
index failures leave one durable, diagnosable retry with bounded backoff
|
|
346
|
+
instead of consuming the maintenance retry budget in each new MCP process.
|
|
347
|
+
Healthy work clears stale diagnostics, revives an old failed vector job when
|
|
348
|
+
needed, and resolves superseded historical failure rows once recovery is
|
|
349
|
+
complete. Provider calls are batched while process-local Orama writes remain
|
|
350
|
+
serialized.
|
|
351
|
+
- Corrected dashboard and HTTP control-plane vector reporting. A process that
|
|
352
|
+
does not own a hydrated MCP search index now states that vector status belongs
|
|
353
|
+
to the active MCP session rather than showing a misleading green `0 / 0`.
|
|
354
|
+
- Corrected Codex install diagnostics: an enabled official Codex plugin owns
|
|
355
|
+
its `.mcp.json` `memorix serve` endpoint, while first-use hook approval is an
|
|
356
|
+
explicit host consent step rather than a broken installation. Plugin setup
|
|
357
|
+
removes only the legacy source-path `memorix` entry after successful plugin
|
|
358
|
+
installation; custom user-managed MCP entries are preserved.
|
|
359
|
+
- Release verification: focused regression tests (96 assertions), `npm run
|
|
360
|
+
lint`, production build, and the full root test suite (all passed, 280s).
|
|
361
|
+
A live OpenRouter `qwen/qwen3-embedding-8b` batch returned two 4096d vectors.
|
|
362
|
+
An isolated tarball install reported CLI version `1.2.8`; its stdio MCP
|
|
363
|
+
handshake exposed 44 tools and completed `memorix_project_context`, and its
|
|
364
|
+
CLI `context --json` path returned a structured Workset. `npm pack --dry-run`
|
|
365
|
+
and `npm publish --dry-run --ignore-scripts` both accepted the final tarball
|
|
366
|
+
without manifest normalization warnings.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memorix",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.9",
|
|
4
4
|
"description": "Local-first shared memory layer for AI coding agents across MCP clients, Git history, reasoning context, and project sessions.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"packages/memcode"
|
|
12
12
|
],
|
|
13
13
|
"bin": {
|
|
14
|
-
"memorix": "
|
|
15
|
-
"memcode": "
|
|
14
|
+
"memorix": "dist/cli/index.js",
|
|
15
|
+
"memcode": "dist/cli/memcode.js"
|
|
16
16
|
},
|
|
17
17
|
"main": "./dist/index.js",
|
|
18
18
|
"types": "./dist/index.d.ts",
|
|
@@ -26,6 +26,8 @@ export interface AgentMcpCheck {
|
|
|
26
26
|
exists: boolean;
|
|
27
27
|
status: AgentIntegrationStatus;
|
|
28
28
|
issues: string[];
|
|
29
|
+
/** This endpoint is supplied by an enabled Codex plugin, not config.toml. */
|
|
30
|
+
managedByPlugin?: boolean;
|
|
29
31
|
server?: {
|
|
30
32
|
transport: 'stdio' | 'http';
|
|
31
33
|
command?: string;
|
|
@@ -229,6 +231,10 @@ function codexPluginPath(): string {
|
|
|
229
231
|
return `${homedir()}/.codex/plugins/${CODEX_PLUGIN_NAME}`;
|
|
230
232
|
}
|
|
231
233
|
|
|
234
|
+
function codexPluginMcpPath(): string {
|
|
235
|
+
return `${codexPluginPath()}/.mcp.json`;
|
|
236
|
+
}
|
|
237
|
+
|
|
232
238
|
function codexMarketplacePath(): string {
|
|
233
239
|
return `${homedir()}/.agents/plugins/marketplace.json`;
|
|
234
240
|
}
|
|
@@ -267,6 +273,7 @@ async function inspectCodexPluginBundle(): Promise<AgentPluginCheck> {
|
|
|
267
273
|
const pluginPath = codexPluginPath();
|
|
268
274
|
const manifestPath = `${pluginPath}/.codex-plugin/plugin.json`;
|
|
269
275
|
const hooksPath = `${pluginPath}/hooks/hooks.json`;
|
|
276
|
+
const mcpPath = codexPluginMcpPath();
|
|
270
277
|
if (!existsSync(pluginPath)) {
|
|
271
278
|
return {
|
|
272
279
|
scope: 'global',
|
|
@@ -317,6 +324,20 @@ async function inspectCodexPluginBundle(): Promise<AgentPluginCheck> {
|
|
|
317
324
|
if (version !== getCliVersion()) issues.push('codex-plugin-version-mismatch');
|
|
318
325
|
if (manifest.hooks !== './hooks/hooks.json') issues.push('codex-hook-manifest-missing');
|
|
319
326
|
|
|
327
|
+
if (!existsSync(mcpPath)) {
|
|
328
|
+
issues.push('codex-plugin-mcp-manifest-missing');
|
|
329
|
+
} else {
|
|
330
|
+
try {
|
|
331
|
+
const mcpConfig = asRecord(JSON.parse(await readFile(mcpPath, 'utf-8')));
|
|
332
|
+
const server = coerceMcpServer('memorix', asRecord(mcpConfig?.mcpServers)?.memorix);
|
|
333
|
+
if (!server || !isRecommendedStdioServer(server)) {
|
|
334
|
+
issues.push('codex-plugin-mcp-manifest-invalid');
|
|
335
|
+
}
|
|
336
|
+
} catch {
|
|
337
|
+
issues.push('codex-plugin-mcp-manifest-unreadable');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
320
341
|
let declared: string[] = [];
|
|
321
342
|
if (!existsSync(hooksPath)) {
|
|
322
343
|
issues.push('codex-hook-manifest-missing');
|
|
@@ -479,7 +500,9 @@ async function inspectCodexHookTrust(): Promise<AgentPluginCheck> {
|
|
|
479
500
|
kind: 'hook-trust',
|
|
480
501
|
path: configPath,
|
|
481
502
|
exists: true,
|
|
482
|
-
|
|
503
|
+
// Codex records hook approval only after the user has reviewed it. Memorix
|
|
504
|
+
// must not treat an intentional, host-owned consent step as a broken setup.
|
|
505
|
+
status: issues.length > 0 ? 'skipped' : 'ok',
|
|
483
506
|
issues,
|
|
484
507
|
hookTrust: { trusted, expected: [...CODEX_HOOK_STATE_NAMES] },
|
|
485
508
|
};
|
|
@@ -562,7 +585,7 @@ function getClaudeLocalConfigPath(): string {
|
|
|
562
585
|
return `${homedir()}/.claude.json`;
|
|
563
586
|
}
|
|
564
587
|
|
|
565
|
-
function
|
|
588
|
+
function coerceMcpServer(name: string, value: unknown): MCPServerEntry | null {
|
|
566
589
|
const entry = asRecord(value);
|
|
567
590
|
if (!entry) return null;
|
|
568
591
|
const args = Array.isArray(entry.args) ? entry.args.map(String) : [];
|
|
@@ -626,7 +649,7 @@ async function inspectClaudeLocalMcp(projectRoot: string): Promise<AgentMcpCheck
|
|
|
626
649
|
}
|
|
627
650
|
|
|
628
651
|
const servers = asRecord(localProject.project.mcpServers);
|
|
629
|
-
const server =
|
|
652
|
+
const server = coerceMcpServer('memorix', servers?.memorix);
|
|
630
653
|
if (!server) {
|
|
631
654
|
return {
|
|
632
655
|
scope: 'local',
|
|
@@ -683,6 +706,59 @@ async function installClaudeLocalMcpConfig(projectRoot: string): Promise<void> {
|
|
|
683
706
|
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
|
|
684
707
|
}
|
|
685
708
|
|
|
709
|
+
async function inspectCodexPluginMcp(): Promise<AgentMcpCheck | null> {
|
|
710
|
+
const runtime = inspectCodexPluginRuntime();
|
|
711
|
+
if (!runtime.runtime?.installed || !runtime.runtime.enabled) return null;
|
|
712
|
+
|
|
713
|
+
const mcpPath = codexPluginMcpPath();
|
|
714
|
+
if (!existsSync(mcpPath)) {
|
|
715
|
+
return {
|
|
716
|
+
scope: 'global',
|
|
717
|
+
path: mcpPath,
|
|
718
|
+
exists: false,
|
|
719
|
+
status: 'repairable',
|
|
720
|
+
issues: ['codex-plugin-mcp-manifest-missing'],
|
|
721
|
+
managedByPlugin: true,
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
try {
|
|
726
|
+
const config = asRecord(JSON.parse(await readFile(mcpPath, 'utf-8')));
|
|
727
|
+
const server = coerceMcpServer('memorix', asRecord(config?.mcpServers)?.memorix);
|
|
728
|
+
if (!server) {
|
|
729
|
+
return {
|
|
730
|
+
scope: 'global',
|
|
731
|
+
path: mcpPath,
|
|
732
|
+
exists: true,
|
|
733
|
+
status: 'repairable',
|
|
734
|
+
issues: ['codex-plugin-mcp-manifest-invalid'],
|
|
735
|
+
managedByPlugin: true,
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
const issues: string[] = [];
|
|
739
|
+
if (looksLikeStaleMemorixCommand(server)) issues.push('stale-command-path');
|
|
740
|
+
if (!isRecommendedStdioServer(server)) issues.push('nonstandard-mcp-command');
|
|
741
|
+
return {
|
|
742
|
+
scope: 'global',
|
|
743
|
+
path: mcpPath,
|
|
744
|
+
exists: true,
|
|
745
|
+
status: issues.length > 0 ? 'repairable' : 'ok',
|
|
746
|
+
issues,
|
|
747
|
+
managedByPlugin: true,
|
|
748
|
+
server: sanitizeServer(server),
|
|
749
|
+
};
|
|
750
|
+
} catch {
|
|
751
|
+
return {
|
|
752
|
+
scope: 'global',
|
|
753
|
+
path: mcpPath,
|
|
754
|
+
exists: true,
|
|
755
|
+
status: 'repairable',
|
|
756
|
+
issues: ['codex-plugin-mcp-manifest-unreadable'],
|
|
757
|
+
managedByPlugin: true,
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
686
762
|
async function inspectMcp(agent: AgentName, projectRoot: string, scope: AgentIntegrationScope): Promise<AgentIntegrationEntry['mcp']> {
|
|
687
763
|
if (!isMcpConfigAgent(agent)) {
|
|
688
764
|
return { status: 'skipped', issues: ['mcp-managed-by-package'], checks: [] };
|
|
@@ -697,6 +773,14 @@ async function inspectMcp(agent: AgentName, projectRoot: string, scope: AgentInt
|
|
|
697
773
|
continue;
|
|
698
774
|
}
|
|
699
775
|
|
|
776
|
+
if (agent === 'codex' && targetScope === 'global') {
|
|
777
|
+
const pluginCheck = await inspectCodexPluginMcp();
|
|
778
|
+
if (pluginCheck) {
|
|
779
|
+
checks.push(pluginCheck);
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
700
784
|
const configPath = adapter.getConfigPath(targetScope === 'project' ? projectRoot : undefined);
|
|
701
785
|
if (targetScope === 'global' && configPath === adapter.getConfigPath(projectRoot)) continue;
|
|
702
786
|
|
|
@@ -872,11 +956,20 @@ export function formatAgentIntegrationReport(report: AgentIntegrationReport): st
|
|
|
872
956
|
lines.push(`${entry.agent}: ${status}`);
|
|
873
957
|
if (entry.mcp.issues.length > 0) lines.push(` MCP: ${entry.mcp.issues.join(', ')}`);
|
|
874
958
|
if (entry.guidance.issues.length > 0) lines.push(` Guidance: ${entry.guidance.issues.join(', ')}`);
|
|
875
|
-
|
|
959
|
+
const hookTrustPending = entry.plugin.issues.includes('codex-hook-trust-pending');
|
|
960
|
+
const pluginIssues = entry.plugin.issues.filter((issue) => issue !== 'codex-hook-trust-pending');
|
|
961
|
+
if (pluginIssues.length > 0) lines.push(` Plugin: ${pluginIssues.join(', ')}`);
|
|
962
|
+
if (hookTrustPending) {
|
|
963
|
+
lines.push(' Hooks: waiting for Codex approval on first use; MCP remains available.');
|
|
964
|
+
}
|
|
876
965
|
}
|
|
877
966
|
|
|
878
967
|
lines.push('');
|
|
879
|
-
|
|
968
|
+
if (report.summary.missing > 0 || report.summary.repairable > 0) {
|
|
969
|
+
lines.push(`Repair: ${report.repairCommand}`);
|
|
970
|
+
} else {
|
|
971
|
+
lines.push('No file repair needed.');
|
|
972
|
+
}
|
|
880
973
|
return lines.join('\n');
|
|
881
974
|
}
|
|
882
975
|
|
|
@@ -899,6 +992,10 @@ export async function repairAgentIntegrations(options: {
|
|
|
899
992
|
if (isMcpConfigAgent(entry.agent)) {
|
|
900
993
|
for (const check of entry.mcp.checks) {
|
|
901
994
|
if (check.status === 'ok' || check.status === 'skipped') continue;
|
|
995
|
+
if (check.managedByPlugin) {
|
|
996
|
+
skipped.push(`${entry.agent}:mcp:${check.scope}:plugin-managed`);
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
902
999
|
if (check.status === 'missing' && !canInstallMissing) {
|
|
903
1000
|
skipped.push(`${entry.agent}:mcp:${check.scope}:missing`);
|
|
904
1001
|
continue;
|
|
@@ -110,7 +110,10 @@ export async function getCliProjectContext(options?: CliContextOptions): Promise
|
|
|
110
110
|
} catch {
|
|
111
111
|
// CLI reads remain available even if optional background maintenance metadata fails.
|
|
112
112
|
}
|
|
113
|
-
await initObservations(dataDir
|
|
113
|
+
await initObservations(dataDir, {
|
|
114
|
+
embeddingWriteMode: 'deferred',
|
|
115
|
+
projectRoot: project.rootPath,
|
|
116
|
+
});
|
|
114
117
|
await initSessionStore(dataDir);
|
|
115
118
|
const teamStore = await initTeamStore(dataDir);
|
|
116
119
|
|
|
@@ -24,6 +24,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
24
24
|
import type { ObservationStore } from '../../store/obs-store.js';
|
|
25
25
|
import { resolveToolProfile } from '../../server/tool-profile.js';
|
|
26
26
|
import { scopeKnowledgeGraphToProject } from '../../memory/graph-scope.js';
|
|
27
|
+
import { projectObservationRetention, summarizeRetentionProjections } from '../../memory/retention.js';
|
|
27
28
|
import { canManageObservation, filterReadableObservations } from '../../memory/visibility.js';
|
|
28
29
|
|
|
29
30
|
export const DEFAULT_SESSION_TIMEOUT_MS = 30 * 60 * 1000;
|
|
@@ -760,20 +761,7 @@ export default defineCommand({
|
|
|
760
761
|
await initGraphStore(statsDataDir);
|
|
761
762
|
const graph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
|
|
762
763
|
|
|
763
|
-
const observations = await loadDashboardProjectObservations(statsDataDir, statsProjectId, 'active')
|
|
764
|
-
type?: string;
|
|
765
|
-
id?: number;
|
|
766
|
-
title?: string;
|
|
767
|
-
entityName?: string;
|
|
768
|
-
createdAt?: string;
|
|
769
|
-
source?: string;
|
|
770
|
-
commitHash?: string;
|
|
771
|
-
filesModified?: string[];
|
|
772
|
-
relatedEntities?: string[];
|
|
773
|
-
status?: string;
|
|
774
|
-
importance?: number;
|
|
775
|
-
accessCount?: number;
|
|
776
|
-
}>;
|
|
764
|
+
const observations = await loadDashboardProjectObservations(statsDataDir, statsProjectId, 'active');
|
|
777
765
|
const statsStore = await getDashboardObservationStore(statsDataDir);
|
|
778
766
|
const nextId = await statsStore.loadIdCounter();
|
|
779
767
|
const typeCounts: Record<string, number> = {};
|
|
@@ -811,20 +799,17 @@ export default defineCommand({
|
|
|
811
799
|
filesModified: o.filesModified,
|
|
812
800
|
}));
|
|
813
801
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
else if (score >= 1) retentionSummary.stale++;
|
|
826
|
-
else retentionSummary.archive++;
|
|
827
|
-
}
|
|
802
|
+
const retention = summarizeRetentionProjections(
|
|
803
|
+
observations
|
|
804
|
+
.filter((observation) => observation.type !== 'probe')
|
|
805
|
+
.map((observation) => projectObservationRetention(observation, { referenceTime: new Date(now) })),
|
|
806
|
+
);
|
|
807
|
+
const retentionSummary = {
|
|
808
|
+
active: retention.active,
|
|
809
|
+
stale: retention.stale,
|
|
810
|
+
archive: retention.archiveCandidates,
|
|
811
|
+
immune: retention.immune,
|
|
812
|
+
};
|
|
828
813
|
|
|
829
814
|
const sorted = [...observations].filter(o => o.type !== 'probe').sort((a, b) => (b.id || 0) - (a.id || 0)).slice(0, 10);
|
|
830
815
|
|
|
@@ -839,10 +824,21 @@ export default defineCommand({
|
|
|
839
824
|
};
|
|
840
825
|
} catch { /* best effort */ }
|
|
841
826
|
|
|
842
|
-
let vectorStatus = {
|
|
827
|
+
let vectorStatus = {
|
|
828
|
+
available: false,
|
|
829
|
+
total: 0,
|
|
830
|
+
missing: 0,
|
|
831
|
+
missingIds: [] as number[],
|
|
832
|
+
backfillRunning: false,
|
|
833
|
+
};
|
|
843
834
|
try {
|
|
844
|
-
const { getVectorStatus } = await import('../../memory/observations.js');
|
|
845
|
-
|
|
835
|
+
const { getSearchIndexStatus, getVectorStatus } = await import('../../memory/observations.js');
|
|
836
|
+
// The HTTP control plane has a different module graph from stdio MCP.
|
|
837
|
+
// Only report vector counts when this process actually owns a hydrated
|
|
838
|
+
// index; an empty singleton must not look like a healthy 0/0 state.
|
|
839
|
+
if (getSearchIndexStatus(statsProjectId).prepared) {
|
|
840
|
+
vectorStatus = { available: true, ...getVectorStatus(statsProjectId) };
|
|
841
|
+
}
|
|
846
842
|
} catch { /* best effort */ }
|
|
847
843
|
|
|
848
844
|
// Real search mode from the last actual search execution
|
|
@@ -948,26 +944,38 @@ export default defineCommand({
|
|
|
948
944
|
|
|
949
945
|
if (apiPath === '/retention') {
|
|
950
946
|
const { projectId: retProjectId, dataDir: retDataDir } = await resolveRequestProject(url);
|
|
951
|
-
const observations = await loadDashboardProjectObservations(retDataDir, retProjectId, 'active')
|
|
952
|
-
const
|
|
953
|
-
const
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
947
|
+
const observations = await loadDashboardProjectObservations(retDataDir, retProjectId, 'active');
|
|
948
|
+
const referenceTime = new Date();
|
|
949
|
+
const rows = observations
|
|
950
|
+
.filter((observation) => observation.type !== 'probe')
|
|
951
|
+
.map((observation) => ({
|
|
952
|
+
observation,
|
|
953
|
+
retention: projectObservationRetention(observation, { referenceTime }),
|
|
954
|
+
}))
|
|
955
|
+
.sort((a, b) => b.retention.displayScore - a.retention.displayScore);
|
|
956
|
+
const summary = summarizeRetentionProjections(rows.map((row) => row.retention));
|
|
957
|
+
const items = rows.map(({ observation, retention }) => ({
|
|
958
|
+
id: observation.id,
|
|
959
|
+
title: observation.title,
|
|
960
|
+
type: observation.type,
|
|
961
|
+
entityName: observation.entityName,
|
|
962
|
+
score: retention.displayScore,
|
|
963
|
+
isImmune: retention.immune,
|
|
964
|
+
zone: retention.zone,
|
|
965
|
+
ageHours: retention.ageHours,
|
|
966
|
+
accessCount: retention.accessCount,
|
|
967
|
+
effectiveRetentionDays: retention.effectiveRetentionDays,
|
|
968
|
+
immunityReason: retention.immunityReason,
|
|
969
|
+
}));
|
|
970
|
+
sendJson({
|
|
971
|
+
summary: {
|
|
972
|
+
active: summary.active,
|
|
973
|
+
stale: summary.stale,
|
|
974
|
+
archive: summary.archiveCandidates,
|
|
975
|
+
immune: summary.immune,
|
|
976
|
+
},
|
|
977
|
+
items,
|
|
964
978
|
});
|
|
965
|
-
scored.sort((a, b) => b.score - a.score);
|
|
966
|
-
const activeCount = scored.filter(s => s.score >= 3).length;
|
|
967
|
-
const staleCount = scored.filter(s => s.score < 3 && s.score >= 1).length;
|
|
968
|
-
const archiveCount = scored.filter(s => s.score < 1).length;
|
|
969
|
-
const immuneCount = scored.filter(s => s.isImmune).length;
|
|
970
|
-
sendJson({ summary: { active: activeCount, stale: staleCount, archive: archiveCount, immune: immuneCount }, items: scored });
|
|
971
979
|
return;
|
|
972
980
|
}
|
|
973
981
|
|
|
@@ -484,6 +484,40 @@ function mergeTomlMcpConfig(existingContent: string | null, generatedContent: st
|
|
|
484
484
|
return `${parts.join('\n\n')}\n`;
|
|
485
485
|
}
|
|
486
486
|
|
|
487
|
+
function isLegacyCodexMemorixNodeServer(server: MCPServerEntry): boolean {
|
|
488
|
+
if (server.name !== 'memorix' || server.command.trim().toLowerCase() !== 'node') return false;
|
|
489
|
+
const startsMemorixStdio = server.args.some((arg) => arg.trim().toLowerCase() === 'serve');
|
|
490
|
+
const sourceEntry = server.args.some((arg) => {
|
|
491
|
+
const normalized = arg.replace(/\\/g, '/').toLowerCase();
|
|
492
|
+
return /\/memorix\/dist\/cli\/index\.(?:[cm]?js)$/.test(normalized);
|
|
493
|
+
});
|
|
494
|
+
return startsMemorixStdio && sourceEntry;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Codex plugins now own Memorix stdio MCP registration. Remove only the old
|
|
499
|
+
* source-path form emitted by pre-plugin setup, never a user's custom server.
|
|
500
|
+
*/
|
|
501
|
+
export async function removeLegacyCodexMemorixMcpConfig(
|
|
502
|
+
configPath = path.join(homedir(), '.codex', 'config.toml'),
|
|
503
|
+
): Promise<{ configPath: string; removed: boolean }> {
|
|
504
|
+
let existingContent: string;
|
|
505
|
+
try {
|
|
506
|
+
existingContent = await readFile(configPath, 'utf-8');
|
|
507
|
+
} catch {
|
|
508
|
+
return { configPath, removed: false };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const adapter = new CodexMCPAdapter();
|
|
512
|
+
const legacyServer = adapter.parse(existingContent).find(isLegacyCodexMemorixNodeServer);
|
|
513
|
+
if (!legacyServer) return { configPath, removed: false };
|
|
514
|
+
|
|
515
|
+
let nextContent = removeTomlSection(existingContent, 'mcp_servers.memorix.env');
|
|
516
|
+
nextContent = removeTomlSection(nextContent, 'mcp_servers.memorix');
|
|
517
|
+
await writeFile(configPath, nextContent ? `${nextContent}\n` : '', 'utf-8');
|
|
518
|
+
return { configPath, removed: true };
|
|
519
|
+
}
|
|
520
|
+
|
|
487
521
|
function mergeYamlMcpConfig(existingContent: string | null, generatedContent: string): string {
|
|
488
522
|
let existing: Record<string, unknown> = {};
|
|
489
523
|
try {
|
|
@@ -1091,6 +1125,16 @@ export async function installAgentSetup(agent: AgentName, plan: SetupPlan, globa
|
|
|
1091
1125
|
const install = tryInstallCodexPlugin();
|
|
1092
1126
|
if (install.ok) p.log.success(install.message);
|
|
1093
1127
|
else p.log.warn(install.message);
|
|
1128
|
+
if (install.ok) {
|
|
1129
|
+
try {
|
|
1130
|
+
const migration = await removeLegacyCodexMemorixMcpConfig();
|
|
1131
|
+
if (migration.removed) {
|
|
1132
|
+
p.log.info(`codex: removed legacy source-path MCP config from ${migration.configPath}`);
|
|
1133
|
+
}
|
|
1134
|
+
} catch {
|
|
1135
|
+
p.log.warn('codex: plugin installed, but the legacy MCP config could not be checked');
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1094
1138
|
} else if (agent === 'claude' && result.marketplaceRoot) {
|
|
1095
1139
|
const install = tryInstallClaudePlugin(result.marketplaceRoot);
|
|
1096
1140
|
if (install.ok) p.log.success(install.message);
|