memorix 1.4.5 → 1.4.6

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.
Files changed (75) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/CLAUDE.md +7 -3
  3. package/README.md +5 -2
  4. package/dist/cli/index.js +1456 -1013
  5. package/dist/index.js +1265 -988
  6. package/dist/{maintenance-jobs-B1-W0ssz.d.ts → maintenance-jobs-DVRF-46r.d.ts} +1 -1
  7. package/dist/maintenance-runner.d.ts +1 -1
  8. package/dist/maintenance-runner.js +1755 -829
  9. package/dist/media-audio-runner.js +1760 -835
  10. package/dist/media-video-runner.js +1755 -830
  11. package/dist/memcode-runtime/CHANGELOG.md +54 -0
  12. package/dist/sdk.js +1257 -987
  13. package/dist/types.d.ts +6 -0
  14. package/dist/vector-backfill-runner.d.ts +1 -1
  15. package/dist/vector-backfill-runner.js +1751 -826
  16. package/docs/1.4.5-AGENT-AUTO-MEMORY-RESEARCH.md +95 -0
  17. package/docs/API_REFERENCE.md +23 -4
  18. package/docs/CONFIGURATION.md +17 -3
  19. package/docs/SETUP.md +5 -3
  20. package/package.json +1 -1
  21. package/plugins/antigravity/memorix/mcp_config.json +3 -1
  22. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  23. package/plugins/claude/memorix/.mcp.json +1 -1
  24. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  25. package/plugins/codebuddy/memorix/.mcp.json +1 -1
  26. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  27. package/plugins/codex/memorix/.mcp.json +1 -1
  28. package/plugins/copilot/memorix/.mcp.json +1 -1
  29. package/plugins/copilot/memorix/plugin.json +1 -1
  30. package/plugins/gemini/memorix/gemini-extension.json +4 -2
  31. package/plugins/omp/memorix/package.json +1 -1
  32. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  33. package/plugins/openclaw/memorix/.mcp.json +3 -1
  34. package/plugins/pi/memorix/package.json +1 -1
  35. package/server.json +2 -2
  36. package/src/cli/commands/dashboard.ts +14 -3
  37. package/src/cli/commands/ingest-commit.ts +47 -3
  38. package/src/cli/commands/memory.ts +17 -4
  39. package/src/cli/commands/purge.ts +120 -0
  40. package/src/cli/commands/serve-http.ts +39 -5
  41. package/src/cli/commands/setup.ts +3 -1
  42. package/src/cli/index.ts +3 -804
  43. package/src/codegraph/auto-context.ts +82 -1
  44. package/src/config/yaml-loader.ts +3 -8
  45. package/src/config.ts +0 -5
  46. package/src/evaluation/session-replay.ts +98 -0
  47. package/src/git/extractor.ts +5 -3
  48. package/src/hooks/installers/index.ts +15 -0
  49. package/src/knowledge/workset.ts +58 -0
  50. package/src/memory/long-term.ts +97 -2
  51. package/src/runtime/control-plane-maintenance.ts +1 -0
  52. package/src/runtime/isolated-maintenance.ts +1 -0
  53. package/src/runtime/lifecycle.ts +19 -0
  54. package/src/runtime/maintenance-jobs.ts +1 -0
  55. package/src/runtime/project-maintenance.ts +8 -0
  56. package/src/search/surfaced-registry.ts +62 -0
  57. package/src/server.ts +74 -14
  58. package/src/store/orama-store.ts +17 -1
  59. package/src/types.ts +6 -0
  60. package/src/cli/commands/audit-list.ts +0 -89
  61. package/src/cli/commands/purge-all-memory.ts +0 -85
  62. package/src/cli/commands/purge-project-memory.ts +0 -83
  63. package/src/cli/commands/uninstall-project-artifacts.ts +0 -86
  64. package/src/cli/tui/Sidebar.tsx +0 -179
  65. package/src/cli/tui/interactive-commands.ts +0 -261
  66. package/src/cli/tui/markdown-render.tsx +0 -371
  67. package/src/cli/tui/use-mouse.ts +0 -157
  68. package/src/cli/tui/useNavigation.ts +0 -56
  69. package/src/orchestrate/context-compact.ts +0 -188
  70. package/src/team/file-locks.ts +0 -173
  71. package/src/team/index.ts +0 -23
  72. package/src/team/messages.ts +0 -203
  73. package/src/team/persistence.ts +0 -96
  74. package/src/team/registry.ts +0 -158
  75. package/src/team/tasks.ts +0 -211
package/src/cli/index.ts CHANGED
@@ -52,679 +52,10 @@ function ensureMemcodePackageDir(): void {
52
52
  }
53
53
  }
54
54
 
55
- // ============================================================
56
- // Workbench — Terminal-native memory control plane
57
- // ============================================================
58
-
59
55
  const DIM = '\x1b[2m';
60
56
  const RESET = '\x1b[0m';
61
- const BOLD = '\x1b[1m';
62
57
  const CYAN = '\x1b[36m';
63
- const GREEN = '\x1b[32m';
64
58
  const YELLOW = '\x1b[33m';
65
- const BLUE = '\x1b[34m';
66
-
67
- type SpinnerLike = {
68
- start: (message: string) => void;
69
- stop: (message?: string) => void;
70
- };
71
-
72
- function createSpinnerForCurrentOutput(): SpinnerLike {
73
- if (process.stdout.isTTY && process.stdin.isTTY) {
74
- return p.spinner();
75
- }
76
- return {
77
- start: () => {},
78
- stop: () => {},
79
- };
80
- }
81
-
82
- async function getWorkbenchHeader(): Promise<string[]> {
83
- const lines: string[] = [];
84
- const ver = `v${getCliVersion()}`;
85
-
86
- // Detect project
87
- let projectLabel = `${YELLOW}no git repo${RESET} ${DIM}— run \`git init\` to enable${RESET}`;
88
- let projectDetected = false;
89
- try {
90
- const { detectProject } = await import('../project/detector.js');
91
- const proj = detectProject(process.cwd());
92
- if (proj) {
93
- projectLabel = `${BOLD}${proj.name}${RESET} ${DIM}(${proj.id})${RESET}`;
94
- projectDetected = true;
95
- }
96
- } catch { /* ignore */ }
97
-
98
- // Detect mode
99
- let modeLabel = `${DIM}CLI${RESET}`;
100
- try {
101
- const { existsSync, readFileSync } = await import('node:fs');
102
- const { join } = await import('node:path');
103
- const { homedir } = await import('node:os');
104
- const bgPath = join(homedir(), '.memorix', 'background.json');
105
- if (existsSync(bgPath)) {
106
- const bg = JSON.parse(readFileSync(bgPath, 'utf-8'));
107
- try { process.kill(bg.pid, 0); modeLabel = `${GREEN}Background${RESET} ${DIM}(port ${bg.port})${RESET}`; } catch { /* dead */ }
108
- }
109
- } catch { /* ignore */ }
110
-
111
- // Detect embedding
112
- let searchLabel = `${DIM}fulltext (BM25)${RESET}`;
113
- try {
114
- const { getEmbeddingMode } = await import('../config.js');
115
- const mode = getEmbeddingMode();
116
- if (mode !== 'off') {
117
- searchLabel = `${CYAN}hybrid${RESET} ${DIM}(BM25 + vector)${RESET}`;
118
- }
119
- } catch { /* ignore */ }
120
-
121
- // Count memories
122
- let memLabel = `${DIM}unknown${RESET}`;
123
- if (projectDetected) {
124
- try {
125
- const { detectProject } = await import('../project/detector.js');
126
- const { getProjectDataDir } = await import('../store/persistence.js');
127
- const { initObservationStore: initStore } = await import('../store/obs-store.js');
128
- const proj = detectProject(process.cwd());
129
- if (proj) {
130
- const dataDir = await getProjectDataDir(proj.id);
131
- await initStore(dataDir);
132
- const { getObservationStore: getStore } = await import('../store/obs-store.js');
133
- const { filterReadableObservations } = await import('../memory/visibility.js');
134
- const obs = filterReadableObservations(await getStore().loadAll(), { projectId: proj.id });
135
- const active = obs.filter((o) => (o.status ?? 'active') === 'active').length;
136
- if (active > 0) {
137
- memLabel = `${BOLD}${active}${RESET} ${DIM}active${RESET}`;
138
- }
139
- }
140
- } catch { /* ignore */ }
141
- }
142
-
143
- lines.push('');
144
- lines.push(` ${BOLD}Memorix Workbench${RESET}${DIM}${' '.repeat(36)}${ver}${RESET}`);
145
- lines.push(` ${DIM}${'─'.repeat(55)}${RESET}`);
146
- lines.push(` ${DIM}Project${RESET} ${projectLabel}`);
147
- lines.push(` ${DIM}Mode${RESET} ${modeLabel}`);
148
- lines.push(` ${DIM}Search${RESET} ${searchLabel}`);
149
- lines.push(` ${DIM}Memories${RESET} ${memLabel}`);
150
- lines.push(` ${DIM}${'─'.repeat(55)}${RESET}`);
151
- lines.push('');
152
-
153
- return lines;
154
- }
155
-
156
- function printSlashHelp(): void {
157
- console.log('');
158
- console.log(` ${BOLD}Commands${RESET}`);
159
- console.log(` ${DIM}${'─'.repeat(45)}${RESET}`);
160
- console.log(` ${CYAN}/search${RESET} ${DIM}<query>${RESET} Search memories`);
161
- console.log(` ${CYAN}/remember${RESET} ${DIM}<text>${RESET} Store a quick memory`);
162
- console.log(` ${CYAN}/recent${RESET} View recent memories`);
163
- console.log(` ${CYAN}/doctor${RESET} System diagnostics`);
164
- console.log(` ${CYAN}/project${RESET} Project details`);
165
- console.log(` ${CYAN}/background${RESET} Background service`);
166
- console.log(` ${CYAN}/dashboard${RESET} Open dashboard`);
167
- console.log(` ${CYAN}/configure${RESET} Settings`);
168
- console.log(` ${CYAN}/integrate${RESET} Set up an IDE`);
169
- console.log(` ${CYAN}/exit${RESET} Exit workbench`);
170
- console.log(` ${DIM}${'─'.repeat(45)}${RESET}`);
171
- console.log(` ${DIM}Or just type to search memories directly.${RESET}`);
172
- console.log('');
173
- }
174
-
175
- async function runRemember(text: string): Promise<void> {
176
- const s = createSpinnerForCurrentOutput();
177
- s.start('Storing memory...');
178
-
179
- try {
180
- const { detectProject } = await import('../project/detector.js');
181
- const { getProjectDataDir } = await import('../store/persistence.js');
182
- const { initObservations, storeObservation } = await import('../memory/observations.js');
183
- const { initObservationStore } = await import('../store/obs-store.js');
184
-
185
- const proj = detectProject(process.cwd());
186
- if (!proj) { s.stop('No git repo'); p.log.error(NO_GIT_MSG); return; }
187
- const dataDir = await getProjectDataDir(proj.id);
188
- await initObservationStore(dataDir);
189
- await initObservations(dataDir);
190
-
191
- const result = await storeObservation({
192
- entityName: 'quick-note',
193
- type: 'discovery',
194
- title: text.slice(0, 100),
195
- narrative: text,
196
- facts: [],
197
- projectId: proj.id,
198
- sourceDetail: 'explicit',
199
- });
200
-
201
- s.stop('Stored');
202
- p.log.success(`Memory #${result.observation.id} saved: ${text.slice(0, 60)}${text.length > 60 ? '...' : ''}`);
203
- } catch (err) {
204
- s.stop('Failed');
205
- p.log.error(`Error: ${err instanceof Error ? err.message : err}`);
206
- }
207
- }
208
-
209
- async function workbench(): Promise<void> {
210
- // Print header
211
- const header = await getWorkbenchHeader();
212
- for (const line of header) console.log(line);
213
-
214
- console.log(` ${DIM}Type to search memories, or /help for commands.${RESET}`);
215
- console.log('');
216
-
217
- // Main input loop
218
- while (true) {
219
- const input = await p.text({
220
- message: `${CYAN}>${RESET}`,
221
- placeholder: 'search memories or /command',
222
- });
223
-
224
- if (p.isCancel(input)) {
225
- p.outro(`${DIM}Goodbye${RESET}`);
226
- process.exit(0);
227
- }
228
-
229
- const raw = (input || '').trim();
230
- if (!raw) continue;
231
-
232
- // Slash command routing
233
- if (raw.startsWith('/')) {
234
- const parts = raw.slice(1).split(/\s+/);
235
- const cmd = parts[0]?.toLowerCase();
236
- const arg = parts.slice(1).join(' ');
237
-
238
- switch (cmd) {
239
- case 'search':
240
- case 's':
241
- if (arg) { await runSearch(arg); } else {
242
- const q = await p.text({ message: 'Search query:', placeholder: 'e.g., authentication bug' });
243
- if (!p.isCancel(q) && q) await runSearch(q);
244
- }
245
- break;
246
- case 'remember':
247
- case 'r':
248
- if (arg) { await runRemember(arg); } else {
249
- const t = await p.text({ message: 'What to remember:', placeholder: 'e.g., Use path.join for Windows' });
250
- if (!p.isCancel(t) && t) await runRemember(t);
251
- }
252
- break;
253
- case 'recent':
254
- await runList();
255
- break;
256
- case 'doctor':
257
- await runCommand('doctor');
258
- break;
259
- case 'project':
260
- case 'status':
261
- await runCommand('status');
262
- break;
263
- case 'background':
264
- case 'bg':
265
- await runBackgroundMenu();
266
- break;
267
- case 'dashboard':
268
- case 'dash':
269
- await runCommand('dashboard');
270
- return;
271
- case 'configure':
272
- case 'config':
273
- await runConfigure();
274
- break;
275
- case 'integrate':
276
- case 'setup': {
277
- const m = await import('./commands/integrate.js');
278
- await m.default.run?.({ args: { _: [] }, rawArgs: [], cmd: m.default } as any);
279
- break;
280
- }
281
- case 'serve':
282
- p.log.info('Starting MCP server on stdio...');
283
- await runCommand('serve');
284
- return;
285
- case 'serve-http':
286
- p.log.info('Starting control plane...');
287
- await runCommand('serve-http');
288
- return;
289
- case 'sync':
290
- await runCommand('sync');
291
- break;
292
- case 'cleanup':
293
- await runCleanupMenu();
294
- break;
295
- case 'ingest':
296
- await runIngestMenu();
297
- break;
298
- case 'help':
299
- case '?':
300
- printSlashHelp();
301
- break;
302
- case 'exit':
303
- case 'quit':
304
- case 'q':
305
- p.outro(`${DIM}Goodbye${RESET}`);
306
- process.exit(0);
307
- break; // unreachable
308
- default:
309
- p.log.warn(`Unknown command: /${cmd}. Type /help for available commands.`);
310
- }
311
- } else {
312
- // Default: treat as search query
313
- await runSearch(raw);
314
- }
315
-
316
- console.log(''); // spacing
317
- }
318
- }
319
-
320
- async function runBackgroundMenu(): Promise<void> {
321
- const action = await p.select({
322
- message: 'Background Control Plane:',
323
- options: [
324
- { value: 'start', label: 'Start', hint: 'launch control plane in background' },
325
- { value: 'stop', label: 'Stop', hint: 'stop the background service' },
326
- { value: 'status', label: 'Status', hint: 'show running state & health' },
327
- { value: 'restart', label: 'Restart', hint: 'stop + start' },
328
- { value: 'logs', label: 'View logs', hint: 'show recent log output' },
329
- { value: 'back', label: '\u2190 Back', hint: 'return to main menu' },
330
- ],
331
- });
332
-
333
- if (p.isCancel(action) || action === 'back') return;
334
-
335
- const m = await import('./commands/background.js');
336
- // Synthesize citty args with the chosen subcommand
337
- await m.default.run?.({
338
- args: { _: [action], port: '3211', follow: false, lines: '50' },
339
- rawArgs: [action],
340
- cmd: m.default,
341
- } as any);
342
- }
343
-
344
- async function runHooksMenu(): Promise<void> {
345
- const action = await p.select({
346
- message: 'Hooks management:',
347
- options: [
348
- { value: 'install', label: 'Install hooks', hint: 'legacy compatibility path' },
349
- { value: 'preview', label: 'Preview installation', hint: 'show files to be created' },
350
- { value: 'uninstall', label: 'Uninstall hooks', hint: 'remove from all agents' },
351
- { value: 'status', label: 'Status', hint: 'show installed hooks' },
352
- { value: 'back', label: '← Back', hint: 'return to main menu' },
353
- ],
354
- });
355
-
356
- if (p.isCancel(action) || action === 'back') return;
357
-
358
- switch (action) {
359
- case 'install': {
360
- const m = await import('./commands/hooks-install.js');
361
- await m.default.run?.({ args: { _: [] }, rawArgs: [], cmd: m.default } as any);
362
- break;
363
- }
364
- case 'preview': {
365
- const m = await import('./commands/hooks-preview.js');
366
- await m.default.run?.({ args: { _: [] }, rawArgs: [], cmd: m.default } as any);
367
- break;
368
- }
369
- case 'uninstall': {
370
- const m = await import('./commands/hooks-uninstall.js');
371
- await m.default.run?.({ args: { _: [] }, rawArgs: [], cmd: m.default } as any);
372
- break;
373
- }
374
- case 'status': {
375
- const m = await import('./commands/hooks-status.js');
376
- await m.default.run?.({ args: { _: [] }, rawArgs: [], cmd: m.default } as any);
377
- break;
378
- }
379
- }
380
- }
381
-
382
- async function runCleanupMenu(): Promise<void> {
383
- const action = await p.select({
384
- message: 'Cleanup options:',
385
- options: [
386
- { value: 'project-artifacts', label: 'Uninstall project artifacts', hint: 'remove hook files only' },
387
- { value: 'project-memory', label: 'Purge project memory', hint: 'delete current project memories' },
388
- { value: 'all-memory', label: 'Purge all memory', hint: '[WARN] delete ALL memories' },
389
- { value: 'back', label: '← Back', hint: 'return to main menu' },
390
- ],
391
- });
392
-
393
- if (p.isCancel(action) || action === 'back') return;
394
-
395
- switch (action) {
396
- case 'project-artifacts': {
397
- const m = await import('./commands/uninstall-project-artifacts.js');
398
- await m.default.run?.({ args: { _: [] }, rawArgs: [], cmd: m.default } as any);
399
- break;
400
- }
401
- case 'project-memory': {
402
- const m = await import('./commands/purge-project-memory.js');
403
- await m.default.run?.({ args: { _: [] }, rawArgs: [], cmd: m.default } as any);
404
- break;
405
- }
406
- case 'all-memory': {
407
- const m = await import('./commands/purge-all-memory.js');
408
- await m.default.run?.({ args: { _: [] }, rawArgs: [], cmd: m.default } as any);
409
- break;
410
- }
411
- }
412
- }
413
-
414
- async function runIngestMenu(): Promise<void> {
415
- const action = await p.select({
416
- message: 'Git → Memory:',
417
- options: [
418
- { value: 'commit', label: 'Ingest commit', hint: 'single commit → memory' },
419
- { value: 'log', label: 'Ingest log', hint: 'batch recent commits → memories' },
420
- { value: 'git-hook', label: 'Install git hook', hint: 'auto-capture on every commit' },
421
- { value: 'git-hook-uninstall', label: 'Uninstall git hook', hint: 'remove auto-capture' },
422
- { value: 'back', label: '← Back', hint: 'return to main menu' },
423
- ],
424
- });
425
-
426
- if (p.isCancel(action) || action === 'back') return;
427
-
428
- switch (action) {
429
- case 'commit': {
430
- const m = await import('./commands/ingest-commit.js');
431
- await m.default.run?.({ args: { _: [] }, rawArgs: [], cmd: m.default } as any);
432
- break;
433
- }
434
- case 'log': {
435
- const m = await import('./commands/ingest-log.js');
436
- await m.default.run?.({ args: { _: [] }, rawArgs: [], cmd: m.default } as any);
437
- break;
438
- }
439
- case 'git-hook': {
440
- const m = await import('./commands/git-hook-install.js');
441
- await m.default.run?.({ args: { _: [] }, rawArgs: [], cmd: m.default } as any);
442
- break;
443
- }
444
- case 'git-hook-uninstall': {
445
- const m = await import('./commands/git-hook-uninstall.js');
446
- await m.default.run?.({ args: { _: [] }, rawArgs: [], cmd: m.default } as any);
447
- break;
448
- }
449
- }
450
- }
451
-
452
- async function runAuditList(): Promise<void> {
453
- const m = await import('./commands/audit-list.js');
454
- await m.default.run?.({ args: { _: [] }, rawArgs: [], cmd: m.default } as any);
455
- }
456
-
457
- async function runConfigure(): Promise<void> {
458
- const configPath = `${process.env.HOME || process.env.USERPROFILE}/.memorix/config.json`;
459
-
460
- // Helper: load config from disk
461
- const loadConfig = async () => {
462
- try {
463
- const fs = await import('node:fs');
464
- if (fs.existsSync(configPath)) {
465
- return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
466
- }
467
- } catch { /* ignore */ }
468
- return {};
469
- };
470
-
471
- // Helper: save config to disk
472
- const saveConfig = async (config: Record<string, unknown>) => {
473
- const fs = await import('node:fs');
474
- const dir = `${process.env.HOME || process.env.USERPROFILE}/.memorix`;
475
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
476
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
477
- };
478
-
479
- // Loop: configure multiple things without going back to main menu
480
- while (true) {
481
- const config = await loadConfig();
482
-
483
- const section = await p.select({
484
- message: 'What would you like to configure?',
485
- options: [
486
- { value: 'llm', label: 'LLM Enhanced Mode', hint: 'smart dedup + fact extraction' },
487
- { value: 'embedding', label: 'Embedding Provider', hint: 'semantic search' },
488
- { value: 'behavior', label: 'Behavior Settings', hint: 'session inject, auto-cleanup, sync advisory' },
489
- { value: 'show', label: 'Show current config', hint: 'view settings' },
490
- { value: 'back', label: '\u2190 Back', hint: 'return to main menu' },
491
- ],
492
- });
493
-
494
- if (p.isCancel(section) || section === 'back') return;
495
-
496
- if (section === 'show') {
497
- console.log('\nCurrent configuration:');
498
- console.log(` Config file: ${configPath}`);
499
- console.log(` LLM Provider: ${config.llm?.provider ?? 'not configured'}`);
500
- console.log(` LLM Model: ${config.llm?.model ?? 'default'}`);
501
- console.log(` LLM Base URL: ${config.llm?.baseUrl ?? '(default)'}`);
502
- console.log(` LLM API Key: ${config.llm?.apiKey ? '***configured***' : 'not set'}`);
503
- console.log(` Embedding: ${config.embedding ?? 'off (BM25 only)'}`);
504
- if (config.embedding === 'api') {
505
- const apiConf = config.embeddingApi;
506
- if (apiConf) {
507
- console.log(` Embedding Model: ${apiConf.model ?? 'text-embedding-3-small'}`);
508
- console.log(` Embedding Base URL: ${apiConf.baseUrl ?? '(default)'}`);
509
- console.log(` Embedding API Key: ${apiConf.apiKey ? '***configured***' : '(reusing LLM key)'}`);
510
- if (apiConf.dimensions) console.log(` Embedding Dimensions: ${apiConf.dimensions}`);
511
- }
512
- }
513
- console.log('\nEnvironment overrides (take priority over config.json):');
514
- console.log(` MEMORIX_LLM_API_KEY: ${process.env.MEMORIX_LLM_API_KEY ? '***set***' : 'not set'}`);
515
- console.log(` OPENAI_API_KEY: ${process.env.OPENAI_API_KEY ? '***set***' : 'not set'}`);
516
- console.log(` MEMORIX_EMBEDDING: ${process.env.MEMORIX_EMBEDDING ?? 'not set'}`);
517
- if (process.env.MEMORIX_EMBEDDING === 'api') {
518
- console.log(` MEMORIX_EMBEDDING_API_KEY: ${process.env.MEMORIX_EMBEDDING_API_KEY ? '***set***' : 'not set'}`);
519
- console.log(` MEMORIX_EMBEDDING_BASE_URL: ${process.env.MEMORIX_EMBEDDING_BASE_URL ?? 'not set'}`);
520
- console.log(` MEMORIX_EMBEDDING_MODEL: ${process.env.MEMORIX_EMBEDDING_MODEL ?? 'text-embedding-3-small'}`);
521
- console.log(` MEMORIX_EMBEDDING_DIMENSIONS: ${process.env.MEMORIX_EMBEDDING_DIMENSIONS ?? 'auto'}`);
522
- }
523
- console.log('');
524
- continue; // Back to configure menu
525
- }
526
-
527
- if (section === 'llm') {
528
- const provider = await p.select({
529
- message: 'Select LLM provider:',
530
- options: [
531
- { value: 'openai', label: 'OpenAI', hint: 'gpt-4o-mini recommended' },
532
- { value: 'anthropic', label: 'Anthropic', hint: 'claude-3-haiku' },
533
- { value: 'openrouter', label: 'OpenRouter', hint: 'multi-provider' },
534
- { value: 'custom', label: 'Custom endpoint', hint: 'OpenAI-compatible proxy / local' },
535
- { value: 'disable', label: 'Disable LLM', hint: 'use free heuristic mode' },
536
- ],
537
- });
538
-
539
- if (p.isCancel(provider)) continue; // Back to configure menu
540
-
541
- if (provider === 'disable') {
542
- config.llm = undefined;
543
- await saveConfig(config);
544
- p.log.success('LLM mode disabled. Using free heuristic deduplication.');
545
- continue;
546
- }
547
-
548
- const apiKey = await p.password({
549
- message: 'Enter API key:',
550
- });
551
-
552
- if (p.isCancel(apiKey) || !apiKey) {
553
- continue; // Back to configure menu
554
- }
555
-
556
- let defaultModel = 'gpt-4o-mini';
557
- if (provider === 'anthropic') defaultModel = 'claude-3-haiku-20240307';
558
-
559
- // Custom endpoint always asks for base URL first
560
- let baseUrl: string | undefined;
561
- if (provider === 'custom') {
562
- const url = await p.text({
563
- message: 'Base URL (OpenAI-compatible):',
564
- placeholder: 'http://localhost:11434/v1',
565
- });
566
- if (p.isCancel(url)) { continue; }
567
- if (url) baseUrl = url;
568
- }
569
-
570
- const customModel = await p.text({
571
- message: 'Model name:',
572
- placeholder: defaultModel,
573
- defaultValue: defaultModel,
574
- });
575
-
576
- if (p.isCancel(customModel)) { continue; }
577
-
578
- config.llm = {
579
- provider: provider === 'custom' ? 'openai' : provider,
580
- apiKey,
581
- model: customModel || defaultModel,
582
- baseUrl,
583
- };
584
-
585
- await saveConfig(config);
586
- p.log.success(`LLM configured: ${config.llm.model} @ ${config.llm.baseUrl || 'default'}`);
587
- p.log.info('Saved to config.json. Restart MCP server to apply.');
588
- continue;
589
- }
590
-
591
- if (section === 'embedding') {
592
- const embedding = await p.select({
593
- message: 'Select embedding provider:',
594
- options: [
595
- { value: 'off', label: 'Off (default)', hint: 'BM25 fulltext only, ~50MB RAM' },
596
- { value: 'api', label: 'API (recommended)', hint: 'OpenAI-compatible, zero local RAM, best quality' },
597
- { value: 'fastembed', label: 'FastEmbed', hint: 'local ONNX, ~300MB RAM' },
598
- { value: 'transformers', label: 'Transformers', hint: 'local JS/WASM, ~500MB RAM' },
599
- ],
600
- });
601
-
602
- if (p.isCancel(embedding)) continue; // Back to configure menu
603
-
604
- if (embedding === 'api') {
605
- const apiKey = await p.password({
606
- message: 'Embedding API key (leave empty to use BM25 fallback):',
607
- });
608
-
609
- if (p.isCancel(apiKey)) continue;
610
-
611
- const baseUrl = await p.text({
612
- message: 'Base URL:',
613
- placeholder: 'https://api.openai.com/v1',
614
- defaultValue: '',
615
- });
616
-
617
- if (p.isCancel(baseUrl)) continue;
618
-
619
- const modelChoice = await p.select({
620
- message: 'Embedding model:',
621
- options: [
622
- { value: 'text-embedding-3-small', label: 'OpenAI text-embedding-3-small', hint: '1536d, $0.02/1M tokens' },
623
- { value: 'text-embedding-3-large', label: 'OpenAI text-embedding-3-large', hint: '3072d, best quality' },
624
- { value: 'text-embedding-v3', label: 'Qwen text-embedding-v3', hint: '1024d, Chinese+English' },
625
- { value: 'text-embedding-v4', label: 'Qwen text-embedding-v4', hint: 'latest, Chinese+English' },
626
- { value: 'custom', label: 'Custom model', hint: 'enter model name' },
627
- ],
628
- });
629
-
630
- if (p.isCancel(modelChoice)) continue;
631
-
632
- let model: string = modelChoice;
633
- if (modelChoice === 'custom') {
634
- const customName = await p.text({
635
- message: 'Model name:',
636
- placeholder: 'e.g., BAAI/bge-m3',
637
- });
638
- if (p.isCancel(customName) || !customName) continue;
639
- model = customName;
640
- }
641
-
642
- const dimInput = await p.text({
643
- message: 'Dimension override (optional, press Enter to auto-detect):',
644
- placeholder: 'e.g., 512 for cost savings',
645
- defaultValue: '',
646
- });
647
-
648
- const dims = (!p.isCancel(dimInput) && dimInput) ? parseInt(dimInput, 10) : null;
649
-
650
- config.embedding = 'api';
651
- config.embeddingApi = {
652
- apiKey: apiKey || undefined,
653
- baseUrl: baseUrl || undefined,
654
- model,
655
- dimensions: (dims && !isNaN(dims)) ? dims : undefined,
656
- };
657
-
658
- await saveConfig(config);
659
- p.log.success(`API embedding configured: ${model}`);
660
- p.log.info('Saved to config.json. Restart MCP server to apply.');
661
- continue;
662
- }
663
-
664
- config.embedding = embedding;
665
- delete config.embeddingApi;
666
-
667
- await saveConfig(config);
668
-
669
- if (embedding === 'off') {
670
- p.log.success('Embedding disabled. Using BM25 fulltext search.');
671
- } else {
672
- p.log.success(`Embedding set to: ${embedding}`);
673
- p.log.info(`Install with: npm install -g ${embedding === 'fastembed' ? 'fastembed' : '@huggingface/transformers'}`);
674
- }
675
- p.log.info('Saved to config.json. Restart MCP server to apply.');
676
- continue;
677
- }
678
-
679
- if (section === 'behavior') {
680
- const current = config.behavior ?? {};
681
-
682
- const sessionInject = await p.select({
683
- message: `Session start injection (current: ${current.sessionInject ?? 'minimal'})`,
684
- options: [
685
- { value: 'full', label: 'Full', hint: 'inject top 5 memories on session start' },
686
- { value: 'minimal', label: 'Minimal (default)', hint: 'one-line hint only' },
687
- { value: 'silent', label: 'Silent', hint: 'no injection, rely on rules/AGENTS.md' },
688
- ],
689
- });
690
- if (p.isCancel(sessionInject)) continue;
691
-
692
- const syncAdvisory = await p.confirm({
693
- message: `Show sync advisory on first search? (current: ${current.syncAdvisory !== false ? 'yes' : 'no'})`,
694
- initialValue: current.syncAdvisory !== false,
695
- });
696
- if (p.isCancel(syncAdvisory)) continue;
697
-
698
- const autoCleanup = await p.confirm({
699
- message: `Auto-archive expired memories on startup? (current: ${current.autoCleanup !== false ? 'yes' : 'no'})`,
700
- initialValue: current.autoCleanup !== false,
701
- });
702
- if (p.isCancel(autoCleanup)) continue;
703
-
704
- const formationMode = await p.select({
705
- message: `Formation Pipeline mode (current: ${current.formationMode ?? 'active'})`,
706
- options: [
707
- { value: 'active', label: 'Active (default)', hint: 'Formation decides storage (new/merge/evolve/discard)' },
708
- { value: 'shadow', label: 'Shadow', hint: 'Formation observes only, old compact decides' },
709
- { value: 'fallback', label: 'Fallback', hint: 'Old compact decides (safe rollback)' },
710
- ],
711
- });
712
- if (p.isCancel(formationMode)) continue;
713
-
714
- config.behavior = {
715
- sessionInject,
716
- syncAdvisory,
717
- autoCleanup,
718
- formationMode,
719
- };
720
-
721
- await saveConfig(config);
722
- p.log.success('Behavior settings updated.');
723
- p.log.info('Saved to config.json. Restart MCP server to apply.');
724
- continue;
725
- }
726
- }
727
- }
728
59
 
729
60
  async function runAsk(question: string): Promise<void> {
730
61
  // Use a smooth dots-style spinner (same frames as TUI ink-spinner "dots")
@@ -794,140 +125,6 @@ async function runAsk(question: string): Promise<void> {
794
125
  }
795
126
  }
796
127
 
797
- async function runSearch(query: string): Promise<void> {
798
- const s = createSpinnerForCurrentOutput();
799
- s.start('Searching memories...');
800
- const perf = !!process.env.MEMORIX_PERF;
801
- const t0 = perf ? performance.now() : 0;
802
- const mark = (label: string) => { if (perf) { const now = performance.now(); process.stderr.write(`[perf] ${label}: ${(now - t0).toFixed(0)}ms\n`); } };
803
-
804
- try {
805
- const { searchObservations, getDb, hydrateIndex } = await import('../store/orama-store.js');
806
- const { filterReadableObservations } = await import('../memory/visibility.js');
807
- const { getProjectDataDir } = await import('../store/persistence.js');
808
- const { detectProject } = await import('../project/detector.js');
809
- const { initObservations } = await import('../memory/observations.js');
810
- const { initObservationStore } = await import('../store/obs-store.js');
811
- mark('imports');
812
-
813
- const project = detectProject(process.cwd());
814
- if (!project) { s.stop('No git repo'); p.log.error(NO_GIT_MSG); return; }
815
- const dataDir = await getProjectDataDir(project.id);
816
- mark('detectProject');
817
- await initObservationStore(dataDir);
818
- await initObservations(dataDir);
819
- mark('initObservations');
820
-
821
- // Parallel: getDb (embedding provider init) + store.loadAll (disk I/O)
822
- const { getObservationStore: getStore } = await import('../store/obs-store.js');
823
- const [, allObs] = await Promise.all([
824
- getDb(),
825
- getStore().loadAll() as Promise<any[]>,
826
- ]);
827
- mark(`getDb+loadObs(${allObs.length})`);
828
- await hydrateIndex(allObs);
829
- mark('hydrateIndex');
830
-
831
- const results = await searchObservations({ query, limit: 10, projectId: project.id, reader: { projectId: project.id } });
832
- mark('search');
833
- s.stop('Search complete');
834
-
835
- if (results.length === 0) {
836
- p.log.warn('No memories found matching your query.');
837
- return;
838
- }
839
-
840
- p.log.success(`Found ${results.length} memories:`);
841
- console.log('');
842
- for (const r of results) {
843
- console.log(` ${r.icon} #${r.id} ${r.title}`);
844
- console.log(` ${r.time} | ${r.tokens} tokens | score: ${(r.score ?? 0).toFixed(2)}`);
845
- console.log('');
846
- }
847
- } catch (err) {
848
- s.stop('Search failed');
849
- p.log.error(`Error: ${err instanceof Error ? err.message : err}`);
850
- }
851
- }
852
-
853
- async function runList(): Promise<void> {
854
- const s = createSpinnerForCurrentOutput();
855
- s.start('Loading recent memories...');
856
-
857
- try {
858
- const { getProjectDataDir } = await import('../store/persistence.js');
859
- const { detectProject } = await import('../project/detector.js');
860
- const { initObservationStore: initStore, getObservationStore: getStore } = await import('../store/obs-store.js');
861
- const { filterReadableObservations } = await import('../memory/visibility.js');
862
-
863
- const project = detectProject(process.cwd());
864
- if (!project) { s.stop('No git repo'); p.log.error(NO_GIT_MSG); return; }
865
- const dataDir = await getProjectDataDir(project.id);
866
- await initStore(dataDir);
867
- const observations = filterReadableObservations(
868
- await getStore().loadAll(),
869
- { projectId: project.id },
870
- );
871
-
872
- const active = observations.filter(o => (o.status ?? 'active') === 'active');
873
- const recent = active.slice(-10).reverse();
874
-
875
- s.stop(`Project: ${project.name} (${active.length} active memories)`);
876
-
877
- if (recent.length === 0) {
878
- p.log.warn('No memories found.');
879
- return;
880
- }
881
-
882
- console.log('');
883
- for (const o of recent) {
884
- const typeLabel = ({ gotcha: '[!]', decision: '[D]', 'problem-solution': '[S]', discovery: '[?]', 'how-it-works': '[H]', 'what-changed': '[C]' } as Record<string, string>)[o.type] ?? '[·]';
885
- console.log(` ${typeLabel} #${o.id} ${o.title?.slice(0, 60) ?? '(untitled)'}`);
886
- }
887
- console.log('');
888
- } catch (err) {
889
- s.stop('Failed to load memories');
890
- p.log.error(`Error: ${err instanceof Error ? err.message : err}`);
891
- }
892
- }
893
-
894
- async function runCommand(cmd: string, _args: string[] = []): Promise<void> {
895
- // Direct imports to ensure bundler includes them
896
- // Using 'as any' to bypass citty's strict type checking for manual invocation
897
- switch (cmd) {
898
- case 'dashboard': {
899
- const m = await import('./commands/dashboard.js');
900
- await m.default.run?.({ args: { _: [] }, rawArgs: [], cmd: m.default } as any);
901
- break;
902
- }
903
- case 'status': {
904
- const m = await import('./commands/status.js');
905
- await m.default.run?.({ args: { _: [] }, rawArgs: [], cmd: m.default } as any);
906
- break;
907
- }
908
- case 'cleanup': {
909
- const m = await import('./commands/cleanup.js');
910
- await m.default.run?.({ args: { _: [], dry: false, force: false }, rawArgs: [], cmd: m.default } as any);
911
- break;
912
- }
913
- case 'serve-http': {
914
- const m = await import('./commands/serve-http.js');
915
- await m.default.run?.({ args: { _: [], port: 3211 }, rawArgs: [], cmd: m.default } as any);
916
- break;
917
- }
918
- case 'sync': {
919
- const m = await import('./commands/sync.js');
920
- await m.default.run?.({ args: { _: [], dry: false }, rawArgs: [], cmd: m.default } as any);
921
- break;
922
- }
923
- case 'serve': {
924
- const m = await import('./commands/serve.js');
925
- await m.default.run?.({ args: { _: [] }, rawArgs: [], cmd: m.default } as any);
926
- break;
927
- }
928
- }
929
- }
930
-
931
128
  // ============================================================
932
129
  // Main command
933
130
  // ============================================================
@@ -1092,6 +289,7 @@ const main = defineCommand({
1092
289
  repair: () => import('./commands/repair.js').then(m => m.default),
1093
290
  dashboard: () => import('./commands/dashboard.js').then(m => m.default),
1094
291
  cleanup: () => import('./commands/cleanup.js').then(m => m.default),
292
+ purge: () => import('./commands/purge.js').then(m => m.default),
1095
293
  uninstall: () => import('./commands/uninstall.js').then(m => m.default),
1096
294
  orchestrate: () => import('./commands/orchestrate.js').then(m => m.default),
1097
295
  workbench: () => Promise.resolve(defineCommand({
@@ -1124,7 +322,7 @@ const main = defineCommand({
1124
322
  'receipt',
1125
323
  'serve', 'serve-http', 'status', 'sync',
1126
324
  'hook', 'hooks', 'ingest', 'media', 'git-hook', 'git-hook-uninstall',
1127
- 'background', 'bg', 'bs', 'doctor', 'repair', 'dashboard', 'cleanup', 'uninstall', 'orchestrate'];
325
+ 'background', 'bg', 'bs', 'doctor', 'repair', 'dashboard', 'cleanup', 'purge', 'uninstall', 'orchestrate'];
1128
326
  if (firstArg && knownSubs.includes(firstArg)) return;
1129
327
 
1130
328
  // No subcommand provided — enter memcode TUI (native coding agent)
@@ -1189,6 +387,7 @@ const main = defineCommand({
1189
387
  console.error(' dashboard Open standalone dashboard (read-mostly)');
1190
388
  console.error(' hooks Open legacy hook installer menu');
1191
389
  console.error(' cleanup Remove old memories');
390
+ console.error(' purge Retire all memories (project by default, --all for everything)');
1192
391
  console.error(' sync Rules/workspace sync plus interactive wizard');
1193
392
  console.error(' ingest Ingest commit, log, or image knowledge');
1194
393
  console.error('\nRun `memorix` in an interactive terminal for memcode TUI.');