Repository: obot-platform/tools
Pull request #529: feat: discord tools
Author: login: g-linville
Created: 2025-03-25T20:10:09Z | Merged: 2025-03-27T00:39:03Z

This adds a set of Discord tools that can be used with a Discord bot. Let me know if you want help setting this up and testing.

There might be a way to do things with OAuth, but I haven't looked into it. For our first use case, just having a bot token is all we really need.


Diff:
diff --git a/.cursor/rules/gpt.mdc b/.cursor/rules/gpt.mdc
new file mode 100644
index 000000000..8a421d54d
--- /dev/null
+++ b/.cursor/rules/gpt.mdc
@@ -0,0 +1,101 @@
+---
+description: GPTScript Tool Files
+globs: *.gpt
+alwaysApply: true
+---
+# GPTScript Tool File Format Rules
+
+## File Extension
+
+- Files with extension `.gpt` are GPTScript tool files
+- These files define tools that can be provided to a large language model for execution
+
+## File Structure
+
+Each tool definition in the file follows this structure:
+
+1. Preamble:
+   - Contains tool directives. Here are some common ones:
+     - `Name`: The name of the tool
+     - `Description`: A description of what the tool does
+     - `Param`: Parameters the tool accepts (optional)
+     - `Share Tools`: Other tools this tool can use (optional)
+     - `Share Context`: Context this tool shares with other tools (optional)
+     - `Tools`: External tools this tool can use (optional)
+     - `Credential`: Path to credential file (optional)
+     - `Metadata`: Additional metadata (optional)
+
+2. Command/Prompt:
+   - Separated from preamble by a blank line
+   - Contains the actual command or prompt to execute
+   - Can be a shell command or a prompt for the language model
+
+3. Tool Separation:
+   - Each tool definition is separated by `---`
+
+## Special Sections
+
+- Context tool definitions start with `Type: context`
+- Metadata sections can be defined using `!metadata:*:key` syntax
+  - You don't need to worry about these, and can just ignore them
+
+## Example Structure
+
+```
+Name: Tool Name
+Description: Tool description
+Param: param1: description
+Share Tools: Tool1, Tool2
+Credential: ./credential
+
+#!/usr/bin/env command
+
+---
+Name: Another Tool
+Description: Another tool description
+Param: param1: description
+
+# Prompt or command here
+```
+
+## Context Tools
+
+A context tool is a way to provide additional prompting to the LLM. Any tool with `Type: context` and any tool that is included in another tool like
+`Context: tool name` is a context tool, and will be called automatically for the LLM to provide helpful information.
+
+## Tool Directives
+
+Here is the full list of tool directives:
+
+| Key                   | Description                                                                                                                                   |
+|-----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
+| `Name`                | The name of the tool.                                                                                                                         |
+| `Description`         | The description of the tool. It is important that this properly describes the tool's purpose as the description is used by the LLM.           |
+| `Tools`               | A comma-separated list of tools that are available to be called by this tool.                                                                 |
+| `Parameter` / `Param` | Parameters for the tool. Each parameter is defined in the format `param-name: description`.                                                   |
+| `JSON Response`       | Setting to `true` will cause the LLM to respond in a JSON format. If you set true you must also include instructions in the tool.             |
+| `Credential`          | Credential tool to call to set credentials as environment variables before doing anything else. One per line.                                 | 
+| `Share Tools`         | A comma-separated list of tools that are shared by the tool.                                                                                  |
+| `Context`             | A comma-separated list of context tools available to the tool.                                                                                |
+| `Share Context`       | A comma-separated list of context tools shared by this tool with any tool including this tool in its context.                                 | 
+
+## Helpful Information
+
+- When a tool is executed, all parameter values become environment variables in all caps. For example, if the value of a param called `color` is set to `blue`, then in the environment, the tool will have `COLOR=blue`.
+
+## Tool Bundles
+
+- A tool bundle is a tool with `Metadata: bundle: true`
+- Tool bundles MUST include all tools defined in their file in their `Share Tools` list
+- This ensures that when other tools include the bundle, they get access to all tools in the bundle
+
+## Best Practices
+
+1. Each tool should have a clear, descriptive name
+2. Descriptions should be detailed and explain the tool's purpose
+3. Parameters should be clearly documented with descriptions
+4. Use appropriate sharing of tools and context to enable tool composition
+
+## Canonical Examples
+
+See [tool.gpt](mdc:slack/tool.gpt), [tool.gpt](mdc:github/tool.gpt), and [tool.gpt](mdc:google/gmail/tool.gpt) for good examples of tool files.
diff --git a/discord/.gitignore b/discord/.gitignore
new file mode 100644
index 000000000..a8b148573
--- /dev/null
+++ b/discord/.gitignore
@@ -0,0 +1,3 @@
+dist/
+node_modules/
+.env
diff --git a/discord/credential/tool.gpt b/discord/credential/tool.gpt
new file mode 100644
index 000000000..5209123cf
--- /dev/null
+++ b/discord/credential/tool.gpt
@@ -0,0 +1,24 @@
+Name: Discord Bot Credential
+Share Credential: discord-cred as discord
+Type: credential
+
+---
+Name: discord-cred
+Tools: ../../generic-credential
+
+#!sys.call ../../generic-credential
+
+{
+	"promptInfo": {
+		"fields" : [
+		    {
+				"name": "Discord Bot Token",
+				"description": "Your Discord bot token",
+				"env": "DISCORD_TOKEN",
+				"sensitive": true
+			}
+		],
+		"message": "Enter your Discord bot token."
+	},
+	"validationTool": "github.com/obot-platform/tools/discord/validate-cred.gpt"
+}
\ No newline at end of file
diff --git a/discord/package-lock.json b/discord/package-lock.json
new file mode 100644
index 000000000..2c05e5df5
--- /dev/null
+++ b/discord/package-lock.json
@@ -0,0 +1,893 @@
+{
+  "name": "discord",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "discord",
+      "version": "1.0.0",
+      "dependencies": {
+        "@gptscript-ai/gptscript": "github:gptscript-ai/node-gptscript#d4b222c34e757b62429dc43ae9cc44aa3f6c9700",
+        "@types/node": "^20.11.24",
+        "discord.js": "^14.14.1",
+        "ts-node": "^10.9.2",
+        "typescript": "^5.3.3"
+      }
+    },
+    "node_modules/@cspotcode/source-map-support": {
+      "version": "0.8.1",
+      "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+      "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/trace-mapping": "0.3.9"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@discordjs/builders": {
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.10.1.tgz",
+      "integrity": "sha512-OWo1fY4ztL1/M/DUyRPShB4d/EzVfuUvPTRRHRIt/YxBrUYSz0a+JicD5F5zHFoNs2oTuWavxCOVFV1UljHTng==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@discordjs/formatters": "^0.6.0",
+        "@discordjs/util": "^1.1.1",
+        "@sapphire/shapeshift": "^4.0.0",
+        "discord-api-types": "^0.37.119",
+        "fast-deep-equal": "^3.1.3",
+        "ts-mixer": "^6.0.4",
+        "tslib": "^2.6.3"
+      },
+      "engines": {
+        "node": ">=16.11.0"
+      },
+      "funding": {
+        "url": "https://github.com/discordjs/discord.js?sponsor"
+      }
+    },
+    "node_modules/@discordjs/collection": {
+      "version": "1.5.3",
+      "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz",
+      "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=16.11.0"
+      }
+    },
+    "node_modules/@discordjs/formatters": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.0.tgz",
+      "integrity": "sha512-YIruKw4UILt/ivO4uISmrGq2GdMY6EkoTtD0oS0GvkJFRZbTSdPhzYiUILbJ/QslsvC9H9nTgGgnarnIl4jMfw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "discord-api-types": "^0.37.114"
+      },
+      "engines": {
+        "node": ">=16.11.0"
+      },
+      "funding": {
+        "url": "https://github.com/discordjs/discord.js?sponsor"
+      }
+    },
+    "node_modules/@discordjs/rest": {
+      "version": "2.4.3",
+      "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.3.tgz",
+      "integrity": "sha512-+SO4RKvWsM+y8uFHgYQrcTl/3+cY02uQOH7/7bKbVZsTfrfpoE62o5p+mmV+s7FVhTX82/kQUGGbu4YlV60RtA==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@discordjs/collection": "^2.1.1",
+        "@discordjs/util": "^1.1.1",
+        "@sapphire/async-queue": "^1.5.3",
+        "@sapphire/snowflake": "^3.5.3",
+        "@vladfrangu/async_event_emitter": "^2.4.6",
+        "discord-api-types": "^0.37.119",
+        "magic-bytes.js": "^1.10.0",
+        "tslib": "^2.6.3",
+        "undici": "6.21.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/discordjs/discord.js?sponsor"
+      }
+    },
+    "node_modules/@discordjs/rest/node_modules/@discordjs/collection": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
+      "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/discordjs/discord.js?sponsor"
+      }
+    },
+    "node_modules/@discordjs/util": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz",
+      "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/discordjs/discord.js?sponsor"
+      }
+    },
+    "node_modules/@discordjs/ws": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.1.tgz",
+      "integrity": "sha512-PBvenhZG56a6tMWF/f4P6f4GxZKJTBG95n7aiGSPTnodmz4N5g60t79rSIAq7ywMbv8A4jFtexMruH+oe51aQQ==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@discordjs/collection": "^2.1.0",
+        "@discordjs/rest": "^2.4.3",
+        "@discordjs/util": "^1.1.0",
+        "@sapphire/async-queue": "^1.5.2",
+        "@types/ws": "^8.5.10",
+        "@vladfrangu/async_event_emitter": "^2.2.4",
+        "discord-api-types": "^0.37.119",
+        "tslib": "^2.6.2",
+        "ws": "^8.17.0"
+      },
+      "engines": {
+        "node": ">=16.11.0"
+      },
+      "funding": {
+        "url": "https://github.com/discordjs/discord.js?sponsor"
+      }
+    },
+    "node_modules/@discordjs/ws/node_modules/@discordjs/collection": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
+      "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/discordjs/discord.js?sponsor"
+      }
+    },
+    "node_modules/@gptscript-ai/gptscript": {
+      "version": "v0.9.5",
+      "resolved": "git+ssh://git@github.com/gptscript-ai/node-gptscript.git#d4b222c34e757b62429dc43ae9cc44aa3f6c9700",
+      "integrity": "sha512-Gp+WR5qZOLtW2d1tWqUP4/Wx/JYTIa5XuhR9NyV5GEYNS2ygvY64h+z6PIbfEBKdkmBcFsTQifiifioU3rbB3w==",
+      "hasInstallScript": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@types/sync-fetch": "^0.4.3",
+        "adm-zip": "^0.5.10",
+        "node-downloader-helper": "^2.1.9",
+        "tar": "^6.2.0"
+      },
+      "bin": {
+        "gptscript": "bin/gptscript"
+      }
+    },
+    "node_modules/@jridgewell/resolve-uri": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+      "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.0.0"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+      "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+      "license": "MIT"
+    },
+    "node_modules/@jridgewell/trace-mapping": {
+      "version": "0.3.9",
+      "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+      "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/resolve-uri": "^3.0.3",
+        "@jridgewell/sourcemap-codec": "^1.4.10"
+      }
+    },
+    "node_modules/@sapphire/async-queue": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
+      "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=v14.0.0",
+        "npm": ">=7.0.0"
+      }
+    },
+    "node_modules/@sapphire/shapeshift": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz",
+      "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==",
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3",
+        "lodash": "^4.17.21"
+      },
+      "engines": {
+        "node": ">=v16"
+      }
+    },
+    "node_modules/@sapphire/snowflake": {
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz",
+      "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=v14.0.0",
+        "npm": ">=7.0.0"
+      }
+    },
+    "node_modules/@tsconfig/node10": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
+      "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
+      "license": "MIT"
+    },
+    "node_modules/@tsconfig/node12": {
+      "version": "1.0.11",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
+      "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
+      "license": "MIT"
+    },
+    "node_modules/@tsconfig/node14": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
+      "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
+      "license": "MIT"
+    },
+    "node_modules/@tsconfig/node16": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
+      "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
+      "license": "MIT"
+    },
+    "node_modules/@types/node": {
+      "version": "20.17.27",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz",
+      "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==",
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~6.19.2"
+      }
+    },
+    "node_modules/@types/node-fetch": {
+      "version": "2.6.12",
+      "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
+      "integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*",
+        "form-data": "^4.0.0"
+      }
+    },
+    "node_modules/@types/sync-fetch": {
+      "version": "0.4.3",
+      "resolved": "https://registry.npmjs.org/@types/sync-fetch/-/sync-fetch-0.4.3.tgz",
+      "integrity": "sha512-RfwkmWFd7yi6tRaDcR7KNrxyp6LpO2oXrT6tYkXBLNfp4xf4EKRX7IlLexbtd5X26g34+G6HDD2pMhOZ5srUmQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/node-fetch": "*"
+      }
+    },
+    "node_modules/@types/ws": {
+      "version": "8.18.0",
+      "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.0.tgz",
+      "integrity": "sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@vladfrangu/async_event_emitter": {
+      "version": "2.4.6",
+      "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz",
+      "integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=v14.0.0",
+        "npm": ">=7.0.0"
+      }
+    },
+    "node_modules/acorn": {
+      "version": "8.14.1",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
+      "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
+      "license": "MIT",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/acorn-walk": {
+      "version": "8.3.4",
+      "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
+      "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
+      "license": "MIT",
+      "dependencies": {
+        "acorn": "^8.11.0"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/adm-zip": {
+      "version": "0.5.16",
+      "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
+      "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0"
+      }
+    },
+    "node_modules/arg": {
+      "version": "4.1.3",
+      "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
+      "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
+      "license": "MIT"
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/chownr": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+      "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/create-require": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+      "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+      "license": "MIT"
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/diff": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
+      "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.3.1"
+      }
+    },
+    "node_modules/discord-api-types": {
+      "version": "0.37.119",
+      "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.119.tgz",
+      "integrity": "sha512-WasbGFXEB+VQWXlo6IpW3oUv73Yuau1Ig4AZF/m13tXcTKnMpc/mHjpztIlz4+BM9FG9BHQkEXiPto3bKduQUg==",
+      "license": "MIT"
+    },
+    "node_modules/discord.js": {
+      "version": "14.18.0",
+      "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.18.0.tgz",
+      "integrity": "sha512-SvU5kVUvwunQhN2/+0t55QW/1EHfB1lp0TtLZUSXVHDmyHTrdOj5LRKdR0zLcybaA15F+NtdWuWmGOX9lE+CAw==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@discordjs/builders": "^1.10.1",
+        "@discordjs/collection": "1.5.3",
+        "@discordjs/formatters": "^0.6.0",
+        "@discordjs/rest": "^2.4.3",
+        "@discordjs/util": "^1.1.1",
+        "@discordjs/ws": "^1.2.1",
+        "@sapphire/snowflake": "3.5.3",
+        "discord-api-types": "^0.37.119",
+        "fast-deep-equal": "3.1.3",
+        "lodash.snakecase": "4.1.1",
+        "tslib": "^2.6.3",
+        "undici": "6.21.1"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/discordjs/discord.js?sponsor"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "license": "MIT"
+    },
+    "node_modules/form-data": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
+      "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fs-minipass": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+      "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+      "license": "ISC",
+      "dependencies": {
+        "minipass": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/fs-minipass/node_modules/minipass": {
+      "version": "3.3.6",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+      "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "license": "MIT"
+    },
+    "node_modules/lodash.snakecase": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
+      "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
+      "license": "MIT"
+    },
+    "node_modules/magic-bytes.js": {
+      "version": "1.10.0",
+      "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz",
+      "integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==",
+      "license": "MIT"
+    },
+    "node_modules/make-error": {
+      "version": "1.3.6",
+      "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
+      "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
+      "license": "ISC"
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/minipass": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+      "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/minizlib": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+      "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+      "license": "MIT",
+      "dependencies": {
+        "minipass": "^3.0.0",
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/minizlib/node_modules/minipass": {
+      "version": "3.3.6",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+      "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+      "license": "ISC",
+      "dependencies": {
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/mkdirp": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+      "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+      "license": "MIT",
+      "bin": {
+        "mkdirp": "bin/cmd.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/node-downloader-helper": {
+      "version": "2.1.9",
+      "resolved": "https://registry.npmjs.org/node-downloader-helper/-/node-downloader-helper-2.1.9.tgz",
+      "integrity": "sha512-FSvAol2Z8UP191sZtsUZwHIN0eGoGue3uEXGdWIH5228e9KH1YHXT7fN8Oa33UGf+FbqGTQg3sJfrRGzmVCaJA==",
+      "license": "MIT",
+      "bin": {
+        "ndh": "bin/ndh"
+      },
+      "engines": {
+        "node": ">=14.18"
+      }
+    },
+    "node_modules/tar": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+      "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
+      "license": "ISC",
+      "dependencies": {
+        "chownr": "^2.0.0",
+        "fs-minipass": "^2.0.0",
+        "minipass": "^5.0.0",
+        "minizlib": "^2.1.1",
+        "mkdirp": "^1.0.3",
+        "yallist": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/ts-mixer": {
+      "version": "6.0.4",
+      "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
+      "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==",
+      "license": "MIT"
+    },
+    "node_modules/ts-node": {
+      "version": "10.9.2",
+      "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
+      "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@cspotcode/source-map-support": "^0.8.0",
+        "@tsconfig/node10": "^1.0.7",
+        "@tsconfig/node12": "^1.0.7",
+        "@tsconfig/node14": "^1.0.0",
+        "@tsconfig/node16": "^1.0.2",
+        "acorn": "^8.4.1",
+        "acorn-walk": "^8.1.1",
+        "arg": "^4.1.0",
+        "create-require": "^1.1.0",
+        "diff": "^4.0.1",
+        "make-error": "^1.1.1",
+        "v8-compile-cache-lib": "^3.0.1",
+        "yn": "3.1.1"
+      },
+      "bin": {
+        "ts-node": "dist/bin.js",
+        "ts-node-cwd": "dist/bin-cwd.js",
+        "ts-node-esm": "dist/bin-esm.js",
+        "ts-node-script": "dist/bin-script.js",
+        "ts-node-transpile-only": "dist/bin-transpile.js",
+        "ts-script": "dist/bin-script-deprecated.js"
+      },
+      "peerDependencies": {
+        "@swc/core": ">=1.2.50",
+        "@swc/wasm": ">=1.2.50",
+        "@types/node": "*",
+        "typescript": ">=2.7"
+      },
+      "peerDependenciesMeta": {
+        "@swc/core": {
+          "optional": true
+        },
+        "@swc/wasm": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/tslib": {
+      "version": "2.8.1",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+      "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+      "license": "0BSD"
+    },
+    "node_modules/typescript": {
+      "version": "5.8.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
+      "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/undici": {
+      "version": "6.21.1",
+      "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz",
+      "integrity": "sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.17"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "6.19.8",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
+      "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
+      "license": "MIT"
+    },
+    "node_modules/v8-compile-cache-lib": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
+      "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
+      "license": "MIT"
+    },
+    "node_modules/ws": {
+      "version": "8.18.1",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
+      "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/yallist": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+      "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
+      "license": "ISC"
+    },
+    "node_modules/yn": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
+      "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    }
+  }
+}
diff --git a/discord/package.json b/discord/package.json
new file mode 100644
index 000000000..79b6033dc
--- /dev/null
+++ b/discord/package.json
@@ -0,0 +1,21 @@
+{
+  "name": "discord",
+  "version": "1.0.0",
+  "main": "dist/index.js",
+  "type": "module",
+  "scripts": {
+    "build": "tsc",
+    "tool": "node --loader ts-node/esm src/index.ts",
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "keywords": [],
+  "author": "",
+  "description": "GPTScript tools for interacting with Discord",
+  "dependencies": {
+    "@gptscript-ai/gptscript": "github:gptscript-ai/node-gptscript#d4b222c34e757b62429dc43ae9cc44aa3f6c9700",
+    "@types/node": "^20.11.24",
+    "discord.js": "^14.14.1",
+    "ts-node": "^10.9.2",
+    "typescript": "^5.3.3"
+  }
+}
diff --git a/discord/src/client.ts b/discord/src/client.ts
new file mode 100644
index 000000000..8c56462c6
--- /dev/null
+++ b/discord/src/client.ts
@@ -0,0 +1,9 @@
+import { Client, GatewayIntentBits } from 'discord.js';
+
+export const client = new Client({
+  intents: [
+    GatewayIntentBits.Guilds,
+    GatewayIntentBits.GuildMessages,
+    GatewayIntentBits.GuildMembers,
+  ],
+}); 
\ No newline at end of file
diff --git a/discord/src/index.ts b/discord/src/index.ts
new file mode 100644
index 000000000..7b94af8ff
--- /dev/null
+++ b/discord/src/index.ts
@@ -0,0 +1,69 @@
+import { client } from './client.js';
+import { listChannels } from './tools/listChannels.js';
+import { searchChannels } from './tools/searchChannels.js';
+import { getChannelHistory } from './tools/getChannelHistory.js';
+import { getChannelHistoryByTime } from './tools/getChannelHistoryByTime.js';
+import { getThreadHistory } from './tools/getThreadHistory.js';
+import { listUsers } from './tools/listUsers.js';
+import { searchUsers } from './tools/searchUsers.js';
+import { sendMessage } from './tools/sendMessage.js';
+import { sendMessageInThread } from './tools/sendMessageInThread.js';
+
+async function login(): Promise<void> {
+  try {
+    await client.login(process.env.DISCORD_TOKEN);
+  } catch (error: any) {
+    console.log('Discord login failed:', error);
+    process.exit(1);
+  }
+}
+
+async function main() {
+  try {
+    await login();
+
+    const command = process.argv[2];
+
+    switch (command) {
+      case 'listChannels':
+        console.log(await listChannels());
+        break;
+      case 'searchChannels':
+        console.log(await searchChannels());
+        break;
+      case 'getChannelHistory':
+        console.log(await getChannelHistory());
+        break;
+      case 'getChannelHistoryByTime':
+        console.log(await getChannelHistoryByTime());
+        break;
+      case 'getThreadHistory':
+        console.log(await getThreadHistory());
+        break;
+      case 'listUsers':
+        console.log(await listUsers());
+        break;
+      case 'searchUsers':
+        console.log(await searchUsers());
+        break;
+      case 'sendMessage':
+        console.log(await sendMessage());
+        break;
+      case 'sendMessageInThread':
+        console.log(await sendMessageInThread());
+        break;
+      case 'login':
+        console.log('Successfully logged in to Discord'); // login happens earlier in this function
+        break;
+      default:
+        throw new Error(`Unknown command: ${command}`);
+    }
+
+    process.exit(0);
+  } catch (error) {
+    console.error('Error:', error);
+    process.exit(1);
+  }
+}
+
+main(); 
\ No newline at end of file
diff --git a/discord/src/tools/getChannelHistory.ts b/discord/src/tools/getChannelHistory.ts
new file mode 100644
index 000000000..dd08fb51e
--- /dev/null
+++ b/discord/src/tools/getChannelHistory.ts
@@ -0,0 +1,85 @@
+import { TextChannel, Attachment, Embed } from 'discord.js';
+import { client } from '../client.js';
+import { formatTime, createDataset } from '../utils.js';
+
+export async function getChannelHistory() {
+  if (!process.env.CHANNELID || !process.env.GUILDID || !process.env.LIMIT) {
+    throw new Error('CHANNELID, GUILDID, and LIMIT environment variables are required for getChannelHistory');
+  }
+
+  const channelId = process.env.CHANNELID;
+  const guildId = process.env.GUILDID;
+  const limit = parseInt(process.env.LIMIT, 10);
+
+  const guild = await client.guilds.fetch(guildId);
+  const channel = await guild.channels.fetch(channelId);
+
+  if (!(channel instanceof TextChannel)) {
+    throw new Error('Channel is not a text channel');
+  }
+
+  const messages = await channel.messages.fetch({ limit });
+  const history = await Promise.all(Array.from(messages.values())
+    .sort((a, b) => a.createdTimestamp - b.createdTimestamp)
+    .map(async msg => {
+      const result: any = {
+        id: msg.id,
+        content: msg.content,
+        type: msg.type,
+        system: msg.system,
+        permalink: `https://discord.com/channels/${guildId}/${channelId}/${msg.id}`,
+        author: {
+          id: msg.author.id,
+          username: msg.author.username,
+          discriminator: msg.author.discriminator,
+        },
+        timestamp: formatTime(msg.createdTimestamp),
+        attachments: msg.attachments.map((att: Attachment) => ({
+          url: att.url,
+          name: att.name,
+        })),
+        embeds: msg.embeds.map((embed: Embed) => ({
+          title: embed.title,
+          description: embed.description,
+          url: embed.url,
+          color: embed.color,
+          fields: embed.fields,
+          timestamp: embed.timestamp ? formatTime(new Date(embed.timestamp).getTime()) : null,
+        })),
+      };
+
+      // Add system message details if applicable
+      if (msg.system) {
+        if (msg.type === 7) { // Member join message
+          result.systemData = {
+            type: 'memberJoin',
+            member: msg.member ? {
+              id: msg.member.id,
+              username: msg.member.user.username,
+              nickname: msg.member.nickname,
+              joinedAt: formatTime(msg.member.joinedTimestamp)
+            } : null
+          };
+        }
+      }
+
+      // Check if message is part of a thread
+      if (msg.thread) {
+        const thread = await channel.threads.fetch(msg.thread.id);
+        if (thread) {
+          result.thread = {
+            id: thread.id,
+            name: thread.name,
+            messageCount: thread.messageCount,
+            memberCount: thread.memberCount,
+            isLocked: thread.locked,
+            isArchived: thread.archived,
+          };
+        }
+      }
+
+      return result;
+    }));
+
+  await createDataset(history, 'discord_channel_history');
+} 
\ No newline at end of file
diff --git a/discord/src/tools/getChannelHistoryByTime.ts b/discord/src/tools/getChannelHistoryByTime.ts
new file mode 100644
index 000000000..d7361d86b
--- /dev/null
+++ b/discord/src/tools/getChannelHistoryByTime.ts
@@ -0,0 +1,114 @@
+import { TextChannel, Message, Attachment, Embed, FetchMessagesOptions } from 'discord.js';
+import { client } from '../client.js';
+import { formatTime, createDataset } from '../utils.js';
+
+export async function getChannelHistoryByTime() {
+  if (!process.env.CHANNELID || !process.env.GUILDID || !process.env.LIMIT || !process.env.START || !process.env.END) {
+    throw new Error('CHANNELID, GUILDID, LIMIT, START, and END environment variables are required for getChannelHistoryByTime');
+  }
+
+  const channelId = process.env.CHANNELID;
+  const guildId = process.env.GUILDID;
+  const limit = parseInt(process.env.LIMIT, 10);
+  const startTime = new Date(process.env.START).getTime();
+  const endTime = new Date(process.env.END).getTime();
+
+  const guild = await client.guilds.fetch(guildId);
+  const channel = await guild.channels.fetch(channelId);
+
+  if (!(channel instanceof TextChannel)) {
+    throw new Error('Channel is not a text channel');
+  }
+
+  // Fetch messages until we have enough or reach the start time
+  const messages: Message[] = [];
+  let lastId: string | undefined;
+
+  while (messages.length < limit) {
+    const options: FetchMessagesOptions = { limit: 100 }; // Fetch 100 at a time for efficiency
+    if (lastId) {
+      options.before = lastId;
+    }
+
+    const batch = await channel.messages.fetch(options);
+    if (batch.size === 0) break;
+
+    for (const msg of batch.values()) {
+      if (msg.createdTimestamp < startTime) {
+        break;
+      }
+      if (msg.createdTimestamp <= endTime) {
+        messages.push(msg);
+        if (messages.length >= limit) break;
+      }
+      lastId = msg.id;
+    }
+
+    if (batch.size < 100 || messages.length >= limit) break;
+  }
+
+  // Sort messages in chronological order
+  messages.sort((a, b) => a.createdTimestamp - b.createdTimestamp);
+
+  const history = await Promise.all(messages.map(async msg => {
+    const result: any = {
+      id: msg.id,
+      content: msg.content,
+      type: msg.type,
+      system: msg.system,
+      permalink: `https://discord.com/channels/${guildId}/${channelId}/${msg.id}`,
+      author: {
+        id: msg.author.id,
+        username: msg.author.username,
+        discriminator: msg.author.discriminator,
+      },
+      timestamp: formatTime(msg.createdTimestamp),
+      attachments: msg.attachments.map((att: Attachment) => ({
+        url: att.url,
+        name: att.name,
+      })),
+      embeds: msg.embeds.map((embed: Embed) => ({
+        title: embed.title,
+        description: embed.description,
+        url: embed.url,
+        color: embed.color,
+        fields: embed.fields,
+        timestamp: embed.timestamp ? formatTime(new Date(embed.timestamp).getTime()) : null,
+      })),
+    };
+
+    // Add system message details if applicable
+    if (msg.system) {
+      if (msg.type === 7) { // Member join message
+        result.systemData = {
+          type: 'memberJoin',
+          member: msg.member ? {
+            id: msg.member.id,
+            username: msg.member.user.username,
+            nickname: msg.member.nickname,
+            joinedAt: formatTime(msg.member.joinedTimestamp)
+          } : null
+        };
+      }
+    }
+
+    // Check if message is part of a thread
+    if (msg.thread) {
+      const thread = await channel.threads.fetch(msg.thread.id);
+      if (thread) {
+        result.thread = {
+          id: thread.id,
+          name: thread.name,
+          messageCount: thread.messageCount,
+          memberCount: thread.memberCount,
+          isLocked: thread.locked,
+          isArchived: thread.archived,
+        };
+      }
+    }
+
+    return result;
+  }));
+
+  await createDataset(history, 'discord_channel_history_by_time');
+} 
\ No newline at end of file
diff --git a/discord/src/tools/getThreadHistory.ts b/discord/src/tools/getThreadHistory.ts
new file mode 100644
index 000000000..d1c88f05a
--- /dev/null
+++ b/discord/src/tools/getThreadHistory.ts
@@ -0,0 +1,68 @@
+import { TextChannel, Attachment, Embed } from 'discord.js';
+import { client } from '../client.js';
+import { formatTime, createDataset } from '../utils.js';
+
+export async function getThreadHistory() {
+  if (!process.env.CHANNELID || !process.env.GUILDID || !process.env.THREADID || !process.env.LIMIT) {
+    throw new Error('CHANNELID, GUILDID, THREADID, and LIMIT environment variables are required for getThreadHistory');
+  }
+
+  const channelId = process.env.CHANNELID;
+  const guildId = process.env.GUILDID;
+  const threadId = process.env.THREADID;
+  const limit = parseInt(process.env.LIMIT, 10);
+
+  const guild = await client.guilds.fetch(guildId);
+  const channel = await guild.channels.fetch(channelId);
+
+  if (!(channel instanceof TextChannel)) {
+    throw new Error('Channel is not a text channel');
+  }
+
+  // Get the thread
+  const thread = await channel.threads.fetch(threadId);
+  if (!thread) {
+    throw new Error('Thread not found');
+  }
+
+  // Fetch messages from the thread
+  const messages = await thread.messages.fetch({ limit });
+  const history = Array.from(messages.values())
+    .sort((a, b) => a.createdTimestamp - b.createdTimestamp)
+    .map(msg => ({
+      id: msg.id,
+      content: msg.content,
+      type: msg.type,
+      system: msg.system,
+      permalink: `https://discord.com/channels/${guildId}/${channelId}/${threadId}/${msg.id}`,
+      author: {
+        id: msg.author.id,
+        username: msg.author.username,
+        discriminator: msg.author.discriminator,
+      },
+      timestamp: formatTime(msg.createdTimestamp),
+      attachments: msg.attachments.map((att: Attachment) => ({
+        url: att.url,
+        name: att.name,
+      })),
+      embeds: msg.embeds.map((embed: Embed) => ({
+        title: embed.title,
+        description: embed.description,
+        url: embed.url,
+        color: embed.color,
+        fields: embed.fields,
+        timestamp: embed.timestamp ? formatTime(new Date(embed.timestamp).getTime()) : null,
+      })),
+      systemData: msg.system && msg.type === 7 ? {
+        type: 'memberJoin',
+        member: msg.member ? {
+          id: msg.member.id,
+          username: msg.member.user.username,
+          nickname: msg.member.nickname,
+          joinedAt: formatTime(msg.member.joinedTimestamp)
+        } : null
+      } : undefined
+    }));
+
+  await createDataset(history, 'discord_thread_history');
+} 
\ No newline at end of file
diff --git a/discord/src/tools/listChannels.ts b/discord/src/tools/listChannels.ts
new file mode 100644
index 000000000..13b4c86eb
--- /dev/null
+++ b/discord/src/tools/listChannels.ts
@@ -0,0 +1,24 @@
+import { TextChannel } from 'discord.js';
+import { client } from '../client.js';
+import { createDataset } from '../utils.js';
+
+export async function listChannels() {
+  const guilds = client.guilds.cache;
+  const channels = [];
+
+  for (const guild of guilds.values()) {
+    const guildChannels = await guild.channels.fetch();
+    for (const channel of guildChannels.values()) {
+      if (channel instanceof TextChannel) {
+        channels.push({
+          id: channel.id,
+          name: channel.name,
+          guildId: guild.id,
+          guildName: guild.name
+        });
+      }
+    }
+  }
+
+  await createDataset(channels, 'discord_channels');
+} 
\ No newline at end of file
diff --git a/discord/src/tools/listUsers.ts b/discord/src/tools/listUsers.ts
new file mode 100644
index 000000000..fc3b882a6
--- /dev/null
+++ b/discord/src/tools/listUsers.ts
@@ -0,0 +1,38 @@
+import { client } from '../client.js';
+import { formatTime, createDataset } from '../utils.js';
+
+export async function listUsers() {
+  if (!process.env.GUILDID) {
+    throw new Error('GUILDID environment variable is required for listUsers');
+  }
+
+  const guildId = process.env.GUILDID;
+  const guild = await client.guilds.fetch(guildId);
+  
+  try {
+    // Fetch all members
+    const members = await guild.members.fetch();
+    
+    const users = members.map(member => ({
+      id: member.user.id,
+      name: member.user.username,
+      username: member.user.username,
+      discriminator: member.user.discriminator,
+      nickname: member.nickname,
+      roles: member.roles.cache.map(role => ({
+        id: role.id,
+        name: role.name,
+        color: role.color,
+        position: role.position
+      })),
+      joinedAt: formatTime(member.joinedTimestamp),
+      isBot: member.user.bot,
+      avatarUrl: member.user.displayAvatarURL()
+    }));
+
+    await createDataset(users, 'discord_users');
+  } catch (error) {
+    console.error('Error fetching members:', error);
+    throw new Error('Failed to fetch members. Make sure the bot has the GuildMembers intent and proper permissions.');
+  }
+} 
\ No newline at end of file
diff --git a/discord/src/tools/searchChannels.ts b/discord/src/tools/searchChannels.ts
new file mode 100644
index 000000000..e5c58adb1
--- /dev/null
+++ b/discord/src/tools/searchChannels.ts
@@ -0,0 +1,29 @@
+import { TextChannel } from 'discord.js';
+import { client } from '../client.js';
+import { createDataset } from '../utils.js';
+
+export async function searchChannels() {
+  if (!process.env.QUERY) {
+    throw new Error('QUERY environment variable is required for searchChannels');
+  }
+
+  const query = process.env.QUERY;
+  const results = [];
+
+  for (const guild of client.guilds.cache.values()) {
+    const guildChannels = await guild.channels.fetch();
+    for (const channel of guildChannels.values()) {
+      if (channel instanceof TextChannel && 
+          channel.name.toLowerCase().includes(query.toLowerCase())) {
+        results.push({
+          id: channel.id,
+          name: channel.name,
+          guildId: guild.id,
+          guildName: guild.name
+        });
+      }
+    }
+  }
+
+  await createDataset(results, 'discord_channel_search');
+}
diff --git a/discord/src/tools/searchUsers.ts b/discord/src/tools/searchUsers.ts
new file mode 100644
index 000000000..4f5181b98
--- /dev/null
+++ b/discord/src/tools/searchUsers.ts
@@ -0,0 +1,36 @@
+import { client } from '../client.js';
+import { formatTime, createDataset } from '../utils.js';
+
+export async function searchUsers() {
+  if (!process.env.QUERY || !process.env.GUILDID) {
+    throw new Error('QUERY and GUILDID environment variables are required for searchUsers');
+  }
+
+  const query = process.env.QUERY.toLowerCase();
+  const guildId = process.env.GUILDID;
+
+  const guild = await client.guilds.fetch(guildId);
+  const members = await guild.members.fetch();
+
+  const results = members.filter(member => 
+    member.user.username.toLowerCase().includes(query) ||
+    (member.nickname && member.nickname.toLowerCase().includes(query))
+  ).map(member => ({
+    id: member.user.id,
+    name: member.user.username,
+    username: member.user.username,
+    discriminator: member.user.discriminator,
+    nickname: member.nickname,
+    roles: member.roles.cache.map(role => ({
+      id: role.id,
+      name: role.name,
+      color: role.color,
+      position: role.position
+    })),
+    joinedAt: formatTime(member.joinedTimestamp),
+    isBot: member.user.bot,
+    avatarUrl: member.user.displayAvatarURL()
+  }));
+
+  await createDataset(results, 'discord_user_search');
+} 
\ No newline at end of file
diff --git a/discord/src/tools/sendMessage.ts b/discord/src/tools/sendMessage.ts
new file mode 100644
index 000000000..ae0d3ccaa
--- /dev/null
+++ b/discord/src/tools/sendMessage.ts
@@ -0,0 +1,42 @@
+import { TextChannel } from 'discord.js';
+import { client } from '../client.js';
+import { formatTime } from '../utils.js';
+
+export async function sendMessage() {
+  if (!process.env.CHANNELID || !process.env.GUILDID || !process.env.TEXT) {
+    throw new Error('CHANNELID, GUILDID, and TEXT environment variables are required for sendMessage');
+  }
+
+  const channelId = process.env.CHANNELID;
+  const guildId = process.env.GUILDID;
+  const text = process.env.TEXT;
+
+  try {
+    const guild = await client.guilds.fetch(guildId);
+    const channel = await guild.channels.fetch(channelId);
+
+    if (!(channel instanceof TextChannel)) {
+      throw new Error('Specified channel is not a text channel');
+    }
+
+    const message = await channel.send(text);
+
+    return JSON.stringify({
+      id: message.id,
+      content: message.content,
+      timestamp: formatTime(message.createdTimestamp),
+      permalink: `https://discord.com/channels/${guildId}/${channelId}/${message.id}`,
+      channel: {
+        id: channel.id,
+        name: channel.name
+      },
+      guild: {
+        id: guild.id,
+        name: guild.name
+      }
+    });
+  } catch (error) {
+    console.error('Error sending message:', error);
+    throw error;
+  }
+} 
\ No newline at end of file
diff --git a/discord/src/tools/sendMessageInThread.ts b/discord/src/tools/sendMessageInThread.ts
new file mode 100644
index 000000000..d6ed872dd
--- /dev/null
+++ b/discord/src/tools/sendMessageInThread.ts
@@ -0,0 +1,52 @@
+import { TextChannel, ThreadChannel } from 'discord.js';
+import { client } from '../client.js';
+import { formatTime } from '../utils.js';
+
+export async function sendMessageInThread() {
+  if (!process.env.CHANNELID || !process.env.GUILDID || !process.env.THREADID || !process.env.TEXT) {
+    throw new Error('CHANNELID, GUILDID, THREADID, and TEXT environment variables are required for sendMessageInThread');
+  }
+
+  const channelId = process.env.CHANNELID;
+  const guildId = process.env.GUILDID;
+  const threadId = process.env.THREADID;
+  const text = process.env.TEXT;
+
+  try {
+    const guild = await client.guilds.fetch(guildId);
+    const channel = await guild.channels.fetch(channelId);
+
+    if (!(channel instanceof TextChannel)) {
+      throw new Error('Specified channel is not a text channel');
+    }
+
+    const thread = await channel.threads.fetch(threadId);
+    if (!thread) {
+      throw new Error('Thread not found');
+    }
+
+    const message = await thread.send(text);
+
+    return JSON.stringify({
+      id: message.id,
+      content: message.content,
+      timestamp: formatTime(message.createdTimestamp),
+      permalink: `https://discord.com/channels/${guildId}/${channelId}/${message.id}`,
+      channel: {
+        id: channel.id,
+        name: channel.name
+      },
+      thread: {
+        id: thread.id,
+        name: thread.name
+      },
+      guild: {
+        id: guild.id,
+        name: guild.name
+      }
+    });
+  } catch (error) {
+    console.error('Error sending message in thread:', error);
+    throw error;
+  }
+} 
\ No newline at end of file
diff --git a/discord/src/types/gptscript.d.ts b/discord/src/types/gptscript.d.ts
new file mode 100644
index 000000000..ec38dd6b3
--- /dev/null
+++ b/discord/src/types/gptscript.d.ts
@@ -0,0 +1,9 @@
+declare module '@gptscript-ai/gptscript' {
+  export class GPTScript {
+    addDatasetElements(elements: Array<{
+      name: string;
+      description: string;
+      contents: string;
+    }>, options: { name: string }): Promise<string>;
+  }
+} 
\ No newline at end of file
diff --git a/discord/src/utils.ts b/discord/src/utils.ts
new file mode 100644
index 000000000..1716c5524
--- /dev/null
+++ b/discord/src/utils.ts
@@ -0,0 +1,61 @@
+import { GPTScript } from '@gptscript-ai/gptscript';
+
+/**
+ * Formats a timestamp in a human-readable format (hh:mm AM/PM Month Day, YYYY (Timezone))
+ * @param timestamp The timestamp in milliseconds since epoch
+ * @returns The formatted date string
+ */
+export function formatTime(timestamp: number | null | undefined): string | null {
+  if (timestamp == null) return null;
+
+  const tz = process.env.OBOT_USER_TIMEZONE || "UTC";
+  const date = new Date(timestamp);
+
+  try {
+    const formatted = date.toLocaleString('en-US', {
+      timeZone: tz,
+      hour: 'numeric',
+      minute: '2-digit',
+      hour12: true,
+      month: 'long',
+      day: 'numeric',
+      year: 'numeric'
+    });
+    return `${formatted} (${tz})`;
+  } catch (e) {
+    // If timezone is invalid, fall back to UTC
+    const formatted = new Date(timestamp).toLocaleString('en-US', {
+      timeZone: 'UTC',
+      hour: 'numeric',
+      minute: '2-digit',
+      hour12: true,
+      month: 'long',
+      day: 'numeric',
+      year: 'numeric'
+    });
+    return `${formatted} (UTC)`;
+  }
+}
+
+/**
+ * Creates a dataset from an array of elements
+ * @param elements Array of elements to add to the dataset
+ * @param datasetName Base name of the dataset
+ */
+export async function createDataset(elements: any[], datasetName: string): Promise<void> {
+  const gptscriptClient = new GPTScript();
+  const datasetElements = elements.map(element => ({
+    name: element.name || element.id,
+    description: element.description || "",
+    contents: JSON.stringify(element),
+  }));
+
+  const timestamp = new Date().getTime();
+  const uniqueDatasetName = `${datasetName}_${timestamp}`;
+
+  const datasetID = await gptscriptClient.addDatasetElements(datasetElements, {
+    name: uniqueDatasetName,
+  });
+
+  console.log(`Created dataset with ID ${datasetID} with ${elements.length} elements`);
+} 
\ No newline at end of file
diff --git a/discord/tool.gpt b/discord/tool.gpt
new file mode 100644
index 000000000..db7e29d98
--- /dev/null
+++ b/discord/tool.gpt
@@ -0,0 +1,138 @@
+---
+Name: Discord Bot
+Description: Tools for interacting with Discord using a Discord Bot
+Metadata: bundle: true
+Share Tools: List Channels, Search Channels, Get Channel History, Get Channel History by Time, Get Thread History, List Users, Search Users, Send Message, Send Message in Thread
+
+---
+Name: List Channels
+Description: List all text channels in all Discord guilds the bot has access to. Returns the name, ID, guild name, and guild ID for each channel.
+Tools: github.com/gptscript-ai/datasets/filter
+Credential: ./credential
+Share Context: Discord Context
+
+#!/usr/bin/env npm --silent --prefix ${GPTSCRIPT_TOOL_DIR} run tool -- listChannels
+
+---
+Name: Search Channels
+Description: Search for text channels in Discord guilds the bot has access to
+Tools: github.com/gptscript-ai/datasets/filter
+Credential: ./credential
+Share Context: Discord Context
+Param: query: the search query
+
+#!/usr/bin/env npm --silent --prefix ${GPTSCRIPT_TOOL_DIR} run tool -- searchChannels
+
+---
+Name: Get Channel History
+Description: Get the chat history for a text channel in Discord
+Tools: github.com/gptscript-ai/datasets/filter
+Credential: ./credential
+Share Tools: List Channels, Search Channels
+Share Context: Discord Context
+Param: channelid: the ID of the channel to get the history for
+Param: guildid: the ID of the guild that the channel belongs to
+Param: limit: the number of messages to return - recommend starting with 10 and increasing if necessary
+
+#!/usr/bin/env npm --silent --prefix ${GPTSCRIPT_TOOL_DIR} run tool -- getChannelHistory
+
+---
+Name: Get Channel History by Time
+Description: Get the chat history for a text channel in Discord within a specific time range
+Tools: github.com/gptscript-ai/datasets/filter
+Credential: ./credential
+Share Tools: List Channels, Search Channels
+Share Context: Discord Context
+Param: channelid: the ID of the channel to get the history for
+Param: guildid: the ID of the guild that the channel belongs to
+Param: limit: the maximum number of messages to return - recommend starting with 10 and increasing if necessary
+Param: start: the start time in RFC 3339 format
+Param: end: the end time in RFC 3339 format
+
+#!/usr/bin/env npm --silent --prefix ${GPTSCRIPT_TOOL_DIR} run tool -- getChannelHistoryByTime
+
+---
+Name: Get Thread History
+Description: Get the chat history for a particular thread in a Discord channel
+Tools: github.com/gptscript-ai/datasets/filter
+Credential: ./credential
+Share Tools: List Channels, Get Channel History
+Share Context: Discord Context
+Param: channelid: the ID of the channel containing the thread
+Param: guildid: the ID of the guild that the channel belongs to
+Param: threadid: the ID of the thread to get the history for
+Param: limit: the number of messages to return - recommend starting with 10 and increasing if necessary
+
+#!/usr/bin/env npm --silent --prefix ${GPTSCRIPT_TOOL_DIR} run tool -- getThreadHistory
+
+---
+Name: List Users
+Description: List all users in a Discord guild. Returns detailed user information including username, nickname, roles, and other metadata.
+Tools: github.com/gptscript-ai/datasets/filter
+Credential: ./credential
+Share Context: Discord Context
+Param: guildid: the ID of the guild to list users from
+
+#!/usr/bin/env npm --silent --prefix ${GPTSCRIPT_TOOL_DIR} run tool -- listUsers
+
+---
+Name: Search Users
+Description: Search for users in a Discord guild by username, nickname, or discriminator.
+Tools: github.com/gptscript-ai/datasets/filter
+Credential: ./credential
+Share Context: Discord Context
+Param: guildid: the ID of the guild to search users in
+Param: query: the search query to match against usernames, nicknames, or discriminators
+
+#!/usr/bin/env npm --silent --prefix ${GPTSCRIPT_TOOL_DIR} run tool -- searchUsers
+
+---
+Name: Send Message
+Description: Send a message to a text channel in a Discord guild
+Credential: ./credential
+Share Tools: List Channels, Search Channels
+Share Context: Discord Context
+Param: channelid: the ID of the channel to send the message to
+Param: guildid: the ID of the guild that the channel belongs to
+Param: text: the text content to send
+
+#!/usr/bin/env npm --silent --prefix ${GPTSCRIPT_TOOL_DIR} run tool -- sendMessage
+
+---
+Name: Send Message in Thread
+Description: Send a message in a thread in a Discord channel
+Credential: ./credential
+Share Tools: List Channels, Search Channels, Get Channel History, Get Thread History
+Share Context: Discord Context
+Param: channelid: the ID of the channel containing the thread
+Param: guildid: the ID of the guild that the channel belongs to
+Param: threadid: the ID of the thread to send the message to
+Param: text: the text content to send
+
+#!/usr/bin/env npm --silent --prefix ${GPTSCRIPT_TOOL_DIR} run tool -- sendMessageInThread
+
+---
+Name: Discord Context
+Type: context
+
+#!sys.echo
+
+## Instructions for using Discord tools
+
+You have access to a set of tools to use the Discord API.
+
+Do not provide guild, channel, thread, message, or user IDs in the output, as these are not helpful for the user.
+
+When displaying the output of a message, format it like this: 
+
+Time User: Content
+  Additional details if needed
+  Link
+
+When displaying times, use the user's default time zone.
+
+## End of instructions for using Discord tools
+
+---
+!metadata:*:icon
+https://cdn.jsdelivr.net/npm/simple-icons@v13/icons/discord.svg
diff --git a/discord/tsconfig.json b/discord/tsconfig.json
new file mode 100644
index 000000000..57dd8a871
--- /dev/null
+++ b/discord/tsconfig.json
@@ -0,0 +1,15 @@
+{
+  "compilerOptions": {
+    "target": "es2016",
+    "module": "esnext",
+    "moduleResolution": "bundler",
+    "outDir": "./dist",
+    "esModuleInterop": true,
+    "forceConsistentCasingInFileNames": true,
+    "strict": true,
+    "skipLibCheck": true,
+    "typeRoots": ["./node_modules/@types", "./src/types"]
+  },
+  "include": ["src/**/*"],
+  "exclude": ["node_modules"]
+} 
\ No newline at end of file
diff --git a/discord/validate-cred.gpt b/discord/validate-cred.gpt
new file mode 100644
index 000000000..7ec87864e
--- /dev/null
+++ b/discord/validate-cred.gpt
@@ -0,0 +1,5 @@
+Name: discord-cred-validate
+Description: Validate Discord credential
+Metadata: index: false
+
+#!/usr/bin/env npm --silent --prefix ${GPTSCRIPT_TOOL_DIR} run tool -- login
\ No newline at end of file
diff --git a/index.yaml b/index.yaml
index 96b496991..c59727fb1 100644
--- a/index.yaml
+++ b/index.yaml
@@ -87,6 +87,8 @@ tools:
     reference: ./search/tavily/obot-search.gpt
   search-combo:
     reference: ./search-combo
+  discord:
+    reference: ./discord
 
 knowledgeDataSources:
   notion-data-source:


Reviews:
user: login: njhale
body: Noticed a few rough edges while trying this out locally:

1. Channel join messages are shown but their content is always empty

  <img width="308" alt="Screenshot 2025-03-26 at 4 05 02 PM" src="https://github.com/user-attachments/assets/69fd6ee4-430f-428b-8137-7f991ab19089" />

<img width="440" alt="Screenshot 2025-03-26 at 4 28 58 PM" src="https://github.com/user-attachments/assets/989a59c3-d9d9-46b5-8e2b-91d7b66a6072" />

2. Times are displayed in UTC. Might want to add some context to display messages in the user's default timezone when available (on the bright side, getting history by time knew to use my timezone when I said "get all messages after 4pm today")

3. Messages are displayed in reverse-chronological order; i.e. The newest messages are printed first. I think this is good for tooling and reasoning, but not great when reading messages as a user.

4. Message formatting is not easy to visually grok (this is more polish than anything, so I don't really expect this to change on the first pass, but wanted to mention it anyway)

<img width="563" alt="Screenshot 2025-03-26 at 4 33 14 PM" src="https://github.com/user-attachments/assets/3b5fbbba-aba2-447b-ab9c-c6a8bda4e792" />






state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-26T20:34:43Z
user: login: g-linville
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T20:38:03Z
user: login: g-linville
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T20:38:32Z
user: login: tybalex
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T20:55:11Z
user: login: tybalex
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T20:56:08Z
user: login: tybalex
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T20:58:50Z
user: login: g-linville
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T21:03:13Z
user: login: g-linville
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T21:03:26Z
user: login: g-linville
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-26T21:03:35Z
user: login: tybalex
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-26T21:07:49Z
user: login: njhale
body: lgtm!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-26T21:56:14Z

Review comments:
diff_hunk: @@ -0,0 +1,175 @@
+import { TextChannel, Attachment, Embed, Message, Collection } from 'discord.js';
+import { client } from '../client.js';
+import { toRFC3339, createDataset } from '../utils.js';
+
+export async function searchMessages() {
path: discord/src/tools/searchMessages.ts
user: login: g-linville
body: Sadly, Discord does not expose a proper "search messages" API for bots, so this is kind of a weak tool, in that it only grabs 100 messages from a channel and then sees if any of them contain the query. Let me know if we should just drop this one.
created_at: 2025-03-26T17:56:30Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,101 @@
+---
path: .cursor/rules/gpt.mdc
user: login: njhale
body: Sweet! I'm assuming cursor detects this automatically?
created_at: 2025-03-26T19:12:21Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,101 @@
+---
+description: GPTScript Tool Files
path: .cursor/rules/gpt.mdc
user: login: tybalex
body: is this file part of the PR?
created_at: 2025-03-26T20:05:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,73 @@
+import { client } from './client.js';
+import { listChannels } from './tools/listChannels.js';
+import { searchChannels } from './tools/searchChannels.js';
+import { getChannelHistory } from './tools/getChannelHistory.js';
+import { getChannelHistoryByTime } from './tools/getChannelHistoryByTime.js';
+import { getThreadHistory } from './tools/getThreadHistory.js';
+import { searchMessages } from './tools/searchMessages.js';
+import { listUsers } from './tools/listUsers.js';
+import { searchUsers } from './tools/searchUsers.js';
+import { sendMessage } from './tools/sendMessage.js';
+import { sendMessageInThread } from './tools/sendMessageInThread.js';
+
+async function login(): Promise<void> {
+  try {
+    await client.login(process.env.DISCORD_TOKEN);
+  } catch (error: any) {
+    console.log('Discord login failed:', error);
+    process.exit(1);
+  }
+}
+
+async function main() {
+  try {
+    await login();
+
+    const command = process.argv[2];
+
+    switch (command) {
+      case 'listChannels':
+        console.log(await listChannels());
+        break;
+      case 'searchChannels':
+        console.log(await searchChannels());
+        break;
+      case 'getChannelHistory':
+        console.log(await getChannelHistory());
+        break;
+      case 'getChannelHistoryByTime':
+        console.log(await getChannelHistoryByTime());
path: discord/src/index.ts
user: login: tybalex
body: shall we merge getChannelHistoryByTime into getChannelHistory?
created_at: 2025-03-26T20:06:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,144 @@
+---
+Name: Discord Bot
+Description: Tools for interacting with Discord using a Discord Bot
+Metadata: bundle: true
+Share Tools: List Channels, Search Channels, Get Channel History, Get Channel History by Time, Get Thread History, Search Messages, List Users, Search Users, Send Message, Send Message in Thread
+
+---
+Name: List Channels
+Description: List all text channels in all Discord guilds the bot has access to. Returns the name, ID, guild name, and guild ID for each channel.
+Tools: github.com/gptscript-ai/datasets/filter
+Credential: ./credential
+Share Context: Discord Context
+
+#!/usr/bin/env npm --silent --prefix ${GPTSCRIPT_TOOL_DIR} run tool -- listChannels
+
+---
+Name: Search Channels
+Description: Search for text channels in Discord guilds the bot has access to
+Tools: github.com/gptscript-ai/datasets/filter
+Credential: ./credential
+Share Context: Discord Context
+Param: query: the search query
+
+#!/usr/bin/env npm --silent --prefix ${GPTSCRIPT_TOOL_DIR} run tool -- searchChannels
+
+---
+Name: Get Channel History
+Description: Get the chat history for a text channel in Discord
+Tools: github.com/gptscript-ai/datasets/filter
+Credential: ./credential
+Share Tools: List Channels, Search Channels
+Share Context: Discord Context
+Param: channelid: the ID of the channel to get the history for
+Param: guildid: the ID of the guild that the channel belongs to
+Param: limit: the number of messages to return - recommend starting with 10 and increasing if necessary
+
+#!/usr/bin/env npm --silent --prefix ${GPTSCRIPT_TOOL_DIR} run tool -- getChannelHistory
+
+---
+Name: Get Channel History by Time
+Description: Get the chat history for a text channel in Discord within a specific time range
+Tools: github.com/gptscript-ai/datasets/filter
+Credential: ./credential
+Share Tools: List Channels, Search Channels
+Share Context: Discord Context
+Param: channelid: the ID of the channel to get the history for
+Param: guildid: the ID of the guild that the channel belongs to
+Param: limit: the maximum number of messages to return - recommend starting with 10 and increasing if necessary
+Param: start: the start time in RFC 3339 format
+Param: end: the end time in RFC 3339 format
+
+#!/usr/bin/env npm --silent --prefix ${GPTSCRIPT_TOOL_DIR} run tool -- getChannelHistoryByTime
+
+---
+Name: Get Thread History
+Description: Get the chat history for a particular thread in a Discord channel
+Tools: github.com/gptscript-ai/datasets/filter
+Credential: ./credential
+Share Tools: List Channels, Get Channel History
+Share Context: Discord Context
+Param: channelid: the ID of the channel containing the thread
+Param: guildid: the ID of the guild that the channel belongs to
+Param: threadid: the ID of the thread to get the history for
+Param: limit: the number of messages to return - recommend starting with 10 and increasing if necessary
+
+#!/usr/bin/env npm --silent --prefix ${GPTSCRIPT_TOOL_DIR} run tool -- getThreadHistory
+
+---
+Name: Search Messages
+Description: Search for messages across all accessible Discord channels. Returns messages matching the query, including thread information if the message is part of a thread.
path: discord/tool.gpt
user: login: njhale
body: I haven't been able to get this to return messages from threads.
created_at: 2025-03-26T20:21:11Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,101 @@
+---
path: .cursor/rules/gpt.mdc
user: login: g-linville
body: yep!
created_at: 2025-03-26T20:37:20Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,144 @@
+---
+Name: Discord Bot
+Description: Tools for interacting with Discord using a Discord Bot
+Metadata: bundle: true
+Share Tools: List Channels, Search Channels, Get Channel History, Get Channel History by Time, Get Thread History, Search Messages, List Users, Search Users, Send Message, Send Message in Thread
+
+---
+Name: List Channels
+Description: List all text channels in all Discord guilds the bot has access to. Returns the name, ID, guild name, and guild ID for each channel.
+Tools: github.com/gptscript-ai/datasets/filter
+Credential: ./credential
+Share Context: Discord Context
+
+#!/usr/bin/env npm --silent --prefix ${GPTSCRIPT_TOOL_DIR} run tool -- listChannels
+
+---
+Name: Search Channels
+Description: Search for text channels in Discord guilds the bot has access to
+Tools: github.com/gptscript-ai/datasets/filter
+Credential: ./credential
+Share Context: Discord Context
+Param: query: the search query
+
+#!/usr/bin/env npm --silent --prefix ${GPTSCRIPT_TOOL_DIR} run tool -- searchChannels
+
+---
+Name: Get Channel History
+Description: Get the chat history for a text channel in Discord
+Tools: github.com/gptscript-ai/datasets/filter
+Credential: ./credential
+Share Tools: List Channels, Search Channels
+Share Context: Discord Context
+Param: channelid: the ID of the channel to get the history for
+Param: guildid: the ID of the guild that the channel belongs to
+Param: limit: the number of messages to return - recommend starting with 10 and increasing if necessary
+
+#!/usr/bin/env npm --silent --prefix ${GPTSCRIPT_TOOL_DIR} run tool -- getChannelHistory
+
+---
+Name: Get Channel History by Time
+Description: Get the chat history for a text channel in Discord within a specific time range
+Tools: github.com/gptscript-ai/datasets/filter
+Credential: ./credential
+Share Tools: List Channels, Search Channels
+Share Context: Discord Context
+Param: channelid: the ID of the channel to get the history for
+Param: guildid: the ID of the guild that the channel belongs to
+Param: limit: the maximum number of messages to return - recommend starting with 10 and increasing if necessary
+Param: start: the start time in RFC 3339 format
+Param: end: the end time in RFC 3339 format
+
+#!/usr/bin/env npm --silent --prefix ${GPTSCRIPT_TOOL_DIR} run tool -- getChannelHistoryByTime
+
+---
+Name: Get Thread History
+Description: Get the chat history for a particular thread in a Discord channel
+Tools: github.com/gptscript-ai/datasets/filter
+Credential: ./credential
+Share Tools: List Channels, Get Channel History
+Share Context: Discord Context
+Param: channelid: the ID of the channel containing the thread
+Param: guildid: the ID of the guild that the channel belongs to
+Param: threadid: the ID of the thread to get the history for
+Param: limit: the number of messages to return - recommend starting with 10 and increasing if necessary
+
+#!/usr/bin/env npm --silent --prefix ${GPTSCRIPT_TOOL_DIR} run tool -- getThreadHistory
+
+---
+Name: Search Messages
+Description: Search for messages across all accessible Discord channels. Returns messages matching the query, including thread information if the message is part of a thread.
path: discord/tool.gpt
user: login: g-linville
body: honestly it's a bad tool anyway. I meant to leave a comment about it but forgot to submit it on the PR. I'm just gonna remove it
created_at: 2025-03-26T20:38:32Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,175 @@
+import { TextChannel, Attachment, Embed, Message, Collection } from 'discord.js';
+import { client } from '../client.js';
+import { toRFC3339, createDataset } from '../utils.js';
+
+export async function searchMessages() {
+  if (!process.env.QUERY || !process.env.LIMIT) {
+    throw new Error('QUERY and LIMIT environment variables are required for searchMessages');
+  }
+
+  const query = process.env.QUERY;
+  const limit = parseInt(process.env.LIMIT, 10);
+  const guildId = process.env.GUILDID; // Optional
+  const channelId = process.env.CHANNELID; // Optional
+
+  // If channelId is provided, guildId must also be provided
+  if (channelId && !guildId) {
+    throw new Error('GUILDID must be provided when CHANNELID is specified');
+  }
+
+  const results = [];
+
+  if (guildId && channelId) {
+    // Search in specific channel
+    const guild = await client.guilds.fetch(guildId);
+    const channel = await guild.channels.fetch(channelId);
+    
+    if (!(channel instanceof TextChannel)) {
+      throw new Error('Specified channel is not a text channel');
+    }
+
+    try {
+      const messages = await channel.messages.fetch({ 
+        limit: Math.min(100, limit)
path: discord/src/tools/searchMessages.ts
user: login: tybalex
body: should we fetch more than just 100 messages to search? or is it too expensive/slow/rate-limit concern..?
created_at: 2025-03-26T20:55:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,101 @@
+---
+description: GPTScript Tool Files
path: .cursor/rules/gpt.mdc
user: login: tybalex
body: saw the conversation above lol
created_at: 2025-03-26T20:56:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,175 @@
+import { TextChannel, Attachment, Embed, Message, Collection } from 'discord.js';
+import { client } from '../client.js';
+import { toRFC3339, createDataset } from '../utils.js';
+
+export async function searchMessages() {
path: discord/src/tools/searchMessages.ts
user: login: tybalex
body: would it be too expensive/slow to paginate and search for like 1k messages..?
created_at: 2025-03-26T20:58:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,101 @@
+---
+description: GPTScript Tool Files
path: .cursor/rules/gpt.mdc
user: login: g-linville
body: yes
created_at: 2025-03-26T21:03:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,73 @@
+import { client } from './client.js';
+import { listChannels } from './tools/listChannels.js';
+import { searchChannels } from './tools/searchChannels.js';
+import { getChannelHistory } from './tools/getChannelHistory.js';
+import { getChannelHistoryByTime } from './tools/getChannelHistoryByTime.js';
+import { getThreadHistory } from './tools/getThreadHistory.js';
+import { searchMessages } from './tools/searchMessages.js';
+import { listUsers } from './tools/listUsers.js';
+import { searchUsers } from './tools/searchUsers.js';
+import { sendMessage } from './tools/sendMessage.js';
+import { sendMessageInThread } from './tools/sendMessageInThread.js';
+
+async function login(): Promise<void> {
+  try {
+    await client.login(process.env.DISCORD_TOKEN);
+  } catch (error: any) {
+    console.log('Discord login failed:', error);
+    process.exit(1);
+  }
+}
+
+async function main() {
+  try {
+    await login();
+
+    const command = process.argv[2];
+
+    switch (command) {
+      case 'listChannels':
+        console.log(await listChannels());
+        break;
+      case 'searchChannels':
+        console.log(await searchChannels());
+        break;
+      case 'getChannelHistory':
+        console.log(await getChannelHistory());
+        break;
+      case 'getChannelHistoryByTime':
+        console.log(await getChannelHistoryByTime());
path: discord/src/index.ts
user: login: g-linville
body: we kept these separate in Slack, so I want to do the same here
created_at: 2025-03-26T21:03:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,175 @@
+import { TextChannel, Attachment, Embed, Message, Collection } from 'discord.js';
+import { client } from '../client.js';
+import { toRFC3339, createDataset } from '../utils.js';
+
+export async function searchMessages() {
+  if (!process.env.QUERY || !process.env.LIMIT) {
+    throw new Error('QUERY and LIMIT environment variables are required for searchMessages');
+  }
+
+  const query = process.env.QUERY;
+  const limit = parseInt(process.env.LIMIT, 10);
+  const guildId = process.env.GUILDID; // Optional
+  const channelId = process.env.CHANNELID; // Optional
+
+  // If channelId is provided, guildId must also be provided
+  if (channelId && !guildId) {
+    throw new Error('GUILDID must be provided when CHANNELID is specified');
+  }
+
+  const results = [];
+
+  if (guildId && channelId) {
+    // Search in specific channel
+    const guild = await client.guilds.fetch(guildId);
+    const channel = await guild.channels.fetch(channelId);
+    
+    if (!(channel instanceof TextChannel)) {
+      throw new Error('Specified channel is not a text channel');
+    }
+
+    try {
+      const messages = await channel.messages.fetch({ 
+        limit: Math.min(100, limit)
path: discord/src/tools/searchMessages.ts
user: login: g-linville
body: I decided to nuke this tool because it was bad
created_at: 2025-03-26T21:03:34Z
author_association: MEMBER

PR comments:
user: login: tybalex
created_at: 2025-03-26T19:35:34Z
body: Discord officially recommends using a bot for REST API interactions, so I think this is the right way. Should we name it to something like `Discord Bot Tool` though?
author_association: CONTRIBUTOR
user: login: g-linville
created_at: 2025-03-26T19:38:21Z
body: @tybalex good call. I just pushed a commit to change the name
author_association: MEMBER
user: login: g-linville
created_at: 2025-03-26T21:03:01Z
body: @njhale I just pushed a commit that addresses what you mentioned
author_association: MEMBER