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,7 @@
1
1
  import { defineCommand } from 'citty';
2
2
  import { storeObservation } from '../../memory/observations.js';
3
3
  import { createHandoffArtifact } from '../../team/handoff.js';
4
- import { emitError, emitResult, getCliProjectContext, parseCsvList, shortId } from './operator-shared.js';
4
+ import { emitError, emitResult, getCliProjectContext, parseCsvList, resolveCliActorId, shortId } from './operator-shared.js';
5
5
 
6
6
  export default defineCommand({
7
7
  meta: {
@@ -23,19 +23,33 @@ export default defineCommand({
23
23
  const asJson = !!args.json;
24
24
 
25
25
  try {
26
- const { project, teamStore } = await getCliProjectContext();
26
+ const { project, teamStore, identity } = await getCliProjectContext();
27
+ const fromAgentId = resolveCliActorId(args.fromAgentId, identity, 'fromAgentId');
27
28
 
28
29
  switch (action) {
29
30
  case 'send': {
30
- if (!args.fromAgentId || !args.summary || !args.context) {
31
- emitError('fromAgentId, summary, and context are required for "memorix handoff send"', asJson);
31
+ if (!fromAgentId || !args.summary || !args.context) {
32
+ emitError('summary, context, and an active CLI identity or fromAgentId are required for "memorix handoff send"', asJson);
32
33
  return;
33
34
  }
35
+ const sender = teamStore.getAgent(fromAgentId);
36
+ if (!sender || sender.project_id !== project.id || sender.status !== 'active') {
37
+ emitError('The handoff sender must be an active coordination member of this project.', asJson);
38
+ return;
39
+ }
40
+ const toAgentId = (args.toAgentId as string | undefined)?.trim();
41
+ if (toAgentId) {
42
+ const recipient = teamStore.getAgent(toAgentId);
43
+ if (!recipient || recipient.project_id !== project.id || recipient.status !== 'active') {
44
+ emitError('The handoff recipient must be an active coordination member of this project.', asJson);
45
+ return;
46
+ }
47
+ }
34
48
  const result = await createHandoffArtifact(
35
49
  {
36
50
  projectId: project.id,
37
- fromAgentId: args.fromAgentId as string,
38
- toAgentId: args.toAgentId as string | undefined,
51
+ fromAgentId,
52
+ toAgentId,
39
53
  taskId: args.taskId as string | undefined,
40
54
  summary: args.summary as string,
41
55
  context: args.context as string,
@@ -57,7 +71,7 @@ export default defineCommand({
57
71
  console.log('Memorix Handoff Commands');
58
72
  console.log('');
59
73
  console.log('Usage:');
60
- console.log(' memorix handoff send --fromAgentId <id> --summary "..." --context "..." [--toAgentId <id>] [--taskId <id>]');
74
+ console.log(' memorix handoff send --summary "..." --context "..." [--fromAgentId <id>] [--toAgentId <id>] [--taskId <id>]');
61
75
  }
62
76
  } catch (error) {
63
77
  emitError(error instanceof Error ? error.message : String(error), asJson);
@@ -0,0 +1,116 @@
1
+ import { defineCommand } from 'citty';
2
+ import { AGENT_TYPE_ROLE_MAP } from '../../team/team-store.js';
3
+ import { clearCliIdentity, loadCliIdentity, saveCliIdentity } from '../identity.js';
4
+ import { emitError, emitResult, getCliProjectContext, parseCsvList } from './operator-shared.js';
5
+
6
+ function requiredText(value: unknown, label: string): string {
7
+ const text = typeof value === 'string' ? value.trim() : '';
8
+ if (!text) throw new Error(`${label} is required`);
9
+ return text;
10
+ }
11
+
12
+ export default defineCommand({
13
+ meta: {
14
+ name: 'identity',
15
+ description: 'Select the explicit CLI actor used for private or team-scoped operations',
16
+ },
17
+ args: {
18
+ agentId: { type: 'string', description: 'Existing active coordination agent ID' },
19
+ agentType: { type: 'string', description: 'Agent type when joining (for example codex)' },
20
+ instanceId: { type: 'string', description: 'Stable instance identity when joining' },
21
+ name: { type: 'string', description: 'Optional display name when joining' },
22
+ role: { type: 'string', description: 'Optional coordination role when joining' },
23
+ capabilities: { type: 'string', description: 'Comma-separated capability list when joining' },
24
+ json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
25
+ },
26
+ run: async ({ args }) => {
27
+ const action = ((args._ as string[])?.[0] || 'status').toLowerCase();
28
+ const asJson = !!args.json;
29
+
30
+ try {
31
+ const { project, dataDir, teamStore, identity, reader, identityWarning } = await getCliProjectContext();
32
+
33
+ switch (action) {
34
+ case 'status': {
35
+ const saved = await loadCliIdentity(dataDir, project.id);
36
+ const agent = saved ? teamStore.getAgent(saved.agentId) : undefined;
37
+ emitResult(
38
+ {
39
+ project,
40
+ identity,
41
+ savedIdentity: saved,
42
+ agent: agent ?? null,
43
+ reader,
44
+ ...(identityWarning ? { warning: identityWarning } : {}),
45
+ },
46
+ identity
47
+ ? `CLI identity: ${agent?.name ?? identity.agentId} (${identity.agentId})`
48
+ : identityWarning ?? 'CLI identity: project-scoped only. Use "memorix identity join" or "memorix identity use --agent-id <id>" for private/team operations.',
49
+ asJson,
50
+ );
51
+ return;
52
+ }
53
+
54
+ case 'join': {
55
+ const agentType = requiredText(args.agentType, 'agentType');
56
+ const agent = teamStore.registerAgent({
57
+ projectId: project.id,
58
+ agentType,
59
+ instanceId: args.instanceId as string | undefined,
60
+ name: args.name as string | undefined,
61
+ role: (args.role as string | undefined) || AGENT_TYPE_ROLE_MAP[agentType] || 'engineer',
62
+ capabilities: parseCsvList(args.capabilities as string | undefined),
63
+ });
64
+ const nextIdentity = {
65
+ agentId: agent.agent_id,
66
+ projectId: project.id,
67
+ activatedAt: new Date().toISOString(),
68
+ };
69
+ await saveCliIdentity(dataDir, nextIdentity);
70
+ emitResult(
71
+ { project, identity: nextIdentity, agent },
72
+ `CLI identity activated: ${agent.name} (${agent.agent_id})`,
73
+ asJson,
74
+ );
75
+ return;
76
+ }
77
+
78
+ case 'use': {
79
+ const agentId = requiredText(args.agentId, 'agentId');
80
+ const agent = teamStore.getAgent(agentId);
81
+ if (!agent || agent.project_id !== project.id || agent.status !== 'active') {
82
+ emitError('agentId must identify an active coordination member of this project.', asJson);
83
+ return;
84
+ }
85
+ const nextIdentity = {
86
+ agentId: agent.agent_id,
87
+ projectId: project.id,
88
+ activatedAt: new Date().toISOString(),
89
+ };
90
+ await saveCliIdentity(dataDir, nextIdentity);
91
+ emitResult(
92
+ { project, identity: nextIdentity, agent },
93
+ `CLI identity activated: ${agent.name} (${agent.agent_id})`,
94
+ asJson,
95
+ );
96
+ return;
97
+ }
98
+
99
+ case 'clear': {
100
+ await clearCliIdentity(dataDir);
101
+ emitResult(
102
+ { project, cleared: true },
103
+ 'CLI identity cleared. Subsequent commands use project-scoped access only.',
104
+ asJson,
105
+ );
106
+ return;
107
+ }
108
+
109
+ default:
110
+ emitError(`unknown identity action "${action}". Use "status", "join", "use", or "clear".`, asJson);
111
+ }
112
+ } catch (error) {
113
+ emitError(error instanceof Error ? error.message : String(error), asJson);
114
+ }
115
+ },
116
+ });
@@ -3,7 +3,7 @@ import { readFileSync } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { analyzeImage } from '../../multimodal/image-loader.js';
5
5
  import { storeObservation } from '../../memory/observations.js';
6
- import { emitError, emitResult, getCliProjectContext } from './operator-shared.js';
6
+ import { emitError, emitResult, getCliProjectContext, resolveCliWriteScope } from './operator-shared.js';
7
7
 
8
8
  function inferMimeType(filePath: string): string {
9
9
  const ext = path.extname(filePath).toLowerCase();
@@ -29,6 +29,7 @@ export default defineCommand({
29
29
  path: { type: 'string', description: 'Path to the image file' },
30
30
  prompt: { type: 'string', description: 'Custom analysis prompt' },
31
31
  mimeType: { type: 'string', description: 'Explicit MIME type override' },
32
+ visibility: { type: 'string', description: 'Memory visibility: project (default), personal, or team' },
32
33
  json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
33
34
  },
34
35
  run: async ({ args }) => {
@@ -39,8 +40,8 @@ export default defineCommand({
39
40
  emitError('path is required for "memorix ingest image"', asJson);
40
41
  return;
41
42
  }
42
- const { project } = await getCliProjectContext();
43
- const resolvedPath = path.resolve(process.cwd(), imagePath);
43
+ const { project, reader, identity } = await getCliProjectContext();
44
+ const resolvedPath = path.resolve(project.rootPath, imagePath);
44
45
  const base64 = readFileSync(resolvedPath).toString('base64');
45
46
  const filename = path.basename(resolvedPath);
46
47
  const analysis = await analyzeImage({
@@ -59,6 +60,7 @@ export default defineCommand({
59
60
  facts: analysis.entities,
60
61
  projectId: project.id,
61
62
  source: 'manual',
63
+ ...resolveCliWriteScope({ reader, identity }, args.visibility as string | undefined),
62
64
  });
63
65
 
64
66
  emitResult(
@@ -1,5 +1,5 @@
1
1
  import { defineCommand } from 'citty';
2
- import { emitError, emitResult, getCliProjectContext, shortId } from './operator-shared.js';
2
+ import { emitError, emitResult, getCliProjectContext, resolveCliActorId, shortId } from './operator-shared.js';
3
3
 
4
4
  export default defineCommand({
5
5
  meta: {
@@ -16,21 +16,22 @@ export default defineCommand({
16
16
  const asJson = !!args.json;
17
17
 
18
18
  try {
19
- const { project, teamStore } = await getCliProjectContext();
19
+ const { project, teamStore, identity } = await getCliProjectContext();
20
+ const agentId = resolveCliActorId(args.agentId, identity);
20
21
 
21
22
  switch (action) {
22
23
  case 'lock': {
23
- if (!args.file || !args.agentId) {
24
- emitError('file and agentId are required for "memorix lock lock"', asJson);
24
+ if (!args.file || !agentId) {
25
+ emitError('file and an active CLI identity or agentId are required for "memorix lock lock"', asJson);
25
26
  return;
26
27
  }
27
- const result = teamStore.acquireLock(project.id, args.file as string, args.agentId as string);
28
+ const result = teamStore.acquireLock(project.id, args.file as string, agentId);
28
29
  if (!result.success) {
29
30
  emitError(`File is already locked by ${shortId(result.lockedBy)}`, asJson);
30
31
  return;
31
32
  }
32
33
  emitResult(
33
- { project, file: args.file, lockedBy: args.agentId },
34
+ { project, file: args.file, lockedBy: agentId },
34
35
  `Locked: ${args.file as string}`,
35
36
  asJson,
36
37
  );
@@ -38,11 +39,11 @@ export default defineCommand({
38
39
  }
39
40
 
40
41
  case 'unlock': {
41
- if (!args.file || !args.agentId) {
42
- emitError('file and agentId are required for "memorix lock unlock"', asJson);
42
+ if (!args.file || !agentId) {
43
+ emitError('file and an active CLI identity or agentId are required for "memorix lock unlock"', asJson);
43
44
  return;
44
45
  }
45
- const released = teamStore.releaseLock(project.id, args.file as string, args.agentId as string);
46
+ const released = teamStore.releaseLock(project.id, args.file as string, agentId);
46
47
  if (!released) {
47
48
  emitError('Cannot unlock: not the owner or the file is not locked', asJson);
48
49
  return;
@@ -68,7 +69,7 @@ export default defineCommand({
68
69
  return;
69
70
  }
70
71
 
71
- const locks = teamStore.listLocks(project.id, args.agentId as string | undefined);
72
+ const locks = teamStore.listLocks(project.id, agentId);
72
73
  emitResult(
73
74
  { project, locks },
74
75
  locks.length === 0
@@ -1,9 +1,19 @@
1
1
  import { defineCommand } from 'citty';
2
2
  import { compactDetail, compactSearch, compactTimeline } from '../../compact/engine.js';
3
3
  import { withFreshIndex } from '../../memory/freshness.js';
4
- import { getAllObservations, getProjectObservations, resolveObservations, storeObservation, suggestTopicKey } from '../../memory/observations.js';
4
+ import { getAllObservations, getObservation, getProjectObservations, resolveObservations, storeObservation, suggestTopicKey } from '../../memory/observations.js';
5
5
  import { buildGraphContextPacket, formatGraphContextPrompt } from '../../memory/graph-context.js';
6
- import { emitError, emitResult, getCliProjectContext, parseCsvList, parsePositiveInt, coerceObservationStatus, coerceObservationType } from './operator-shared.js';
6
+ import { canManageObservation, filterReadableObservations, resolveObservationVisibility } from '../../memory/visibility.js';
7
+ import {
8
+ coerceObservationStatus,
9
+ coerceObservationType,
10
+ emitError,
11
+ emitResult,
12
+ getCliProjectContext,
13
+ parseCsvList,
14
+ parsePositiveInt,
15
+ resolveCliWriteScope,
16
+ } from './operator-shared.js';
7
17
 
8
18
  export default defineCommand({
9
19
  meta: {
@@ -19,6 +29,7 @@ export default defineCommand({
19
29
  facts: { type: 'string', description: 'Comma-separated facts' },
20
30
  files: { type: 'string', description: 'Comma-separated file list' },
21
31
  concepts: { type: 'string', description: 'Comma-separated concept list' },
32
+ visibility: { type: 'string', description: 'Memory visibility: project (default), personal, or team' },
22
33
  ids: { type: 'string', description: 'Comma-separated observation IDs' },
23
34
  id: { type: 'string', description: 'Single observation ID' },
24
35
  status: { type: 'string', description: 'Resolved or archived' },
@@ -32,6 +43,7 @@ export default defineCommand({
32
43
  after: { type: 'string', description: 'Timeline depth after anchor' },
33
44
  threshold: { type: 'string', description: 'Similarity threshold for consolidate' },
34
45
  dryRun: { type: 'boolean', description: 'Preview changes without mutating data' },
46
+ 'dry-run': { type: 'boolean', description: 'Kebab-case alias for --dryRun' },
35
47
  trigger: { type: 'string', description: 'Custom trigger text for promoted mini-skills' },
36
48
  instruction: { type: 'string', description: 'Custom instruction for promoted mini-skills' },
37
49
  tags: { type: 'string', description: 'Comma-separated extra tags for promoted mini-skills' },
@@ -44,7 +56,7 @@ export default defineCommand({
44
56
  const asJson = !!args.json;
45
57
 
46
58
  try {
47
- const { project } = await getCliProjectContext({ searchIndex: true });
59
+ const { project, dataDir, reader, identity } = await getCliProjectContext({ searchIndex: true });
48
60
 
49
61
  switch (action) {
50
62
  case 'search': {
@@ -54,7 +66,7 @@ export default defineCommand({
54
66
  return;
55
67
  }
56
68
  const limit = parsePositiveInt(args.limit as string | undefined, 10);
57
- const result = await compactSearch({ query, limit, projectId: project.id });
69
+ const result = await compactSearch({ query, limit, projectId: project.id, reader });
58
70
  emitResult({ project, entries: result.entries }, result.formatted, asJson);
59
71
  return;
60
72
  }
@@ -67,7 +79,7 @@ export default defineCommand({
67
79
  emitError('query is required for "memorix memory graph-context"', asJson);
68
80
  return;
69
81
  }
70
- const observations = getAllObservations();
82
+ const observations = filterReadableObservations(getAllObservations(), reader);
71
83
  const packet = buildGraphContextPacket(observations, {
72
84
  projectId: project.id,
73
85
  query,
@@ -92,7 +104,7 @@ export default defineCommand({
92
104
 
93
105
  case 'recent': {
94
106
  const limit = parsePositiveInt(args.limit as string | undefined, 10);
95
- const observations = getProjectObservations(project.id)
107
+ const observations = filterReadableObservations(getProjectObservations(project.id), reader)
96
108
  .filter((obs) => (obs.status ?? 'active') === 'active')
97
109
  .slice(-limit)
98
110
  .reverse();
@@ -118,6 +130,10 @@ export default defineCommand({
118
130
  (args.topicKey as string | undefined)?.trim() ||
119
131
  suggestTopicKey(type, title) ||
120
132
  undefined;
133
+ const writeScope = resolveCliWriteScope(
134
+ { reader, identity },
135
+ args.visibility as string | undefined,
136
+ );
121
137
  const result = await storeObservation({
122
138
  entityName: (args.entity as string | undefined)?.trim() || 'general',
123
139
  type,
@@ -129,6 +145,7 @@ export default defineCommand({
129
145
  projectId: project.id,
130
146
  topicKey,
131
147
  source: 'manual',
148
+ ...writeScope,
132
149
  });
133
150
  emitResult(
134
151
  { project, observation: result.observation, upserted: result.upserted },
@@ -166,7 +183,7 @@ export default defineCommand({
166
183
  ? `obs:${numericId}@${project.id}`
167
184
  : ref;
168
185
  });
169
- const result = await compactDetail(scopedRefs);
186
+ const result = await compactDetail(scopedRefs, { reader });
170
187
  emitResult({ project, documents: result.documents }, result.formatted, asJson);
171
188
  return;
172
189
  }
@@ -182,6 +199,7 @@ export default defineCommand({
182
199
  project.id,
183
200
  parsePositiveInt(args.before as string | undefined, 3),
184
201
  parsePositiveInt(args.after as string | undefined, 3),
202
+ reader,
185
203
  );
186
204
  emitResult({ project, timeline: result.timeline }, result.formatted, asJson);
187
205
  return;
@@ -196,7 +214,15 @@ export default defineCommand({
196
214
  return;
197
215
  }
198
216
  const status = coerceObservationStatus(args.status as string | undefined);
199
- const result = await resolveObservations(ids, status);
217
+ const authorizedIds = ids.filter((id) => {
218
+ const observation = getObservation(id, project.id);
219
+ return observation ? canManageObservation(observation, reader) : false;
220
+ });
221
+ if (authorizedIds.length === 0) {
222
+ emitError('No requested observations are manageable with the active CLI scope.', asJson);
223
+ return;
224
+ }
225
+ const result = await resolveObservations(authorizedIds, status);
200
226
  emitResult(
201
227
  { project, result, status },
202
228
  `Resolved ${result.resolved.length} observation(s) to ${status}${result.notFound.length > 0 ? `; not found: ${result.notFound.join(', ')}` : ''}`,
@@ -207,7 +233,7 @@ export default defineCommand({
207
233
 
208
234
  case 'deduplicate': {
209
235
  const query = (args.query as string | undefined)?.trim();
210
- const dryRun = !!args.dryRun;
236
+ const dryRun = !!args.dryRun || !!args['dry-run'];
211
237
  const { isLLMEnabled } = await import('../../llm/provider.js');
212
238
  if (!isLLMEnabled()) {
213
239
  emitResult(
@@ -220,7 +246,10 @@ export default defineCommand({
220
246
 
221
247
  const { deduplicateMemory } = await import('../../llm/memory-manager.js');
222
248
  const allObs = await withFreshIndex(() =>
223
- getAllObservations().filter((obs) => (obs.status ?? 'active') === 'active' && obs.projectId === project.id),
249
+ filterReadableObservations(
250
+ getAllObservations().filter((obs) => (obs.status ?? 'active') === 'active' && obs.projectId === project.id),
251
+ reader,
252
+ ),
224
253
  );
225
254
 
226
255
  if (allObs.length < 2) {
@@ -230,7 +259,7 @@ export default defineCommand({
230
259
 
231
260
  let candidates = allObs;
232
261
  if (query) {
233
- const searchResult = await compactSearch({ query, limit: 20, projectId: project.id, status: 'active' });
262
+ const searchResult = await compactSearch({ query, limit: 20, projectId: project.id, status: 'active', reader });
234
263
  const ids = new Set(searchResult.entries.map((entry) => entry.id));
235
264
  candidates = allObs.filter((obs) => ids.has(obs.id));
236
265
  } else {
@@ -277,7 +306,13 @@ export default defineCommand({
277
306
  return;
278
307
  }
279
308
 
280
- const result = await resolveObservations([...new Set(toResolve)], 'resolved');
309
+ const result = await resolveObservations(
310
+ [...new Set(toResolve)].filter((id) => {
311
+ const observation = getObservation(id, project.id);
312
+ return observation ? canManageObservation(observation, reader) : false;
313
+ }),
314
+ 'resolved',
315
+ );
281
316
  emitResult(
282
317
  { project, actions, resolved: result.resolved, notFound: result.notFound, dryRun: false },
283
318
  `Resolved ${result.resolved.length} duplicate observation(s).`,
@@ -292,7 +327,7 @@ export default defineCommand({
292
327
  const { findConsolidationCandidates, executeConsolidation } = await import('../../memory/consolidation.js');
293
328
 
294
329
  if (consolidationAction === 'preview') {
295
- const clusters = await findConsolidationCandidates(process.cwd(), project.id, { threshold });
330
+ const clusters = await findConsolidationCandidates(project.rootPath, project.id, { threshold });
296
331
  emitResult(
297
332
  { project, clusters, action: consolidationAction },
298
333
  clusters.length === 0
@@ -306,7 +341,7 @@ export default defineCommand({
306
341
  }
307
342
 
308
343
  if (consolidationAction === 'execute') {
309
- const result = await executeConsolidation(process.cwd(), project.id, { threshold });
344
+ const result = await executeConsolidation(project.rootPath, project.id, { threshold });
310
345
  emitResult(
311
346
  { project, action: consolidationAction, ...result },
312
347
  result.clustersFound === 0
@@ -325,11 +360,10 @@ export default defineCommand({
325
360
  const promoteAction = (args.action as string | undefined) || 'promote';
326
361
  const { promoteToMiniSkill, loadAllMiniSkills, deleteMiniSkill } = await import('../../skills/mini-skills.js');
327
362
  const { initMiniSkillStore } = await import('../../store/mini-skill-store.js');
328
- const { dataDir } = await getCliProjectContext();
329
363
  await initMiniSkillStore(dataDir);
330
364
 
331
365
  if (promoteAction === 'list') {
332
- const skills = await loadAllMiniSkills(process.cwd());
366
+ const skills = await loadAllMiniSkills(project.rootPath);
333
367
  emitResult(
334
368
  { project, skills, action: promoteAction },
335
369
  skills.length === 0 ? 'No mini-skills found.' : skills.map((skill) => `- #${skill.id} ${skill.title}`).join('\n'),
@@ -344,7 +378,7 @@ export default defineCommand({
344
378
  emitError('skillId is required for "memorix memory promote --action delete"', asJson);
345
379
  return;
346
380
  }
347
- const deleted = await deleteMiniSkill(process.cwd(), skillId);
381
+ const deleted = await deleteMiniSkill(project.rootPath, skillId);
348
382
  if (!deleted) {
349
383
  emitError(`Mini-skill #${skillId} not found`, asJson);
350
384
  return;
@@ -361,12 +395,15 @@ export default defineCommand({
361
395
  return;
362
396
  }
363
397
  const observations = await withFreshIndex(() => getAllObservations());
364
- const matched = observations.filter((obs) => ids.includes(obs.id));
398
+ const matched = filterReadableObservations(
399
+ observations.filter((obs) => obs.projectId === project.id && ids.includes(obs.id)),
400
+ reader,
401
+ ).filter((observation) => resolveObservationVisibility(observation) === 'project');
365
402
  if (matched.length === 0) {
366
- emitError(`No observations found for IDs: ${ids.join(', ')}`, asJson);
403
+ emitError(`No project-visible observations found for IDs: ${ids.join(', ')}. Private and team records cannot be promoted into shared skills.`, asJson);
367
404
  return;
368
405
  }
369
- const skill = await promoteToMiniSkill(process.cwd(), project.id, matched, {
406
+ const skill = await promoteToMiniSkill(project.rootPath, project.id, matched, {
370
407
  trigger: args.trigger as string | undefined,
371
408
  instruction: args.instruction as string | undefined,
372
409
  tags: parseCsvList(args.tags as string | undefined),
@@ -385,7 +422,7 @@ export default defineCommand({
385
422
  console.log('Usage:');
386
423
  console.log(' memorix memory search --query "timeout bug" [--limit 10]');
387
424
  console.log(' memorix memory recent [--limit 10]');
388
- console.log(' memorix memory store --text "..." [--title "..."] [--type discovery]');
425
+ console.log(' memorix memory store --text "..." [--title "..."] [--type discovery] [--visibility project|personal|team]');
389
426
  console.log(' memorix memory suggest-topic-key --type decision --title "..."');
390
427
  console.log(' memorix memory detail --id 42');
391
428
  console.log(' memorix memory detail obs:42@org/project');
@@ -1,5 +1,5 @@
1
1
  import { defineCommand } from 'citty';
2
- import { emitError, emitResult, getCliProjectContext, shortId } from './operator-shared.js';
2
+ import { emitError, emitResult, getCliProjectContext, resolveCliActorId, shortId } from './operator-shared.js';
3
3
 
4
4
  export default defineCommand({
5
5
  meta: {
@@ -22,12 +22,18 @@ export default defineCommand({
22
22
  const asJson = !!args.json;
23
23
 
24
24
  try {
25
- const { project, teamStore } = await getCliProjectContext();
25
+ const { project, teamStore, identity } = await getCliProjectContext();
26
+ const senderAgentId = resolveCliActorId(args.from, identity, 'from');
27
+ const inboxAgentId = resolveCliActorId(
28
+ (args.agentId as string | undefined) ?? (args.from as string | undefined),
29
+ identity,
30
+ 'agentId',
31
+ );
26
32
 
27
33
  switch (action) {
28
34
  case 'send': {
29
- if (!args.from || !args.type || !args.content) {
30
- emitError('from, type, and content are required for "memorix message send"', asJson);
35
+ if (!senderAgentId || !args.type || !args.content) {
36
+ emitError('type, content, and an active CLI identity or from agentId are required for "memorix message send"', asJson);
31
37
  return;
32
38
  }
33
39
  if (!args.to && !args.toRole) {
@@ -36,7 +42,7 @@ export default defineCommand({
36
42
  }
37
43
  const message = teamStore.sendMessage({
38
44
  projectId: project.id,
39
- senderAgentId: args.from as string,
45
+ senderAgentId,
40
46
  recipientAgentId: (args.to as string | undefined) ?? null,
41
47
  type: args.type as string,
42
48
  content: args.content as string,
@@ -58,13 +64,13 @@ export default defineCommand({
58
64
  }
59
65
 
60
66
  case 'broadcast': {
61
- if (!args.from || !args.type || !args.content) {
62
- emitError('from, type, and content are required for "memorix message broadcast"', asJson);
67
+ if (!senderAgentId || !args.type || !args.content) {
68
+ emitError('type, content, and an active CLI identity or from agentId are required for "memorix message broadcast"', asJson);
63
69
  return;
64
70
  }
65
71
  const message = teamStore.sendMessage({
66
72
  projectId: project.id,
67
- senderAgentId: args.from as string,
73
+ senderAgentId,
68
74
  recipientAgentId: null,
69
75
  type: args.type as string,
70
76
  content: args.content as string,
@@ -82,15 +88,14 @@ export default defineCommand({
82
88
  }
83
89
 
84
90
  case 'inbox': {
85
- const agentId = (args.agentId as string | undefined) || (args.from as string | undefined);
86
- if (!agentId) {
87
- emitError('agentId is required for "memorix message inbox"', asJson);
91
+ if (!inboxAgentId) {
92
+ emitError('an active CLI identity or agentId is required for "memorix message inbox"', asJson);
88
93
  return;
89
94
  }
90
- const messages = teamStore.getInbox(project.id, agentId);
91
- const unreadCount = teamStore.getUnreadCount(project.id, agentId);
95
+ const messages = teamStore.getInbox(project.id, inboxAgentId);
96
+ const unreadCount = teamStore.getUnreadCount(project.id, inboxAgentId);
92
97
  if (args.markRead) {
93
- teamStore.markAllRead(project.id, agentId);
98
+ teamStore.markAllRead(project.id, inboxAgentId);
94
99
  }
95
100
  emitResult(
96
101
  { project, unreadCount, messages },