feat: add Slack + Discord tool packages and fix 50 skipped sync packages
Add @tpmjs/tools-slack (10 tools) and @tpmjs/tools-discord (15 tools) with full API coverage, typed outputs, and domain-validated blocks. Add 7 missing business categories (finance, legal, hr, marketing, cx, edu, sales) to TPMJS_CATEGORIES so 50 previously skipped packages can sync to tpmjs.com. Fix lefthook secrets hook to skip gracefully when git-secrets is not installed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7327992a9d
commit
cc69d98b6f
14 changed files with 3803 additions and 2 deletions
|
|
@ -43,7 +43,7 @@ blocks:
|
|||
measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance]
|
||||
```
|
||||
|
||||
**Category prefix** (before the dot): `research`, `web`, `data`, `documentation`, `engineering`, `security`, `statistics`, `ops`, `agent`, `sandbox`, `utilities`, `html`, `compliance`.
|
||||
**Category prefix** (before the dot): `research`, `web`, `data`, `documentation`, `engineering`, `security`, `statistics`, `ops`, `agent`, `sandbox`, `utilities`, `html`, `compliance`, `finance`, `legal`, `hr`, `marketing`, `cx`, `edu`, `sales`.
|
||||
|
||||
For domain entities and quality measures, see [references/domain.md](references/domain.md).
|
||||
|
||||
|
|
@ -222,9 +222,15 @@ Each tool gets its own entry in blocks.yml (same `path`) and in `tpmjs.tools` ar
|
|||
|
||||
## Step 4: Validate
|
||||
|
||||
The blocks CLI domain validator requires an OpenAI API key. Source it from `.env.local` before running:
|
||||
|
||||
```bash
|
||||
cd packages/tools/official
|
||||
|
||||
# Load the OpenAI API key for domain validation
|
||||
source ../../../.env.local
|
||||
export OPENAI_API_KEY
|
||||
|
||||
pnpm blocks run <tool-name> # Validate (schema → shape → domain)
|
||||
pnpm blocks run <tool-name> --force # Force full validation (skip cache)
|
||||
pnpm blocks run <tool-name> --json # JSON output for debugging
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ pre-commit:
|
|||
secrets:
|
||||
# Scan for secrets before allowing commit - runs first
|
||||
priority: 1
|
||||
run: git secrets --scan --cached
|
||||
run: command -v git-secrets >/dev/null 2>&1 && git secrets --scan --cached || true
|
||||
fail_text: "🚨 Secrets detected! Remove sensitive data before committing."
|
||||
|
||||
format:
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
48
packages/tools/official/discord/README.md
Normal file
48
packages/tools/official/discord/README.md
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# @tpmjs/tools-discord
|
||||
|
||||
Discord API tools for AI agents. Send messages, manage guilds, channels, threads, members, reactions, and more.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @tpmjs/tools-discord
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
Set the `DISCORD_BOT_TOKEN` environment variable. Get your token from [Discord Developer Portal](https://discord.com/developers/applications).
|
||||
|
||||
Required bot permissions: Send Messages, Read Message History, Manage Messages, Manage Channels, Add Reactions, Manage Threads.
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { sendMessage, listGuilds } from '@tpmjs/tools-discord';
|
||||
|
||||
const result = await sendMessage.execute({ channel_id: '123456789', content: 'Hello from AI!' });
|
||||
const guilds = await listGuilds.execute({});
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| sendMessage | Send a message to a channel |
|
||||
| listGuilds | List guilds the bot is in |
|
||||
| getGuild | Get guild details |
|
||||
| listChannels | List guild channels |
|
||||
| getChannel | Get channel details |
|
||||
| listMessages | Get recent messages from a channel |
|
||||
| createChannel | Create a text/voice/category channel |
|
||||
| editMessage | Edit a previously sent message |
|
||||
| deleteMessage | Delete a message |
|
||||
| addReaction | Add emoji reaction to a message |
|
||||
| listMembers | List guild members |
|
||||
| getMember | Get member details |
|
||||
| createThread | Create a thread from a message |
|
||||
| listThreads | List active threads |
|
||||
| pinMessage | Pin a message |
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
114
packages/tools/official/discord/package.json
Normal file
114
packages/tools/official/discord/package.json
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
{
|
||||
"name": "@tpmjs/tools-discord",
|
||||
"version": "0.1.0",
|
||||
"description": "Discord API tools for AI agents. Send messages, manage guilds, channels, threads, members, reactions, and more.",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"tpmjs",
|
||||
"discord",
|
||||
"messaging",
|
||||
"ops",
|
||||
"agent"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rm -rf dist .turbo"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tpmjs/tsconfig": "workspace:*",
|
||||
"tsup": "^8.5.1",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"ai": "6.0.49"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tpmjs/tpmjs.git",
|
||||
"directory": "packages/tools/official/discord"
|
||||
},
|
||||
"homepage": "https://tpmjs.com",
|
||||
"license": "MIT",
|
||||
"tpmjs": {
|
||||
"category": "ops",
|
||||
"frameworks": [
|
||||
"vercel-ai"
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"name": "sendMessage",
|
||||
"description": "Send a message to a Discord channel."
|
||||
},
|
||||
{
|
||||
"name": "listGuilds",
|
||||
"description": "List guilds (servers) the bot is a member of."
|
||||
},
|
||||
{
|
||||
"name": "getGuild",
|
||||
"description": "Get detailed information about a Discord guild."
|
||||
},
|
||||
{
|
||||
"name": "listChannels",
|
||||
"description": "List all channels in a Discord guild."
|
||||
},
|
||||
{
|
||||
"name": "getChannel",
|
||||
"description": "Get detailed information about a specific Discord channel."
|
||||
},
|
||||
{
|
||||
"name": "listMessages",
|
||||
"description": "Retrieve recent messages from a Discord channel."
|
||||
},
|
||||
{
|
||||
"name": "createChannel",
|
||||
"description": "Create a new text or voice channel in a Discord guild."
|
||||
},
|
||||
{
|
||||
"name": "editMessage",
|
||||
"description": "Edit a previously sent message in a Discord channel."
|
||||
},
|
||||
{
|
||||
"name": "deleteMessage",
|
||||
"description": "Delete a message from a Discord channel."
|
||||
},
|
||||
{
|
||||
"name": "addReaction",
|
||||
"description": "Add an emoji reaction to a Discord message."
|
||||
},
|
||||
{
|
||||
"name": "listMembers",
|
||||
"description": "List members of a Discord guild with pagination."
|
||||
},
|
||||
{
|
||||
"name": "getMember",
|
||||
"description": "Get detailed information about a specific guild member."
|
||||
},
|
||||
{
|
||||
"name": "createThread",
|
||||
"description": "Create a new thread from a message in a Discord channel."
|
||||
},
|
||||
{
|
||||
"name": "listThreads",
|
||||
"description": "List active threads in a Discord channel."
|
||||
},
|
||||
{
|
||||
"name": "pinMessage",
|
||||
"description": "Pin a message in a Discord channel."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
750
packages/tools/official/discord/src/index.ts
Normal file
750
packages/tools/official/discord/src/index.ts
Normal file
|
|
@ -0,0 +1,750 @@
|
|||
/**
|
||||
* @tpmjs/tools-discord — Discord API Tools for AI Agents
|
||||
*
|
||||
* Full access to the Discord REST API: send messages, manage channels, guilds,
|
||||
* members, threads, reactions, and more.
|
||||
*
|
||||
* @requires DISCORD_BOT_TOKEN environment variable
|
||||
*/
|
||||
|
||||
import { jsonSchema, tool } from 'ai';
|
||||
|
||||
const BASE_URL = 'https://discord.com/api/v10';
|
||||
|
||||
// ─── Client Infrastructure ──────────────────────────────────────────────────
|
||||
|
||||
function getApiKey(): string {
|
||||
const key = process.env.DISCORD_BOT_TOKEN;
|
||||
if (!key) {
|
||||
throw new Error(
|
||||
'DISCORD_BOT_TOKEN environment variable is required. Get your token from https://discord.com/developers/applications'
|
||||
);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
async function apiRequest<T>(method: string, path: string, body?: unknown): Promise<T> {
|
||||
const key = getApiKey();
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bot ${key}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const options: RequestInit = { method, headers };
|
||||
if (body !== undefined) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const response = await fetch(`${BASE_URL}${path}`, options);
|
||||
|
||||
if (!response.ok) {
|
||||
await handleApiError(response);
|
||||
}
|
||||
|
||||
if (response.status === 204) {
|
||||
return { success: true } as T;
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
if (!text) {
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
return JSON.parse(text) as T;
|
||||
}
|
||||
|
||||
async function handleApiError(response: Response): Promise<never> {
|
||||
let errorMessage: string;
|
||||
try {
|
||||
const errorData = (await response.json()) as { message?: string; code?: number };
|
||||
errorMessage = errorData.message || `HTTP ${response.status}`;
|
||||
if (errorData.code) {
|
||||
errorMessage = `${errorMessage} (Discord Error Code: ${errorData.code})`;
|
||||
}
|
||||
} catch {
|
||||
errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
||||
}
|
||||
|
||||
switch (response.status) {
|
||||
case 400:
|
||||
throw new Error(`Bad request: ${errorMessage}`);
|
||||
case 401:
|
||||
throw new Error('Authentication failed: Invalid Discord bot token. Check DISCORD_BOT_TOKEN.');
|
||||
case 403:
|
||||
throw new Error(`Access forbidden: ${errorMessage}`);
|
||||
case 404:
|
||||
throw new Error(`Not found: ${errorMessage}`);
|
||||
case 429:
|
||||
throw new Error(`Rate limit exceeded: ${errorMessage}`);
|
||||
default:
|
||||
throw new Error(`Discord API error (${response.status}): ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
function buildQueryString(params: Record<string, unknown>): string {
|
||||
const entries = Object.entries(params).filter(
|
||||
([, v]) => v !== undefined && v !== null && v !== ''
|
||||
);
|
||||
if (entries.length === 0) return '';
|
||||
return (
|
||||
'?' +
|
||||
entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join('&')
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Output Types ────────────────────────────────────────────────────────────
|
||||
|
||||
export interface DiscordMessage {
|
||||
id: string;
|
||||
channel_id: string;
|
||||
content: string;
|
||||
author: { id: string; username: string; discriminator: string };
|
||||
timestamp: string;
|
||||
tts: boolean;
|
||||
pinned: boolean;
|
||||
}
|
||||
|
||||
export interface DiscordGuild {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string | null;
|
||||
owner_id: string;
|
||||
member_count?: number;
|
||||
approximate_member_count?: number;
|
||||
approximate_presence_count?: number;
|
||||
features: string[];
|
||||
}
|
||||
|
||||
export interface DiscordChannel {
|
||||
id: string;
|
||||
type: number;
|
||||
guild_id?: string;
|
||||
name?: string;
|
||||
topic?: string | null;
|
||||
position?: number;
|
||||
parent_id?: string | null;
|
||||
}
|
||||
|
||||
export interface DiscordMember {
|
||||
user: { id: string; username: string; discriminator: string };
|
||||
nick: string | null;
|
||||
roles: string[];
|
||||
joined_at: string;
|
||||
}
|
||||
|
||||
export interface DiscordThread {
|
||||
id: string;
|
||||
name: string;
|
||||
type: number;
|
||||
guild_id: string;
|
||||
parent_id: string;
|
||||
owner_id: string;
|
||||
message_count: number;
|
||||
member_count: number;
|
||||
}
|
||||
|
||||
export interface SuccessResult {
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
export interface ListThreadsResult {
|
||||
threads: DiscordThread[];
|
||||
members: { id: string; user_id: string; join_timestamp: string }[];
|
||||
}
|
||||
|
||||
// ─── Messages ───────────────────────────────────────────────────────────────
|
||||
|
||||
export interface SendMessageInput {
|
||||
channel_id: string;
|
||||
content: string;
|
||||
tts?: boolean;
|
||||
}
|
||||
|
||||
export const sendMessage = tool({
|
||||
description: 'Send a message to a Discord channel with optional text-to-speech.',
|
||||
inputSchema: jsonSchema<SendMessageInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
channel_id: { type: 'string', description: 'The ID of the channel to send the message to.' },
|
||||
content: { type: 'string', description: 'The message content (up to 2000 characters).' },
|
||||
tts: {
|
||||
type: 'boolean',
|
||||
description: 'Whether this message should be sent as text-to-speech.',
|
||||
},
|
||||
},
|
||||
required: ['channel_id', 'content'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: SendMessageInput): Promise<DiscordMessage> {
|
||||
try {
|
||||
if (!input.channel_id || !input.content) {
|
||||
throw new Error('channel_id and content are required and must be non-empty');
|
||||
}
|
||||
if (input.content.length > 2000) {
|
||||
throw new Error('Message content must be 2000 characters or less');
|
||||
}
|
||||
return await apiRequest<DiscordMessage>('POST', `/channels/${input.channel_id}/messages`, {
|
||||
content: input.content,
|
||||
tts: input.tts,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to send message: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export interface ListMessagesInput {
|
||||
channel_id: string;
|
||||
limit?: number;
|
||||
before?: string;
|
||||
after?: string;
|
||||
around?: string;
|
||||
}
|
||||
|
||||
export const listMessages = tool({
|
||||
description:
|
||||
'Get recent messages from a Discord channel with optional pagination using message IDs.',
|
||||
inputSchema: jsonSchema<ListMessagesInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
channel_id: { type: 'string', description: 'The ID of the channel to get messages from.' },
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Number of messages to retrieve (1-100, default: 50).',
|
||||
},
|
||||
before: { type: 'string', description: 'Get messages before this message ID.' },
|
||||
after: { type: 'string', description: 'Get messages after this message ID.' },
|
||||
around: { type: 'string', description: 'Get messages around this message ID.' },
|
||||
},
|
||||
required: ['channel_id'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: ListMessagesInput): Promise<DiscordMessage[]> {
|
||||
try {
|
||||
if (!input.channel_id) {
|
||||
throw new Error('channel_id is required');
|
||||
}
|
||||
if (input.limit !== undefined && (input.limit < 1 || input.limit > 100)) {
|
||||
throw new Error('Limit must be between 1 and 100');
|
||||
}
|
||||
const qs = buildQueryString({
|
||||
limit: input.limit,
|
||||
before: input.before,
|
||||
after: input.after,
|
||||
around: input.around,
|
||||
});
|
||||
return await apiRequest<DiscordMessage[]>(
|
||||
'GET',
|
||||
`/channels/${input.channel_id}/messages${qs}`
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to list messages: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export interface EditMessageInput {
|
||||
channel_id: string;
|
||||
message_id: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export const editMessage = tool({
|
||||
description: 'Edit an existing message sent by the bot in a Discord channel.',
|
||||
inputSchema: jsonSchema<EditMessageInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
channel_id: { type: 'string', description: 'The ID of the channel containing the message.' },
|
||||
message_id: { type: 'string', description: 'The ID of the message to edit.' },
|
||||
content: { type: 'string', description: 'The new message content (up to 2000 characters).' },
|
||||
},
|
||||
required: ['channel_id', 'message_id', 'content'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: EditMessageInput): Promise<DiscordMessage> {
|
||||
try {
|
||||
if (!input.channel_id || !input.message_id || !input.content) {
|
||||
throw new Error('channel_id, message_id, and content are required and must be non-empty');
|
||||
}
|
||||
if (input.content.length > 2000) {
|
||||
throw new Error('Message content must be 2000 characters or less');
|
||||
}
|
||||
return await apiRequest<DiscordMessage>(
|
||||
'PATCH',
|
||||
`/channels/${input.channel_id}/messages/${input.message_id}`,
|
||||
{ content: input.content }
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to edit message: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export interface DeleteMessageInput {
|
||||
channel_id: string;
|
||||
message_id: string;
|
||||
}
|
||||
|
||||
export const deleteMessage = tool({
|
||||
description: 'Delete a message from a Discord channel. Requires appropriate permissions.',
|
||||
inputSchema: jsonSchema<DeleteMessageInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
channel_id: { type: 'string', description: 'The ID of the channel containing the message.' },
|
||||
message_id: { type: 'string', description: 'The ID of the message to delete.' },
|
||||
},
|
||||
required: ['channel_id', 'message_id'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: DeleteMessageInput): Promise<SuccessResult> {
|
||||
try {
|
||||
if (!input.channel_id || !input.message_id) {
|
||||
throw new Error('channel_id and message_id are required');
|
||||
}
|
||||
return await apiRequest<SuccessResult>(
|
||||
'DELETE',
|
||||
`/channels/${input.channel_id}/messages/${input.message_id}`
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to delete message: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export interface PinMessageInput {
|
||||
channel_id: string;
|
||||
message_id: string;
|
||||
}
|
||||
|
||||
export const pinMessage = tool({
|
||||
description: 'Pin a message in a Discord channel. Maximum 50 pinned messages per channel.',
|
||||
inputSchema: jsonSchema<PinMessageInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
channel_id: { type: 'string', description: 'The ID of the channel containing the message.' },
|
||||
message_id: { type: 'string', description: 'The ID of the message to pin.' },
|
||||
},
|
||||
required: ['channel_id', 'message_id'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: PinMessageInput): Promise<SuccessResult> {
|
||||
try {
|
||||
if (!input.channel_id || !input.message_id) {
|
||||
throw new Error('channel_id and message_id are required');
|
||||
}
|
||||
return await apiRequest<SuccessResult>(
|
||||
'PUT',
|
||||
`/channels/${input.channel_id}/pins/${input.message_id}`
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to pin message: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Reactions ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface AddReactionInput {
|
||||
channel_id: string;
|
||||
message_id: string;
|
||||
emoji: string;
|
||||
}
|
||||
|
||||
export const addReaction = tool({
|
||||
description:
|
||||
'Add a reaction emoji to a message. Use URL-encoded emoji like %F0%9F%91%8D or custom emoji format name:id.',
|
||||
inputSchema: jsonSchema<AddReactionInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
channel_id: { type: 'string', description: 'The ID of the channel containing the message.' },
|
||||
message_id: { type: 'string', description: 'The ID of the message to react to.' },
|
||||
emoji: {
|
||||
type: 'string',
|
||||
description:
|
||||
'URL-encoded emoji (e.g., %F0%9F%91%8D for thumbs up) or custom emoji in name:id format.',
|
||||
},
|
||||
},
|
||||
required: ['channel_id', 'message_id', 'emoji'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: AddReactionInput): Promise<SuccessResult> {
|
||||
try {
|
||||
if (!input.channel_id || !input.message_id || !input.emoji) {
|
||||
throw new Error('channel_id, message_id, and emoji are required');
|
||||
}
|
||||
return await apiRequest<SuccessResult>(
|
||||
'PUT',
|
||||
`/channels/${input.channel_id}/messages/${input.message_id}/reactions/${input.emoji}/@me`
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to add reaction: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Guilds ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface ListGuildsInput {
|
||||
limit?: number;
|
||||
before?: string;
|
||||
after?: string;
|
||||
}
|
||||
|
||||
export const listGuilds = tool({
|
||||
description:
|
||||
'List all guilds (servers) the bot is a member of with optional pagination using guild IDs.',
|
||||
inputSchema: jsonSchema<ListGuildsInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'number', description: 'Number of guilds to retrieve (1-200, default: 200).' },
|
||||
before: { type: 'string', description: 'Get guilds before this guild ID.' },
|
||||
after: { type: 'string', description: 'Get guilds after this guild ID.' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: ListGuildsInput): Promise<DiscordGuild[]> {
|
||||
try {
|
||||
if (input.limit !== undefined && (input.limit < 1 || input.limit > 200)) {
|
||||
throw new Error('Limit must be between 1 and 200');
|
||||
}
|
||||
const qs = buildQueryString({
|
||||
limit: input.limit,
|
||||
before: input.before,
|
||||
after: input.after,
|
||||
});
|
||||
return await apiRequest<DiscordGuild[]>('GET', `/users/@me/guilds${qs}`);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to list guilds: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export interface GetGuildInput {
|
||||
guild_id: string;
|
||||
with_counts?: boolean;
|
||||
}
|
||||
|
||||
export const getGuild = tool({
|
||||
description:
|
||||
'Get detailed information about a Discord guild including roles, emojis, and features.',
|
||||
inputSchema: jsonSchema<GetGuildInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
guild_id: { type: 'string', description: 'The ID of the guild to retrieve.' },
|
||||
with_counts: {
|
||||
type: 'boolean',
|
||||
description: 'Include approximate member and presence counts (default: false).',
|
||||
},
|
||||
},
|
||||
required: ['guild_id'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: GetGuildInput): Promise<DiscordGuild> {
|
||||
try {
|
||||
if (!input.guild_id) {
|
||||
throw new Error('guild_id is required');
|
||||
}
|
||||
const qs = buildQueryString({ with_counts: input.with_counts });
|
||||
return await apiRequest<DiscordGuild>('GET', `/guilds/${input.guild_id}${qs}`);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get guild: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Channels ───────────────────────────────────────────────────────────────
|
||||
|
||||
export interface ListChannelsInput {
|
||||
guild_id: string;
|
||||
}
|
||||
|
||||
export const listChannels = tool({
|
||||
description: 'List all channels in a Discord guild including text, voice, and category channels.',
|
||||
inputSchema: jsonSchema<ListChannelsInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
guild_id: { type: 'string', description: 'The ID of the guild to list channels from.' },
|
||||
},
|
||||
required: ['guild_id'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: ListChannelsInput): Promise<DiscordChannel[]> {
|
||||
try {
|
||||
if (!input.guild_id) {
|
||||
throw new Error('guild_id is required');
|
||||
}
|
||||
return await apiRequest<DiscordChannel[]>('GET', `/guilds/${input.guild_id}/channels`);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to list channels: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export interface GetChannelInput {
|
||||
channel_id: string;
|
||||
}
|
||||
|
||||
export const getChannel = tool({
|
||||
description: 'Get detailed information about a specific Discord channel.',
|
||||
inputSchema: jsonSchema<GetChannelInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
channel_id: { type: 'string', description: 'The ID of the channel to retrieve.' },
|
||||
},
|
||||
required: ['channel_id'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: GetChannelInput): Promise<DiscordChannel> {
|
||||
try {
|
||||
if (!input.channel_id) {
|
||||
throw new Error('channel_id is required');
|
||||
}
|
||||
return await apiRequest<DiscordChannel>('GET', `/channels/${input.channel_id}`);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get channel: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export interface CreateChannelInput {
|
||||
guild_id: string;
|
||||
name: string;
|
||||
type?: number;
|
||||
topic?: string;
|
||||
parent_id?: string;
|
||||
}
|
||||
|
||||
const VALID_CHANNEL_TYPES = [0, 2, 4, 5];
|
||||
|
||||
export const createChannel = tool({
|
||||
description:
|
||||
'Create a new channel in a Discord guild. Channel types: 0=text, 2=voice, 4=category, 5=announcement.',
|
||||
inputSchema: jsonSchema<CreateChannelInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
guild_id: { type: 'string', description: 'The ID of the guild to create the channel in.' },
|
||||
name: { type: 'string', description: 'The name of the channel (1-100 characters).' },
|
||||
type: {
|
||||
type: 'number',
|
||||
description: 'Channel type: 0=text, 2=voice, 4=category, 5=announcement (default: 0).',
|
||||
},
|
||||
topic: {
|
||||
type: 'string',
|
||||
description: 'Channel topic (0-1024 characters, text channels only).',
|
||||
},
|
||||
parent_id: {
|
||||
type: 'string',
|
||||
description: 'ID of the parent category for the channel.',
|
||||
},
|
||||
},
|
||||
required: ['guild_id', 'name'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: CreateChannelInput): Promise<DiscordChannel> {
|
||||
try {
|
||||
if (!input.guild_id || !input.name) {
|
||||
throw new Error('guild_id and name are required');
|
||||
}
|
||||
if (input.name.length < 1 || input.name.length > 100) {
|
||||
throw new Error('Channel name must be between 1 and 100 characters');
|
||||
}
|
||||
if (input.type !== undefined && !VALID_CHANNEL_TYPES.includes(input.type)) {
|
||||
throw new Error(
|
||||
'Channel type must be 0 (text), 2 (voice), 4 (category), or 5 (announcement)'
|
||||
);
|
||||
}
|
||||
if (input.topic !== undefined && input.topic.length > 1024) {
|
||||
throw new Error('Topic must be 1024 characters or less');
|
||||
}
|
||||
return await apiRequest<DiscordChannel>('POST', `/guilds/${input.guild_id}/channels`, {
|
||||
name: input.name,
|
||||
type: input.type ?? 0,
|
||||
topic: input.topic,
|
||||
parent_id: input.parent_id,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create channel: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Members ────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface ListMembersInput {
|
||||
guild_id: string;
|
||||
limit?: number;
|
||||
after?: string;
|
||||
}
|
||||
|
||||
export const listMembers = tool({
|
||||
description: 'List members of a Discord guild with optional pagination using user IDs.',
|
||||
inputSchema: jsonSchema<ListMembersInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
guild_id: { type: 'string', description: 'The ID of the guild to list members from.' },
|
||||
limit: { type: 'number', description: 'Number of members to retrieve (1-1000, default: 1).' },
|
||||
after: { type: 'string', description: 'Get members after this user ID.' },
|
||||
},
|
||||
required: ['guild_id'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: ListMembersInput): Promise<DiscordMember[]> {
|
||||
try {
|
||||
if (!input.guild_id) {
|
||||
throw new Error('guild_id is required');
|
||||
}
|
||||
if (input.limit !== undefined && (input.limit < 1 || input.limit > 1000)) {
|
||||
throw new Error('Limit must be between 1 and 1000');
|
||||
}
|
||||
const qs = buildQueryString({
|
||||
limit: input.limit,
|
||||
after: input.after,
|
||||
});
|
||||
return await apiRequest<DiscordMember[]>('GET', `/guilds/${input.guild_id}/members${qs}`);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to list members: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export interface GetMemberInput {
|
||||
guild_id: string;
|
||||
user_id: string;
|
||||
}
|
||||
|
||||
export const getMember = tool({
|
||||
description:
|
||||
'Get detailed information about a specific member in a guild including roles and join date.',
|
||||
inputSchema: jsonSchema<GetMemberInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
guild_id: { type: 'string', description: 'The ID of the guild.' },
|
||||
user_id: { type: 'string', description: 'The ID of the user to get member info for.' },
|
||||
},
|
||||
required: ['guild_id', 'user_id'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: GetMemberInput): Promise<DiscordMember> {
|
||||
try {
|
||||
if (!input.guild_id || !input.user_id) {
|
||||
throw new Error('guild_id and user_id are required');
|
||||
}
|
||||
return await apiRequest<DiscordMember>(
|
||||
'GET',
|
||||
`/guilds/${input.guild_id}/members/${input.user_id}`
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get member: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Threads ────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface CreateThreadInput {
|
||||
channel_id: string;
|
||||
message_id: string;
|
||||
name: string;
|
||||
auto_archive_duration?: number;
|
||||
}
|
||||
|
||||
const VALID_ARCHIVE_DURATIONS = [60, 1440, 4320, 10080];
|
||||
|
||||
export const createThread = tool({
|
||||
description:
|
||||
'Create a thread from an existing message. Auto-archive durations: 60 (1h), 1440 (1d), 4320 (3d), 10080 (7d) minutes.',
|
||||
inputSchema: jsonSchema<CreateThreadInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
channel_id: { type: 'string', description: 'The ID of the channel containing the message.' },
|
||||
message_id: { type: 'string', description: 'The ID of the message to create a thread from.' },
|
||||
name: { type: 'string', description: 'The name of the thread (1-100 characters).' },
|
||||
auto_archive_duration: {
|
||||
type: 'number',
|
||||
description:
|
||||
'Duration in minutes to auto-archive: 60 (1 hour), 1440 (1 day), 4320 (3 days), or 10080 (7 days). Default: 1440.',
|
||||
},
|
||||
},
|
||||
required: ['channel_id', 'message_id', 'name'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: CreateThreadInput): Promise<DiscordThread> {
|
||||
try {
|
||||
if (!input.channel_id || !input.message_id || !input.name) {
|
||||
throw new Error('channel_id, message_id, and name are required');
|
||||
}
|
||||
if (input.name.length < 1 || input.name.length > 100) {
|
||||
throw new Error('Thread name must be between 1 and 100 characters');
|
||||
}
|
||||
if (
|
||||
input.auto_archive_duration !== undefined &&
|
||||
!VALID_ARCHIVE_DURATIONS.includes(input.auto_archive_duration)
|
||||
) {
|
||||
throw new Error('auto_archive_duration must be 60, 1440, 4320, or 10080');
|
||||
}
|
||||
return await apiRequest<DiscordThread>(
|
||||
'POST',
|
||||
`/channels/${input.channel_id}/messages/${input.message_id}/threads`,
|
||||
{
|
||||
name: input.name,
|
||||
auto_archive_duration: input.auto_archive_duration,
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create thread: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export interface ListThreadsInput {
|
||||
guild_id: string;
|
||||
}
|
||||
|
||||
export const listThreads = tool({
|
||||
description: 'List all active threads in a Discord guild across all channels.',
|
||||
inputSchema: jsonSchema<ListThreadsInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
guild_id: { type: 'string', description: 'The ID of the guild to list active threads from.' },
|
||||
},
|
||||
required: ['guild_id'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: ListThreadsInput): Promise<ListThreadsResult> {
|
||||
try {
|
||||
if (!input.guild_id) {
|
||||
throw new Error('guild_id is required');
|
||||
}
|
||||
return await apiRequest<ListThreadsResult>('GET', `/guilds/${input.guild_id}/threads/active`);
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to list threads: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Default Export ─────────────────────────────────────────────────────────
|
||||
|
||||
export default {
|
||||
// Messages
|
||||
sendMessage,
|
||||
listMessages,
|
||||
editMessage,
|
||||
deleteMessage,
|
||||
pinMessage,
|
||||
// Reactions
|
||||
addReaction,
|
||||
// Guilds
|
||||
listGuilds,
|
||||
getGuild,
|
||||
// Channels
|
||||
listChannels,
|
||||
getChannel,
|
||||
createChannel,
|
||||
// Members
|
||||
listMembers,
|
||||
getMember,
|
||||
// Threads
|
||||
createThread,
|
||||
listThreads,
|
||||
};
|
||||
11
packages/tools/official/discord/tsconfig.json
Normal file
11
packages/tools/official/discord/tsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "@tpmjs/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"incremental": false,
|
||||
"composite": false
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
10
packages/tools/official/discord/tsup.config.ts
Normal file
10
packages/tools/official/discord/tsup.config.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
clean: true,
|
||||
treeshake: true,
|
||||
splitting: false,
|
||||
});
|
||||
43
packages/tools/official/slack/README.md
Normal file
43
packages/tools/official/slack/README.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# @tpmjs/tools-slack
|
||||
|
||||
Slack API tools for AI agents. Send messages, manage channels, list users, search messages, upload files, and more.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @tpmjs/tools-slack
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
Set the `SLACK_BOT_TOKEN` environment variable. Get your token from [Slack API Apps](https://api.slack.com/apps).
|
||||
|
||||
Required bot scopes: `chat:write`, `channels:read`, `channels:history`, `users:read`, `reactions:write`, `files:write`, `search:read`.
|
||||
|
||||
## Usage
|
||||
|
||||
```typescript
|
||||
import { sendMessage, listChannels } from '@tpmjs/tools-slack';
|
||||
|
||||
const result = await sendMessage.execute({ channel: '#general', text: 'Hello from AI!' });
|
||||
const channels = await listChannels.execute({});
|
||||
```
|
||||
|
||||
## Tools
|
||||
|
||||
| Tool | Description |
|
||||
|------|-------------|
|
||||
| sendMessage | Send a message to a channel or thread |
|
||||
| listChannels | List workspace channels by type |
|
||||
| getChannel | Get channel details |
|
||||
| listUsers | List workspace users |
|
||||
| getUser | Get user profile details |
|
||||
| addReaction | Add emoji reaction to a message |
|
||||
| uploadFile | Upload a text file or snippet |
|
||||
| setChannelTopic | Set a channel's topic |
|
||||
| listMessages | Get recent messages from a channel |
|
||||
| searchMessages | Search messages across the workspace |
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
94
packages/tools/official/slack/package.json
Normal file
94
packages/tools/official/slack/package.json
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
{
|
||||
"name": "@tpmjs/tools-slack",
|
||||
"version": "0.1.0",
|
||||
"description": "Slack API tools for AI agents. Send messages, manage channels, list users, search messages, upload files, and more.",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"tpmjs",
|
||||
"slack",
|
||||
"messaging",
|
||||
"ops",
|
||||
"agent"
|
||||
],
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"default": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"type-check": "tsc --noEmit",
|
||||
"clean": "rm -rf dist .turbo"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tpmjs/tsconfig": "workspace:*",
|
||||
"tsup": "^8.5.1",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"ai": "6.0.49"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/tpmjs/tpmjs.git",
|
||||
"directory": "packages/tools/official/slack"
|
||||
},
|
||||
"homepage": "https://tpmjs.com",
|
||||
"license": "MIT",
|
||||
"tpmjs": {
|
||||
"category": "ops",
|
||||
"frameworks": [
|
||||
"vercel-ai"
|
||||
],
|
||||
"tools": [
|
||||
{
|
||||
"name": "sendMessage",
|
||||
"description": "Send a message to a Slack channel or thread."
|
||||
},
|
||||
{
|
||||
"name": "listChannels",
|
||||
"description": "List public and private channels in a Slack workspace."
|
||||
},
|
||||
{
|
||||
"name": "getChannel",
|
||||
"description": "Get detailed information about a specific Slack channel."
|
||||
},
|
||||
{
|
||||
"name": "listUsers",
|
||||
"description": "List all users in a Slack workspace."
|
||||
},
|
||||
{
|
||||
"name": "getUser",
|
||||
"description": "Get profile details of a specific Slack user."
|
||||
},
|
||||
{
|
||||
"name": "addReaction",
|
||||
"description": "Add an emoji reaction to a Slack message."
|
||||
},
|
||||
{
|
||||
"name": "uploadFile",
|
||||
"description": "Upload a text file or snippet to a Slack channel."
|
||||
},
|
||||
{
|
||||
"name": "setChannelTopic",
|
||||
"description": "Set the topic of a Slack channel."
|
||||
},
|
||||
{
|
||||
"name": "listMessages",
|
||||
"description": "Retrieve recent messages from a Slack channel."
|
||||
},
|
||||
{
|
||||
"name": "searchMessages",
|
||||
"description": "Search for messages across a Slack workspace."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
598
packages/tools/official/slack/src/index.ts
Normal file
598
packages/tools/official/slack/src/index.ts
Normal file
|
|
@ -0,0 +1,598 @@
|
|||
/**
|
||||
* @tpmjs/tools-slack — Slack API Tools for AI Agents
|
||||
*
|
||||
* Full access to the Slack Web API: send messages, manage channels, users,
|
||||
* reactions, file uploads, and search.
|
||||
*
|
||||
* @requires SLACK_BOT_TOKEN environment variable
|
||||
*/
|
||||
|
||||
import { jsonSchema, tool } from 'ai';
|
||||
|
||||
const BASE_URL = 'https://slack.com/api';
|
||||
|
||||
// ─── Client Infrastructure ──────────────────────────────────────────────────
|
||||
|
||||
function getApiKey(): string {
|
||||
const key = process.env.SLACK_BOT_TOKEN;
|
||||
if (!key) {
|
||||
throw new Error(
|
||||
'SLACK_BOT_TOKEN environment variable is required. Get your token from https://api.slack.com/apps'
|
||||
);
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
async function apiRequest<T>(method: string, body?: unknown): Promise<T> {
|
||||
const token = getApiKey();
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
const options: RequestInit = {
|
||||
method: 'POST',
|
||||
headers,
|
||||
};
|
||||
|
||||
if (body !== undefined) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const response = await fetch(`${BASE_URL}/${method}`, options);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Slack HTTP error ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { ok: boolean; error?: string } & T;
|
||||
|
||||
if (!data.ok) {
|
||||
throw new Error(`Slack API error: ${data.error || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function apiGetRequest<T>(method: string, params: Record<string, unknown>): Promise<T> {
|
||||
const token = getApiKey();
|
||||
|
||||
const qs = buildQueryString(params);
|
||||
const url = `${BASE_URL}/${method}${qs}`;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${token}`,
|
||||
};
|
||||
|
||||
const response = await fetch(url, { method: 'GET', headers });
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Slack HTTP error ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { ok: boolean; error?: string } & T;
|
||||
|
||||
if (!data.ok) {
|
||||
throw new Error(`Slack API error: ${data.error || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function buildQueryString(params: Record<string, unknown>): string {
|
||||
const entries = Object.entries(params).filter(
|
||||
([, v]) => v !== undefined && v !== null && v !== ''
|
||||
);
|
||||
if (entries.length === 0) return '';
|
||||
return (
|
||||
'?' +
|
||||
entries.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`).join('&')
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Output Types ────────────────────────────────────────────────────────────
|
||||
|
||||
export interface SlackMessage {
|
||||
ts: string;
|
||||
channel: string;
|
||||
text: string;
|
||||
user?: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface SlackChannel {
|
||||
id: string;
|
||||
name: string;
|
||||
is_channel: boolean;
|
||||
is_private: boolean;
|
||||
topic: { value: string };
|
||||
purpose: { value: string };
|
||||
num_members: number;
|
||||
}
|
||||
|
||||
export interface SlackUser {
|
||||
id: string;
|
||||
name: string;
|
||||
real_name: string;
|
||||
is_bot: boolean;
|
||||
deleted: boolean;
|
||||
profile: { email?: string; display_name?: string; status_text?: string };
|
||||
}
|
||||
|
||||
export interface SlackFile {
|
||||
id: string;
|
||||
name: string;
|
||||
title: string;
|
||||
filetype: string;
|
||||
size: number;
|
||||
url_private: string;
|
||||
}
|
||||
|
||||
export interface SendMessageResult {
|
||||
ok: boolean;
|
||||
channel: string;
|
||||
ts: string;
|
||||
message: SlackMessage;
|
||||
}
|
||||
|
||||
export interface ListMessagesResult {
|
||||
ok: boolean;
|
||||
messages: SlackMessage[];
|
||||
has_more: boolean;
|
||||
response_metadata?: { next_cursor: string };
|
||||
}
|
||||
|
||||
export interface SearchMessagesResult {
|
||||
ok: boolean;
|
||||
messages: {
|
||||
total: number;
|
||||
matches: SlackMessage[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ListChannelsResult {
|
||||
ok: boolean;
|
||||
channels: SlackChannel[];
|
||||
response_metadata?: { next_cursor: string };
|
||||
}
|
||||
|
||||
export interface GetChannelResult {
|
||||
ok: boolean;
|
||||
channel: SlackChannel;
|
||||
}
|
||||
|
||||
export interface ListUsersResult {
|
||||
ok: boolean;
|
||||
members: SlackUser[];
|
||||
response_metadata?: { next_cursor: string };
|
||||
}
|
||||
|
||||
export interface GetUserResult {
|
||||
ok: boolean;
|
||||
user: SlackUser;
|
||||
}
|
||||
|
||||
export interface AddReactionResult {
|
||||
ok: boolean;
|
||||
}
|
||||
|
||||
export interface UploadFileResult {
|
||||
ok: boolean;
|
||||
file: SlackFile;
|
||||
}
|
||||
|
||||
export interface SetChannelTopicResult {
|
||||
ok: boolean;
|
||||
topic: string;
|
||||
}
|
||||
|
||||
// ─── Messages ───────────────────────────────────────────────────────────────
|
||||
|
||||
export interface SendMessageInput {
|
||||
channel: string;
|
||||
text: string;
|
||||
thread_ts?: string;
|
||||
unfurl_links?: boolean;
|
||||
}
|
||||
|
||||
export const sendMessage = tool({
|
||||
description: 'Send a message to a Slack channel or thread. Supports Slack markdown formatting.',
|
||||
inputSchema: jsonSchema<SendMessageInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
channel: {
|
||||
type: 'string',
|
||||
description: 'Channel ID or name (e.g., "C1234567890" or "#general").',
|
||||
},
|
||||
text: { type: 'string', description: 'Message text (supports Slack markdown).' },
|
||||
thread_ts: { type: 'string', description: 'Optional thread timestamp to reply in a thread.' },
|
||||
unfurl_links: {
|
||||
type: 'boolean',
|
||||
description: 'Enable or disable link unfurling (default: true).',
|
||||
},
|
||||
},
|
||||
required: ['channel', 'text'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: SendMessageInput): Promise<SendMessageResult> {
|
||||
try {
|
||||
if (!input.channel || !input.text) {
|
||||
throw new Error('Channel and text are required and must be non-empty');
|
||||
}
|
||||
return await apiRequest<SendMessageResult>('chat.postMessage', {
|
||||
channel: input.channel,
|
||||
text: input.text,
|
||||
thread_ts: input.thread_ts,
|
||||
unfurl_links: input.unfurl_links,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to send message: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export interface ListMessagesInput {
|
||||
channel: string;
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
oldest?: string;
|
||||
latest?: string;
|
||||
}
|
||||
|
||||
export const listMessages = tool({
|
||||
description: 'Get recent messages from a channel with optional pagination and time filtering.',
|
||||
inputSchema: jsonSchema<ListMessagesInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
channel: { type: 'string', description: 'Channel ID.' },
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Number of messages to return (1-1000, default: 100).',
|
||||
},
|
||||
cursor: { type: 'string', description: 'Pagination cursor from previous response.' },
|
||||
oldest: {
|
||||
type: 'string',
|
||||
description: 'Only messages after this Unix timestamp (e.g., "1234567890.123456").',
|
||||
},
|
||||
latest: {
|
||||
type: 'string',
|
||||
description: 'Only messages before this Unix timestamp (e.g., "1234567890.123456").',
|
||||
},
|
||||
},
|
||||
required: ['channel'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: ListMessagesInput): Promise<ListMessagesResult> {
|
||||
try {
|
||||
if (!input.channel) {
|
||||
throw new Error('Channel is required');
|
||||
}
|
||||
if (input.limit !== undefined && (input.limit < 1 || input.limit > 1000)) {
|
||||
throw new Error('Limit must be between 1 and 1000');
|
||||
}
|
||||
return await apiGetRequest<ListMessagesResult>('conversations.history', {
|
||||
channel: input.channel,
|
||||
limit: input.limit,
|
||||
cursor: input.cursor,
|
||||
oldest: input.oldest,
|
||||
latest: input.latest,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to list messages: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export interface SearchMessagesInput {
|
||||
query: string;
|
||||
sort?: string;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
export const searchMessages = tool({
|
||||
description: 'Search for messages across all channels in the workspace using keyword queries.',
|
||||
inputSchema: jsonSchema<SearchMessagesInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Search query (supports operators like from:, in:, has:).',
|
||||
},
|
||||
sort: {
|
||||
type: 'string',
|
||||
description: 'Sort by: score (relevance) or timestamp (default: score).',
|
||||
},
|
||||
count: { type: 'number', description: 'Number of results to return (1-100, default: 20).' },
|
||||
},
|
||||
required: ['query'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: SearchMessagesInput): Promise<SearchMessagesResult> {
|
||||
try {
|
||||
if (!input.query) {
|
||||
throw new Error('Query is required and must be non-empty');
|
||||
}
|
||||
if (input.count !== undefined && (input.count < 1 || input.count > 100)) {
|
||||
throw new Error('Count must be between 1 and 100');
|
||||
}
|
||||
if (input.sort !== undefined && input.sort !== 'score' && input.sort !== 'timestamp') {
|
||||
throw new Error('Sort must be "score" or "timestamp"');
|
||||
}
|
||||
return await apiGetRequest<SearchMessagesResult>('search.messages', {
|
||||
query: input.query,
|
||||
sort: input.sort,
|
||||
count: input.count,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to search messages: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Channels ───────────────────────────────────────────────────────────────
|
||||
|
||||
export interface ListChannelsInput {
|
||||
types?: string;
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
}
|
||||
|
||||
export const listChannels = tool({
|
||||
description: 'List channels in the workspace. Defaults to public channels if no types specified.',
|
||||
inputSchema: jsonSchema<ListChannelsInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
types: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Comma-separated channel types: public_channel, private_channel, mpim, im (default: public_channel).',
|
||||
},
|
||||
limit: {
|
||||
type: 'number',
|
||||
description: 'Number of channels to return (1-1000, default: 100).',
|
||||
},
|
||||
cursor: { type: 'string', description: 'Pagination cursor from previous response.' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: ListChannelsInput): Promise<ListChannelsResult> {
|
||||
try {
|
||||
if (input.limit !== undefined && (input.limit < 1 || input.limit > 1000)) {
|
||||
throw new Error('Limit must be between 1 and 1000');
|
||||
}
|
||||
return await apiGetRequest<ListChannelsResult>('conversations.list', {
|
||||
types: input.types || 'public_channel',
|
||||
limit: input.limit,
|
||||
cursor: input.cursor,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to list channels: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export interface GetChannelInput {
|
||||
channel: string;
|
||||
}
|
||||
|
||||
export const getChannel = tool({
|
||||
description:
|
||||
'Get detailed information about a channel including name, topic, purpose, and member count.',
|
||||
inputSchema: jsonSchema<GetChannelInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
channel: { type: 'string', description: 'Channel ID (e.g., "C1234567890").' },
|
||||
},
|
||||
required: ['channel'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: GetChannelInput): Promise<GetChannelResult> {
|
||||
try {
|
||||
if (!input.channel) {
|
||||
throw new Error('Channel is required and must be non-empty');
|
||||
}
|
||||
return await apiGetRequest<GetChannelResult>('conversations.info', {
|
||||
channel: input.channel,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get channel: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export interface SetChannelTopicInput {
|
||||
channel: string;
|
||||
topic: string;
|
||||
}
|
||||
|
||||
export const setChannelTopic = tool({
|
||||
description: 'Set the topic for a channel. Requires appropriate permissions in the channel.',
|
||||
inputSchema: jsonSchema<SetChannelTopicInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
channel: { type: 'string', description: 'Channel ID.' },
|
||||
topic: { type: 'string', description: 'New topic text (max 250 characters).' },
|
||||
},
|
||||
required: ['channel', 'topic'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: SetChannelTopicInput): Promise<SetChannelTopicResult> {
|
||||
try {
|
||||
if (!input.channel || !input.topic) {
|
||||
throw new Error('Channel and topic are required');
|
||||
}
|
||||
if (input.topic.length > 250) {
|
||||
throw new Error('Topic must be 250 characters or less');
|
||||
}
|
||||
return await apiRequest<SetChannelTopicResult>('conversations.setTopic', {
|
||||
channel: input.channel,
|
||||
topic: input.topic,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to set channel topic: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Users ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface ListUsersInput {
|
||||
limit?: number;
|
||||
cursor?: string;
|
||||
}
|
||||
|
||||
export const listUsers = tool({
|
||||
description: 'List all users in the workspace including bots and deactivated users.',
|
||||
inputSchema: jsonSchema<ListUsersInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
limit: { type: 'number', description: 'Number of users to return (1-1000, default: 100).' },
|
||||
cursor: { type: 'string', description: 'Pagination cursor from previous response.' },
|
||||
},
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: ListUsersInput): Promise<ListUsersResult> {
|
||||
try {
|
||||
if (input.limit !== undefined && (input.limit < 1 || input.limit > 1000)) {
|
||||
throw new Error('Limit must be between 1 and 1000');
|
||||
}
|
||||
return await apiGetRequest<ListUsersResult>('users.list', {
|
||||
limit: input.limit,
|
||||
cursor: input.cursor,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to list users: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export interface GetUserInput {
|
||||
user: string;
|
||||
}
|
||||
|
||||
export const getUser = tool({
|
||||
description:
|
||||
'Get detailed profile information for a specific user including name, email, and status.',
|
||||
inputSchema: jsonSchema<GetUserInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
user: { type: 'string', description: 'User ID (e.g., "U1234567890").' },
|
||||
},
|
||||
required: ['user'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: GetUserInput): Promise<GetUserResult> {
|
||||
try {
|
||||
if (!input.user) {
|
||||
throw new Error('User ID is required and must be non-empty');
|
||||
}
|
||||
return await apiGetRequest<GetUserResult>('users.info', {
|
||||
user: input.user,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get user: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Reactions ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface AddReactionInput {
|
||||
channel: string;
|
||||
timestamp: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const addReaction = tool({
|
||||
description: 'Add an emoji reaction to a message. The emoji name should be without colons.',
|
||||
inputSchema: jsonSchema<AddReactionInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
channel: { type: 'string', description: 'Channel ID where the message is.' },
|
||||
timestamp: { type: 'string', description: 'Message timestamp (ts field from message).' },
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Emoji name without colons (e.g., "thumbsup", "fire", "rocket").',
|
||||
},
|
||||
},
|
||||
required: ['channel', 'timestamp', 'name'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: AddReactionInput): Promise<AddReactionResult> {
|
||||
try {
|
||||
if (!input.channel || !input.timestamp || !input.name) {
|
||||
throw new Error('Channel, timestamp, and name are required');
|
||||
}
|
||||
return await apiRequest<AddReactionResult>('reactions.add', {
|
||||
channel: input.channel,
|
||||
timestamp: input.timestamp,
|
||||
name: input.name,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to add reaction: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Files ──────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface UploadFileInput {
|
||||
channel_id: string;
|
||||
content: string;
|
||||
filename: string;
|
||||
title?: string;
|
||||
initial_comment?: string;
|
||||
}
|
||||
|
||||
export const uploadFile = tool({
|
||||
description: 'Upload a text file or snippet to a channel with optional title and comment.',
|
||||
inputSchema: jsonSchema<UploadFileInput>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
channel_id: { type: 'string', description: 'Channel ID to upload to.' },
|
||||
content: { type: 'string', description: 'File content (text).' },
|
||||
filename: { type: 'string', description: 'Filename (e.g., "code.js", "notes.txt").' },
|
||||
title: { type: 'string', description: 'Optional title for the file.' },
|
||||
initial_comment: { type: 'string', description: 'Optional message to post with the file.' },
|
||||
},
|
||||
required: ['channel_id', 'content', 'filename'],
|
||||
additionalProperties: false,
|
||||
}),
|
||||
async execute(input: UploadFileInput): Promise<UploadFileResult> {
|
||||
try {
|
||||
if (!input.channel_id || !input.content || !input.filename) {
|
||||
throw new Error('channel_id, content, and filename are required and must be non-empty');
|
||||
}
|
||||
return await apiRequest<UploadFileResult>('files.upload', {
|
||||
channels: input.channel_id,
|
||||
content: input.content,
|
||||
filename: input.filename,
|
||||
title: input.title,
|
||||
initial_comment: input.initial_comment,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to upload file: ${(error as Error).message}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// ─── Default Export ─────────────────────────────────────────────────────────
|
||||
|
||||
export default {
|
||||
// Messages
|
||||
sendMessage,
|
||||
listMessages,
|
||||
searchMessages,
|
||||
// Channels
|
||||
listChannels,
|
||||
getChannel,
|
||||
setChannelTopic,
|
||||
// Users
|
||||
listUsers,
|
||||
getUser,
|
||||
// Reactions
|
||||
addReaction,
|
||||
// Files
|
||||
uploadFile,
|
||||
};
|
||||
11
packages/tools/official/slack/tsconfig.json
Normal file
11
packages/tools/official/slack/tsconfig.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "@tpmjs/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"incremental": false,
|
||||
"composite": false
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
10
packages/tools/official/slack/tsup.config.ts
Normal file
10
packages/tools/official/slack/tsup.config.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/index.ts'],
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
clean: true,
|
||||
treeshake: true,
|
||||
splitting: false,
|
||||
});
|
||||
|
|
@ -30,6 +30,14 @@ export const TPMJS_CATEGORIES = [
|
|||
'automation',
|
||||
'ai-ml',
|
||||
'monitoring',
|
||||
// Business categories
|
||||
'finance',
|
||||
'legal',
|
||||
'hr',
|
||||
'marketing',
|
||||
'cx',
|
||||
'edu',
|
||||
'sales',
|
||||
// Aliases
|
||||
'doc',
|
||||
'text',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue