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,429 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { config } from '../config.js';
4
+ import { getCategories as getCategoriesFromDb, saveCategories as saveCategoriesToDb } from './sqlite.js';
5
+
6
+ interface CustomCategory {
7
+ name: string;
8
+ description: string;
9
+ created_at: string;
10
+ parent?: string; // Optional parent category for clustering
11
+ color?: string; // Optional color hex code (e.g., "#3b82f6")
12
+ is_parent?: boolean; // True if this category is a parent/cluster container
13
+ }
14
+
15
+ interface CustomCategoriesFile {
16
+ version: number;
17
+ categories: CustomCategory[];
18
+ }
19
+
20
+ const NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
21
+ const MIN_LENGTH = 2;
22
+ const MAX_LENGTH = 30;
23
+
24
+ // Callback to notify when categories change externally
25
+ let onExternalChangeCallback: (() => void) | null = null;
26
+
27
+ // Rich color palette for category visualization (36 distinct colors)
28
+ const COLOR_PALETTE = [
29
+ // Blues & Cyans
30
+ '#3b82f6', // Blue
31
+ '#0ea5e9', // Sky
32
+ '#06b6d4', // Cyan
33
+ '#0891b2', // Dark Cyan
34
+ '#1e40af', // Deep Blue
35
+ '#38bdf8', // Light Sky
36
+
37
+ // Purples & Violets
38
+ '#8b5cf6', // Purple
39
+ '#a855f7', // Violet
40
+ '#6366f1', // Indigo
41
+ '#c084fc', // Light Purple
42
+ '#7c3aed', // Deep Violet
43
+ '#d946ef', // Fuchsia
44
+
45
+ // Pinks & Roses
46
+ '#ec4899', // Pink
47
+ '#f43f5e', // Rose
48
+ '#db2777', // Deep Pink
49
+ '#f472b6', // Light Pink
50
+ '#be185d', // Dark Rose
51
+ '#fb7185', // Coral Pink
52
+
53
+ // Reds & Oranges
54
+ '#ef4444', // Red
55
+ '#f97316', // Orange
56
+ '#dc2626', // Dark Red
57
+ '#fb923c', // Light Orange
58
+ '#b91c1c', // Crimson
59
+ '#fdba74', // Peach
60
+
61
+ // Yellows & Ambers
62
+ '#eab308', // Yellow
63
+ '#f59e0b', // Amber
64
+ '#fbbf24', // Gold
65
+ '#fcd34d', // Light Yellow
66
+ '#d97706', // Dark Amber
67
+ '#fde047', // Bright Yellow
68
+
69
+ // Greens
70
+ '#22c55e', // Green
71
+ '#10b981', // Emerald
72
+ '#84cc16', // Lime
73
+ '#4ade80', // Light Green
74
+ '#059669', // Dark Emerald
75
+ '#14b8a6', // Teal
76
+ ];
77
+
78
+ let cachedCategories: CustomCategory[] | null = null;
79
+ let fileWatcher: fs.FSWatcher | null = null;
80
+ let watchedFilePath: string | null = null;
81
+
82
+ export function invalidateCategoryCache(): void {
83
+ cachedCategories = null;
84
+ }
85
+
86
+ export function setOnExternalChange(callback: () => void): void {
87
+ onExternalChangeCallback = callback;
88
+ }
89
+
90
+ /**
91
+ * Start watching the per-connection categories cache file.
92
+ * Re-entrant: if the active connection changed, stops the old watcher and starts a new one.
93
+ */
94
+ export function startWatchingCategories(): void {
95
+ const filePath = getCategoriesPath();
96
+
97
+ // If already watching the correct file, do nothing
98
+ if (fileWatcher && watchedFilePath === filePath) return;
99
+
100
+ // Stop old watcher if switching files
101
+ stopWatchingCategories();
102
+
103
+ // Ensure the cache file exists — start empty (SQLite is source of truth)
104
+ if (!fs.existsSync(filePath)) {
105
+ const dir = path.dirname(filePath);
106
+ if (!fs.existsSync(dir)) {
107
+ fs.mkdirSync(dir, { recursive: true });
108
+ }
109
+ saveCategoriesToDisk([]);
110
+ }
111
+
112
+ fileWatcher = fs.watch(filePath, (eventType) => {
113
+ if (eventType === 'change') {
114
+ // Invalidate cache and notify
115
+ invalidateCategoryCache();
116
+ if (onExternalChangeCallback) {
117
+ onExternalChangeCallback();
118
+ }
119
+ }
120
+ });
121
+ watchedFilePath = filePath;
122
+ }
123
+
124
+ export function stopWatchingCategories(): void {
125
+ if (fileWatcher) {
126
+ fileWatcher.close();
127
+ fileWatcher = null;
128
+ }
129
+ watchedFilePath = null;
130
+ }
131
+
132
+ /**
133
+ * Returns the categories cache file path.
134
+ * Single file since we use one SQLite database (no multi-connection concept).
135
+ */
136
+ function getCategoriesPath(): string {
137
+ return path.join(config.dataDir, 'custom-categories.json');
138
+ }
139
+
140
+ function loadCategoriesFromDisk(): CustomCategory[] {
141
+ const filePath = getCategoriesPath();
142
+ try {
143
+ const raw = fs.readFileSync(filePath, 'utf-8');
144
+ const data: CustomCategoriesFile = JSON.parse(raw);
145
+ if (data.version === 1 && Array.isArray(data.categories)) {
146
+ return data.categories;
147
+ }
148
+ return [];
149
+ } catch {
150
+ return [];
151
+ }
152
+ }
153
+
154
+ function saveCategoriesToDisk(categories: CustomCategory[]): void {
155
+ const filePath = getCategoriesPath();
156
+ const dir = path.dirname(filePath);
157
+ if (!fs.existsSync(dir)) {
158
+ fs.mkdirSync(dir, { recursive: true });
159
+ }
160
+ const data: CustomCategoriesFile = { version: 1, categories };
161
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
162
+
163
+ // Fire-and-forget write-through to SQLite (source of truth)
164
+ saveCategoriesToDb(categories).catch((err) => {
165
+ console.error('Failed to sync categories to SQLite:', err instanceof Error ? err.message : err);
166
+ });
167
+ }
168
+
169
+ /**
170
+ * Initialize the local category cache for the active connection.
171
+ * If the per-connection cache file doesn't exist, tries to load from database first,
172
+ * then falls back to the old global file, then starts empty.
173
+ */
174
+ export async function initCategoryCache(): Promise<void> {
175
+ const filePath = getCategoriesPath();
176
+
177
+ if (fs.existsSync(filePath)) {
178
+ // Cache file exists, just load it
179
+ invalidateCategoryCache();
180
+ return;
181
+ }
182
+
183
+ const dir = path.dirname(filePath);
184
+ if (!fs.existsSync(dir)) {
185
+ fs.mkdirSync(dir, { recursive: true });
186
+ }
187
+
188
+ // Try loading from SQLite (source of truth)
189
+ const dbCategories = await getCategoriesFromDb();
190
+ if (dbCategories) {
191
+ const data: CustomCategoriesFile = { version: 1, categories: dbCategories };
192
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
193
+ invalidateCategoryCache();
194
+ return;
195
+ }
196
+
197
+ // Database empty — start with empty categories
198
+ const data: CustomCategoriesFile = { version: 1, categories: [] };
199
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
200
+
201
+ invalidateCategoryCache();
202
+ }
203
+
204
+ /**
205
+ * Call after a connection switch to re-point the file watcher
206
+ * at the new connection's category file and sync from database if needed.
207
+ */
208
+ export async function switchCategoryConnection(): Promise<void> {
209
+ invalidateCategoryCache();
210
+ await initCategoryCache();
211
+ startWatchingCategories(); // Will detect different path and re-watch
212
+ }
213
+
214
+ export function getCategories(): CustomCategory[] {
215
+ if (cachedCategories === null) {
216
+ cachedCategories = loadCategoriesFromDisk();
217
+ }
218
+ return cachedCategories;
219
+ }
220
+
221
+ /** @deprecated Use getCategories() instead. Kept for backward compatibility. */
222
+ export function getCustomCategories(): CustomCategory[] {
223
+ return getCategories();
224
+ }
225
+
226
+ export function getAllCategories(): string[] {
227
+ return getCategories().map((c) => c.name);
228
+ }
229
+
230
+ export function getCategoryDescription(name: string): string {
231
+ const cat = getCategories().find((c) => c.name === name);
232
+ return cat?.description || '';
233
+ }
234
+
235
+ export function categoryExists(name: string): boolean {
236
+ return getAllCategories().includes(name);
237
+ }
238
+
239
+ export function validateCategoryName(name: string): { valid: boolean; error?: string } {
240
+ if (name.length < MIN_LENGTH || name.length > MAX_LENGTH) {
241
+ return { valid: false, error: `Category name must be ${MIN_LENGTH}-${MAX_LENGTH} characters. Got ${name.length}.` };
242
+ }
243
+ if (!NAME_PATTERN.test(name)) {
244
+ return { valid: false, error: `Category name must match /^[a-z][a-z0-9-]*$/ (lowercase, starts with letter, only letters/digits/hyphens).` };
245
+ }
246
+ if (categoryExists(name)) {
247
+ return { valid: false, error: `Category "${name}" already exists.` };
248
+ }
249
+ return { valid: true };
250
+ }
251
+
252
+ export function validateCategory(name: string): { valid: boolean; error?: string } {
253
+ if (categoryExists(name)) {
254
+ return { valid: true };
255
+ }
256
+ const all = getAllCategories();
257
+ return {
258
+ valid: false,
259
+ error: `Unknown category "${name}". Valid categories: ${all.join(', ')}`,
260
+ };
261
+ }
262
+
263
+ export function addCategory(name: string, description: string, parent?: string, color?: string, is_parent?: boolean): void {
264
+ const cats = getCategories();
265
+ const newCat: CustomCategory = {
266
+ name,
267
+ description,
268
+ created_at: new Date().toISOString(),
269
+ };
270
+
271
+ if (parent) newCat.parent = parent;
272
+ if (color) newCat.color = color;
273
+ if (is_parent && !parent) newCat.is_parent = true;
274
+
275
+ cats.push(newCat);
276
+ cachedCategories = cats;
277
+ saveCategoriesToDisk(cats);
278
+ }
279
+
280
+ export function removeCategory(name: string): void {
281
+ const cats = getCategories().filter((c) => c.name !== name);
282
+ cachedCategories = cats;
283
+ saveCategoriesToDisk(cats);
284
+ }
285
+
286
+ export function updateCategory(
287
+ name: string,
288
+ updates: {
289
+ new_name?: string;
290
+ description?: string;
291
+ parent?: string | null;
292
+ color?: string | null;
293
+ is_parent?: boolean;
294
+ }
295
+ ): void {
296
+ const cats = getCategories();
297
+ const catIndex = cats.findIndex((c) => c.name === name);
298
+
299
+ if (catIndex === -1) {
300
+ throw new Error(`Category "${name}" not found`);
301
+ }
302
+
303
+ const cat = cats[catIndex];
304
+
305
+ // If renaming, update all children that reference this category as parent
306
+ if (updates.new_name && updates.new_name !== name) {
307
+ cats.forEach((c) => {
308
+ if (c.parent === name) {
309
+ c.parent = updates.new_name;
310
+ }
311
+ });
312
+ cat.name = updates.new_name;
313
+ }
314
+
315
+ // Update other fields
316
+ if (updates.description !== undefined) {
317
+ cat.description = updates.description;
318
+ }
319
+ if (updates.parent !== undefined) {
320
+ if (updates.parent === null) {
321
+ delete cat.parent;
322
+ } else {
323
+ cat.parent = updates.parent;
324
+ }
325
+ }
326
+ if (updates.color !== undefined) {
327
+ if (updates.color === null) {
328
+ delete cat.color;
329
+ } else {
330
+ cat.color = updates.color;
331
+ }
332
+ }
333
+ if (updates.is_parent !== undefined) {
334
+ if (updates.is_parent && !cat.parent) {
335
+ cat.is_parent = true;
336
+ } else {
337
+ delete cat.is_parent;
338
+ }
339
+ }
340
+
341
+ cachedCategories = cats;
342
+ saveCategoriesToDisk(cats);
343
+ }
344
+
345
+ export function buildCategoryDescription(): string {
346
+ const cats = getCategories();
347
+ const parentCats = cats.filter((c) => c.is_parent);
348
+ const childCats = cats.filter((c) => c.parent);
349
+ const standaloneCats = cats.filter((c) => !c.is_parent && !c.parent);
350
+
351
+ const lines: string[] = [];
352
+
353
+ // Parent categories with their children
354
+ for (const parent of parentCats) {
355
+ const children = childCats.filter((c) => c.parent === parent.name);
356
+ lines.push(`[${parent.name}] (parent) — ${parent.description}`);
357
+ if (children.length > 0) {
358
+ for (const child of children) {
359
+ lines.push(` ${child.name}=${child.description}`);
360
+ }
361
+ }
362
+ }
363
+
364
+ // Standalone categories (no parent, not a parent)
365
+ if (standaloneCats.length > 0) {
366
+ for (const cat of standaloneCats) {
367
+ lines.push(`${cat.name}=${cat.description}`);
368
+ }
369
+ }
370
+
371
+ return 'Read each category description as a guideline for what belongs there. Match your memory to the most specific category. Parent categories group related children.\n' + lines.join(', ');
372
+ }
373
+
374
+ // Parent/child category helpers
375
+ export function getParentCategories(): CustomCategory[] {
376
+ return getCategories().filter((c) => !c.parent);
377
+ }
378
+
379
+ export function getChildCategories(parentName: string): CustomCategory[] {
380
+ return getCategories().filter((c) => c.parent === parentName);
381
+ }
382
+
383
+ export function getCategoryTree(): Record<string, CustomCategory[]> {
384
+ const parents = getParentCategories();
385
+ const tree: Record<string, CustomCategory[]> = {};
386
+
387
+ // Add parent categories
388
+ parents.forEach((parent) => {
389
+ tree[parent.name] = getChildCategories(parent.name);
390
+ });
391
+
392
+ // Add standalone categories (no parent)
393
+ const orphans = getCategories().filter((c) => !c.parent && !tree[c.name]);
394
+ if (orphans.length > 0) {
395
+ tree['_uncategorized'] = orphans;
396
+ }
397
+
398
+ return tree;
399
+ }
400
+
401
+ export function assignColor(categoryName: string): string {
402
+ const cats = getCategories();
403
+ const cat = cats.find((c) => c.name === categoryName);
404
+ if (cat?.color) return cat.color;
405
+
406
+ // Auto-assign from palette based on hash
407
+ const hash = categoryName.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0);
408
+ return COLOR_PALETTE[hash % COLOR_PALETTE.length];
409
+ }
410
+
411
+ export function updateCategoryColor(name: string, color: string): void {
412
+ const cats = getCategories();
413
+ const cat = cats.find((c) => c.name === name);
414
+ if (cat) {
415
+ cat.color = color;
416
+ cachedCategories = cats;
417
+ saveCategoriesToDisk(cats);
418
+ }
419
+ }
420
+
421
+ export function getCategoryWithMeta(name: string): (CustomCategory & { color: string }) | null {
422
+ const cat = getCategories().find((c) => c.name === name);
423
+ if (!cat) return null;
424
+
425
+ return {
426
+ ...cat,
427
+ color: cat.color || assignColor(name),
428
+ };
429
+ }