@tinyclaw/plugin-channel-discord 1.0.1-dev.3f91a71

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/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # @tinyclaw/plugin-channel-discord
2
+
3
+ Discord channel plugin for Tiny Claw. Connects a Discord bot to the agent, enabling conversations via Direct Messages and @mentions in guild channels.
4
+
5
+ ## Setup
6
+
7
+ 1. Create a Discord bot at <https://discord.com/developers/applications>
8
+ 2. Enable **Message Content Intent** under Privileged Gateway Intents
9
+ 3. Run Tiny Claw and ask it to pair the Discord channel
10
+ 4. Provide the bot token when prompted
11
+ 5. The agent auto-restarts and the bot connects automatically
12
+
13
+ ## How It Works
14
+
15
+ - Listens for DMs and @mentions via discord.js
16
+ - Routes messages through the agent loop as `discord:<user-id>`
17
+ - Splits long responses to respect Discord's 2000-character limit
18
+
19
+ ## Pairing Tools
20
+
21
+ | Tool | Description |
22
+ |------|-------------|
23
+ | `discord_pair` | Store a bot token and enable the plugin |
24
+ | `discord_unpair` | Disable the plugin (token kept in secrets) |
25
+
26
+ ## License
27
+
28
+ GPLv3
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Discord Channel Plugin
3
+ *
4
+ * Connects a Discord bot to the Tiny Claw agent.
5
+ * Responds to Direct Messages and @mentions in guild channels.
6
+ *
7
+ * Setup:
8
+ * 1. Create a Discord bot at https://discord.com/developers/applications
9
+ * 2. Enable: Message Content Intent (under Privileged Gateway Intents)
10
+ * 3. Run Tiny Claw and ask it to pair the Discord channel
11
+ * 4. Provide the bot token when prompted
12
+ * 5. Agent auto-restarts — the bot will connect automatically
13
+ *
14
+ * userId format: "discord:<discord-user-id>"
15
+ * Prefixed to prevent collisions with web UI user IDs.
16
+ */
17
+ import type { ChannelPlugin } from '@tinyclaw/types';
18
+ declare const discordPlugin: ChannelPlugin;
19
+ /** Split a string into chunks without cutting words at boundaries. */
20
+ export declare function splitIntoChunks(text: string, maxLength: number): string[];
21
+ export default discordPlugin;
package/dist/index.js ADDED
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Discord Channel Plugin
3
+ *
4
+ * Connects a Discord bot to the Tiny Claw agent.
5
+ * Responds to Direct Messages and @mentions in guild channels.
6
+ *
7
+ * Setup:
8
+ * 1. Create a Discord bot at https://discord.com/developers/applications
9
+ * 2. Enable: Message Content Intent (under Privileged Gateway Intents)
10
+ * 3. Run Tiny Claw and ask it to pair the Discord channel
11
+ * 4. Provide the bot token when prompted
12
+ * 5. Agent auto-restarts — the bot will connect automatically
13
+ *
14
+ * userId format: "discord:<discord-user-id>"
15
+ * Prefixed to prevent collisions with web UI user IDs.
16
+ */
17
+ import { Client, GatewayIntentBits, Partials, Events, } from 'discord.js';
18
+ import { logger } from '@tinyclaw/logger';
19
+ import { createDiscordPairingTools, DISCORD_TOKEN_SECRET_KEY, DISCORD_ENABLED_CONFIG_KEY, } from './pairing.js';
20
+ let client = null;
21
+ const discordPlugin = {
22
+ id: '@tinyclaw/plugin-channel-discord',
23
+ name: 'Discord',
24
+ description: 'Connect Tiny Claw to a Discord bot',
25
+ type: 'channel',
26
+ version: '0.1.0',
27
+ getPairingTools(secrets, configManager) {
28
+ return createDiscordPairingTools(secrets, configManager);
29
+ },
30
+ async start(context) {
31
+ const isEnabled = context.configManager.get(DISCORD_ENABLED_CONFIG_KEY);
32
+ if (!isEnabled) {
33
+ logger.info('Discord plugin: not enabled — run pairing to enable');
34
+ return;
35
+ }
36
+ const token = await context.secrets.retrieve(DISCORD_TOKEN_SECRET_KEY);
37
+ if (!token) {
38
+ logger.warn('Discord plugin: enabled but no token found — re-pair to fix');
39
+ return;
40
+ }
41
+ client = new Client({
42
+ intents: [
43
+ GatewayIntentBits.Guilds,
44
+ GatewayIntentBits.GuildMessages,
45
+ GatewayIntentBits.MessageContent,
46
+ GatewayIntentBits.DirectMessages,
47
+ ],
48
+ partials: [Partials.Channel],
49
+ });
50
+ client.once(Events.ClientReady, (readyClient) => {
51
+ logger.info(`Discord bot ready: ${readyClient.user.tag}`);
52
+ });
53
+ client.on(Events.MessageCreate, async (msg) => {
54
+ // Ignore messages from bots (including self)
55
+ if (msg.author.bot)
56
+ return;
57
+ const isDM = msg.channel.isDMBased();
58
+ const isMention = client?.user
59
+ ? msg.mentions.users.has(client.user.id)
60
+ : false;
61
+ // Only respond to DMs or @mentions
62
+ if (!isDM && !isMention)
63
+ return;
64
+ // Strip @mention tokens from guild messages
65
+ const rawContent = msg.content
66
+ .replace(/<@!?[\d]+>/g, '')
67
+ .trim();
68
+ if (!rawContent)
69
+ return;
70
+ // Prefix userId to isolate Discord sessions from web UI sessions
71
+ const userId = `discord:${msg.author.id}`;
72
+ try {
73
+ if ('sendTyping' in msg.channel) {
74
+ await msg.channel.sendTyping();
75
+ }
76
+ const response = await context.enqueue(userId, rawContent);
77
+ // Discord has a 2000-character message limit
78
+ if (response.length <= 2000) {
79
+ await msg.reply(response);
80
+ }
81
+ else {
82
+ const chunks = splitIntoChunks(response, 1900);
83
+ for (const chunk of chunks) {
84
+ if ('send' in msg.channel) {
85
+ await msg.channel.send(chunk);
86
+ }
87
+ }
88
+ }
89
+ }
90
+ catch (err) {
91
+ logger.error('Discord plugin: error handling message', err);
92
+ try {
93
+ await msg.reply('Sorry, I ran into an error. Please try again.');
94
+ }
95
+ catch {
96
+ // If replying also fails, just log it
97
+ }
98
+ }
99
+ });
100
+ await client.login(token);
101
+ logger.info('Discord bot connected');
102
+ },
103
+ async stop() {
104
+ if (client) {
105
+ client.destroy();
106
+ client = null;
107
+ logger.info('Discord bot disconnected');
108
+ }
109
+ },
110
+ };
111
+ /** Split a string into chunks without cutting words at boundaries. */
112
+ export function splitIntoChunks(text, maxLength) {
113
+ const chunks = [];
114
+ let remaining = text;
115
+ while (remaining.length > maxLength) {
116
+ let splitAt = remaining.lastIndexOf('\n', maxLength);
117
+ if (splitAt === -1)
118
+ splitAt = remaining.lastIndexOf(' ', maxLength);
119
+ if (splitAt === -1)
120
+ splitAt = maxLength;
121
+ chunks.push(remaining.slice(0, splitAt));
122
+ remaining = remaining.slice(splitAt).trimStart();
123
+ }
124
+ if (remaining.length > 0) {
125
+ chunks.push(remaining);
126
+ }
127
+ return chunks;
128
+ }
129
+ export default discordPlugin;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Discord Pairing Tools
3
+ *
4
+ * Two tools that implement the Discord bot pairing flow:
5
+ *
6
+ * 1. discord_pair — Store the bot token and enable the plugin
7
+ * 2. discord_unpair — Remove from enabled plugins and disable
8
+ *
9
+ * These tools are injected into the agent's tool list at boot so the agent
10
+ * can invoke them conversationally when a user asks to connect Discord.
11
+ */
12
+ import type { Tool, SecretsManagerInterface, ConfigManagerInterface } from '@tinyclaw/types';
13
+ /** Secret key for the Discord bot token. */
14
+ export declare const DISCORD_TOKEN_SECRET_KEY: string;
15
+ /** Config key for the enabled flag. */
16
+ export declare const DISCORD_ENABLED_CONFIG_KEY = "channels.discord.enabled";
17
+ /** The plugin's package ID. */
18
+ export declare const DISCORD_PLUGIN_ID = "@tinyclaw/plugin-channel-discord";
19
+ export declare function createDiscordPairingTools(secrets: SecretsManagerInterface, configManager: ConfigManagerInterface): Tool[];
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Discord Pairing Tools
3
+ *
4
+ * Two tools that implement the Discord bot pairing flow:
5
+ *
6
+ * 1. discord_pair — Store the bot token and enable the plugin
7
+ * 2. discord_unpair — Remove from enabled plugins and disable
8
+ *
9
+ * These tools are injected into the agent's tool list at boot so the agent
10
+ * can invoke them conversationally when a user asks to connect Discord.
11
+ */
12
+ import { buildChannelKeyName } from '@tinyclaw/types';
13
+ /** Secret key for the Discord bot token. */
14
+ export const DISCORD_TOKEN_SECRET_KEY = buildChannelKeyName('discord');
15
+ /** Config key for the enabled flag. */
16
+ export const DISCORD_ENABLED_CONFIG_KEY = 'channels.discord.enabled';
17
+ /** The plugin's package ID. */
18
+ export const DISCORD_PLUGIN_ID = '@tinyclaw/plugin-channel-discord';
19
+ export function createDiscordPairingTools(secrets, configManager) {
20
+ return [
21
+ {
22
+ name: 'discord_pair',
23
+ description: 'Pair Tiny Claw with a Discord bot. ' +
24
+ 'Stores the bot token securely and enables the Discord channel plugin. ' +
25
+ 'After pairing, call tinyclaw_restart to connect the bot. ' +
26
+ 'To get a token: go to https://discord.com/developers/applications, ' +
27
+ 'create an application, add a Bot, copy the token, and enable ' +
28
+ '"Message Content Intent" under Privileged Gateway Intents.',
29
+ parameters: {
30
+ type: 'object',
31
+ properties: {
32
+ token: {
33
+ type: 'string',
34
+ description: 'Discord bot token',
35
+ },
36
+ },
37
+ required: ['token'],
38
+ },
39
+ async execute(args) {
40
+ const token = args.token;
41
+ if (!token || token.trim() === '') {
42
+ return 'Error: token must be a non-empty string.';
43
+ }
44
+ try {
45
+ // 1. Store token in secrets engine
46
+ await secrets.store(DISCORD_TOKEN_SECRET_KEY, token.trim());
47
+ // 2. Enable channel in config
48
+ configManager.set(DISCORD_ENABLED_CONFIG_KEY, true);
49
+ configManager.set('channels.discord.tokenRef', DISCORD_TOKEN_SECRET_KEY);
50
+ // 3. Add plugin to enabled list (deduplicated)
51
+ const current = configManager.get('plugins.enabled') ?? [];
52
+ if (!current.includes(DISCORD_PLUGIN_ID)) {
53
+ configManager.set('plugins.enabled', [...current, DISCORD_PLUGIN_ID]);
54
+ }
55
+ return ('Discord bot paired successfully! ' +
56
+ 'Token stored securely and plugin enabled. ' +
57
+ 'Use the tinyclaw_restart tool now to connect the bot. ' +
58
+ 'Make sure "Message Content Intent" is enabled in the Discord Developer Portal.');
59
+ }
60
+ catch (err) {
61
+ return `Error pairing Discord: ${err.message}`;
62
+ }
63
+ },
64
+ },
65
+ {
66
+ name: 'discord_unpair',
67
+ description: 'Disconnect the Discord bot and disable the Discord channel plugin. ' +
68
+ 'The bot token is kept in secrets for safety. Call tinyclaw_restart after.',
69
+ parameters: {
70
+ type: 'object',
71
+ properties: {},
72
+ required: [],
73
+ },
74
+ async execute() {
75
+ try {
76
+ // Disable in config
77
+ configManager.set(DISCORD_ENABLED_CONFIG_KEY, false);
78
+ // Remove from plugins.enabled
79
+ const current = configManager.get('plugins.enabled') ?? [];
80
+ configManager.set('plugins.enabled', current.filter((id) => id !== DISCORD_PLUGIN_ID));
81
+ return ('Discord plugin disabled. ' +
82
+ 'Use the tinyclaw_restart tool now to apply the changes. ' +
83
+ 'The bot token is still stored in secrets — use list_secrets to manage it.');
84
+ }
85
+ catch (err) {
86
+ return `Error unpairing Discord: ${err.message}`;
87
+ }
88
+ },
89
+ },
90
+ ];
91
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@tinyclaw/plugin-channel-discord",
3
+ "version": "1.0.1-dev.3f91a71",
4
+ "description": "Discord channel plugin for Tiny Claw",
5
+ "license": "GPL-3.0",
6
+ "author": "Waren Gonzaga",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": "./dist/index.js"
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/warengonzaga/tinyclaw.git",
19
+ "directory": "plugins/channel/plugin-channel-discord"
20
+ },
21
+ "homepage": "https://github.com/warengonzaga/tinyclaw/tree/main/plugins/channel/plugin-channel-discord#readme",
22
+ "bugs": {
23
+ "url": "https://github.com/warengonzaga/tinyclaw/issues"
24
+ },
25
+ "keywords": [
26
+ "tinyclaw",
27
+ "plugin",
28
+ "channel",
29
+ "discord"
30
+ ],
31
+ "scripts": {
32
+ "build": "tsc -p tsconfig.json"
33
+ },
34
+ "dependencies": {
35
+ "@tinyclaw/logger": "workspace:*",
36
+ "@tinyclaw/types": "workspace:*",
37
+ "discord.js": "^14.0.0"
38
+ }
39
+ }