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,702 @@
1
+ /**
2
+ * Discord REST API v10 client for MCP tools.
3
+ * Follows the neural-interface.ts pattern: typed HTTP client with
4
+ * timeout handling, rate limit retry, and name resolution helpers.
5
+ */
6
+
7
+ const API_BASE = 'https://discord.com/api/v10';
8
+ const DEFAULT_TIMEOUT = 10_000;
9
+ const LONG_TIMEOUT = 30_000;
10
+ const MAX_RETRIES = 3;
11
+
12
+ // ── Types ──────────────────────────────────────────────────────
13
+
14
+ export interface DiscordResponse {
15
+ ok?: boolean;
16
+ error?: string;
17
+ data?: unknown;
18
+ [key: string]: unknown;
19
+ }
20
+
21
+ interface CacheEntry<T> {
22
+ data: T;
23
+ expiresAt: number;
24
+ }
25
+
26
+ // ── Permission flags ───────────────────────────────────────────
27
+
28
+ export const PERMISSION_FLAGS: Record<string, bigint> = {
29
+ CREATE_INSTANT_INVITE: 1n << 0n,
30
+ KICK_MEMBERS: 1n << 1n,
31
+ BAN_MEMBERS: 1n << 2n,
32
+ ADMINISTRATOR: 1n << 3n,
33
+ MANAGE_CHANNELS: 1n << 4n,
34
+ MANAGE_GUILD: 1n << 5n,
35
+ ADD_REACTIONS: 1n << 6n,
36
+ VIEW_AUDIT_LOG: 1n << 7n,
37
+ PRIORITY_SPEAKER: 1n << 8n,
38
+ STREAM: 1n << 9n,
39
+ VIEW_CHANNEL: 1n << 10n,
40
+ SEND_MESSAGES: 1n << 11n,
41
+ SEND_TTS_MESSAGES: 1n << 12n,
42
+ MANAGE_MESSAGES: 1n << 13n,
43
+ EMBED_LINKS: 1n << 14n,
44
+ ATTACH_FILES: 1n << 15n,
45
+ READ_MESSAGE_HISTORY: 1n << 16n,
46
+ MENTION_EVERYONE: 1n << 17n,
47
+ USE_EXTERNAL_EMOJIS: 1n << 18n,
48
+ VIEW_GUILD_INSIGHTS: 1n << 19n,
49
+ CONNECT: 1n << 20n,
50
+ SPEAK: 1n << 21n,
51
+ MUTE_MEMBERS: 1n << 22n,
52
+ DEAFEN_MEMBERS: 1n << 23n,
53
+ MOVE_MEMBERS: 1n << 24n,
54
+ USE_VAD: 1n << 25n,
55
+ CHANGE_NICKNAME: 1n << 26n,
56
+ MANAGE_NICKNAMES: 1n << 27n,
57
+ MANAGE_ROLES: 1n << 28n,
58
+ MANAGE_WEBHOOKS: 1n << 29n,
59
+ MANAGE_GUILD_EXPRESSIONS: 1n << 30n,
60
+ USE_APPLICATION_COMMANDS: 1n << 31n,
61
+ REQUEST_TO_SPEAK: 1n << 32n,
62
+ MANAGE_EVENTS: 1n << 33n,
63
+ MANAGE_THREADS: 1n << 34n,
64
+ CREATE_PUBLIC_THREADS: 1n << 35n,
65
+ CREATE_PRIVATE_THREADS: 1n << 36n,
66
+ USE_EXTERNAL_STICKERS: 1n << 37n,
67
+ SEND_MESSAGES_IN_THREADS: 1n << 38n,
68
+ USE_EMBEDDED_ACTIVITIES: 1n << 39n,
69
+ MODERATE_MEMBERS: 1n << 40n,
70
+ VIEW_CREATOR_MONETIZATION_ANALYTICS: 1n << 41n,
71
+ USE_SOUNDBOARD: 1n << 42n,
72
+ CREATE_GUILD_EXPRESSIONS: 1n << 43n,
73
+ CREATE_EVENTS: 1n << 44n,
74
+ USE_EXTERNAL_SOUNDS: 1n << 45n,
75
+ SEND_VOICE_MESSAGES: 1n << 46n,
76
+ SEND_POLLS: 1n << 49n,
77
+ USE_EXTERNAL_APPS: 1n << 50n,
78
+ };
79
+
80
+ /** Convert comma-separated permission flag names to a bitfield string. */
81
+ export function resolvePermissions(flags: string): string {
82
+ let bits = 0n;
83
+ for (const flag of flags.split(',').map(f => f.trim().toUpperCase())) {
84
+ const val = PERMISSION_FLAGS[flag];
85
+ if (!val) {
86
+ const available = Object.keys(PERMISSION_FLAGS).join(', ');
87
+ throw new Error(`Unknown permission flag: "${flag}". Available: ${available}`);
88
+ }
89
+ bits |= val;
90
+ }
91
+ return bits.toString();
92
+ }
93
+
94
+ // ── Channel type mapping ───────────────────────────────────────
95
+
96
+ export const CHANNEL_TYPES: Record<string, number> = {
97
+ text: 0,
98
+ voice: 2,
99
+ category: 4,
100
+ announcement: 5,
101
+ stage: 13,
102
+ forum: 15,
103
+ };
104
+
105
+ export const CHANNEL_TYPE_NAMES: Record<number, string> = {
106
+ 0: 'text',
107
+ 2: 'voice',
108
+ 4: 'category',
109
+ 5: 'announcement',
110
+ 10: 'announcement-thread',
111
+ 11: 'public-thread',
112
+ 12: 'private-thread',
113
+ 13: 'stage',
114
+ 15: 'forum',
115
+ };
116
+
117
+ // ── Verification levels ────────────────────────────────────────
118
+
119
+ export const VERIFICATION_LEVELS: Record<string, number> = {
120
+ none: 0,
121
+ low: 1,
122
+ medium: 2,
123
+ high: 3,
124
+ very_high: 4,
125
+ };
126
+
127
+ // ── Cache ──────────────────────────────────────────────────────
128
+
129
+ const cache = new Map<string, CacheEntry<unknown>>();
130
+ const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
131
+
132
+ function getCached<T>(key: string): T | null {
133
+ const entry = cache.get(key);
134
+ if (!entry || Date.now() > entry.expiresAt) {
135
+ cache.delete(key);
136
+ return null;
137
+ }
138
+ return entry.data as T;
139
+ }
140
+
141
+ function setCache<T>(key: string, data: T): void {
142
+ cache.set(key, { data, expiresAt: Date.now() + CACHE_TTL });
143
+ }
144
+
145
+ export function invalidateCache(pattern?: string): void {
146
+ if (!pattern) {
147
+ cache.clear();
148
+ return;
149
+ }
150
+ for (const key of cache.keys()) {
151
+ if (key.includes(pattern)) cache.delete(key);
152
+ }
153
+ }
154
+
155
+ // ── Token validation ───────────────────────────────────────────
156
+
157
+ function getToken(): string {
158
+ const token = process.env.DISCORD_BOT_TOKEN;
159
+ if (!token) {
160
+ throw new Error(
161
+ 'DISCORD_BOT_TOKEN not set. Add it to your .env file:\n' +
162
+ 'DISCORD_BOT_TOKEN=your_bot_token_here\n\n' +
163
+ 'Create a bot at https://discord.com/developers/applications'
164
+ );
165
+ }
166
+ return token;
167
+ }
168
+
169
+ // ── Core HTTP request ──────────────────────────────────────────
170
+
171
+ export async function discordRequest(
172
+ method: string,
173
+ path: string,
174
+ body?: Record<string, unknown>,
175
+ timeout = DEFAULT_TIMEOUT,
176
+ retryCount = 0
177
+ ): Promise<DiscordResponse> {
178
+ let token: string;
179
+ try {
180
+ token = getToken();
181
+ } catch (err) {
182
+ return { error: (err as Error).message };
183
+ }
184
+
185
+ const controller = new AbortController();
186
+ const timer = setTimeout(() => controller.abort(), timeout);
187
+
188
+ try {
189
+ const opts: RequestInit = {
190
+ method,
191
+ headers: {
192
+ 'Authorization': `Bot ${token}`,
193
+ 'Content-Type': 'application/json',
194
+ 'User-Agent': 'SynaBun-MCP (https://github.com/synabun, 1.0.0)',
195
+ },
196
+ signal: controller.signal,
197
+ };
198
+ if (body && method !== 'GET') {
199
+ opts.body = JSON.stringify(body);
200
+ }
201
+
202
+ const res = await fetch(`${API_BASE}${path}`, opts);
203
+
204
+ // Rate limited — retry with Retry-After
205
+ if (res.status === 429 && retryCount < MAX_RETRIES) {
206
+ const retryData = await res.json() as { retry_after?: number };
207
+ const retryAfter = retryData.retry_after || 1;
208
+ await new Promise(r => setTimeout(r, retryAfter * 1000));
209
+ return discordRequest(method, path, body, timeout, retryCount + 1);
210
+ }
211
+
212
+ // No content (successful delete, etc.)
213
+ if (res.status === 204) {
214
+ return { ok: true };
215
+ }
216
+
217
+ const data = await res.json() as Record<string, unknown>;
218
+
219
+ if (!res.ok) {
220
+ // Discord error response
221
+ const message = data.message as string || `HTTP ${res.status}`;
222
+ const code = data.code as number;
223
+ return { error: `Discord API error ${code || res.status}: ${message}` };
224
+ }
225
+
226
+ return { ok: true, data };
227
+ } catch (err: unknown) {
228
+ const msg = err instanceof Error ? err.message : String(err);
229
+ if (msg.includes('abort')) {
230
+ return { error: `Request timed out after ${timeout}ms` };
231
+ }
232
+ return { error: `Discord API unreachable: ${msg}` };
233
+ } finally {
234
+ clearTimeout(timer);
235
+ }
236
+ }
237
+
238
+ // ── Guild resolution ───────────────────────────────────────────
239
+
240
+ export async function resolveGuildId(guildId?: string): Promise<{ guildId: string } | { error: string }> {
241
+ if (guildId) return { guildId };
242
+
243
+ const envGuild = process.env.DISCORD_GUILD_ID;
244
+ if (envGuild) return { guildId: envGuild };
245
+
246
+ // Auto-detect: if bot is in exactly 1 guild, use it
247
+ const res = await discordRequest('GET', '/users/@me/guilds');
248
+ if (res.error) return { error: res.error };
249
+ const guilds = res.data as { id: string; name: string }[];
250
+
251
+ if (guilds.length === 1) return { guildId: guilds[0].id };
252
+ if (guilds.length === 0) return { error: 'Bot is not in any guilds. Invite it to a server first.' };
253
+
254
+ const list = guilds.map(g => ` ${g.id} — ${g.name}`).join('\n');
255
+ return { error: `Bot is in multiple guilds. Specify guild_id:\n${list}\n\nOr set DISCORD_GUILD_ID in your .env file.` };
256
+ }
257
+
258
+ // ── Name resolution helpers ────────────────────────────────────
259
+
260
+ const SNOWFLAKE_PATTERN = /^\d{17,20}$/;
261
+
262
+ function isSnowflake(value: string): boolean {
263
+ return SNOWFLAKE_PATTERN.test(value);
264
+ }
265
+
266
+ export async function resolveChannel(
267
+ nameOrId: string,
268
+ guildId: string
269
+ ): Promise<{ id: string; name: string; type: number } | { error: string }> {
270
+ if (isSnowflake(nameOrId)) {
271
+ return { id: nameOrId, name: nameOrId, type: -1 };
272
+ }
273
+
274
+ const channels = await getGuildChannels(guildId);
275
+ if (!Array.isArray(channels)) return { error: (channels as DiscordResponse).error || 'Failed to fetch channels' };
276
+
277
+ const normalized = nameOrId.toLowerCase().replace(/^#/, '');
278
+ const matches = channels
279
+ .filter(c => c.name.toLowerCase() === normalized);
280
+
281
+ if (matches.length === 0) {
282
+ return { error: `Channel "${nameOrId}" not found in guild.` };
283
+ }
284
+ if (matches.length > 1) {
285
+ const list = matches.map(c => ` ${c.id} — #${c.name} (${CHANNEL_TYPE_NAMES[c.type] || 'unknown'})`).join('\n');
286
+ return { error: `Multiple channels named "${nameOrId}". Specify by ID:\n${list}` };
287
+ }
288
+ return matches[0];
289
+ }
290
+
291
+ export async function resolveRole(
292
+ nameOrId: string,
293
+ guildId: string
294
+ ): Promise<{ id: string; name: string } | { error: string }> {
295
+ if (isSnowflake(nameOrId)) {
296
+ return { id: nameOrId, name: nameOrId };
297
+ }
298
+
299
+ const roles = await getGuildRoles(guildId);
300
+ if (!Array.isArray(roles)) return { error: (roles as DiscordResponse).error || 'Failed to fetch roles' };
301
+
302
+ const normalized = nameOrId.toLowerCase().replace(/^@/, '');
303
+ const matches = roles
304
+ .filter(r => r.name.toLowerCase() === normalized);
305
+
306
+ if (matches.length === 0) {
307
+ return { error: `Role "${nameOrId}" not found in guild.` };
308
+ }
309
+ if (matches.length > 1) {
310
+ const list = matches.map(r => ` ${r.id} — @${r.name}`).join('\n');
311
+ return { error: `Multiple roles named "${nameOrId}". Specify by ID:\n${list}` };
312
+ }
313
+ return matches[0];
314
+ }
315
+
316
+ export async function resolveUser(
317
+ nameOrTag: string,
318
+ guildId: string
319
+ ): Promise<{ id: string; name: string } | { error: string }> {
320
+ if (isSnowflake(nameOrTag)) {
321
+ return { id: nameOrTag, name: nameOrTag };
322
+ }
323
+
324
+ // Search guild members by query
325
+ const res = await discordRequest('GET', `/guilds/${guildId}/members/search?query=${encodeURIComponent(nameOrTag)}&limit=10`);
326
+ if (res.error) return { error: res.error };
327
+
328
+ const members = res.data as { user: { id: string; username: string; global_name?: string } }[];
329
+ if (members.length === 0) {
330
+ return { error: `User "${nameOrTag}" not found in guild.` };
331
+ }
332
+ if (members.length === 1) {
333
+ return { id: members[0].user.id, name: members[0].user.global_name || members[0].user.username };
334
+ }
335
+
336
+ const list = members.map(m =>
337
+ ` ${m.user.id} — ${m.user.global_name || m.user.username} (@${m.user.username})`
338
+ ).join('\n');
339
+ return { error: `Multiple users match "${nameOrTag}". Specify by ID:\n${list}` };
340
+ }
341
+
342
+ // ── Guild operations ───────────────────────────────────────────
343
+
344
+ export async function getGuild(guildId: string): Promise<DiscordResponse> {
345
+ return discordRequest('GET', `/guilds/${guildId}?with_counts=true`);
346
+ }
347
+
348
+ export async function getGuildChannels(guildId: string): Promise<DiscordResponse | { id: string; name: string; type: number }[]> {
349
+ const cacheKey = `channels:${guildId}`;
350
+ const cached = getCached<{ id: string; name: string; type: number }[]>(cacheKey);
351
+ if (cached) return cached;
352
+
353
+ const res = await discordRequest('GET', `/guilds/${guildId}/channels`);
354
+ if (res.error) return { error: res.error };
355
+
356
+ const channels = res.data as { id: string; name: string; type: number }[];
357
+ setCache(cacheKey, channels);
358
+ return channels;
359
+ }
360
+
361
+ export async function getGuildMembers(
362
+ guildId: string,
363
+ limit = 100,
364
+ after?: string
365
+ ): Promise<DiscordResponse> {
366
+ let path = `/guilds/${guildId}/members?limit=${limit}`;
367
+ if (after) path += `&after=${after}`;
368
+ return discordRequest('GET', path);
369
+ }
370
+
371
+ export async function getGuildRoles(guildId: string): Promise<DiscordResponse | { id: string; name: string; color: number; position: number }[]> {
372
+ const cacheKey = `roles:${guildId}`;
373
+ const cached = getCached<{ id: string; name: string; color: number; position: number }[]>(cacheKey);
374
+ if (cached) return cached;
375
+
376
+ const res = await discordRequest('GET', `/guilds/${guildId}/roles`);
377
+ if (res.error) return { error: res.error };
378
+
379
+ const roles = res.data as { id: string; name: string; color: number; position: number }[];
380
+ setCache(cacheKey, roles);
381
+ return roles;
382
+ }
383
+
384
+ export async function getAuditLog(
385
+ guildId: string,
386
+ opts?: { action_type?: number; limit?: number }
387
+ ): Promise<DiscordResponse> {
388
+ let path = `/guilds/${guildId}/audit-logs?limit=${opts?.limit || 50}`;
389
+ if (opts?.action_type !== undefined) path += `&action_type=${opts.action_type}`;
390
+ return discordRequest('GET', path);
391
+ }
392
+
393
+ export async function editGuildSettings(
394
+ guildId: string,
395
+ settings: Record<string, unknown>
396
+ ): Promise<DiscordResponse> {
397
+ return discordRequest('PATCH', `/guilds/${guildId}`, settings);
398
+ }
399
+
400
+ // ── Channel operations ─────────────────────────────────────────
401
+
402
+ export async function createChannel(
403
+ guildId: string,
404
+ data: Record<string, unknown>
405
+ ): Promise<DiscordResponse> {
406
+ invalidateCache(`channels:${guildId}`);
407
+ return discordRequest('POST', `/guilds/${guildId}/channels`, data);
408
+ }
409
+
410
+ export async function editChannel(
411
+ channelId: string,
412
+ data: Record<string, unknown>
413
+ ): Promise<DiscordResponse> {
414
+ invalidateCache('channels:');
415
+ return discordRequest('PATCH', `/channels/${channelId}`, data);
416
+ }
417
+
418
+ export async function deleteChannel(channelId: string): Promise<DiscordResponse> {
419
+ invalidateCache('channels:');
420
+ return discordRequest('DELETE', `/channels/${channelId}`);
421
+ }
422
+
423
+ export async function setChannelPermissions(
424
+ channelId: string,
425
+ overwriteId: string,
426
+ data: { type: number; allow: string; deny: string }
427
+ ): Promise<DiscordResponse> {
428
+ return discordRequest('PUT', `/channels/${channelId}/permissions/${overwriteId}`, data as unknown as Record<string, unknown>);
429
+ }
430
+
431
+ // ── Role operations ────────────────────────────────────────────
432
+
433
+ export async function createRole(
434
+ guildId: string,
435
+ data: Record<string, unknown>
436
+ ): Promise<DiscordResponse> {
437
+ invalidateCache(`roles:${guildId}`);
438
+ return discordRequest('POST', `/guilds/${guildId}/roles`, data);
439
+ }
440
+
441
+ export async function editRole(
442
+ guildId: string,
443
+ roleId: string,
444
+ data: Record<string, unknown>
445
+ ): Promise<DiscordResponse> {
446
+ invalidateCache(`roles:${guildId}`);
447
+ return discordRequest('PATCH', `/guilds/${guildId}/roles/${roleId}`, data);
448
+ }
449
+
450
+ export async function deleteRole(
451
+ guildId: string,
452
+ roleId: string
453
+ ): Promise<DiscordResponse> {
454
+ invalidateCache(`roles:${guildId}`);
455
+ return discordRequest('DELETE', `/guilds/${guildId}/roles/${roleId}`);
456
+ }
457
+
458
+ export async function addMemberRole(
459
+ guildId: string,
460
+ userId: string,
461
+ roleId: string
462
+ ): Promise<DiscordResponse> {
463
+ return discordRequest('PUT', `/guilds/${guildId}/members/${userId}/roles/${roleId}`, {});
464
+ }
465
+
466
+ export async function removeMemberRole(
467
+ guildId: string,
468
+ userId: string,
469
+ roleId: string
470
+ ): Promise<DiscordResponse> {
471
+ return discordRequest('DELETE', `/guilds/${guildId}/members/${userId}/roles/${roleId}`);
472
+ }
473
+
474
+ // ── Message operations ─────────────────────────────────────────
475
+
476
+ export async function sendMessage(
477
+ channelId: string,
478
+ data: Record<string, unknown>
479
+ ): Promise<DiscordResponse> {
480
+ return discordRequest('POST', `/channels/${channelId}/messages`, data);
481
+ }
482
+
483
+ export async function editMessage(
484
+ channelId: string,
485
+ messageId: string,
486
+ data: Record<string, unknown>
487
+ ): Promise<DiscordResponse> {
488
+ return discordRequest('PATCH', `/channels/${channelId}/messages/${messageId}`, data);
489
+ }
490
+
491
+ export async function deleteMessage(
492
+ channelId: string,
493
+ messageId: string
494
+ ): Promise<DiscordResponse> {
495
+ return discordRequest('DELETE', `/channels/${channelId}/messages/${messageId}`);
496
+ }
497
+
498
+ export async function pinMessage(
499
+ channelId: string,
500
+ messageId: string
501
+ ): Promise<DiscordResponse> {
502
+ return discordRequest('PUT', `/channels/${channelId}/pins/${messageId}`, {});
503
+ }
504
+
505
+ export async function unpinMessage(
506
+ channelId: string,
507
+ messageId: string
508
+ ): Promise<DiscordResponse> {
509
+ return discordRequest('DELETE', `/channels/${channelId}/pins/${messageId}`);
510
+ }
511
+
512
+ export async function addReaction(
513
+ channelId: string,
514
+ messageId: string,
515
+ emoji: string
516
+ ): Promise<DiscordResponse> {
517
+ const encoded = encodeURIComponent(emoji);
518
+ return discordRequest('PUT', `/channels/${channelId}/messages/${messageId}/reactions/${encoded}/@me`, {});
519
+ }
520
+
521
+ export async function getMessages(
522
+ channelId: string,
523
+ opts?: { limit?: number; before?: string; after?: string }
524
+ ): Promise<DiscordResponse> {
525
+ let path = `/channels/${channelId}/messages?limit=${opts?.limit || 50}`;
526
+ if (opts?.before) path += `&before=${opts.before}`;
527
+ if (opts?.after) path += `&after=${opts.after}`;
528
+ return discordRequest('GET', path);
529
+ }
530
+
531
+ export async function bulkDeleteMessages(
532
+ channelId: string,
533
+ messageIds: string[]
534
+ ): Promise<DiscordResponse> {
535
+ return discordRequest('POST', `/channels/${channelId}/messages/bulk-delete`, { messages: messageIds });
536
+ }
537
+
538
+ // ── Member operations ──────────────────────────────────────────
539
+
540
+ export async function getMember(
541
+ guildId: string,
542
+ userId: string
543
+ ): Promise<DiscordResponse> {
544
+ return discordRequest('GET', `/guilds/${guildId}/members/${userId}`);
545
+ }
546
+
547
+ export async function kickMember(
548
+ guildId: string,
549
+ userId: string,
550
+ reason?: string
551
+ ): Promise<DiscordResponse> {
552
+ const headers = reason ? { 'X-Audit-Log-Reason': encodeURIComponent(reason) } : undefined;
553
+ // Can't pass custom headers through discordRequest — use reason in query if needed
554
+ return discordRequest('DELETE', `/guilds/${guildId}/members/${userId}`);
555
+ }
556
+
557
+ export async function banMember(
558
+ guildId: string,
559
+ userId: string,
560
+ data?: { delete_message_seconds?: number }
561
+ ): Promise<DiscordResponse> {
562
+ return discordRequest('PUT', `/guilds/${guildId}/bans/${userId}`, data || {});
563
+ }
564
+
565
+ export async function unbanMember(
566
+ guildId: string,
567
+ userId: string
568
+ ): Promise<DiscordResponse> {
569
+ return discordRequest('DELETE', `/guilds/${guildId}/bans/${userId}`);
570
+ }
571
+
572
+ export async function timeoutMember(
573
+ guildId: string,
574
+ userId: string,
575
+ durationMinutes: number
576
+ ): Promise<DiscordResponse> {
577
+ const until = durationMinutes > 0
578
+ ? new Date(Date.now() + durationMinutes * 60_000).toISOString()
579
+ : null;
580
+ return discordRequest('PATCH', `/guilds/${guildId}/members/${userId}`, {
581
+ communication_disabled_until: until,
582
+ });
583
+ }
584
+
585
+ export async function setNickname(
586
+ guildId: string,
587
+ userId: string,
588
+ nick: string | null
589
+ ): Promise<DiscordResponse> {
590
+ return discordRequest('PATCH', `/guilds/${guildId}/members/${userId}`, { nick });
591
+ }
592
+
593
+ // ── Onboarding operations ──────────────────────────────────────
594
+
595
+ export async function getWelcomeScreen(guildId: string): Promise<DiscordResponse> {
596
+ return discordRequest('GET', `/guilds/${guildId}/welcome-screen`);
597
+ }
598
+
599
+ export async function setWelcomeScreen(
600
+ guildId: string,
601
+ data: Record<string, unknown>
602
+ ): Promise<DiscordResponse> {
603
+ return discordRequest('PATCH', `/guilds/${guildId}/welcome-screen`, data);
604
+ }
605
+
606
+ export async function getOnboarding(guildId: string): Promise<DiscordResponse> {
607
+ return discordRequest('GET', `/guilds/${guildId}/onboarding`);
608
+ }
609
+
610
+ export async function setOnboarding(
611
+ guildId: string,
612
+ data: Record<string, unknown>
613
+ ): Promise<DiscordResponse> {
614
+ return discordRequest('PUT', `/guilds/${guildId}/onboarding`, data);
615
+ }
616
+
617
+ // ── Webhook operations ─────────────────────────────────────────
618
+
619
+ export async function createWebhook(
620
+ channelId: string,
621
+ data: { name: string; avatar?: string }
622
+ ): Promise<DiscordResponse> {
623
+ return discordRequest('POST', `/channels/${channelId}/webhooks`, data as unknown as Record<string, unknown>);
624
+ }
625
+
626
+ export async function editWebhook(
627
+ webhookId: string,
628
+ data: Record<string, unknown>
629
+ ): Promise<DiscordResponse> {
630
+ return discordRequest('PATCH', `/webhooks/${webhookId}`, data);
631
+ }
632
+
633
+ export async function deleteWebhook(webhookId: string): Promise<DiscordResponse> {
634
+ return discordRequest('DELETE', `/webhooks/${webhookId}`);
635
+ }
636
+
637
+ export async function listChannelWebhooks(channelId: string): Promise<DiscordResponse> {
638
+ return discordRequest('GET', `/channels/${channelId}/webhooks`);
639
+ }
640
+
641
+ export async function listGuildWebhooks(guildId: string): Promise<DiscordResponse> {
642
+ return discordRequest('GET', `/guilds/${guildId}/webhooks`);
643
+ }
644
+
645
+ export async function executeWebhook(
646
+ webhookId: string,
647
+ webhookToken: string,
648
+ data: Record<string, unknown>
649
+ ): Promise<DiscordResponse> {
650
+ // Webhook execution uses token auth, not bot auth — make direct fetch call
651
+ const controller = new AbortController();
652
+ const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);
653
+
654
+ try {
655
+ const res = await fetch(`${API_BASE}/webhooks/${webhookId}/${webhookToken}?wait=true`, {
656
+ method: 'POST',
657
+ headers: { 'Content-Type': 'application/json' },
658
+ signal: controller.signal,
659
+ body: JSON.stringify(data),
660
+ });
661
+
662
+ if (res.status === 204) return { ok: true };
663
+ const result = await res.json() as Record<string, unknown>;
664
+ if (!res.ok) {
665
+ return { error: `Webhook error ${res.status}: ${result.message || 'Unknown error'}` };
666
+ }
667
+ return { ok: true, data: result };
668
+ } catch (err: unknown) {
669
+ const msg = err instanceof Error ? err.message : String(err);
670
+ return { error: `Webhook execution failed: ${msg}` };
671
+ } finally {
672
+ clearTimeout(timer);
673
+ }
674
+ }
675
+
676
+ // ── Thread operations ──────────────────────────────────────────
677
+
678
+ export async function createThread(
679
+ channelId: string,
680
+ data: Record<string, unknown>
681
+ ): Promise<DiscordResponse> {
682
+ return discordRequest('POST', `/channels/${channelId}/threads`, data);
683
+ }
684
+
685
+ export async function createThreadFromMessage(
686
+ channelId: string,
687
+ messageId: string,
688
+ data: Record<string, unknown>
689
+ ): Promise<DiscordResponse> {
690
+ return discordRequest('POST', `/channels/${channelId}/messages/${messageId}/threads`, data);
691
+ }
692
+
693
+ export async function modifyThread(
694
+ threadId: string,
695
+ data: Record<string, unknown>
696
+ ): Promise<DiscordResponse> {
697
+ return discordRequest('PATCH', `/channels/${threadId}`, data);
698
+ }
699
+
700
+ export async function deleteThread(threadId: string): Promise<DiscordResponse> {
701
+ return discordRequest('DELETE', `/channels/${threadId}`);
702
+ }