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.
- package/CHANGELOG.md +665 -0
- package/LICENSE +191 -0
- package/README.md +853 -0
- package/hooks/claude-code/post-plan.mjs +391 -0
- package/hooks/claude-code/post-remember.mjs +239 -0
- package/hooks/claude-code/pre-compact.mjs +241 -0
- package/hooks/claude-code/pre-websearch.mjs +90 -0
- package/hooks/claude-code/prompt-submit.mjs +861 -0
- package/hooks/claude-code/session-start.mjs +494 -0
- package/hooks/claude-code/shared.mjs +368 -0
- package/hooks/claude-code/stop.mjs +646 -0
- package/hooks/claude-code/test-user-learning.mjs +531 -0
- package/mcp-server/package.json +38 -0
- package/mcp-server/run.mjs +10 -0
- package/mcp-server/src/config.ts +70 -0
- package/mcp-server/src/http.ts +93 -0
- package/mcp-server/src/index.ts +203 -0
- package/mcp-server/src/preload-http.ts +25 -0
- package/mcp-server/src/preload.ts +29 -0
- package/mcp-server/src/scripts/migrate-qdrant-to-sqlite.ts +506 -0
- package/mcp-server/src/services/categories.ts +429 -0
- package/mcp-server/src/services/discord.ts +702 -0
- package/mcp-server/src/services/file-checksums.ts +33 -0
- package/mcp-server/src/services/local-embeddings.ts +77 -0
- package/mcp-server/src/services/neural-interface.ts +344 -0
- package/mcp-server/src/services/sqlite.ts +823 -0
- package/mcp-server/src/tools/browser-advanced.ts +123 -0
- package/mcp-server/src/tools/browser-interact.ts +255 -0
- package/mcp-server/src/tools/browser-navigate.ts +77 -0
- package/mcp-server/src/tools/browser-observe.ts +1330 -0
- package/mcp-server/src/tools/browser.ts +107 -0
- package/mcp-server/src/tools/card-tools.ts +193 -0
- package/mcp-server/src/tools/card.ts +22 -0
- package/mcp-server/src/tools/category.ts +379 -0
- package/mcp-server/src/tools/discord-channel.ts +274 -0
- package/mcp-server/src/tools/discord-guild.ts +188 -0
- package/mcp-server/src/tools/discord-member.ts +158 -0
- package/mcp-server/src/tools/discord-message.ts +231 -0
- package/mcp-server/src/tools/discord-onboarding.ts +228 -0
- package/mcp-server/src/tools/discord-role.ts +209 -0
- package/mcp-server/src/tools/discord-thread.ts +110 -0
- package/mcp-server/src/tools/discord-webhook.ts +176 -0
- package/mcp-server/src/tools/discord.ts +40 -0
- package/mcp-server/src/tools/forget.ts +38 -0
- package/mcp-server/src/tools/git-tools.ts +141 -0
- package/mcp-server/src/tools/git.ts +13 -0
- package/mcp-server/src/tools/image-tools.ts +98 -0
- package/mcp-server/src/tools/image.ts +17 -0
- package/mcp-server/src/tools/leonardo-browser-tools.ts +281 -0
- package/mcp-server/src/tools/leonardo.ts +28 -0
- package/mcp-server/src/tools/loop.ts +348 -0
- package/mcp-server/src/tools/memories.ts +118 -0
- package/mcp-server/src/tools/recall.ts +323 -0
- package/mcp-server/src/tools/reflect.ts +145 -0
- package/mcp-server/src/tools/remember.ts +115 -0
- package/mcp-server/src/tools/response.ts +26 -0
- package/mcp-server/src/tools/restore.ts +34 -0
- package/mcp-server/src/tools/sync.ts +114 -0
- package/mcp-server/src/tools/tictactoe-tools.ts +98 -0
- package/mcp-server/src/tools/tictactoe.ts +13 -0
- package/mcp-server/src/tools/utils.ts +25 -0
- package/mcp-server/src/tools/whiteboard-tools.ts +260 -0
- package/mcp-server/src/tools/whiteboard.ts +22 -0
- package/mcp-server/src/tui.ts +940 -0
- package/mcp-server/src/types.ts +85 -0
- package/mcp-server/tsconfig.json +19 -0
- package/neural-interface/package.json +27 -0
- package/neural-interface/public/apple-touch-icon.png +0 -0
- package/neural-interface/public/category-logos/claude.svg +1 -0
- package/neural-interface/public/category-logos/gemini-color.svg +1 -0
- package/neural-interface/public/category-logos/openai.svg +1 -0
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude-chat.css +878 -0
- package/neural-interface/public/claude-chat.html +92 -0
- package/neural-interface/public/claude-chat.js +1168 -0
- package/neural-interface/public/favicon-16x16.png +0 -0
- package/neural-interface/public/favicon-32x32.png +0 -0
- package/neural-interface/public/gallery.svg +6 -0
- package/neural-interface/public/icon-192.png +0 -0
- package/neural-interface/public/icon-512.png +0 -0
- package/neural-interface/public/index-legacy.html +14677 -0
- package/neural-interface/public/index.html +212 -0
- package/neural-interface/public/index2d-legacy.html +8193 -0
- package/neural-interface/public/index2d.html +127 -0
- package/neural-interface/public/invite.html +351 -0
- package/neural-interface/public/leonardoai.svg +1 -0
- package/neural-interface/public/logo-synabun.png +0 -0
- package/neural-interface/public/logoHD.png +0 -0
- package/neural-interface/public/manifest.json +14 -0
- package/neural-interface/public/offline.html +299 -0
- package/neural-interface/public/onboarding.html +2335 -0
- package/neural-interface/public/openclaw-logo-text.png +0 -0
- package/neural-interface/public/pin-svgrepo-com.svg +25 -0
- package/neural-interface/public/sessionicon.svg +8 -0
- package/neural-interface/public/shared/ansi-parser.js +305 -0
- package/neural-interface/public/shared/api.js +1043 -0
- package/neural-interface/public/shared/colors.js +153 -0
- package/neural-interface/public/shared/constants.js +141 -0
- package/neural-interface/public/shared/html-shell.js +777 -0
- package/neural-interface/public/shared/html-term-renderer.js +1022 -0
- package/neural-interface/public/shared/i18n.js +147 -0
- package/neural-interface/public/shared/registry.js +82 -0
- package/neural-interface/public/shared/state.js +86 -0
- package/neural-interface/public/shared/storage.js +150 -0
- package/neural-interface/public/shared/styles.css +21212 -0
- package/neural-interface/public/shared/ui-automation-studio.js +3573 -0
- package/neural-interface/public/shared/ui-bookmarks.js +231 -0
- package/neural-interface/public/shared/ui-claude-panel.js +6333 -0
- package/neural-interface/public/shared/ui-command-runner.js +534 -0
- package/neural-interface/public/shared/ui-cost-widget.js +602 -0
- package/neural-interface/public/shared/ui-detail.js +1454 -0
- package/neural-interface/public/shared/ui-explorer.js +1356 -0
- package/neural-interface/public/shared/ui-file-explorer.js +2750 -0
- package/neural-interface/public/shared/ui-games.js +411 -0
- package/neural-interface/public/shared/ui-help.js +107 -0
- package/neural-interface/public/shared/ui-image-gallery.js +461 -0
- package/neural-interface/public/shared/ui-invite.js +519 -0
- package/neural-interface/public/shared/ui-keybinds.js +452 -0
- package/neural-interface/public/shared/ui-layouts.js +297 -0
- package/neural-interface/public/shared/ui-link.js +731 -0
- package/neural-interface/public/shared/ui-loading.js +255 -0
- package/neural-interface/public/shared/ui-menubar.js +499 -0
- package/neural-interface/public/shared/ui-multiselect.js +93 -0
- package/neural-interface/public/shared/ui-navbar.js +222 -0
- package/neural-interface/public/shared/ui-notifications.js +287 -0
- package/neural-interface/public/shared/ui-panels.js +309 -0
- package/neural-interface/public/shared/ui-resume.js +653 -0
- package/neural-interface/public/shared/ui-search.js +145 -0
- package/neural-interface/public/shared/ui-sessions.js +531 -0
- package/neural-interface/public/shared/ui-settings.js +5987 -0
- package/neural-interface/public/shared/ui-sidebar.js +1905 -0
- package/neural-interface/public/shared/ui-skills.js +2805 -0
- package/neural-interface/public/shared/ui-stats.js +38 -0
- package/neural-interface/public/shared/ui-sync.js +201 -0
- package/neural-interface/public/shared/ui-terminal.js +6385 -0
- package/neural-interface/public/shared/ui-tooltip.js +123 -0
- package/neural-interface/public/shared/ui-trash.js +439 -0
- package/neural-interface/public/shared/ui-tutorial-draw.js +875 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +2276 -0
- package/neural-interface/public/shared/ui-tutorial.js +1117 -0
- package/neural-interface/public/shared/ui-update.js +244 -0
- package/neural-interface/public/shared/ui-whiteboard.js +3262 -0
- package/neural-interface/public/shared/ui-workspaces.js +722 -0
- package/neural-interface/public/shared/utils.js +360 -0
- package/neural-interface/public/shared/vterm-buffer.js +1102 -0
- package/neural-interface/public/sw.js +52 -0
- package/neural-interface/public/synabun.png +0 -0
- package/neural-interface/public/toolbar-album.svg +8 -0
- package/neural-interface/public/toolbar-question.svg +6 -0
- package/neural-interface/public/toolbar-sessions.svg +6 -0
- package/neural-interface/public/variant/2d/background.js +113 -0
- package/neural-interface/public/variant/2d/context-menu.js +133 -0
- package/neural-interface/public/variant/2d/gfx.js +45 -0
- package/neural-interface/public/variant/2d/graph.js +1635 -0
- package/neural-interface/public/variant/2d/hulls.js +207 -0
- package/neural-interface/public/variant/2d/lasso.js +135 -0
- package/neural-interface/public/variant/2d/layout.js +594 -0
- package/neural-interface/public/variant/2d/main.js +521 -0
- package/neural-interface/public/variant/2d/minimap.js +312 -0
- package/neural-interface/public/variant/2d/settings-gfx.js +98 -0
- package/neural-interface/public/variant/3d/background.js +884 -0
- package/neural-interface/public/variant/3d/camera.js +661 -0
- package/neural-interface/public/variant/3d/gfx.js +116 -0
- package/neural-interface/public/variant/3d/graph.js +2399 -0
- package/neural-interface/public/variant/3d/main.js +500 -0
- package/neural-interface/public/variant/3d/settings-gfx.js +228 -0
- package/neural-interface/scripts/rebuild-pty.js +100 -0
- package/neural-interface/server.js +15000 -0
- package/neural-interface/templates/CLAUDE-template.md +211 -0
- package/package.json +53 -0
- package/postinstall.js +135 -0
- package/setup.js +260 -0
- package/skills/leonardo/SKILL.md +159 -0
- package/skills/leonardo/icon.svg +1 -0
- package/skills/leonardo/modules/browser-guide.md +378 -0
- package/skills/leonardo/modules/image-prompter.md +277 -0
- package/skills/leonardo/modules/model-advisor.md +100 -0
- package/skills/leonardo/modules/prompt-library.md +110 -0
- package/skills/leonardo/modules/style-guide.md +186 -0
- package/skills/leonardo/modules/video-prompter.md +311 -0
- package/skills/synabun/SKILL.md +185 -0
- package/skills/synabun/icon.png +0 -0
- package/skills/synabun/modules/audit.md +374 -0
- package/skills/synabun/modules/changelog.md +194 -0
- package/skills/synabun/modules/idea.md +144 -0
- package/skills/synabun/modules/memorize.md +188 -0
- 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
|
+
}
|