From cc69d98b6f1ccaa287d80af01fb38174da06fb8c Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Mon, 9 Feb 2026 20:41:51 +1000 Subject: [PATCH] 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 --- .claude/skills/tpmjs-tool-creator/SKILL.md | 8 +- lefthook.yml | 2 +- packages/tools/official/blocks.yml | 2098 +++++++++++++++++ packages/tools/official/discord/README.md | 48 + packages/tools/official/discord/package.json | 114 + packages/tools/official/discord/src/index.ts | 750 ++++++ packages/tools/official/discord/tsconfig.json | 11 + .../tools/official/discord/tsup.config.ts | 10 + packages/tools/official/slack/README.md | 43 + packages/tools/official/slack/package.json | 94 + packages/tools/official/slack/src/index.ts | 598 +++++ packages/tools/official/slack/tsconfig.json | 11 + packages/tools/official/slack/tsup.config.ts | 10 + packages/types/src/tpmjs.ts | 8 + 14 files changed, 3803 insertions(+), 2 deletions(-) create mode 100644 packages/tools/official/discord/README.md create mode 100644 packages/tools/official/discord/package.json create mode 100644 packages/tools/official/discord/src/index.ts create mode 100644 packages/tools/official/discord/tsconfig.json create mode 100644 packages/tools/official/discord/tsup.config.ts create mode 100644 packages/tools/official/slack/README.md create mode 100644 packages/tools/official/slack/package.json create mode 100644 packages/tools/official/slack/src/index.ts create mode 100644 packages/tools/official/slack/tsconfig.json create mode 100644 packages/tools/official/slack/tsup.config.ts diff --git a/.claude/skills/tpmjs-tool-creator/SKILL.md b/.claude/skills/tpmjs-tool-creator/SKILL.md index 590bc67..38d0c7b 100644 --- a/.claude/skills/tpmjs-tool-creator/SKILL.md +++ b/.claude/skills/tpmjs-tool-creator/SKILL.md @@ -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 # Validate (schema → shape → domain) pnpm blocks run --force # Force full validation (skip cache) pnpm blocks run --json # JSON output for debugging diff --git a/lefthook.yml b/lefthook.yml index e6d061d..f816df9 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -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: diff --git a/packages/tools/official/blocks.yml b/packages/tools/official/blocks.yml index 0f234a9..37b3914 100644 --- a/packages/tools/official/blocks.yml +++ b/packages/tools/official/blocks.yml @@ -11007,6 +11007,2104 @@ blocks: description: "Whether the action succeeded" measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + # ─── GitHub API ────────────────────────────────────────────────────────── + + ops.githubGetRepo: + type: utility + description: "Get details of a GitHub repository including stars, forks, and language." + path: "github" + domain_rules: + - id: api_integration + description: "Must call GitHub API GET /repos/{owner}/{repo} endpoint" + inputs: + - name: owner + type: string + description: "Repository owner" + - name: repo + type: string + description: "Repository name" + outputs: + - name: repository + type: object + description: "Repository details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.githubListRepos: + type: utility + description: "List repositories for a user or organization with optional filtering." + path: "github" + domain_rules: + - id: api_integration + description: "Must call GitHub API GET /users/{username}/repos endpoint" + inputs: + - name: username + type: string + description: "GitHub username" + - name: sort + type: string + optional: true + description: "Sort field" + - name: per_page + type: number + optional: true + description: "Results per page" + outputs: + - name: repositories + type: array + description: "Array of repository objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.githubCreateIssue: + type: utility + description: "Create a new issue on a GitHub repository." + path: "github" + domain_rules: + - id: api_integration + description: "Must call GitHub API POST /repos/{owner}/{repo}/issues endpoint" + inputs: + - name: owner + type: string + description: "Repository owner" + - name: repo + type: string + description: "Repository name" + - name: title + type: string + description: "Issue title" + - name: body + type: string + optional: true + description: "Issue body" + outputs: + - name: issue + type: object + description: "Created issue" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.githubGetIssue: + type: utility + description: "Get details of a specific issue by number." + path: "github" + domain_rules: + - id: api_integration + description: "Must call GitHub API GET /repos/{owner}/{repo}/issues/{issue_number} endpoint" + inputs: + - name: owner + type: string + description: "Repository owner" + - name: repo + type: string + description: "Repository name" + - name: issue_number + type: number + description: "Issue number" + outputs: + - name: issue + type: object + description: "Issue details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.githubListIssues: + type: utility + description: "List issues on a repository with state and label filters." + path: "github" + domain_rules: + - id: api_integration + description: "Must call GitHub API GET /repos/{owner}/{repo}/issues endpoint" + inputs: + - name: owner + type: string + description: "Repository owner" + - name: repo + type: string + description: "Repository name" + - name: state + type: string + optional: true + description: "Filter by state" + - name: per_page + type: number + optional: true + description: "Results per page" + outputs: + - name: issues + type: array + description: "Array of issue objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.githubCreateIssueComment: + type: utility + description: "Add a comment to an existing GitHub issue or pull request." + path: "github" + domain_rules: + - id: api_integration + description: "Must call GitHub API POST /repos/{owner}/{repo}/issues/{issue_number}/comments endpoint" + inputs: + - name: owner + type: string + description: "Repository owner" + - name: repo + type: string + description: "Repository name" + - name: issue_number + type: number + description: "Issue number" + - name: body + type: string + description: "Comment body" + outputs: + - name: comment + type: object + description: "Created comment" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.githubGetPullRequest: + type: utility + description: "Get details of a specific pull request by number." + path: "github" + domain_rules: + - id: api_integration + description: "Must call GitHub API GET /repos/{owner}/{repo}/pulls/{pull_number} endpoint" + inputs: + - name: owner + type: string + description: "Repository owner" + - name: repo + type: string + description: "Repository name" + - name: pull_number + type: number + description: "Pull request number" + outputs: + - name: pullRequest + type: object + description: "Pull request details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.githubListPullRequests: + type: utility + description: "List pull requests on a repository with state and head/base filters." + path: "github" + domain_rules: + - id: api_integration + description: "Must call GitHub API GET /repos/{owner}/{repo}/pulls endpoint" + inputs: + - name: owner + type: string + description: "Repository owner" + - name: repo + type: string + description: "Repository name" + - name: state + type: string + optional: true + description: "Filter by state" + - name: per_page + type: number + optional: true + description: "Results per page" + outputs: + - name: pullRequests + type: array + description: "Array of pull request objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.githubGetFileContent: + type: utility + description: "Get the contents of a file or directory from a GitHub repository." + path: "github" + domain_rules: + - id: api_integration + description: "Must call GitHub API GET /repos/{owner}/{repo}/contents/{path} endpoint" + inputs: + - name: owner + type: string + description: "Repository owner" + - name: repo + type: string + description: "Repository name" + - name: path + type: string + description: "File path" + - name: ref + type: string + optional: true + description: "Branch or commit ref" + outputs: + - name: content + type: object + description: "File content or directory listing" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.githubSearchCode: + type: utility + description: "Search for code across GitHub repositories." + path: "github" + domain_rules: + - id: api_integration + description: "Must call GitHub API GET /search/code endpoint" + inputs: + - name: q + type: string + description: "Search query" + - name: per_page + type: number + optional: true + description: "Results per page" + outputs: + - name: results + type: object + description: "Search results with items" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.githubSearchRepositories: + type: utility + description: "Search GitHub repositories by query with sort and order options." + path: "github" + domain_rules: + - id: api_integration + description: "Must call GitHub API GET /search/repositories endpoint" + inputs: + - name: q + type: string + description: "Search query" + - name: sort + type: string + optional: true + description: "Sort field" + - name: per_page + type: number + optional: true + description: "Results per page" + outputs: + - name: results + type: object + description: "Search results with items" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.githubListBranches: + type: utility + description: "List branches on a GitHub repository." + path: "github" + domain_rules: + - id: api_integration + description: "Must call GitHub API GET /repos/{owner}/{repo}/branches endpoint" + inputs: + - name: owner + type: string + description: "Repository owner" + - name: repo + type: string + description: "Repository name" + - name: per_page + type: number + optional: true + description: "Results per page" + outputs: + - name: branches + type: array + description: "Array of branch objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.githubGetCommit: + type: utility + description: "Get details of a specific commit by SHA." + path: "github" + domain_rules: + - id: api_integration + description: "Must call GitHub API GET /repos/{owner}/{repo}/commits/{ref} endpoint" + inputs: + - name: owner + type: string + description: "Repository owner" + - name: repo + type: string + description: "Repository name" + - name: ref + type: string + description: "Commit SHA or ref" + outputs: + - name: commit + type: object + description: "Commit details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.githubListCommits: + type: utility + description: "List commits on a repository with optional path and author filters." + path: "github" + domain_rules: + - id: api_integration + description: "Must call GitHub API GET /repos/{owner}/{repo}/commits endpoint" + inputs: + - name: owner + type: string + description: "Repository owner" + - name: repo + type: string + description: "Repository name" + - name: path + type: string + optional: true + description: "File path filter" + - name: author + type: string + optional: true + description: "Author filter" + - name: per_page + type: number + optional: true + description: "Results per page" + outputs: + - name: commits + type: array + description: "Array of commit objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.githubListReleases: + type: utility + description: "List releases on a GitHub repository." + path: "github" + domain_rules: + - id: api_integration + description: "Must call GitHub API GET /repos/{owner}/{repo}/releases endpoint" + inputs: + - name: owner + type: string + description: "Repository owner" + - name: repo + type: string + description: "Repository name" + - name: per_page + type: number + optional: true + description: "Results per page" + outputs: + - name: releases + type: array + description: "Array of release objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.githubGetRelease: + type: utility + description: "Get details of a specific release by tag name." + path: "github" + domain_rules: + - id: api_integration + description: "Must call GitHub API GET /repos/{owner}/{repo}/releases/tags/{tag} endpoint" + inputs: + - name: owner + type: string + description: "Repository owner" + - name: repo + type: string + description: "Repository name" + - name: tag + type: string + description: "Release tag name" + outputs: + - name: release + type: object + description: "Release details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.githubCreateGist: + type: utility + description: "Create a new GitHub Gist with one or more files." + path: "github" + domain_rules: + - id: api_integration + description: "Must call GitHub API POST /gists endpoint" + inputs: + - name: files + type: object + description: "Files to include in gist" + - name: description + type: string + optional: true + description: "Gist description" + - name: public + type: boolean + optional: true + description: "Whether gist is public" + outputs: + - name: gist + type: object + description: "Created gist" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.githubListGists: + type: utility + description: "List gists for the authenticated user or a specific user." + path: "github" + domain_rules: + - id: api_integration + description: "Must call GitHub API GET /gists or GET /users/{username}/gists endpoint" + inputs: + - name: username + type: string + optional: true + description: "GitHub username" + - name: per_page + type: number + optional: true + description: "Results per page" + outputs: + - name: gists + type: array + description: "Array of gist objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.githubListWorkflowRuns: + type: utility + description: "List recent GitHub Actions workflow runs for a repository." + path: "github" + domain_rules: + - id: api_integration + description: "Must call GitHub API GET /repos/{owner}/{repo}/actions/runs endpoint" + inputs: + - name: owner + type: string + description: "Repository owner" + - name: repo + type: string + description: "Repository name" + - name: status + type: string + optional: true + description: "Filter by status" + - name: per_page + type: number + optional: true + description: "Results per page" + outputs: + - name: workflowRuns + type: object + description: "Workflow runs with total count" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.githubGetWorkflowRun: + type: utility + description: "Get details of a specific GitHub Actions workflow run." + path: "github" + domain_rules: + - id: api_integration + description: "Must call GitHub API GET /repos/{owner}/{repo}/actions/runs/{run_id} endpoint" + inputs: + - name: owner + type: string + description: "Repository owner" + - name: repo + type: string + description: "Repository name" + - name: run_id + type: number + description: "Workflow run ID" + outputs: + - name: workflowRun + type: object + description: "Workflow run details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + # ─── Vercel API ───────────────────────────────────────────────────────── + + ops.vercelListDeployments: + type: utility + description: "List deployments for a project or team with optional state filtering." + path: "vercel" + domain_rules: + - id: api_integration + description: "Must call Vercel API GET /v6/deployments endpoint" + inputs: + - name: projectId + type: string + optional: true + description: "Project ID to filter" + - name: teamId + type: string + optional: true + description: "Team ID for scoping" + - name: limit + type: number + optional: true + description: "Max results" + outputs: + - name: deployments + type: array + description: "Array of deployment objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.vercelGetDeployment: + type: utility + description: "Get details of a specific deployment by ID or URL." + path: "vercel" + domain_rules: + - id: api_integration + description: "Must call Vercel API GET /v13/deployments/{idOrUrl} endpoint" + inputs: + - name: idOrUrl + type: string + description: "Deployment ID or URL" + - name: teamId + type: string + optional: true + description: "Team ID for scoping" + outputs: + - name: deployment + type: object + description: "Deployment details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.vercelCancelDeployment: + type: utility + description: "Cancel a deployment that is currently building." + path: "vercel" + domain_rules: + - id: api_integration + description: "Must call Vercel API PATCH /v12/deployments/{id}/cancel endpoint" + inputs: + - name: id + type: string + description: "Deployment ID" + - name: teamId + type: string + optional: true + description: "Team ID for scoping" + outputs: + - name: result + type: object + description: "Cancellation result" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.vercelListProjects: + type: utility + description: "List all projects in your Vercel account or team." + path: "vercel" + domain_rules: + - id: api_integration + description: "Must call Vercel API GET /v9/projects endpoint" + inputs: + - name: teamId + type: string + optional: true + description: "Team ID for scoping" + - name: limit + type: number + optional: true + description: "Max results" + - name: search + type: string + optional: true + description: "Search query" + outputs: + - name: projects + type: array + description: "Array of project objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.vercelGetProject: + type: utility + description: "Get details of a specific Vercel project by name or ID." + path: "vercel" + domain_rules: + - id: api_integration + description: "Must call Vercel API GET /v9/projects/{idOrName} endpoint" + inputs: + - name: idOrName + type: string + description: "Project ID or name" + - name: teamId + type: string + optional: true + description: "Team ID for scoping" + outputs: + - name: project + type: object + description: "Project details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.vercelListDomains: + type: utility + description: "List all domains configured in your Vercel account." + path: "vercel" + domain_rules: + - id: api_integration + description: "Must call Vercel API GET /v5/domains endpoint" + inputs: + - name: teamId + type: string + optional: true + description: "Team ID for scoping" + - name: limit + type: number + optional: true + description: "Max results" + outputs: + - name: domains + type: array + description: "Array of domain objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.vercelGetDomain: + type: utility + description: "Get details and configuration for a specific domain." + path: "vercel" + domain_rules: + - id: api_integration + description: "Must call Vercel API GET /v5/domains/{name} endpoint" + inputs: + - name: name + type: string + description: "Domain name" + - name: teamId + type: string + optional: true + description: "Team ID for scoping" + outputs: + - name: domain + type: object + description: "Domain details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.vercelListProjectDomains: + type: utility + description: "List all domains assigned to a specific project." + path: "vercel" + domain_rules: + - id: api_integration + description: "Must call Vercel API GET /v9/projects/{idOrName}/domains endpoint" + inputs: + - name: idOrName + type: string + description: "Project ID or name" + - name: teamId + type: string + optional: true + description: "Team ID for scoping" + outputs: + - name: domains + type: array + description: "Array of project domain objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.vercelListEnvVars: + type: utility + description: "List environment variables for a Vercel project." + path: "vercel" + domain_rules: + - id: api_integration + description: "Must call Vercel API GET /v9/projects/{idOrName}/env endpoint" + inputs: + - name: idOrName + type: string + description: "Project ID or name" + - name: teamId + type: string + optional: true + description: "Team ID for scoping" + outputs: + - name: envVars + type: array + description: "Array of environment variable objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.vercelCreateEnvVar: + type: utility + description: "Create a new environment variable on a Vercel project." + path: "vercel" + domain_rules: + - id: api_integration + description: "Must call Vercel API POST /v10/projects/{idOrName}/env endpoint" + inputs: + - name: idOrName + type: string + description: "Project ID or name" + - name: key + type: string + description: "Variable name" + - name: value + type: string + description: "Variable value" + - name: target + type: array + description: "Target environments" + - name: teamId + type: string + optional: true + description: "Team ID for scoping" + outputs: + - name: envVar + type: object + description: "Created environment variable" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.vercelDeleteEnvVar: + type: utility + description: "Delete an environment variable from a Vercel project." + path: "vercel" + domain_rules: + - id: api_integration + description: "Must call Vercel API DELETE /v9/projects/{idOrName}/env/{envId} endpoint" + inputs: + - name: idOrName + type: string + description: "Project ID or name" + - name: envId + type: string + description: "Environment variable ID" + - name: teamId + type: string + optional: true + description: "Team ID for scoping" + outputs: + - name: result + type: object + description: "Deletion result" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.vercelGetDeploymentEvents: + type: utility + description: "Get build logs and runtime events for a deployment." + path: "vercel" + domain_rules: + - id: api_integration + description: "Must call Vercel API GET /v3/deployments/{idOrUrl}/events endpoint" + inputs: + - name: idOrUrl + type: string + description: "Deployment ID or URL" + - name: teamId + type: string + optional: true + description: "Team ID for scoping" + outputs: + - name: events + type: array + description: "Array of deployment events" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.vercelGetUsage: + type: utility + description: "Get billing usage and consumption data (requests, bandwidth, invocations) for a date range." + path: "vercel" + domain_rules: + - id: api_integration + description: "Must call Vercel API GET /v1/billing/charges endpoint with JSONL response parsing" + inputs: + - name: from + type: string + description: "Start date in ISO 8601 UTC format (inclusive)" + - name: to + type: string + description: "End date in ISO 8601 UTC format (exclusive)" + - name: teamId + type: string + optional: true + description: "Team ID for scoping" + outputs: + - name: summary + type: array + description: "Usage aggregated by service name with total cost and quantity" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.vercelGetRuntimeLogs: + type: utility + description: "Get runtime request logs for a deployment (method, path, status code, source)." + path: "vercel" + domain_rules: + - id: api_integration + description: "Must call Vercel API GET /v1/projects/{projectId}/deployments/{deploymentId}/runtime-logs endpoint" + inputs: + - name: projectId + type: string + description: "Project ID" + - name: deploymentId + type: string + description: "Deployment ID" + - name: teamId + type: string + optional: true + description: "Team ID for scoping" + outputs: + - name: logs + type: array + description: "Array of runtime log entries" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + # ─── Neon Serverless Postgres ────────────────────────────────────────── + + ops.neonListProjects: + type: utility + description: "List all Neon projects with optional search filtering." + path: "neon" + domain_rules: + - id: api_integration + description: "Must call Neon API GET /projects endpoint" + inputs: + - name: search + type: string + optional: true + description: "Search by project name or ID" + - name: limit + type: number + optional: true + description: "Max results (default 10, max 400)" + outputs: + - name: projects + type: array + description: "Array of project objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.neonGetProject: + type: utility + description: "Get details of a specific Neon project by ID." + path: "neon" + domain_rules: + - id: api_integration + description: "Must call Neon API GET /projects/{project_id} endpoint" + inputs: + - name: projectId + type: string + description: "Neon project ID" + outputs: + - name: project + type: object + description: "Project details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.neonCreateProject: + type: utility + description: "Create a new Neon project with a default branch and database." + path: "neon" + domain_rules: + - id: api_integration + description: "Must call Neon API POST /projects endpoint" + inputs: + - name: name + type: string + optional: true + description: "Project name" + - name: regionId + type: string + optional: true + description: "Deployment region" + - name: pgVersion + type: number + optional: true + description: "PostgreSQL major version" + outputs: + - name: project + type: object + description: "Created project with connection URIs" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.neonDeleteProject: + type: utility + description: "Permanently delete a Neon project and all its resources." + path: "neon" + domain_rules: + - id: api_integration + description: "Must call Neon API DELETE /projects/{project_id} endpoint" + inputs: + - name: projectId + type: string + description: "Neon project ID to delete" + outputs: + - name: project + type: object + description: "Deleted project details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.neonListBranches: + type: utility + description: "List all branches in a Neon project." + path: "neon" + domain_rules: + - id: api_integration + description: "Must call Neon API GET /projects/{project_id}/branches endpoint" + inputs: + - name: projectId + type: string + description: "Neon project ID" + outputs: + - name: branches + type: array + description: "Array of branch objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.neonCreateBranch: + type: utility + description: "Create a new branch from a parent branch in a Neon project." + path: "neon" + domain_rules: + - id: api_integration + description: "Must call Neon API POST /projects/{project_id}/branches endpoint" + inputs: + - name: projectId + type: string + description: "Neon project ID" + - name: name + type: string + optional: true + description: "Branch name" + - name: parentBranchId + type: string + optional: true + description: "Parent branch ID to fork from" + outputs: + - name: branch + type: object + description: "Created branch with endpoints" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.neonDeleteBranch: + type: utility + description: "Delete a branch from a Neon project." + path: "neon" + domain_rules: + - id: api_integration + description: "Must call Neon API DELETE /projects/{project_id}/branches/{branch_id} endpoint" + inputs: + - name: projectId + type: string + description: "Neon project ID" + - name: branchId + type: string + description: "Branch ID to delete" + outputs: + - name: branch + type: object + description: "Deleted branch details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.neonListDatabases: + type: utility + description: "List all databases on a specific branch." + path: "neon" + domain_rules: + - id: api_integration + description: "Must call Neon API GET /projects/{project_id}/branches/{branch_id}/databases endpoint" + inputs: + - name: projectId + type: string + description: "Neon project ID" + - name: branchId + type: string + description: "Branch ID" + outputs: + - name: databases + type: array + description: "Array of database objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.neonCreateDatabase: + type: utility + description: "Create a new database on a branch." + path: "neon" + domain_rules: + - id: api_integration + description: "Must call Neon API POST /projects/{project_id}/branches/{branch_id}/databases endpoint" + inputs: + - name: projectId + type: string + description: "Neon project ID" + - name: branchId + type: string + description: "Branch ID" + - name: name + type: string + description: "Database name" + - name: ownerName + type: string + description: "Owner role name" + outputs: + - name: database + type: object + description: "Created database details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.neonDeleteDatabase: + type: utility + description: "Delete a database from a branch." + path: "neon" + domain_rules: + - id: api_integration + description: "Must call Neon API DELETE /projects/{project_id}/branches/{branch_id}/databases/{database_name} endpoint" + inputs: + - name: projectId + type: string + description: "Neon project ID" + - name: branchId + type: string + description: "Branch ID" + - name: databaseName + type: string + description: "Database name to delete" + outputs: + - name: database + type: object + description: "Deleted database details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.neonListEndpoints: + type: utility + description: "List compute endpoints for a Neon project." + path: "neon" + domain_rules: + - id: api_integration + description: "Must call Neon API GET /projects/{project_id}/endpoints endpoint" + inputs: + - name: projectId + type: string + description: "Neon project ID" + outputs: + - name: endpoints + type: array + description: "Array of endpoint objects with status and config" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.neonStartEndpoint: + type: utility + description: "Start a suspended compute endpoint." + path: "neon" + domain_rules: + - id: api_integration + description: "Must call Neon API POST /projects/{project_id}/endpoints/{endpoint_id}/start endpoint" + inputs: + - name: projectId + type: string + description: "Neon project ID" + - name: endpointId + type: string + description: "Endpoint ID to start" + outputs: + - name: endpoint + type: object + description: "Started endpoint details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.neonSuspendEndpoint: + type: utility + description: "Suspend a running compute endpoint to save costs." + path: "neon" + domain_rules: + - id: api_integration + description: "Must call Neon API POST /projects/{project_id}/endpoints/{endpoint_id}/suspend endpoint" + inputs: + - name: projectId + type: string + description: "Neon project ID" + - name: endpointId + type: string + description: "Endpoint ID to suspend" + outputs: + - name: endpoint + type: object + description: "Suspended endpoint details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.neonGetConnectionUri: + type: utility + description: "Get a PostgreSQL connection string for a database." + path: "neon" + domain_rules: + - id: api_integration + description: "Must call Neon API GET /projects/{project_id}/connection_uri endpoint" + inputs: + - name: projectId + type: string + description: "Neon project ID" + - name: databaseName + type: string + description: "Database name" + - name: roleName + type: string + description: "Role name" + - name: branchId + type: string + optional: true + description: "Branch ID (defaults to default branch)" + outputs: + - name: uri + type: string + description: "PostgreSQL connection URI" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.neonListOperations: + type: utility + description: "List recent operations for a Neon project." + path: "neon" + domain_rules: + - id: api_integration + description: "Must call Neon API GET /projects/{project_id}/operations endpoint" + inputs: + - name: projectId + type: string + description: "Neon project ID" + - name: limit + type: number + optional: true + description: "Max results" + outputs: + - name: operations + type: array + description: "Array of operation objects with status and duration" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.neonGetConsumption: + type: utility + description: "Get usage metrics (compute time, storage, data written) for a date range." + path: "neon" + domain_rules: + - id: api_integration + description: "Must call Neon API GET /consumption_history/account or /consumption_history/projects endpoint" + inputs: + - name: from + type: string + description: "Start date in ISO 8601 format" + - name: to + type: string + description: "End date in ISO 8601 format" + - name: granularity + type: string + description: "Time granularity: hourly, daily, or monthly" + - name: projectIds + type: string + optional: true + description: "Comma-separated project IDs to filter" + outputs: + - name: consumption + type: object + description: "Usage metrics by time period" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + # ─── Cloudflare API ───────────────────────────────────────────────────── + + ops.cloudflareListZones: + type: utility + description: "List Cloudflare zones with optional name and status filtering." + path: "cloudflare" + domain_rules: + - id: api_integration + description: "Must call Cloudflare API GET /zones endpoint" + inputs: + - name: name + type: string + optional: true + description: "Zone name filter" + - name: status + type: string + optional: true + description: "Zone status filter" + - name: per_page + type: number + optional: true + description: "Results per page" + outputs: + - name: zones + type: array + description: "Array of zone objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.cloudflareGetZone: + type: utility + description: "Get details of a specific Cloudflare zone by ID." + path: "cloudflare" + domain_rules: + - id: api_integration + description: "Must call Cloudflare API GET /zones/{zone_id} endpoint" + inputs: + - name: zone_id + type: string + description: "Zone ID" + outputs: + - name: zone + type: object + description: "Zone details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.cloudflarePurgeCache: + type: utility + description: "Purge cached content from a Cloudflare zone by URLs or purge everything." + path: "cloudflare" + domain_rules: + - id: api_integration + description: "Must call Cloudflare API POST /zones/{zone_id}/purge_cache endpoint" + inputs: + - name: zone_id + type: string + description: "Zone ID" + - name: purge_everything + type: boolean + optional: true + description: "Purge all cached content" + - name: files + type: array + optional: true + description: "Specific URLs to purge" + outputs: + - name: result + type: object + description: "Purge result" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.cloudflareListDnsRecords: + type: utility + description: "List DNS records for a Cloudflare zone with type and name filters." + path: "cloudflare" + domain_rules: + - id: api_integration + description: "Must call Cloudflare API GET /zones/{zone_id}/dns_records endpoint" + inputs: + - name: zone_id + type: string + description: "Zone ID" + - name: type + type: string + optional: true + description: "Record type filter" + - name: name + type: string + optional: true + description: "Record name filter" + - name: per_page + type: number + optional: true + description: "Results per page" + outputs: + - name: records + type: array + description: "Array of DNS record objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.cloudflareCreateDnsRecord: + type: utility + description: "Create a new DNS record in a Cloudflare zone." + path: "cloudflare" + domain_rules: + - id: api_integration + description: "Must call Cloudflare API POST /zones/{zone_id}/dns_records endpoint" + inputs: + - name: zone_id + type: string + description: "Zone ID" + - name: type + type: string + description: "Record type (A, AAAA, CNAME, etc.)" + - name: name + type: string + description: "Record name" + - name: content + type: string + description: "Record content" + - name: proxied + type: boolean + optional: true + description: "Whether proxied through Cloudflare" + outputs: + - name: record + type: object + description: "Created DNS record" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.cloudflareUpdateDnsRecord: + type: utility + description: "Update an existing DNS record in a Cloudflare zone." + path: "cloudflare" + domain_rules: + - id: api_integration + description: "Must call Cloudflare API PUT /zones/{zone_id}/dns_records/{record_id} endpoint" + inputs: + - name: zone_id + type: string + description: "Zone ID" + - name: record_id + type: string + description: "DNS record ID" + - name: type + type: string + description: "Record type" + - name: name + type: string + description: "Record name" + - name: content + type: string + description: "Record content" + - name: proxied + type: boolean + optional: true + description: "Whether proxied through Cloudflare" + outputs: + - name: record + type: object + description: "Updated DNS record" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.cloudflareDeleteDnsRecord: + type: utility + description: "Delete a DNS record from a Cloudflare zone." + path: "cloudflare" + domain_rules: + - id: api_integration + description: "Must call Cloudflare API DELETE /zones/{zone_id}/dns_records/{record_id} endpoint" + inputs: + - name: zone_id + type: string + description: "Zone ID" + - name: record_id + type: string + description: "DNS record ID" + outputs: + - name: result + type: object + description: "Deletion result" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.cloudflareListWorkers: + type: utility + description: "List all Workers scripts in your Cloudflare account." + path: "cloudflare" + domain_rules: + - id: api_integration + description: "Must call Cloudflare API GET /accounts/{account_id}/workers/scripts endpoint" + inputs: [] + outputs: + - name: workers + type: array + description: "Array of Worker script objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.cloudflareGetWorker: + type: utility + description: "Get metadata and bindings for a specific Worker script." + path: "cloudflare" + domain_rules: + - id: api_integration + description: "Must call Cloudflare API GET /accounts/{account_id}/workers/scripts/{script_name}/settings endpoint" + inputs: + - name: script_name + type: string + description: "Worker script name" + outputs: + - name: worker + type: object + description: "Worker metadata and bindings" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.cloudflareDeleteWorker: + type: utility + description: "Delete a Worker script from your Cloudflare account." + path: "cloudflare" + domain_rules: + - id: api_integration + description: "Must call Cloudflare API DELETE /accounts/{account_id}/workers/scripts/{script_name} endpoint" + inputs: + - name: script_name + type: string + description: "Worker script name" + outputs: + - name: result + type: object + description: "Deletion result" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.cloudflareListKvNamespaces: + type: utility + description: "List KV namespaces in your Cloudflare account." + path: "cloudflare" + domain_rules: + - id: api_integration + description: "Must call Cloudflare API GET /accounts/{account_id}/storage/kv/namespaces endpoint" + inputs: + - name: per_page + type: number + optional: true + description: "Results per page" + outputs: + - name: namespaces + type: array + description: "Array of KV namespace objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.cloudflareListKvKeys: + type: utility + description: "List keys in a Cloudflare KV namespace with optional prefix filter." + path: "cloudflare" + domain_rules: + - id: api_integration + description: "Must call Cloudflare API GET /accounts/{account_id}/storage/kv/namespaces/{namespace_id}/keys endpoint" + inputs: + - name: namespace_id + type: string + description: "KV namespace ID" + - name: prefix + type: string + optional: true + description: "Key prefix filter" + - name: limit + type: number + optional: true + description: "Max results" + outputs: + - name: keys + type: array + description: "Array of key objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.cloudflareGetKvValue: + type: utility + description: "Get the value of a key from a Cloudflare KV namespace." + path: "cloudflare" + domain_rules: + - id: api_integration + description: "Must call Cloudflare API GET /accounts/{account_id}/storage/kv/namespaces/{namespace_id}/values/{key_name} endpoint" + inputs: + - name: namespace_id + type: string + description: "KV namespace ID" + - name: key_name + type: string + description: "Key name" + outputs: + - name: value + type: string + description: "The stored value" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.cloudflarePutKvValue: + type: utility + description: "Write a key-value pair to a Cloudflare KV namespace." + path: "cloudflare" + domain_rules: + - id: api_integration + description: "Must call Cloudflare API PUT /accounts/{account_id}/storage/kv/namespaces/{namespace_id}/values/{key_name} endpoint" + inputs: + - name: namespace_id + type: string + description: "KV namespace ID" + - name: key_name + type: string + description: "Key name" + - name: value + type: string + description: "Value to store" + outputs: + - name: result + type: object + description: "Write confirmation" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.cloudflareDeleteKvKey: + type: utility + description: "Delete a key from a Cloudflare KV namespace." + path: "cloudflare" + domain_rules: + - id: api_integration + description: "Must call Cloudflare API DELETE /accounts/{account_id}/storage/kv/namespaces/{namespace_id}/values/{key_name} endpoint" + inputs: + - name: namespace_id + type: string + description: "KV namespace ID" + - name: key_name + type: string + description: "Key name" + outputs: + - name: result + type: object + description: "Deletion result" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + # ─── Slack Tools ───────────────────────────────────────────────────────────── + + ops.slackSendMessage: + type: utility + description: "Send a message to a Slack channel or thread." + path: "slack" + domain_rules: + - id: api_integration + description: "Must call Slack API chat.postMessage endpoint" + inputs: + - name: channel + type: string + description: "Channel ID or name" + - name: text + type: string + description: "Message text" + - name: thread_ts + type: string + optional: true + description: "Thread timestamp to reply in" + - name: unfurl_links + type: boolean + optional: true + description: "Enable link unfurling" + outputs: + - name: message + type: object + description: "Sent message details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.slackListChannels: + type: utility + description: "List public and private channels in a Slack workspace." + path: "slack" + domain_rules: + - id: api_integration + description: "Must call Slack API conversations.list endpoint" + inputs: + - name: types + type: string + optional: true + description: "Channel types filter" + - name: limit + type: number + optional: true + description: "Results per page" + - name: cursor + type: string + optional: true + description: "Pagination cursor" + outputs: + - name: channels + type: array + description: "Array of channel objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.slackGetChannel: + type: utility + description: "Get detailed information about a specific Slack channel." + path: "slack" + domain_rules: + - id: api_integration + description: "Must call Slack API conversations.info endpoint" + inputs: + - name: channel + type: string + description: "Channel ID" + outputs: + - name: channel + type: object + description: "Channel details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.slackListUsers: + type: utility + description: "List all users in a Slack workspace." + path: "slack" + domain_rules: + - id: api_integration + description: "Must call Slack API users.list endpoint" + inputs: + - name: limit + type: number + optional: true + description: "Results per page" + - name: cursor + type: string + optional: true + description: "Pagination cursor" + outputs: + - name: members + type: array + description: "Array of user objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.slackGetUser: + type: utility + description: "Get profile details of a specific Slack user." + path: "slack" + domain_rules: + - id: api_integration + description: "Must call Slack API users.info endpoint" + inputs: + - name: user + type: string + description: "User ID" + outputs: + - name: user + type: object + description: "User profile details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.slackAddReaction: + type: utility + description: "Add an emoji reaction to a Slack message." + path: "slack" + domain_rules: + - id: api_integration + description: "Must call Slack API reactions.add endpoint" + inputs: + - name: channel + type: string + description: "Channel ID" + - name: timestamp + type: string + description: "Message timestamp" + - name: name + type: string + description: "Emoji name without colons" + outputs: + - name: result + type: object + description: "Reaction result" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.slackUploadFile: + type: utility + description: "Upload a text file or snippet to a Slack channel." + path: "slack" + domain_rules: + - id: api_integration + description: "Must call Slack API files.upload endpoint" + inputs: + - name: channel_id + type: string + description: "Channel ID" + - name: content + type: string + description: "File content" + - name: filename + type: string + description: "Filename" + - name: title + type: string + optional: true + description: "File title" + - name: initial_comment + type: string + optional: true + description: "Comment with the file" + outputs: + - name: file + type: object + description: "Uploaded file details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.slackSetChannelTopic: + type: utility + description: "Set the topic of a Slack channel." + path: "slack" + domain_rules: + - id: api_integration + description: "Must call Slack API conversations.setTopic endpoint" + inputs: + - name: channel + type: string + description: "Channel ID" + - name: topic + type: string + description: "New topic text" + outputs: + - name: channel + type: object + description: "Updated channel" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.slackListMessages: + type: utility + description: "Retrieve recent messages from a Slack channel." + path: "slack" + domain_rules: + - id: api_integration + description: "Must call Slack API conversations.history endpoint" + inputs: + - name: channel + type: string + description: "Channel ID" + - name: limit + type: number + optional: true + description: "Number of messages" + - name: cursor + type: string + optional: true + description: "Pagination cursor" + - name: oldest + type: string + optional: true + description: "Messages after this Unix timestamp" + - name: latest + type: string + optional: true + description: "Messages before this Unix timestamp" + outputs: + - name: messages + type: array + description: "Array of message objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.slackSearchMessages: + type: utility + description: "Search for messages across a Slack workspace." + path: "slack" + domain_rules: + - id: api_integration + description: "Must call Slack API search.messages endpoint" + inputs: + - name: query + type: string + description: "Search query" + - name: sort + type: string + optional: true + description: "Sort by score or timestamp" + - name: count + type: number + optional: true + description: "Number of results" + outputs: + - name: messages + type: object + description: "Search results" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + # ─── Discord Tools ────────────────────────────────────────────────────────── + + ops.discordSendMessage: + type: utility + description: "Send a message to a Discord channel." + path: "discord" + domain_rules: + - id: api_integration + description: "Must call Discord API POST /channels/{channel_id}/messages endpoint" + inputs: + - name: channel_id + type: string + description: "Channel ID" + - name: content + type: string + description: "Message content" + - name: tts + type: boolean + optional: true + description: "Text-to-speech" + outputs: + - name: message + type: object + description: "Sent message details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.discordListGuilds: + type: utility + description: "List guilds (servers) the bot is a member of." + path: "discord" + domain_rules: + - id: api_integration + description: "Must call Discord API GET /users/@me/guilds endpoint" + inputs: + - name: limit + type: number + optional: true + description: "Number of guilds" + - name: before + type: string + optional: true + description: "Get guilds before this ID" + - name: after + type: string + optional: true + description: "Get guilds after this ID" + outputs: + - name: guilds + type: array + description: "Array of guild objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.discordGetGuild: + type: utility + description: "Get detailed information about a Discord guild." + path: "discord" + domain_rules: + - id: api_integration + description: "Must call Discord API GET /guilds/{guild_id} endpoint" + inputs: + - name: guild_id + type: string + description: "Guild ID" + - name: with_counts + type: boolean + optional: true + description: "Include member and presence counts" + outputs: + - name: guild + type: object + description: "Guild details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.discordListChannels: + type: utility + description: "List all channels in a Discord guild." + path: "discord" + domain_rules: + - id: api_integration + description: "Must call Discord API GET /guilds/{guild_id}/channels endpoint" + inputs: + - name: guild_id + type: string + description: "Guild ID" + outputs: + - name: channels + type: array + description: "Array of channel objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.discordGetChannel: + type: utility + description: "Get detailed information about a specific Discord channel." + path: "discord" + domain_rules: + - id: api_integration + description: "Must call Discord API GET /channels/{channel_id} endpoint" + inputs: + - name: channel_id + type: string + description: "Channel ID" + outputs: + - name: channel + type: object + description: "Channel details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.discordListMessages: + type: utility + description: "Retrieve recent messages from a Discord channel." + path: "discord" + domain_rules: + - id: api_integration + description: "Must call Discord API GET /channels/{channel_id}/messages endpoint" + inputs: + - name: channel_id + type: string + description: "Channel ID" + - name: limit + type: number + optional: true + description: "Number of messages (1-100)" + - name: before + type: string + optional: true + description: "Get messages before this ID" + - name: after + type: string + optional: true + description: "Get messages after this ID" + - name: around + type: string + optional: true + description: "Get messages around this ID" + outputs: + - name: messages + type: array + description: "Array of message objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.discordCreateChannel: + type: utility + description: "Create a new text or voice channel in a Discord guild." + path: "discord" + domain_rules: + - id: api_integration + description: "Must call Discord API POST /guilds/{guild_id}/channels endpoint" + inputs: + - name: guild_id + type: string + description: "Guild ID" + - name: name + type: string + description: "Channel name" + - name: type + type: number + optional: true + description: "Channel type (0=text, 2=voice, 4=category, 5=announcement)" + - name: topic + type: string + optional: true + description: "Channel topic" + - name: parent_id + type: string + optional: true + description: "Parent category ID" + outputs: + - name: channel + type: object + description: "Created channel details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.discordEditMessage: + type: utility + description: "Edit a previously sent message in a Discord channel." + path: "discord" + domain_rules: + - id: api_integration + description: "Must call Discord API PATCH /channels/{channel_id}/messages/{message_id} endpoint" + inputs: + - name: channel_id + type: string + description: "Channel ID" + - name: message_id + type: string + description: "Message ID" + - name: content + type: string + description: "New message content" + outputs: + - name: message + type: object + description: "Updated message" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.discordDeleteMessage: + type: utility + description: "Delete a message from a Discord channel." + path: "discord" + domain_rules: + - id: api_integration + description: "Must call Discord API DELETE /channels/{channel_id}/messages/{message_id} endpoint" + inputs: + - name: channel_id + type: string + description: "Channel ID" + - name: message_id + type: string + description: "Message ID" + outputs: + - name: result + type: object + description: "Deletion confirmation" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.discordAddReaction: + type: utility + description: "Add an emoji reaction to a Discord message." + path: "discord" + domain_rules: + - id: api_integration + description: "Must call Discord API PUT /channels/{channel_id}/messages/{message_id}/reactions/{emoji}/@me endpoint" + inputs: + - name: channel_id + type: string + description: "Channel ID" + - name: message_id + type: string + description: "Message ID" + - name: emoji + type: string + description: "URL-encoded emoji or custom emoji name:id" + outputs: + - name: result + type: object + description: "Reaction result" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.discordListMembers: + type: utility + description: "List members of a Discord guild with pagination." + path: "discord" + domain_rules: + - id: api_integration + description: "Must call Discord API GET /guilds/{guild_id}/members endpoint" + inputs: + - name: guild_id + type: string + description: "Guild ID" + - name: limit + type: number + optional: true + description: "Number of members (1-1000)" + - name: after + type: string + optional: true + description: "Get members after this user ID" + outputs: + - name: members + type: array + description: "Array of member objects" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.discordGetMember: + type: utility + description: "Get detailed information about a specific guild member." + path: "discord" + domain_rules: + - id: api_integration + description: "Must call Discord API GET /guilds/{guild_id}/members/{user_id} endpoint" + inputs: + - name: guild_id + type: string + description: "Guild ID" + - name: user_id + type: string + description: "User ID" + outputs: + - name: member + type: object + description: "Member details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.discordCreateThread: + type: utility + description: "Create a new thread from a message in a Discord channel." + path: "discord" + domain_rules: + - id: api_integration + description: "Must call Discord API POST /channels/{channel_id}/messages/{message_id}/threads endpoint" + inputs: + - name: channel_id + type: string + description: "Channel ID" + - name: message_id + type: string + description: "Message ID" + - name: name + type: string + description: "Thread name" + - name: auto_archive_duration + type: number + optional: true + description: "Auto-archive duration in minutes" + outputs: + - name: thread + type: object + description: "Created thread details" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.discordListThreads: + type: utility + description: "List active threads in a Discord guild." + path: "discord" + domain_rules: + - id: api_integration + description: "Must call Discord API GET /guilds/{guild_id}/threads/active endpoint" + inputs: + - name: guild_id + type: string + description: "Guild ID" + outputs: + - name: threads + type: object + description: "Active threads and members" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + + ops.discordPinMessage: + type: utility + description: "Pin a message in a Discord channel." + path: "discord" + domain_rules: + - id: api_integration + description: "Must call Discord API PUT /channels/{channel_id}/pins/{message_id} endpoint" + inputs: + - name: channel_id + type: string + description: "Channel ID" + - name: message_id + type: string + description: "Message ID" + outputs: + - name: result + type: object + description: "Pin confirmation" + measures: [working_implementation, valid_output_structure, proper_error_handling, ai_sdk_compliance] + # ============================================================================= # VALIDATORS - Which validators to run against each block # ============================================================================= diff --git a/packages/tools/official/discord/README.md b/packages/tools/official/discord/README.md new file mode 100644 index 0000000..ba053cc --- /dev/null +++ b/packages/tools/official/discord/README.md @@ -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 diff --git a/packages/tools/official/discord/package.json b/packages/tools/official/discord/package.json new file mode 100644 index 0000000..ac702e7 --- /dev/null +++ b/packages/tools/official/discord/package.json @@ -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." + } + ] + } +} diff --git a/packages/tools/official/discord/src/index.ts b/packages/tools/official/discord/src/index.ts new file mode 100644 index 0000000..d9a6274 --- /dev/null +++ b/packages/tools/official/discord/src/index.ts @@ -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(method: string, path: string, body?: unknown): Promise { + const key = getApiKey(); + + const headers: Record = { + 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 { + 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 { + 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({ + 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 { + 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('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({ + 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 { + 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( + '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({ + 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 { + 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( + '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({ + 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 { + try { + if (!input.channel_id || !input.message_id) { + throw new Error('channel_id and message_id are required'); + } + return await apiRequest( + '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({ + 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 { + try { + if (!input.channel_id || !input.message_id) { + throw new Error('channel_id and message_id are required'); + } + return await apiRequest( + '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({ + 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 { + try { + if (!input.channel_id || !input.message_id || !input.emoji) { + throw new Error('channel_id, message_id, and emoji are required'); + } + return await apiRequest( + '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({ + 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 { + 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('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({ + 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 { + try { + if (!input.guild_id) { + throw new Error('guild_id is required'); + } + const qs = buildQueryString({ with_counts: input.with_counts }); + return await apiRequest('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({ + 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 { + try { + if (!input.guild_id) { + throw new Error('guild_id is required'); + } + return await apiRequest('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({ + 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 { + try { + if (!input.channel_id) { + throw new Error('channel_id is required'); + } + return await apiRequest('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({ + 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 { + 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('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({ + 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 { + 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('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({ + 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 { + try { + if (!input.guild_id || !input.user_id) { + throw new Error('guild_id and user_id are required'); + } + return await apiRequest( + '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({ + 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 { + 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( + '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({ + 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 { + try { + if (!input.guild_id) { + throw new Error('guild_id is required'); + } + return await apiRequest('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, +}; diff --git a/packages/tools/official/discord/tsconfig.json b/packages/tools/official/discord/tsconfig.json new file mode 100644 index 0000000..6521d56 --- /dev/null +++ b/packages/tools/official/discord/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@tpmjs/tsconfig/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "incremental": false, + "composite": false + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/tools/official/discord/tsup.config.ts b/packages/tools/official/discord/tsup.config.ts new file mode 100644 index 0000000..a242871 --- /dev/null +++ b/packages/tools/official/discord/tsup.config.ts @@ -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, +}); diff --git a/packages/tools/official/slack/README.md b/packages/tools/official/slack/README.md new file mode 100644 index 0000000..e09870d --- /dev/null +++ b/packages/tools/official/slack/README.md @@ -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 diff --git a/packages/tools/official/slack/package.json b/packages/tools/official/slack/package.json new file mode 100644 index 0000000..aeb684b --- /dev/null +++ b/packages/tools/official/slack/package.json @@ -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." + } + ] + } +} diff --git a/packages/tools/official/slack/src/index.ts b/packages/tools/official/slack/src/index.ts new file mode 100644 index 0000000..8ba1f3e --- /dev/null +++ b/packages/tools/official/slack/src/index.ts @@ -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(method: string, body?: unknown): Promise { + const token = getApiKey(); + + const headers: Record = { + 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(method: string, params: Record): Promise { + const token = getApiKey(); + + const qs = buildQueryString(params); + const url = `${BASE_URL}/${method}${qs}`; + + const headers: Record = { + 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 { + 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({ + 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 { + try { + if (!input.channel || !input.text) { + throw new Error('Channel and text are required and must be non-empty'); + } + return await apiRequest('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({ + 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 { + 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('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({ + 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 { + 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('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({ + 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 { + try { + if (input.limit !== undefined && (input.limit < 1 || input.limit > 1000)) { + throw new Error('Limit must be between 1 and 1000'); + } + return await apiGetRequest('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({ + type: 'object', + properties: { + channel: { type: 'string', description: 'Channel ID (e.g., "C1234567890").' }, + }, + required: ['channel'], + additionalProperties: false, + }), + async execute(input: GetChannelInput): Promise { + try { + if (!input.channel) { + throw new Error('Channel is required and must be non-empty'); + } + return await apiGetRequest('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({ + 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 { + 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('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({ + 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 { + try { + if (input.limit !== undefined && (input.limit < 1 || input.limit > 1000)) { + throw new Error('Limit must be between 1 and 1000'); + } + return await apiGetRequest('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({ + type: 'object', + properties: { + user: { type: 'string', description: 'User ID (e.g., "U1234567890").' }, + }, + required: ['user'], + additionalProperties: false, + }), + async execute(input: GetUserInput): Promise { + try { + if (!input.user) { + throw new Error('User ID is required and must be non-empty'); + } + return await apiGetRequest('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({ + 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 { + try { + if (!input.channel || !input.timestamp || !input.name) { + throw new Error('Channel, timestamp, and name are required'); + } + return await apiRequest('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({ + 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 { + 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('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, +}; diff --git a/packages/tools/official/slack/tsconfig.json b/packages/tools/official/slack/tsconfig.json new file mode 100644 index 0000000..6521d56 --- /dev/null +++ b/packages/tools/official/slack/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@tpmjs/tsconfig/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "incremental": false, + "composite": false + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/tools/official/slack/tsup.config.ts b/packages/tools/official/slack/tsup.config.ts new file mode 100644 index 0000000..a242871 --- /dev/null +++ b/packages/tools/official/slack/tsup.config.ts @@ -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, +}); diff --git a/packages/types/src/tpmjs.ts b/packages/types/src/tpmjs.ts index dec3ce5..a20c355 100644 --- a/packages/types/src/tpmjs.ts +++ b/packages/types/src/tpmjs.ts @@ -30,6 +30,14 @@ export const TPMJS_CATEGORIES = [ 'automation', 'ai-ml', 'monitoring', + // Business categories + 'finance', + 'legal', + 'hr', + 'marketing', + 'cx', + 'edu', + 'sales', // Aliases 'doc', 'text',