memorix 1.2.1 → 1.2.2

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 (199) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +14 -2
  3. package/README.zh-CN.md +14 -2
  4. package/TEAM.md +86 -86
  5. package/dist/cli/index.js +15407 -13779
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/index.js +1321 -529
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.d.ts +1 -1
  10. package/dist/maintenance-runner.js +8458 -8087
  11. package/dist/maintenance-runner.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +16 -0
  13. package/dist/sdk.d.ts +7 -2
  14. package/dist/sdk.js +1349 -535
  15. package/dist/sdk.js.map +1 -1
  16. package/dist/types.d.ts +49 -1
  17. package/dist/types.js.map +1 -1
  18. package/docs/1.2.2-MEMORY-CONTROL-PLANE.md +434 -0
  19. package/docs/AGENT_OPERATOR_PLAYBOOK.md +4 -0
  20. package/docs/API_REFERENCE.md +24 -4
  21. package/docs/DESIGN_DECISIONS.md +357 -357
  22. package/docs/README.md +1 -1
  23. package/docs/dev-log/progress.txt +91 -11
  24. package/package.json +1 -1
  25. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  26. package/src/audit/index.ts +156 -156
  27. package/src/cli/command-guide.ts +192 -0
  28. package/src/cli/commands/audit-list.ts +89 -89
  29. package/src/cli/commands/audit.ts +9 -4
  30. package/src/cli/commands/background.ts +659 -659
  31. package/src/cli/commands/cleanup.ts +5 -1
  32. package/src/cli/commands/codegraph.ts +15 -5
  33. package/src/cli/commands/context.ts +3 -2
  34. package/src/cli/commands/doctor.ts +4 -2
  35. package/src/cli/commands/explain.ts +9 -3
  36. package/src/cli/commands/formation.ts +48 -48
  37. package/src/cli/commands/git-hook-install.ts +111 -111
  38. package/src/cli/commands/handoff.ts +75 -61
  39. package/src/cli/commands/hooks-status.ts +63 -63
  40. package/src/cli/commands/identity.ts +116 -0
  41. package/src/cli/commands/ingest-commit.ts +153 -153
  42. package/src/cli/commands/ingest-image.ts +71 -69
  43. package/src/cli/commands/ingest-log.ts +180 -180
  44. package/src/cli/commands/ingest.ts +44 -44
  45. package/src/cli/commands/integrate-shared.ts +15 -15
  46. package/src/cli/commands/lock.ts +93 -92
  47. package/src/cli/commands/memory.ts +58 -21
  48. package/src/cli/commands/message.ts +123 -118
  49. package/src/cli/commands/operator-shared.ts +98 -3
  50. package/src/cli/commands/poll.ts +74 -64
  51. package/src/cli/commands/purge-all-memory.ts +85 -85
  52. package/src/cli/commands/purge-project-memory.ts +83 -83
  53. package/src/cli/commands/reasoning.ts +135 -121
  54. package/src/cli/commands/retention.ts +9 -4
  55. package/src/cli/commands/serve-http.ts +8 -2
  56. package/src/cli/commands/serve-shared.ts +118 -118
  57. package/src/cli/commands/session.ts +29 -3
  58. package/src/cli/commands/skills.ts +124 -119
  59. package/src/cli/commands/status.ts +4 -3
  60. package/src/cli/commands/task.ts +193 -184
  61. package/src/cli/commands/team.ts +14 -10
  62. package/src/cli/commands/transfer.ts +108 -55
  63. package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
  64. package/src/cli/identity.ts +89 -0
  65. package/src/cli/index.ts +96 -19
  66. package/src/cli/invocation.ts +115 -0
  67. package/src/cli/tui/ChatView.tsx +234 -234
  68. package/src/cli/tui/CommandBar.tsx +312 -312
  69. package/src/cli/tui/ContextRail.tsx +118 -118
  70. package/src/cli/tui/HeaderBar.tsx +72 -72
  71. package/src/cli/tui/LogoBanner.tsx +51 -51
  72. package/src/cli/tui/Sidebar.tsx +179 -179
  73. package/src/cli/tui/chat-service.ts +41 -18
  74. package/src/cli/tui/data.ts +23 -44
  75. package/src/cli/tui/index.ts +41 -41
  76. package/src/cli/tui/markdown-render.tsx +371 -371
  77. package/src/cli/tui/operator-context.ts +60 -0
  78. package/src/cli/tui/use-mouse.ts +157 -157
  79. package/src/cli/tui/useNavigation.ts +56 -56
  80. package/src/cli/tui/views/MemoryView.tsx +10 -8
  81. package/src/cli/update-checker.ts +211 -211
  82. package/src/cli/version.ts +7 -7
  83. package/src/cli/workbench.ts +1 -1
  84. package/src/codegraph/auto-context.ts +31 -2
  85. package/src/codegraph/context-pack.ts +1 -0
  86. package/src/codegraph/project-context.ts +2 -0
  87. package/src/compact/engine.ts +26 -10
  88. package/src/compact/index-format.ts +25 -2
  89. package/src/compact/token-budget.ts +74 -74
  90. package/src/dashboard/project-classification.ts +64 -64
  91. package/src/dashboard/server.ts +46 -9
  92. package/src/embedding/fastembed-provider.ts +142 -142
  93. package/src/embedding/transformers-provider.ts +111 -111
  94. package/src/git/extractor.ts +209 -209
  95. package/src/git/hooks-path.ts +85 -85
  96. package/src/hooks/admission.ts +117 -0
  97. package/src/hooks/handler.ts +98 -91
  98. package/src/hooks/pattern-detector.ts +173 -173
  99. package/src/hooks/significance-filter.ts +250 -250
  100. package/src/knowledge/context-assembly.ts +97 -0
  101. package/src/knowledge/workset.ts +179 -10
  102. package/src/llm/memory-manager.ts +328 -328
  103. package/src/llm/provider.ts +885 -885
  104. package/src/llm/quality.ts +248 -248
  105. package/src/memory/admission.ts +57 -0
  106. package/src/memory/attribution-guard.ts +249 -249
  107. package/src/memory/consolidation.ts +13 -2
  108. package/src/memory/disclosure-policy.ts +140 -135
  109. package/src/memory/entity-extractor.ts +197 -197
  110. package/src/memory/export-import.ts +11 -3
  111. package/src/memory/formation/evaluate.ts +217 -217
  112. package/src/memory/formation/extract.ts +361 -361
  113. package/src/memory/formation/index.ts +417 -417
  114. package/src/memory/formation/resolve.ts +344 -344
  115. package/src/memory/formation/types.ts +315 -315
  116. package/src/memory/freshness.ts +122 -122
  117. package/src/memory/graph-context.ts +8 -2
  118. package/src/memory/graph.ts +197 -197
  119. package/src/memory/observations.ts +162 -4
  120. package/src/memory/quality-audit.ts +2 -0
  121. package/src/memory/refs.ts +94 -94
  122. package/src/memory/retention.ts +22 -2
  123. package/src/memory/secret-filter.ts +79 -79
  124. package/src/memory/session.ts +5 -2
  125. package/src/memory/visibility.ts +80 -0
  126. package/src/multimodal/image-loader.ts +143 -143
  127. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  128. package/src/orchestrate/adapters/claude.ts +111 -111
  129. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  130. package/src/orchestrate/adapters/codex.ts +41 -41
  131. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  132. package/src/orchestrate/adapters/gemini.ts +42 -42
  133. package/src/orchestrate/adapters/index.ts +73 -73
  134. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  135. package/src/orchestrate/adapters/opencode.ts +47 -47
  136. package/src/orchestrate/adapters/spawn-helper.ts +286 -286
  137. package/src/orchestrate/adapters/types.ts +77 -77
  138. package/src/orchestrate/capability-router.ts +284 -284
  139. package/src/orchestrate/context-compact.ts +188 -188
  140. package/src/orchestrate/cost-tracker.ts +219 -219
  141. package/src/orchestrate/error-recovery.ts +191 -191
  142. package/src/orchestrate/evidence.ts +140 -140
  143. package/src/orchestrate/ledger.ts +110 -110
  144. package/src/orchestrate/memorix-bridge.ts +378 -340
  145. package/src/orchestrate/output-budget.ts +80 -80
  146. package/src/orchestrate/permission.ts +152 -152
  147. package/src/orchestrate/pipeline-trace.ts +131 -131
  148. package/src/orchestrate/prompt-builder.ts +155 -155
  149. package/src/orchestrate/ring-buffer.ts +37 -37
  150. package/src/orchestrate/task-graph.ts +389 -389
  151. package/src/orchestrate/worktree.ts +232 -232
  152. package/src/project/aliases.ts +374 -374
  153. package/src/project/detector.ts +268 -268
  154. package/src/rules/adapters/claude-code.ts +99 -99
  155. package/src/rules/adapters/codex.ts +97 -97
  156. package/src/rules/adapters/copilot.ts +124 -124
  157. package/src/rules/adapters/cursor.ts +114 -114
  158. package/src/rules/adapters/kiro.ts +126 -126
  159. package/src/rules/adapters/trae.ts +56 -56
  160. package/src/rules/adapters/windsurf.ts +83 -83
  161. package/src/rules/syncer.ts +235 -235
  162. package/src/runtime/control-plane-maintenance.ts +1 -0
  163. package/src/runtime/isolated-maintenance.ts +1 -0
  164. package/src/runtime/lifecycle.ts +18 -0
  165. package/src/runtime/maintenance-jobs.ts +1 -0
  166. package/src/runtime/maintenance-runner.ts +2 -0
  167. package/src/runtime/project-maintenance.ts +89 -0
  168. package/src/sdk.ts +334 -304
  169. package/src/search/intent-detector.ts +289 -289
  170. package/src/search/query-expansion.ts +52 -52
  171. package/src/server/formation-timeout.ts +27 -27
  172. package/src/server.ts +260 -81
  173. package/src/skills/mini-skills.ts +386 -386
  174. package/src/store/chat-store.ts +119 -119
  175. package/src/store/graph-store.ts +249 -249
  176. package/src/store/mini-skill-store.ts +349 -349
  177. package/src/store/orama-store.ts +61 -6
  178. package/src/store/persistence-json.ts +212 -212
  179. package/src/store/persistence.ts +291 -291
  180. package/src/store/project-affinity.ts +195 -195
  181. package/src/store/sqlite-db.ts +23 -1
  182. package/src/store/sqlite-store.ts +12 -2
  183. package/src/team/event-bus.ts +76 -76
  184. package/src/team/file-locks.ts +173 -173
  185. package/src/team/handoff.ts +168 -161
  186. package/src/team/messages.ts +203 -203
  187. package/src/team/poll.ts +132 -132
  188. package/src/team/tasks.ts +211 -211
  189. package/src/types.ts +51 -0
  190. package/src/wiki/generator.ts +2 -0
  191. package/src/workspace/mcp-adapters/codex.ts +191 -191
  192. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  193. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  194. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  195. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  196. package/src/workspace/mcp-adapters/trae.ts +134 -134
  197. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  198. package/src/workspace/sanitizer.ts +60 -60
  199. package/src/workspace/workflow-sync.ts +131 -131
@@ -1,179 +1,179 @@
1
- /**
2
- * Sidebar right panel with quick action hints and health snapshot.
3
- *
4
- * Modern design: Unicode separators, status dots, active indicator
5
- * with arrow symbol, brand-colored section headers.
6
- */
7
-
8
- import React from 'react';
9
- import { Box, Text, useInput } from 'ink';
10
- import { COLORS, SEP, STATUS_DOTS, SYMBOLS } from './theme.js';
11
- import type { HealthInfo, BackgroundInfo } from './data.js';
12
- import type { ViewType } from './theme.js';
13
-
14
- interface SidebarProps {
15
- health: HealthInfo;
16
- background: BackgroundInfo;
17
- onAction: (cmd: string) => void;
18
- activeView: ViewType;
19
- /** When true, Sidebar captures shortcut keys and drives navigation. */
20
- isFocused?: boolean;
21
- }
22
-
23
- const ACTIONS = [
24
- { key: 's', label: 'Search', cmd: '/search' },
25
- { key: 'r', label: 'Remember', cmd: '/remember' },
26
- { key: 'v', label: 'Recent', cmd: '/recent' },
27
- { key: 'd', label: 'Doctor', cmd: '/doctor' },
28
- { key: 'b', label: 'Background', cmd: '/background' },
29
- { key: 'w', label: 'Dashboard', cmd: '/dashboard' },
30
- { key: 'p', label: 'Project', cmd: '/project' },
31
- { key: 'c', label: 'Configure', cmd: '/configure' },
32
- { key: 'i', label: 'Integrate', cmd: '/integrate' },
33
- { key: 'h', label: 'Home', cmd: '/home' },
34
- ];
35
-
36
- const SIDEBAR_WIDTH = 32;
37
- const INNER_WIDTH = SIDEBAR_WIDTH - 4;
38
- const ACTION_LABEL_WIDTH = INNER_WIDTH - 2;
39
- const VALUE_WIDTH = INNER_WIDTH - 8;
40
-
41
- function separator(width: number): string {
42
- return SEP.thin.repeat(width);
43
- }
44
-
45
- function colorForMode(mode: string): string {
46
- const normalized = mode.toLowerCase();
47
- if (normalized.includes('hybrid')) return COLORS.success;
48
- if (normalized.includes('vector')) return COLORS.accent;
49
- return COLORS.warning;
50
- }
51
-
52
- function charWidth(ch: string): number {
53
- const cp = ch.codePointAt(0)!;
54
- return ((cp >= 0x4E00 && cp <= 0x9FFF) || (cp >= 0x3040 && cp <= 0x30FF) ||
55
- (cp >= 0xAC00 && cp <= 0xD7AF) || (cp >= 0xFF01 && cp <= 0xFF60) ||
56
- (cp >= 0x3400 && cp <= 0x4DBF)) ? 2 : 1;
57
- }
58
-
59
- function fit(text: string, max: number): string {
60
- if (max <= 0) return '';
61
- let width = 0;
62
- for (const ch of text) {
63
- width += charWidth(ch);
64
- if (width > max) break;
65
- }
66
- if (width <= max) return text.padEnd(text.length + (max - width));
67
- if (max === 1) return '…';
68
- let result = '';
69
- let used = 0;
70
- for (const ch of text) {
71
- const cw = charWidth(ch);
72
- if (used + cw > max - 1) break;
73
- result += ch;
74
- used += cw;
75
- }
76
- const truncated = `${result}…`;
77
- return truncated.padEnd(truncated.length + Math.max(0, max - (used + 1)));
78
- }
79
-
80
- // Build a key→cmd lookup from ACTIONS for O(1) dispatch
81
- const KEY_TO_CMD: Record<string, string> = {};
82
- for (const a of ACTIONS) KEY_TO_CMD[a.key] = a.cmd;
83
-
84
- export function Sidebar({ health, background, onAction, activeView, isFocused = false }: SidebarProps): React.ReactElement {
85
- // ── Interactive navigation: Sidebar owns shortcut key dispatch ──
86
- useInput((ch, key) => {
87
- // Esc: return home from any secondary view
88
- if (key.escape && activeView !== 'home') {
89
- onAction('/home');
90
- return;
91
- }
92
- const cmd = KEY_TO_CMD[ch];
93
- if (cmd) {
94
- onAction(cmd);
95
- }
96
- }, { isActive: isFocused });
97
-
98
- // Map view types to sidebar action commands for highlight
99
- const activeCmd = ACTIONS.find(a => {
100
- const viewMap: Record<string, string> = {
101
- '/search': 'search', '/recent': 'recent', '/doctor': 'doctor',
102
- '/background': 'background', '/dashboard': 'dashboard',
103
- '/project': 'project', '/configure': 'configure',
104
- '/integrate': 'integrate', '/home': 'home',
105
- };
106
- return viewMap[a.cmd] === activeView;
107
- })?.cmd;
108
-
109
- return (
110
- <Box
111
- flexDirection="column"
112
- width={SIDEBAR_WIDTH}
113
- borderStyle="single"
114
- borderColor={COLORS.border}
115
- paddingX={1}
116
- >
117
- <Box flexDirection="column" marginBottom={1}>
118
- <Text color={COLORS.brand} bold>Quick Actions</Text>
119
- <Text color={COLORS.border}>{separator(INNER_WIDTH)}</Text>
120
- {ACTIONS.map((action) => {
121
- const isActive = action.cmd === activeCmd;
122
- return (
123
- <Box key={action.key}>
124
- <Text color={isActive ? COLORS.brand : COLORS.muted}>
125
- {`${isActive ? SYMBOLS.arrow : action.key} `}
126
- </Text>
127
- <Text color={isActive ? COLORS.accent : COLORS.text} bold={isActive}>{fit(action.label, ACTION_LABEL_WIDTH)}</Text>
128
- </Box>
129
- );
130
- })}
131
- </Box>
132
-
133
- <Box flexDirection="column">
134
- <Text color={COLORS.brand} bold>Health</Text>
135
- <Text color={COLORS.border}>{separator(INNER_WIDTH)}</Text>
136
-
137
- <Box>
138
- <Text color={COLORS.muted}>{'Embed'.padEnd(8)}</Text>
139
- <Text color={
140
- health.embeddingProvider === 'ready' ? COLORS.success
141
- : health.embeddingProvider === 'unavailable' ? COLORS.warning
142
- : COLORS.muted
143
- }>
144
- {fit(`${health.embeddingProvider === 'ready' ? STATUS_DOTS.ok : health.embeddingProvider === 'unavailable' ? STATUS_DOTS.warn : STATUS_DOTS.off} ${health.embeddingLabel}`, VALUE_WIDTH)}
145
- </Text>
146
- </Box>
147
- <Box>
148
- <Text color={COLORS.muted}>{'Search'.padEnd(8)}</Text>
149
- <Text color={colorForMode(health.searchModeLabel)}>{fit(`${STATUS_DOTS.ok} ${health.searchModeLabel}`, VALUE_WIDTH)}</Text>
150
- </Box>
151
- <Box>
152
- <Text color={COLORS.muted}>{'Sess'.padEnd(8)}</Text>
153
- <Text color={COLORS.text}>{fit(`${health.sessions}`, VALUE_WIDTH)}</Text>
154
- </Box>
155
-
156
- <Box marginTop={1} flexDirection="column">
157
- <Text color={COLORS.brand} bold>Background</Text>
158
- <Text color={COLORS.border}>{separator(INNER_WIDTH)}</Text>
159
- <Box>
160
- <Text color={COLORS.muted}>{'Status'.padEnd(8)}</Text>
161
- <Text color={background.healthy ? COLORS.success : background.running ? COLORS.warning : COLORS.muted}>
162
- {fit(`${background.healthy ? STATUS_DOTS.running : background.running ? STATUS_DOTS.warn : STATUS_DOTS.stopped} ${background.healthy ? 'Running' : background.running ? 'Warning' : 'Stopped'}`, VALUE_WIDTH)}
163
- </Text>
164
- </Box>
165
- {background.port && (
166
- <Box>
167
- <Text color={COLORS.muted}>{'Port'.padEnd(8)}</Text>
168
- <Text color={COLORS.text}>{fit(`${background.port}`, VALUE_WIDTH)}</Text>
169
- </Box>
170
- )}
171
- </Box>
172
- </Box>
173
-
174
- <Box marginTop={1}>
175
- <Text color={COLORS.muted} italic>Keys or /cmd</Text>
176
- </Box>
177
- </Box>
178
- );
179
- }
1
+ /**
2
+ * Sidebar right panel with quick action hints and health snapshot.
3
+ *
4
+ * Modern design: Unicode separators, status dots, active indicator
5
+ * with arrow symbol, brand-colored section headers.
6
+ */
7
+
8
+ import React from 'react';
9
+ import { Box, Text, useInput } from 'ink';
10
+ import { COLORS, SEP, STATUS_DOTS, SYMBOLS } from './theme.js';
11
+ import type { HealthInfo, BackgroundInfo } from './data.js';
12
+ import type { ViewType } from './theme.js';
13
+
14
+ interface SidebarProps {
15
+ health: HealthInfo;
16
+ background: BackgroundInfo;
17
+ onAction: (cmd: string) => void;
18
+ activeView: ViewType;
19
+ /** When true, Sidebar captures shortcut keys and drives navigation. */
20
+ isFocused?: boolean;
21
+ }
22
+
23
+ const ACTIONS = [
24
+ { key: 's', label: 'Search', cmd: '/search' },
25
+ { key: 'r', label: 'Remember', cmd: '/remember' },
26
+ { key: 'v', label: 'Recent', cmd: '/recent' },
27
+ { key: 'd', label: 'Doctor', cmd: '/doctor' },
28
+ { key: 'b', label: 'Background', cmd: '/background' },
29
+ { key: 'w', label: 'Dashboard', cmd: '/dashboard' },
30
+ { key: 'p', label: 'Project', cmd: '/project' },
31
+ { key: 'c', label: 'Configure', cmd: '/configure' },
32
+ { key: 'i', label: 'Integrate', cmd: '/integrate' },
33
+ { key: 'h', label: 'Home', cmd: '/home' },
34
+ ];
35
+
36
+ const SIDEBAR_WIDTH = 32;
37
+ const INNER_WIDTH = SIDEBAR_WIDTH - 4;
38
+ const ACTION_LABEL_WIDTH = INNER_WIDTH - 2;
39
+ const VALUE_WIDTH = INNER_WIDTH - 8;
40
+
41
+ function separator(width: number): string {
42
+ return SEP.thin.repeat(width);
43
+ }
44
+
45
+ function colorForMode(mode: string): string {
46
+ const normalized = mode.toLowerCase();
47
+ if (normalized.includes('hybrid')) return COLORS.success;
48
+ if (normalized.includes('vector')) return COLORS.accent;
49
+ return COLORS.warning;
50
+ }
51
+
52
+ function charWidth(ch: string): number {
53
+ const cp = ch.codePointAt(0)!;
54
+ return ((cp >= 0x4E00 && cp <= 0x9FFF) || (cp >= 0x3040 && cp <= 0x30FF) ||
55
+ (cp >= 0xAC00 && cp <= 0xD7AF) || (cp >= 0xFF01 && cp <= 0xFF60) ||
56
+ (cp >= 0x3400 && cp <= 0x4DBF)) ? 2 : 1;
57
+ }
58
+
59
+ function fit(text: string, max: number): string {
60
+ if (max <= 0) return '';
61
+ let width = 0;
62
+ for (const ch of text) {
63
+ width += charWidth(ch);
64
+ if (width > max) break;
65
+ }
66
+ if (width <= max) return text.padEnd(text.length + (max - width));
67
+ if (max === 1) return '…';
68
+ let result = '';
69
+ let used = 0;
70
+ for (const ch of text) {
71
+ const cw = charWidth(ch);
72
+ if (used + cw > max - 1) break;
73
+ result += ch;
74
+ used += cw;
75
+ }
76
+ const truncated = `${result}…`;
77
+ return truncated.padEnd(truncated.length + Math.max(0, max - (used + 1)));
78
+ }
79
+
80
+ // Build a key→cmd lookup from ACTIONS for O(1) dispatch
81
+ const KEY_TO_CMD: Record<string, string> = {};
82
+ for (const a of ACTIONS) KEY_TO_CMD[a.key] = a.cmd;
83
+
84
+ export function Sidebar({ health, background, onAction, activeView, isFocused = false }: SidebarProps): React.ReactElement {
85
+ // ── Interactive navigation: Sidebar owns shortcut key dispatch ──
86
+ useInput((ch, key) => {
87
+ // Esc: return home from any secondary view
88
+ if (key.escape && activeView !== 'home') {
89
+ onAction('/home');
90
+ return;
91
+ }
92
+ const cmd = KEY_TO_CMD[ch];
93
+ if (cmd) {
94
+ onAction(cmd);
95
+ }
96
+ }, { isActive: isFocused });
97
+
98
+ // Map view types to sidebar action commands for highlight
99
+ const activeCmd = ACTIONS.find(a => {
100
+ const viewMap: Record<string, string> = {
101
+ '/search': 'search', '/recent': 'recent', '/doctor': 'doctor',
102
+ '/background': 'background', '/dashboard': 'dashboard',
103
+ '/project': 'project', '/configure': 'configure',
104
+ '/integrate': 'integrate', '/home': 'home',
105
+ };
106
+ return viewMap[a.cmd] === activeView;
107
+ })?.cmd;
108
+
109
+ return (
110
+ <Box
111
+ flexDirection="column"
112
+ width={SIDEBAR_WIDTH}
113
+ borderStyle="single"
114
+ borderColor={COLORS.border}
115
+ paddingX={1}
116
+ >
117
+ <Box flexDirection="column" marginBottom={1}>
118
+ <Text color={COLORS.brand} bold>Quick Actions</Text>
119
+ <Text color={COLORS.border}>{separator(INNER_WIDTH)}</Text>
120
+ {ACTIONS.map((action) => {
121
+ const isActive = action.cmd === activeCmd;
122
+ return (
123
+ <Box key={action.key}>
124
+ <Text color={isActive ? COLORS.brand : COLORS.muted}>
125
+ {`${isActive ? SYMBOLS.arrow : action.key} `}
126
+ </Text>
127
+ <Text color={isActive ? COLORS.accent : COLORS.text} bold={isActive}>{fit(action.label, ACTION_LABEL_WIDTH)}</Text>
128
+ </Box>
129
+ );
130
+ })}
131
+ </Box>
132
+
133
+ <Box flexDirection="column">
134
+ <Text color={COLORS.brand} bold>Health</Text>
135
+ <Text color={COLORS.border}>{separator(INNER_WIDTH)}</Text>
136
+
137
+ <Box>
138
+ <Text color={COLORS.muted}>{'Embed'.padEnd(8)}</Text>
139
+ <Text color={
140
+ health.embeddingProvider === 'ready' ? COLORS.success
141
+ : health.embeddingProvider === 'unavailable' ? COLORS.warning
142
+ : COLORS.muted
143
+ }>
144
+ {fit(`${health.embeddingProvider === 'ready' ? STATUS_DOTS.ok : health.embeddingProvider === 'unavailable' ? STATUS_DOTS.warn : STATUS_DOTS.off} ${health.embeddingLabel}`, VALUE_WIDTH)}
145
+ </Text>
146
+ </Box>
147
+ <Box>
148
+ <Text color={COLORS.muted}>{'Search'.padEnd(8)}</Text>
149
+ <Text color={colorForMode(health.searchModeLabel)}>{fit(`${STATUS_DOTS.ok} ${health.searchModeLabel}`, VALUE_WIDTH)}</Text>
150
+ </Box>
151
+ <Box>
152
+ <Text color={COLORS.muted}>{'Sess'.padEnd(8)}</Text>
153
+ <Text color={COLORS.text}>{fit(`${health.sessions}`, VALUE_WIDTH)}</Text>
154
+ </Box>
155
+
156
+ <Box marginTop={1} flexDirection="column">
157
+ <Text color={COLORS.brand} bold>Background</Text>
158
+ <Text color={COLORS.border}>{separator(INNER_WIDTH)}</Text>
159
+ <Box>
160
+ <Text color={COLORS.muted}>{'Status'.padEnd(8)}</Text>
161
+ <Text color={background.healthy ? COLORS.success : background.running ? COLORS.warning : COLORS.muted}>
162
+ {fit(`${background.healthy ? STATUS_DOTS.running : background.running ? STATUS_DOTS.warn : STATUS_DOTS.stopped} ${background.healthy ? 'Running' : background.running ? 'Warning' : 'Stopped'}`, VALUE_WIDTH)}
163
+ </Text>
164
+ </Box>
165
+ {background.port && (
166
+ <Box>
167
+ <Text color={COLORS.muted}>{'Port'.padEnd(8)}</Text>
168
+ <Text color={COLORS.text}>{fit(`${background.port}`, VALUE_WIDTH)}</Text>
169
+ </Box>
170
+ )}
171
+ </Box>
172
+ </Box>
173
+
174
+ <Box marginTop={1}>
175
+ <Text color={COLORS.muted} italic>Keys or /cmd</Text>
176
+ </Box>
177
+ </Box>
178
+ );
179
+ }
@@ -3,11 +3,11 @@ import { loadDotenv } from '../../config/dotenv-loader.js';
3
3
  import { initLLM, isLLMEnabled, getLLMConfig, callLLMWithTools } from '../../llm/provider.js';
4
4
  import type { ChatMessage, ToolDefinition, ToolCall } from '../../llm/provider.js';
5
5
  import { initObservations, prepareSearchIndex, storeObservation, resolveObservations, getObservation, getAllObservations } from '../../memory/observations.js';
6
- import type { ObservationType } from '../../types.js';
7
- import { detectProject } from '../../project/detector.js';
6
+ import { canManageObservation, filterReadableObservations } from '../../memory/visibility.js';
7
+ import type { ObservationReader, ObservationType } from '../../types.js';
8
8
  import { getLastSearchMode } from '../../store/orama-store.js';
9
- import { getProjectDataDir } from '../../store/persistence.js';
10
9
  import type { MemorixDocument } from '../../types.js';
10
+ import { getTuiOperatorContext } from './operator-context.js';
11
11
 
12
12
  export interface ChatHistoryTurn {
13
13
  role: 'user' | 'assistant';
@@ -208,19 +208,29 @@ async function prepareProjectSearch(projectId: string, dataDir: string): Promise
208
208
 
209
209
  interface ToolExecutionContext {
210
210
  projectId: string;
211
+ reader: ObservationReader;
212
+ writerAgentId?: string;
211
213
  collectedSources: ChatSource[];
212
214
  }
213
215
 
216
+ async function resolveTuiContext() {
217
+ try {
218
+ return await getTuiOperatorContext();
219
+ } catch {
220
+ return null;
221
+ }
222
+ }
223
+
214
224
  function executeSearchMemories(args: { query: string; limit?: number }, ctx: ToolExecutionContext): Promise<string> {
215
225
  const limit = Math.min(args.limit ?? SEARCH_LIMIT, 10);
216
- return compactSearch({ query: args.query, limit, projectId: ctx.projectId, status: 'active' })
226
+ return compactSearch({ query: args.query, limit, projectId: ctx.projectId, status: 'active', reader: ctx.reader })
217
227
  .then((result) => {
218
228
  const entries = result.entries.slice(0, DETAIL_LIMIT);
219
229
  if (entries.length === 0) return 'No memories found for that query.';
220
230
 
221
231
  // Collect sources for citation tracking
222
232
  const refs = entries.map((e) => ({ id: e.id, projectId: ctx.projectId }));
223
- return compactDetail(refs).then((detail) => {
233
+ return compactDetail(refs, { reader: ctx.reader }).then((detail) => {
224
234
  for (let i = 0; i < detail.documents.length; i++) {
225
235
  const doc = detail.documents[i];
226
236
  const score = entries[i]?.score ?? 0;
@@ -235,7 +245,7 @@ function executeSearchMemories(args: { query: string; limit?: number }, ctx: Too
235
245
  }
236
246
 
237
247
  function executeGetMemoryDetail(args: { id: number }, ctx: ToolExecutionContext): Promise<string> {
238
- return compactDetail([{ id: args.id, projectId: ctx.projectId }])
248
+ return compactDetail([{ id: args.id, projectId: ctx.projectId }], { reader: ctx.reader })
239
249
  .then((detail) => {
240
250
  if (detail.documents.length === 0) return `Observation ${args.id} not found.`;
241
251
  const doc = detail.documents[0];
@@ -295,6 +305,8 @@ function executeStoreMemory(
295
305
  facts: args.facts,
296
306
  projectId: ctx.projectId,
297
307
  source: 'agent',
308
+ visibilityReader: ctx.reader,
309
+ ...(ctx.writerAgentId ? { createdByAgentId: ctx.writerAgentId } : {}),
298
310
  }).then((result) => {
299
311
  const obs = result.observation;
300
312
  ctx.collectedSources.push({
@@ -314,7 +326,7 @@ function executeUpdateMemory(
314
326
  ctx: ToolExecutionContext,
315
327
  ): string {
316
328
  const obs = getObservation(args.id, ctx.projectId);
317
- if (!obs) return `Observation ${args.id} not found.`;
329
+ if (!obs || !canManageObservation(obs, ctx.reader)) return `Observation ${args.id} not found.`;
318
330
 
319
331
  const shouldAppend = args.append !== false; // default true
320
332
  const newNarrative = shouldAppend
@@ -334,6 +346,7 @@ function executeUpdateMemory(
334
346
  projectId: ctx.projectId,
335
347
  topicKey: obs.topicKey,
336
348
  source: 'agent',
349
+ visibilityReader: ctx.reader,
337
350
  }).catch(() => {/* non-blocking */});
338
351
 
339
352
  return `Updated observation [obs:${args.id}] — ${shouldAppend ? 'appended to' : 'replaced'} narrative${args.facts?.length ? ` and added ${args.facts.length} facts` : ''}. Changes will be persisted asynchronously.`;
@@ -344,7 +357,7 @@ function executeDeleteMemory(
344
357
  ctx: ToolExecutionContext,
345
358
  ): Promise<string> {
346
359
  const obs = getObservation(args.id, ctx.projectId);
347
- if (!obs) return Promise.resolve(`Observation ${args.id} not found.`);
360
+ if (!obs || !canManageObservation(obs, ctx.reader)) return Promise.resolve(`Observation ${args.id} not found.`);
348
361
 
349
362
  return resolveObservations([args.id], 'resolved')
350
363
  .then((result) => {
@@ -361,8 +374,10 @@ function executeListRecentMemories(
361
374
  ctx: ToolExecutionContext,
362
375
  ): string {
363
376
  const limit = Math.min(args.limit ?? 10, 20);
364
- let allObs = getAllObservations()
365
- .filter(o => o.projectId === ctx.projectId && o.status !== 'archived' && o.status !== 'resolved');
377
+ let allObs = filterReadableObservations(
378
+ getAllObservations().filter(o => o.projectId === ctx.projectId && o.status !== 'archived' && o.status !== 'resolved'),
379
+ ctx.reader,
380
+ );
366
381
 
367
382
  if (args.type) {
368
383
  allObs = allObs.filter(o => o.type === args.type);
@@ -399,10 +414,10 @@ export async function askMemoryQuestion(
399
414
  };
400
415
  }
401
416
 
402
- const project = detectProject(process.cwd());
417
+ const operatorContext = await resolveTuiContext();
403
418
 
404
419
  // No project + no LLM → dead end
405
- if (!project) {
420
+ if (!operatorContext) {
406
421
  loadDotenv(process.cwd());
407
422
  initLLM({ scope: 'agent' });
408
423
  if (!isLLMEnabled()) {
@@ -438,10 +453,11 @@ export async function askMemoryQuestion(
438
453
  };
439
454
  }
440
455
 
456
+ const { project, dataDir, reader, identity } = operatorContext;
457
+
441
458
  loadDotenv(project.rootPath);
442
459
  initLLM({ scope: 'agent' });
443
460
 
444
- const dataDir = await getProjectDataDir(project.id);
445
461
  await prepareProjectSearch(project.id, dataDir);
446
462
 
447
463
  const searchMode = normalizeSearchMode(getLastSearchMode(project.id) || 'fulltext');
@@ -453,11 +469,12 @@ export async function askMemoryQuestion(
453
469
  limit: SEARCH_LIMIT,
454
470
  projectId: project.id,
455
471
  status: 'active',
472
+ reader,
456
473
  });
457
474
  const topEntries = searchResult.entries.slice(0, DETAIL_LIMIT);
458
475
  const detailRefs = topEntries.map((entry) => ({ id: entry.id, projectId: project.id }));
459
476
  const detailResult = detailRefs.length > 0
460
- ? await compactDetail(detailRefs)
477
+ ? await compactDetail(detailRefs, { reader })
461
478
  : { documents: [], formatted: '', totalTokens: 0 };
462
479
  const sources = detailResult.documents.map((doc, index) => toSource(doc, topEntries[index]?.score ?? 0));
463
480
 
@@ -488,6 +505,8 @@ export async function askMemoryQuestion(
488
505
 
489
506
  const ctx: ToolExecutionContext = {
490
507
  projectId: project.id,
508
+ reader,
509
+ ...(identity ? { writerAgentId: identity.agentId } : {}),
491
510
  collectedSources: [],
492
511
  };
493
512
 
@@ -584,10 +603,10 @@ export async function askMemoryQuestionStream(
584
603
  };
585
604
  }
586
605
 
587
- const project = detectProject(process.cwd());
606
+ const operatorContext = await resolveTuiContext();
588
607
 
589
608
  // No project + no LLM → dead end
590
- if (!project) {
609
+ if (!operatorContext) {
591
610
  loadDotenv(process.cwd());
592
611
  initLLM({ scope: 'agent' });
593
612
  if (!isLLMEnabled()) {
@@ -626,10 +645,11 @@ export async function askMemoryQuestionStream(
626
645
  };
627
646
  }
628
647
 
648
+ const { project, dataDir, reader, identity } = operatorContext;
649
+
629
650
  loadDotenv(project.rootPath);
630
651
  initLLM({ scope: 'agent' });
631
652
 
632
- const dataDir = await getProjectDataDir(project.id);
633
653
  await prepareProjectSearch(project.id, dataDir);
634
654
 
635
655
  const searchMode = normalizeSearchMode(getLastSearchMode(project.id) || 'fulltext');
@@ -641,11 +661,12 @@ export async function askMemoryQuestionStream(
641
661
  limit: SEARCH_LIMIT,
642
662
  projectId: project.id,
643
663
  status: 'active',
664
+ reader,
644
665
  });
645
666
  const topEntries = searchResult.entries.slice(0, DETAIL_LIMIT);
646
667
  const detailRefs = topEntries.map((entry) => ({ id: entry.id, projectId: project.id }));
647
668
  const detailResult = detailRefs.length > 0
648
- ? await compactDetail(detailRefs)
669
+ ? await compactDetail(detailRefs, { reader })
649
670
  : { documents: [], formatted: '', totalTokens: 0 };
650
671
  const sources = detailResult.documents.map((doc, index) => toSource(doc, topEntries[index]?.score ?? 0));
651
672
 
@@ -672,6 +693,8 @@ export async function askMemoryQuestionStream(
672
693
 
673
694
  const ctx: ToolExecutionContext = {
674
695
  projectId: project.id,
696
+ reader,
697
+ ...(identity ? { writerAgentId: identity.agentId } : {}),
675
698
  collectedSources: [],
676
699
  };
677
700