memorix 1.1.12 → 1.1.13

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.
@@ -1,3 +1,4 @@
1
+ import { spawnSync } from 'node:child_process';
1
2
  import { existsSync } from 'node:fs';
2
3
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
4
  import { basename, dirname, resolve } from 'node:path';
@@ -9,9 +10,12 @@ import {
9
10
  buildMemorixServer,
10
11
  getMcpAdapter,
11
12
  getSetupAgentTargets,
13
+ installPluginPackage,
12
14
  installMcpConfig,
15
+ tryInstallCodexPlugin,
13
16
  type McpConfigAgent,
14
17
  } from './setup.js';
18
+ import { getCliVersion } from '../version.js';
15
19
 
16
20
  export type AgentIntegrationScope = 'local' | 'project' | 'global' | 'all';
17
21
  export type AgentIntegrationStatus = 'ok' | 'missing' | 'repairable' | 'skipped';
@@ -40,6 +44,35 @@ export interface AgentGuidanceCheck {
40
44
  issues: string[];
41
45
  }
42
46
 
47
+ export interface AgentPluginCheck {
48
+ scope: 'global';
49
+ kind: 'bundle' | 'marketplace' | 'runtime' | 'hook-trust';
50
+ path: string;
51
+ exists: boolean;
52
+ status: AgentIntegrationStatus;
53
+ issues: string[];
54
+ version?: string;
55
+ hooks?: {
56
+ declared: string[];
57
+ expected: string[];
58
+ };
59
+ runtime?: {
60
+ installed: boolean;
61
+ enabled: boolean;
62
+ version?: string;
63
+ };
64
+ hookTrust?: {
65
+ trusted: string[];
66
+ expected: string[];
67
+ };
68
+ }
69
+
70
+ export interface AgentPluginStatus {
71
+ status: AgentIntegrationStatus;
72
+ issues: string[];
73
+ checks: AgentPluginCheck[];
74
+ }
75
+
43
76
  export interface AgentIntegrationEntry {
44
77
  agent: AgentName;
45
78
  mcp: {
@@ -52,6 +85,7 @@ export interface AgentIntegrationEntry {
52
85
  issues: string[];
53
86
  checks: AgentGuidanceCheck[];
54
87
  };
88
+ plugin: AgentPluginStatus;
55
89
  }
56
90
 
57
91
  export interface AgentIntegrationReport {
@@ -89,6 +123,12 @@ const GUIDANCE_AGENTS = new Set<AgentName>([
89
123
  'trae',
90
124
  ]);
91
125
 
126
+ const CODEX_PLUGIN_NAME = 'memorix';
127
+ const CODEX_PLUGIN_MARKETPLACE = 'personal';
128
+ const CODEX_PLUGIN_ID = `${CODEX_PLUGIN_NAME}@${CODEX_PLUGIN_MARKETPLACE}`;
129
+ const CODEX_HOOK_EVENTS = ['SessionStart', 'UserPromptSubmit', 'PostToolUse', 'PreCompact', 'Stop'];
130
+ const CODEX_HOOK_STATE_NAMES = ['session_start', 'user_prompt_submit', 'post_tool_use', 'pre_compact', 'stop'];
131
+
92
132
  type ConcreteAgentIntegrationScope = Exclude<AgentIntegrationScope, 'all'>;
93
133
  type JsonRecord = Record<string, unknown>;
94
134
 
@@ -179,6 +219,310 @@ function asRecord(value: unknown): JsonRecord | null {
179
219
  return value && typeof value === 'object' && !Array.isArray(value) ? value as JsonRecord : null;
180
220
  }
181
221
 
222
+ function asRecordArray(value: unknown): JsonRecord[] {
223
+ return Array.isArray(value)
224
+ ? value.map(asRecord).filter((entry): entry is JsonRecord => entry !== null)
225
+ : [];
226
+ }
227
+
228
+ function codexPluginPath(): string {
229
+ return `${homedir()}/.codex/plugins/${CODEX_PLUGIN_NAME}`;
230
+ }
231
+
232
+ function codexMarketplacePath(): string {
233
+ return `${homedir()}/.agents/plugins/marketplace.json`;
234
+ }
235
+
236
+ function codexConfigPath(): string {
237
+ return `${homedir()}/.codex/config.toml`;
238
+ }
239
+
240
+ function normalizeMarketplacePath(value: string): string {
241
+ return value.replace(/\\/g, '/').replace(/^\.\//, '');
242
+ }
243
+
244
+ function isMemorixCodexPlugin(value: JsonRecord): boolean {
245
+ return value.pluginId === CODEX_PLUGIN_ID
246
+ || (value.name === CODEX_PLUGIN_NAME && value.marketplaceName === CODEX_PLUGIN_MARKETPLACE);
247
+ }
248
+
249
+ export function parseCodexPluginList(output: string): AgentPluginCheck['runtime'] | null {
250
+ try {
251
+ const parsed = asRecord(JSON.parse(output));
252
+ if (!parsed) return null;
253
+ const entries = [...asRecordArray(parsed.installed), ...asRecordArray(parsed.available)];
254
+ const plugin = entries.find(isMemorixCodexPlugin);
255
+ if (!plugin) return { installed: false, enabled: false };
256
+ return {
257
+ installed: plugin.installed === true,
258
+ enabled: plugin.enabled === true,
259
+ ...(typeof plugin.version === 'string' ? { version: plugin.version } : {}),
260
+ };
261
+ } catch {
262
+ return null;
263
+ }
264
+ }
265
+
266
+ async function inspectCodexPluginBundle(): Promise<AgentPluginCheck> {
267
+ const pluginPath = codexPluginPath();
268
+ const manifestPath = `${pluginPath}/.codex-plugin/plugin.json`;
269
+ const hooksPath = `${pluginPath}/hooks/hooks.json`;
270
+ if (!existsSync(pluginPath)) {
271
+ return {
272
+ scope: 'global',
273
+ kind: 'bundle',
274
+ path: pluginPath,
275
+ exists: false,
276
+ status: 'missing',
277
+ issues: ['codex-plugin-bundle-missing'],
278
+ };
279
+ }
280
+ if (!existsSync(manifestPath)) {
281
+ return {
282
+ scope: 'global',
283
+ kind: 'bundle',
284
+ path: manifestPath,
285
+ exists: false,
286
+ status: 'repairable',
287
+ issues: ['codex-plugin-manifest-missing'],
288
+ };
289
+ }
290
+
291
+ let manifest: JsonRecord | null = null;
292
+ try {
293
+ manifest = asRecord(JSON.parse(await readFile(manifestPath, 'utf-8')));
294
+ } catch {
295
+ return {
296
+ scope: 'global',
297
+ kind: 'bundle',
298
+ path: manifestPath,
299
+ exists: true,
300
+ status: 'repairable',
301
+ issues: ['codex-plugin-manifest-unreadable'],
302
+ };
303
+ }
304
+ if (!manifest || manifest.name !== CODEX_PLUGIN_NAME) {
305
+ return {
306
+ scope: 'global',
307
+ kind: 'bundle',
308
+ path: manifestPath,
309
+ exists: true,
310
+ status: 'repairable',
311
+ issues: ['codex-plugin-manifest-invalid'],
312
+ };
313
+ }
314
+
315
+ const version = typeof manifest.version === 'string' ? manifest.version : undefined;
316
+ const issues: string[] = [];
317
+ if (version !== getCliVersion()) issues.push('codex-plugin-version-mismatch');
318
+ if (manifest.hooks !== './hooks/hooks.json') issues.push('codex-hook-manifest-missing');
319
+
320
+ let declared: string[] = [];
321
+ if (!existsSync(hooksPath)) {
322
+ issues.push('codex-hook-manifest-missing');
323
+ } else {
324
+ try {
325
+ const hooksConfig = asRecord(JSON.parse(await readFile(hooksPath, 'utf-8')));
326
+ const hooks = asRecord(hooksConfig?.hooks);
327
+ declared = hooks ? Object.keys(hooks) : [];
328
+ if (CODEX_HOOK_EVENTS.some((event) => !declared.includes(event))) {
329
+ issues.push('codex-hook-events-missing');
330
+ }
331
+ } catch {
332
+ issues.push('codex-hook-manifest-unreadable');
333
+ }
334
+ }
335
+
336
+ return {
337
+ scope: 'global',
338
+ kind: 'bundle',
339
+ path: pluginPath,
340
+ exists: true,
341
+ status: issues.length > 0 ? 'repairable' : 'ok',
342
+ issues: unique(issues),
343
+ ...(version ? { version } : {}),
344
+ hooks: { declared, expected: [...CODEX_HOOK_EVENTS] },
345
+ };
346
+ }
347
+
348
+ async function inspectCodexMarketplace(): Promise<AgentPluginCheck> {
349
+ const marketplacePath = codexMarketplacePath();
350
+ if (!existsSync(marketplacePath)) {
351
+ return {
352
+ scope: 'global',
353
+ kind: 'marketplace',
354
+ path: marketplacePath,
355
+ exists: false,
356
+ status: 'missing',
357
+ issues: ['codex-marketplace-missing'],
358
+ };
359
+ }
360
+
361
+ let catalog: JsonRecord | null = null;
362
+ try {
363
+ catalog = asRecord(JSON.parse(await readFile(marketplacePath, 'utf-8')));
364
+ } catch {
365
+ return {
366
+ scope: 'global',
367
+ kind: 'marketplace',
368
+ path: marketplacePath,
369
+ exists: true,
370
+ status: 'repairable',
371
+ issues: ['codex-marketplace-unreadable'],
372
+ };
373
+ }
374
+
375
+ const entry = asRecordArray(catalog?.plugins).find((plugin) => plugin.name === CODEX_PLUGIN_NAME);
376
+ if (!entry) {
377
+ return {
378
+ scope: 'global',
379
+ kind: 'marketplace',
380
+ path: marketplacePath,
381
+ exists: true,
382
+ status: 'repairable',
383
+ issues: ['codex-marketplace-entry-missing'],
384
+ };
385
+ }
386
+
387
+ const source = asRecord(entry.source);
388
+ const sourcePath = typeof source?.path === 'string' ? source.path : '';
389
+ const issues = source?.source === 'local' && normalizeMarketplacePath(sourcePath) === '.codex/plugins/memorix'
390
+ ? []
391
+ : ['codex-marketplace-entry-stale'];
392
+ return {
393
+ scope: 'global',
394
+ kind: 'marketplace',
395
+ path: marketplacePath,
396
+ exists: true,
397
+ status: issues.length > 0 ? 'repairable' : 'ok',
398
+ issues,
399
+ };
400
+ }
401
+
402
+ function inspectCodexPluginRuntime(): AgentPluginCheck {
403
+ const command = 'codex plugin list --marketplace personal --available --json';
404
+ const result = spawnSync('codex', ['plugin', 'list', '--marketplace', CODEX_PLUGIN_MARKETPLACE, '--available', '--json'], {
405
+ encoding: 'utf-8',
406
+ stdio: 'pipe',
407
+ shell: process.platform === 'win32',
408
+ });
409
+ if (result.error || result.status !== 0) {
410
+ return {
411
+ scope: 'global',
412
+ kind: 'runtime',
413
+ path: command,
414
+ exists: false,
415
+ status: 'skipped',
416
+ issues: ['codex-plugin-runtime-unavailable'],
417
+ };
418
+ }
419
+
420
+ const runtime = parseCodexPluginList(String(result.stdout ?? ''));
421
+ if (!runtime) {
422
+ return {
423
+ scope: 'global',
424
+ kind: 'runtime',
425
+ path: command,
426
+ exists: true,
427
+ status: 'skipped',
428
+ issues: ['codex-plugin-runtime-unreadable'],
429
+ };
430
+ }
431
+
432
+ const issues: string[] = [];
433
+ if (!runtime.installed) issues.push('codex-plugin-not-installed');
434
+ else if (!runtime.enabled) issues.push('codex-plugin-disabled');
435
+ return {
436
+ scope: 'global',
437
+ kind: 'runtime',
438
+ path: command,
439
+ exists: true,
440
+ status: issues.length > 0 ? 'repairable' : 'ok',
441
+ issues,
442
+ runtime,
443
+ };
444
+ }
445
+
446
+ async function inspectCodexHookTrust(): Promise<AgentPluginCheck> {
447
+ const configPath = codexConfigPath();
448
+ if (!existsSync(configPath)) {
449
+ return {
450
+ scope: 'global',
451
+ kind: 'hook-trust',
452
+ path: configPath,
453
+ exists: false,
454
+ status: 'skipped',
455
+ issues: ['codex-hook-trust-unavailable'],
456
+ };
457
+ }
458
+
459
+ let config = '';
460
+ try {
461
+ config = await readFile(configPath, 'utf-8');
462
+ } catch {
463
+ return {
464
+ scope: 'global',
465
+ kind: 'hook-trust',
466
+ path: configPath,
467
+ exists: true,
468
+ status: 'skipped',
469
+ issues: ['codex-hook-trust-unreadable'],
470
+ };
471
+ }
472
+
473
+ const trusted = CODEX_HOOK_STATE_NAMES.filter((event) =>
474
+ config.includes(`[hooks.state."${CODEX_PLUGIN_ID}:hooks/hooks.json:${event}:`)
475
+ );
476
+ const issues = trusted.length === CODEX_HOOK_STATE_NAMES.length ? [] : ['codex-hook-trust-pending'];
477
+ return {
478
+ scope: 'global',
479
+ kind: 'hook-trust',
480
+ path: configPath,
481
+ exists: true,
482
+ status: issues.length > 0 ? 'repairable' : 'ok',
483
+ issues,
484
+ hookTrust: { trusted, expected: [...CODEX_HOOK_STATE_NAMES] },
485
+ };
486
+ }
487
+
488
+ function aggregatePluginStatus(checks: AgentPluginCheck[]): AgentIntegrationStatus {
489
+ if (checks.length === 0) return 'skipped';
490
+ return worstStatus(checks.map((check) => check.status));
491
+ }
492
+
493
+ function aggregatePluginIssues(checks: AgentPluginCheck[]): string[] {
494
+ const status = aggregatePluginStatus(checks);
495
+ if (status === 'ok') {
496
+ return unique(checks.filter((check) => check.status === 'skipped').flatMap((check) => check.issues));
497
+ }
498
+ if (status === 'repairable') {
499
+ return unique(checks
500
+ .filter((check) => check.status === 'repairable' || check.status === 'missing')
501
+ .flatMap((check) => check.issues));
502
+ }
503
+ return unique(checks
504
+ .filter((check) => check.status === status)
505
+ .flatMap((check) => check.issues));
506
+ }
507
+
508
+ async function inspectPlugin(agent: AgentName, scope: AgentIntegrationScope): Promise<AgentPluginStatus> {
509
+ if (agent !== 'codex' || scope === 'local' || scope === 'project') {
510
+ return { status: 'skipped', issues: [], checks: [] };
511
+ }
512
+
513
+ const checks = [
514
+ await inspectCodexPluginBundle(),
515
+ await inspectCodexMarketplace(),
516
+ inspectCodexPluginRuntime(),
517
+ await inspectCodexHookTrust(),
518
+ ];
519
+ return {
520
+ status: aggregatePluginStatus(checks),
521
+ issues: aggregatePluginIssues(checks),
522
+ checks,
523
+ };
524
+ }
525
+
182
526
  function looksLikeStaleMemorixCommand(server: MCPServerEntry): boolean {
183
527
  const text = [server.command, ...(server.args ?? [])].join(' ');
184
528
  return /[\\/]\.worktrees[\\/]/i.test(text) || /[\\/]dist[\\/]cli[\\/]index\.js/i.test(text);
@@ -492,10 +836,11 @@ export async function inspectAgentIntegrations(options: {
492
836
  agent,
493
837
  mcp: await inspectMcp(agent, projectRoot, scope),
494
838
  guidance: await inspectGuidance(agent, projectRoot, scope),
839
+ plugin: await inspectPlugin(agent, scope),
495
840
  });
496
841
  }
497
842
 
498
- const entryStatuses = entries.map((entry) => worstStatus([entry.mcp.status, entry.guidance.status]));
843
+ const entryStatuses = entries.map((entry) => worstStatus([entry.mcp.status, entry.guidance.status, entry.plugin.status]));
499
844
  return {
500
845
  projectRoot,
501
846
  scope,
@@ -519,10 +864,11 @@ export function formatAgentIntegrationReport(report: AgentIntegrationReport): st
519
864
  ];
520
865
 
521
866
  for (const entry of report.entries) {
522
- const status = worstStatus([entry.mcp.status, entry.guidance.status]);
867
+ const status = worstStatus([entry.mcp.status, entry.guidance.status, entry.plugin.status]);
523
868
  lines.push(`${entry.agent}: ${status}`);
524
869
  if (entry.mcp.issues.length > 0) lines.push(` MCP: ${entry.mcp.issues.join(', ')}`);
525
870
  if (entry.guidance.issues.length > 0) lines.push(` Guidance: ${entry.guidance.issues.join(', ')}`);
871
+ if (entry.plugin.issues.length > 0) lines.push(` Plugin: ${entry.plugin.issues.join(', ')}`);
526
872
  }
527
873
 
528
874
  lines.push('');
@@ -586,6 +932,42 @@ export async function repairAgentIntegrations(options: {
586
932
  } else {
587
933
  skipped.push(`${entry.agent}:guidance`);
588
934
  }
935
+
936
+ if (entry.agent === 'codex' && entry.plugin.status !== 'ok' && entry.plugin.status !== 'skipped') {
937
+ const bundleOrMarketplaceNeedsRepair = entry.plugin.checks.some((check) =>
938
+ (check.kind === 'bundle' || check.kind === 'marketplace') && check.status !== 'ok'
939
+ );
940
+ const runtime = entry.plugin.checks.find((check) => check.kind === 'runtime');
941
+ const hookTrust = entry.plugin.checks.find((check) => check.kind === 'hook-trust');
942
+ const needsInstall = runtime?.runtime?.installed === false;
943
+ const needsManualEnable = runtime?.runtime?.installed === true && runtime.runtime.enabled === false;
944
+ const needsHookTrust = hookTrust?.status === 'repairable';
945
+ const missingPluginFiles = entry.plugin.checks.some((check) =>
946
+ (check.kind === 'bundle' || check.kind === 'marketplace') && check.status === 'missing'
947
+ );
948
+
949
+ if (needsManualEnable) {
950
+ skipped.push('codex:plugin:global:enable-in-plugin-browser');
951
+ }
952
+ if (needsHookTrust) {
953
+ skipped.push('codex:plugin:global:review-hooks');
954
+ }
955
+
956
+ if (bundleOrMarketplaceNeedsRepair || needsInstall) {
957
+ if ((missingPluginFiles || needsInstall) && !canInstallMissing) {
958
+ skipped.push('codex:plugin:global:missing');
959
+ } else {
960
+ if (!options.dry) {
961
+ await installPluginPackage({ agent: 'codex' });
962
+ if (needsInstall) {
963
+ const install = tryInstallCodexPlugin();
964
+ if (!install.ok) skipped.push('codex:plugin:global:install-pending');
965
+ }
966
+ }
967
+ changed.push('codex:plugin:global');
968
+ }
969
+ }
970
+ }
589
971
  }
590
972
 
591
973
  return {
@@ -300,6 +300,17 @@ async function stripHookCaptureFromPlugin(pluginPath: string, agent: PluginInsta
300
300
  }
301
301
  }
302
302
 
303
+ async function stampCodexPluginVersion(pluginPath: string): Promise<void> {
304
+ const manifestPath = path.join(pluginPath, '.codex-plugin', 'plugin.json');
305
+ const manifest = JSON.parse(await readFile(manifestPath, 'utf-8')) as Record<string, unknown>;
306
+ if (manifest.name !== 'memorix') {
307
+ throw new Error(`Unexpected Codex plugin manifest at ${manifestPath}`);
308
+ }
309
+
310
+ manifest.version = getCliVersion();
311
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
312
+ }
313
+
303
314
  async function stripHookCaptureFromOpenClawBundle(bundlePath: string): Promise<void> {
304
315
  await rm(path.join(bundlePath, 'hooks'), { recursive: true, force: true });
305
316
 
@@ -636,6 +647,7 @@ export async function installPluginPackage(options: PluginInstallOptions): Promi
636
647
  const pluginPath = path.join(home, '.codex', 'plugins', 'memorix');
637
648
  const marketplacePath = path.join(home, '.agents', 'plugins', 'marketplace.json');
638
649
  await copyDir(source, pluginPath);
650
+ await stampCodexPluginVersion(pluginPath);
639
651
  if (!includeHooks) await stripHookCaptureFromPlugin(pluginPath, 'codex');
640
652
  await writeOfficialSkills(path.join(pluginPath, 'skills'));
641
653
  await upsertCodexMarketplace(marketplacePath, pluginPath);
@@ -862,7 +874,7 @@ function tryInstallClaudePlugin(marketplaceRoot: string): { ok: boolean; message
862
874
  };
863
875
  }
864
876
 
865
- function tryInstallCodexPlugin(): { ok: boolean; message: string } {
877
+ export function tryInstallCodexPlugin(): { ok: boolean; message: string } {
866
878
  const result = spawnSync('codex', ['plugin', 'add', 'memorix@personal', '--json'], {
867
879
  encoding: 'utf-8',
868
880
  stdio: 'pipe',