memorix 1.6.1 → 1.6.3

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 (122) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/TEAM.md +86 -86
  3. package/dist/cli/index.js +2582 -1845
  4. package/dist/index.js +1355 -683
  5. package/dist/maintenance-runner.js +1621 -1096
  6. package/dist/media-audio-runner.js +1612 -1088
  7. package/dist/media-video-runner.js +1612 -1088
  8. package/dist/memcode-runtime/CHANGELOG.md +35 -0
  9. package/dist/sdk.js +2007 -1396
  10. package/dist/vector-backfill-runner.js +1610 -1086
  11. package/docs/DESIGN_DECISIONS.md +357 -357
  12. package/docs/PERFORMANCE.md +15 -0
  13. package/package.json +2 -1
  14. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  15. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  16. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  17. package/plugins/copilot/memorix/plugin.json +1 -1
  18. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  19. package/plugins/omp/memorix/package.json +1 -1
  20. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  21. package/plugins/pi/memorix/package.json +1 -1
  22. package/server.json +2 -2
  23. package/src/audit/index.ts +156 -156
  24. package/src/cli/commands/background.ts +51 -5
  25. package/src/cli/commands/formation.ts +48 -48
  26. package/src/cli/commands/git-hook-install.ts +111 -111
  27. package/src/cli/commands/handoff.ts +54 -54
  28. package/src/cli/commands/hooks-status.ts +63 -63
  29. package/src/cli/commands/ingest.ts +44 -44
  30. package/src/cli/commands/integrate-shared.ts +15 -15
  31. package/src/cli/commands/lock.ts +82 -82
  32. package/src/cli/commands/message.ts +104 -104
  33. package/src/cli/commands/operator-shared.ts +18 -11
  34. package/src/cli/commands/poll.ts +58 -58
  35. package/src/cli/commands/reasoning.ts +118 -118
  36. package/src/cli/commands/serve-http.ts +29 -7
  37. package/src/cli/commands/serve-shared.ts +49 -6
  38. package/src/cli/commands/serve.ts +4 -0
  39. package/src/cli/commands/skills.ts +114 -114
  40. package/src/cli/commands/task.ts +167 -167
  41. package/src/cli/commands/transfer.ts +47 -47
  42. package/src/cli/index.ts +406 -406
  43. package/src/cli/tui/ChatView.tsx +234 -234
  44. package/src/cli/tui/CommandBar.tsx +312 -312
  45. package/src/cli/tui/ContextRail.tsx +118 -118
  46. package/src/cli/tui/HeaderBar.tsx +72 -72
  47. package/src/cli/tui/LogoBanner.tsx +51 -51
  48. package/src/cli/tui/index.ts +41 -41
  49. package/src/cli/version.ts +7 -7
  50. package/src/cli/workbench.ts +1 -1
  51. package/src/dashboard/project-classification.ts +64 -64
  52. package/src/embedding/api-provider.ts +503 -27
  53. package/src/embedding/provider.ts +20 -3
  54. package/src/git/hooks-path.ts +85 -85
  55. package/src/hooks/lifecycle.ts +1 -0
  56. package/src/hooks/pattern-detector.ts +173 -173
  57. package/src/hooks/significance-filter.ts +250 -250
  58. package/src/llm/memory-manager.ts +328 -328
  59. package/src/llm/quality.ts +248 -248
  60. package/src/memory/attribution-guard.ts +249 -249
  61. package/src/memory/disclosure-policy.ts +135 -135
  62. package/src/memory/entity-extractor.ts +197 -197
  63. package/src/memory/formation/evaluate.ts +217 -217
  64. package/src/memory/freshness.ts +122 -122
  65. package/src/memory/graph.ts +197 -197
  66. package/src/memory/observations.ts +68 -18
  67. package/src/memory/refs.ts +94 -94
  68. package/src/memory/secret-filter.ts +79 -79
  69. package/src/memory/session.ts +13 -3
  70. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  71. package/src/orchestrate/adapters/claude.ts +111 -111
  72. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  73. package/src/orchestrate/adapters/codex.ts +41 -41
  74. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  75. package/src/orchestrate/adapters/gemini.ts +42 -42
  76. package/src/orchestrate/adapters/index.ts +73 -73
  77. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  78. package/src/orchestrate/adapters/opencode.ts +47 -47
  79. package/src/orchestrate/adapters/types.ts +77 -77
  80. package/src/orchestrate/capability-router.ts +284 -284
  81. package/src/orchestrate/cost-tracker.ts +219 -219
  82. package/src/orchestrate/error-recovery.ts +191 -191
  83. package/src/orchestrate/evidence.ts +140 -140
  84. package/src/orchestrate/ledger.ts +110 -110
  85. package/src/orchestrate/memorix-bridge.ts +343 -343
  86. package/src/orchestrate/output-budget.ts +80 -80
  87. package/src/orchestrate/permission.ts +152 -152
  88. package/src/orchestrate/pipeline-trace.ts +131 -131
  89. package/src/orchestrate/prompt-builder.ts +155 -155
  90. package/src/orchestrate/ring-buffer.ts +37 -37
  91. package/src/orchestrate/task-graph.ts +389 -389
  92. package/src/project/aliases.ts +374 -374
  93. package/src/project/launch-root.ts +115 -0
  94. package/src/rules/adapters/trae.ts +56 -56
  95. package/src/rules/syncer.ts +235 -235
  96. package/src/runtime/isolated-maintenance.ts +12 -1
  97. package/src/sdk.ts +12 -8
  98. package/src/search/query-expansion.ts +52 -52
  99. package/src/server/formation-timeout.ts +27 -27
  100. package/src/server.ts +38 -5
  101. package/src/skills/mini-skills.ts +386 -386
  102. package/src/store/chat-store.ts +119 -119
  103. package/src/store/graph-store.ts +249 -249
  104. package/src/store/mini-skill-store.ts +349 -349
  105. package/src/store/obs-store.ts +2 -2
  106. package/src/store/orama-store.ts +21 -3
  107. package/src/store/persistence-json.ts +212 -212
  108. package/src/store/persistence.ts +291 -291
  109. package/src/store/project-affinity.ts +195 -195
  110. package/src/store/sqlite-db.ts +2 -0
  111. package/src/store/sqlite-store.ts +19 -4
  112. package/src/team/event-bus.ts +76 -76
  113. package/src/team/handoff.ts +161 -161
  114. package/src/team/poll.ts +132 -132
  115. package/src/workspace/mcp-adapters/codex.ts +191 -191
  116. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  117. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  118. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  119. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  120. package/src/workspace/mcp-adapters/trae.ts +134 -134
  121. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  122. package/src/workspace/sanitizer.ts +60 -60
package/src/cli/index.ts CHANGED
@@ -1,406 +1,406 @@
1
- /**
2
- * Memorix CLI
3
- *
4
- * Command-line interface for Memorix management.
5
- * Built with: citty (1.1K stars, zero-deps) + @clack/prompts (7.4K stars)
6
- *
7
- * Commands:
8
- * memorix — Enter memcode TUI (native coding agent)
9
- * memorix memcode — Enter memcode TUI (explicit)
10
- * memorix ask "q" — Single-shot chat question (pipe: echo "q" | memorix ask)
11
- * memorix serve — Start MCP Server on stdio
12
- * memorix status — Show project info + rules sync status
13
- * memorix sync — Interactive cross-agent rule sync
14
- */
15
-
16
- import { defineCommand, runMain } from 'citty';
17
- import * as p from '@clack/prompts';
18
- import { getCliVersion } from './version.js';
19
- import { importBundledMemcode } from './memcode-bootstrap.js';
20
- import { installCliPipeErrorGuard } from './pipe-errors.js';
21
- import { normalizeCliInvocation } from './invocation.js';
22
- import { printCliGuideForHelp, renderCliGuide } from './command-guide.js';
23
-
24
- installCliPipeErrorGuard();
25
-
26
- const NO_GIT_MSG = 'Memorix requires a git repo to establish project identity. Run `git init` in this workspace first.';
27
-
28
- /**
29
- * Set PI_PACKAGE_DIR so bundled memcode can find its theme files.
30
- * When tsup bundles memcode into the CLI, __dirname points to dist/cli/,
31
- * not packages/memcode/. This env var tells config.ts where to look.
32
- *
33
- * In dev: resolves to packages/memcode/
34
- * In global npm install: packages/memcode/ won't exist; theme files
35
- * should be copied to dist/memcode/ by the build (see tsup onSuccess).
36
- */
37
- function ensureMemcodePackageDir(): void {
38
- if (process.env.MEMCODE_PACKAGE_DIR) return;
39
- // Walk up from __dirname (dist/cli/) to find packages/memcode/package.json
40
- const path = require('node:path') as typeof import('node:path');
41
- const fs = require('node:fs') as typeof import('node:fs');
42
- let dir: string = __dirname;
43
- for (let i = 0; i < 5; i++) {
44
- const candidate = path.join(dir, 'packages', 'memcode');
45
- if (fs.existsSync(path.join(candidate, 'package.json'))) {
46
- process.env.MEMCODE_PACKAGE_DIR = candidate;
47
- return;
48
- }
49
- const parent = path.dirname(dir);
50
- if (parent === dir) break;
51
- dir = parent;
52
- }
53
- }
54
-
55
- const DIM = '\x1b[2m';
56
- const RESET = '\x1b[0m';
57
- const CYAN = '\x1b[36m';
58
- const YELLOW = '\x1b[33m';
59
-
60
- async function runAsk(question: string): Promise<void> {
61
- // Use a smooth dots-style spinner (same frames as TUI ink-spinner "dots")
62
- // instead of @clack's ASCII spinner which flickers in non-Ink terminals.
63
- const dotsFrames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
64
- let frameIdx = 0;
65
- let interval: ReturnType<typeof setInterval> | null = null;
66
-
67
- const startSmoothSpinner = (msg: string) => {
68
- if (process.stdout.isTTY) {
69
- process.stdout.write(`${CYAN}${dotsFrames[0]}${RESET} ${msg}`);
70
- interval = setInterval(() => {
71
- frameIdx = (frameIdx + 1) % dotsFrames.length;
72
- process.stdout.write(`\r${CYAN}${dotsFrames[frameIdx]}${RESET} ${msg}`);
73
- }, 80);
74
- } else {
75
- process.stderr.write(`${msg}...\n`);
76
- }
77
- };
78
-
79
- const stopSmoothSpinner = () => {
80
- if (interval) { clearInterval(interval); interval = null; }
81
- if (process.stdout.isTTY) {
82
- process.stdout.write('\r' + ' '.repeat(40) + '\r'); // clear line
83
- }
84
- };
85
-
86
- startSmoothSpinner('Thinking…');
87
-
88
- try {
89
- const { askMemoryQuestion } = await import('./tui/chat-service.js');
90
- const result = await askMemoryQuestion(question);
91
-
92
- stopSmoothSpinner();
93
-
94
- // Output the answer
95
- console.log('');
96
- console.log(result.answer);
97
-
98
- // Show sources if any
99
- if (result.sources.length > 0) {
100
- console.log('');
101
- console.log(`${DIM}Sources:${RESET}`);
102
- for (const src of result.sources.slice(0, 5)) {
103
- console.log(` ${DIM}[obs:${src.id}]${RESET} ${src.title}`);
104
- }
105
- }
106
-
107
- // Show warnings
108
- if (result.warning) {
109
- console.log('');
110
- console.log(`${YELLOW}[WARN] ${result.warning}${RESET}`);
111
- }
112
-
113
- // Metadata footer
114
- const meta: string[] = [];
115
- if (result.usedLLM && result.llmModel) meta.push(result.llmModel);
116
- if (result.searchMode) meta.push(result.searchMode);
117
- if (result.toolCallsCount) meta.push(`${result.toolCallsCount} tool call${result.toolCallsCount > 1 ? 's' : ''}`);
118
- if (meta.length > 0) {
119
- console.log(`${DIM} ${meta.join(' · ')}${RESET}`);
120
- }
121
- } catch (err) {
122
- stopSmoothSpinner();
123
- p.log.error(`Error: ${err instanceof Error ? err.message : err}`);
124
- process.exitCode = 1;
125
- }
126
- }
127
-
128
- // ============================================================
129
- // Main command
130
- // ============================================================
131
-
132
- async function runMemoryShortcut(action: string, args: Record<string, unknown>): Promise<void> {
133
- const { detectProject } = await import('../project/detector.js');
134
- if (!detectProject(process.cwd())) {
135
- console.log(NO_GIT_MSG);
136
- process.exitCode = 1;
137
- return;
138
- }
139
- const memory = await import('./commands/memory.js');
140
- await memory.default.run?.({
141
- args: { ...args, _: [action] },
142
- rawArgs: [],
143
- cmd: memory.default,
144
- } as any);
145
- }
146
-
147
- const main = defineCommand({
148
- meta: {
149
- name: 'memorix',
150
- version: getCliVersion(),
151
- description: 'Local-first memory control plane for AI coding agents through CLI, MCP, and local workflows',
152
- },
153
- subCommands: {
154
- // One-shot product commands (primary user paths)
155
- ask: () => Promise.resolve(defineCommand({
156
- meta: { name: 'ask', description: 'Ask Memorix a question (single-shot chat). Pipe: echo "q" | memorix ask' },
157
- args: {
158
- question: { type: 'positional', description: 'Question to ask (or pipe via stdin)', required: false },
159
- },
160
- async run({ args }) {
161
- let q = (args.question as string) || '';
162
- // Read from stdin if no positional arg and stdin is piped
163
- if (!q && !process.stdin.isTTY) {
164
- q = await new Promise<string>((resolve) => {
165
- let data = '';
166
- process.stdin.setEncoding('utf-8');
167
- process.stdin.on('data', (chunk) => { data += chunk; });
168
- process.stdin.on('end', () => resolve(data.trim()));
169
- process.stdin.on('error', () => resolve(''));
170
- });
171
- }
172
- if (!q) {
173
- p.log.error('No question provided. Usage: memorix ask "your question" or echo "q" | memorix ask');
174
- return;
175
- }
176
- await runAsk(q);
177
- },
178
- })),
179
- search: () => Promise.resolve(defineCommand({
180
- meta: { name: 'search', description: 'Shortcut for `memorix memory search`' },
181
- args: {
182
- query: { type: 'positional', description: 'Search query', required: true },
183
- limit: { type: 'string', description: 'Maximum results' },
184
- quality: { type: 'string', description: 'Retrieval profile: fast, balanced (default), or thorough' },
185
- json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
186
- },
187
- async run({ args }) {
188
- await runMemoryShortcut('search', {
189
- query: args.query,
190
- limit: args.limit,
191
- quality: args.quality,
192
- json: args.json,
193
- });
194
- },
195
- })),
196
- remember: () => Promise.resolve(defineCommand({
197
- meta: { name: 'remember', description: 'Shortcut for `memorix memory store`' },
198
- args: {
199
- text: { type: 'positional', description: 'Text to remember', required: true },
200
- title: { type: 'string', description: 'Optional observation title' },
201
- type: { type: 'string', description: 'Observation type' },
202
- visibility: { type: 'string', description: 'project (default), personal, or team' },
203
- json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
204
- },
205
- async run({ args }) {
206
- await runMemoryShortcut('store', {
207
- text: args.text,
208
- title: args.title,
209
- type: args.type,
210
- visibility: args.visibility,
211
- json: args.json,
212
- });
213
- },
214
- })),
215
- recent: () => Promise.resolve(defineCommand({
216
- meta: { name: 'recent', description: 'Shortcut for `memorix memory recent`' },
217
- args: {
218
- limit: { type: 'string', description: 'Maximum results' },
219
- json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
220
- },
221
- async run({ args }) { await runMemoryShortcut('recent', { limit: args.limit, json: args.json }); },
222
- })),
223
- help: () => Promise.resolve(defineCommand({
224
- meta: { name: 'help', description: 'Show action-oriented help for a command group' },
225
- args: {
226
- command: { type: 'positional', description: 'Command group to inspect', required: false },
227
- },
228
- async run({ args }) {
229
- console.log(renderCliGuide(args.command as string | undefined));
230
- },
231
- })),
232
- // Infrastructure commands
233
- init: () => import('./commands/init.js').then(m => m.default),
234
- setup: () => import('./commands/setup.js').then(m => m.default),
235
- config: () => Promise.resolve(defineCommand({
236
- meta: { name: 'config', description: 'Inspect Memorix TOML configuration' },
237
- subCommands: {
238
- path: () => import('./commands/config-path.js').then(m => m.default),
239
- get: () => import('./commands/config-get.js').then(m => m.default),
240
- migrate: () => import('./commands/config-migrate.js').then(m => m.default),
241
- },
242
- })),
243
- integrate: () => import('./commands/integrate.js').then(m => m.default),
244
- memory: () => import('./commands/memory.js').then(m => m.default),
245
- context: () => import('./commands/context.js').then(m => m.default),
246
- resume: () => import('./commands/resume.js').then(m => m.default),
247
- explain: () => import('./commands/explain.js').then(m => m.default),
248
- codegraph: () => import('./commands/codegraph.js').then(m => m.default),
249
- checkpoint: () => import('./commands/checkpoint.js').then(m => m.default),
250
- knowledge: () => import('./commands/knowledge.js').then(m => m.default),
251
- reasoning: () => import('./commands/reasoning.js').then(m => m.default),
252
- retention: () => import('./commands/retention.js').then(m => m.default),
253
- formation: () => import('./commands/formation.js').then(m => m.default),
254
- audit: () => import('./commands/audit.js').then(m => m.default),
255
- transfer: () => import('./commands/transfer.js').then(m => m.default),
256
- skills: () => import('./commands/skills.js').then(m => m.default),
257
- identity: () => import('./commands/identity.js').then(m => m.default),
258
- session: () => import('./commands/session.js').then(m => m.default),
259
- team: () => import('./commands/team.js').then(m => m.default),
260
- task: () => import('./commands/task.js').then(m => m.default),
261
- message: () => import('./commands/message.js').then(m => m.default),
262
- lock: () => import('./commands/lock.js').then(m => m.default),
263
- handoff: () => import('./commands/handoff.js').then(m => m.default),
264
- poll: () => import('./commands/poll.js').then(m => m.default),
265
- receipt: () => import('./commands/receipt.js').then(m => m.default),
266
- serve: () => import('./commands/serve.js').then(m => m.default),
267
- 'serve-http': () => import('./commands/serve-http.js').then(m => m.default),
268
- status: () => import('./commands/status.js').then(m => m.default),
269
- sync: () => import('./commands/sync.js').then(m => m.default),
270
- hook: () => import('./commands/hook.js').then(m => m.default),
271
- hooks: () => import('./commands/hooks.js').then(m => m.default),
272
- ingest: () => import('./commands/ingest.js').then(m => m.default),
273
- media: () => import('./commands/media.js').then(m => m.default),
274
- 'git-hook': () => import('./commands/git-hook-install.js').then(m => m.default),
275
- 'git-hook-uninstall': () => import('./commands/git-hook-uninstall.js').then(m => m.default),
276
- background: () => import('./commands/background.js').then(m => m.default),
277
- bg: () => import('./commands/background.js').then(m => m.default),
278
- bs: () => Promise.resolve(defineCommand({
279
- meta: { name: 'bs', description: 'Shortcut: background start' },
280
- args: { port: { type: 'string', description: 'HTTP port (default: 3211)', required: false } },
281
- async run({ args }) {
282
- // Directly invoke background start instead of going through citty's CommandContext
283
- const port = parseInt((args.port as string) || '3211', 10);
284
- const { doStart } = await import('./commands/background.js');
285
- await doStart(port);
286
- },
287
- })),
288
- doctor: () => import('./commands/doctor.js').then(m => m.default),
289
- repair: () => import('./commands/repair.js').then(m => m.default),
290
- dashboard: () => import('./commands/dashboard.js').then(m => m.default),
291
- cleanup: () => import('./commands/cleanup.js').then(m => m.default),
292
- purge: () => import('./commands/purge.js').then(m => m.default),
293
- uninstall: () => import('./commands/uninstall.js').then(m => m.default),
294
- orchestrate: () => import('./commands/orchestrate.js').then(m => m.default),
295
- workbench: () => Promise.resolve(defineCommand({
296
- meta: { name: 'workbench', description: 'Open the interactive terminal memory control plane' },
297
- async run() {
298
- const { startWorkbench } = await import('./workbench.js');
299
- await startWorkbench();
300
- },
301
- })),
302
- memcode: () => Promise.resolve(defineCommand({
303
- meta: { name: 'memcode', description: 'Enter memcode TUI — native coding agent with memory' },
304
- async run() {
305
- try {
306
- const { runCli } = await importBundledMemcode();
307
- await runCli(process.argv.slice(3));
308
- } catch (err) {
309
- console.error('Failed to start memcode:', err instanceof Error ? err.message : err);
310
- process.exit(1);
311
- }
312
- },
313
- })),
314
- },
315
- async run() {
316
- // Guard: if citty already resolved a subcommand, its run() was called before this.
317
- // Detect by checking if the first CLI arg matches a registered subcommand name.
318
- const firstArg = process.argv[2];
319
- const knownSubs = ['ask', 'search', 'remember', 'recent', 'help', 'workbench', 'memcode', 'config',
320
- 'init', 'setup', 'integrate', 'memory', 'context', 'resume', 'explain', 'codegraph', 'checkpoint', 'knowledge', 'reasoning', 'retention', 'formation', 'audit', 'transfer', 'skills', 'identity',
321
- 'session', 'team', 'task', 'message', 'lock', 'handoff', 'poll',
322
- 'receipt',
323
- 'serve', 'serve-http', 'status', 'sync',
324
- 'hook', 'hooks', 'ingest', 'media', 'git-hook', 'git-hook-uninstall',
325
- 'background', 'bg', 'bs', 'doctor', 'repair', 'dashboard', 'cleanup', 'purge', 'uninstall', 'orchestrate'];
326
- if (firstArg && knownSubs.includes(firstArg)) return;
327
-
328
- // No subcommand provided — enter memcode TUI (native coding agent)
329
- if (!firstArg) {
330
- try {
331
- const { runCli } = await importBundledMemcode();
332
- await runCli(process.argv.slice(2));
333
- return;
334
- } catch (err) {
335
- console.error('Failed to start memcode:', err instanceof Error ? err.message : err);
336
- process.exit(1);
337
- }
338
- }
339
-
340
- // Fallback: show usage hint
341
- if (process.stdout.isTTY && process.stdin.isTTY) {
342
- // Fire-and-forget: background update check. Default is notify-only; stderr only, never blocks TUI.
343
- import('./update-checker.js').then(m => m.checkForUpdates()).catch(() => {});
344
- const { startWorkbench } = await import('./workbench.js');
345
- await startWorkbench();
346
- } else {
347
- // Non-interactive mode: show usage hint
348
- console.error(`Memorix v${getCliVersion()} — Local-first memory control plane\n`);
349
- console.error('Usage: memorix <command>\n');
350
- console.error('Commands:');
351
- console.error(' help Show action-oriented help (`memorix memory --help`)');
352
- console.error(' workbench Open interactive terminal memory control plane');
353
- console.error(' memcode Enter memcode TUI (native coding agent)');
354
- console.error(' ask "q" Ask Memorix a question (single-shot chat)');
355
- console.error(' Pipe: echo "q" | memorix ask');
356
- console.error(' background Start/stop/status background control plane');
357
- console.error(' session Start/end/context for coding sessions');
358
- console.error(' memory Search/store/detail/timeline/resolve observations');
359
- console.error(' context Show the Memory Autopilot brief for this project');
360
- console.error(' resume Resume prior work with one bounded project brief');
361
- console.error(' explain Explain where Memorix project context comes from');
362
- console.error(' codegraph Refresh/status/context-pack for CodeGraph Memory');
363
- console.error(' checkpoint Inspect native compact continuity checkpoints');
364
- console.error(' knowledge Review source-backed knowledge pages and project workflows');
365
- console.error(' reasoning Store/search decision rationale');
366
- console.error(' retention Inspect stale/archive status');
367
- console.error(' formation Inspect Memory Formation metrics');
368
- console.error(' audit Audit trail and project attribution checks');
369
- console.error(' transfer Export/import memory snapshots');
370
- console.error(' skills List/generate/show project skills');
371
- console.error(' identity Select the explicit CLI actor for private/team memory');
372
- console.error(' team Join/status/role operations for coordination state');
373
- console.error(' task Create/claim/complete/list team tasks');
374
- console.error(' message Send/broadcast/read team messages');
375
- console.error(' lock Manage advisory file locks');
376
- console.error(' handoff Create durable handoff artifacts');
377
- console.error(' poll Snapshot project coordination state');
378
- console.error(' receipt Privacy-safe memory handoff diagnostic');
379
- console.error(' serve-http Start HTTP MCP + dashboard control plane');
380
- console.error(' serve Start MCP server on stdio');
381
- console.error(' init Create global defaults or project config');
382
- console.error(' setup Install Memorix plugin/MCP/rules/hooks for an agent');
383
- console.error(' repair Repair Memorix-owned agent integration files');
384
- console.error(' config Show TOML config paths and resolved values');
385
- console.error(' integrate Install one IDE integration into the current repo');
386
- console.error(' status Show project info + stats');
387
- console.error(' dashboard Open standalone dashboard (read-mostly)');
388
- console.error(' hooks Open legacy hook installer menu');
389
- console.error(' cleanup Remove old memories');
390
- console.error(' purge Retire all memories (project by default, --all for everything)');
391
- console.error(' sync Rules/workspace sync plus interactive wizard');
392
- console.error(' ingest Ingest commit, log, or image knowledge');
393
- console.error('\nRun `memorix` in an interactive terminal for memcode TUI.');
394
- }
395
- },
396
- });
397
-
398
- try {
399
- normalizeCliInvocation();
400
- if (!printCliGuideForHelp()) {
401
- runMain(main);
402
- }
403
- } catch (error) {
404
- console.error(`Memorix CLI invocation error: ${error instanceof Error ? error.message : String(error)}`);
405
- process.exitCode = 1;
406
- }
1
+ /**
2
+ * Memorix CLI
3
+ *
4
+ * Command-line interface for Memorix management.
5
+ * Built with: citty (1.1K stars, zero-deps) + @clack/prompts (7.4K stars)
6
+ *
7
+ * Commands:
8
+ * memorix — Enter memcode TUI (native coding agent)
9
+ * memorix memcode — Enter memcode TUI (explicit)
10
+ * memorix ask "q" — Single-shot chat question (pipe: echo "q" | memorix ask)
11
+ * memorix serve — Start MCP Server on stdio
12
+ * memorix status — Show project info + rules sync status
13
+ * memorix sync — Interactive cross-agent rule sync
14
+ */
15
+
16
+ import { defineCommand, runMain } from 'citty';
17
+ import * as p from '@clack/prompts';
18
+ import { getCliVersion } from './version.js';
19
+ import { importBundledMemcode } from './memcode-bootstrap.js';
20
+ import { installCliPipeErrorGuard } from './pipe-errors.js';
21
+ import { normalizeCliInvocation } from './invocation.js';
22
+ import { printCliGuideForHelp, renderCliGuide } from './command-guide.js';
23
+
24
+ installCliPipeErrorGuard();
25
+
26
+ const NO_GIT_MSG = 'Memorix requires a git repo to establish project identity. Run `git init` in this workspace first.';
27
+
28
+ /**
29
+ * Set PI_PACKAGE_DIR so bundled memcode can find its theme files.
30
+ * When tsup bundles memcode into the CLI, __dirname points to dist/cli/,
31
+ * not packages/memcode/. This env var tells config.ts where to look.
32
+ *
33
+ * In dev: resolves to packages/memcode/
34
+ * In global npm install: packages/memcode/ won't exist; theme files
35
+ * should be copied to dist/memcode/ by the build (see tsup onSuccess).
36
+ */
37
+ function ensureMemcodePackageDir(): void {
38
+ if (process.env.MEMCODE_PACKAGE_DIR) return;
39
+ // Walk up from __dirname (dist/cli/) to find packages/memcode/package.json
40
+ const path = require('node:path') as typeof import('node:path');
41
+ const fs = require('node:fs') as typeof import('node:fs');
42
+ let dir: string = __dirname;
43
+ for (let i = 0; i < 5; i++) {
44
+ const candidate = path.join(dir, 'packages', 'memcode');
45
+ if (fs.existsSync(path.join(candidate, 'package.json'))) {
46
+ process.env.MEMCODE_PACKAGE_DIR = candidate;
47
+ return;
48
+ }
49
+ const parent = path.dirname(dir);
50
+ if (parent === dir) break;
51
+ dir = parent;
52
+ }
53
+ }
54
+
55
+ const DIM = '\x1b[2m';
56
+ const RESET = '\x1b[0m';
57
+ const CYAN = '\x1b[36m';
58
+ const YELLOW = '\x1b[33m';
59
+
60
+ async function runAsk(question: string): Promise<void> {
61
+ // Use a smooth dots-style spinner (same frames as TUI ink-spinner "dots")
62
+ // instead of @clack's ASCII spinner which flickers in non-Ink terminals.
63
+ const dotsFrames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
64
+ let frameIdx = 0;
65
+ let interval: ReturnType<typeof setInterval> | null = null;
66
+
67
+ const startSmoothSpinner = (msg: string) => {
68
+ if (process.stdout.isTTY) {
69
+ process.stdout.write(`${CYAN}${dotsFrames[0]}${RESET} ${msg}`);
70
+ interval = setInterval(() => {
71
+ frameIdx = (frameIdx + 1) % dotsFrames.length;
72
+ process.stdout.write(`\r${CYAN}${dotsFrames[frameIdx]}${RESET} ${msg}`);
73
+ }, 80);
74
+ } else {
75
+ process.stderr.write(`${msg}...\n`);
76
+ }
77
+ };
78
+
79
+ const stopSmoothSpinner = () => {
80
+ if (interval) { clearInterval(interval); interval = null; }
81
+ if (process.stdout.isTTY) {
82
+ process.stdout.write('\r' + ' '.repeat(40) + '\r'); // clear line
83
+ }
84
+ };
85
+
86
+ startSmoothSpinner('Thinking…');
87
+
88
+ try {
89
+ const { askMemoryQuestion } = await import('./tui/chat-service.js');
90
+ const result = await askMemoryQuestion(question);
91
+
92
+ stopSmoothSpinner();
93
+
94
+ // Output the answer
95
+ console.log('');
96
+ console.log(result.answer);
97
+
98
+ // Show sources if any
99
+ if (result.sources.length > 0) {
100
+ console.log('');
101
+ console.log(`${DIM}Sources:${RESET}`);
102
+ for (const src of result.sources.slice(0, 5)) {
103
+ console.log(` ${DIM}[obs:${src.id}]${RESET} ${src.title}`);
104
+ }
105
+ }
106
+
107
+ // Show warnings
108
+ if (result.warning) {
109
+ console.log('');
110
+ console.log(`${YELLOW}[WARN] ${result.warning}${RESET}`);
111
+ }
112
+
113
+ // Metadata footer
114
+ const meta: string[] = [];
115
+ if (result.usedLLM && result.llmModel) meta.push(result.llmModel);
116
+ if (result.searchMode) meta.push(result.searchMode);
117
+ if (result.toolCallsCount) meta.push(`${result.toolCallsCount} tool call${result.toolCallsCount > 1 ? 's' : ''}`);
118
+ if (meta.length > 0) {
119
+ console.log(`${DIM} ${meta.join(' · ')}${RESET}`);
120
+ }
121
+ } catch (err) {
122
+ stopSmoothSpinner();
123
+ p.log.error(`Error: ${err instanceof Error ? err.message : err}`);
124
+ process.exitCode = 1;
125
+ }
126
+ }
127
+
128
+ // ============================================================
129
+ // Main command
130
+ // ============================================================
131
+
132
+ async function runMemoryShortcut(action: string, args: Record<string, unknown>): Promise<void> {
133
+ const { detectProject } = await import('../project/detector.js');
134
+ if (!detectProject(process.cwd())) {
135
+ console.log(NO_GIT_MSG);
136
+ process.exitCode = 1;
137
+ return;
138
+ }
139
+ const memory = await import('./commands/memory.js');
140
+ await memory.default.run?.({
141
+ args: { ...args, _: [action] },
142
+ rawArgs: [],
143
+ cmd: memory.default,
144
+ } as any);
145
+ }
146
+
147
+ const main = defineCommand({
148
+ meta: {
149
+ name: 'memorix',
150
+ version: getCliVersion(),
151
+ description: 'Local-first memory control plane for AI coding agents through CLI, MCP, and local workflows',
152
+ },
153
+ subCommands: {
154
+ // One-shot product commands (primary user paths)
155
+ ask: () => Promise.resolve(defineCommand({
156
+ meta: { name: 'ask', description: 'Ask Memorix a question (single-shot chat). Pipe: echo "q" | memorix ask' },
157
+ args: {
158
+ question: { type: 'positional', description: 'Question to ask (or pipe via stdin)', required: false },
159
+ },
160
+ async run({ args }) {
161
+ let q = (args.question as string) || '';
162
+ // Read from stdin if no positional arg and stdin is piped
163
+ if (!q && !process.stdin.isTTY) {
164
+ q = await new Promise<string>((resolve) => {
165
+ let data = '';
166
+ process.stdin.setEncoding('utf-8');
167
+ process.stdin.on('data', (chunk) => { data += chunk; });
168
+ process.stdin.on('end', () => resolve(data.trim()));
169
+ process.stdin.on('error', () => resolve(''));
170
+ });
171
+ }
172
+ if (!q) {
173
+ p.log.error('No question provided. Usage: memorix ask "your question" or echo "q" | memorix ask');
174
+ return;
175
+ }
176
+ await runAsk(q);
177
+ },
178
+ })),
179
+ search: () => Promise.resolve(defineCommand({
180
+ meta: { name: 'search', description: 'Shortcut for `memorix memory search`' },
181
+ args: {
182
+ query: { type: 'positional', description: 'Search query', required: true },
183
+ limit: { type: 'string', description: 'Maximum results' },
184
+ quality: { type: 'string', description: 'Retrieval profile: fast, balanced (default), or thorough' },
185
+ json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
186
+ },
187
+ async run({ args }) {
188
+ await runMemoryShortcut('search', {
189
+ query: args.query,
190
+ limit: args.limit,
191
+ quality: args.quality,
192
+ json: args.json,
193
+ });
194
+ },
195
+ })),
196
+ remember: () => Promise.resolve(defineCommand({
197
+ meta: { name: 'remember', description: 'Shortcut for `memorix memory store`' },
198
+ args: {
199
+ text: { type: 'positional', description: 'Text to remember', required: true },
200
+ title: { type: 'string', description: 'Optional observation title' },
201
+ type: { type: 'string', description: 'Observation type' },
202
+ visibility: { type: 'string', description: 'project (default), personal, or team' },
203
+ json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
204
+ },
205
+ async run({ args }) {
206
+ await runMemoryShortcut('store', {
207
+ text: args.text,
208
+ title: args.title,
209
+ type: args.type,
210
+ visibility: args.visibility,
211
+ json: args.json,
212
+ });
213
+ },
214
+ })),
215
+ recent: () => Promise.resolve(defineCommand({
216
+ meta: { name: 'recent', description: 'Shortcut for `memorix memory recent`' },
217
+ args: {
218
+ limit: { type: 'string', description: 'Maximum results' },
219
+ json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
220
+ },
221
+ async run({ args }) { await runMemoryShortcut('recent', { limit: args.limit, json: args.json }); },
222
+ })),
223
+ help: () => Promise.resolve(defineCommand({
224
+ meta: { name: 'help', description: 'Show action-oriented help for a command group' },
225
+ args: {
226
+ command: { type: 'positional', description: 'Command group to inspect', required: false },
227
+ },
228
+ async run({ args }) {
229
+ console.log(renderCliGuide(args.command as string | undefined));
230
+ },
231
+ })),
232
+ // Infrastructure commands
233
+ init: () => import('./commands/init.js').then(m => m.default),
234
+ setup: () => import('./commands/setup.js').then(m => m.default),
235
+ config: () => Promise.resolve(defineCommand({
236
+ meta: { name: 'config', description: 'Inspect Memorix TOML configuration' },
237
+ subCommands: {
238
+ path: () => import('./commands/config-path.js').then(m => m.default),
239
+ get: () => import('./commands/config-get.js').then(m => m.default),
240
+ migrate: () => import('./commands/config-migrate.js').then(m => m.default),
241
+ },
242
+ })),
243
+ integrate: () => import('./commands/integrate.js').then(m => m.default),
244
+ memory: () => import('./commands/memory.js').then(m => m.default),
245
+ context: () => import('./commands/context.js').then(m => m.default),
246
+ resume: () => import('./commands/resume.js').then(m => m.default),
247
+ explain: () => import('./commands/explain.js').then(m => m.default),
248
+ codegraph: () => import('./commands/codegraph.js').then(m => m.default),
249
+ checkpoint: () => import('./commands/checkpoint.js').then(m => m.default),
250
+ knowledge: () => import('./commands/knowledge.js').then(m => m.default),
251
+ reasoning: () => import('./commands/reasoning.js').then(m => m.default),
252
+ retention: () => import('./commands/retention.js').then(m => m.default),
253
+ formation: () => import('./commands/formation.js').then(m => m.default),
254
+ audit: () => import('./commands/audit.js').then(m => m.default),
255
+ transfer: () => import('./commands/transfer.js').then(m => m.default),
256
+ skills: () => import('./commands/skills.js').then(m => m.default),
257
+ identity: () => import('./commands/identity.js').then(m => m.default),
258
+ session: () => import('./commands/session.js').then(m => m.default),
259
+ team: () => import('./commands/team.js').then(m => m.default),
260
+ task: () => import('./commands/task.js').then(m => m.default),
261
+ message: () => import('./commands/message.js').then(m => m.default),
262
+ lock: () => import('./commands/lock.js').then(m => m.default),
263
+ handoff: () => import('./commands/handoff.js').then(m => m.default),
264
+ poll: () => import('./commands/poll.js').then(m => m.default),
265
+ receipt: () => import('./commands/receipt.js').then(m => m.default),
266
+ serve: () => import('./commands/serve.js').then(m => m.default),
267
+ 'serve-http': () => import('./commands/serve-http.js').then(m => m.default),
268
+ status: () => import('./commands/status.js').then(m => m.default),
269
+ sync: () => import('./commands/sync.js').then(m => m.default),
270
+ hook: () => import('./commands/hook.js').then(m => m.default),
271
+ hooks: () => import('./commands/hooks.js').then(m => m.default),
272
+ ingest: () => import('./commands/ingest.js').then(m => m.default),
273
+ media: () => import('./commands/media.js').then(m => m.default),
274
+ 'git-hook': () => import('./commands/git-hook-install.js').then(m => m.default),
275
+ 'git-hook-uninstall': () => import('./commands/git-hook-uninstall.js').then(m => m.default),
276
+ background: () => import('./commands/background.js').then(m => m.default),
277
+ bg: () => import('./commands/background.js').then(m => m.default),
278
+ bs: () => Promise.resolve(defineCommand({
279
+ meta: { name: 'bs', description: 'Shortcut: background start' },
280
+ args: { port: { type: 'string', description: 'HTTP port (default: 3211)', required: false } },
281
+ async run({ args }) {
282
+ // Directly invoke background start instead of going through citty's CommandContext
283
+ const port = parseInt((args.port as string) || '3211', 10);
284
+ const { doStart } = await import('./commands/background.js');
285
+ await doStart(port);
286
+ },
287
+ })),
288
+ doctor: () => import('./commands/doctor.js').then(m => m.default),
289
+ repair: () => import('./commands/repair.js').then(m => m.default),
290
+ dashboard: () => import('./commands/dashboard.js').then(m => m.default),
291
+ cleanup: () => import('./commands/cleanup.js').then(m => m.default),
292
+ purge: () => import('./commands/purge.js').then(m => m.default),
293
+ uninstall: () => import('./commands/uninstall.js').then(m => m.default),
294
+ orchestrate: () => import('./commands/orchestrate.js').then(m => m.default),
295
+ workbench: () => Promise.resolve(defineCommand({
296
+ meta: { name: 'workbench', description: 'Open the interactive terminal memory control plane' },
297
+ async run() {
298
+ const { startWorkbench } = await import('./workbench.js');
299
+ await startWorkbench();
300
+ },
301
+ })),
302
+ memcode: () => Promise.resolve(defineCommand({
303
+ meta: { name: 'memcode', description: 'Enter memcode TUI — native coding agent with memory' },
304
+ async run() {
305
+ try {
306
+ const { runCli } = await importBundledMemcode();
307
+ await runCli(process.argv.slice(3));
308
+ } catch (err) {
309
+ console.error('Failed to start memcode:', err instanceof Error ? err.message : err);
310
+ process.exit(1);
311
+ }
312
+ },
313
+ })),
314
+ },
315
+ async run() {
316
+ // Guard: if citty already resolved a subcommand, its run() was called before this.
317
+ // Detect by checking if the first CLI arg matches a registered subcommand name.
318
+ const firstArg = process.argv[2];
319
+ const knownSubs = ['ask', 'search', 'remember', 'recent', 'help', 'workbench', 'memcode', 'config',
320
+ 'init', 'setup', 'integrate', 'memory', 'context', 'resume', 'explain', 'codegraph', 'checkpoint', 'knowledge', 'reasoning', 'retention', 'formation', 'audit', 'transfer', 'skills', 'identity',
321
+ 'session', 'team', 'task', 'message', 'lock', 'handoff', 'poll',
322
+ 'receipt',
323
+ 'serve', 'serve-http', 'status', 'sync',
324
+ 'hook', 'hooks', 'ingest', 'media', 'git-hook', 'git-hook-uninstall',
325
+ 'background', 'bg', 'bs', 'doctor', 'repair', 'dashboard', 'cleanup', 'purge', 'uninstall', 'orchestrate'];
326
+ if (firstArg && knownSubs.includes(firstArg)) return;
327
+
328
+ // No subcommand provided — enter memcode TUI (native coding agent)
329
+ if (!firstArg) {
330
+ try {
331
+ const { runCli } = await importBundledMemcode();
332
+ await runCli(process.argv.slice(2));
333
+ return;
334
+ } catch (err) {
335
+ console.error('Failed to start memcode:', err instanceof Error ? err.message : err);
336
+ process.exit(1);
337
+ }
338
+ }
339
+
340
+ // Fallback: show usage hint
341
+ if (process.stdout.isTTY && process.stdin.isTTY) {
342
+ // Fire-and-forget: background update check. Default is notify-only; stderr only, never blocks TUI.
343
+ import('./update-checker.js').then(m => m.checkForUpdates()).catch(() => {});
344
+ const { startWorkbench } = await import('./workbench.js');
345
+ await startWorkbench();
346
+ } else {
347
+ // Non-interactive mode: show usage hint
348
+ console.error(`Memorix v${getCliVersion()} — Local-first memory control plane\n`);
349
+ console.error('Usage: memorix <command>\n');
350
+ console.error('Commands:');
351
+ console.error(' help Show action-oriented help (`memorix memory --help`)');
352
+ console.error(' workbench Open interactive terminal memory control plane');
353
+ console.error(' memcode Enter memcode TUI (native coding agent)');
354
+ console.error(' ask "q" Ask Memorix a question (single-shot chat)');
355
+ console.error(' Pipe: echo "q" | memorix ask');
356
+ console.error(' background Start/stop/status background control plane');
357
+ console.error(' session Start/end/context for coding sessions');
358
+ console.error(' memory Search/store/detail/timeline/resolve observations');
359
+ console.error(' context Show the Memory Autopilot brief for this project');
360
+ console.error(' resume Resume prior work with one bounded project brief');
361
+ console.error(' explain Explain where Memorix project context comes from');
362
+ console.error(' codegraph Refresh/status/context-pack for CodeGraph Memory');
363
+ console.error(' checkpoint Inspect native compact continuity checkpoints');
364
+ console.error(' knowledge Review source-backed knowledge pages and project workflows');
365
+ console.error(' reasoning Store/search decision rationale');
366
+ console.error(' retention Inspect stale/archive status');
367
+ console.error(' formation Inspect Memory Formation metrics');
368
+ console.error(' audit Audit trail and project attribution checks');
369
+ console.error(' transfer Export/import memory snapshots');
370
+ console.error(' skills List/generate/show project skills');
371
+ console.error(' identity Select the explicit CLI actor for private/team memory');
372
+ console.error(' team Join/status/role operations for coordination state');
373
+ console.error(' task Create/claim/complete/list team tasks');
374
+ console.error(' message Send/broadcast/read team messages');
375
+ console.error(' lock Manage advisory file locks');
376
+ console.error(' handoff Create durable handoff artifacts');
377
+ console.error(' poll Snapshot project coordination state');
378
+ console.error(' receipt Privacy-safe memory handoff diagnostic');
379
+ console.error(' serve-http Start HTTP MCP + dashboard control plane');
380
+ console.error(' serve Start MCP server on stdio');
381
+ console.error(' init Create global defaults or project config');
382
+ console.error(' setup Install Memorix plugin/MCP/rules/hooks for an agent');
383
+ console.error(' repair Repair Memorix-owned agent integration files');
384
+ console.error(' config Show TOML config paths and resolved values');
385
+ console.error(' integrate Install one IDE integration into the current repo');
386
+ console.error(' status Show project info + stats');
387
+ console.error(' dashboard Open standalone dashboard (read-mostly)');
388
+ console.error(' hooks Open legacy hook installer menu');
389
+ console.error(' cleanup Remove old memories');
390
+ console.error(' purge Retire all memories (project by default, --all for everything)');
391
+ console.error(' sync Rules/workspace sync plus interactive wizard');
392
+ console.error(' ingest Ingest commit, log, or image knowledge');
393
+ console.error('\nRun `memorix` in an interactive terminal for memcode TUI.');
394
+ }
395
+ },
396
+ });
397
+
398
+ try {
399
+ normalizeCliInvocation();
400
+ if (!printCliGuideForHelp()) {
401
+ runMain(main);
402
+ }
403
+ } catch (error) {
404
+ console.error(`Memorix CLI invocation error: ${error instanceof Error ? error.message : String(error)}`);
405
+ process.exitCode = 1;
406
+ }