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.
@@ -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: 'AGENTS.md (rules only, hooks are experimental)',
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 { initObservationStore } from '../../store/obs-store.js';
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 getDb();
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
 
@@ -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' : 'unavailable';
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, getDb, hydrateIndex } = await import('../../store/orama-store.js');
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 getDb();
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