memorix 1.2.0 → 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 (212) hide show
  1. package/CHANGELOG.md +30 -1
  2. package/README.md +18 -4
  3. package/README.zh-CN.md +18 -4
  4. package/TEAM.md +86 -86
  5. package/dist/cli/index.js +15919 -14055
  6. package/dist/cli/index.js.map +1 -1
  7. package/dist/index.js +1997 -1021
  8. package/dist/index.js.map +1 -1
  9. package/dist/maintenance-runner.d.ts +1 -1
  10. package/dist/maintenance-runner.js +8481 -8005
  11. package/dist/maintenance-runner.js.map +1 -1
  12. package/dist/memcode-runtime/CHANGELOG.md +30 -1
  13. package/dist/sdk.d.ts +7 -2
  14. package/dist/sdk.js +2022 -1024
  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 +27 -5
  21. package/docs/DESIGN_DECISIONS.md +357 -357
  22. package/docs/DEVELOPMENT.md +4 -0
  23. package/docs/README.md +1 -1
  24. package/docs/SETUP.md +7 -1
  25. package/docs/dev-log/progress.txt +91 -11
  26. package/docs/knowledge/workflows/memorix-release.md +57 -0
  27. package/package.json +1 -1
  28. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  29. package/src/audit/index.ts +156 -156
  30. package/src/cli/command-guide.ts +192 -0
  31. package/src/cli/commands/audit-list.ts +89 -89
  32. package/src/cli/commands/audit.ts +9 -4
  33. package/src/cli/commands/background.ts +659 -659
  34. package/src/cli/commands/cleanup.ts +5 -1
  35. package/src/cli/commands/codegraph.ts +17 -8
  36. package/src/cli/commands/context.ts +3 -2
  37. package/src/cli/commands/doctor.ts +4 -2
  38. package/src/cli/commands/explain.ts +9 -3
  39. package/src/cli/commands/formation.ts +48 -48
  40. package/src/cli/commands/git-hook-install.ts +111 -111
  41. package/src/cli/commands/handoff.ts +75 -61
  42. package/src/cli/commands/hooks-status.ts +63 -63
  43. package/src/cli/commands/identity.ts +116 -0
  44. package/src/cli/commands/ingest-commit.ts +153 -153
  45. package/src/cli/commands/ingest-image.ts +71 -69
  46. package/src/cli/commands/ingest-log.ts +180 -180
  47. package/src/cli/commands/ingest.ts +44 -44
  48. package/src/cli/commands/integrate-shared.ts +15 -15
  49. package/src/cli/commands/knowledge.ts +40 -0
  50. package/src/cli/commands/lock.ts +93 -92
  51. package/src/cli/commands/memory.ts +58 -21
  52. package/src/cli/commands/message.ts +123 -118
  53. package/src/cli/commands/operator-shared.ts +98 -3
  54. package/src/cli/commands/poll.ts +74 -64
  55. package/src/cli/commands/purge-all-memory.ts +85 -85
  56. package/src/cli/commands/purge-project-memory.ts +83 -83
  57. package/src/cli/commands/reasoning.ts +135 -121
  58. package/src/cli/commands/retention.ts +9 -4
  59. package/src/cli/commands/serve-http.ts +22 -43
  60. package/src/cli/commands/serve-shared.ts +118 -118
  61. package/src/cli/commands/session.ts +29 -3
  62. package/src/cli/commands/setup.ts +9 -3
  63. package/src/cli/commands/skills.ts +124 -119
  64. package/src/cli/commands/status.ts +4 -3
  65. package/src/cli/commands/task.ts +193 -184
  66. package/src/cli/commands/team.ts +14 -10
  67. package/src/cli/commands/transfer.ts +108 -55
  68. package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
  69. package/src/cli/identity.ts +89 -0
  70. package/src/cli/index.ts +96 -19
  71. package/src/cli/invocation.ts +115 -0
  72. package/src/cli/tui/ChatView.tsx +234 -234
  73. package/src/cli/tui/CommandBar.tsx +312 -312
  74. package/src/cli/tui/ContextRail.tsx +118 -118
  75. package/src/cli/tui/HeaderBar.tsx +72 -72
  76. package/src/cli/tui/LogoBanner.tsx +51 -51
  77. package/src/cli/tui/Sidebar.tsx +179 -179
  78. package/src/cli/tui/chat-service.ts +41 -18
  79. package/src/cli/tui/data.ts +23 -44
  80. package/src/cli/tui/index.ts +41 -41
  81. package/src/cli/tui/markdown-render.tsx +371 -371
  82. package/src/cli/tui/operator-context.ts +60 -0
  83. package/src/cli/tui/use-mouse.ts +157 -157
  84. package/src/cli/tui/useNavigation.ts +56 -56
  85. package/src/cli/tui/views/MemoryView.tsx +10 -8
  86. package/src/cli/update-checker.ts +211 -211
  87. package/src/cli/version.ts +7 -7
  88. package/src/cli/workbench.ts +1 -1
  89. package/src/codegraph/auto-context.ts +34 -17
  90. package/src/codegraph/context-pack.ts +1 -0
  91. package/src/codegraph/current-facts.ts +19 -1
  92. package/src/codegraph/project-context.ts +2 -0
  93. package/src/codegraph/task-lens.ts +49 -5
  94. package/src/compact/engine.ts +26 -10
  95. package/src/compact/index-format.ts +25 -2
  96. package/src/compact/token-budget.ts +74 -74
  97. package/src/dashboard/project-classification.ts +64 -64
  98. package/src/dashboard/server.ts +58 -52
  99. package/src/embedding/fastembed-provider.ts +142 -142
  100. package/src/embedding/transformers-provider.ts +111 -111
  101. package/src/git/extractor.ts +209 -209
  102. package/src/git/hooks-path.ts +85 -85
  103. package/src/hooks/admission.ts +117 -0
  104. package/src/hooks/handler.ts +98 -91
  105. package/src/hooks/pattern-detector.ts +173 -173
  106. package/src/hooks/significance-filter.ts +250 -250
  107. package/src/knowledge/claims.ts +51 -1
  108. package/src/knowledge/context-assembly.ts +97 -0
  109. package/src/knowledge/types.ts +1 -0
  110. package/src/knowledge/workflows.ts +34 -3
  111. package/src/knowledge/workset.ts +179 -10
  112. package/src/llm/memory-manager.ts +328 -328
  113. package/src/llm/provider.ts +885 -885
  114. package/src/llm/quality.ts +248 -248
  115. package/src/memory/admission.ts +57 -0
  116. package/src/memory/attribution-guard.ts +249 -249
  117. package/src/memory/auto-relations.ts +21 -0
  118. package/src/memory/consolidation.ts +13 -2
  119. package/src/memory/disclosure-policy.ts +140 -135
  120. package/src/memory/entity-extractor.ts +197 -197
  121. package/src/memory/export-import.ts +11 -3
  122. package/src/memory/formation/evaluate.ts +217 -217
  123. package/src/memory/formation/extract.ts +361 -361
  124. package/src/memory/formation/index.ts +417 -417
  125. package/src/memory/formation/resolve.ts +344 -344
  126. package/src/memory/formation/types.ts +315 -315
  127. package/src/memory/freshness.ts +122 -122
  128. package/src/memory/graph-context.ts +8 -2
  129. package/src/memory/graph-scope.ts +46 -0
  130. package/src/memory/graph.ts +197 -197
  131. package/src/memory/observations.ts +162 -4
  132. package/src/memory/quality-audit.ts +2 -0
  133. package/src/memory/refs.ts +94 -94
  134. package/src/memory/retention.ts +22 -2
  135. package/src/memory/secret-filter.ts +79 -79
  136. package/src/memory/session.ts +5 -2
  137. package/src/memory/visibility.ts +80 -0
  138. package/src/multimodal/image-loader.ts +143 -143
  139. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  140. package/src/orchestrate/adapters/claude.ts +111 -111
  141. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  142. package/src/orchestrate/adapters/codex.ts +41 -41
  143. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  144. package/src/orchestrate/adapters/gemini.ts +42 -42
  145. package/src/orchestrate/adapters/index.ts +73 -73
  146. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  147. package/src/orchestrate/adapters/opencode.ts +47 -47
  148. package/src/orchestrate/adapters/spawn-helper.ts +286 -286
  149. package/src/orchestrate/adapters/types.ts +77 -77
  150. package/src/orchestrate/capability-router.ts +284 -284
  151. package/src/orchestrate/context-compact.ts +188 -188
  152. package/src/orchestrate/cost-tracker.ts +219 -219
  153. package/src/orchestrate/error-recovery.ts +191 -191
  154. package/src/orchestrate/evidence.ts +140 -140
  155. package/src/orchestrate/ledger.ts +110 -110
  156. package/src/orchestrate/memorix-bridge.ts +378 -340
  157. package/src/orchestrate/output-budget.ts +80 -80
  158. package/src/orchestrate/permission.ts +152 -152
  159. package/src/orchestrate/pipeline-trace.ts +131 -131
  160. package/src/orchestrate/prompt-builder.ts +155 -155
  161. package/src/orchestrate/ring-buffer.ts +37 -37
  162. package/src/orchestrate/task-graph.ts +389 -389
  163. package/src/orchestrate/verify-gate.ts +33 -10
  164. package/src/orchestrate/worktree.ts +232 -232
  165. package/src/project/aliases.ts +374 -374
  166. package/src/project/detector.ts +268 -268
  167. package/src/rules/adapters/claude-code.ts +99 -99
  168. package/src/rules/adapters/codex.ts +97 -97
  169. package/src/rules/adapters/copilot.ts +124 -124
  170. package/src/rules/adapters/cursor.ts +114 -114
  171. package/src/rules/adapters/kiro.ts +126 -126
  172. package/src/rules/adapters/trae.ts +56 -56
  173. package/src/rules/adapters/windsurf.ts +83 -83
  174. package/src/rules/syncer.ts +235 -235
  175. package/src/runtime/control-plane-maintenance.ts +1 -0
  176. package/src/runtime/isolated-maintenance.ts +1 -0
  177. package/src/runtime/lifecycle.ts +18 -0
  178. package/src/runtime/maintenance-jobs.ts +1 -0
  179. package/src/runtime/maintenance-runner.ts +2 -0
  180. package/src/runtime/project-maintenance.ts +89 -0
  181. package/src/sdk.ts +334 -304
  182. package/src/search/intent-detector.ts +289 -289
  183. package/src/search/query-expansion.ts +52 -52
  184. package/src/server/formation-timeout.ts +27 -27
  185. package/src/server.ts +334 -93
  186. package/src/skills/mini-skills.ts +386 -386
  187. package/src/store/chat-store.ts +119 -119
  188. package/src/store/graph-store.ts +249 -249
  189. package/src/store/mini-skill-store.ts +349 -349
  190. package/src/store/orama-store.ts +61 -6
  191. package/src/store/persistence-json.ts +212 -212
  192. package/src/store/persistence.ts +291 -291
  193. package/src/store/project-affinity.ts +195 -195
  194. package/src/store/sqlite-db.ts +23 -1
  195. package/src/store/sqlite-store.ts +12 -2
  196. package/src/team/event-bus.ts +76 -76
  197. package/src/team/file-locks.ts +173 -173
  198. package/src/team/handoff.ts +168 -161
  199. package/src/team/messages.ts +203 -203
  200. package/src/team/poll.ts +132 -132
  201. package/src/team/tasks.ts +211 -211
  202. package/src/types.ts +51 -0
  203. package/src/wiki/generator.ts +2 -0
  204. package/src/workspace/mcp-adapters/codex.ts +191 -191
  205. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  206. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  207. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  208. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  209. package/src/workspace/mcp-adapters/trae.ts +134 -134
  210. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  211. package/src/workspace/sanitizer.ts +60 -60
  212. 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