@thesammykins/tether 1.0.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/.env.example +44 -0
- package/LICENSE +21 -0
- package/README.md +313 -0
- package/bin/tether.ts +1010 -0
- package/index.ts +32 -0
- package/package.json +64 -0
- package/src/adapters/claude.ts +107 -0
- package/src/adapters/codex.ts +77 -0
- package/src/adapters/opencode.ts +83 -0
- package/src/adapters/registry.ts +23 -0
- package/src/adapters/types.ts +17 -0
- package/src/api.ts +494 -0
- package/src/bot.ts +653 -0
- package/src/db.ts +123 -0
- package/src/discord.ts +80 -0
- package/src/features/ack.ts +10 -0
- package/src/features/brb.ts +79 -0
- package/src/features/channel-context.ts +23 -0
- package/src/features/pause-resume.ts +86 -0
- package/src/features/session-limits.ts +48 -0
- package/src/features/thread-naming.ts +33 -0
- package/src/middleware/allowlist.ts +64 -0
- package/src/middleware/rate-limiter.ts +46 -0
- package/src/queue.ts +43 -0
- package/src/spawner.ts +110 -0
- package/src/worker.ts +97 -0
package/bin/tether.ts
ADDED
|
@@ -0,0 +1,1010 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Tether CLI - Manage your Discord-AI Agent bridge
|
|
4
|
+
*
|
|
5
|
+
* Management Commands:
|
|
6
|
+
* tether start - Start bot and worker
|
|
7
|
+
* tether stop - Stop all processes
|
|
8
|
+
* tether status - Show running status
|
|
9
|
+
* tether health - Check Distether connection
|
|
10
|
+
* tether setup - Interactive setup wizard
|
|
11
|
+
*
|
|
12
|
+
* Distether Commands:
|
|
13
|
+
* tether send <channel> "message"
|
|
14
|
+
* tether embed <channel> "description" [--title, --color, --field, etc.]
|
|
15
|
+
* tether file <channel> <filepath> "message"
|
|
16
|
+
* tether buttons <channel> "prompt" --button label="..." id="..." [style, reply, webhook]
|
|
17
|
+
* tether ask <channel> "question" --option "A" --option "B" [--timeout 300]
|
|
18
|
+
* tether typing <channel>
|
|
19
|
+
* tether edit <channel> <messageId> "content"
|
|
20
|
+
* tether delete <channel> <messageId>
|
|
21
|
+
* tether rename <threadId> "name"
|
|
22
|
+
* tether reply <channel> <messageId> "message"
|
|
23
|
+
* tether thread <channel> <messageId> "name"
|
|
24
|
+
* tether react <channel> <messageId> "emoji"
|
|
25
|
+
* tether state <channel> <messageId> <state> (processing, done, error, or custom)
|
|
26
|
+
*
|
|
27
|
+
* DM Commands:
|
|
28
|
+
* tether dm <user-id> "message"
|
|
29
|
+
* tether dm <user-id> --embed "description" [--title, --color, --field, etc.]
|
|
30
|
+
* tether dm <user-id> --file <filepath> ["message"]
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { spawn, spawnSync } from 'bun';
|
|
34
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync } from 'fs';
|
|
35
|
+
import { join, dirname } from 'path';
|
|
36
|
+
import * as readline from 'readline';
|
|
37
|
+
import { homedir } from 'os';
|
|
38
|
+
import { randomUUID } from 'crypto';
|
|
39
|
+
|
|
40
|
+
const PID_FILE = join(process.cwd(), '.tether.pid');
|
|
41
|
+
const API_BASE = process.env.TETHER_API_URL || 'http://localhost:2643';
|
|
42
|
+
|
|
43
|
+
const command = process.argv[2];
|
|
44
|
+
const args = process.argv.slice(3);
|
|
45
|
+
|
|
46
|
+
// Color name to Distether color int mapping
|
|
47
|
+
const COLORS: Record<string, number> = {
|
|
48
|
+
red: 15158332, // 0xE74C3C
|
|
49
|
+
green: 3066993, // 0x2ECC71
|
|
50
|
+
blue: 3447003, // 0x3498DB
|
|
51
|
+
yellow: 16776960, // 0xFFFF00
|
|
52
|
+
purple: 10181046, // 0x9B59B6
|
|
53
|
+
orange: 15105570, // 0xE67E22
|
|
54
|
+
gray: 9807270, // 0x95A5A6
|
|
55
|
+
grey: 9807270, // 0x95A5A6
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Button style name to Distether style int mapping
|
|
59
|
+
const BUTTON_STYLES: Record<string, number> = {
|
|
60
|
+
primary: 1,
|
|
61
|
+
secondary: 2,
|
|
62
|
+
success: 3,
|
|
63
|
+
danger: 4,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
async function prompt(question: string): Promise<string> {
|
|
67
|
+
const rl = readline.createInterface({
|
|
68
|
+
input: process.stdin,
|
|
69
|
+
output: process.stdout,
|
|
70
|
+
});
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
rl.question(question, (answer) => {
|
|
73
|
+
rl.close();
|
|
74
|
+
resolve(answer.trim());
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ============ API Helper ============
|
|
80
|
+
|
|
81
|
+
async function apiCall(endpoint: string, body: any): Promise<any> {
|
|
82
|
+
try {
|
|
83
|
+
const response = await fetch(`${API_BASE}${endpoint}`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'Content-Type': 'application/json' },
|
|
86
|
+
body: JSON.stringify(body),
|
|
87
|
+
});
|
|
88
|
+
const data = await response.json();
|
|
89
|
+
if (!response.ok || data.error) {
|
|
90
|
+
console.error('Error:', data.error || 'Request failed');
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
return data;
|
|
94
|
+
} catch (error: any) {
|
|
95
|
+
if (error.code === 'ECONNREFUSED') {
|
|
96
|
+
console.error('Error: Cannot connect to Tether API. Is the bot running? (tether start)');
|
|
97
|
+
} else {
|
|
98
|
+
console.error('Error:', error.message);
|
|
99
|
+
}
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ============ Distether Commands ============
|
|
105
|
+
|
|
106
|
+
async function sendMessage() {
|
|
107
|
+
const channel = args[0];
|
|
108
|
+
const message = args[1];
|
|
109
|
+
if (!channel || !message) {
|
|
110
|
+
console.error('Usage: tether send <channel> "message"');
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
const result = await apiCall('/command', {
|
|
114
|
+
command: 'send-to-thread',
|
|
115
|
+
args: { thread: channel, message },
|
|
116
|
+
});
|
|
117
|
+
console.log(`Sent message: ${result.messageId}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function sendEmbed() {
|
|
121
|
+
const channel = args[0];
|
|
122
|
+
if (!channel) {
|
|
123
|
+
console.error('Usage: tether embed <channel> "description" [--title "..." --color green ...]');
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Parse flags and find positional description
|
|
128
|
+
const embed: any = {};
|
|
129
|
+
const fields: any[] = [];
|
|
130
|
+
let description = '';
|
|
131
|
+
let i = 1;
|
|
132
|
+
|
|
133
|
+
while (i < args.length) {
|
|
134
|
+
const arg = args[i];
|
|
135
|
+
if (arg === '--title' && args[i + 1]) {
|
|
136
|
+
embed.title = args[++i];
|
|
137
|
+
} else if (arg === '--url' && args[i + 1]) {
|
|
138
|
+
embed.url = args[++i];
|
|
139
|
+
} else if (arg === '--color' && args[i + 1]) {
|
|
140
|
+
const colorArg = args[++i].toLowerCase();
|
|
141
|
+
embed.color = COLORS[colorArg] || parseInt(colorArg.replace('0x', ''), 16) || 0;
|
|
142
|
+
} else if (arg === '--author' && args[i + 1]) {
|
|
143
|
+
embed.author = embed.author || {};
|
|
144
|
+
embed.author.name = args[++i];
|
|
145
|
+
} else if (arg === '--author-url' && args[i + 1]) {
|
|
146
|
+
embed.author = embed.author || {};
|
|
147
|
+
embed.author.url = args[++i];
|
|
148
|
+
} else if (arg === '--author-icon' && args[i + 1]) {
|
|
149
|
+
embed.author = embed.author || {};
|
|
150
|
+
embed.author.icon_url = args[++i];
|
|
151
|
+
} else if (arg === '--thumbnail' && args[i + 1]) {
|
|
152
|
+
embed.thumbnail = { url: args[++i] };
|
|
153
|
+
} else if (arg === '--image' && args[i + 1]) {
|
|
154
|
+
embed.image = { url: args[++i] };
|
|
155
|
+
} else if (arg === '--footer' && args[i + 1]) {
|
|
156
|
+
embed.footer = embed.footer || {};
|
|
157
|
+
embed.footer.text = args[++i];
|
|
158
|
+
} else if (arg === '--footer-icon' && args[i + 1]) {
|
|
159
|
+
embed.footer = embed.footer || {};
|
|
160
|
+
embed.footer.icon_url = args[++i];
|
|
161
|
+
} else if (arg === '--timestamp') {
|
|
162
|
+
embed.timestamp = new Date().toISOString();
|
|
163
|
+
} else if (arg === '--field' && args[i + 1]) {
|
|
164
|
+
const fieldStr = args[++i];
|
|
165
|
+
const parts = fieldStr.split(':');
|
|
166
|
+
if (parts.length >= 2) {
|
|
167
|
+
fields.push({
|
|
168
|
+
name: parts[0],
|
|
169
|
+
value: parts[1],
|
|
170
|
+
inline: parts[2]?.toLowerCase() === 'inline',
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
} else if (!arg.startsWith('--')) {
|
|
174
|
+
description = arg;
|
|
175
|
+
}
|
|
176
|
+
i++;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (description) embed.description = description;
|
|
180
|
+
if (fields.length > 0) embed.fields = fields;
|
|
181
|
+
|
|
182
|
+
const result = await apiCall('/command', {
|
|
183
|
+
command: 'send-to-thread',
|
|
184
|
+
args: { thread: channel, embeds: [embed] },
|
|
185
|
+
});
|
|
186
|
+
console.log(`Sent embed: ${result.messageId}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function sendFile() {
|
|
190
|
+
const channel = args[0];
|
|
191
|
+
const filepath = args[1];
|
|
192
|
+
const message = args[2] || '';
|
|
193
|
+
|
|
194
|
+
if (!channel || !filepath) {
|
|
195
|
+
console.error('Usage: tether file <channel> <filepath> ["message"]');
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!existsSync(filepath)) {
|
|
200
|
+
console.error(`Error: File not found: ${filepath}`);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const fileContent = readFileSync(filepath, 'utf-8');
|
|
205
|
+
const fileName = filepath.split('/').pop() || 'file.txt';
|
|
206
|
+
|
|
207
|
+
const result = await apiCall('/send-with-file', {
|
|
208
|
+
channelId: channel,
|
|
209
|
+
fileName,
|
|
210
|
+
fileContent,
|
|
211
|
+
content: message,
|
|
212
|
+
});
|
|
213
|
+
console.log(`Sent file: ${result.messageId}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function sendButtons() {
|
|
217
|
+
const channel = args[0];
|
|
218
|
+
if (!channel) {
|
|
219
|
+
console.error('Usage: tether buttons <channel> "prompt" --button label="..." id="..." [style="success"] [reply="..."] [webhook="..."]');
|
|
220
|
+
process.exit(1);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
let promptText = '';
|
|
224
|
+
const buttons: any[] = [];
|
|
225
|
+
let i = 1;
|
|
226
|
+
|
|
227
|
+
while (i < args.length) {
|
|
228
|
+
const arg = args[i];
|
|
229
|
+
if (arg === '--button') {
|
|
230
|
+
// Collect all following key=value pairs until next flag or end
|
|
231
|
+
const button: any = {};
|
|
232
|
+
i++;
|
|
233
|
+
while (i < args.length && !args[i].startsWith('--')) {
|
|
234
|
+
const kvMatch = args[i].match(/^(\w+)=(.*)$/);
|
|
235
|
+
if (kvMatch) {
|
|
236
|
+
const [, key, value] = kvMatch;
|
|
237
|
+
if (key === 'style') {
|
|
238
|
+
button.style = BUTTON_STYLES[value.toLowerCase()] || 1;
|
|
239
|
+
} else {
|
|
240
|
+
button[key] = value;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
i++;
|
|
244
|
+
}
|
|
245
|
+
if (button.label && button.id) {
|
|
246
|
+
// Convert to API format
|
|
247
|
+
const apiButton: any = {
|
|
248
|
+
label: button.label,
|
|
249
|
+
customId: button.id,
|
|
250
|
+
style: button.style || 1,
|
|
251
|
+
};
|
|
252
|
+
if (button.reply || button.webhook) {
|
|
253
|
+
apiButton.handler = {};
|
|
254
|
+
if (button.reply) {
|
|
255
|
+
apiButton.handler.type = 'inline';
|
|
256
|
+
apiButton.handler.content = button.reply;
|
|
257
|
+
apiButton.handler.ephemeral = true;
|
|
258
|
+
}
|
|
259
|
+
if (button.webhook) {
|
|
260
|
+
apiButton.handler.type = button.reply ? 'inline' : 'webhook';
|
|
261
|
+
apiButton.handler.webhookUrl = button.webhook;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
buttons.push(apiButton);
|
|
265
|
+
}
|
|
266
|
+
continue; // Don't increment i again
|
|
267
|
+
} else if (!arg.startsWith('--')) {
|
|
268
|
+
promptText = arg;
|
|
269
|
+
}
|
|
270
|
+
i++;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (buttons.length === 0) {
|
|
274
|
+
console.error('Error: At least one --button is required');
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const result = await apiCall('/send-with-buttons', {
|
|
279
|
+
channelId: channel,
|
|
280
|
+
content: promptText,
|
|
281
|
+
buttons,
|
|
282
|
+
});
|
|
283
|
+
console.log(`Sent buttons: ${result.messageId}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function askQuestion() {
|
|
287
|
+
const channel = args[0];
|
|
288
|
+
const questionText = args[1];
|
|
289
|
+
|
|
290
|
+
if (!channel || !questionText) {
|
|
291
|
+
console.error('Usage: tether ask <channelId> "question text" --option "Option A" --option "Option B" [--timeout 300]');
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Parse options and timeout
|
|
296
|
+
const options: string[] = [];
|
|
297
|
+
let timeout = 300; // Default 300 seconds (5 minutes)
|
|
298
|
+
let i = 2;
|
|
299
|
+
|
|
300
|
+
while (i < args.length) {
|
|
301
|
+
const arg = args[i];
|
|
302
|
+
if (arg === '--option' && args[i + 1]) {
|
|
303
|
+
options.push(args[i + 1]);
|
|
304
|
+
i += 2;
|
|
305
|
+
} else if (arg === '--timeout' && args[i + 1]) {
|
|
306
|
+
timeout = parseInt(args[i + 1], 10);
|
|
307
|
+
if (isNaN(timeout) || timeout <= 0) {
|
|
308
|
+
console.error('Error: --timeout must be a positive number');
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
i += 2;
|
|
312
|
+
} else {
|
|
313
|
+
i++;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (options.length === 0) {
|
|
318
|
+
console.error('Error: At least one --option is required');
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Generate unique request ID
|
|
323
|
+
const requestId = randomUUID();
|
|
324
|
+
const API_PORT = process.env.TETHER_API_PORT ? parseInt(process.env.TETHER_API_PORT) : 2643;
|
|
325
|
+
|
|
326
|
+
// Build buttons array: one per option + "Type answer" button
|
|
327
|
+
const buttons = options.map((label, index) => ({
|
|
328
|
+
label,
|
|
329
|
+
customId: `ask_${requestId}_${index}`,
|
|
330
|
+
style: 'primary',
|
|
331
|
+
handler: {
|
|
332
|
+
type: 'webhook',
|
|
333
|
+
url: `http://localhost:${API_PORT}/question-response/${requestId}`,
|
|
334
|
+
data: {
|
|
335
|
+
option: label,
|
|
336
|
+
optionIndex: index,
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
}));
|
|
340
|
+
|
|
341
|
+
// Add "Type answer" button
|
|
342
|
+
buttons.push({
|
|
343
|
+
label: '✏️ Type answer',
|
|
344
|
+
customId: `ask_${requestId}_type`,
|
|
345
|
+
style: 'secondary',
|
|
346
|
+
handler: {
|
|
347
|
+
type: 'webhook',
|
|
348
|
+
url: `http://localhost:${API_PORT}/question-response/${requestId}`,
|
|
349
|
+
data: {
|
|
350
|
+
option: '__type__',
|
|
351
|
+
optionIndex: -1,
|
|
352
|
+
threadId: channel,
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
} as any);
|
|
356
|
+
|
|
357
|
+
// Send buttons message
|
|
358
|
+
try {
|
|
359
|
+
await apiCall('/send-with-buttons', {
|
|
360
|
+
channelId: channel,
|
|
361
|
+
content: questionText,
|
|
362
|
+
buttons,
|
|
363
|
+
});
|
|
364
|
+
} catch (error) {
|
|
365
|
+
console.error('Failed to send question message');
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Poll for response
|
|
370
|
+
const pollInterval = 2000; // 2 seconds
|
|
371
|
+
const maxAttempts = Math.ceil((timeout * 1000) / pollInterval);
|
|
372
|
+
let attempts = 0;
|
|
373
|
+
|
|
374
|
+
while (attempts < maxAttempts) {
|
|
375
|
+
try {
|
|
376
|
+
const response = await fetch(`http://localhost:${API_PORT}/question-response/${requestId}`);
|
|
377
|
+
|
|
378
|
+
if (response.status === 404) {
|
|
379
|
+
// Not yet registered or answered, keep polling
|
|
380
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
381
|
+
attempts++;
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (response.ok) {
|
|
386
|
+
const data = await response.json() as { answered: boolean; answer?: string; optionIndex?: number };
|
|
387
|
+
|
|
388
|
+
if (data.answered) {
|
|
389
|
+
if (data.answer === '__type__') {
|
|
390
|
+
// User clicked "Type answer" - keep polling for typed response
|
|
391
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
392
|
+
attempts++;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Got a real answer
|
|
397
|
+
console.log(data.answer);
|
|
398
|
+
process.exit(0);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
} catch (error) {
|
|
402
|
+
// Network error, keep trying
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
406
|
+
attempts++;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Timeout
|
|
410
|
+
console.error('No response received');
|
|
411
|
+
process.exit(1);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function startTyping() {
|
|
415
|
+
const channel = args[0];
|
|
416
|
+
if (!channel) {
|
|
417
|
+
console.error('Usage: tether typing <channel>');
|
|
418
|
+
process.exit(1);
|
|
419
|
+
}
|
|
420
|
+
await apiCall('/command', {
|
|
421
|
+
command: 'start-typing',
|
|
422
|
+
args: { channel },
|
|
423
|
+
});
|
|
424
|
+
console.log('Typing indicator sent');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function editMessage() {
|
|
428
|
+
const channel = args[0];
|
|
429
|
+
const messageId = args[1];
|
|
430
|
+
const content = args[2];
|
|
431
|
+
if (!channel || !messageId || !content) {
|
|
432
|
+
console.error('Usage: tether edit <channel> <messageId> "new content"');
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
await apiCall('/command', {
|
|
436
|
+
command: 'edit-message',
|
|
437
|
+
args: { channel, message: messageId, content },
|
|
438
|
+
});
|
|
439
|
+
console.log(`Edited message: ${messageId}`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function deleteMessage() {
|
|
443
|
+
const channel = args[0];
|
|
444
|
+
const messageId = args[1];
|
|
445
|
+
if (!channel || !messageId) {
|
|
446
|
+
console.error('Usage: tether delete <channel> <messageId>');
|
|
447
|
+
process.exit(1);
|
|
448
|
+
}
|
|
449
|
+
await apiCall('/command', {
|
|
450
|
+
command: 'delete-message',
|
|
451
|
+
args: { channel, message: messageId },
|
|
452
|
+
});
|
|
453
|
+
console.log(`Deleted message: ${messageId}`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function renameThread() {
|
|
457
|
+
const threadId = args[0];
|
|
458
|
+
const name = args[1];
|
|
459
|
+
if (!threadId || !name) {
|
|
460
|
+
console.error('Usage: tether rename <threadId> "new name"');
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
await apiCall('/command', {
|
|
464
|
+
command: 'rename-thread',
|
|
465
|
+
args: { thread: threadId, name },
|
|
466
|
+
});
|
|
467
|
+
console.log(`Renamed thread: ${threadId}`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function replyToMessage() {
|
|
471
|
+
const channel = args[0];
|
|
472
|
+
const messageId = args[1];
|
|
473
|
+
const message = args[2];
|
|
474
|
+
if (!channel || !messageId || !message) {
|
|
475
|
+
console.error('Usage: tether reply <channel> <messageId> "message"');
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
const result = await apiCall('/command', {
|
|
479
|
+
command: 'reply-to-message',
|
|
480
|
+
args: { channel, message: messageId, content: message },
|
|
481
|
+
});
|
|
482
|
+
console.log(`Replied to message: ${result.messageId}`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function createThread() {
|
|
486
|
+
const channel = args[0];
|
|
487
|
+
const messageId = args[1];
|
|
488
|
+
const name = args[2];
|
|
489
|
+
if (!channel || !messageId || !name) {
|
|
490
|
+
console.error('Usage: tether thread <channel> <messageId> "thread name"');
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
const result = await apiCall('/command', {
|
|
494
|
+
command: 'create-thread',
|
|
495
|
+
args: { channel, message: messageId, name },
|
|
496
|
+
});
|
|
497
|
+
console.log(`Created thread: ${result.threadId}`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async function addReaction() {
|
|
501
|
+
const channel = args[0];
|
|
502
|
+
const messageId = args[1];
|
|
503
|
+
const emoji = args[2];
|
|
504
|
+
if (!channel || !messageId || !emoji) {
|
|
505
|
+
console.error('Usage: tether react <channel> <messageId> "emoji"');
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
await apiCall('/command', {
|
|
509
|
+
command: 'add-reaction',
|
|
510
|
+
args: { channel, message: messageId, emoji },
|
|
511
|
+
});
|
|
512
|
+
console.log(`Added reaction: ${emoji}`);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function sendDM() {
|
|
516
|
+
const userId = args[0];
|
|
517
|
+
if (!userId) {
|
|
518
|
+
console.error('Usage: tether dm <user-id> "message"');
|
|
519
|
+
console.error(' tether dm <user-id> --embed "description" [--title, --color, ...]');
|
|
520
|
+
console.error(' tether dm <user-id> --file <filepath> ["message"]');
|
|
521
|
+
process.exit(1);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const subArgs = args.slice(1);
|
|
525
|
+
|
|
526
|
+
// --file mode: tether dm <user-id> --file <path> ["message"]
|
|
527
|
+
if (subArgs[0] === '--file') {
|
|
528
|
+
const filepath = subArgs[1];
|
|
529
|
+
const message = subArgs[2] || '';
|
|
530
|
+
|
|
531
|
+
if (!filepath) {
|
|
532
|
+
console.error('Usage: tether dm <user-id> --file <filepath> ["message"]');
|
|
533
|
+
process.exit(1);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
if (!existsSync(filepath)) {
|
|
537
|
+
console.error(`Error: File not found: ${filepath}`);
|
|
538
|
+
process.exit(1);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const fileContent = readFileSync(filepath, 'utf-8');
|
|
542
|
+
const fileName = filepath.split('/').pop() || 'file.txt';
|
|
543
|
+
|
|
544
|
+
const result = await apiCall('/send-dm-file', {
|
|
545
|
+
userId,
|
|
546
|
+
fileName,
|
|
547
|
+
fileContent,
|
|
548
|
+
content: message,
|
|
549
|
+
});
|
|
550
|
+
console.log(`DM file sent: ${result.messageId}`);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// --embed mode: tether dm <user-id> --embed "description" [--title, --color, ...]
|
|
555
|
+
if (subArgs[0] === '--embed') {
|
|
556
|
+
const embed: any = {};
|
|
557
|
+
const fields: any[] = [];
|
|
558
|
+
let description = '';
|
|
559
|
+
let i = 1;
|
|
560
|
+
|
|
561
|
+
while (i < subArgs.length) {
|
|
562
|
+
const arg = subArgs[i];
|
|
563
|
+
if (arg === '--title' && subArgs[i + 1]) {
|
|
564
|
+
embed.title = subArgs[++i];
|
|
565
|
+
} else if (arg === '--color' && subArgs[i + 1]) {
|
|
566
|
+
const colorArg = subArgs[++i].toLowerCase();
|
|
567
|
+
embed.color = COLORS[colorArg] || parseInt(colorArg.replace('0x', ''), 16) || 0;
|
|
568
|
+
} else if (arg === '--footer' && subArgs[i + 1]) {
|
|
569
|
+
embed.footer = { text: subArgs[++i] };
|
|
570
|
+
} else if (arg === '--field' && subArgs[i + 1]) {
|
|
571
|
+
const fieldStr = subArgs[++i];
|
|
572
|
+
const parts = fieldStr.split(':');
|
|
573
|
+
if (parts.length >= 2) {
|
|
574
|
+
fields.push({
|
|
575
|
+
name: parts[0],
|
|
576
|
+
value: parts[1],
|
|
577
|
+
inline: parts[2]?.toLowerCase() === 'inline',
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
} else if (arg === '--timestamp') {
|
|
581
|
+
embed.timestamp = new Date().toISOString();
|
|
582
|
+
} else if (!arg.startsWith('--')) {
|
|
583
|
+
description = arg;
|
|
584
|
+
}
|
|
585
|
+
i++;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (description) embed.description = description;
|
|
589
|
+
if (fields.length > 0) embed.fields = fields;
|
|
590
|
+
|
|
591
|
+
const result = await apiCall('/command', {
|
|
592
|
+
command: 'send-dm',
|
|
593
|
+
args: { userId, embeds: [embed] },
|
|
594
|
+
});
|
|
595
|
+
console.log(`DM embed sent: ${result.messageId}`);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Default: text message — tether dm <user-id> "message"
|
|
600
|
+
const message = subArgs[0];
|
|
601
|
+
if (!message) {
|
|
602
|
+
console.error('Usage: tether dm <user-id> "message"');
|
|
603
|
+
process.exit(1);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const result = await apiCall('/command', {
|
|
607
|
+
command: 'send-dm',
|
|
608
|
+
args: { userId, message },
|
|
609
|
+
});
|
|
610
|
+
console.log(`DM sent: ${result.messageId}`);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// State presets for thread status updates
|
|
614
|
+
const STATE_PRESETS: Record<string, string> = {
|
|
615
|
+
processing: '🤖 Processing...',
|
|
616
|
+
thinking: '🧠 Thinking...',
|
|
617
|
+
searching: '🔍 Searching...',
|
|
618
|
+
writing: '✍️ Writing...',
|
|
619
|
+
done: '✅ Done',
|
|
620
|
+
error: '❌ Something went wrong',
|
|
621
|
+
waiting: '⏳ Waiting for input...',
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
async function updateState() {
|
|
625
|
+
const channel = args[0];
|
|
626
|
+
const messageId = args[1];
|
|
627
|
+
const stateOrCustom = args[2];
|
|
628
|
+
|
|
629
|
+
if (!channel || !messageId || !stateOrCustom) {
|
|
630
|
+
console.error('Usage: tether state <channel> <messageId> <state>');
|
|
631
|
+
console.error('');
|
|
632
|
+
console.error('Preset states:');
|
|
633
|
+
console.error(' processing - 🤖 Processing...');
|
|
634
|
+
console.error(' thinking - 🧠 Thinking...');
|
|
635
|
+
console.error(' searching - 🔍 Searching...');
|
|
636
|
+
console.error(' writing - ✍️ Writing...');
|
|
637
|
+
console.error(' done - ✅ Done');
|
|
638
|
+
console.error(' error - ❌ Something went wrong');
|
|
639
|
+
console.error(' waiting - ⏳ Waiting for input...');
|
|
640
|
+
console.error('');
|
|
641
|
+
console.error('Or use custom text: tether state <channel> <messageId> "Custom status"');
|
|
642
|
+
process.exit(1);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const content = STATE_PRESETS[stateOrCustom.toLowerCase()] || stateOrCustom;
|
|
646
|
+
|
|
647
|
+
await apiCall('/command', {
|
|
648
|
+
command: 'edit-message',
|
|
649
|
+
args: { channel, message: messageId, content },
|
|
650
|
+
});
|
|
651
|
+
console.log(`Updated state: ${content}`);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// ============ Management Commands ============
|
|
655
|
+
|
|
656
|
+
async function setup() {
|
|
657
|
+
console.log('\n🔌 Tether Setup\n');
|
|
658
|
+
|
|
659
|
+
// Check for .env
|
|
660
|
+
const envPath = join(process.cwd(), '.env');
|
|
661
|
+
const envExamplePath = join(process.cwd(), '.env.example');
|
|
662
|
+
|
|
663
|
+
if (existsSync(envPath)) {
|
|
664
|
+
console.log('✓ .env file exists');
|
|
665
|
+
} else if (existsSync(envExamplePath)) {
|
|
666
|
+
console.log('Creating .env from .env.example...\n');
|
|
667
|
+
|
|
668
|
+
const token = await prompt('Distether Bot Token: ');
|
|
669
|
+
if (!token) {
|
|
670
|
+
console.log('Token required. Run setup again when ready.');
|
|
671
|
+
process.exit(1);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const tz = await prompt('Timezone (default: America/New_York): ') || 'America/New_York';
|
|
675
|
+
|
|
676
|
+
let envContent = readFileSync(envExamplePath, 'utf-8');
|
|
677
|
+
envContent = envContent.replace('your-bot-token-here', token);
|
|
678
|
+
envContent = envContent.replace('TZ=America/New_York', `TZ=${tz}`);
|
|
679
|
+
|
|
680
|
+
writeFileSync(envPath, envContent);
|
|
681
|
+
console.log('\n✓ .env file created');
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Check Redis
|
|
685
|
+
const redis = spawnSync(['redis-cli', 'ping'], { stdout: 'pipe', stderr: 'pipe' });
|
|
686
|
+
if (redis.exitCode === 0) {
|
|
687
|
+
console.log('✓ Redis is running');
|
|
688
|
+
} else {
|
|
689
|
+
console.log('⚠ Redis not running. Start it with: redis-server');
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Check Claude CLI
|
|
693
|
+
const claude = spawnSync(['claude', '--version'], { stdout: 'pipe', stderr: 'pipe' });
|
|
694
|
+
if (claude.exitCode === 0) {
|
|
695
|
+
console.log('✓ Claude CLI installed');
|
|
696
|
+
} else {
|
|
697
|
+
console.log('⚠ Claude CLI not found. Install from: https://claude.ai/code');
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Install Claude Code skill
|
|
701
|
+
const skillsDir = join(homedir(), '.claude', 'skills', 'cord');
|
|
702
|
+
const cordRoot = join(dirname(import.meta.dir));
|
|
703
|
+
const sourceSkillsDir = join(cordRoot, 'skills', 'cord');
|
|
704
|
+
|
|
705
|
+
if (existsSync(sourceSkillsDir)) {
|
|
706
|
+
console.log('\n📚 Claude Code Skill');
|
|
707
|
+
console.log(' Teaches your assistant how to send Distether messages, embeds,');
|
|
708
|
+
console.log(' files, and interactive buttons.');
|
|
709
|
+
const installSkill = await prompt('Install skill? (Y/n): ');
|
|
710
|
+
if (installSkill.toLowerCase() !== 'n') {
|
|
711
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
712
|
+
cpSync(sourceSkillsDir, skillsDir, { recursive: true });
|
|
713
|
+
console.log(`✓ Skill installed to ${skillsDir}`);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
console.log('\n✨ Setup complete! Run: tether start\n');
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
async function start() {
|
|
721
|
+
if (existsSync(PID_FILE)) {
|
|
722
|
+
console.log('Tether is already running. Run: tether stop');
|
|
723
|
+
process.exit(1);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
console.log('Starting Tether...\n');
|
|
727
|
+
|
|
728
|
+
// Start bot
|
|
729
|
+
const bot = spawn(['bun', 'run', 'src/bot.ts'], {
|
|
730
|
+
stdout: 'inherit',
|
|
731
|
+
stderr: 'inherit',
|
|
732
|
+
cwd: process.cwd(),
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
// Start worker
|
|
736
|
+
const worker = spawn(['bun', 'run', 'src/worker.ts'], {
|
|
737
|
+
stdout: 'inherit',
|
|
738
|
+
stderr: 'inherit',
|
|
739
|
+
cwd: process.cwd(),
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
// Save PIDs
|
|
743
|
+
writeFileSync(PID_FILE, JSON.stringify({
|
|
744
|
+
bot: bot.pid,
|
|
745
|
+
worker: worker.pid,
|
|
746
|
+
startedAt: new Date().toISOString(),
|
|
747
|
+
}));
|
|
748
|
+
|
|
749
|
+
console.log(`Bot PID: ${bot.pid}`);
|
|
750
|
+
console.log(`Worker PID: ${worker.pid}`);
|
|
751
|
+
console.log('\nTether is running. Press Ctrl+C to stop.\n');
|
|
752
|
+
|
|
753
|
+
// Handle exit
|
|
754
|
+
process.on('SIGINT', () => {
|
|
755
|
+
console.log('\nStopping Tether...');
|
|
756
|
+
bot.kill();
|
|
757
|
+
worker.kill();
|
|
758
|
+
if (existsSync(PID_FILE)) {
|
|
759
|
+
const fs = require('fs');
|
|
760
|
+
fs.unlinkSync(PID_FILE);
|
|
761
|
+
}
|
|
762
|
+
process.exit(0);
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
// Wait for processes
|
|
766
|
+
await Promise.all([bot.exited, worker.exited]);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function stop() {
|
|
770
|
+
if (!existsSync(PID_FILE)) {
|
|
771
|
+
console.log('Tether is not running.');
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const pids = JSON.parse(readFileSync(PID_FILE, 'utf-8'));
|
|
776
|
+
|
|
777
|
+
try {
|
|
778
|
+
process.kill(pids.bot);
|
|
779
|
+
console.log(`Stopped bot (PID ${pids.bot})`);
|
|
780
|
+
} catch {}
|
|
781
|
+
|
|
782
|
+
try {
|
|
783
|
+
process.kill(pids.worker);
|
|
784
|
+
console.log(`Stopped worker (PID ${pids.worker})`);
|
|
785
|
+
} catch {}
|
|
786
|
+
|
|
787
|
+
const fs = require('fs');
|
|
788
|
+
fs.unlinkSync(PID_FILE);
|
|
789
|
+
console.log('Tether stopped.');
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function status() {
|
|
793
|
+
if (!existsSync(PID_FILE)) {
|
|
794
|
+
console.log('Tether is not running.');
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const pids = JSON.parse(readFileSync(PID_FILE, 'utf-8'));
|
|
799
|
+
|
|
800
|
+
const botAlive = isProcessRunning(pids.bot);
|
|
801
|
+
const workerAlive = isProcessRunning(pids.worker);
|
|
802
|
+
|
|
803
|
+
console.log(`Bot: ${botAlive ? '✓ running' : '✗ stopped'} (PID ${pids.bot})`);
|
|
804
|
+
console.log(`Worker: ${workerAlive ? '✓ running' : '✗ stopped'} (PID ${pids.worker})`);
|
|
805
|
+
console.log(`Started: ${pids.startedAt}`);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function isProcessRunning(pid: number): boolean {
|
|
809
|
+
try {
|
|
810
|
+
process.kill(pid, 0);
|
|
811
|
+
return true;
|
|
812
|
+
} catch {
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
async function health() {
|
|
818
|
+
try {
|
|
819
|
+
const response = await fetch(`${API_BASE}/health`);
|
|
820
|
+
const data = await response.json() as { status: string; connected: boolean; user: string };
|
|
821
|
+
|
|
822
|
+
if (data.connected) {
|
|
823
|
+
console.log(`✓ Connected as ${data.user}`);
|
|
824
|
+
} else {
|
|
825
|
+
console.log('✗ Bot not connected to Discord');
|
|
826
|
+
}
|
|
827
|
+
} catch (error: any) {
|
|
828
|
+
if (error.code === 'ECONNREFUSED') {
|
|
829
|
+
console.log('✗ Cannot connect to Tether API. Is the bot running? (tether start)');
|
|
830
|
+
} else {
|
|
831
|
+
console.log(`✗ Error: ${error.message}`);
|
|
832
|
+
}
|
|
833
|
+
process.exit(1);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
function showHelp() {
|
|
838
|
+
console.log(`
|
|
839
|
+
Tether - Distether to Claude Code bridge
|
|
840
|
+
|
|
841
|
+
Usage: tether <command> [options]
|
|
842
|
+
|
|
843
|
+
Management Commands:
|
|
844
|
+
start Start bot and worker
|
|
845
|
+
stop Stop all processes
|
|
846
|
+
status Show running status
|
|
847
|
+
health Check Distether connection
|
|
848
|
+
setup Interactive setup wizard
|
|
849
|
+
help Show this help
|
|
850
|
+
|
|
851
|
+
Distether Commands:
|
|
852
|
+
send <channel> "message"
|
|
853
|
+
Send a text message
|
|
854
|
+
|
|
855
|
+
embed <channel> "description" [options]
|
|
856
|
+
Send an embed with optional formatting
|
|
857
|
+
--title "..." Embed title
|
|
858
|
+
--url "..." Title link URL
|
|
859
|
+
--color <name|hex> red, green, blue, yellow, purple, orange, or 0xHEX
|
|
860
|
+
--author "..." Author name
|
|
861
|
+
--author-url "..." Author link
|
|
862
|
+
--author-icon "..." Author icon URL
|
|
863
|
+
--thumbnail "..." Small image (top right)
|
|
864
|
+
--image "..." Large image (bottom)
|
|
865
|
+
--footer "..." Footer text
|
|
866
|
+
--footer-icon "..." Footer icon URL
|
|
867
|
+
--timestamp Add current timestamp
|
|
868
|
+
--field "Name:Value" Add field (use :inline for inline)
|
|
869
|
+
|
|
870
|
+
file <channel> <filepath> ["message"]
|
|
871
|
+
Send a file attachment
|
|
872
|
+
|
|
873
|
+
buttons <channel> "prompt" --button label="..." id="..." [options]
|
|
874
|
+
Send interactive buttons
|
|
875
|
+
Button options:
|
|
876
|
+
label="..." Button text (required)
|
|
877
|
+
id="..." Custom ID (required)
|
|
878
|
+
style="..." primary, secondary, success, danger
|
|
879
|
+
reply="..." Ephemeral reply when clicked
|
|
880
|
+
webhook="..." URL to POST click data to
|
|
881
|
+
|
|
882
|
+
ask <channel> "question" --option "A" --option "B" [--timeout 300]
|
|
883
|
+
Ask a blocking question with button options (blocks until answered)
|
|
884
|
+
Prints selected answer to stdout, exits 0 on success, 1 on timeout
|
|
885
|
+
Automatically includes a "Type answer" button for free-form input
|
|
886
|
+
|
|
887
|
+
typing <channel>
|
|
888
|
+
Show typing indicator
|
|
889
|
+
|
|
890
|
+
edit <channel> <messageId> "content"
|
|
891
|
+
Edit an existing message
|
|
892
|
+
|
|
893
|
+
delete <channel> <messageId>
|
|
894
|
+
Delete a message
|
|
895
|
+
|
|
896
|
+
rename <threadId> "name"
|
|
897
|
+
Rename a thread
|
|
898
|
+
|
|
899
|
+
reply <channel> <messageId> "message"
|
|
900
|
+
Reply to a specific message
|
|
901
|
+
|
|
902
|
+
thread <channel> <messageId> "name"
|
|
903
|
+
Create a thread from a message
|
|
904
|
+
|
|
905
|
+
react <channel> <messageId> "emoji"
|
|
906
|
+
Add a reaction to a message
|
|
907
|
+
|
|
908
|
+
state <channel> <messageId> <state>
|
|
909
|
+
Update thread status with preset or custom text
|
|
910
|
+
Presets: processing, thinking, searching, writing, done, error, waiting
|
|
911
|
+
|
|
912
|
+
DM Commands (proactive outreach):
|
|
913
|
+
dm <user-id> "message"
|
|
914
|
+
Send a text DM to a user
|
|
915
|
+
|
|
916
|
+
dm <user-id> --embed "description" [options]
|
|
917
|
+
Send an embed DM (same options as embed command)
|
|
918
|
+
|
|
919
|
+
dm <user-id> --file <filepath> ["message"]
|
|
920
|
+
Send a file attachment via DM
|
|
921
|
+
|
|
922
|
+
Examples:
|
|
923
|
+
tether send 123456789 "Hello world!"
|
|
924
|
+
tether embed 123456789 "Status update" --title "Daily Report" --color green --field "Tasks:5 done:inline"
|
|
925
|
+
tether buttons 123456789 "Approve?" --button label="Yes" id="approve" style="success" reply="Approved!"
|
|
926
|
+
tether ask 123456789 "Deploy to prod?" --option "Yes" --option "No" --timeout 600
|
|
927
|
+
tether file 123456789 ./report.md "Here's the report"
|
|
928
|
+
tether state 123456789 1234567890 processing
|
|
929
|
+
tether state 123456789 1234567890 done
|
|
930
|
+
tether dm 987654321 "Hey, I need your approval on this PR"
|
|
931
|
+
tether dm 987654321 --embed "Build passed" --title "CI Update" --color green
|
|
932
|
+
tether dm 987654321 --file ./report.md "Here's the report"
|
|
933
|
+
`);
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
// ============ Main ============
|
|
937
|
+
|
|
938
|
+
switch (command) {
|
|
939
|
+
// Management
|
|
940
|
+
case 'start':
|
|
941
|
+
start();
|
|
942
|
+
break;
|
|
943
|
+
case 'stop':
|
|
944
|
+
stop();
|
|
945
|
+
break;
|
|
946
|
+
case 'status':
|
|
947
|
+
status();
|
|
948
|
+
break;
|
|
949
|
+
case 'setup':
|
|
950
|
+
setup();
|
|
951
|
+
break;
|
|
952
|
+
case 'health':
|
|
953
|
+
health();
|
|
954
|
+
break;
|
|
955
|
+
case 'help':
|
|
956
|
+
case '--help':
|
|
957
|
+
case '-h':
|
|
958
|
+
case undefined:
|
|
959
|
+
showHelp();
|
|
960
|
+
break;
|
|
961
|
+
|
|
962
|
+
// Distether commands
|
|
963
|
+
case 'send':
|
|
964
|
+
sendMessage();
|
|
965
|
+
break;
|
|
966
|
+
case 'embed':
|
|
967
|
+
sendEmbed();
|
|
968
|
+
break;
|
|
969
|
+
case 'file':
|
|
970
|
+
sendFile();
|
|
971
|
+
break;
|
|
972
|
+
case 'buttons':
|
|
973
|
+
sendButtons();
|
|
974
|
+
break;
|
|
975
|
+
case 'ask':
|
|
976
|
+
askQuestion();
|
|
977
|
+
break;
|
|
978
|
+
case 'typing':
|
|
979
|
+
startTyping();
|
|
980
|
+
break;
|
|
981
|
+
case 'edit':
|
|
982
|
+
editMessage();
|
|
983
|
+
break;
|
|
984
|
+
case 'delete':
|
|
985
|
+
deleteMessage();
|
|
986
|
+
break;
|
|
987
|
+
case 'rename':
|
|
988
|
+
renameThread();
|
|
989
|
+
break;
|
|
990
|
+
case 'reply':
|
|
991
|
+
replyToMessage();
|
|
992
|
+
break;
|
|
993
|
+
case 'thread':
|
|
994
|
+
createThread();
|
|
995
|
+
break;
|
|
996
|
+
case 'react':
|
|
997
|
+
addReaction();
|
|
998
|
+
break;
|
|
999
|
+
case 'state':
|
|
1000
|
+
updateState();
|
|
1001
|
+
break;
|
|
1002
|
+
case 'dm':
|
|
1003
|
+
sendDM();
|
|
1004
|
+
break;
|
|
1005
|
+
|
|
1006
|
+
default:
|
|
1007
|
+
console.log(`Unknown command: ${command}`);
|
|
1008
|
+
showHelp();
|
|
1009
|
+
process.exit(1);
|
|
1010
|
+
}
|