memorix 1.1.11 → 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.
- package/CHANGELOG.md +25 -0
- package/README.md +3 -1
- package/README.zh-CN.md +3 -1
- package/dist/cli/index.js +679 -78
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +289 -44
- package/dist/index.js.map +1 -1
- package/dist/maintenance-runner.js +252 -36
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +25 -0
- package/dist/memcode-runtime/package.json +5 -4
- package/dist/sdk.js +289 -44
- package/dist/sdk.js.map +1 -1
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +2 -2
- package/docs/DEVELOPMENT.md +3 -10
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/SETUP.md +3 -1
- package/docs/dev-log/progress.txt +34 -53
- package/docs/hooks-architecture.md +24 -12
- package/package.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +2 -1
- package/plugins/codex/memorix/hooks/hooks.json +30 -0
- package/src/cli/commands/agent-integrations.ts +384 -2
- package/src/cli/commands/hooks-install.ts +2 -1
- package/src/cli/commands/setup.ts +20 -2
- package/src/cli/tui/chat-service.ts +3 -9
- package/src/cli/tui/data.ts +9 -11
- package/src/embedding/api-provider.ts +105 -6
- package/src/embedding/provider.ts +31 -3
- package/src/hooks/handler.ts +51 -14
- package/src/hooks/installers/index.ts +48 -7
- package/src/hooks/normalizer.ts +15 -0
- package/src/memory/observations.ts +90 -15
- package/src/store/orama-store.ts +169 -17
|
@@ -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 {
|
|
@@ -142,6 +142,7 @@ async function installSingleAgent(agent: string, cwd: string, global: boolean):
|
|
|
142
142
|
function getAgentLabel(agent: string): string {
|
|
143
143
|
const labels: Record<string, string> = {
|
|
144
144
|
claude: 'Claude Code',
|
|
145
|
+
codex: 'Codex',
|
|
145
146
|
windsurf: 'Windsurf',
|
|
146
147
|
cursor: 'Cursor',
|
|
147
148
|
copilot: 'VS Code Copilot',
|
|
@@ -171,7 +172,7 @@ function getAgentHint(agent: string): string {
|
|
|
171
172
|
hermes: 'Hermes plugin hooks; use `memorix setup --agent hermes`',
|
|
172
173
|
omp: 'Oh-my-Pi package hooks; use `memorix setup --agent omp`',
|
|
173
174
|
trae: '.trae/rules/project_rules.md (rules only, no hooks system)',
|
|
174
|
-
codex: '
|
|
175
|
+
codex: 'Codex plugin hooks; use `memorix setup --agent codex`',
|
|
175
176
|
};
|
|
176
177
|
return hints[agent] || '';
|
|
177
178
|
}
|
|
@@ -192,6 +192,7 @@ export function buildSetupPlan(options: {
|
|
|
192
192
|
hooks?: boolean;
|
|
193
193
|
rules?: boolean;
|
|
194
194
|
plugin?: boolean;
|
|
195
|
+
global?: boolean;
|
|
195
196
|
}): SetupPlan {
|
|
196
197
|
const mcp = options.mcp ?? 'stdio';
|
|
197
198
|
const actions: SetupAction[] = [];
|
|
@@ -206,7 +207,7 @@ export function buildSetupPlan(options: {
|
|
|
206
207
|
if (wantsPlugin && options.agent !== 'all') {
|
|
207
208
|
if (options.agent === 'opencode') {
|
|
208
209
|
actions.push('opencode-local-plugin');
|
|
209
|
-
} else if (PLUGIN_PACKAGE_AGENTS.has(options.agent)) {
|
|
210
|
+
} else if (PLUGIN_PACKAGE_AGENTS.has(options.agent) && options.global) {
|
|
210
211
|
actions.push('plugin-package');
|
|
211
212
|
} else if (EXTENSION_PACKAGE_AGENTS.has(options.agent)) {
|
|
212
213
|
actions.push('extension-package');
|
|
@@ -299,6 +300,17 @@ async function stripHookCaptureFromPlugin(pluginPath: string, agent: PluginInsta
|
|
|
299
300
|
}
|
|
300
301
|
}
|
|
301
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
|
+
|
|
302
314
|
async function stripHookCaptureFromOpenClawBundle(bundlePath: string): Promise<void> {
|
|
303
315
|
await rm(path.join(bundlePath, 'hooks'), { recursive: true, force: true });
|
|
304
316
|
|
|
@@ -635,6 +647,7 @@ export async function installPluginPackage(options: PluginInstallOptions): Promi
|
|
|
635
647
|
const pluginPath = path.join(home, '.codex', 'plugins', 'memorix');
|
|
636
648
|
const marketplacePath = path.join(home, '.agents', 'plugins', 'marketplace.json');
|
|
637
649
|
await copyDir(source, pluginPath);
|
|
650
|
+
await stampCodexPluginVersion(pluginPath);
|
|
638
651
|
if (!includeHooks) await stripHookCaptureFromPlugin(pluginPath, 'codex');
|
|
639
652
|
await writeOfficialSkills(path.join(pluginPath, 'skills'));
|
|
640
653
|
await upsertCodexMarketplace(marketplacePath, pluginPath);
|
|
@@ -861,7 +874,7 @@ function tryInstallClaudePlugin(marketplaceRoot: string): { ok: boolean; message
|
|
|
861
874
|
};
|
|
862
875
|
}
|
|
863
876
|
|
|
864
|
-
function tryInstallCodexPlugin(): { ok: boolean; message: string } {
|
|
877
|
+
export function tryInstallCodexPlugin(): { ok: boolean; message: string } {
|
|
865
878
|
const result = spawnSync('codex', ['plugin', 'add', 'memorix@personal', '--json'], {
|
|
866
879
|
encoding: 'utf-8',
|
|
867
880
|
stdio: 'pipe',
|
|
@@ -1061,6 +1074,10 @@ async function installAgentSetup(agent: AgentName, plan: SetupPlan, global: bool
|
|
|
1061
1074
|
const hasOmpPackage = plan.actions.includes('omp-package') && agent === 'omp';
|
|
1062
1075
|
const wantsMcpConfig = plan.mcp !== 'none' && agent !== 'pi';
|
|
1063
1076
|
|
|
1077
|
+
if (PLUGIN_PACKAGE_AGENTS.has(agent) && plan.includePlugin && !global) {
|
|
1078
|
+
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.`);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1064
1081
|
if (hasPluginPackage) {
|
|
1065
1082
|
const result = await installPluginPackage({
|
|
1066
1083
|
agent: agent as PluginInstallOptions['agent'],
|
|
@@ -1322,6 +1339,7 @@ export default defineCommand({
|
|
|
1322
1339
|
hooks: !args.noHooks,
|
|
1323
1340
|
rules: !args.noRules,
|
|
1324
1341
|
plugin: !args.noPlugin,
|
|
1342
|
+
global: Boolean(args.global),
|
|
1325
1343
|
});
|
|
1326
1344
|
await installAgentSetup(agent, plan, Boolean(args.global));
|
|
1327
1345
|
}
|
|
@@ -2,11 +2,10 @@ import { compactDetail, compactSearch } from '../../compact/engine.js';
|
|
|
2
2
|
import { loadDotenv } from '../../config/dotenv-loader.js';
|
|
3
3
|
import { initLLM, isLLMEnabled, getLLMConfig, callLLMWithTools } from '../../llm/provider.js';
|
|
4
4
|
import type { ChatMessage, ToolDefinition, ToolCall } from '../../llm/provider.js';
|
|
5
|
-
import { initObservations, storeObservation, resolveObservations, getObservation, getAllObservations } from '../../memory/observations.js';
|
|
5
|
+
import { initObservations, prepareSearchIndex, storeObservation, resolveObservations, getObservation, getAllObservations } from '../../memory/observations.js';
|
|
6
6
|
import type { ObservationType } from '../../types.js';
|
|
7
7
|
import { detectProject } from '../../project/detector.js';
|
|
8
|
-
import {
|
|
9
|
-
import { getDb, getLastSearchMode, hydrateIndex } from '../../store/orama-store.js';
|
|
8
|
+
import { getLastSearchMode } from '../../store/orama-store.js';
|
|
10
9
|
import { getProjectDataDir } from '../../store/persistence.js';
|
|
11
10
|
import type { MemorixDocument } from '../../types.js';
|
|
12
11
|
|
|
@@ -200,13 +199,8 @@ const preparedProjects = new Set<string>();
|
|
|
200
199
|
async function prepareProjectSearch(projectId: string, dataDir: string): Promise<void> {
|
|
201
200
|
if (preparedProjects.has(projectId)) return;
|
|
202
201
|
|
|
203
|
-
await initObservationStore(dataDir);
|
|
204
202
|
await initObservations(dataDir); // loads observations into memory once
|
|
205
|
-
await
|
|
206
|
-
// Use the already-loaded in-memory observations (from initObservations)
|
|
207
|
-
// instead of calling loadAll() a second time.
|
|
208
|
-
const allObs = getAllObservations() as any[];
|
|
209
|
-
await hydrateIndex(allObs); // idempotent: skips if index already populated
|
|
203
|
+
await prepareSearchIndex(); // lexical-ready now; vector recovery stays in the background
|
|
210
204
|
preparedProjects.add(projectId);
|
|
211
205
|
}
|
|
212
206
|
|
package/src/cli/tui/data.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface ProjectInfo {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export interface HealthInfo {
|
|
18
|
-
embeddingProvider: 'ready' | 'unavailable' | 'disabled';
|
|
18
|
+
embeddingProvider: 'ready' | 'pending' | 'unavailable' | 'disabled';
|
|
19
19
|
embeddingProviderName?: string;
|
|
20
20
|
embeddingLabel: string;
|
|
21
21
|
searchMode: string;
|
|
@@ -84,6 +84,7 @@ function formatEmbeddingLabel(
|
|
|
84
84
|
providerName?: string,
|
|
85
85
|
): string {
|
|
86
86
|
if (status === 'disabled') return 'Disabled';
|
|
87
|
+
if (status === 'pending') return 'On demand';
|
|
87
88
|
if (status === 'unavailable') return 'Unavailable';
|
|
88
89
|
if ((providerName || '').startsWith('api-')) return 'API ready';
|
|
89
90
|
if (providerName) return 'Local ready';
|
|
@@ -165,8 +166,8 @@ export async function getHealthInfo(projectId?: string): Promise<HealthInfo> {
|
|
|
165
166
|
if (isEmbeddingExplicitlyDisabled()) {
|
|
166
167
|
defaults.embeddingProvider = 'disabled';
|
|
167
168
|
} else {
|
|
168
|
-
const provider = await getEmbeddingProvider();
|
|
169
|
-
defaults.embeddingProvider = provider ? 'ready' : '
|
|
169
|
+
const provider = await getEmbeddingProvider({ allowNetworkProbe: false });
|
|
170
|
+
defaults.embeddingProvider = provider ? 'ready' : 'pending';
|
|
170
171
|
defaults.embeddingProviderName = provider?.name;
|
|
171
172
|
}
|
|
172
173
|
defaults.embeddingLabel = formatEmbeddingLabel(
|
|
@@ -197,6 +198,8 @@ export async function getHealthInfo(projectId?: string): Promise<HealthInfo> {
|
|
|
197
198
|
defaults.searchDiagnostic = 'Vector index ready';
|
|
198
199
|
} else if (defaults.embeddingProvider === 'ready' && !vectorActive) {
|
|
199
200
|
defaults.searchDiagnostic = 'Provider ready, no index yet';
|
|
201
|
+
} else if (defaults.embeddingProvider === 'pending') {
|
|
202
|
+
defaults.searchDiagnostic = 'BM25 ready; embeddings initialize on demand';
|
|
200
203
|
} else if (defaults.embeddingProvider === 'unavailable') {
|
|
201
204
|
defaults.searchDiagnostic = 'BM25 only (no provider)';
|
|
202
205
|
} else {
|
|
@@ -247,22 +250,17 @@ export async function getRecentMemories(limit = 8, projectId?: string): Promise<
|
|
|
247
250
|
|
|
248
251
|
export async function searchMemories(query: string, limit = 10): Promise<SearchResult[]> {
|
|
249
252
|
try {
|
|
250
|
-
const { searchObservations
|
|
253
|
+
const { searchObservations } = await import('../../store/orama-store.js');
|
|
251
254
|
const { getProjectDataDir } = await import('../../store/persistence.js');
|
|
252
255
|
const { detectProject } = await import('../../project/detector.js');
|
|
253
|
-
const { initObservations } = await import('../../memory/observations.js');
|
|
254
|
-
const { initObservationStore, getObservationStore: getStore } = await import('../../store/obs-store.js');
|
|
256
|
+
const { initObservations, prepareSearchIndex } = await import('../../memory/observations.js');
|
|
255
257
|
|
|
256
258
|
const proj = detectProject(process.cwd());
|
|
257
259
|
if (!proj) return [];
|
|
258
260
|
|
|
259
261
|
const dataDir = await getProjectDataDir(proj.id);
|
|
260
|
-
await initObservationStore(dataDir);
|
|
261
262
|
await initObservations(dataDir);
|
|
262
|
-
await
|
|
263
|
-
|
|
264
|
-
const allObs = (await getStore().loadAll()) as any[];
|
|
265
|
-
await hydrateIndex(allObs);
|
|
263
|
+
await prepareSearchIndex();
|
|
266
264
|
|
|
267
265
|
const results = await searchObservations({ query, limit, projectId: proj.id });
|
|
268
266
|
|