dis-gravity 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +20 -0
- package/README.md +312 -2
- package/bin/dis-gravity.js +702 -0
- package/changelog.md +89 -0
- package/dist/extension.js +9670 -0
- package/media/discord.svg +4 -0
- package/media/icon.png +0 -0
- package/media/screenshot-chat.png +0 -0
- package/media/screenshot-settings.png +0 -0
- package/package.json +206 -5
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Dis-Gravity Standalone CLI Daemon
|
|
5
|
+
* Run Dis-Gravity as an editor-agnostic background service for JetBrains IDEs,
|
|
6
|
+
* Neovim, Emacs, Zed, or headless remote servers.
|
|
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
|
+
function parseArgs() {
|
|
15
|
+
const args = process.argv.slice(2);
|
|
16
|
+
const config = {
|
|
17
|
+
token: process.env.DISCORD_BOT_TOKEN || '',
|
|
18
|
+
channelId: process.env.DISCORD_CHANNEL_ID || '',
|
|
19
|
+
guildId: process.env.DISCORD_GUILD_ID || '',
|
|
20
|
+
allowedUsers: (process.env.DISCORD_ALLOWED_USERS || '').split(',').map(s => s.trim()).filter(Boolean),
|
|
21
|
+
provider: process.env.DISCORD_AGENT_PROVIDER || 'cli',
|
|
22
|
+
debugPort: parseInt(process.env.DEBUG_PORT || '9000', 10),
|
|
23
|
+
workspace: process.cwd(),
|
|
24
|
+
tableStyle: process.env.TABLE_STYLE || 'image',
|
|
25
|
+
autoAttachTableImages: true
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < args.length; i++) {
|
|
29
|
+
const arg = args[i];
|
|
30
|
+
if (arg === '--help' || arg === '-h') {
|
|
31
|
+
printHelp();
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
if (arg === '--token' || arg === '-t') config.token = args[++i];
|
|
35
|
+
else if (arg.startsWith('--token=')) config.token = arg.split('=')[1];
|
|
36
|
+
else if (arg === '--channel' || arg === '-c') config.channelId = args[++i];
|
|
37
|
+
else if (arg.startsWith('--channel=')) config.channelId = arg.split('=')[1];
|
|
38
|
+
else if (arg === '--guild' || arg === '-g') config.guildId = args[++i];
|
|
39
|
+
else if (arg.startsWith('--guild=')) config.guildId = arg.split('=')[1];
|
|
40
|
+
else if (arg === '--provider' || arg === '-p') config.provider = args[++i];
|
|
41
|
+
else if (arg.startsWith('--provider=')) config.provider = arg.split('=')[1];
|
|
42
|
+
else if (arg === '--port') config.debugPort = parseInt(args[++i], 10);
|
|
43
|
+
else if (arg.startsWith('--port=')) config.debugPort = parseInt(arg.split('=')[1], 10);
|
|
44
|
+
else if (arg === '--workspace' || arg === '-w') config.workspace = path.resolve(args[++i]);
|
|
45
|
+
else if (arg.startsWith('--workspace=')) config.workspace = path.resolve(arg.split('=')[1]);
|
|
46
|
+
else if (arg === '--allowed-users' || arg === '-u') {
|
|
47
|
+
config.allowedUsers = args[++i].split(',').map(s => s.trim()).filter(Boolean);
|
|
48
|
+
} else if (arg.startsWith('--allowed-users=')) {
|
|
49
|
+
config.allowedUsers = arg.split('=')[1].split(',').map(s => s.trim()).filter(Boolean);
|
|
50
|
+
} else if (arg === '--table-style') config.tableStyle = args[++i];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return config;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function printHelp() {
|
|
57
|
+
console.log(`
|
|
58
|
+
╔════════════════════════════════════════════════════════════════════════╗
|
|
59
|
+
║ Dis-Gravity Standalone CLI Daemon ║
|
|
60
|
+
║ The Ultimate Discord Bridge for JetBrains, Neovim & Headless ║
|
|
61
|
+
╚════════════════════════════════════════════════════════════════════════╝
|
|
62
|
+
|
|
63
|
+
Usage:
|
|
64
|
+
npx dis-gravity [options]
|
|
65
|
+
node bin/dis-gravity.js [options]
|
|
66
|
+
|
|
67
|
+
Options:
|
|
68
|
+
-t, --token <string> Discord Bot Token (env: DISCORD_BOT_TOKEN)
|
|
69
|
+
-c, --channel <string> Discord Channel ID to bind (env: DISCORD_CHANNEL_ID)
|
|
70
|
+
-g, --guild <string> Discord Guild ID (optional, for instant slash commands)
|
|
71
|
+
-p, --provider <string> AI Provider: 'cli', 'antigravity', 'cursor', 'windsurf' (default: cli)
|
|
72
|
+
-w, --workspace <path> Workspace folder path (default: current directory)
|
|
73
|
+
-u, --allowed-users <ids> Comma-separated Discord User IDs allowed to execute commands
|
|
74
|
+
--port <number> CDP port for Antigravity/Cursor/Windsurf (default: 9000)
|
|
75
|
+
--table-style <style> Table formatting: image, cards, rounded, boxed, ascii (default: image)
|
|
76
|
+
-h, --help Show this help menu
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
npx dis-gravity --token="BOT_TOKEN" --channel="123456789" --provider=cli
|
|
80
|
+
npx dis-gravity --provider=antigravity --port=9000
|
|
81
|
+
`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Minimal logger
|
|
85
|
+
const log = {
|
|
86
|
+
appendLine: (msg) => {
|
|
87
|
+
const ts = new Date().toISOString().substring(11, 19);
|
|
88
|
+
console.log(`[${ts}] ${msg}`);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
function executeShellCommand(cmdStr, cwd, timeoutMs = 30000) {
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
const startTime = Date.now();
|
|
95
|
+
cp.exec(cmdStr, { cwd, timeout: timeoutMs, maxBuffer: 1024 * 1024 * 10 }, (err, stdout, stderr) => {
|
|
96
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
97
|
+
resolve({
|
|
98
|
+
code: err ? (err.code || 1) : 0,
|
|
99
|
+
stdout: stdout || "",
|
|
100
|
+
stderr: stderr || "",
|
|
101
|
+
duration,
|
|
102
|
+
error: err ? err.message : null
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Find headless browser for table card rendering
|
|
109
|
+
function findHeadlessBrowser() {
|
|
110
|
+
if (process.platform === 'win32') {
|
|
111
|
+
const candidates = [
|
|
112
|
+
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
113
|
+
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
|
|
114
|
+
'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
|
|
115
|
+
'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe'
|
|
116
|
+
];
|
|
117
|
+
for (const c of candidates) {
|
|
118
|
+
if (fs.existsSync(c)) return c;
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
const candidates = ['google-chrome', 'chromium', 'chromium-browser', 'microsoft-edge'];
|
|
122
|
+
for (const c of candidates) {
|
|
123
|
+
try {
|
|
124
|
+
cp.execSync(`which ${c}`, { stdio: 'ignore' });
|
|
125
|
+
return c;
|
|
126
|
+
} catch {}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function buildTableHtml(headers, rows, alignments) {
|
|
133
|
+
const thHtml = headers.map((h, i) => {
|
|
134
|
+
const align = alignments[i] || 'left';
|
|
135
|
+
return `<th style="padding: 10px 14px; text-align: ${align}; border-bottom: 2px solid #5865F2; color: #fff; font-size: 14px; font-weight: 600;">${escapeHtml(h)}</th>`;
|
|
136
|
+
}).join('');
|
|
137
|
+
|
|
138
|
+
const trHtml = rows.map((row, rIdx) => {
|
|
139
|
+
const bg = rIdx % 2 === 0 ? '#2b2d31' : '#313338';
|
|
140
|
+
const tdHtml = row.map((cell, cIdx) => {
|
|
141
|
+
const align = alignments[cIdx] || 'left';
|
|
142
|
+
return `<td style="padding: 9px 14px; text-align: ${align}; border-bottom: 1px solid #3f4147; color: #dbdee1; font-size: 13px;">${escapeHtml(cell)}</td>`;
|
|
143
|
+
}).join('');
|
|
144
|
+
return `<tr style="background: ${bg};">${tdHtml}</tr>`;
|
|
145
|
+
}).join('');
|
|
146
|
+
|
|
147
|
+
return `<!DOCTYPE html><html><head><meta charset="utf-8"><style>
|
|
148
|
+
body { margin: 0; padding: 16px; background: #1e1f22; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; display: inline-block; }
|
|
149
|
+
.table-card { border-radius: 8px; overflow: hidden; box-shadow: 0 4px 12px rgba(0,0,0,0.4); border: 1px solid #383a40; background: #2b2d31; display: inline-block; }
|
|
150
|
+
table { border-collapse: collapse; width: 100%; min-width: 320px; }
|
|
151
|
+
.header-banner { background: linear-gradient(90deg, #5865F2, #4752C4); color: white; padding: 8px 14px; font-size: 12px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
152
|
+
</style></head><body>
|
|
153
|
+
<div class="table-card">
|
|
154
|
+
<div class="header-banner">📊 Dis-Gravity Data Table</div>
|
|
155
|
+
<table>
|
|
156
|
+
<thead><tr style="background: #232428;">${thHtml}</tr></thead>
|
|
157
|
+
<tbody>${trHtml}</tbody>
|
|
158
|
+
</table>
|
|
159
|
+
</div>
|
|
160
|
+
</body></html>`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function escapeHtml(str) {
|
|
164
|
+
if (!str) return '';
|
|
165
|
+
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function renderTableToPng(headers, rows, alignments) {
|
|
169
|
+
const browserPath = findHeadlessBrowser();
|
|
170
|
+
if (!browserPath) return null;
|
|
171
|
+
|
|
172
|
+
const tempHtml = path.join(os.tmpdir(), `table_${Date.now()}_${Math.random().toString(36).substring(2)}.html`);
|
|
173
|
+
const tempPng = path.join(os.tmpdir(), `table_${Date.now()}_${Math.random().toString(36).substring(2)}.png`);
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const html = buildTableHtml(headers, rows, alignments);
|
|
177
|
+
fs.writeFileSync(tempHtml, html, 'utf8');
|
|
178
|
+
|
|
179
|
+
const colCount = headers.length || 3;
|
|
180
|
+
const rowCount = rows.length || 3;
|
|
181
|
+
const width = Math.min(1200, Math.max(450, colCount * 180));
|
|
182
|
+
const height = Math.min(1000, Math.max(200, (rowCount + 2) * 45 + 80));
|
|
183
|
+
|
|
184
|
+
const args = [
|
|
185
|
+
'--headless',
|
|
186
|
+
'--disable-gpu',
|
|
187
|
+
'--hide-scrollbars',
|
|
188
|
+
`--screenshot=${tempPng}`,
|
|
189
|
+
`--window-size=${width},${height}`,
|
|
190
|
+
`file:///${tempHtml.replace(/\\/g, '/')}`
|
|
191
|
+
];
|
|
192
|
+
cp.execFileSync(browserPath, args, { timeout: 6000, stdio: 'ignore' });
|
|
193
|
+
|
|
194
|
+
if (fs.existsSync(tempPng)) {
|
|
195
|
+
const buf = fs.readFileSync(tempPng);
|
|
196
|
+
try { fs.unlinkSync(tempHtml); } catch {}
|
|
197
|
+
try { fs.unlinkSync(tempPng); } catch {}
|
|
198
|
+
return buf;
|
|
199
|
+
}
|
|
200
|
+
} catch {
|
|
201
|
+
} finally {
|
|
202
|
+
try { if (fs.existsSync(tempHtml)) fs.unlinkSync(tempHtml); } catch {}
|
|
203
|
+
try { if (fs.existsSync(tempPng)) fs.unlinkSync(tempPng); } catch {}
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Main Daemon Application
|
|
209
|
+
async function startDaemon() {
|
|
210
|
+
const config = parseArgs();
|
|
211
|
+
|
|
212
|
+
let Client, GatewayIntentBits, Partials, ActionRowBuilder, ButtonBuilder, ButtonStyle;
|
|
213
|
+
try {
|
|
214
|
+
const djs = require('discord.js');
|
|
215
|
+
Client = djs.Client;
|
|
216
|
+
GatewayIntentBits = djs.GatewayIntentBits;
|
|
217
|
+
Partials = djs.Partials;
|
|
218
|
+
ActionRowBuilder = djs.ActionRowBuilder;
|
|
219
|
+
ButtonBuilder = djs.ButtonBuilder;
|
|
220
|
+
ButtonStyle = djs.ButtonStyle;
|
|
221
|
+
} catch (err) {
|
|
222
|
+
console.error('❌ Error: discord.js is required to run the standalone daemon.');
|
|
223
|
+
console.error(' Please install it with: npm install discord.js');
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
console.log(`
|
|
228
|
+
╔════════════════════════════════════════════════════════════════════════╗
|
|
229
|
+
║ Dis-Gravity Standalone CLI Daemon ║
|
|
230
|
+
╚════════════════════════════════════════════════════════════════════════╝
|
|
231
|
+
`);
|
|
232
|
+
|
|
233
|
+
if (!config.token) {
|
|
234
|
+
console.error('❌ Error: Discord Bot Token is required (--token or DISCORD_BOT_TOKEN).');
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!config.channelId) {
|
|
239
|
+
console.error('❌ Error: Discord Channel ID is required (--channel or DISCORD_CHANNEL_ID).');
|
|
240
|
+
process.exit(1);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
log.appendLine(`Initializing Daemon with workspace: ${config.workspace}`);
|
|
244
|
+
log.appendLine(`Provider: ${config.provider.toUpperCase()} | Channel ID: ${config.channelId}`);
|
|
245
|
+
if (config.allowedUsers.length > 0) {
|
|
246
|
+
log.appendLine(`Security Whitelist: ${config.allowedUsers.length} user(s) allowed (${config.allowedUsers.join(', ')})`);
|
|
247
|
+
} else {
|
|
248
|
+
log.appendLine(`⚠️ SECURITY WARNING: No --allowed-users configured! Anyone with access to channel #${config.channelId} can execute commands and access files in this workspace.`);
|
|
249
|
+
log.appendLine(` To restrict access to your user only, launch with: --allowed-users="YOUR_DISCORD_USER_ID"`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const client = new Client({
|
|
253
|
+
intents: [
|
|
254
|
+
GatewayIntentBits.Guilds,
|
|
255
|
+
GatewayIntentBits.GuildMessages,
|
|
256
|
+
GatewayIntentBits.MessageContent,
|
|
257
|
+
GatewayIntentBits.DirectMessages
|
|
258
|
+
],
|
|
259
|
+
partials: [Partials.Channel, Partials.Message]
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
function isUserAllowed(userId) {
|
|
263
|
+
if (!config.allowedUsers || config.allowedUsers.length === 0) return true;
|
|
264
|
+
return config.allowedUsers.includes(userId);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Active CLI agent supervisor
|
|
268
|
+
let activeProcess = null;
|
|
269
|
+
let activeStepStatus = null;
|
|
270
|
+
let pendingApproval = null;
|
|
271
|
+
let approvalResolver = null;
|
|
272
|
+
|
|
273
|
+
async function cancelActiveAgent() {
|
|
274
|
+
if (activeProcess) {
|
|
275
|
+
try { activeProcess.kill('SIGINT'); } catch {}
|
|
276
|
+
activeProcess = null;
|
|
277
|
+
}
|
|
278
|
+
activeStepStatus = null;
|
|
279
|
+
if (approvalResolver) {
|
|
280
|
+
approvalResolver('cancel');
|
|
281
|
+
approvalResolver = null;
|
|
282
|
+
pendingApproval = null;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function executeCliAgent(promptText, onStepUpdate) {
|
|
287
|
+
const isWindows = process.platform === 'win32';
|
|
288
|
+
const checkCmd = isWindows ? 'where.exe' : 'which';
|
|
289
|
+
|
|
290
|
+
let engine = 'shell';
|
|
291
|
+
const check = (bin) => {
|
|
292
|
+
try { cp.execSync(`${checkCmd} ${bin}`, { stdio: 'ignore' }); return true; } catch { return false; }
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
if (check('agy')) engine = 'agy';
|
|
296
|
+
else if (check('claude')) engine = 'claude';
|
|
297
|
+
else if (check('aider')) engine = 'aider';
|
|
298
|
+
|
|
299
|
+
let binary = '';
|
|
300
|
+
let args = [];
|
|
301
|
+
|
|
302
|
+
if (engine === 'agy') {
|
|
303
|
+
binary = 'agy';
|
|
304
|
+
args = [promptText];
|
|
305
|
+
} else if (engine === 'claude') {
|
|
306
|
+
binary = 'claude';
|
|
307
|
+
args = ['-p', promptText];
|
|
308
|
+
} else if (engine === 'aider') {
|
|
309
|
+
binary = 'aider';
|
|
310
|
+
args = ['--message', promptText, '--yes-always'];
|
|
311
|
+
} else {
|
|
312
|
+
binary = isWindows ? 'powershell.exe' : '/bin/bash';
|
|
313
|
+
args = isWindows ? ['-NoProfile', '-Command', promptText] : ['-c', promptText];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
log.appendLine(`[Agent] Spawning ${binary} (${engine.toUpperCase()}) in ${config.workspace}...`);
|
|
317
|
+
activeStepStatus = `Running ${engine.toUpperCase()} agent...`;
|
|
318
|
+
onStepUpdate?.(activeStepStatus);
|
|
319
|
+
|
|
320
|
+
return new Promise((resolve) => {
|
|
321
|
+
let stdout = '';
|
|
322
|
+
let stderr = '';
|
|
323
|
+
const startTime = Date.now();
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const proc = cp.spawn(binary, args, {
|
|
327
|
+
cwd: config.workspace,
|
|
328
|
+
env: { ...process.env, CI: '1', TERM: 'dumb' }
|
|
329
|
+
});
|
|
330
|
+
activeProcess = proc;
|
|
331
|
+
|
|
332
|
+
const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
333
|
+
|
|
334
|
+
proc.stdout.on('data', (chunk) => {
|
|
335
|
+
const text = stripAnsi(chunk.toString());
|
|
336
|
+
stdout += text;
|
|
337
|
+
const lastLine = text.trim().split('\n').pop() || '';
|
|
338
|
+
if (lastLine) {
|
|
339
|
+
activeStepStatus = lastLine.substring(0, 80);
|
|
340
|
+
onStepUpdate?.(activeStepStatus);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (/(?:\(|\[)\s*[yY]\s*[\/|]\s*[nN]\s*(?:\)|\])|(?:proceed|allow|continue|confirm|choice|run this)\b.*\?|\?\s+\[[yY]\/[nN]\]/i.test(lastLine)) {
|
|
344
|
+
pendingApproval = {
|
|
345
|
+
question: lastLine,
|
|
346
|
+
options: [
|
|
347
|
+
{ id: 'yes', text: '🟢 Proceed / Allow (y)' },
|
|
348
|
+
{ id: 'no', text: '🔴 Deny / Cancel (n)' }
|
|
349
|
+
]
|
|
350
|
+
};
|
|
351
|
+
approvalResolver = (ans) => {
|
|
352
|
+
const isYes = /^(yes|proceed|allow|y)/i.test(ans || '');
|
|
353
|
+
try { proc.stdin.write(isYes ? 'y\n' : 'n\n'); } catch {}
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
proc.stderr.on('data', (chunk) => {
|
|
359
|
+
stderr += stripAnsi(chunk.toString());
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
proc.on('close', (code) => {
|
|
363
|
+
activeProcess = null;
|
|
364
|
+
activeStepStatus = null;
|
|
365
|
+
pendingApproval = null;
|
|
366
|
+
approvalResolver = null;
|
|
367
|
+
const dur = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
368
|
+
const badge = code === 0 ? `✅ **Process completed in ${dur}s (exit 0)**` : `⚠️ **Process exited with code ${code} (${dur}s)**`;
|
|
369
|
+
const output = (stdout.trim() || stderr.trim()) || '(No output recorded)';
|
|
370
|
+
resolve(`${badge}\n\n\`\`\`text\n${output.substring(0, 1800)}\n\`\`\``);
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
proc.on('error', (err) => {
|
|
374
|
+
activeProcess = null;
|
|
375
|
+
activeStepStatus = null;
|
|
376
|
+
resolve(`❌ **Failed to spawn CLI process (${binary}):** ${err.message}`);
|
|
377
|
+
});
|
|
378
|
+
} catch (err) {
|
|
379
|
+
activeProcess = null;
|
|
380
|
+
activeStepStatus = null;
|
|
381
|
+
resolve(`❌ **Execution error:** ${err.message}`);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function getDaemonStatusPayload(customHeader = null) {
|
|
387
|
+
const cpus = os.cpus();
|
|
388
|
+
const cpuModel = cpus && cpus[0] ? cpus[0].model.trim() : 'CPU';
|
|
389
|
+
const freeGB = (os.freemem() / (1024 ** 3)).toFixed(1);
|
|
390
|
+
const totalGB = (os.totalmem() / (1024 ** 3)).toFixed(1);
|
|
391
|
+
const st = await executeShellCommand('git rev-parse --abbrev-ref HEAD', config.workspace);
|
|
392
|
+
const branch = st.stdout.trim() || 'unknown';
|
|
393
|
+
|
|
394
|
+
let msg = "";
|
|
395
|
+
if (customHeader) {
|
|
396
|
+
msg += `${customHeader}\n\n`;
|
|
397
|
+
}
|
|
398
|
+
msg += `📊 **Dis-Gravity Standalone Daemon Status**\n\n`;
|
|
399
|
+
msg += `• **Project:** 📁 \`${path.basename(config.workspace)}\`\n`;
|
|
400
|
+
msg += `• **Workspace:** \`${config.workspace}\`\n`;
|
|
401
|
+
msg += `• **Git Branch:** 🌿 \`${branch}\`\n`;
|
|
402
|
+
msg += `• **Provider:** 🤖 🟢 **Standalone Daemon (${config.provider.toUpperCase()})**\n`;
|
|
403
|
+
msg += `• **Daemon State:** ${activeProcess ? '⏳ Busy (Process active)' : '🟢 Idle (Waiting for commands)'}\n`;
|
|
404
|
+
msg += `• **Host System:** 💻 ${os.platform()} (${os.release()}, ${os.arch()})\n`;
|
|
405
|
+
msg += `• **Memory:** 📈 ${freeGB} GB free / ${totalGB} GB total\n`;
|
|
406
|
+
if (!config.allowedUsers || config.allowedUsers.length === 0) {
|
|
407
|
+
msg += `\n> ⚠️ *Security notice: No \`allowedUsers\` whitelist configured. Add your Discord User ID to restrict access.*`;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const actionRow = new ActionRowBuilder().addComponents(
|
|
411
|
+
new ButtonBuilder()
|
|
412
|
+
.setCustomId("daemon:status")
|
|
413
|
+
.setLabel("🔄 Refresh")
|
|
414
|
+
.setStyle(ButtonStyle.Primary),
|
|
415
|
+
new ButtonBuilder()
|
|
416
|
+
.setCustomId("daemon:diff")
|
|
417
|
+
.setLabel("🌿 Git Diff")
|
|
418
|
+
.setStyle(ButtonStyle.Secondary),
|
|
419
|
+
new ButtonBuilder()
|
|
420
|
+
.setCustomId("daemon:info")
|
|
421
|
+
.setLabel("🖥️ System Info")
|
|
422
|
+
.setStyle(ButtonStyle.Secondary)
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
return { content: msg.substring(0, 1900), components: [actionRow] };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function sendDaemonStatusNotification(channel, header = "🚀 **Dis-Gravity Daemon Connected & Ready**") {
|
|
429
|
+
try {
|
|
430
|
+
const payload = await getDaemonStatusPayload(header);
|
|
431
|
+
await channel.send(payload);
|
|
432
|
+
log.appendLine(`Sent automatic status notification to #${channel.name || channel.id}`);
|
|
433
|
+
} catch (err) {
|
|
434
|
+
log.appendLine(`Failed to send daemon status notification: ${err.message}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
client.once('ready', async () => {
|
|
439
|
+
log.appendLine(`🚀 Dis-Gravity Daemon logged in as ${client.user.tag}`);
|
|
440
|
+
const channel = await client.channels.fetch(config.channelId).catch(() => null);
|
|
441
|
+
if (!channel) {
|
|
442
|
+
log.appendLine(`⚠️ Warning: Could not find channel with ID ${config.channelId}`);
|
|
443
|
+
} else {
|
|
444
|
+
log.appendLine(`Connected to Discord Channel: #${channel.name || config.channelId}`);
|
|
445
|
+
await sendDaemonStatusNotification(channel, "🚀 **Dis-Gravity Daemon Connected & Ready**");
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Register slash commands
|
|
449
|
+
try {
|
|
450
|
+
const slashCommands = [
|
|
451
|
+
{ name: "sh", description: "Execute shell command in workspace", options: [{ name: "command", description: "Command to execute", type: 3, required: true }] },
|
|
452
|
+
{ name: "diff", description: "View git diff summary and preview" },
|
|
453
|
+
{ name: "info", description: "View host hardware and workspace health dashboard" },
|
|
454
|
+
{ name: "tree", description: "View workspace directory tree" },
|
|
455
|
+
{ name: "stop", description: "Cancel running agent or command" },
|
|
456
|
+
{ name: "status", description: "View daemon status and provider connection" },
|
|
457
|
+
{ name: "help", description: "Display Dis-Gravity commands" }
|
|
458
|
+
];
|
|
459
|
+
|
|
460
|
+
if (config.guildId) {
|
|
461
|
+
const guild = await client.guilds.fetch(config.guildId).catch(() => null);
|
|
462
|
+
if (guild) {
|
|
463
|
+
await guild.commands.set(slashCommands);
|
|
464
|
+
log.appendLine(`Registered ${slashCommands.length} guild slash commands in ${guild.name}`);
|
|
465
|
+
}
|
|
466
|
+
} else {
|
|
467
|
+
await client.application.commands.set(slashCommands);
|
|
468
|
+
log.appendLine(`Registered ${slashCommands.length} global slash commands`);
|
|
469
|
+
}
|
|
470
|
+
} catch (err) {
|
|
471
|
+
log.appendLine(`Notice: Could not register slash commands (${err.message}). Prefix commands (!sh, !diff, etc.) are fully active.`);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// Handle Slash Commands and Buttons
|
|
476
|
+
client.on('interactionCreate', async (interaction) => {
|
|
477
|
+
try {
|
|
478
|
+
if (!isUserAllowed(interaction.user.id)) {
|
|
479
|
+
if (!interaction.replied && !interaction.deferred) {
|
|
480
|
+
await interaction.reply({ content: `⛔ **Unauthorized:** Your Discord User ID (\`${interaction.user.id}\`) is not on the whitelist.`, ephemeral: true });
|
|
481
|
+
}
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (interaction.isButton()) {
|
|
485
|
+
const id = interaction.customId;
|
|
486
|
+
if (id === 'daemon:status') {
|
|
487
|
+
await interaction.deferUpdate().catch(() => {});
|
|
488
|
+
const statusPayload = await getDaemonStatusPayload();
|
|
489
|
+
await interaction.editReply(statusPayload).catch(() => {});
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (id === 'daemon:diff') {
|
|
493
|
+
await interaction.deferReply({ ephemeral: true }).catch(() => {});
|
|
494
|
+
const st = await executeShellCommand('git status --short', config.workspace);
|
|
495
|
+
const df = await executeShellCommand('git diff --stat', config.workspace);
|
|
496
|
+
let reply = `🌿 **Git Status & Diff Preview**\n\n\`\`\`diff\n${st.stdout || 'Clean working tree'}\n\`\`\`\n`;
|
|
497
|
+
if (df.stdout) reply += `**Diff Stat:**\n\`\`\`text\n${df.stdout}\n\`\`\``;
|
|
498
|
+
await interaction.editReply(reply).catch(() => {});
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (id === 'daemon:info') {
|
|
502
|
+
await interaction.deferReply({ ephemeral: true }).catch(() => {});
|
|
503
|
+
const cpus = os.cpus();
|
|
504
|
+
const cpuModel = cpus && cpus[0] ? cpus[0].model.trim() : 'CPU';
|
|
505
|
+
const freeGB = (os.freemem() / (1024 ** 3)).toFixed(1);
|
|
506
|
+
const totalGB = (os.totalmem() / (1024 ** 3)).toFixed(1);
|
|
507
|
+
const st = await executeShellCommand('git rev-parse --abbrev-ref HEAD', config.workspace);
|
|
508
|
+
let msg = `🖥️ **Host & System Info**\n\n`;
|
|
509
|
+
msg += `• **Project:** \`${path.basename(config.workspace)}\`\n`;
|
|
510
|
+
msg += `• **Workspace:** \`${config.workspace}\`\n`;
|
|
511
|
+
msg += `• **Branch:** \`${st.stdout.trim() || 'unknown'}\`\n`;
|
|
512
|
+
msg += `• **System:** ${os.platform()} (${os.release()}, ${os.arch()})\n`;
|
|
513
|
+
msg += `• **CPU:** ${cpuModel} (${cpus ? cpus.length : 1} cores)\n`;
|
|
514
|
+
msg += `• **Memory:** ${freeGB} GB free / ${totalGB} GB total\n`;
|
|
515
|
+
await interaction.editReply(msg).catch(() => {});
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (id.startsWith('approve_') && approvalResolver) {
|
|
519
|
+
const choice = id.replace('approve_', '');
|
|
520
|
+
approvalResolver(choice);
|
|
521
|
+
approvalResolver = null;
|
|
522
|
+
pendingApproval = null;
|
|
523
|
+
await interaction.reply({ content: `✅ **Action approved:** \`${choice}\``, ephemeral: true }).catch(() => {});
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
if (id === 'agent_cancel') {
|
|
527
|
+
await cancelActiveAgent();
|
|
528
|
+
await interaction.reply({ content: "🛑 **Execution halted.**", ephemeral: true }).catch(() => {});
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (interaction.isChatInputCommand()) {
|
|
534
|
+
const cmd = interaction.commandName;
|
|
535
|
+
if (cmd === 'sh') {
|
|
536
|
+
const commandStr = interaction.options.getString('command');
|
|
537
|
+
await interaction.deferReply();
|
|
538
|
+
const res = await executeShellCommand(commandStr, config.workspace);
|
|
539
|
+
const badge = res.code === 0 ? `✅ **Exit 0 (${res.duration}s)**` : `❌ **Exit ${res.code} (${res.duration}s)**`;
|
|
540
|
+
const out = res.stdout || res.stderr || '(No output)';
|
|
541
|
+
await interaction.editReply(`${badge}\n\`\`\`text\n${out.substring(0, 1800)}\n\`\`\``);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (cmd === 'diff') {
|
|
546
|
+
await interaction.deferReply();
|
|
547
|
+
const st = await executeShellCommand('git status --short', config.workspace);
|
|
548
|
+
const df = await executeShellCommand('git diff --stat', config.workspace);
|
|
549
|
+
let reply = `🌿 **Git Status & Diff Preview**\n\n`;
|
|
550
|
+
reply += `\`\`\`diff\n${st.stdout || 'Clean working tree'}\n\`\`\`\n`;
|
|
551
|
+
if (df.stdout) reply += `**Diff Stat:**\n\`\`\`text\n${df.stdout}\n\`\`\``;
|
|
552
|
+
await interaction.editReply(reply);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (cmd === 'info') {
|
|
557
|
+
await interaction.deferReply();
|
|
558
|
+
const cpus = os.cpus();
|
|
559
|
+
const cpuModel = cpus && cpus[0] ? cpus[0].model.trim() : 'CPU';
|
|
560
|
+
const freeGB = (os.freemem() / (1024 ** 3)).toFixed(1);
|
|
561
|
+
const totalGB = (os.totalmem() / (1024 ** 3)).toFixed(1);
|
|
562
|
+
const st = await executeShellCommand('git rev-parse --abbrev-ref HEAD', config.workspace);
|
|
563
|
+
let msg = `🖥️ **Dis-Gravity Host & Workspace Health Dashboard**\n\n`;
|
|
564
|
+
msg += `• **Project:** \`${path.basename(config.workspace)}\`\n`;
|
|
565
|
+
msg += `• **Path:** \`${config.workspace}\`\n`;
|
|
566
|
+
msg += `• **Git Branch:** \`${st.stdout.trim() || 'unknown'}\`\n`;
|
|
567
|
+
msg += `• **Provider:** 🟢 **Standalone Daemon (${config.provider.toUpperCase()})**\n`;
|
|
568
|
+
msg += `• **System:** ${os.platform()} (${os.release()}, ${os.arch()})\n`;
|
|
569
|
+
msg += `• **CPU:** ${cpuModel} (${cpus ? cpus.length : 1} cores)\n`;
|
|
570
|
+
msg += `• **Memory:** ${freeGB} GB free / ${totalGB} GB total\n`;
|
|
571
|
+
msg += `• **Node Runtime:** ${process.version} (PID: ${process.pid})\n`;
|
|
572
|
+
await interaction.editReply(msg);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (cmd === 'status') {
|
|
577
|
+
await interaction.deferReply().catch(() => {});
|
|
578
|
+
const statusPayload = await getDaemonStatusPayload();
|
|
579
|
+
await interaction.editReply(statusPayload).catch(() => {});
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (cmd === 'stop') {
|
|
584
|
+
await cancelActiveAgent();
|
|
585
|
+
await interaction.reply('🛑 **Agent generation / command cancelled.**');
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (cmd === 'help') {
|
|
590
|
+
let h = `🤖 **Dis-Gravity Standalone Daemon Commands**\n\n`;
|
|
591
|
+
h += `• \`/sh <cmd>\` or \`!sh <cmd>\` — Run terminal command\n`;
|
|
592
|
+
h += `• \`/diff\` or \`!diff\` — View git status and diff preview\n`;
|
|
593
|
+
h += `• \`/info\` or \`!info\` — System hardware and workspace dashboard\n`;
|
|
594
|
+
h += `• \`/stop\` or \`!stop\` — Cancel running process\n`;
|
|
595
|
+
await interaction.reply(h);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
} catch (err) {
|
|
600
|
+
log.appendLine(`Interaction error: ${err.message}`);
|
|
601
|
+
}
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// Handle Chat Messages
|
|
605
|
+
client.on('messageCreate', async (message) => {
|
|
606
|
+
if (message.author.bot) return;
|
|
607
|
+
if (message.channelId !== config.channelId && !message.channel.isDMBased()) return;
|
|
608
|
+
|
|
609
|
+
if (!isUserAllowed(message.author.id)) {
|
|
610
|
+
await message.reply(`⛔ **Unauthorized:** Your Discord User ID (\`${message.author.id}\`) is not in the Dis-Gravity whitelist.`).catch(() => {});
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const text = message.content.trim();
|
|
615
|
+
|
|
616
|
+
// Shell command prefix
|
|
617
|
+
if (text.startsWith('!sh ') || text.startsWith('$ ')) {
|
|
618
|
+
const cmdStr = text.replace(/^(!sh |\$ )/, '');
|
|
619
|
+
const statusMsg = await message.reply(`⚡ Running: \`${cmdStr}\`...`);
|
|
620
|
+
const res = await executeShellCommand(cmdStr, config.workspace);
|
|
621
|
+
const badge = res.code === 0 ? `✅ **Exit 0 (${res.duration}s)**` : `❌ **Exit ${res.code} (${res.duration}s)**`;
|
|
622
|
+
const out = res.stdout || res.stderr || '(No output)';
|
|
623
|
+
await statusMsg.edit(`${badge}\n\`\`\`text\n${out.substring(0, 1800)}\n\`\`\``);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Status
|
|
628
|
+
if (text === '!status' || text === '/status') {
|
|
629
|
+
const statusPayload = await getDaemonStatusPayload();
|
|
630
|
+
await message.reply(statusPayload).catch(() => {});
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Git Diff
|
|
635
|
+
if (text === '!diff' || text === '/diff') {
|
|
636
|
+
const st = await executeShellCommand('git status --short', config.workspace);
|
|
637
|
+
const df = await executeShellCommand('git diff --stat', config.workspace);
|
|
638
|
+
let reply = `🌿 **Git Status & Diff Preview**\n\n\`\`\`diff\n${st.stdout || 'Clean working tree'}\n\`\`\`\n`;
|
|
639
|
+
if (df.stdout) reply += `**Diff Stat:**\n\`\`\`text\n${df.stdout}\n\`\`\``;
|
|
640
|
+
await message.reply(reply);
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Info
|
|
645
|
+
if (text === '!info' || text === '/info') {
|
|
646
|
+
const cpus = os.cpus();
|
|
647
|
+
const cpuModel = cpus && cpus[0] ? cpus[0].model.trim() : 'CPU';
|
|
648
|
+
const freeGB = (os.freemem() / (1024 ** 3)).toFixed(1);
|
|
649
|
+
const totalGB = (os.totalmem() / (1024 ** 3)).toFixed(1);
|
|
650
|
+
const st = await executeShellCommand('git rev-parse --abbrev-ref HEAD', config.workspace);
|
|
651
|
+
let msg = `🖥️ **Dis-Gravity Host & Workspace Health Dashboard**\n\n`;
|
|
652
|
+
msg += `• **Project:** \`${path.basename(config.workspace)}\`\n`;
|
|
653
|
+
msg += `• **Path:** \`${config.workspace}\`\n`;
|
|
654
|
+
msg += `• **Git Branch:** \`${st.stdout.trim() || 'unknown'}\`\n`;
|
|
655
|
+
msg += `• **Provider:** 🟢 **Standalone Daemon (${config.provider.toUpperCase()})**\n`;
|
|
656
|
+
msg += `• **System:** ${os.platform()} (${os.release()}, ${os.arch()})\n`;
|
|
657
|
+
msg += `• **CPU:** ${cpuModel} (${cpus ? cpus.length : 1} cores)\n`;
|
|
658
|
+
msg += `• **Memory:** ${freeGB} GB free / ${totalGB} GB total\n`;
|
|
659
|
+
await message.reply(msg);
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Stop
|
|
664
|
+
if (text === '!stop' || text === '/stop') {
|
|
665
|
+
await cancelActiveAgent();
|
|
666
|
+
await message.reply('🛑 **Agent generation / command cancelled.**');
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Forward general prompt to AI Agent
|
|
671
|
+
const statusMsg = await message.reply('⏳ **Analyzing prompt... (0s)**');
|
|
672
|
+
let elapsed = 0;
|
|
673
|
+
const timer = setInterval(async () => {
|
|
674
|
+
elapsed += 2;
|
|
675
|
+
const statusText = activeStepStatus ? `⏳ **Working (${elapsed}s):** \`${activeStepStatus}\`` : `⏳ **Analyzing... (${elapsed}s)**`;
|
|
676
|
+
await statusMsg.edit(statusText).catch(() => {});
|
|
677
|
+
}, 2000);
|
|
678
|
+
|
|
679
|
+
try {
|
|
680
|
+
const res = await executeCliAgent(text, (step) => {
|
|
681
|
+
statusMsg.edit(`⏳ **Working (${elapsed}s):** \`${step}\``).catch(() => {});
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
clearInterval(timer);
|
|
685
|
+
await statusMsg.edit(res);
|
|
686
|
+
} catch (err) {
|
|
687
|
+
clearInterval(timer);
|
|
688
|
+
await statusMsg.edit(`❌ **Error:** ${err.message}`);
|
|
689
|
+
}
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
await client.login(config.token);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (require.main === module) {
|
|
696
|
+
startDaemon().catch((err) => {
|
|
697
|
+
console.error('Fatal Daemon Error:', err);
|
|
698
|
+
process.exit(1);
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
module.exports = { startDaemon };
|