memorix 1.2.1 → 1.2.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 (90) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +14 -2
  3. package/README.zh-CN.md +14 -2
  4. package/dist/cli/index.js +15424 -13780
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/index.js +1337 -536
  7. package/dist/index.js.map +1 -1
  8. package/dist/maintenance-runner.d.ts +1 -1
  9. package/dist/maintenance-runner.js +8458 -8087
  10. package/dist/maintenance-runner.js.map +1 -1
  11. package/dist/memcode-runtime/CHANGELOG.md +23 -0
  12. package/dist/sdk.d.ts +7 -2
  13. package/dist/sdk.js +1365 -542
  14. package/dist/sdk.js.map +1 -1
  15. package/dist/types.d.ts +49 -1
  16. package/dist/types.js.map +1 -1
  17. package/docs/1.2.2-MEMORY-CONTROL-PLANE.md +434 -0
  18. package/docs/AGENT_OPERATOR_PLAYBOOK.md +4 -0
  19. package/docs/API_REFERENCE.md +24 -4
  20. package/docs/README.md +1 -1
  21. package/docs/dev-log/progress.txt +101 -11
  22. package/package.json +1 -1
  23. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  24. package/src/cli/command-guide.ts +192 -0
  25. package/src/cli/commands/audit.ts +9 -4
  26. package/src/cli/commands/cleanup.ts +5 -1
  27. package/src/cli/commands/codegraph.ts +15 -5
  28. package/src/cli/commands/context.ts +3 -2
  29. package/src/cli/commands/doctor.ts +4 -2
  30. package/src/cli/commands/explain.ts +9 -3
  31. package/src/cli/commands/handoff.ts +21 -7
  32. package/src/cli/commands/identity.ts +116 -0
  33. package/src/cli/commands/ingest-image.ts +5 -3
  34. package/src/cli/commands/lock.ts +11 -10
  35. package/src/cli/commands/memory.ts +58 -21
  36. package/src/cli/commands/message.ts +19 -14
  37. package/src/cli/commands/operator-shared.ts +98 -3
  38. package/src/cli/commands/poll.ts +16 -6
  39. package/src/cli/commands/reasoning.ts +17 -3
  40. package/src/cli/commands/retention.ts +9 -4
  41. package/src/cli/commands/serve-http.ts +8 -2
  42. package/src/cli/commands/session.ts +44 -10
  43. package/src/cli/commands/skills.ts +10 -5
  44. package/src/cli/commands/status.ts +4 -3
  45. package/src/cli/commands/task.ts +26 -17
  46. package/src/cli/commands/team.ts +14 -10
  47. package/src/cli/commands/transfer.ts +63 -10
  48. package/src/cli/identity.ts +89 -0
  49. package/src/cli/index.ts +96 -19
  50. package/src/cli/invocation.ts +115 -0
  51. package/src/cli/tui/chat-service.ts +41 -18
  52. package/src/cli/tui/data.ts +23 -44
  53. package/src/cli/tui/operator-context.ts +60 -0
  54. package/src/cli/tui/session-service.ts +3 -2
  55. package/src/cli/tui/views/MemoryView.tsx +10 -8
  56. package/src/codegraph/auto-context.ts +31 -2
  57. package/src/codegraph/context-pack.ts +1 -0
  58. package/src/codegraph/project-context.ts +2 -0
  59. package/src/compact/engine.ts +26 -10
  60. package/src/compact/index-format.ts +25 -2
  61. package/src/dashboard/server.ts +46 -9
  62. package/src/hooks/admission.ts +117 -0
  63. package/src/hooks/handler.ts +98 -91
  64. package/src/knowledge/context-assembly.ts +97 -0
  65. package/src/knowledge/workset.ts +179 -10
  66. package/src/memory/admission.ts +57 -0
  67. package/src/memory/consolidation.ts +13 -2
  68. package/src/memory/disclosure-policy.ts +6 -1
  69. package/src/memory/export-import.ts +11 -3
  70. package/src/memory/graph-context.ts +8 -2
  71. package/src/memory/observations.ts +162 -4
  72. package/src/memory/quality-audit.ts +2 -0
  73. package/src/memory/retention.ts +22 -2
  74. package/src/memory/session.ts +29 -11
  75. package/src/memory/visibility.ts +80 -0
  76. package/src/orchestrate/memorix-bridge.ts +38 -0
  77. package/src/runtime/control-plane-maintenance.ts +1 -0
  78. package/src/runtime/isolated-maintenance.ts +1 -0
  79. package/src/runtime/lifecycle.ts +18 -0
  80. package/src/runtime/maintenance-jobs.ts +1 -0
  81. package/src/runtime/maintenance-runner.ts +2 -0
  82. package/src/runtime/project-maintenance.ts +89 -0
  83. package/src/sdk.ts +35 -5
  84. package/src/server.ts +267 -83
  85. package/src/store/orama-store.ts +61 -6
  86. package/src/store/sqlite-db.ts +23 -1
  87. package/src/store/sqlite-store.ts +12 -2
  88. package/src/team/handoff.ts +7 -0
  89. package/src/types.ts +51 -0
  90. package/src/wiki/generator.ts +2 -0
@@ -1,7 +1,41 @@
1
1
  import { defineCommand } from 'citty';
2
+ import { promises as fs } from 'node:fs';
3
+ import path from 'node:path';
2
4
  import { exportAsJson, exportAsMarkdown, importFromJson } from '../../memory/export-import.js';
3
5
  import { emitError, emitResult, getCliProjectContext } from './operator-shared.js';
4
6
 
7
+ async function readStandardInput(): Promise<string> {
8
+ return new Promise((resolve, reject) => {
9
+ let content = '';
10
+ process.stdin.setEncoding('utf8');
11
+ process.stdin.on('data', (chunk) => {
12
+ content += chunk;
13
+ });
14
+ process.stdin.on('end', () => resolve(content));
15
+ process.stdin.on('error', reject);
16
+ });
17
+ }
18
+
19
+ async function readImportPayload(args: Record<string, unknown>, projectRoot: string): Promise<string> {
20
+ const data = typeof args.data === 'string' ? args.data.trim() : '';
21
+ const file = typeof args.file === 'string' ? args.file.trim() : '';
22
+ const readFromStdin = args.stdin === true || data === '-';
23
+ const sources = Number(Boolean(data && data !== '-')) + Number(Boolean(file)) + Number(readFromStdin);
24
+ if (sources !== 1) {
25
+ throw new Error('Choose exactly one import source: --data <json>, --file <path>, or --stdin.');
26
+ }
27
+ if (readFromStdin) return readStandardInput();
28
+ if (file) return fs.readFile(path.resolve(projectRoot, file), 'utf8');
29
+ return data;
30
+ }
31
+
32
+ async function writeExportPayload(projectRoot: string, output: string, payload: string): Promise<string> {
33
+ const outputPath = path.resolve(projectRoot, output);
34
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
35
+ await fs.writeFile(outputPath, payload, 'utf8');
36
+ return outputPath;
37
+ }
38
+
5
39
  export default defineCommand({
6
40
  meta: {
7
41
  name: 'transfer',
@@ -10,6 +44,9 @@ export default defineCommand({
10
44
  args: {
11
45
  format: { type: 'string', description: 'Export format: json or markdown' },
12
46
  data: { type: 'string', description: 'JSON payload from a previous export' },
47
+ file: { type: 'string', description: 'File containing a JSON payload to import' },
48
+ stdin: { type: 'boolean', description: 'Read a JSON import payload from standard input' },
49
+ out: { type: 'string', description: 'Write the export payload to this file instead of stdout' },
13
50
  json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
14
51
  },
15
52
  run: async ({ args }) => {
@@ -17,31 +54,45 @@ export default defineCommand({
17
54
  const asJson = !!args.json;
18
55
 
19
56
  try {
20
- const { project, dataDir } = await getCliProjectContext();
57
+ const { project, dataDir, reader } = await getCliProjectContext();
21
58
 
22
59
  switch (action) {
23
60
  case 'export': {
24
61
  const format = (args.format as string | undefined) === 'markdown' ? 'markdown' : 'json';
25
62
  if (format === 'markdown') {
26
- const markdown = await exportAsMarkdown(dataDir, project.id);
63
+ const markdown = await exportAsMarkdown(dataDir, project.id, reader);
64
+ const output = (args.out as string | undefined)?.trim();
65
+ if (output) {
66
+ const outputPath = await writeExportPayload(project.rootPath, output, markdown);
67
+ emitResult({ project, format, outputPath }, `Export written: ${outputPath}`, asJson);
68
+ return;
69
+ }
27
70
  emitResult({ project, format, markdown }, markdown, asJson);
28
71
  return;
29
72
  }
30
- const exported = await exportAsJson(dataDir, project.id);
73
+ const exported = await exportAsJson(dataDir, project.id, reader);
74
+ const serialized = JSON.stringify(exported, null, 2);
75
+ const output = (args.out as string | undefined)?.trim();
76
+ if (output) {
77
+ const outputPath = await writeExportPayload(project.rootPath, output, `${serialized}\n`);
78
+ emitResult(
79
+ { project, format, outputPath, stats: exported.stats },
80
+ `Export written: ${outputPath}`,
81
+ asJson,
82
+ );
83
+ return;
84
+ }
31
85
  emitResult(
32
86
  { project, format, export: exported },
33
- JSON.stringify(exported, null, 2),
87
+ serialized,
34
88
  asJson,
35
89
  );
36
90
  return;
37
91
  }
38
92
 
39
93
  case 'import': {
40
- const raw = (args.data as string | undefined)?.trim();
41
- if (!raw) {
42
- emitError('data is required for "memorix transfer import"', asJson);
43
- return;
44
- }
94
+ const raw = (await readImportPayload(args as Record<string, unknown>, project.rootPath)).trim();
95
+ if (!raw) throw new Error('Import payload is empty.');
45
96
  let parsed: Parameters<typeof importFromJson>[1];
46
97
  try {
47
98
  parsed = JSON.parse(raw);
@@ -62,7 +113,9 @@ export default defineCommand({
62
113
  console.log('Memorix Transfer Commands');
63
114
  console.log('');
64
115
  console.log('Usage:');
65
- console.log(' memorix transfer export [--format json|markdown]');
116
+ console.log(' memorix transfer export [--format json|markdown] [--out ./.memorix-export.json]');
117
+ console.log(' memorix transfer import --file ./.memorix-export.json');
118
+ console.log(' memorix transfer import --stdin');
66
119
  console.log(' memorix transfer import --data "<json>"');
67
120
  }
68
121
  } catch (error) {
@@ -0,0 +1,89 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import path from 'node:path';
3
+ import type { ObservationReader, ProjectInfo } from '../types.js';
4
+ import type { TeamStore } from '../team/team-store.js';
5
+
6
+ export interface CliIdentity {
7
+ agentId: string;
8
+ projectId: string;
9
+ activatedAt: string;
10
+ }
11
+
12
+ export interface CliIdentityResolution {
13
+ identity: CliIdentity | null;
14
+ reader: ObservationReader;
15
+ warning?: string;
16
+ }
17
+
18
+ function identityPath(dataDir: string): string {
19
+ return path.join(dataDir, 'cli-identity.json');
20
+ }
21
+
22
+ function isCliIdentity(value: unknown): value is CliIdentity {
23
+ if (!value || typeof value !== 'object') return false;
24
+ const candidate = value as Partial<CliIdentity>;
25
+ return typeof candidate.agentId === 'string'
26
+ && typeof candidate.projectId === 'string'
27
+ && typeof candidate.activatedAt === 'string';
28
+ }
29
+
30
+ export async function loadCliIdentity(dataDir: string, projectId: string): Promise<CliIdentity | null> {
31
+ try {
32
+ const parsed = JSON.parse(await fs.readFile(identityPath(dataDir), 'utf8'));
33
+ return isCliIdentity(parsed) && parsed.projectId === projectId ? parsed : null;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ export async function saveCliIdentity(dataDir: string, identity: CliIdentity): Promise<void> {
40
+ await fs.writeFile(identityPath(dataDir), `${JSON.stringify(identity, null, 2)}\n`, 'utf8');
41
+ }
42
+
43
+ export async function clearCliIdentity(dataDir: string): Promise<void> {
44
+ await fs.rm(identityPath(dataDir), { force: true });
45
+ }
46
+
47
+ /**
48
+ * An unbound terminal remains project-scoped. Private and team access becomes
49
+ * available only after the operator explicitly chooses an active team identity.
50
+ */
51
+ export async function resolveCliIdentity(options: {
52
+ project: ProjectInfo;
53
+ dataDir: string;
54
+ teamStore: TeamStore;
55
+ explicitActorId?: string;
56
+ }): Promise<CliIdentityResolution> {
57
+ const stored = await loadCliIdentity(options.dataDir, options.project.id);
58
+ const requestedAgentId = options.explicitActorId ?? stored?.agentId;
59
+ const projectReader: ObservationReader = { projectId: options.project.id };
60
+ if (!requestedAgentId) return { identity: null, reader: projectReader };
61
+
62
+ const agent = options.teamStore.getAgent(requestedAgentId);
63
+ const activeForProject = agent
64
+ && agent.project_id === options.project.id
65
+ && agent.status === 'active';
66
+ if (!activeForProject) {
67
+ if (options.explicitActorId) {
68
+ throw new Error(`CLI actor "${requestedAgentId}" is not an active member of this project.`);
69
+ }
70
+ return {
71
+ identity: null,
72
+ reader: projectReader,
73
+ warning: `Saved CLI identity "${requestedAgentId}" is no longer active for this project.`,
74
+ };
75
+ }
76
+
77
+ return {
78
+ identity: stored ?? {
79
+ agentId: agent.agent_id,
80
+ projectId: options.project.id,
81
+ activatedAt: new Date().toISOString(),
82
+ },
83
+ reader: {
84
+ projectId: options.project.id,
85
+ agentId: agent.agent_id,
86
+ isTeamMember: true,
87
+ },
88
+ };
89
+ }
package/src/cli/index.ts CHANGED
@@ -19,6 +19,8 @@ import { execSync, spawn } from 'node:child_process';
19
19
  import { getCliVersion } from './version.js';
20
20
  import { importBundledMemcode } from './memcode-bootstrap.js';
21
21
  import { installCliPipeErrorGuard } from './pipe-errors.js';
22
+ import { normalizeCliInvocation } from './invocation.js';
23
+ import { printCliGuideForHelp, renderCliGuide } from './command-guide.js';
22
24
 
23
25
  installCliPipeErrorGuard();
24
26
 
@@ -129,8 +131,9 @@ async function getWorkbenchHeader(): Promise<string[]> {
129
131
  const dataDir = await getProjectDataDir(proj.id);
130
132
  await initStore(dataDir);
131
133
  const { getObservationStore: getStore } = await import('../store/obs-store.js');
132
- const obs = await getStore().loadAll() as any[];
133
- const active = obs.filter((o: any) => (o.status ?? 'active') === 'active').length;
134
+ const { filterReadableObservations } = await import('../memory/visibility.js');
135
+ const obs = filterReadableObservations(await getStore().loadAll(), { projectId: proj.id });
136
+ const active = obs.filter((o) => (o.status ?? 'active') === 'active').length;
134
137
  if (active > 0) {
135
138
  memLabel = `${BOLD}${active}${RESET} ${DIM}active${RESET}`;
136
139
  }
@@ -801,6 +804,7 @@ async function runSearch(query: string): Promise<void> {
801
804
 
802
805
  try {
803
806
  const { searchObservations, getDb, hydrateIndex } = await import('../store/orama-store.js');
807
+ const { filterReadableObservations } = await import('../memory/visibility.js');
804
808
  const { getProjectDataDir } = await import('../store/persistence.js');
805
809
  const { detectProject } = await import('../project/detector.js');
806
810
  const { initObservations } = await import('../memory/observations.js');
@@ -825,7 +829,7 @@ async function runSearch(query: string): Promise<void> {
825
829
  await hydrateIndex(allObs);
826
830
  mark('hydrateIndex');
827
831
 
828
- const results = await searchObservations({ query, limit: 10, projectId: project.id });
832
+ const results = await searchObservations({ query, limit: 10, projectId: project.id, reader: { projectId: project.id } });
829
833
  mark('search');
830
834
  s.stop('Search complete');
831
835
 
@@ -855,14 +859,16 @@ async function runList(): Promise<void> {
855
859
  const { getProjectDataDir } = await import('../store/persistence.js');
856
860
  const { detectProject } = await import('../project/detector.js');
857
861
  const { initObservationStore: initStore, getObservationStore: getStore } = await import('../store/obs-store.js');
862
+ const { filterReadableObservations } = await import('../memory/visibility.js');
858
863
 
859
864
  const project = detectProject(process.cwd());
860
865
  if (!project) { s.stop('No git repo'); p.log.error(NO_GIT_MSG); return; }
861
866
  const dataDir = await getProjectDataDir(project.id);
862
867
  await initStore(dataDir);
863
- const observations = await getStore().loadAll() as unknown as Array<{
864
- id: number; title: string; type: string; timestamp: string; status?: string;
865
- }>;
868
+ const observations = filterReadableObservations(
869
+ await getStore().loadAll(),
870
+ { projectId: project.id },
871
+ );
866
872
 
867
873
  const active = observations.filter(o => (o.status ?? 'active') === 'active');
868
874
  const recent = active.slice(-10).reverse();
@@ -876,7 +882,7 @@ async function runList(): Promise<void> {
876
882
 
877
883
  console.log('');
878
884
  for (const o of recent) {
879
- const typeLabel = { gotcha: '[!]', decision: '[D]', 'problem-solution': '[S]', discovery: '[?]', 'how-it-works': '[H]', 'what-changed': '[C]' }[o.type] ?? '[·]';
885
+ const typeLabel = ({ gotcha: '[!]', decision: '[D]', 'problem-solution': '[S]', discovery: '[?]', 'how-it-works': '[H]', 'what-changed': '[C]' } as Record<string, string>)[o.type] ?? '[·]';
880
886
  console.log(` ${typeLabel} #${o.id} ${o.title?.slice(0, 60) ?? '(untitled)'}`);
881
887
  }
882
888
  console.log('');
@@ -927,11 +933,26 @@ async function runCommand(cmd: string, _args: string[] = []): Promise<void> {
927
933
  // Main command
928
934
  // ============================================================
929
935
 
936
+ async function runMemoryShortcut(action: string, args: Record<string, unknown>): Promise<void> {
937
+ const { detectProject } = await import('../project/detector.js');
938
+ if (!detectProject(process.cwd())) {
939
+ console.log(NO_GIT_MSG);
940
+ process.exitCode = 1;
941
+ return;
942
+ }
943
+ const memory = await import('./commands/memory.js');
944
+ await memory.default.run?.({
945
+ args: { ...args, _: [action] },
946
+ rawArgs: [],
947
+ cmd: memory.default,
948
+ } as any);
949
+ }
950
+
930
951
  const main = defineCommand({
931
952
  meta: {
932
953
  name: 'memorix',
933
954
  version: getCliVersion(),
934
- description: 'Local-first memory control plane for AI coding agents via MCP',
955
+ description: 'Local-first memory control plane for AI coding agents through CLI, MCP, and local workflows',
935
956
  },
936
957
  subCommands: {
937
958
  // One-shot product commands (primary user paths)
@@ -960,18 +981,55 @@ const main = defineCommand({
960
981
  },
961
982
  })),
962
983
  search: () => Promise.resolve(defineCommand({
963
- meta: { name: 'search', description: 'Search memories' },
964
- args: { query: { type: 'positional', description: 'Search query', required: true } },
965
- async run({ args }) { await runSearch(args.query as string); },
984
+ meta: { name: 'search', description: 'Shortcut for `memorix memory search`' },
985
+ args: {
986
+ query: { type: 'positional', description: 'Search query', required: true },
987
+ limit: { type: 'string', description: 'Maximum results' },
988
+ json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
989
+ },
990
+ async run({ args }) {
991
+ await runMemoryShortcut('search', {
992
+ query: args.query,
993
+ limit: args.limit,
994
+ json: args.json,
995
+ });
996
+ },
966
997
  })),
967
998
  remember: () => Promise.resolve(defineCommand({
968
- meta: { name: 'remember', description: 'Store a quick memory' },
969
- args: { text: { type: 'positional', description: 'Text to remember', required: true } },
970
- async run({ args }) { await runRemember(args.text as string); },
999
+ meta: { name: 'remember', description: 'Shortcut for `memorix memory store`' },
1000
+ args: {
1001
+ text: { type: 'positional', description: 'Text to remember', required: true },
1002
+ title: { type: 'string', description: 'Optional observation title' },
1003
+ type: { type: 'string', description: 'Observation type' },
1004
+ visibility: { type: 'string', description: 'project (default), personal, or team' },
1005
+ json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
1006
+ },
1007
+ async run({ args }) {
1008
+ await runMemoryShortcut('store', {
1009
+ text: args.text,
1010
+ title: args.title,
1011
+ type: args.type,
1012
+ visibility: args.visibility,
1013
+ json: args.json,
1014
+ });
1015
+ },
971
1016
  })),
972
1017
  recent: () => Promise.resolve(defineCommand({
973
- meta: { name: 'recent', description: 'View recent memories' },
974
- async run() { await runList(); },
1018
+ meta: { name: 'recent', description: 'Shortcut for `memorix memory recent`' },
1019
+ args: {
1020
+ limit: { type: 'string', description: 'Maximum results' },
1021
+ json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
1022
+ },
1023
+ async run({ args }) { await runMemoryShortcut('recent', { limit: args.limit, json: args.json }); },
1024
+ })),
1025
+ help: () => Promise.resolve(defineCommand({
1026
+ meta: { name: 'help', description: 'Show action-oriented help for a command group' },
1027
+ args: {
1028
+ command: { type: 'positional', description: 'Command group to inspect', required: false },
1029
+ },
1030
+ async run({ args }) {
1031
+ console.log(renderCliGuide(args.command as string | undefined));
1032
+ },
975
1033
  })),
976
1034
  // Infrastructure commands
977
1035
  init: () => import('./commands/init.js').then(m => m.default),
@@ -996,6 +1054,7 @@ const main = defineCommand({
996
1054
  audit: () => import('./commands/audit.js').then(m => m.default),
997
1055
  transfer: () => import('./commands/transfer.js').then(m => m.default),
998
1056
  skills: () => import('./commands/skills.js').then(m => m.default),
1057
+ identity: () => import('./commands/identity.js').then(m => m.default),
999
1058
  session: () => import('./commands/session.js').then(m => m.default),
1000
1059
  team: () => import('./commands/team.js').then(m => m.default),
1001
1060
  task: () => import('./commands/task.js').then(m => m.default),
@@ -1031,6 +1090,13 @@ const main = defineCommand({
1031
1090
  cleanup: () => import('./commands/cleanup.js').then(m => m.default),
1032
1091
  uninstall: () => import('./commands/uninstall.js').then(m => m.default),
1033
1092
  orchestrate: () => import('./commands/orchestrate.js').then(m => m.default),
1093
+ workbench: () => Promise.resolve(defineCommand({
1094
+ meta: { name: 'workbench', description: 'Open the interactive terminal memory control plane' },
1095
+ async run() {
1096
+ const { startWorkbench } = await import('./workbench.js');
1097
+ await startWorkbench();
1098
+ },
1099
+ })),
1034
1100
  memcode: () => Promise.resolve(defineCommand({
1035
1101
  meta: { name: 'memcode', description: 'Enter memcode TUI — native coding agent with memory' },
1036
1102
  async run() {
@@ -1048,8 +1114,8 @@ const main = defineCommand({
1048
1114
  // Guard: if citty already resolved a subcommand, its run() was called before this.
1049
1115
  // Detect by checking if the first CLI arg matches a registered subcommand name.
1050
1116
  const firstArg = process.argv[2];
1051
- const knownSubs = ['ask', 'search', 'remember', 'recent', 'memcode', 'config',
1052
- 'init', 'setup', 'integrate', 'memory', 'context', 'explain', 'codegraph', 'knowledge', 'reasoning', 'retention', 'formation', 'audit', 'transfer', 'skills',
1117
+ const knownSubs = ['ask', 'search', 'remember', 'recent', 'help', 'workbench', 'memcode', 'config',
1118
+ 'init', 'setup', 'integrate', 'memory', 'context', 'explain', 'codegraph', 'knowledge', 'reasoning', 'retention', 'formation', 'audit', 'transfer', 'skills', 'identity',
1053
1119
  'session', 'team', 'task', 'message', 'lock', 'handoff', 'poll',
1054
1120
  'receipt',
1055
1121
  'serve', 'serve-http', 'status', 'sync',
@@ -1080,6 +1146,8 @@ const main = defineCommand({
1080
1146
  console.error(`Memorix v${getCliVersion()} — Local-first memory control plane\n`);
1081
1147
  console.error('Usage: memorix <command>\n');
1082
1148
  console.error('Commands:');
1149
+ console.error(' help Show action-oriented help (`memorix memory --help`)');
1150
+ console.error(' workbench Open interactive terminal memory control plane');
1083
1151
  console.error(' memcode Enter memcode TUI (native coding agent)');
1084
1152
  console.error(' ask "q" Ask Memorix a question (single-shot chat)');
1085
1153
  console.error(' Pipe: echo "q" | memorix ask');
@@ -1096,6 +1164,7 @@ const main = defineCommand({
1096
1164
  console.error(' audit Audit trail and project attribution checks');
1097
1165
  console.error(' transfer Export/import memory snapshots');
1098
1166
  console.error(' skills List/generate/show project skills');
1167
+ console.error(' identity Select the explicit CLI actor for private/team memory');
1099
1168
  console.error(' team Join/status/role operations for coordination state');
1100
1169
  console.error(' task Create/claim/complete/list team tasks');
1101
1170
  console.error(' message Send/broadcast/read team messages');
@@ -1121,4 +1190,12 @@ const main = defineCommand({
1121
1190
  },
1122
1191
  });
1123
1192
 
1124
- runMain(main);
1193
+ try {
1194
+ normalizeCliInvocation();
1195
+ if (!printCliGuideForHelp()) {
1196
+ runMain(main);
1197
+ }
1198
+ } catch (error) {
1199
+ console.error(`Memorix CLI invocation error: ${error instanceof Error ? error.message : String(error)}`);
1200
+ process.exitCode = 1;
1201
+ }
@@ -0,0 +1,115 @@
1
+ import { existsSync, statSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+
4
+ export interface CliInvocation {
5
+ projectRoot?: string;
6
+ actorId?: string;
7
+ }
8
+
9
+ type InvocationOption = 'projectRoot' | 'actorId';
10
+
11
+ const GLOBAL_OPTIONS: Record<string, InvocationOption> = {
12
+ '--cwd': 'projectRoot',
13
+ '--project-root': 'projectRoot',
14
+ '--as': 'actorId',
15
+ '--actor': 'actorId',
16
+ };
17
+
18
+ // Existing commands use camelCase Citty keys. Preserve them for compatibility
19
+ // while accepting the shell-native kebab-case spelling everywhere.
20
+ const FLAG_ALIASES: Record<string, string> = {
21
+ '--agent-type': '--agentType',
22
+ '--instance-id': '--instanceId',
23
+ '--agent-id': '--agentId',
24
+ '--from-agent-id': '--fromAgentId',
25
+ '--to-agent-id': '--toAgentId',
26
+ '--join-team': '--joinTeam',
27
+ '--session-id': '--sessionId',
28
+ '--task-id': '--taskId',
29
+ '--topic-key': '--topicKey',
30
+ '--graph-limit': '--graphLimit',
31
+ '--graph-query': '--graphQuery',
32
+ '--skill-id': '--skillId',
33
+ '--required-role': '--requiredRole',
34
+ '--preferred-role': '--preferredRole',
35
+ '--to-role': '--toRole',
36
+ '--handoff-status': '--handoffStatus',
37
+ '--max-concurrent': '--maxConcurrent',
38
+ '--role-id': '--roleId',
39
+ '--preferred-agent-types': '--preferredAgentTypes',
40
+ '--files-modified': '--filesModified',
41
+ '--expected-outcome': '--expectedOutcome',
42
+ '--related-commits': '--relatedCommits',
43
+ '--related-entities': '--relatedEntities',
44
+ '--mime-type': '--mimeType',
45
+ '--mark-read': '--markRead',
46
+ '--mark-inbox-read': '--markInboxRead',
47
+ };
48
+
49
+ function readOption(argument: string): { name: string; value?: string } {
50
+ const equals = argument.indexOf('=');
51
+ return equals === -1
52
+ ? { name: argument }
53
+ : { name: argument.slice(0, equals), value: argument.slice(equals + 1) };
54
+ }
55
+
56
+ /**
57
+ * Normalize the small set of global operator flags before Citty resolves a
58
+ * command. This gives every CLI command the same project and actor anchors
59
+ * without duplicating those flags across dozens of command definitions.
60
+ */
61
+ export function normalizeCliInvocation(argv: string[] = process.argv): CliInvocation {
62
+ const normalized = argv.slice(0, 2);
63
+ const invocation: CliInvocation = {};
64
+
65
+ for (let index = 2; index < argv.length; index += 1) {
66
+ const argument = argv[index];
67
+ if (argument === '--') {
68
+ normalized.push(...argv.slice(index));
69
+ break;
70
+ }
71
+
72
+ const { name, value: inlineValue } = readOption(argument);
73
+ const target = GLOBAL_OPTIONS[name];
74
+ if (!target) {
75
+ const alias = FLAG_ALIASES[name];
76
+ normalized.push(alias
77
+ ? (inlineValue === undefined ? alias : `${alias}=${inlineValue}`)
78
+ : argument);
79
+ continue;
80
+ }
81
+
82
+ const value = inlineValue ?? argv[index + 1];
83
+ if (!value || value.startsWith('--')) {
84
+ throw new Error(`${name} requires a value`);
85
+ }
86
+ invocation[target] = value;
87
+ if (inlineValue === undefined) index += 1;
88
+ }
89
+
90
+ if (invocation.projectRoot) {
91
+ const projectRoot = resolve(invocation.projectRoot);
92
+ if (!existsSync(projectRoot) || !statSync(projectRoot).isDirectory()) {
93
+ throw new Error(`CLI project directory does not exist: ${projectRoot}`);
94
+ }
95
+ process.chdir(projectRoot);
96
+ process.env.MEMORIX_CLI_PROJECT_ROOT = projectRoot;
97
+ invocation.projectRoot = projectRoot;
98
+ }
99
+
100
+ if (invocation.actorId) {
101
+ process.env.MEMORIX_CLI_ACTOR_ID = invocation.actorId;
102
+ }
103
+
104
+ process.argv = normalized;
105
+ return invocation;
106
+ }
107
+
108
+ export function getCliInvocation(): CliInvocation {
109
+ const projectRoot = process.env.MEMORIX_CLI_PROJECT_ROOT?.trim();
110
+ const actorId = process.env.MEMORIX_CLI_ACTOR_ID?.trim();
111
+ return {
112
+ ...(projectRoot ? { projectRoot } : {}),
113
+ ...(actorId ? { actorId } : {}),
114
+ };
115
+ }