memorix 1.2.9 → 1.2.10

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.
@@ -243,7 +243,7 @@ memorix setup --agent omp --global
243
243
  What this installs depends on the target agent:
244
244
 
245
245
  - Claude Code: local `memorix-local` marketplace plugin, best-effort `claude plugin install memorix@memorix-local`, plugin-bundled stdio MCP, hooks, skills, plus `CLAUDE.md` guidance.
246
- - Codex: local Personal marketplace plugin under `~/.codex/plugins/memorix`, marketplace entry at `~/.agents/plugins/marketplace.json`, best-effort `codex plugin add memorix@personal`, plugin-bundled stdio MCP, skills, and `SessionStart` / `UserPromptSubmit` / `PostToolUse` / `PreCompact` / `Stop` hooks, plus `AGENTS.md` guidance. Codex asks the user to review plugin hooks once in `/hooks` before non-managed hooks run.
246
+ - Codex: local Personal marketplace plugin under `~/plugins/memorix`, marketplace entry at `~/.agents/plugins/marketplace.json`, best-effort `codex plugin add memorix@personal`, plugin-bundled stdio MCP, skills, and `SessionStart` / `UserPromptSubmit` / `PostToolUse` / `PreCompact` / `Stop` hooks. It does not create project `.codex` files or overwrite model, approval, or sandbox settings. Codex asks the user to review plugin hooks once in `/hooks` before non-managed hooks run.
247
247
  - GitHub Copilot CLI: local plugin package under `~/.copilot/plugins/local/memorix`, best-effort `copilot plugin install <local-path>`, plugin-bundled stdio MCP, hooks, and skills.
248
248
  - Cursor: Cursor MCP config, `.cursor/rules/memorix.mdc`, skills, and hook guidance through Cursor's project config surfaces.
249
249
  - Pi: user Pi package with extension-based hook capture and a Memorix skill, registered through `pi install <path> --approve`; project-local setup uses `-l`.
@@ -668,7 +668,7 @@ If it reports stale MCP command paths, missing `memorix` MCP entries, missing Cl
668
668
  memorix repair agents --agent <agent>
669
669
  ```
670
670
 
671
- Repair only rewrites Memorix-owned MCP entries, Memorix-owned guidance blocks, and an explicitly targeted Codex Memorix plugin bundle. It does not edit unrelated user config or turn on a user-disabled Codex plugin; enable that one from `/plugins`.
671
+ Repair only rewrites Memorix-owned MCP entries, Memorix-owned guidance blocks, and an explicitly targeted Codex Memorix plugin bundle. It does not edit unrelated user config, grant Codex hook trust, or turn on a user-disabled Codex plugin; review hooks in `/hooks` and enable a disabled plugin in `/plugins`.
672
672
 
673
673
  ### 1. Is the workspace a Git repo?
674
674
 
@@ -321,7 +321,13 @@ npm publish --access public
321
321
 
322
322
  Notes:
323
323
 
324
- - `prepublishOnly` runs build + test, but does not contact live model catalog APIs
324
+ - `prepublishOnly` synchronizes and checks Registry metadata, then runs build + test; it does not contact live model catalog APIs
325
+ - `server.json` is the official MCP Registry record. Keep it synchronized with
326
+ `package.json` by running `npm run sync:mcp-registry`; verify it with
327
+ `npm run check:mcp-registry` before a release.
328
+ - The GitHub publish workflow publishes npm first, then uses GitHub OIDC to
329
+ publish the same verified version to the official MCP Registry. It does not
330
+ require a Registry token or private key in repository secrets.
325
331
  - `@memorix/ai`, `@memorix/agent-core`, `@memorix/tui`, and `@memorix/memcode` are internal workspaces. Their code is bundled into the root `memorix` distribution and they must stay `private` unless the project deliberately establishes an owned npm scope and a separate package-support contract.
326
332
  - npm publish is usually manual, especially when 2FA is enabled
327
333
  - GitHub release automation should not be treated as a substitute for manual runtime validation
@@ -49,7 +49,7 @@ Generated guidance also has scope:
49
49
  | Agent | Recommended install | Official entry | What gets installed | Notes |
50
50
  | --- | --- | --- | --- | --- |
51
51
  | Claude Code | `memorix setup --agent claude --global` | Claude Code plugin marketplace | Local `memorix-local` marketplace, plugin-bundled stdio MCP, skills, hooks, plus `CLAUDE.md` guidance | Setup attempts `claude plugin marketplace add` and `claude plugin install memorix@memorix-local`. |
52
- | Codex | `memorix setup --agent codex --global` | Codex Personal marketplace plugin | Local plugin under `~/.codex/plugins/memorix`, Personal marketplace entry, plugin-bundled stdio MCP, skills, hooks, plus `AGENTS.md` guidance | Setup attempts `codex plugin add memorix@personal`. Run `memorix doctor agents --agent codex --scope global` to verify the bundle, marketplace, enabled state, and five declared/trusted hooks; start a new thread after setup. |
52
+ | Codex | `memorix setup --agent codex --global` | Codex Personal marketplace plugin | User plugin under `~/plugins/memorix`, Personal marketplace entry, plugin-bundled stdio MCP, skills, and hooks | Setup attempts `codex plugin add memorix@personal`. It leaves project `.codex` files and model/approval/sandbox settings alone. `memorix doctor agents --agent codex --scope global` verifies the bundle, marketplace, and enabled state, then reports any one-time hook review Codex still requires; start a new thread after setup. |
53
53
  | GitHub Copilot CLI | `memorix setup --agent copilot --global` | Copilot CLI plugin package | Local plugin under `~/.copilot/plugins/local/memorix` with MCP, skills, and hooks | Setup attempts `copilot plugin install <local-path>` when Copilot CLI is available. |
54
54
  | Cursor | `memorix setup --agent cursor --global` | Cursor MCP and rules config | Cursor MCP config, `.cursor/rules/memorix.mdc`, skills, and hook guidance | Reload Cursor after setup so it can pick up project config changes. |
55
55
  | Gemini CLI | `memorix setup --agent gemini-cli --global` | Gemini CLI extension | Extension under `~/.gemini/extensions/memorix` with MCP, `GEMINI.md` context, hooks, commands, and skills | Antigravity CLI has an official Gemini CLI migration path, but Gemini CLI remains an active standalone Google CLI target. |
package/docs/SETUP.md CHANGED
@@ -76,7 +76,7 @@ memorix setup --agent omp --global
76
76
  What this does:
77
77
 
78
78
  - Claude Code: installs a local marketplace plugin, attempts `claude plugin install memorix@memorix-local`, and writes `CLAUDE.md` guidance.
79
- - Codex: installs a local Personal marketplace plugin, attempts `codex plugin add memorix@personal`, and writes `AGENTS.md` guidance.
79
+ - Codex: installs a user-level Personal marketplace plugin under `~/plugins/memorix`, attempts `codex plugin add memorix@personal`, and keeps project-local `.codex` configuration untouched. The plugin owns its MCP server, skills, and optional hooks without changing your model, approval, or sandbox settings.
80
80
  - GitHub Copilot CLI: installs a local plugin package and attempts `copilot plugin install <local-path>`.
81
81
  - Cursor: writes Cursor MCP config, rules, skills, and hook guidance.
82
82
  - Pi: installs the user-level Memorix Pi package and attempts `pi install <path> --approve`.
@@ -132,9 +132,9 @@ memorix doctor agents --agent <agent>
132
132
  memorix repair agents --agent <agent>
133
133
  ```
134
134
 
135
- The doctor checks Memorix-owned MCP entries and guidance files for stale command paths, missing `memorix` MCP servers, missing Claude `alwaysLoad`, and outdated Memory Autopilot rules. For `codex --scope global`, it also checks the local plugin bundle, Personal marketplace entry, five declared lifecycle hooks, Codex's trusted hook state, and the installed/enabled state reported by `codex plugin list`.
135
+ The doctor checks Memorix-owned MCP entries and guidance files for stale command paths, missing `memorix` MCP servers, missing Claude `alwaysLoad`, and outdated Memory Autopilot rules. For `codex --scope global`, MCP is owned by the user-level plugin rather than a direct `config.toml` entry. The doctor checks the plugin bundle, Personal marketplace entry, five declared lifecycle hooks, and the installed/enabled state reported by `codex plugin list`; it reports, but never auto-grants, any hook review Codex still requires.
136
136
 
137
- After Codex setup, start a new thread so enabled plugins can load. Codex does not expose a command that proves hooks are already active inside the current thread. If doctor reports `codex-plugin-disabled`, open `/plugins` and turn Memorix on there; repair does not toggle a user-disabled plugin.
137
+ After Codex setup, start a new thread so enabled plugins can load. If Codex asks to review plugin hooks, approve or decline them in `/hooks`; this is user-level plugin consent, not project trust. If doctor reports `codex-plugin-disabled`, open `/plugins` and turn Memorix on there; repair does not toggle a user-disabled plugin.
138
138
 
139
139
  ### Option B: manual stdio MCP
140
140
 
@@ -330,7 +330,9 @@ HTTP variants in Windsurf-like clients may use `serverUrl`:
330
330
  }
331
331
  ```
332
332
 
333
- ### Codex fallback
333
+ ### Codex manual MCP compatibility
334
+
335
+ The user-level plugin is the default and should be the only Memorix route for Codex. Use this compatibility fallback only when the Codex plugin system is unavailable; do not add it alongside `memorix@personal`, because that creates duplicate MCP ownership.
334
336
 
335
337
  Config file: `~/.codex/config.toml`
336
338
 
@@ -27,7 +27,7 @@
27
27
  |-------|---------|------|
28
28
  | VS Code Copilot | `.github/hooks/*.json` | Claude Code 兼容 |
29
29
  | Claude Code | `.claude/settings.json` | 原生 |
30
- | Codex | Memorix 插件内 `hooks/hooks.json` | Codex plugin hooks;通过 `memorix setup --agent codex` 安装 |
30
+ | Codex | 用户级 Memorix 插件内 `hooks/hooks.json` | Codex plugin hooks;通过 `memorix setup --agent codex --global` 安装,不写项目 `.codex/hooks.json` |
31
31
  | Windsurf | `.windsurf/cascade.json` | Windsurf 格式 |
32
32
  | Cursor | `.cursor/hooks.json` | Cursor 格式 |
33
33
  | Kiro | `.kiro/hooks/*.hook.md` | Markdown + YAML |
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.2.9",
3
+ "mcpName": "io.github.AVIDS2/memorix",
4
+ "version": "1.2.10",
4
5
  "description": "Local-first shared memory layer for AI coding agents across MCP clients, Git history, reasoning context, and project sessions.",
5
6
  "type": "module",
6
7
  "sideEffects": false,
@@ -33,6 +34,7 @@
33
34
  "files": [
34
35
  "dist",
35
36
  "src",
37
+ "server.json",
36
38
  "!src/**/*.js",
37
39
  "!src/**/*.js.map",
38
40
  "!src/**/*.d.ts",
@@ -63,9 +65,11 @@
63
65
  "test:e2e": "vitest run tests/e2e/",
64
66
  "benchmark:codegraph": "node scripts/benchmark-codegraph-provider.cjs",
65
67
  "benchmark:retrieval": "npm run build && node scripts/benchmark-retrieval.mjs",
68
+ "sync:mcp-registry": "node scripts/sync-mcp-registry-manifest.mjs",
69
+ "check:mcp-registry": "node scripts/check-mcp-registry-manifest.mjs",
66
70
  "audit:pi-upstream": "node scripts/pi-upstream-audit.mjs",
67
71
  "lint": "tsc --noEmit",
68
- "prepublishOnly": "npm run build && npm test"
72
+ "prepublishOnly": "npm run sync:mcp-registry && npm run check:mcp-registry && npm run build && npm test"
69
73
  },
70
74
  "keywords": [
71
75
  "mcp",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "memorix",
4
- "version": "1.2.9",
4
+ "version": "1.2.10",
5
5
  "description": "Shared workspace memory for Claude Code and other AI coding agents.",
6
6
  "author": {
7
7
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.2.9",
3
+ "version": "1.2.10",
4
4
  "description": "Shared workspace memory for Codex and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.2.9",
3
+ "version": "1.2.10",
4
4
  "description": "Shared local workspace memory for GitHub Copilot CLI and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.2.9",
3
+ "version": "1.2.10",
4
4
  "mcpServers": {
5
5
  "memorix": {
6
6
  "command": "memorix",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix-omp-package",
3
- "version": "1.2.9",
3
+ "version": "1.2.10",
4
4
  "description": "Memorix shared workspace memory package for Oh-my-Pi.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.2.9",
3
+ "version": "1.2.10",
4
4
  "description": "Shared workspace memory for OpenClaw and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix-pi-package",
3
- "version": "1.2.9",
3
+ "version": "1.2.10",
4
4
  "description": "Memorix shared workspace memory package for Pi coding agent.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
package/server.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.AVIDS2/memorix",
4
+ "title": "Memorix",
5
+ "description": "Local-first persistent project memory for AI coding agents across MCP clients and sessions.",
6
+ "repository": {
7
+ "url": "https://github.com/AVIDS2/memorix",
8
+ "source": "github"
9
+ },
10
+ "version": "1.2.10",
11
+ "websiteUrl": "https://github.com/AVIDS2/memorix#readme",
12
+ "icons": [
13
+ {
14
+ "src": "https://raw.githubusercontent.com/AVIDS2/memorix/main/assets/logo.png",
15
+ "mimeType": "image/png",
16
+ "sizes": [
17
+ "1024x1024"
18
+ ]
19
+ }
20
+ ],
21
+ "packages": [
22
+ {
23
+ "registryType": "npm",
24
+ "identifier": "memorix",
25
+ "version": "1.2.10",
26
+ "runtimeHint": "npx",
27
+ "packageArguments": [
28
+ {
29
+ "type": "positional",
30
+ "value": "serve"
31
+ }
32
+ ],
33
+ "transport": {
34
+ "type": "stdio"
35
+ }
36
+ }
37
+ ]
38
+ }
@@ -12,6 +12,7 @@ import {
12
12
  getSetupAgentTargets,
13
13
  installPluginPackage,
14
14
  installMcpConfig,
15
+ migrateLegacyCodexIntegration,
15
16
  tryInstallCodexPlugin,
16
17
  type McpConfigAgent,
17
18
  } from './setup.js';
@@ -228,7 +229,7 @@ function asRecordArray(value: unknown): JsonRecord[] {
228
229
  }
229
230
 
230
231
  function codexPluginPath(): string {
231
- return `${homedir()}/.codex/plugins/${CODEX_PLUGIN_NAME}`;
232
+ return `${homedir()}/plugins/${CODEX_PLUGIN_NAME}`;
232
233
  }
233
234
 
234
235
  function codexPluginMcpPath(): string {
@@ -254,7 +255,10 @@ function isMemorixCodexPlugin(value: JsonRecord): boolean {
254
255
 
255
256
  export function parseCodexPluginList(output: string): AgentPluginCheck['runtime'] | null {
256
257
  try {
257
- const parsed = asRecord(JSON.parse(output));
258
+ const start = output.indexOf('{');
259
+ const end = output.lastIndexOf('}');
260
+ if (start < 0 || end < start) return null;
261
+ const parsed = asRecord(JSON.parse(output.slice(start, end + 1)));
258
262
  if (!parsed) return null;
259
263
  const entries = [...asRecordArray(parsed.installed), ...asRecordArray(parsed.available)];
260
264
  const plugin = entries.find(isMemorixCodexPlugin);
@@ -407,7 +411,7 @@ async function inspectCodexMarketplace(): Promise<AgentPluginCheck> {
407
411
 
408
412
  const source = asRecord(entry.source);
409
413
  const sourcePath = typeof source?.path === 'string' ? source.path : '';
410
- const issues = source?.source === 'local' && normalizeMarketplacePath(sourcePath) === '.codex/plugins/memorix'
414
+ const issues = source?.source === 'local' && normalizeMarketplacePath(sourcePath) === 'plugins/memorix'
411
415
  ? []
412
416
  : ['codex-marketplace-entry-stale'];
413
417
  return {
@@ -1060,9 +1064,11 @@ export async function repairAgentIntegrations(options: {
1060
1064
  } else {
1061
1065
  if (!options.dry) {
1062
1066
  await installPluginPackage({ agent: 'codex' });
1063
- if (needsInstall) {
1064
- const install = tryInstallCodexPlugin();
1065
- if (!install.ok) skipped.push('codex:plugin:global:install-pending');
1067
+ const install = tryInstallCodexPlugin();
1068
+ if (install.ok) {
1069
+ await migrateLegacyCodexIntegration({ projectRoot });
1070
+ } else {
1071
+ skipped.push('codex:plugin:global:install-pending');
1066
1072
  }
1067
1073
  }
1068
1074
  changed.push('codex:plugin:global');
@@ -162,7 +162,9 @@ const OPENCLAW_BUNDLE_AGENTS = new Set<AgentName>(['openclaw']);
162
162
  const ANTIGRAVITY_PLUGIN_AGENTS = new Set<AgentName>(['antigravity']);
163
163
  const HERMES_PLUGIN_AGENTS = new Set<AgentName>(['hermes']);
164
164
  const OMP_PACKAGE_AGENTS = new Set<AgentName>(['omp']);
165
- const PACKAGE_OWNED_INTEGRATION_AGENTS = new Set<AgentName>(['antigravity', 'openclaw', 'hermes', 'omp']);
165
+ // These hosts have a native package/plugin surface. Their default setup must not
166
+ // create project-local fallback files beside the user's own agent configuration.
167
+ const PACKAGE_OWNED_INTEGRATION_AGENTS = new Set<AgentName>(['codex', 'antigravity', 'openclaw', 'hermes', 'omp']);
166
168
  const SUPPORTED_SETUP_AGENTS: AgentName[] = [
167
169
  'claude',
168
170
  'codex',
@@ -484,6 +486,32 @@ function mergeTomlMcpConfig(existingContent: string | null, generatedContent: st
484
486
  return `${parts.join('\n\n')}\n`;
485
487
  }
486
488
 
489
+ type LegacyArtifactStatus = 'removed' | 'preserved' | 'missing';
490
+
491
+ export interface CodexLegacyMigrationResult {
492
+ mcp: LegacyArtifactStatus;
493
+ projectHooks: LegacyArtifactStatus;
494
+ pluginFolder: LegacyArtifactStatus;
495
+ }
496
+
497
+ function getTomlSection(content: string, section: string): string | null {
498
+ const lines = content.split(/\r?\n/);
499
+ const header = `[${section}]`;
500
+ const start = lines.findIndex((line) => line.trim() === header);
501
+ if (start < 0) return null;
502
+
503
+ let end = lines.length;
504
+ for (let index = start + 1; index < lines.length; index += 1) {
505
+ const trimmed = lines[index].trim();
506
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
507
+ end = index;
508
+ break;
509
+ }
510
+ }
511
+
512
+ return lines.slice(start + 1, end).join('\n');
513
+ }
514
+
487
515
  function isLegacyCodexMemorixNodeServer(server: MCPServerEntry): boolean {
488
516
  if (server.name !== 'memorix' || server.command.trim().toLowerCase() !== 'node') return false;
489
517
  const startsMemorixStdio = server.args.some((arg) => arg.trim().toLowerCase() === 'serve');
@@ -494,6 +522,22 @@ function isLegacyCodexMemorixNodeServer(server: MCPServerEntry): boolean {
494
522
  return startsMemorixStdio && sourceEntry;
495
523
  }
496
524
 
525
+ function isOwnedLegacyCodexMcp(content: string): boolean {
526
+ const section = getTomlSection(content, 'mcp_servers.memorix');
527
+ if (section === null || getTomlSection(content, 'mcp_servers.memorix.env') !== null) return false;
528
+
529
+ const allowedKeys = new Set(['command', 'args', 'type', 'startup_timeout_sec', 'tool_timeout_sec', 'enabled']);
530
+ for (const rawLine of section.split(/\r?\n/)) {
531
+ const line = rawLine.trim();
532
+ if (!line || line.startsWith('#')) continue;
533
+ const key = line.match(/^([A-Za-z0-9_]+)\s*=/)?.[1];
534
+ if (!key || !allowedKeys.has(key)) return false;
535
+ }
536
+
537
+ const server = new CodexMCPAdapter().parse(content).find((entry) => entry.name === 'memorix');
538
+ return Boolean(server && isLegacyCodexMemorixNodeServer(server));
539
+ }
540
+
497
541
  /**
498
542
  * Codex plugins now own Memorix stdio MCP registration. Remove only the old
499
543
  * source-path form emitted by pre-plugin setup, never a user's custom server.
@@ -508,9 +552,7 @@ export async function removeLegacyCodexMemorixMcpConfig(
508
552
  return { configPath, removed: false };
509
553
  }
510
554
 
511
- const adapter = new CodexMCPAdapter();
512
- const legacyServer = adapter.parse(existingContent).find(isLegacyCodexMemorixNodeServer);
513
- if (!legacyServer) return { configPath, removed: false };
555
+ if (!isOwnedLegacyCodexMcp(existingContent)) return { configPath, removed: false };
514
556
 
515
557
  let nextContent = removeTomlSection(existingContent, 'mcp_servers.memorix.env');
516
558
  nextContent = removeTomlSection(nextContent, 'mcp_servers.memorix');
@@ -518,6 +560,88 @@ export async function removeLegacyCodexMemorixMcpConfig(
518
560
  return { configPath, removed: true };
519
561
  }
520
562
 
563
+ function collectHookCommands(value: unknown, commands: string[]): void {
564
+ if (Array.isArray(value)) {
565
+ for (const item of value) collectHookCommands(item, commands);
566
+ return;
567
+ }
568
+ if (!value || typeof value !== 'object') return;
569
+
570
+ for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
571
+ if ((key === 'command' || key === 'commandWindows') && typeof child === 'string') {
572
+ commands.push(child);
573
+ } else {
574
+ collectHookCommands(child, commands);
575
+ }
576
+ }
577
+ }
578
+
579
+ function isOwnedLegacyCodexHooks(content: string): boolean {
580
+ try {
581
+ const parsed = JSON.parse(content) as { hooks?: unknown };
582
+ if (!parsed.hooks || typeof parsed.hooks !== 'object') return false;
583
+ const commands: string[] = [];
584
+ collectHookCommands(parsed.hooks, commands);
585
+ return commands.length > 0
586
+ && commands.every((command) => /^memorix(?:\.cmd)?\s+hook(?:\s|$)/i.test(command.trim()));
587
+ } catch {
588
+ return false;
589
+ }
590
+ }
591
+
592
+ async function removeOwnedLegacyCodexPluginFolder(homeDir: string): Promise<LegacyArtifactStatus> {
593
+ const canonicalPath = path.join(homeDir, 'plugins', 'memorix');
594
+ const legacyPath = path.join(homeDir, '.codex', 'plugins', 'memorix');
595
+ if (!existsSync(legacyPath)) return 'missing';
596
+ if (!existsSync(canonicalPath)) return 'preserved';
597
+
598
+ try {
599
+ const manifest = JSON.parse(await readFile(path.join(legacyPath, '.codex-plugin', 'plugin.json'), 'utf-8')) as Record<string, unknown>;
600
+ const repository = String(manifest.repository ?? '');
601
+ if (manifest.name !== 'memorix' || !repository.includes('github.com/AVIDS2/memorix')) return 'preserved';
602
+ await rm(legacyPath, { recursive: true, force: true });
603
+ return 'removed';
604
+ } catch {
605
+ return 'preserved';
606
+ }
607
+ }
608
+
609
+ export async function migrateLegacyCodexIntegration(options: {
610
+ homeDir?: string;
611
+ projectRoot?: string;
612
+ } = {}): Promise<CodexLegacyMigrationResult> {
613
+ const homeDir = options.homeDir ?? homedir();
614
+ const projectRoot = options.projectRoot ?? process.cwd();
615
+ const configPath = path.join(homeDir, '.codex', 'config.toml');
616
+ let mcp: LegacyArtifactStatus = 'missing';
617
+
618
+ try {
619
+ const content = await readFile(configPath, 'utf-8');
620
+ if (getTomlSection(content, 'mcp_servers.memorix') !== null) {
621
+ mcp = (await removeLegacyCodexMemorixMcpConfig(configPath)).removed ? 'removed' : 'preserved';
622
+ }
623
+ } catch {
624
+ mcp = 'missing';
625
+ }
626
+
627
+ const hooksPath = path.join(projectRoot, '.codex', 'hooks.json');
628
+ let projectHooks: LegacyArtifactStatus = 'missing';
629
+ try {
630
+ const content = await readFile(hooksPath, 'utf-8');
631
+ if (isOwnedLegacyCodexHooks(content)) {
632
+ await rm(hooksPath, { force: true });
633
+ projectHooks = 'removed';
634
+ } else {
635
+ projectHooks = 'preserved';
636
+ }
637
+ } catch {
638
+ projectHooks = 'missing';
639
+ }
640
+
641
+ const pluginFolder = await removeOwnedLegacyCodexPluginFolder(homeDir);
642
+ return { mcp, projectHooks, pluginFolder };
643
+ }
644
+
521
645
  function mergeYamlMcpConfig(existingContent: string | null, generatedContent: string): string {
522
646
  let existing: Record<string, unknown> = {};
523
647
  try {
@@ -678,7 +802,9 @@ export async function installPluginPackage(options: PluginInstallOptions): Promi
678
802
  }
679
803
 
680
804
  if (options.agent === 'codex') {
681
- const pluginPath = path.join(home, '.codex', 'plugins', 'memorix');
805
+ // Codex discovers personal plugins from the user marketplace. Keep the
806
+ // package outside ~/.codex so setup does not look like project/config state.
807
+ const pluginPath = path.join(home, 'plugins', 'memorix');
682
808
  const marketplacePath = path.join(home, '.agents', 'plugins', 'marketplace.json');
683
809
  await copyDir(source, pluginPath);
684
810
  await stampCodexPluginVersion(pluginPath);
@@ -908,6 +1034,39 @@ function tryInstallClaudePlugin(marketplaceRoot: string): { ok: boolean; message
908
1034
  };
909
1035
  }
910
1036
 
1037
+ export type CodexPluginState = 'ready' | 'disabled' | 'missing' | 'unknown';
1038
+
1039
+ export function parseCodexPluginState(content: string): CodexPluginState {
1040
+ try {
1041
+ // Some Windows builds emit a one-line diagnostic before the requested JSON.
1042
+ const start = content.indexOf('{');
1043
+ const end = content.lastIndexOf('}');
1044
+ if (start < 0 || end < start) return 'unknown';
1045
+ const parsed = JSON.parse(content.slice(start, end + 1)) as { installed?: unknown };
1046
+ if (!Array.isArray(parsed.installed)) return 'unknown';
1047
+
1048
+ const plugin = parsed.installed.find((entry): entry is Record<string, unknown> => (
1049
+ Boolean(entry)
1050
+ && typeof entry === 'object'
1051
+ && (entry as Record<string, unknown>).pluginId === 'memorix@personal'
1052
+ ));
1053
+ if (!plugin || plugin.installed !== true) return 'missing';
1054
+ return plugin.enabled === true ? 'ready' : 'disabled';
1055
+ } catch {
1056
+ return 'unknown';
1057
+ }
1058
+ }
1059
+
1060
+ function getCodexPluginState(): CodexPluginState {
1061
+ const result = spawnSync('codex', ['plugin', 'list', '--json'], {
1062
+ encoding: 'utf-8',
1063
+ stdio: 'pipe',
1064
+ shell: process.platform === 'win32',
1065
+ });
1066
+ if (result.status !== 0) return 'unknown';
1067
+ return parseCodexPluginState(result.stdout || '');
1068
+ }
1069
+
911
1070
  export function tryInstallCodexPlugin(): { ok: boolean; message: string } {
912
1071
  const result = spawnSync('codex', ['plugin', 'add', 'memorix@personal', '--json'], {
913
1072
  encoding: 'utf-8',
@@ -915,13 +1074,28 @@ export function tryInstallCodexPlugin(): { ok: boolean; message: string } {
915
1074
  shell: process.platform === 'win32',
916
1075
  });
917
1076
 
918
- if (result.status === 0) {
919
- return { ok: true, message: 'codex: plugin installed from Personal marketplace' };
920
- }
921
-
922
1077
  const output = `${result.stderr || ''}\n${result.stdout || ''}`.toLowerCase();
923
- if (output.includes('already')) {
924
- return { ok: true, message: 'codex: plugin already installed from Personal marketplace' };
1078
+ const addFinished = result.status === 0 || output.includes('already');
1079
+ if (addFinished) {
1080
+ const pluginState = getCodexPluginState();
1081
+ if (pluginState === 'ready') {
1082
+ return {
1083
+ ok: true,
1084
+ message: result.status === 0
1085
+ ? 'codex: plugin installed and enabled from Personal marketplace'
1086
+ : 'codex: plugin already installed and enabled from Personal marketplace',
1087
+ };
1088
+ }
1089
+ if (pluginState === 'disabled') {
1090
+ return {
1091
+ ok: false,
1092
+ message: 'codex: Memorix plugin is installed but disabled. Enable it in Codex Plugins before any legacy MCP or project hooks are removed.',
1093
+ };
1094
+ }
1095
+ return {
1096
+ ok: false,
1097
+ message: 'codex: plugin registration finished, but Codex did not confirm an enabled Memorix plugin. Legacy MCP and project hooks were kept unchanged.',
1098
+ };
925
1099
  }
926
1100
 
927
1101
  const detail = (result.stderr || result.stdout || result.error?.message || '').trim();
@@ -1106,7 +1280,9 @@ export async function installAgentSetup(agent: AgentName, plan: SetupPlan, globa
1106
1280
  const hasAntigravityPlugin = plan.actions.includes('antigravity-plugin') && agent === 'antigravity';
1107
1281
  const hasHermesPlugin = plan.actions.includes('hermes-plugin') && agent === 'hermes';
1108
1282
  const hasOmpPackage = plan.actions.includes('omp-package') && agent === 'omp';
1109
- const wantsMcpConfig = plan.mcp !== 'none' && agent !== 'pi';
1283
+ // Codex's supported default is the user-level plugin. Never create a
1284
+ // project-local .codex/config.toml as an implicit fallback.
1285
+ const wantsMcpConfig = plan.mcp !== 'none' && agent !== 'pi' && !(agent === 'codex' && !global);
1110
1286
 
1111
1287
  if (PLUGIN_PACKAGE_AGENTS.has(agent) && plan.includePlugin && !global) {
1112
1288
  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.`);
@@ -1115,7 +1291,7 @@ export async function installAgentSetup(agent: AgentName, plan: SetupPlan, globa
1115
1291
  if (hasPluginPackage) {
1116
1292
  const result = await installPluginPackage({
1117
1293
  agent: agent as PluginInstallOptions['agent'],
1118
- includeHooks: plan.actions.includes('hooks'),
1294
+ includeHooks: plan.includeHooks,
1119
1295
  });
1120
1296
  p.log.success(`${agent}: plugin package -> ${result.pluginPath}`);
1121
1297
  if (result.marketplacePath) {
@@ -1123,17 +1299,16 @@ export async function installAgentSetup(agent: AgentName, plan: SetupPlan, globa
1123
1299
  }
1124
1300
  if (agent === 'codex') {
1125
1301
  const install = tryInstallCodexPlugin();
1126
- if (install.ok) p.log.success(install.message);
1127
- else p.log.warn(install.message);
1128
1302
  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
- }
1303
+ p.log.success(install.message);
1304
+ const migration = await migrateLegacyCodexIntegration({ projectRoot: process.cwd() });
1305
+ if (migration.mcp === 'removed') p.log.info('codex: removed the legacy source-path MCP block; the enabled plugin now owns MCP.');
1306
+ if (migration.mcp === 'preserved') p.log.warn('codex: kept a customized legacy Memorix MCP block; remove it manually only after confirming plugin MCP works.');
1307
+ if (migration.projectHooks === 'removed') p.log.info('codex: removed a legacy project-local Memorix hooks.json; plugin hooks now own lifecycle capture.');
1308
+ if (migration.projectHooks === 'preserved') p.log.warn('codex: kept a project .codex/hooks.json because it contains non-Memorix commands.');
1309
+ if (migration.pluginFolder === 'removed') p.log.info('codex: removed the migrated legacy plugin folder under ~/.codex/plugins.');
1310
+ } else {
1311
+ p.log.warn(install.message);
1137
1312
  }
1138
1313
  } else if (agent === 'claude' && result.marketplaceRoot) {
1139
1314
  const install = tryInstallClaudePlugin(result.marketplaceRoot);
@@ -1224,6 +1399,8 @@ export async function installAgentSetup(agent: AgentName, plan: SetupPlan, globa
1224
1399
  const result = await installMcpConfig({ agent: agent as McpConfigAgent, projectRoot: targetRoot, global, mcp });
1225
1400
  p.log.success(`${agent}: MCP config -> ${result.configPath}`);
1226
1401
  }
1402
+ } else if (agent === 'codex' && !global && plan.mcp !== 'none') {
1403
+ p.log.info('codex: project-local .codex config is intentionally untouched. Run `memorix setup --agent codex --global` to install the user-level plugin.');
1227
1404
  }
1228
1405
 
1229
1406
  if (hasPluginPackage) {