synabun 1.3.0

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 (187) hide show
  1. package/CHANGELOG.md +665 -0
  2. package/LICENSE +191 -0
  3. package/README.md +853 -0
  4. package/hooks/claude-code/post-plan.mjs +391 -0
  5. package/hooks/claude-code/post-remember.mjs +239 -0
  6. package/hooks/claude-code/pre-compact.mjs +241 -0
  7. package/hooks/claude-code/pre-websearch.mjs +90 -0
  8. package/hooks/claude-code/prompt-submit.mjs +861 -0
  9. package/hooks/claude-code/session-start.mjs +494 -0
  10. package/hooks/claude-code/shared.mjs +368 -0
  11. package/hooks/claude-code/stop.mjs +646 -0
  12. package/hooks/claude-code/test-user-learning.mjs +531 -0
  13. package/mcp-server/package.json +38 -0
  14. package/mcp-server/run.mjs +10 -0
  15. package/mcp-server/src/config.ts +70 -0
  16. package/mcp-server/src/http.ts +93 -0
  17. package/mcp-server/src/index.ts +203 -0
  18. package/mcp-server/src/preload-http.ts +25 -0
  19. package/mcp-server/src/preload.ts +29 -0
  20. package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
  21. package/mcp-server/src/services/categories.ts +429 -0
  22. package/mcp-server/src/services/discord.ts +702 -0
  23. package/mcp-server/src/services/file-checksums.ts +33 -0
  24. package/mcp-server/src/services/local-embeddings.ts +77 -0
  25. package/mcp-server/src/services/neural-interface.ts +344 -0
  26. package/mcp-server/src/services/sqlite.ts +823 -0
  27. package/mcp-server/src/tools/browser-advanced.ts +123 -0
  28. package/mcp-server/src/tools/browser-interact.ts +255 -0
  29. package/mcp-server/src/tools/browser-navigate.ts +77 -0
  30. package/mcp-server/src/tools/browser-observe.ts +1330 -0
  31. package/mcp-server/src/tools/browser.ts +107 -0
  32. package/mcp-server/src/tools/card-tools.ts +193 -0
  33. package/mcp-server/src/tools/card.ts +22 -0
  34. package/mcp-server/src/tools/category.ts +379 -0
  35. package/mcp-server/src/tools/discord-channel.ts +274 -0
  36. package/mcp-server/src/tools/discord-guild.ts +188 -0
  37. package/mcp-server/src/tools/discord-member.ts +158 -0
  38. package/mcp-server/src/tools/discord-message.ts +231 -0
  39. package/mcp-server/src/tools/discord-onboarding.ts +228 -0
  40. package/mcp-server/src/tools/discord-role.ts +209 -0
  41. package/mcp-server/src/tools/discord-thread.ts +110 -0
  42. package/mcp-server/src/tools/discord-webhook.ts +176 -0
  43. package/mcp-server/src/tools/discord.ts +40 -0
  44. package/mcp-server/src/tools/forget.ts +38 -0
  45. package/mcp-server/src/tools/git-tools.ts +141 -0
  46. package/mcp-server/src/tools/git.ts +13 -0
  47. package/mcp-server/src/tools/image-tools.ts +98 -0
  48. package/mcp-server/src/tools/image.ts +17 -0
  49. package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
  50. package/mcp-server/src/tools/leonardo.ts +28 -0
  51. package/mcp-server/src/tools/loop.ts +348 -0
  52. package/mcp-server/src/tools/memories.ts +118 -0
  53. package/mcp-server/src/tools/recall.ts +323 -0
  54. package/mcp-server/src/tools/reflect.ts +145 -0
  55. package/mcp-server/src/tools/remember.ts +115 -0
  56. package/mcp-server/src/tools/response.ts +26 -0
  57. package/mcp-server/src/tools/restore.ts +34 -0
  58. package/mcp-server/src/tools/sync.ts +114 -0
  59. package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
  60. package/mcp-server/src/tools/tictactoe.ts +13 -0
  61. package/mcp-server/src/tools/utils.ts +25 -0
  62. package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
  63. package/mcp-server/src/tools/whiteboard.ts +22 -0
  64. package/mcp-server/src/tui.ts +940 -0
  65. package/mcp-server/src/types.ts +85 -0
  66. package/mcp-server/tsconfig.json +19 -0
  67. package/neural-interface/package.json +27 -0
  68. package/neural-interface/public/apple-touch-icon.png +0 -0
  69. package/neural-interface/public/category-logos/claude.svg +1 -0
  70. package/neural-interface/public/category-logos/gemini-color.svg +1 -0
  71. package/neural-interface/public/category-logos/openai.svg +1 -0
  72. package/neural-interface/public/category-logos/synabun.png +0 -0
  73. package/neural-interface/public/claude-chat.css +878 -0
  74. package/neural-interface/public/claude-chat.html +92 -0
  75. package/neural-interface/public/claude-chat.js +1168 -0
  76. package/neural-interface/public/favicon-16x16.png +0 -0
  77. package/neural-interface/public/favicon-32x32.png +0 -0
  78. package/neural-interface/public/gallery.svg +6 -0
  79. package/neural-interface/public/icon-192.png +0 -0
  80. package/neural-interface/public/icon-512.png +0 -0
  81. package/neural-interface/public/index-legacy.html +14677 -0
  82. package/neural-interface/public/index.html +212 -0
  83. package/neural-interface/public/index2d-legacy.html +8193 -0
  84. package/neural-interface/public/index2d.html +127 -0
  85. package/neural-interface/public/invite.html +351 -0
  86. package/neural-interface/public/leonardoai.svg +1 -0
  87. package/neural-interface/public/logo-synabun.png +0 -0
  88. package/neural-interface/public/logoHD.png +0 -0
  89. package/neural-interface/public/manifest.json +14 -0
  90. package/neural-interface/public/offline.html +299 -0
  91. package/neural-interface/public/onboarding.html +2335 -0
  92. package/neural-interface/public/openclaw-logo-text.png +0 -0
  93. package/neural-interface/public/pin-svgrepo-com.svg +25 -0
  94. package/neural-interface/public/sessionicon.svg +8 -0
  95. package/neural-interface/public/shared/ansi-parser.js +305 -0
  96. package/neural-interface/public/shared/api.js +1043 -0
  97. package/neural-interface/public/shared/colors.js +153 -0
  98. package/neural-interface/public/shared/constants.js +141 -0
  99. package/neural-interface/public/shared/html-shell.js +777 -0
  100. package/neural-interface/public/shared/html-term-renderer.js +1022 -0
  101. package/neural-interface/public/shared/i18n.js +147 -0
  102. package/neural-interface/public/shared/registry.js +82 -0
  103. package/neural-interface/public/shared/state.js +86 -0
  104. package/neural-interface/public/shared/storage.js +150 -0
  105. package/neural-interface/public/shared/styles.css +21212 -0
  106. package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
  107. package/neural-interface/public/shared/ui-bookmarks.js +231 -0
  108. package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
  109. package/neural-interface/public/shared/ui-command-runner.js +534 -0
  110. package/neural-interface/public/shared/ui-cost-widget.js +602 -0
  111. package/neural-interface/public/shared/ui-detail.js +1454 -0
  112. package/neural-interface/public/shared/ui-explorer.js +1356 -0
  113. package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
  114. package/neural-interface/public/shared/ui-games.js +411 -0
  115. package/neural-interface/public/shared/ui-help.js +107 -0
  116. package/neural-interface/public/shared/ui-image-gallery.js +461 -0
  117. package/neural-interface/public/shared/ui-invite.js +519 -0
  118. package/neural-interface/public/shared/ui-keybinds.js +452 -0
  119. package/neural-interface/public/shared/ui-layouts.js +297 -0
  120. package/neural-interface/public/shared/ui-link.js +731 -0
  121. package/neural-interface/public/shared/ui-loading.js +255 -0
  122. package/neural-interface/public/shared/ui-menubar.js +499 -0
  123. package/neural-interface/public/shared/ui-multiselect.js +93 -0
  124. package/neural-interface/public/shared/ui-navbar.js +222 -0
  125. package/neural-interface/public/shared/ui-notifications.js +287 -0
  126. package/neural-interface/public/shared/ui-panels.js +309 -0
  127. package/neural-interface/public/shared/ui-resume.js +653 -0
  128. package/neural-interface/public/shared/ui-search.js +145 -0
  129. package/neural-interface/public/shared/ui-sessions.js +531 -0
  130. package/neural-interface/public/shared/ui-settings.js +5987 -0
  131. package/neural-interface/public/shared/ui-sidebar.js +1905 -0
  132. package/neural-interface/public/shared/ui-skills.js +2805 -0
  133. package/neural-interface/public/shared/ui-stats.js +38 -0
  134. package/neural-interface/public/shared/ui-sync.js +201 -0
  135. package/neural-interface/public/shared/ui-terminal.js +6385 -0
  136. package/neural-interface/public/shared/ui-tooltip.js +123 -0
  137. package/neural-interface/public/shared/ui-trash.js +439 -0
  138. package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
  139. package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
  140. package/neural-interface/public/shared/ui-tutorial.js +1117 -0
  141. package/neural-interface/public/shared/ui-update.js +244 -0
  142. package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
  143. package/neural-interface/public/shared/ui-workspaces.js +722 -0
  144. package/neural-interface/public/shared/utils.js +360 -0
  145. package/neural-interface/public/shared/vterm-buffer.js +1102 -0
  146. package/neural-interface/public/sw.js +52 -0
  147. package/neural-interface/public/synabun.png +0 -0
  148. package/neural-interface/public/toolbar-album.svg +8 -0
  149. package/neural-interface/public/toolbar-question.svg +6 -0
  150. package/neural-interface/public/toolbar-sessions.svg +6 -0
  151. package/neural-interface/public/variant/2d/background.js +113 -0
  152. package/neural-interface/public/variant/2d/context-menu.js +133 -0
  153. package/neural-interface/public/variant/2d/gfx.js +45 -0
  154. package/neural-interface/public/variant/2d/graph.js +1635 -0
  155. package/neural-interface/public/variant/2d/hulls.js +207 -0
  156. package/neural-interface/public/variant/2d/lasso.js +135 -0
  157. package/neural-interface/public/variant/2d/layout.js +594 -0
  158. package/neural-interface/public/variant/2d/main.js +521 -0
  159. package/neural-interface/public/variant/2d/minimap.js +312 -0
  160. package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
  161. package/neural-interface/public/variant/3d/background.js +884 -0
  162. package/neural-interface/public/variant/3d/camera.js +661 -0
  163. package/neural-interface/public/variant/3d/gfx.js +116 -0
  164. package/neural-interface/public/variant/3d/graph.js +2399 -0
  165. package/neural-interface/public/variant/3d/main.js +500 -0
  166. package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
  167. package/neural-interface/scripts/rebuild-pty.js +100 -0
  168. package/neural-interface/server.js +15000 -0
  169. package/neural-interface/templates/CLAUDE-template.md +211 -0
  170. package/package.json +53 -0
  171. package/postinstall.js +135 -0
  172. package/setup.js +260 -0
  173. package/skills/leonardo/SKILL.md +159 -0
  174. package/skills/leonardo/icon.svg +1 -0
  175. package/skills/leonardo/modules/browser-guide.md +378 -0
  176. package/skills/leonardo/modules/image-prompter.md +277 -0
  177. package/skills/leonardo/modules/model-advisor.md +100 -0
  178. package/skills/leonardo/modules/prompt-library.md +110 -0
  179. package/skills/leonardo/modules/style-guide.md +186 -0
  180. package/skills/leonardo/modules/video-prompter.md +311 -0
  181. package/skills/synabun/SKILL.md +185 -0
  182. package/skills/synabun/icon.png +0 -0
  183. package/skills/synabun/modules/audit.md +374 -0
  184. package/skills/synabun/modules/changelog.md +194 -0
  185. package/skills/synabun/modules/idea.md +144 -0
  186. package/skills/synabun/modules/memorize.md +188 -0
  187. package/skills/synabun/synabunicon.png +0 -0
@@ -0,0 +1,940 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Load .env from parent directory before any service imports
4
+ import { readFileSync } from 'fs';
5
+ import { resolve, dirname } from 'path';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ const __dirname_ = dirname(fileURLToPath(import.meta.url));
9
+ const envPath = process.env.DOTENV_PATH || resolve(__dirname_, '..', '..', '.env');
10
+ try {
11
+ const envContent = readFileSync(envPath, 'utf-8');
12
+ for (const line of envContent.split('\n')) {
13
+ const trimmed = line.trim();
14
+ if (!trimmed || trimmed.startsWith('#')) continue;
15
+ const eq = trimmed.indexOf('=');
16
+ if (eq === -1) continue;
17
+ const key = trimmed.slice(0, eq).trim();
18
+ const val = trimmed.slice(eq + 1).trim();
19
+ if (!process.env[key]) process.env[key] = val;
20
+ }
21
+ } catch { /* .env not found, rely on existing env vars */ }
22
+
23
+ import { select, input, confirm } from '@inquirer/prompts';
24
+ import chalk from 'chalk';
25
+ import { v4 as uuidv4 } from 'uuid';
26
+ import { ensureCollection, scrollMemories, searchMemories, getMemory, deleteMemory, updatePayload, updateVector, upsertMemory, getMemoryStats, countMemories, updatePayloadByFilter } from './services/sqlite.js';
27
+ import { getAllCategories, getCategories, getCustomCategories, getCategoryDescription, validateCategoryName, addCategory, removeCategory } from './services/categories.js';
28
+ import { generateEmbedding } from './services/local-embeddings.js';
29
+ import { detectProject } from './config.js';
30
+ import type { MemoryPayload, MemorySource } from './types.js';
31
+
32
+ // ── Box drawing characters ────────────────────────────────────────
33
+
34
+ const BOX = {
35
+ tl: '\u256D', tr: '\u256E', bl: '\u2570', br: '\u256F',
36
+ h: '\u2500', v: '\u2502',
37
+ ltee: '\u251C', rtee: '\u2524',
38
+ dh: '\u2550', dtl: '\u2554', dtr: '\u2557', dbl: '\u255A', dbr: '\u255D', dv: '\u2551',
39
+ } as const;
40
+
41
+ const ICONS = {
42
+ brain: '\u2022', // bullet
43
+ search: '\u25C9', // fisheye
44
+ plus: '\u253C', // cross
45
+ folder: '\u25A0', // square
46
+ chart: '\u25B2', // triangle
47
+ exit: '\u25C6', // diamond
48
+ tag: '#',
49
+ arrow: '\u25B6', // right triangle
50
+ dot: '\u2022',
51
+ star: '\u2605',
52
+ check: '\u2713',
53
+ cross: '\u2717',
54
+ sparkle: '\u2734',
55
+ memo: '\u2261', // triple bar
56
+ clock: '\u25CB', // circle
57
+ link: '\u221E', // infinity
58
+ edit: '\u270E', // pencil
59
+ trash: '\u2620', // skull
60
+ back: '\u25C0', // left triangle
61
+ more: '\u22EF', // midline ellipsis
62
+ } as const;
63
+
64
+ // ── Color helpers ──────────────────────────────────────────────────
65
+
66
+ // Dynamic category color palette — cycles through for categories without a preset
67
+ const CATEGORY_COLOR_PALETTE = [
68
+ '#5B9BD5', '#70AD47', '#FFC000', '#ED7D31', '#44C8F5',
69
+ '#A5A5A5', '#4472C4', '#FF6B9D', '#00B050', '#E040FB',
70
+ '#00BFA5', '#7C4DFF', '#FF5252', '#D32F2F', '#64FFDA',
71
+ '#536DFE', '#FFD740', '#69F0AE', '#18FFFF', '#B2FF59',
72
+ ];
73
+
74
+ const CATEGORY_ICON_PALETTE = [
75
+ '\u25C8', '\u25A3', '\u2261', '\u2605', '\u25CF',
76
+ '\u25AC', '\u25A4', '\u2665', '\u25B2', '\u25C6',
77
+ '\u25CB', '\u25AA', '\u2726', '\u25D0', '\u25B6',
78
+ '\u2662', '\u2716', '\u25C9', '\u2756', '\u2318',
79
+ ];
80
+
81
+ function getCategoryColorFn(cat: string): (s: string) => string {
82
+ const allCats = getAllCategories();
83
+ const idx = allCats.indexOf(cat);
84
+ const hex = idx >= 0 ? CATEGORY_COLOR_PALETTE[idx % CATEGORY_COLOR_PALETTE.length] : '#888888';
85
+ return chalk.hex(hex);
86
+ }
87
+
88
+ function getCategoryIcon(cat: string): string {
89
+ const allCats = getAllCategories();
90
+ const idx = allCats.indexOf(cat);
91
+ return idx >= 0 ? CATEGORY_ICON_PALETTE[idx % CATEGORY_ICON_PALETTE.length] : ICONS.dot;
92
+ }
93
+
94
+ const accent = chalk.hex('#7B68EE'); // medium slate blue - main brand color
95
+ const accent2 = chalk.hex('#9370DB'); // medium purple
96
+ const dim = chalk.hex('#555555');
97
+ const success = chalk.hex('#00E676');
98
+ const warn = chalk.hex('#FFAB40');
99
+ const danger = chalk.hex('#FF5252');
100
+ const info = chalk.hex('#40C4FF');
101
+ const muted = chalk.hex('#666666');
102
+
103
+ function colorCategory(cat: string): string {
104
+ const fn = getCategoryColorFn(cat);
105
+ const icon = getCategoryIcon(cat);
106
+ return fn(`${icon} ${cat}`);
107
+ }
108
+
109
+ function colorCategoryName(cat: string): string {
110
+ const fn = getCategoryColorFn(cat);
111
+ return fn(cat);
112
+ }
113
+
114
+ function importanceBadge(imp: number): string {
115
+ if (imp >= 9) return chalk.bgHex('#FF5252').white.bold(` ${imp} `);
116
+ if (imp >= 8) return chalk.bgHex('#FF7043').white(` ${imp} `);
117
+ if (imp >= 6) return chalk.bgHex('#FFA726').black(` ${imp} `);
118
+ if (imp >= 4) return chalk.bgHex('#78909C').white(` ${imp} `);
119
+ return chalk.bgHex('#455A64').white(` ${imp} `);
120
+ }
121
+
122
+ function formatAge(iso: string): string {
123
+ const ms = Date.now() - new Date(iso).getTime();
124
+ const mins = Math.floor(ms / 60000);
125
+ if (mins < 1) return 'just now';
126
+ if (mins < 60) return `${mins}m ago`;
127
+ const hours = Math.floor(mins / 60);
128
+ if (hours < 24) return `${hours}h ago`;
129
+ const days = Math.floor(hours / 24);
130
+ if (days === 1) return '1d ago';
131
+ if (days < 30) return `${days}d ago`;
132
+ const months = Math.floor(days / 30);
133
+ if (months === 1) return '1mo ago';
134
+ if (months < 12) return `${months}mo ago`;
135
+ const years = Math.floor(months / 12);
136
+ return years === 1 ? '1y ago' : `${years}y ago`;
137
+ }
138
+
139
+ function formatDate(iso: string): string {
140
+ return new Date(iso).toLocaleDateString('en-US', {
141
+ year: 'numeric', month: 'short', day: 'numeric',
142
+ hour: '2-digit', minute: '2-digit',
143
+ });
144
+ }
145
+
146
+ function truncate(s: string, max: number): string {
147
+ const line = s.replace(/\n/g, ' ').trim();
148
+ return line.length <= max ? line : line.slice(0, max - 1) + '\u2026';
149
+ }
150
+
151
+ // ── Box & layout helpers ──────────────────────────────────────────
152
+
153
+ function boxTop(width: number, title?: string): string {
154
+ if (title) {
155
+ const titleStr = ` ${title} `;
156
+ const stripped = titleStr.replace(/\x1B\[[0-9;]*m/g, '');
157
+ const remaining = width - stripped.length;
158
+ const left = Math.max(0, Math.floor(remaining / 2));
159
+ const right = Math.max(0, remaining - left);
160
+ return dim(BOX.tl + BOX.h.repeat(left)) + accent.bold(titleStr) + dim(BOX.h.repeat(right) + BOX.tr);
161
+ }
162
+ return dim(BOX.tl + BOX.h.repeat(width) + BOX.tr);
163
+ }
164
+
165
+ function boxBottom(width: number): string {
166
+ return dim(BOX.bl + BOX.h.repeat(width) + BOX.br);
167
+ }
168
+
169
+ function boxRow(width: number, content: string, pad = 1): string {
170
+ // Strip ANSI for length calculation
171
+ const stripped = content.replace(/\x1B\[[0-9;]*m/g, '');
172
+ const remaining = width - stripped.length - (pad * 2);
173
+ const right = Math.max(0, remaining);
174
+ return dim(BOX.v) + ' '.repeat(pad) + content + ' '.repeat(right + pad) + dim(BOX.v);
175
+ }
176
+
177
+ function boxEmpty(width: number): string {
178
+ return dim(BOX.v) + ' '.repeat(width) + dim(BOX.v);
179
+ }
180
+
181
+ function boxSep(width: number): string {
182
+ return dim(BOX.ltee + BOX.h.repeat(width) + BOX.rtee);
183
+ }
184
+
185
+ // Renders a double-bordered row: ║ <centered content padded to W> ║
186
+ function dboxRow(width: number, content: string): string {
187
+ const stripped = content.replace(/\x1B\[[0-9;]*m/g, '');
188
+ const totalPad = width - stripped.length;
189
+ const left = Math.max(0, Math.floor(totalPad / 2));
190
+ const right = Math.max(0, totalPad - left);
191
+ return dim(BOX.dv) + ' '.repeat(left) + content + ' '.repeat(right) + dim(BOX.dv);
192
+ }
193
+
194
+ // ── ASCII Art ─────────────────────────────────────────────────────
195
+
196
+ function printLogo(): void {
197
+ const W = 56;
198
+
199
+ // The art lines with fixed left indent for consistent visual alignment
200
+ const indent = ' ';
201
+ const rawArt = [
202
+ `${indent} __ __ `,
203
+ `${indent} | \\/ | ___ _ __ ___ ___ _ __ _ _`,
204
+ `${indent}| |\\/| |/ _ \\ '_ \` _ \\ / _ \\| '__| | | |`,
205
+ `${indent}| | | | __/ | | | | | (_) | | | |_| |`,
206
+ `${indent}|_| |_|\\___|_| |_| |_|\\___/|_| \\__, |`,
207
+ `${indent} |___/ `,
208
+ ];
209
+
210
+ // Fixed-width row: ║ + left pad + content + right pad + ║
211
+ function artRow(content: string): string {
212
+ const stripped = content.replace(/\x1B\[[0-9;]*m/g, '');
213
+ const pad = Math.max(0, W - stripped.length);
214
+ const left = 3; // fixed left margin
215
+ const right = Math.max(0, pad - left);
216
+ return dim(BOX.dv) + ' '.repeat(left) + content + ' '.repeat(right) + dim(BOX.dv);
217
+ }
218
+
219
+ console.log('');
220
+ console.log(dim(BOX.dtl + BOX.dh.repeat(W) + BOX.dtr));
221
+ console.log(dboxRow(W, ''));
222
+
223
+ for (const line of rawArt) {
224
+ console.log(artRow(accent.bold(line)));
225
+ }
226
+
227
+ // Empty line before tagline
228
+ console.log(dboxRow(W, ''));
229
+
230
+ // Tagline (centered)
231
+ const tagline = accent2('Vector-Powered') + dim(' \u2022 ') + info('SQLite') + dim(' \u2022 ') + chalk.white('v2.0.0');
232
+ console.log(dboxRow(W, tagline));
233
+
234
+ console.log(dim(BOX.dbl + BOX.dh.repeat(W) + BOX.dbr));
235
+ console.log('');
236
+ }
237
+
238
+ function printScreenHeader(title: string, subtitle?: string): void {
239
+ const W = 56;
240
+ console.log('');
241
+ console.log(' ' + boxTop(W, title));
242
+ if (subtitle) {
243
+ console.log(' ' + boxRow(W, muted(subtitle)));
244
+ }
245
+ console.log(' ' + boxBottom(W));
246
+ console.log('');
247
+ }
248
+
249
+ function printSpinner(text: string): void {
250
+ console.log(muted(`\n ${ICONS.more} ${text}`));
251
+ }
252
+
253
+ // ── Prompt wrapper (Ctrl+C -> null instead of crash) ──────────────
254
+
255
+ async function safePrompt<T>(fn: () => Promise<T>): Promise<T | null> {
256
+ try {
257
+ return await fn();
258
+ } catch (err: unknown) {
259
+ if (err && typeof err === 'object' && 'name' in err && (err as { name: string }).name === 'ExitPromptError') {
260
+ return null;
261
+ }
262
+ throw err;
263
+ }
264
+ }
265
+
266
+ // ── Format memories ───────────────────────────────────────────────
267
+
268
+ function formatMemoryDetail(id: string, p: MemoryPayload): string {
269
+ const W = 62;
270
+ const lines: string[] = [];
271
+
272
+ lines.push(' ' + boxTop(W, `${ICONS.memo} Memory Detail`));
273
+ lines.push(' ' + boxEmpty(W));
274
+
275
+ // Content - may be multi-line
276
+ const contentLines = p.content.split('\n');
277
+ lines.push(' ' + boxRow(W, chalk.bold('Content')));
278
+ for (const cl of contentLines) {
279
+ lines.push(' ' + boxRow(W, ` ${chalk.white(cl)}`));
280
+ }
281
+
282
+ lines.push(' ' + boxSep(W));
283
+
284
+ // Metadata grid
285
+ const catColorFn = getCategoryColorFn(p.category);
286
+ const catIcon = getCategoryIcon(p.category);
287
+
288
+ lines.push(' ' + boxRow(W, `${muted('Category')} ${catColorFn(`${catIcon} ${p.category}`)}${p.subcategory ? muted(` / ${p.subcategory}`) : ''}`));
289
+ lines.push(' ' + boxRow(W, `${muted('Project')} ${info(p.project)}`));
290
+ lines.push(' ' + boxRow(W, `${muted('Importance')} ${importanceBadge(p.importance)}`));
291
+ lines.push(' ' + boxRow(W, `${muted('Source')} ${chalk.white(p.source)}`));
292
+
293
+ if (p.tags.length) {
294
+ const tagStr = p.tags.map(t => info(`${ICONS.tag}${t}`)).join(' ');
295
+ lines.push(' ' + boxRow(W, `${muted('Tags')} ${tagStr}`));
296
+ }
297
+
298
+ lines.push(' ' + boxSep(W));
299
+
300
+ // Timestamps
301
+ lines.push(' ' + boxRow(W, `${muted('Created')} ${chalk.white(formatDate(p.created_at))} ${dim(`(${formatAge(p.created_at)})`)}`));
302
+ lines.push(' ' + boxRow(W, `${muted('Updated')} ${chalk.white(formatDate(p.updated_at))}`));
303
+ lines.push(' ' + boxRow(W, `${muted('Accessed')} ${chalk.white(String(p.access_count))} time${p.access_count !== 1 ? 's' : ''} ${dim(`\u2022 last ${formatAge(p.accessed_at)}`)}`));
304
+
305
+ if (p.related_files?.length) {
306
+ lines.push(' ' + boxSep(W));
307
+ lines.push(' ' + boxRow(W, `${muted('Files')} ${p.related_files.map(f => dim(f)).join(', ')}`));
308
+ }
309
+ if (p.related_memory_ids?.length) {
310
+ if (!p.related_files?.length) lines.push(' ' + boxSep(W));
311
+ lines.push(' ' + boxRow(W, `${muted('Related')} ${p.related_memory_ids.map(r => dim(r.slice(0, 8) + '\u2026')).join(', ')}`));
312
+ }
313
+
314
+ lines.push(' ' + boxEmpty(W));
315
+ lines.push(' ' + boxRow(W, dim(`ID: ${id}`)));
316
+ lines.push(' ' + boxBottom(W));
317
+
318
+ return lines.join('\n');
319
+ }
320
+
321
+ function formatMemoryLine(id: string, p: MemoryPayload): string {
322
+ const catColorFn = getCategoryColorFn(p.category);
323
+ const catIconStr = getCategoryIcon(p.category);
324
+ const imp = importanceBadge(p.importance);
325
+ const age = dim(formatAge(p.created_at).padStart(7));
326
+ const content = chalk.white(truncate(p.content, 38));
327
+ const cat = catColorFn(catIconStr + ' ' + p.category.slice(0, 4).padEnd(4));
328
+ return `${imp} ${cat} ${content} ${age}`;
329
+ }
330
+
331
+ // ── Screens ───────────────────────────────────────────────────────
332
+
333
+ async function detailView(id: string, payload: MemoryPayload): Promise<void> {
334
+ while (true) {
335
+ console.log('\n' + formatMemoryDetail(id, payload));
336
+
337
+ const action = await safePrompt(() =>
338
+ select({
339
+ message: accent(` ${ICONS.arrow} Action`),
340
+ choices: [
341
+ { name: `${ICONS.back} Back`, value: 'back' },
342
+ { name: `${ICONS.edit} Edit`, value: 'edit' },
343
+ { name: `${ICONS.trash} Delete`, value: 'delete' },
344
+ ],
345
+ })
346
+ );
347
+
348
+ if (action === null || action === 'back') return;
349
+
350
+ if (action === 'delete') {
351
+ const yes = await safePrompt(() =>
352
+ confirm({ message: danger(` ${ICONS.trash} Delete this memory permanently?`), default: false })
353
+ );
354
+ if (yes) {
355
+ await deleteMemory(id);
356
+ console.log(success(`\n ${ICONS.check} Memory deleted.`));
357
+ return;
358
+ }
359
+ continue;
360
+ }
361
+
362
+ if (action === 'edit') {
363
+ await editMemory(id, payload);
364
+ const refreshed = await getMemory(id);
365
+ if (refreshed) {
366
+ payload = refreshed.payload as unknown as MemoryPayload;
367
+ } else {
368
+ console.log(warn(`\n ${ICONS.cross} Memory no longer exists.`));
369
+ return;
370
+ }
371
+ }
372
+ }
373
+ }
374
+
375
+ async function editMemory(id: string, payload: MemoryPayload): Promise<void> {
376
+ printScreenHeader(`${ICONS.edit} Edit Memory`, 'Select a field to modify');
377
+
378
+ const field = await safePrompt(() =>
379
+ select({
380
+ message: accent(' Field to edit'),
381
+ choices: [
382
+ { name: `${ICONS.memo} Content: ${muted(truncate(payload.content, 35))}`, value: 'content' },
383
+ { name: `${ICONS.folder} Category: ${colorCategoryName(payload.category)}`, value: 'category' },
384
+ { name: `${ICONS.star} Importance: ${importanceBadge(payload.importance)}`, value: 'importance' },
385
+ { name: `${ICONS.tag} Tags: ${payload.tags.length ? payload.tags.map(t => info(`#${t}`)).join(' ') : muted('none')}`, value: 'tags' },
386
+ { name: `${ICONS.dot} Subcategory: ${muted(payload.subcategory || 'none')}`, value: 'subcategory' },
387
+ { name: `${ICONS.folder} Project: ${info(payload.project)}`, value: 'project' },
388
+ { name: `${ICONS.back} Cancel`, value: 'cancel' },
389
+ ],
390
+ })
391
+ );
392
+
393
+ if (field === null || field === 'cancel') return;
394
+
395
+ const now = new Date().toISOString();
396
+
397
+ if (field === 'content') {
398
+ const val = await safePrompt(() =>
399
+ input({ message: ' New content:', default: payload.content })
400
+ );
401
+ if (val === null || val === payload.content) return;
402
+ const merged: MemoryPayload = { ...payload, content: val, updated_at: now };
403
+ printSpinner('Re-generating embedding...');
404
+ const vector = await generateEmbedding(val);
405
+ await updateVector(id, vector, merged);
406
+ console.log(success(` ${ICONS.check} Content updated & re-embedded.`));
407
+ } else if (field === 'category') {
408
+ const cats = getAllCategories();
409
+ const val = await safePrompt(() =>
410
+ select({
411
+ message: ' Category:',
412
+ choices: cats.map(c => ({
413
+ name: `${colorCategory(c)} ${dim(getCategoryDescription(c))}`,
414
+ value: c,
415
+ })),
416
+ default: payload.category,
417
+ })
418
+ );
419
+ if (val === null || val === payload.category) return;
420
+ await updatePayload(id, { category: val, updated_at: now });
421
+ console.log(success(` ${ICONS.check} Category ${ICONS.arrow} ${colorCategoryName(val)}`));
422
+ } else if (field === 'importance') {
423
+ const val = await safePrompt(() =>
424
+ input({
425
+ message: ' Importance (1-10):',
426
+ default: String(payload.importance),
427
+ validate: (v: string) => { const n = Number(v); return (Number.isInteger(n) && n >= 1 && n <= 10) || 'Must be 1-10'; },
428
+ })
429
+ );
430
+ if (val === null) return;
431
+ const num = Number(val);
432
+ if (num === payload.importance) return;
433
+ await updatePayload(id, { importance: num, updated_at: now });
434
+ console.log(success(` ${ICONS.check} Importance ${ICONS.arrow} ${importanceBadge(num)}`));
435
+ } else if (field === 'tags') {
436
+ const val = await safePrompt(() =>
437
+ input({
438
+ message: ' Tags (comma-separated):',
439
+ default: payload.tags.join(', '),
440
+ })
441
+ );
442
+ if (val === null) return;
443
+ const tags = val.split(',').map(t => t.trim()).filter(Boolean);
444
+ await updatePayload(id, { tags, updated_at: now });
445
+ console.log(success(` ${ICONS.check} Tags updated: ${tags.map(t => info(`#${t}`)).join(' ') || muted('none')}`));
446
+ } else if (field === 'subcategory') {
447
+ const val = await safePrompt(() =>
448
+ input({ message: ' Subcategory:', default: payload.subcategory || '' })
449
+ );
450
+ if (val === null) return;
451
+ await updatePayload(id, { subcategory: val || undefined, updated_at: now });
452
+ console.log(success(` ${ICONS.check} Subcategory ${ICONS.arrow} ${val || muted('none')}`));
453
+ } else if (field === 'project') {
454
+ const val = await safePrompt(() =>
455
+ input({ message: ' Project:', default: payload.project })
456
+ );
457
+ if (val === null) return;
458
+ await updatePayload(id, { project: val, updated_at: now });
459
+ console.log(success(` ${ICONS.check} Project ${ICONS.arrow} ${info(val)}`));
460
+ }
461
+ }
462
+
463
+ async function browseScreen(): Promise<void> {
464
+ let offset: string | undefined;
465
+
466
+ while (true) {
467
+ printScreenHeader(`${ICONS.brain} Browse Memories`, 'Navigate and select a memory to view details');
468
+ printSpinner('Loading memories...');
469
+
470
+ const result = await scrollMemories(undefined, 20, offset);
471
+ const points = result.points;
472
+
473
+ if (points.length === 0 && !offset) {
474
+ console.log(warn(`\n ${ICONS.cross} No memories found. Create some first!`));
475
+ await safePrompt(() => input({ message: ' Press Enter to go back' }));
476
+ return;
477
+ }
478
+
479
+ // Sort by created_at descending
480
+ points.sort((a, b) => {
481
+ const pa = (a.payload as unknown as MemoryPayload).created_at;
482
+ const pb = (b.payload as unknown as MemoryPayload).created_at;
483
+ return pb.localeCompare(pa);
484
+ });
485
+
486
+ const choices = points.map(pt => ({
487
+ name: ` ${formatMemoryLine(pt.id as string, pt.payload as unknown as MemoryPayload)}`,
488
+ value: pt.id as string,
489
+ }));
490
+
491
+ const nextOffset = result.next_page_offset as string | undefined;
492
+ if (nextOffset) {
493
+ choices.push({ name: info(` ${ICONS.more} Load more...`), value: '__more__' });
494
+ }
495
+ choices.push({ name: dim(` ${ICONS.back} Back to menu`), value: '__back__' });
496
+
497
+ const choice = await safePrompt(() =>
498
+ select({
499
+ message: accent(` ${ICONS.arrow} Select memory`),
500
+ choices,
501
+ pageSize: 15,
502
+ })
503
+ );
504
+
505
+ if (choice === null || choice === '__back__') return;
506
+
507
+ if (choice === '__more__') {
508
+ offset = nextOffset;
509
+ continue;
510
+ }
511
+
512
+ const mem = await getMemory(choice);
513
+ if (mem) {
514
+ await detailView(mem.id as string, mem.payload as unknown as MemoryPayload);
515
+ }
516
+ offset = undefined;
517
+ }
518
+ }
519
+
520
+ async function searchScreen(): Promise<void> {
521
+ while (true) {
522
+ printScreenHeader(`${ICONS.search} Search Memories`, 'Semantic search powered by local embeddings');
523
+
524
+ const query = await safePrompt(() =>
525
+ input({ message: accent(` ${ICONS.search} Query:`) })
526
+ );
527
+ if (query === null || query.trim() === '') return;
528
+
529
+ // Optional category filter
530
+ const cats = getAllCategories();
531
+ const catFilter = await safePrompt(() =>
532
+ select({
533
+ message: accent(' Filter by category'),
534
+ choices: [
535
+ { name: `${ICONS.sparkle} All categories`, value: '__all__' },
536
+ ...cats.map(c => ({ name: ` ${colorCategory(c)}`, value: c })),
537
+ ],
538
+ })
539
+ );
540
+ if (catFilter === null) return;
541
+
542
+ // Optional importance filter
543
+ const minImpStr = await safePrompt(() =>
544
+ input({
545
+ message: accent(' Min importance (1-10, Enter = any):'),
546
+ default: '',
547
+ validate: (v: string) => v === '' || (Number.isInteger(Number(v)) && Number(v) >= 1 && Number(v) <= 10) || 'Must be 1-10 or empty',
548
+ })
549
+ );
550
+ if (minImpStr === null) return;
551
+
552
+ // Build search filter
553
+ const must: Record<string, unknown>[] = [];
554
+ if (catFilter !== '__all__') {
555
+ must.push({ key: 'category', match: { value: catFilter } });
556
+ }
557
+ if (minImpStr !== '') {
558
+ must.push({ key: 'importance', range: { gte: Number(minImpStr) } });
559
+ }
560
+ const filter = must.length > 0 ? { must } : undefined;
561
+
562
+ printSpinner('Generating embedding & searching...');
563
+ const vector = await generateEmbedding(query);
564
+ const results = await searchMemories(vector, 20, filter, 0.3);
565
+
566
+ if (results.length === 0) {
567
+ console.log(warn(`\n ${ICONS.cross} No matching memories found.`));
568
+ const again = await safePrompt(() =>
569
+ confirm({ message: accent(' Try another search?'), default: true })
570
+ );
571
+ if (again === null || !again) return;
572
+ continue;
573
+ }
574
+
575
+ const resultHeader = success(` ${ICONS.check} Found ${results.length} result${results.length !== 1 ? 's' : ''}`);
576
+ console.log(`\n${resultHeader}\n`);
577
+
578
+ const choices = results.map(r => {
579
+ const p = r.payload as unknown as MemoryPayload;
580
+ const pct = Math.round(r.score * 100);
581
+ const scoreBar = pct >= 80 ? success(`${pct}%`) : pct >= 50 ? warn(`${pct}%`) : dim(`${pct}%`);
582
+ return {
583
+ name: ` ${scoreBar.padEnd(18)} ${formatMemoryLine(r.id as string, p)}`,
584
+ value: r.id as string,
585
+ };
586
+ });
587
+ choices.push({ name: info(` ${ICONS.search} Search again`), value: '__again__' });
588
+ choices.push({ name: dim(` ${ICONS.back} Back to menu`), value: '__back__' });
589
+
590
+ const choice = await safePrompt(() =>
591
+ select({
592
+ message: accent(` ${ICONS.arrow} Select result`),
593
+ choices,
594
+ pageSize: 15,
595
+ })
596
+ );
597
+
598
+ if (choice === null || choice === '__back__') return;
599
+ if (choice === '__again__') continue;
600
+
601
+ const mem = await getMemory(choice);
602
+ if (mem) {
603
+ await detailView(mem.id as string, mem.payload as unknown as MemoryPayload);
604
+ }
605
+ }
606
+ }
607
+
608
+ async function createScreen(): Promise<void> {
609
+ printScreenHeader(`${ICONS.plus} Create Memory`, 'Store a new piece of knowledge');
610
+
611
+ // Content
612
+ const content = await safePrompt(() =>
613
+ input({
614
+ message: accent(` ${ICONS.memo} Content:`),
615
+ validate: (v: string) => v.trim().length > 0 || 'Content is required',
616
+ })
617
+ );
618
+ if (content === null) return;
619
+
620
+ // Category
621
+ const cats = getAllCategories();
622
+ const category = await safePrompt(() =>
623
+ select({
624
+ message: accent(` ${ICONS.folder} Category:`),
625
+ choices: cats.map(c => ({
626
+ name: ` ${colorCategory(c)} ${dim(getCategoryDescription(c))}`,
627
+ value: c,
628
+ })),
629
+ })
630
+ );
631
+ if (category === null) return;
632
+
633
+ // Project
634
+ const defaultProject = detectProject();
635
+ const project = await safePrompt(() =>
636
+ input({ message: accent(` ${ICONS.folder} Project:`), default: defaultProject })
637
+ );
638
+ if (project === null) return;
639
+
640
+ // Importance
641
+ const impStr = await safePrompt(() =>
642
+ input({
643
+ message: accent(` ${ICONS.star} Importance (1-10):`),
644
+ default: '5',
645
+ validate: (v: string) => { const n = Number(v); return (Number.isInteger(n) && n >= 1 && n <= 10) || 'Must be 1-10'; },
646
+ })
647
+ );
648
+ if (impStr === null) return;
649
+ const importance = Number(impStr);
650
+
651
+ // Tags
652
+ const tagsStr = await safePrompt(() =>
653
+ input({ message: accent(` ${ICONS.tag} Tags (comma-separated):`), default: '' })
654
+ );
655
+ if (tagsStr === null) return;
656
+ const tags = tagsStr.split(',').map(t => t.trim()).filter(Boolean);
657
+
658
+ // Source
659
+ const source = await safePrompt(() =>
660
+ select<MemorySource>({
661
+ message: accent(` ${ICONS.arrow} Source:`),
662
+ choices: [
663
+ { name: ` ${chalk.white('user-told')} ${dim('User explicitly shared')}`, value: 'user-told' as MemorySource },
664
+ { name: ` ${chalk.white('self-discovered')} ${dim('Found during work')}`, value: 'self-discovered' as MemorySource },
665
+ { name: ` ${chalk.white('auto-saved')} ${dim('Session context')}`, value: 'auto-saved' as MemorySource },
666
+ ],
667
+ default: 'user-told' as MemorySource,
668
+ })
669
+ );
670
+ if (source === null) return;
671
+
672
+ // Subcategory (optional)
673
+ const subcategory = await safePrompt(() =>
674
+ input({ message: accent(` ${ICONS.dot} Subcategory (optional):`), default: '' })
675
+ );
676
+ if (subcategory === null) return;
677
+
678
+ // Build payload
679
+ const now = new Date().toISOString();
680
+ const payload: MemoryPayload = {
681
+ content,
682
+ category,
683
+ project,
684
+ importance,
685
+ tags,
686
+ source,
687
+ subcategory: subcategory || undefined,
688
+ created_at: now,
689
+ updated_at: now,
690
+ accessed_at: now,
691
+ access_count: 0,
692
+ };
693
+
694
+ // Preview
695
+ console.log('\n' + formatMemoryDetail('(preview)', payload));
696
+
697
+ const ok = await safePrompt(() =>
698
+ confirm({ message: accent(` ${ICONS.sparkle} Save this memory?`), default: true })
699
+ );
700
+ if (ok === null || !ok) {
701
+ console.log(warn(` ${ICONS.cross} Cancelled.`));
702
+ return;
703
+ }
704
+
705
+ const id = uuidv4();
706
+ printSpinner('Generating embedding...');
707
+ const vector = await generateEmbedding(content);
708
+ await upsertMemory(id, vector, payload);
709
+ console.log(success(`\n ${ICONS.check} Memory saved!`) + dim(` ID: ${id}`));
710
+ }
711
+
712
+ async function categoriesScreen(): Promise<void> {
713
+ while (true) {
714
+ printScreenHeader(`${ICONS.folder} Categories`, 'Manage memory categories');
715
+
716
+ const action = await safePrompt(() =>
717
+ select({
718
+ message: accent(` ${ICONS.arrow} Action`),
719
+ choices: [
720
+ { name: ` ${ICONS.memo} List all categories`, value: 'list' },
721
+ { name: ` ${ICONS.plus} Create custom category`, value: 'create' },
722
+ { name: ` ${ICONS.trash} Delete custom category`, value: 'delete' },
723
+ { name: dim(` ${ICONS.back} Back to menu`), value: 'back' },
724
+ ],
725
+ })
726
+ );
727
+
728
+ if (action === null || action === 'back') return;
729
+
730
+ if (action === 'list') {
731
+ const W = 52;
732
+ console.log('\n ' + boxTop(W, 'All Categories'));
733
+
734
+ const allCats = getCategories();
735
+ for (const c of allCats) {
736
+ console.log(' ' + boxRow(W, `${colorCategory(c.name)} ${dim(c.description)}`));
737
+ }
738
+
739
+ console.log(' ' + boxBottom(W));
740
+ console.log('');
741
+ }
742
+
743
+ if (action === 'create') {
744
+ const name = await safePrompt(() =>
745
+ input({
746
+ message: accent(' Category name (a-z, 0-9, hyphens):'),
747
+ validate: (v: string) => {
748
+ const result = validateCategoryName(v);
749
+ return result.valid || result.error!;
750
+ },
751
+ })
752
+ );
753
+ if (name === null) continue;
754
+
755
+ const description = await safePrompt(() =>
756
+ input({ message: accent(' Description:'), validate: (v: string) => v.trim().length > 0 || 'Required' })
757
+ );
758
+ if (description === null) continue;
759
+
760
+ addCategory(name, description);
761
+ console.log(success(`\n ${ICONS.check} Category "${name}" created.`));
762
+ }
763
+
764
+ if (action === 'delete') {
765
+ const custom = getCustomCategories();
766
+ if (custom.length === 0) {
767
+ console.log(warn(`\n ${ICONS.cross} No custom categories to delete.`));
768
+ continue;
769
+ }
770
+
771
+ const name = await safePrompt(() =>
772
+ select({
773
+ message: accent(' Delete which category?'),
774
+ choices: [
775
+ ...custom.map(c => ({
776
+ name: ` ${chalk.hex('#FF6B9D')(`${ICONS.sparkle} ${c.name}`)} ${dim(`- ${c.description}`)}`,
777
+ value: c.name,
778
+ })),
779
+ { name: dim(` ${ICONS.back} Cancel`), value: '__cancel__' },
780
+ ],
781
+ })
782
+ );
783
+ if (name === null || name === '__cancel__') continue;
784
+
785
+ const count = await countMemories({
786
+ must: [{ key: 'category', match: { value: name } }],
787
+ });
788
+
789
+ if (count > 0) {
790
+ console.log(warn(`\n ${ICONS.cross} ${count} memories use this category.`));
791
+ const reassign = await safePrompt(() =>
792
+ confirm({ message: accent(' Reassign them to another category?'), default: true })
793
+ );
794
+ if (reassign === null || !reassign) continue;
795
+
796
+ const target = await safePrompt(() =>
797
+ select({
798
+ message: accent(' Reassign to:'),
799
+ choices: getAllCategories()
800
+ .filter(c => c !== name)
801
+ .map(c => ({ name: ` ${colorCategory(c)}`, value: c })),
802
+ })
803
+ );
804
+ if (target === null) continue;
805
+
806
+ await updatePayloadByFilter(
807
+ { must: [{ key: 'category', match: { value: name } }] },
808
+ { category: target, updated_at: new Date().toISOString() }
809
+ );
810
+ console.log(success(` ${ICONS.check} Reassigned ${count} memories to ${colorCategoryName(target)}.`));
811
+ }
812
+
813
+ const ok = await safePrompt(() =>
814
+ confirm({ message: danger(` ${ICONS.trash} Delete category "${name}"?`), default: false })
815
+ );
816
+ if (ok) {
817
+ removeCategory(name);
818
+ console.log(success(` ${ICONS.check} Category "${name}" deleted.`));
819
+ }
820
+ }
821
+ }
822
+ }
823
+
824
+ async function statsScreen(): Promise<void> {
825
+ printScreenHeader(`${ICONS.chart} Statistics`, 'Memory database overview');
826
+ printSpinner('Loading statistics...');
827
+ const stats = await getMemoryStats();
828
+
829
+ const W = 56;
830
+ console.log('\n ' + boxTop(W, 'Overview'));
831
+ console.log(' ' + boxRow(W, `${chalk.bold.white(String(stats.total))} total memories stored`));
832
+ if (stats.oldest) {
833
+ console.log(' ' + boxRow(W, `${muted('Oldest:')} ${chalk.white(formatDate(stats.oldest))} ${dim(`(${formatAge(stats.oldest)})`)}`));
834
+ }
835
+ if (stats.newest) {
836
+ console.log(' ' + boxRow(W, `${muted('Newest:')} ${chalk.white(formatDate(stats.newest))} ${dim(`(${formatAge(stats.newest)})`)}`));
837
+ }
838
+ console.log(' ' + boxBottom(W));
839
+
840
+ // Category chart
841
+ const catEntries = Object.entries(stats.by_category)
842
+ .filter(([, count]) => count > 0)
843
+ .sort((a, b) => b[1] - a[1]);
844
+
845
+ if (catEntries.length > 0) {
846
+ console.log('\n ' + boxTop(W, 'By Category'));
847
+ const maxCount = Math.max(...catEntries.map(([, c]) => c));
848
+ const barWidth = 24;
849
+ for (const [cat, count] of catEntries) {
850
+ const catColorFn = getCategoryColorFn(cat);
851
+ const catIconStr = getCategoryIcon(cat);
852
+ const filled = Math.max(1, Math.round((count / maxCount) * barWidth));
853
+ const empty = barWidth - filled;
854
+ const bar = catColorFn('\u2588'.repeat(filled)) + dim('\u2591'.repeat(empty));
855
+ // icon(1) + space(1) + name + padding = 16 chars visual
856
+ const nameVisual = `${catIconStr} ${cat}`;
857
+ const padLen = Math.max(1, 16 - nameVisual.length);
858
+ const label = catColorFn(nameVisual) + ' '.repeat(padLen);
859
+ console.log(' ' + boxRow(W, `${label}${bar} ${chalk.bold.white(String(count).padStart(3))}`));
860
+ }
861
+ console.log(' ' + boxBottom(W));
862
+ }
863
+
864
+ // Project breakdown
865
+ const projEntries = Object.entries(stats.by_project)
866
+ .sort((a, b) => b[1] - a[1]);
867
+
868
+ if (projEntries.length > 0) {
869
+ console.log('\n ' + boxTop(W, 'By Project'));
870
+ for (const [proj, count] of projEntries) {
871
+ const pct = Math.round((count / stats.total) * 100);
872
+ const pctBar = dim(`(${pct}%)`);
873
+ console.log(' ' + boxRow(W, `${info(proj.padEnd(20))} ${chalk.bold.white(String(count).padStart(4))} ${pctBar}`));
874
+ }
875
+ console.log(' ' + boxBottom(W));
876
+ }
877
+
878
+ console.log('');
879
+ await safePrompt(() => input({ message: dim(' Press Enter to go back') }));
880
+ }
881
+
882
+ // ── Main Menu ─────────────────────────────────────────────────────
883
+
884
+ async function mainMenu(): Promise<void> {
885
+ printLogo();
886
+
887
+ while (true) {
888
+ const choice = await safePrompt(() =>
889
+ select({
890
+ message: accent(` ${ICONS.arrow} Main Menu`),
891
+ choices: [
892
+ { name: ` ${accent2(ICONS.brain)} ${chalk.white('Browse Memories')} ${dim('Paginated list')}`, value: 'browse' },
893
+ { name: ` ${accent2(ICONS.search)} ${chalk.white('Search Memories')} ${dim('Semantic search')}`, value: 'search' },
894
+ { name: ` ${accent2(ICONS.plus)} ${chalk.white('Create Memory')} ${dim('Store new knowledge')}`, value: 'create' },
895
+ { name: ` ${accent2(ICONS.folder)} ${chalk.white('Manage Categories')} ${dim('Create & organize')}`, value: 'categories' },
896
+ { name: ` ${accent2(ICONS.chart)} ${chalk.white('Statistics')} ${dim('Database overview')}`, value: 'stats' },
897
+ { name: dim(` ${ICONS.exit} Exit`), value: 'exit' },
898
+ ],
899
+ })
900
+ );
901
+
902
+ if (choice === null || choice === 'exit') {
903
+ console.log(dim(`\n ${ICONS.exit} Goodbye!\n`));
904
+ process.exit(0);
905
+ }
906
+
907
+ switch (choice) {
908
+ case 'browse': await browseScreen(); break;
909
+ case 'search': await searchScreen(); break;
910
+ case 'create': await createScreen(); break;
911
+ case 'categories': await categoriesScreen(); break;
912
+ case 'stats': await statsScreen(); break;
913
+ }
914
+ }
915
+ }
916
+
917
+ // ── Entry Point ───────────────────────────────────────────────────
918
+
919
+ async function main(): Promise<void> {
920
+ try {
921
+ // Suppress API key warnings
922
+ const origWarn = console.warn;
923
+ console.warn = (...args: unknown[]) => {
924
+ const msg = String(args[0] || '');
925
+ if (msg.includes('unsecure connection')) return;
926
+ origWarn.apply(console, args);
927
+ };
928
+
929
+ await ensureCollection();
930
+
931
+ console.warn = origWarn;
932
+
933
+ await mainMenu();
934
+ } catch (err) {
935
+ console.error(danger(`\n ${ICONS.cross} Fatal error:`), err);
936
+ process.exit(1);
937
+ }
938
+ }
939
+
940
+ main();