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,379 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ validateCategoryName,
4
+ addCategory,
5
+ getAllCategories,
6
+ getCategories,
7
+ categoryExists,
8
+ updateCategory,
9
+ removeCategory,
10
+ validateCategory,
11
+ getChildCategories,
12
+ getParentCategories,
13
+ getCategoryTree,
14
+ } from '../services/categories.js';
15
+ import { countMemories, updatePayloadByFilter } from '../services/sqlite.js';
16
+ import { refreshCategorySchemas } from '../index.js';
17
+ import { text } from './response.js';
18
+
19
+ export const categorySchema = {
20
+ action: z
21
+ .enum(['create', 'update', 'delete', 'list'] as const)
22
+ .describe('Action to perform: create, update, delete, or list categories.'),
23
+ name: z
24
+ .string()
25
+ .optional()
26
+ .describe('Category name (required for create/update/delete).'),
27
+ description: z
28
+ .string()
29
+ .optional()
30
+ .describe('Category description (required for create, optional for update).'),
31
+ is_parent: z
32
+ .boolean()
33
+ .optional()
34
+ .describe('Make this a parent/cluster category (create only).'),
35
+ parent: z
36
+ .string()
37
+ .optional()
38
+ .describe('Parent category name. Empty string "" to remove parent.'),
39
+ color: z
40
+ .string()
41
+ .optional()
42
+ .describe('Color hex code, e.g. "#3b82f6". Empty string "" to remove.'),
43
+ new_name: z
44
+ .string()
45
+ .optional()
46
+ .describe('Rename category to this (update only).'),
47
+ reassign_to: z
48
+ .string()
49
+ .optional()
50
+ .describe('Reassign memories to this category before deleting (delete only).'),
51
+ reassign_children_to: z
52
+ .string()
53
+ .optional()
54
+ .describe('Reassign child categories to this parent when deleting a parent. Empty string "" for top-level (delete only).'),
55
+ format: z
56
+ .enum(['flat', 'tree', 'parents-only'] as const)
57
+ .optional()
58
+ .describe('Output format for list action: flat, tree (default), or parents-only.'),
59
+ };
60
+
61
+ export const categoryDescription =
62
+ 'Manage memory categories. Actions: create, update, delete, list. Categories persist across sessions.';
63
+
64
+ // ── Create ──────────────────────────────────────────────────────
65
+
66
+ async function handleCreate(args: {
67
+ name?: string;
68
+ description?: string;
69
+ parent?: string;
70
+ color?: string;
71
+ is_parent?: boolean;
72
+ }) {
73
+ if (!args.name) {
74
+ return text('name is required for create action.');
75
+ }
76
+ if (!args.description?.trim()) {
77
+ return text('description is required for create action.');
78
+ }
79
+
80
+ const nameCheck = validateCategoryName(args.name);
81
+ if (!nameCheck.valid) {
82
+ return text(nameCheck.error!);
83
+ }
84
+
85
+ if (args.color && !/^#[0-9a-f]{6}$/i.test(args.color)) {
86
+ return text('Invalid color format. Use hex format: #rrggbb (e.g., #3b82f6)');
87
+ }
88
+
89
+ addCategory(args.name, args.description.trim(), args.parent, args.color, args.is_parent);
90
+ refreshCategorySchemas();
91
+
92
+ const all = getAllCategories();
93
+ let msg = `Created category "${args.name}" (${args.description.trim()})`;
94
+ if (args.is_parent) msg += ` as a parent category`;
95
+ if (args.parent) msg += ` under "${args.parent}" cluster`;
96
+ if (args.color) msg += ` with color ${args.color}`;
97
+ msg += `.\n\nAll categories (${all.length}): ${all.join(', ')}`;
98
+
99
+ return text(msg);
100
+ }
101
+
102
+ // ── Update ──────────────────────────────────────────────────────
103
+
104
+ async function handleUpdate(args: {
105
+ name?: string;
106
+ new_name?: string;
107
+ description?: string;
108
+ parent?: string;
109
+ color?: string;
110
+ }) {
111
+ const { name, new_name, description, parent, color } = args;
112
+
113
+ if (!name) {
114
+ return text('name is required for update action.');
115
+ }
116
+
117
+ if (!categoryExists(name)) {
118
+ return text(`Category "${name}" does not exist. Available: ${getAllCategories().join(', ')}`);
119
+ }
120
+
121
+ if (new_name) {
122
+ const NAME_PATTERN = /^[a-z][a-z0-9-]*$/;
123
+ const MIN_LENGTH = 2;
124
+ const MAX_LENGTH = 30;
125
+
126
+ if (new_name.length < MIN_LENGTH || new_name.length > MAX_LENGTH) {
127
+ return text(`New category name must be ${MIN_LENGTH}-${MAX_LENGTH} characters. Got ${new_name.length}.`);
128
+ }
129
+ if (!NAME_PATTERN.test(new_name)) {
130
+ return text('New category name must match /^[a-z][a-z0-9-]*$/ (lowercase, starts with letter, only letters/digits/hyphens).');
131
+ }
132
+ if (new_name !== name && categoryExists(new_name)) {
133
+ return text(`Category "${new_name}" already exists.`);
134
+ }
135
+ }
136
+
137
+ if (parent !== undefined && parent !== '') {
138
+ if (parent === name) {
139
+ return text(`Cannot set category "${name}" as its own parent. This would create a circular reference.`);
140
+ }
141
+ if (!categoryExists(parent)) {
142
+ return text(`Parent category "${parent}" does not exist. Available: ${getAllCategories().join(', ')}`);
143
+ }
144
+ const categories = getCategories();
145
+ let currentParent = parent;
146
+ while (currentParent) {
147
+ if (currentParent === (new_name || name)) {
148
+ return text(`Cannot set parent to "${parent}": would create circular dependency.`);
149
+ }
150
+ const parentCat = categories.find(c => c.name === currentParent);
151
+ currentParent = parentCat?.parent || '';
152
+ }
153
+ }
154
+
155
+ if (color !== undefined && color !== '' && !/^#[0-9a-f]{6}$/i.test(color)) {
156
+ return text('Invalid color format. Use hex format: #rrggbb (e.g., #3b82f6) or empty string to remove.');
157
+ }
158
+
159
+ const children = getChildCategories(name);
160
+ const isBecomingChild = parent !== undefined && parent !== '';
161
+
162
+ let warningMsg = '';
163
+ if (children.length > 0 && isBecomingChild) {
164
+ warningMsg = `\n\nWarning: This category has ${children.length} child categor${children.length === 1 ? 'y' : 'ies'} (${children.map(c => c.name).join(', ')}). Making it a child category will create nested levels.`;
165
+ }
166
+
167
+ const updates: {
168
+ new_name?: string;
169
+ description?: string;
170
+ parent?: string | null;
171
+ color?: string | null;
172
+ } = {};
173
+
174
+ if (new_name !== undefined) updates.new_name = new_name;
175
+ if (description !== undefined) updates.description = description;
176
+ if (parent !== undefined) updates.parent = parent === '' ? null : parent;
177
+ if (color !== undefined) updates.color = color === '' ? null : color;
178
+
179
+ updateCategory(name, updates);
180
+ refreshCategorySchemas();
181
+
182
+ const all = getAllCategories();
183
+ let msg = `Updated category "${name}"`;
184
+ if (new_name) msg += ` → "${new_name}"`;
185
+ msg += ':';
186
+
187
+ const changes: string[] = [];
188
+ if (description !== undefined) changes.push(`description: "${description}"`);
189
+ if (parent !== undefined) changes.push(`parent: ${parent === '' ? 'none (top-level)' : `"${parent}"`}`);
190
+ if (color !== undefined) changes.push(`color: ${color === '' ? 'auto-assigned' : color}`);
191
+
192
+ if (changes.length > 0) {
193
+ msg += '\n ' + changes.join('\n ');
194
+ }
195
+
196
+ msg += warningMsg;
197
+ msg += `\n\nAll categories (${all.length}): ${all.join(', ')}`;
198
+
199
+ return text(msg);
200
+ }
201
+
202
+ // ── Delete ──────────────────────────────────────────────────────
203
+
204
+ async function handleDelete(args: {
205
+ name?: string;
206
+ reassign_to?: string;
207
+ reassign_children_to?: string;
208
+ }) {
209
+ const { name, reassign_to, reassign_children_to } = args;
210
+
211
+ if (!name) {
212
+ return text('name is required for delete action.');
213
+ }
214
+
215
+ if (!categoryExists(name)) {
216
+ return text(`Category "${name}" does not exist. Available: ${getAllCategories().join(', ')}`);
217
+ }
218
+
219
+ if (reassign_to) {
220
+ const targetCheck = validateCategory(reassign_to);
221
+ if (!targetCheck.valid) {
222
+ return text(`Invalid reassign target: ${targetCheck.error}`);
223
+ }
224
+ if (reassign_to === name) {
225
+ return text('Cannot reassign to the same category being deleted.');
226
+ }
227
+ }
228
+
229
+ const children = getChildCategories(name);
230
+ if (children.length > 0 && reassign_children_to === undefined) {
231
+ return text(
232
+ `Cannot delete parent category "${name}": it has ${children.length} child categor${children.length === 1 ? 'y' : 'ies'} (${children.map(c => c.name).join(', ')}).\n\nProvide reassign_children_to to specify a new parent for them, or use empty string "" to make them top-level categories.`
233
+ );
234
+ }
235
+
236
+ if (reassign_children_to !== undefined && reassign_children_to !== '' && !categoryExists(reassign_children_to)) {
237
+ return text(`Invalid reassign_children_to: category "${reassign_children_to}" does not exist.`);
238
+ }
239
+
240
+ const memoryCount = await countMemories({
241
+ must: [{ key: 'category', match: { value: name } }],
242
+ });
243
+
244
+ if (memoryCount > 0 && !reassign_to) {
245
+ return text(
246
+ `Cannot delete category "${name}": ${memoryCount} memories use it. Provide reassign_to to move them to another category first.`
247
+ );
248
+ }
249
+
250
+ if (memoryCount > 0 && reassign_to) {
251
+ await updatePayloadByFilter(
252
+ { must: [{ key: 'category', match: { value: name } }] },
253
+ { category: reassign_to }
254
+ );
255
+ }
256
+
257
+ let childrenMsg = '';
258
+ if (children.length > 0) {
259
+ const newParent = reassign_children_to === '' ? null : reassign_children_to;
260
+ children.forEach((child) => {
261
+ updateCategory(child.name, { parent: newParent });
262
+ });
263
+
264
+ if (reassign_children_to === '') {
265
+ childrenMsg = ` ${children.length} child categor${children.length === 1 ? 'y' : 'ies'} made top-level.`;
266
+ } else {
267
+ childrenMsg = ` ${children.length} child categor${children.length === 1 ? 'y' : 'ies'} reassigned to "${reassign_children_to}".`;
268
+ }
269
+ }
270
+
271
+ removeCategory(name);
272
+ refreshCategorySchemas();
273
+
274
+ const all = getAllCategories();
275
+ const reassignMsg = memoryCount > 0
276
+ ? ` ${memoryCount} memories reassigned to "${reassign_to}".`
277
+ : '';
278
+
279
+ return text(`Deleted category "${name}".${reassignMsg}${childrenMsg}\n\nRemaining categories (${all.length}): ${all.join(', ')}`);
280
+ }
281
+
282
+ // ── List ────────────────────────────────────────────────────────
283
+
284
+ async function handleList(args: { format?: 'flat' | 'tree' | 'parents-only' }) {
285
+ const format = args.format || 'tree';
286
+ const categories = getCategories();
287
+
288
+ if (format === 'flat') {
289
+ const lines = categories.map((cat) => {
290
+ let line = `- ${cat.name}: ${cat.description}`;
291
+ if (cat.parent) line += ` (parent: ${cat.parent})`;
292
+ if (cat.color) line += ` [${cat.color}]`;
293
+ return line;
294
+ });
295
+
296
+ return text(`All categories (${categories.length}):\n\n${lines.join('\n')}`);
297
+ }
298
+
299
+ if (format === 'parents-only') {
300
+ const parents = getParentCategories();
301
+ const lines = parents.map((cat) => {
302
+ const children = getChildCategories(cat.name);
303
+ let line = `- ${cat.name}: ${cat.description}`;
304
+ if (cat.color) line += ` [${cat.color}]`;
305
+ if (children.length > 0) {
306
+ line += ` (${children.length} children)`;
307
+ }
308
+ return line;
309
+ });
310
+
311
+ return text(`Parent categories (${parents.length}):\n\n${lines.join('\n')}`);
312
+ }
313
+
314
+ // Tree format (default)
315
+ const tree = getCategoryTree();
316
+ const parents = getParentCategories();
317
+ const lines: string[] = [];
318
+
319
+ parents.forEach((parent) => {
320
+ const children = tree[parent.name] || [];
321
+ let parentLine = `## ${parent.name}`;
322
+ if (parent.color) parentLine += ` [${parent.color}]`;
323
+ parentLine += `\n${parent.description}`;
324
+
325
+ lines.push(parentLine);
326
+
327
+ if (children.length > 0) {
328
+ children.forEach((child) => {
329
+ let childLine = ` - ${child.name}: ${child.description}`;
330
+ if (child.color) childLine += ` [${child.color}]`;
331
+ lines.push(childLine);
332
+ });
333
+ } else {
334
+ lines.push(' (no children)');
335
+ }
336
+
337
+ lines.push('');
338
+ });
339
+
340
+ const orphans = categories.filter((c) => !c.parent && !parents.find((p) => p.name === c.name));
341
+ if (orphans.length > 0) {
342
+ lines.push('## Uncategorized');
343
+ orphans.forEach((cat) => {
344
+ let line = ` - ${cat.name}: ${cat.description}`;
345
+ if (cat.color) line += ` [${cat.color}]`;
346
+ lines.push(line);
347
+ });
348
+ }
349
+
350
+ return text(`Category Tree (${categories.length} total, ${parents.length} parent clusters):\n\n${lines.join('\n')}`);
351
+ }
352
+
353
+ // ── Main dispatcher ─────────────────────────────────────────────
354
+
355
+ export async function handleCategory(args: {
356
+ action: string;
357
+ name?: string;
358
+ description?: string;
359
+ is_parent?: boolean;
360
+ parent?: string;
361
+ color?: string;
362
+ new_name?: string;
363
+ reassign_to?: string;
364
+ reassign_children_to?: string;
365
+ format?: string;
366
+ }) {
367
+ switch (args.action) {
368
+ case 'create':
369
+ return handleCreate(args);
370
+ case 'update':
371
+ return handleUpdate(args);
372
+ case 'delete':
373
+ return handleDelete(args);
374
+ case 'list':
375
+ return handleList(args as { format?: 'flat' | 'tree' | 'parents-only' });
376
+ default:
377
+ return text(`Unknown action "${args.action}". Use: create, update, delete, list.`);
378
+ }
379
+ }
@@ -0,0 +1,274 @@
1
+ import { z } from 'zod';
2
+ import * as discord from '../services/discord.js';
3
+ import { text } from './response.js';
4
+
5
+ export const discordChannelSchema = {
6
+ action: z.enum(['create', 'edit', 'delete', 'list', 'permissions'] as const)
7
+ .describe('Action: create, edit, delete, list, or permissions.'),
8
+ guild_id: z.string().optional()
9
+ .describe('Guild ID. Defaults to DISCORD_GUILD_ID env var.'),
10
+ channel: z.string().optional()
11
+ .describe('Channel name or ID (required for edit/delete/permissions).'),
12
+ name: z.string().optional()
13
+ .describe('Channel name (create required, edit optional).'),
14
+ type: z.enum(['text', 'voice', 'category', 'announcement', 'forum', 'stage'] as const).optional()
15
+ .describe('Channel type (create only, default: text). Use "category" for channel categories.'),
16
+ topic: z.string().optional()
17
+ .describe('Channel topic/description (create/edit).'),
18
+ parent: z.string().optional()
19
+ .describe('Parent category name or ID (create/edit). Empty string to remove.'),
20
+ position: z.number().optional()
21
+ .describe('Sort position (create/edit).'),
22
+ nsfw: z.boolean().optional()
23
+ .describe('NSFW flag (create/edit).'),
24
+ slowmode: z.number().optional()
25
+ .describe('Slowmode in seconds, 0-21600 (create/edit).'),
26
+ bitrate: z.number().optional()
27
+ .describe('Voice bitrate in bps (voice channels, create/edit).'),
28
+ user_limit: z.number().optional()
29
+ .describe('Voice user limit, 0=unlimited (voice channels, create/edit).'),
30
+ target: z.string().optional()
31
+ .describe('Role or user name/ID (permissions action). Use "type:role" or "type:user" prefix, or bare name/ID.'),
32
+ allow: z.string().optional()
33
+ .describe('Permission flags to allow, comma-separated (permissions). E.g. "SEND_MESSAGES,VIEW_CHANNEL".'),
34
+ deny: z.string().optional()
35
+ .describe('Permission flags to deny, comma-separated (permissions). E.g. "SEND_MESSAGES".'),
36
+ reason: z.string().optional()
37
+ .describe('Audit log reason.'),
38
+ };
39
+
40
+ export const discordChannelDescription =
41
+ 'Discord channel management. Actions: create, edit, delete, list, permissions. Handles text, voice, categories, forums, stages.';
42
+
43
+ // ── Create ─────────────────────────────────────────────────────
44
+
45
+ async function handleCreate(guildId: string, args: {
46
+ name?: string; type?: string; topic?: string; parent?: string;
47
+ position?: number; nsfw?: boolean; slowmode?: number;
48
+ bitrate?: number; user_limit?: number; reason?: string;
49
+ }) {
50
+ if (!args.name) {
51
+ return text('name is required for create action.');
52
+ }
53
+
54
+ const channelType = args.type ? discord.CHANNEL_TYPES[args.type] : 0;
55
+ if (channelType === undefined) {
56
+ return text(`Unknown channel type "${args.type}". Use: text, voice, category, announcement, forum, stage.`);
57
+ }
58
+
59
+ const data: Record<string, unknown> = {
60
+ name: args.name,
61
+ type: channelType,
62
+ };
63
+
64
+ if (args.topic !== undefined) data.topic = args.topic;
65
+ if (args.position !== undefined) data.position = args.position;
66
+ if (args.nsfw !== undefined) data.nsfw = args.nsfw;
67
+ if (args.slowmode !== undefined) data.rate_limit_per_user = args.slowmode;
68
+ if (args.bitrate !== undefined) data.bitrate = args.bitrate;
69
+ if (args.user_limit !== undefined) data.user_limit = args.user_limit;
70
+
71
+ // Resolve parent category
72
+ if (args.parent) {
73
+ const parentResolved = await discord.resolveChannel(args.parent, guildId);
74
+ if ('error' in parentResolved) return text(`Parent category: ${parentResolved.error}`);
75
+ data.parent_id = parentResolved.id;
76
+ }
77
+
78
+ const res = await discord.createChannel(guildId, data);
79
+ if (res.error) return text(res.error);
80
+
81
+ const ch = res.data as { id: string; name: string; type: number };
82
+ const typeName = discord.CHANNEL_TYPE_NAMES[ch.type] || 'unknown';
83
+ return text(`Created ${typeName} channel #${ch.name} (${ch.id})${args.topic ? ` — "${args.topic}"` : ''}`);
84
+ }
85
+
86
+ // ── Edit ───────────────────────────────────────────────────────
87
+
88
+ async function handleEdit(guildId: string, args: {
89
+ channel?: string; name?: string; topic?: string; parent?: string;
90
+ position?: number; nsfw?: boolean; slowmode?: number;
91
+ bitrate?: number; user_limit?: number; reason?: string;
92
+ }) {
93
+ if (!args.channel) {
94
+ return text('channel is required for edit action.');
95
+ }
96
+
97
+ const resolved = await discord.resolveChannel(args.channel, guildId);
98
+ if ('error' in resolved) return text(resolved.error);
99
+
100
+ const data: Record<string, unknown> = {};
101
+ if (args.name !== undefined) data.name = args.name;
102
+ if (args.topic !== undefined) data.topic = args.topic;
103
+ if (args.position !== undefined) data.position = args.position;
104
+ if (args.nsfw !== undefined) data.nsfw = args.nsfw;
105
+ if (args.slowmode !== undefined) data.rate_limit_per_user = args.slowmode;
106
+ if (args.bitrate !== undefined) data.bitrate = args.bitrate;
107
+ if (args.user_limit !== undefined) data.user_limit = args.user_limit;
108
+
109
+ if (args.parent !== undefined) {
110
+ if (args.parent === '') {
111
+ data.parent_id = null;
112
+ } else {
113
+ const parentResolved = await discord.resolveChannel(args.parent, guildId);
114
+ if ('error' in parentResolved) return text(`Parent category: ${parentResolved.error}`);
115
+ data.parent_id = parentResolved.id;
116
+ }
117
+ }
118
+
119
+ if (Object.keys(data).length === 0) {
120
+ return text('No changes specified. Provide at least one field to update.');
121
+ }
122
+
123
+ const res = await discord.editChannel(resolved.id, data);
124
+ if (res.error) return text(res.error);
125
+
126
+ const changes = Object.entries(data).map(([k, v]) => `${k}: ${JSON.stringify(v)}`).join(', ');
127
+ return text(`Updated #${resolved.name} (${resolved.id}): ${changes}`);
128
+ }
129
+
130
+ // ── Delete ─────────────────────────────────────────────────────
131
+
132
+ async function handleDelete(guildId: string, args: { channel?: string; reason?: string }) {
133
+ if (!args.channel) {
134
+ return text('channel is required for delete action.');
135
+ }
136
+
137
+ const resolved = await discord.resolveChannel(args.channel, guildId);
138
+ if ('error' in resolved) return text(resolved.error);
139
+
140
+ const res = await discord.deleteChannel(resolved.id);
141
+ if (res.error) return text(res.error);
142
+
143
+ return text(`Deleted channel #${resolved.name} (${resolved.id}).`);
144
+ }
145
+
146
+ // ── List ───────────────────────────────────────────────────────
147
+
148
+ async function handleList(guildId: string) {
149
+ const channels = await discord.getGuildChannels(guildId);
150
+ if ('error' in channels) return text((channels as { error: string }).error);
151
+
152
+ const list = channels as { id: string; name: string; type: number; parent_id?: string; position: number; topic?: string }[];
153
+ const categories = list.filter(c => c.type === 4).sort((a, b) => a.position - b.position);
154
+ const uncategorized = list.filter(c => c.type !== 4 && !c.parent_id).sort((a, b) => a.position - b.position);
155
+
156
+ const lines: string[] = [];
157
+
158
+ if (uncategorized.length > 0) {
159
+ lines.push('**No Category:**');
160
+ for (const ch of uncategorized) {
161
+ const typeName = discord.CHANNEL_TYPE_NAMES[ch.type] || 'unknown';
162
+ lines.push(` #${ch.name} (${typeName}, ${ch.id})${ch.topic ? ` — ${ch.topic}` : ''}`);
163
+ }
164
+ lines.push('');
165
+ }
166
+
167
+ for (const cat of categories) {
168
+ const children = list.filter(c => c.parent_id === cat.id).sort((a, b) => a.position - b.position);
169
+ lines.push(`**${cat.name}** (category, ${cat.id})`);
170
+ for (const ch of children) {
171
+ const typeName = discord.CHANNEL_TYPE_NAMES[ch.type] || 'unknown';
172
+ lines.push(` #${ch.name} (${typeName}, ${ch.id})${ch.topic ? ` — ${ch.topic}` : ''}`);
173
+ }
174
+ if (children.length === 0) lines.push(' (empty)');
175
+ lines.push('');
176
+ }
177
+
178
+ return text(`Channels (${list.length}):\n\n${lines.join('\n')}`);
179
+ }
180
+
181
+ // ── Permissions ────────────────────────────────────────────────
182
+
183
+ async function handlePermissions(guildId: string, args: {
184
+ channel?: string; target?: string; allow?: string; deny?: string;
185
+ }) {
186
+ if (!args.channel) {
187
+ return text('channel is required for permissions action.');
188
+ }
189
+ if (!args.target) {
190
+ return text('target (role or user name/ID) is required for permissions action.');
191
+ }
192
+
193
+ const channelResolved = await discord.resolveChannel(args.channel, guildId);
194
+ if ('error' in channelResolved) return text(channelResolved.error);
195
+
196
+ // Determine if target is a role (type 0) or member (type 1)
197
+ // Try role first, then user
198
+ let targetId: string;
199
+ let targetType: number;
200
+ let targetName: string;
201
+
202
+ const roleResolved = await discord.resolveRole(args.target, guildId);
203
+ if (!('error' in roleResolved)) {
204
+ targetId = roleResolved.id;
205
+ targetType = 0; // role
206
+ targetName = `@${roleResolved.name}`;
207
+ } else {
208
+ const userResolved = await discord.resolveUser(args.target, guildId);
209
+ if ('error' in userResolved) {
210
+ return text(`Could not resolve target "${args.target}" as a role or user.`);
211
+ }
212
+ targetId = userResolved.id;
213
+ targetType = 1; // member
214
+ targetName = userResolved.name;
215
+ }
216
+
217
+ let allowBits = '0';
218
+ let denyBits = '0';
219
+
220
+ try {
221
+ if (args.allow) allowBits = discord.resolvePermissions(args.allow);
222
+ if (args.deny) denyBits = discord.resolvePermissions(args.deny);
223
+ } catch (err) {
224
+ return text((err as Error).message);
225
+ }
226
+
227
+ const res = await discord.setChannelPermissions(channelResolved.id, targetId, {
228
+ type: targetType,
229
+ allow: allowBits,
230
+ deny: denyBits,
231
+ });
232
+ if (res.error) return text(res.error);
233
+
234
+ const parts: string[] = [];
235
+ if (args.allow) parts.push(`allow: ${args.allow}`);
236
+ if (args.deny) parts.push(`deny: ${args.deny}`);
237
+
238
+ return text(`Set permissions on #${channelResolved.name} for ${targetName}: ${parts.join(', ')}`);
239
+ }
240
+
241
+ // ── Main dispatcher ────────────────────────────────────────────
242
+
243
+ export async function handleDiscordChannel(args: {
244
+ action: string;
245
+ guild_id?: string;
246
+ channel?: string;
247
+ name?: string;
248
+ type?: string;
249
+ topic?: string;
250
+ parent?: string;
251
+ position?: number;
252
+ nsfw?: boolean;
253
+ slowmode?: number;
254
+ bitrate?: number;
255
+ user_limit?: number;
256
+ target?: string;
257
+ allow?: string;
258
+ deny?: string;
259
+ reason?: string;
260
+ }) {
261
+ const resolved = await discord.resolveGuildId(args.guild_id);
262
+ if ('error' in resolved) return text(resolved.error);
263
+ const guildId = resolved.guildId;
264
+
265
+ switch (args.action) {
266
+ case 'create': return handleCreate(guildId, args);
267
+ case 'edit': return handleEdit(guildId, args);
268
+ case 'delete': return handleDelete(guildId, args);
269
+ case 'list': return handleList(guildId);
270
+ case 'permissions': return handlePermissions(guildId, args);
271
+ default:
272
+ return text(`Unknown action "${args.action}". Use: create, edit, delete, list, permissions.`);
273
+ }
274
+ }