@thesammykins/tether 1.2.0 ā 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tether.ts +32 -4
- package/docs/configuration.md +2 -0
- package/package.json +1 -1
- package/src/bot.ts +68 -23
- package/src/config.ts +2 -0
package/bin/tether.ts
CHANGED
|
@@ -817,18 +817,46 @@ async function start() {
|
|
|
817
817
|
|
|
818
818
|
console.log('Starting Tether...\n');
|
|
819
819
|
|
|
820
|
+
// Resolve script paths relative to the package root, not process.cwd().
|
|
821
|
+
// When installed globally or via npx, cwd is the user's project dir where
|
|
822
|
+
// src/bot.ts doesn't exist. import.meta.dir is bin/, so one level up is root.
|
|
823
|
+
const packageRoot = dirname(import.meta.dir);
|
|
824
|
+
const botScript = join(packageRoot, 'src', 'bot.ts');
|
|
825
|
+
const workerScript = join(packageRoot, 'src', 'worker.ts');
|
|
826
|
+
|
|
827
|
+
// Load config store values into child process environment.
|
|
828
|
+
// Secrets are encrypted on disk ā decrypt them so bot/worker can read
|
|
829
|
+
// DISCORD_BOT_TOKEN etc. from process.env as they expect.
|
|
830
|
+
const childEnv: Record<string, string | undefined> = { ...process.env };
|
|
831
|
+
const prefs = readPreferences();
|
|
832
|
+
for (const [key, value] of Object.entries(prefs)) {
|
|
833
|
+
if (!childEnv[key]) childEnv[key] = value;
|
|
834
|
+
}
|
|
835
|
+
if (hasSecrets()) {
|
|
836
|
+
const pw = await promptPassword('Encryption password: ');
|
|
837
|
+
try {
|
|
838
|
+
const secrets = readSecrets(pw);
|
|
839
|
+
for (const [key, value] of Object.entries(secrets)) {
|
|
840
|
+
if (!childEnv[key]) childEnv[key] = value;
|
|
841
|
+
}
|
|
842
|
+
} catch {
|
|
843
|
+
console.error('Wrong password or corrupted secrets file.');
|
|
844
|
+
process.exit(1);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
820
848
|
// Start bot
|
|
821
|
-
const bot = spawn(['bun', 'run',
|
|
849
|
+
const bot = spawn(['bun', 'run', botScript], {
|
|
822
850
|
stdout: 'inherit',
|
|
823
851
|
stderr: 'inherit',
|
|
824
|
-
|
|
852
|
+
env: childEnv,
|
|
825
853
|
});
|
|
826
854
|
|
|
827
855
|
// Start worker
|
|
828
|
-
const worker = spawn(['bun', 'run',
|
|
856
|
+
const worker = spawn(['bun', 'run', workerScript], {
|
|
829
857
|
stdout: 'inherit',
|
|
830
858
|
stderr: 'inherit',
|
|
831
|
-
|
|
859
|
+
env: childEnv,
|
|
832
860
|
});
|
|
833
861
|
|
|
834
862
|
// Save PIDs
|
package/docs/configuration.md
CHANGED
|
@@ -111,6 +111,8 @@ tether config list
|
|
|
111
111
|
| Key | Default | Description |
|
|
112
112
|
|-----|---------|-------------|
|
|
113
113
|
| `ENABLE_DMS` | `false` | Allow the bot to respond to direct messages |
|
|
114
|
+
| `FORUM_SESSIONS` | `false` | Use forum channel posts instead of text channel threads for sessions |
|
|
115
|
+
| `FORUM_CHANNEL_ID` | (empty) | Discord forum channel ID for session posts (required when `FORUM_SESSIONS=true`) |
|
|
114
116
|
|
|
115
117
|
### Database
|
|
116
118
|
|
package/package.json
CHANGED
package/src/bot.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
TextChannel,
|
|
17
17
|
DMChannel,
|
|
18
18
|
ChannelType,
|
|
19
|
+
ForumChannel,
|
|
19
20
|
Partials,
|
|
20
21
|
ThreadAutoArchiveDuration,
|
|
21
22
|
SlashCommandBuilder,
|
|
@@ -40,6 +41,10 @@ import { questionResponses, pendingTypedAnswers } from './api.js';
|
|
|
40
41
|
// DM support - opt-in via env var (disabled by default for security)
|
|
41
42
|
const ENABLE_DMS = process.env.ENABLE_DMS === 'true';
|
|
42
43
|
|
|
44
|
+
// Forum session support - create sessions as forum posts instead of text threads
|
|
45
|
+
const FORUM_SESSIONS = process.env.FORUM_SESSIONS === 'true';
|
|
46
|
+
const FORUM_CHANNEL_ID = process.env.FORUM_CHANNEL_ID || '';
|
|
47
|
+
|
|
43
48
|
// Allowed working directories (configurable via env, comma-separated)
|
|
44
49
|
// If not set, any existing directory is allowed (backward compatible)
|
|
45
50
|
const ALLOWED_DIRS = process.env.CORD_ALLOWED_DIRS
|
|
@@ -536,30 +541,57 @@ client.on(Events.MessageCreate, async (message: Message) => {
|
|
|
536
541
|
let thread;
|
|
537
542
|
let channelContext = '';
|
|
538
543
|
try {
|
|
539
|
-
|
|
540
|
-
|
|
544
|
+
if (FORUM_SESSIONS && FORUM_CHANNEL_ID) {
|
|
545
|
+
// Forum mode: create a forum post in the configured forum channel
|
|
546
|
+
const forumChannel = await client.channels.fetch(FORUM_CHANNEL_ID);
|
|
547
|
+
if (!forumChannel || forumChannel.type !== ChannelType.GuildForum) {
|
|
548
|
+
log(`FORUM_CHANNEL_ID ${FORUM_CHANNEL_ID} is not a forum channel`);
|
|
549
|
+
await message.reply('Forum channel is misconfigured. Check FORUM_CHANNEL_ID.');
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
541
552
|
|
|
542
|
-
|
|
543
|
-
const threadName = generateThreadName(cleanedMessage);
|
|
553
|
+
const threadName = generateThreadName(cleanedMessage);
|
|
544
554
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
555
|
+
// Forum posts require an initial message (unlike text threads)
|
|
556
|
+
thread = await (forumChannel as ForumChannel).threads.create({
|
|
557
|
+
name: threadName,
|
|
558
|
+
autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
|
|
559
|
+
message: {
|
|
560
|
+
content: `**${message.author.tag}:** ${cleanedMessage}`,
|
|
561
|
+
},
|
|
562
|
+
});
|
|
550
563
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
564
|
+
// Reply in the original channel with a link to the forum post
|
|
565
|
+
await message.reply(`š Session started: <#${thread.id}>`);
|
|
566
|
+
|
|
567
|
+
// Get context from the source channel (not the forum)
|
|
568
|
+
channelContext = await getChannelContext(message.channel as TextChannel);
|
|
569
|
+
if (channelContext) {
|
|
570
|
+
log(`Channel context: ${channelContext.slice(0, 100)}...`);
|
|
571
|
+
}
|
|
572
|
+
} else {
|
|
573
|
+
// Default mode: create a text thread from a status message
|
|
574
|
+
statusMessage = await (message.channel as TextChannel).send('Processing...');
|
|
556
575
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
576
|
+
const threadName = generateThreadName(cleanedMessage);
|
|
577
|
+
|
|
578
|
+
thread = await statusMessage.startThread({
|
|
579
|
+
name: threadName,
|
|
580
|
+
autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// Feature: Get channel context for new conversations
|
|
584
|
+
channelContext = await getChannelContext(message.channel as TextChannel);
|
|
585
|
+
if (channelContext) {
|
|
586
|
+
log(`Channel context: ${channelContext.slice(0, 100)}...`);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// Copy the original message content into the thread for context
|
|
590
|
+
const originalMessages = await message.channel.messages.fetch({ limit: 10 });
|
|
591
|
+
const userMessage = originalMessages.find(m => m.id === message.id);
|
|
592
|
+
if (userMessage) {
|
|
593
|
+
await thread.send(`**${message.author.tag}:** ${cleanedMessage}`);
|
|
594
|
+
}
|
|
563
595
|
}
|
|
564
596
|
} catch (error) {
|
|
565
597
|
log(`Failed to create thread: ${error}`);
|
|
@@ -631,10 +663,23 @@ client.on(Events.MessageReactionAdd, async (reaction, user) => {
|
|
|
631
663
|
|
|
632
664
|
log(`ā
reaction on last message in thread ${thread.id}`);
|
|
633
665
|
|
|
634
|
-
//
|
|
635
|
-
// The thread ID equals the starter message ID (thread was created from that message)
|
|
666
|
+
// Determine parent channel type for the correct "Done" update
|
|
636
667
|
const parentChannel = await client.channels.fetch(parentChannelId);
|
|
637
|
-
|
|
668
|
+
|
|
669
|
+
if (parentChannel?.type === ChannelType.GuildForum) {
|
|
670
|
+
// Forum thread: edit the starter message inside the thread
|
|
671
|
+
try {
|
|
672
|
+
const starterMessage = await thread.fetchStarterMessage();
|
|
673
|
+
if (starterMessage) {
|
|
674
|
+
await starterMessage.edit(`${starterMessage.content}\n\nā
Done`);
|
|
675
|
+
log(`Forum thread ${thread.id} marked as Done`);
|
|
676
|
+
}
|
|
677
|
+
} catch (error) {
|
|
678
|
+
log(`Failed to edit forum starter message: ${error}`);
|
|
679
|
+
}
|
|
680
|
+
} else if (parentChannel?.isTextBased()) {
|
|
681
|
+
// Text thread: edit the status message in the parent channel
|
|
682
|
+
// The thread ID equals the starter message ID (thread was created from that message)
|
|
638
683
|
const starterMessage = await (parentChannel as TextChannel).messages.fetch(thread.id);
|
|
639
684
|
await starterMessage.edit('ā
Done');
|
|
640
685
|
log(`Thread ${thread.id} marked as Done`);
|
package/src/config.ts
CHANGED
|
@@ -81,6 +81,8 @@ const CONFIG_KEYS: Record<string, ConfigKeyMeta> = {
|
|
|
81
81
|
|
|
82
82
|
// Features
|
|
83
83
|
ENABLE_DMS: { section: 'features', default: 'false', description: 'Enable direct message support' },
|
|
84
|
+
FORUM_SESSIONS: { section: 'features', default: 'false', description: 'Use forum channel posts instead of text channel threads' },
|
|
85
|
+
FORUM_CHANNEL_ID: { section: 'features', default: '', description: 'Discord forum channel ID for session posts' },
|
|
84
86
|
|
|
85
87
|
// Database
|
|
86
88
|
DB_PATH: { section: 'database', default: './data/threads.db', description: 'SQLite database path' },
|