synabun 2026.3.307 → 2026.3.310

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