dis-gravity 1.1.0 → 1.2.5

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.
@@ -0,0 +1,512 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Dis-Gravity Shared Onboarding Engine
5
+ * Shared between CLI daemon (bin/dis-gravity.js) and VS Code Extension (dist/extension.js).
6
+ * Zero runtime dependencies (uses native Node 18+ global fetch).
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const os = require('os');
12
+ const cp = require('child_process');
13
+
14
+ const DISCORD_API_BASE = 'https://discord.com/api/v10';
15
+ const GATEWAY_MESSAGE_CONTENT_LIMITED = 1 << 19; // 524288
16
+
17
+ // Permissions needed: Manage Channels (16), View Channel (1024), Send Messages (2048),
18
+ // Embed Links (16384), Attach Files (32768), Read Message History (65536),
19
+ // Add Reactions (64), Send Messages in Threads (274877906944)
20
+ const DISGRAVITY_PERMISSIONS = (
21
+ 16n |
22
+ 64n |
23
+ 1024n |
24
+ 2048n |
25
+ 16384n |
26
+ 32768n |
27
+ 65536n |
28
+ 274877906944n
29
+ ).toString(); // "274878024256"
30
+
31
+ const CONFIG_DIR = path.join(os.homedir(), '.dis-gravity');
32
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
33
+
34
+ /**
35
+ * Validates whether string looks like a Discord Bot Token
36
+ */
37
+ function looksLikeBotToken(token) {
38
+ if (!token || typeof token !== 'string') return false;
39
+ const trimmed = token.trim().replace(/^Bot\s+/i, '').trim();
40
+ // Format: [20+ chars].[5+ chars].[25+ chars]
41
+ return /^[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{25,}$/.test(trimmed);
42
+ }
43
+
44
+ /**
45
+ * Builds OAuth2 invite link with exact required permissions and bot + applications.commands scopes
46
+ */
47
+ function buildInviteUrl(clientId, permissions = DISGRAVITY_PERMISSIONS) {
48
+ if (!clientId) throw new Error('clientId is required to build invite URL');
49
+ return `https://discord.com/oauth2/authorize?client_id=${clientId}&scope=bot%20applications.commands&permissions=${permissions}`;
50
+ }
51
+
52
+ /**
53
+ * Low-level Discord REST API request handler with automatic rate-limit retry
54
+ */
55
+ async function discordRequest(token, method, endpoint, body = null, retries = 3) {
56
+ if (!token) throw new Error('Discord token is required for API requests');
57
+ if (typeof fetch !== 'function') {
58
+ throw new Error('Native fetch is not available in this Node runtime. Node 18+ is required.');
59
+ }
60
+
61
+ const cleanToken = token.trim().replace(/^Bot\s+/i, '');
62
+ const url = endpoint.startsWith('http') ? endpoint : `${DISCORD_API_BASE}${endpoint.startsWith('/') ? '' : '/'}${endpoint}`;
63
+ const options = {
64
+ method,
65
+ headers: {
66
+ Authorization: `Bot ${cleanToken}`,
67
+ 'Content-Type': 'application/json',
68
+ 'User-Agent': 'DisGravity-Onboarding/1.1 (https://gitlab.com/rethinakumarsambath8/dis-gravity)'
69
+ }
70
+ };
71
+
72
+ if (body && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
73
+ options.body = typeof body === 'string' ? body : JSON.stringify(body);
74
+ }
75
+
76
+ const res = await fetch(url, options);
77
+
78
+ // Rate-limiting handler (HTTP 429)
79
+ if (res.status === 429 && retries > 0) {
80
+ let waitMs = 1500;
81
+ try {
82
+ const data = await res.json();
83
+ if (data.retry_after) waitMs = Math.ceil(data.retry_after * 1000) + 200;
84
+ } catch {
85
+ const retryHeader = res.headers.get('retry-after');
86
+ if (retryHeader) waitMs = Math.ceil(parseFloat(retryHeader) * 1000) + 200;
87
+ }
88
+ await new Promise((r) => setTimeout(r, waitMs));
89
+ return discordRequest(token, method, endpoint, body, retries - 1);
90
+ }
91
+
92
+ if (res.status === 204) {
93
+ return null;
94
+ }
95
+
96
+ let json;
97
+ const text = await res.text();
98
+ try {
99
+ json = JSON.parse(text);
100
+ } catch {
101
+ json = { raw: text };
102
+ }
103
+
104
+ if (!res.ok) {
105
+ const err = new Error(json.message || `Discord API error (${res.status}): ${text.substring(0, 200)}`);
106
+ err.status = res.status;
107
+ err.code = json.code;
108
+ err.data = json;
109
+ throw err;
110
+ }
111
+
112
+ return json;
113
+ }
114
+
115
+ /**
116
+ * Low-level Discord REST API multipart/form-data request handler for file attachments
117
+ * @param {string} token - Discord Bot Token
118
+ * @param {string} method - HTTP method ('POST' | 'PATCH')
119
+ * @param {string} endpoint - API endpoint
120
+ * @param {object} payloadJson - JSON payload object (content, embeds, components, etc.)
121
+ * @param {Array<{name: string, buffer: Buffer, contentType?: string}>} files - Files to attach
122
+ * @param {number} retries - Rate-limit retry count
123
+ */
124
+ async function discordMultipartRequest(token, method, endpoint, payloadJson = {}, files = [], retries = 2) {
125
+ if (!token) throw new Error('Discord token is required for API requests');
126
+ if (typeof fetch !== 'function' || typeof FormData !== 'function' || typeof Blob !== 'function') {
127
+ throw new Error('Native fetch/FormData/Blob are required (Node 18+).');
128
+ }
129
+
130
+ const cleanToken = token.trim().replace(/^Bot\s+/i, '');
131
+ const url = endpoint.startsWith('http') ? endpoint : `${DISCORD_API_BASE}${endpoint.startsWith('/') ? '' : '/'}${endpoint}`;
132
+
133
+ const form = new FormData();
134
+ const attachmentsMeta = files.map((f, idx) => ({
135
+ id: idx,
136
+ filename: f.name || `file-${idx}.png`
137
+ }));
138
+
139
+ const bodyPayload = {
140
+ ...payloadJson,
141
+ ...(attachmentsMeta.length > 0 ? { attachments: attachmentsMeta } : {})
142
+ };
143
+ form.append('payload_json', JSON.stringify(bodyPayload));
144
+
145
+ files.forEach((f, idx) => {
146
+ const blob = new Blob([f.buffer], { type: f.contentType || 'image/png' });
147
+ form.append(`files[${idx}]`, blob, f.name || `file-${idx}.png`);
148
+ });
149
+
150
+ const res = await fetch(url, {
151
+ method,
152
+ headers: {
153
+ Authorization: `Bot ${cleanToken}`,
154
+ 'User-Agent': 'DisGravity-Onboarding/1.2 (https://gitlab.com/rethinakumarsambath8/dis-gravity)'
155
+ },
156
+ body: form
157
+ });
158
+
159
+ if (res.status === 429 && retries > 0) {
160
+ let waitMs = 1500;
161
+ try {
162
+ const data = await res.json();
163
+ if (data.retry_after) waitMs = Math.ceil(data.retry_after * 1000) + 200;
164
+ } catch {}
165
+ await new Promise((r) => setTimeout(r, waitMs));
166
+ return discordMultipartRequest(token, method, endpoint, payloadJson, files, retries - 1);
167
+ }
168
+
169
+ if (res.status === 204) return null;
170
+
171
+ const text = await res.text();
172
+ let json;
173
+ try {
174
+ json = JSON.parse(text);
175
+ } catch {
176
+ json = { raw: text };
177
+ }
178
+
179
+ if (!res.ok) {
180
+ const err = new Error(json.message || `Discord API multipart error (${res.status}): ${text.substring(0, 200)}`);
181
+ err.status = res.status;
182
+ err.code = json.code;
183
+ err.data = json;
184
+ throw err;
185
+ }
186
+
187
+ return json;
188
+ }
189
+
190
+ /**
191
+ * Inspects the Discord application associated with the bot token.
192
+ * Returns appId, botId, ownerId, appName, teamMembers, flags
193
+ */
194
+ async function inspectApplication(token) {
195
+ const [app, botUser] = await Promise.all([
196
+ discordRequest(token, 'GET', '/applications/@me'),
197
+ discordRequest(token, 'GET', '/users/@me')
198
+ ]);
199
+
200
+ const ownerId = app.owner ? app.owner.id : (app.team ? app.team.owner_user_id : null);
201
+ const teamMembers = app.team?.members ? app.team.members.map(m => ({ id: m.user.id, username: m.user.username })) : [];
202
+
203
+ return {
204
+ appId: app.id,
205
+ botId: botUser.id,
206
+ botTag: `${botUser.username}#${botUser.discriminator || '0'}`,
207
+ botUsername: botUser.username,
208
+ appName: app.name,
209
+ flags: app.flags || 0,
210
+ ownerId,
211
+ ownerUsername: app.owner?.username || 'Owner',
212
+ isTeam: Boolean(app.team),
213
+ teamMembers,
214
+ messageContentEnabled: Boolean((app.flags || 0) & GATEWAY_MESSAGE_CONTENT_LIMITED)
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Ensures Message Content Intent is enabled.
220
+ * First tries PATCH /applications/@me with GATEWAY_MESSAGE_CONTENT_LIMITED flag.
221
+ * If API rejects or flag not set, returns fallback deep link instructions.
222
+ */
223
+ async function ensureMessageContentIntent(token) {
224
+ const app = await discordRequest(token, 'GET', '/applications/@me');
225
+ const currentFlags = app.flags || 0;
226
+
227
+ if ((currentFlags & GATEWAY_MESSAGE_CONTENT_LIMITED) !== 0) {
228
+ return {
229
+ success: true,
230
+ alreadyEnabled: true,
231
+ flags: currentFlags
232
+ };
233
+ }
234
+
235
+ // Attempt automatic update via PATCH /applications/@me
236
+ const targetFlags = currentFlags | GATEWAY_MESSAGE_CONTENT_LIMITED;
237
+ try {
238
+ const patched = await discordRequest(token, 'PATCH', '/applications/@me', {
239
+ flags: targetFlags
240
+ });
241
+
242
+ if ((patched.flags & GATEWAY_MESSAGE_CONTENT_LIMITED) !== 0) {
243
+ return {
244
+ success: true,
245
+ alreadyEnabled: false,
246
+ autoEnabled: true,
247
+ flags: patched.flags
248
+ };
249
+ }
250
+ } catch (err) {
251
+ // Discord may reject PATCH for apps with certain flags or developer requirements
252
+ }
253
+
254
+ // Verification re-fetch
255
+ const recheck = await discordRequest(token, 'GET', '/applications/@me');
256
+ if ((recheck.flags & GATEWAY_MESSAGE_CONTENT_LIMITED) !== 0) {
257
+ return {
258
+ success: true,
259
+ alreadyEnabled: false,
260
+ autoEnabled: true,
261
+ flags: recheck.flags
262
+ };
263
+ }
264
+
265
+ return {
266
+ success: false,
267
+ autoEnabled: false,
268
+ portalUrl: `https://discord.com/developers/applications/${app.id}/bot`,
269
+ message: "Discord requires toggling 'Message Content Intent' manually in the Developer Portal for this application."
270
+ };
271
+ }
272
+
273
+ /**
274
+ * Fetches list of guilds the bot is currently in
275
+ */
276
+ async function listUserGuilds(token) {
277
+ return discordRequest(token, 'GET', '/users/@me/guilds');
278
+ }
279
+
280
+ /**
281
+ * Polls for a new guild authorization or waits for user to authorize the bot
282
+ */
283
+ async function waitForGuildJoin(token, options = {}) {
284
+ const timeoutMs = options.timeoutMs || 120000;
285
+ const pollIntervalMs = options.pollIntervalMs || 2500;
286
+ const initialGuildIds = new Set(options.initialGuildIds || []);
287
+ const startTime = Date.now();
288
+
289
+ while (Date.now() - startTime < timeoutMs) {
290
+ try {
291
+ const guilds = await listUserGuilds(token);
292
+ const newGuild = guilds.find(g => !initialGuildIds.has(g.id));
293
+ if (newGuild) {
294
+ return newGuild;
295
+ }
296
+ if (options.onPoll) {
297
+ options.onPoll({ elapsedMs: Date.now() - startTime, guildCount: guilds.length });
298
+ }
299
+ } catch {}
300
+
301
+ await new Promise(r => setTimeout(r, pollIntervalMs));
302
+ }
303
+
304
+ // Timeout reached, return whatever guilds we currently have
305
+ const finalGuilds = await listUserGuilds(token).catch(() => []);
306
+ if (finalGuilds.length === 1 && initialGuildIds.size === 0) {
307
+ return finalGuilds[0];
308
+ }
309
+ const err = new Error(`Timed out waiting for bot to join server (${Math.round(timeoutMs / 1000)}s).`);
310
+ err.guilds = finalGuilds;
311
+ throw err;
312
+ }
313
+
314
+ /**
315
+ * Builds permission overwrite objects for private channel (#dis-gravity)
316
+ * - @everyone: ViewChannel denied
317
+ * - bot: full permissions allowed
318
+ * - owner(s): full permissions allowed
319
+ */
320
+ function privateChannelOverwrites(guildId, botId, ownerIds = []) {
321
+ const overwrites = [];
322
+
323
+ // Deny @everyone View Channel (1024)
324
+ overwrites.push({
325
+ id: guildId,
326
+ type: 0, // role
327
+ allow: '0',
328
+ deny: '1024'
329
+ });
330
+
331
+ // Allow Bot
332
+ if (botId) {
333
+ overwrites.push({
334
+ id: botId,
335
+ type: 1, // member
336
+ allow: DISGRAVITY_PERMISSIONS,
337
+ deny: '0'
338
+ });
339
+ }
340
+
341
+ // Allow Owners
342
+ const owners = Array.isArray(ownerIds) ? ownerIds : [ownerIds];
343
+ for (const oId of owners) {
344
+ if (oId && oId !== botId) {
345
+ overwrites.push({
346
+ id: oId,
347
+ type: 1, // member
348
+ allow: DISGRAVITY_PERMISSIONS,
349
+ deny: '0'
350
+ });
351
+ }
352
+ }
353
+
354
+ return overwrites;
355
+ }
356
+
357
+ /**
358
+ * Locates existing channel named #dis-gravity or creates a private one
359
+ */
360
+ async function findOrCreateChannel(token, guildId, options = {}) {
361
+ const channelName = (options.channelName || 'dis-gravity').toLowerCase().replace(/\s+/g, '-');
362
+ const ownerIds = options.ownerIds || (options.ownerId ? [options.ownerId] : []);
363
+ const botId = options.botId;
364
+
365
+ // 1. Check existing channels
366
+ const channels = await discordRequest(token, 'GET', `/guilds/${guildId}/channels`);
367
+ const existing = channels.find(c => c.name === channelName && c.type === 0);
368
+ if (existing) {
369
+ return {
370
+ channel: existing,
371
+ created: false,
372
+ reused: true
373
+ };
374
+ }
375
+
376
+ // 2. Create private channel
377
+ const overwrites = privateChannelOverwrites(guildId, botId, ownerIds);
378
+ try {
379
+ const created = await discordRequest(token, 'POST', `/guilds/${guildId}/channels`, {
380
+ name: channelName,
381
+ type: 0, // GUILD_TEXT
382
+ topic: 'Dis-Gravity Remote AI IDE & Terminal Bridge',
383
+ permission_overwrites: overwrites
384
+ });
385
+
386
+ return {
387
+ channel: created,
388
+ created: true,
389
+ reused: false
390
+ };
391
+ } catch (err) {
392
+ // If bot lacks MANAGE_CHANNELS, fall back to first writable text channel
393
+ if (err.status === 403 || err.code === 50013) {
394
+ const fallback = channels.find(c => c.type === 0);
395
+ if (fallback) {
396
+ const warningErr = new Error(`Bot lacks 'Manage Channels' permission to create #${channelName}. You can use existing channel #${fallback.name} or grant the bot 'Manage Channels' in Server Settings.`);
397
+ warningErr.code = 'MISSING_MANAGE_CHANNELS';
398
+ warningErr.fallbackChannel = fallback;
399
+ warningErr.availableChannels = channels.filter(c => c.type === 0);
400
+ throw warningErr;
401
+ }
402
+ }
403
+ throw err;
404
+ }
405
+ }
406
+
407
+ /**
408
+ * Sends initial welcome and ping message to owner in the newly configured channel
409
+ */
410
+ async function sendWelcomeMessage(token, channelId, options = {}) {
411
+ const ownerIds = options.ownerIds || (options.ownerId ? [options.ownerId] : []);
412
+ const hostName = options.hostName || os.hostname();
413
+ const provider = (options.provider || 'Antigravity / VS Code').toUpperCase();
414
+
415
+ const mentions = ownerIds.map(id => `<@${id}>`).join(' ');
416
+ const text = `🚀 **Dis-Gravity Bridge Activated!**\n\n` +
417
+ (mentions ? `Welcome ${mentions}! ` : '') +
418
+ `Your remote AI agent and terminal bridge is online.\n\n` +
419
+ `• **Host Machine:** \`${hostName}\` (${os.platform()} ${os.arch()})\n` +
420
+ `• **AI Engine:** 🟢 **${provider}**\n` +
421
+ `• **Channel:** <#${channelId}>\n\n` +
422
+ `> 💡 **Quick Test:** Send \`!info\` or \`!status\` here to view your system dashboard, or ask your AI coding agent a question!`;
423
+
424
+ return discordRequest(token, 'POST', `/channels/${channelId}/messages`, {
425
+ content: text
426
+ });
427
+ }
428
+
429
+ /**
430
+ * Loads configuration from ~/.dis-gravity/config.json
431
+ */
432
+ function loadConfig() {
433
+ try {
434
+ if (fs.existsSync(CONFIG_FILE)) {
435
+ const content = fs.readFileSync(CONFIG_FILE, 'utf8');
436
+ return JSON.parse(content);
437
+ }
438
+ } catch {}
439
+ return {};
440
+ }
441
+
442
+ /**
443
+ * Saves configuration to ~/.dis-gravity/config.json with 0600 permissions
444
+ */
445
+ function saveConfig(configData) {
446
+ try {
447
+ if (!fs.existsSync(CONFIG_DIR)) {
448
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });
449
+ }
450
+ const current = loadConfig();
451
+ const merged = { ...current, ...configData, updatedAt: new Date().toISOString() };
452
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), { encoding: 'utf8', mode: 0o600 });
453
+ try {
454
+ fs.chmodSync(CONFIG_FILE, 0o600);
455
+ } catch {}
456
+ return true;
457
+ } catch (err) {
458
+ throw new Error(`Failed to save config to ${CONFIG_FILE}: ${err.message}`);
459
+ }
460
+ }
461
+
462
+ /**
463
+ * Clears ~/.dis-gravity/config.json
464
+ */
465
+ function clearConfig() {
466
+ try {
467
+ if (fs.existsSync(CONFIG_FILE)) {
468
+ fs.unlinkSync(CONFIG_FILE);
469
+ return true;
470
+ }
471
+ } catch {}
472
+ return false;
473
+ }
474
+
475
+ /**
476
+ * Opens a URL in the user's default browser across platforms
477
+ */
478
+ function openBrowser(url) {
479
+ if (!url || typeof url !== 'string' || !/^https?:\/\/[^\s"<>`]+$/i.test(url)) return;
480
+ try {
481
+ if (process.platform === 'win32') {
482
+ cp.execFile('cmd.exe', ['/c', 'start', '', url], { windowsHide: true }, () => {});
483
+ } else if (process.platform === 'darwin') {
484
+ cp.execFile('open', [url], () => {});
485
+ } else {
486
+ cp.execFile('xdg-open', [url], () => {});
487
+ }
488
+ } catch {}
489
+ }
490
+
491
+ module.exports = {
492
+ DISCORD_API_BASE,
493
+ DISGRAVITY_PERMISSIONS,
494
+ GATEWAY_MESSAGE_CONTENT_LIMITED,
495
+ CONFIG_DIR,
496
+ CONFIG_FILE,
497
+ looksLikeBotToken,
498
+ buildInviteUrl,
499
+ discordRequest,
500
+ discordMultipartRequest,
501
+ inspectApplication,
502
+ ensureMessageContentIntent,
503
+ listUserGuilds,
504
+ waitForGuildJoin,
505
+ privateChannelOverwrites,
506
+ findOrCreateChannel,
507
+ sendWelcomeMessage,
508
+ loadConfig,
509
+ saveConfig,
510
+ clearConfig,
511
+ openBrowser
512
+ };
package/manifest.json ADDED
@@ -0,0 +1,115 @@
1
+ {
2
+ "manifest_version": "0.3",
3
+ "dxt_version": "0.3",
4
+ "name": "dis-gravity",
5
+ "display_name": "Dis-Gravity",
6
+ "version": "1.2.5",
7
+ "description": "Control Claude from Discord. Receive live notifications, approve agent tool calls in 38ms, render 2x Retina table cards, and execute remote commands directly from your phone.",
8
+ "author": {
9
+ "name": "Sambath Rethinakumar",
10
+ "url": "https://gitlab.com/rethinakumarsambath8"
11
+ },
12
+ "license": "MIT",
13
+ "homepage": "https://gitlab.com/rethinakumarsambath8/dis-gravity#readme",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://gitlab.com/rethinakumarsambath8/dis-gravity.git"
17
+ },
18
+ "icon": "media/mcp-icon.png",
19
+ "server": {
20
+ "type": "node",
21
+ "entry_point": "mcp/server.js",
22
+ "mcp_config": {
23
+ "command": "node",
24
+ "args": [
25
+ "${__dirname}/mcp/server.js"
26
+ ],
27
+ "env": {
28
+ "DISCORD_BOT_TOKEN": "${user_config.bot_token}",
29
+ "DISCORD_CHANNEL_ID": "${user_config.channel_id}",
30
+ "DISCORD_ALLOWED_USERS": "${user_config.allowed_users}"
31
+ }
32
+ }
33
+ },
34
+ "user_config": {
35
+ "bot_token": {
36
+ "type": "string",
37
+ "title": "Discord Bot Token",
38
+ "description": "Discord Bot Token from Discord Developer Portal (Only required field! Dis-Gravity auto-provisions intents, channels, and security)",
39
+ "default": "",
40
+ "sensitive": true,
41
+ "required": false
42
+ },
43
+ "channel_id": {
44
+ "type": "string",
45
+ "title": "Discord Channel ID (Optional)",
46
+ "description": "Discord Channel ID (Optional - Dis-Gravity automatically creates a private locked #dis-gravity channel if left blank)",
47
+ "default": "",
48
+ "sensitive": false,
49
+ "required": false
50
+ },
51
+ "allowed_users": {
52
+ "type": "string",
53
+ "title": "Allowed Discord User IDs",
54
+ "description": "Comma-separated Discord User IDs allowed to approve actions and run commands (optional)",
55
+ "default": "",
56
+ "sensitive": false,
57
+ "required": false
58
+ }
59
+ },
60
+ "tools": [
61
+ {
62
+ "name": "notify_discord",
63
+ "description": "Send a rich status notification, milestone update, or message to the user's linked Discord channel and mobile phone. Automatically renders Markdown tables into 2x Retina PNG cards."
64
+ },
65
+ {
66
+ "name": "ask_discord_approval",
67
+ "description": "Pause execution and request interactive 1-tap confirmation from the developer on their mobile Discord app. Halts until the developer taps [ Allow ] or [ Deny ]."
68
+ },
69
+ {
70
+ "name": "get_discord_bridge_status",
71
+ "description": "Retrieve current Dis-Gravity Discord connection status, active channel, and host system health."
72
+ },
73
+ {
74
+ "name": "run_workspace_command",
75
+ "description": "Execute a terminal shell command inside the workspace directory, with optional mobile Discord approval prompt for high-risk operations."
76
+ },
77
+ {
78
+ "name": "get_git_diff",
79
+ "description": "Retrieve current Git branch, short status summary, and unified diff stat for the active workspace."
80
+ },
81
+ {
82
+ "name": "render_table_card",
83
+ "description": "Convert structured tabular data (headers and rows) into a 2x Retina PNG card and deliver it to Discord."
84
+ },
85
+ {
86
+ "name": "read_workspace_file",
87
+ "description": "Safely inspect the content of a file within the workspace with path traversal protection."
88
+ },
89
+ {
90
+ "name": "list_workspace_tree",
91
+ "description": "View the directory tree structure of the workspace."
92
+ },
93
+ {
94
+ "name": "get_incoming_messages",
95
+ "description": "Retrieve recent messages and instructions sent from the developer in the linked Discord channel."
96
+ },
97
+ {
98
+ "name": "setup_discord_bridge",
99
+ "description": "Run automated frictionless onboarding wizard to configure Dis-Gravity. Validates bot token, auto-enables Message Content intent, opens OAuth invite link, auto-creates private #dis-gravity channel, auto-whitelists bot owner, and connects live bridge."
100
+ }
101
+ ],
102
+ "prompts": [
103
+ {
104
+ "name": "setup-discord-bridge",
105
+ "description": "Run the interactive 60-second setup wizard to connect Claude Desktop to Discord and your mobile phone.",
106
+ "arguments": ["bot_token"],
107
+ "text": "Run the Dis-Gravity Discord Bridge setup wizard using setup_discord_bridge with bot_token: ${arguments.bot_token}"
108
+ },
109
+ {
110
+ "name": "discord-bridge-status",
111
+ "description": "Check Dis-Gravity Discord bridge connection status, host health, and send a test notification to mobile.",
112
+ "text": "Check the Dis-Gravity Discord bridge status using get_discord_bridge_status and send a test notification using notify_discord."
113
+ }
114
+ ]
115
+ }