diff --git a/.claude/commands/add-block.md b/.claude/commands/add-block.md new file mode 100644 index 0000000000..40f15e772f --- /dev/null +++ b/.claude/commands/add-block.md @@ -0,0 +1,591 @@ +--- +description: Create a block configuration for a Sim Studio integration with proper subBlocks, conditions, and tool wiring +argument-hint: +--- + +# Add Block Skill + +You are an expert at creating block configurations for Sim Studio. You understand the serializer, subBlock types, conditions, dependsOn, modes, and all UI patterns. + +## Your Task + +When the user asks you to create a block: +1. Create the block file in `apps/sim/blocks/blocks/{service}.ts` +2. Configure all subBlocks with proper types, conditions, and dependencies +3. Wire up tools correctly + +## Block Configuration Structure + +```typescript +import { {ServiceName}Icon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' + +export const {ServiceName}Block: BlockConfig = { + type: '{service}', // snake_case identifier + name: '{Service Name}', // Human readable + description: 'Brief description', // One sentence + longDescription: 'Detailed description for docs', + docsLink: 'https://docs.sim.ai/tools/{service}', + category: 'tools', // 'tools' | 'blocks' | 'triggers' + bgColor: '#HEXCOLOR', // Brand color + icon: {ServiceName}Icon, + + // Auth mode + authMode: AuthMode.OAuth, // or AuthMode.ApiKey + + subBlocks: [ + // Define all UI fields here + ], + + tools: { + access: ['tool_id_1', 'tool_id_2'], // Array of tool IDs this block can use + config: { + tool: (params) => `{service}_${params.operation}`, // Tool selector function + params: (params) => ({ + // Transform subBlock values to tool params + }), + }, + }, + + inputs: { + // Optional: define expected inputs from other blocks + }, + + outputs: { + // Define outputs available to downstream blocks + }, +} +``` + +## SubBlock Types Reference + +**Critical:** Every subblock `id` must be unique within the block. Duplicate IDs cause conflicts even with different conditions. + +### Text Inputs +```typescript +// Single-line input +{ id: 'field', title: 'Label', type: 'short-input', placeholder: '...' } + +// Multi-line input +{ id: 'field', title: 'Label', type: 'long-input', placeholder: '...', rows: 6 } + +// Password input +{ id: 'apiKey', title: 'API Key', type: 'short-input', password: true } +``` + +### Selection Inputs +```typescript +// Dropdown (static options) +{ + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Create', id: 'create' }, + { label: 'Update', id: 'update' }, + ], + value: () => 'create', // Default value function +} + +// Combobox (searchable dropdown) +{ + id: 'field', + title: 'Label', + type: 'combobox', + options: [...], + searchable: true, +} +``` + +### Code/JSON Inputs +```typescript +{ + id: 'code', + title: 'Code', + type: 'code', + language: 'javascript', // 'javascript' | 'json' | 'python' + placeholder: '// Enter code...', +} +``` + +### OAuth/Credentials +```typescript +{ + id: 'credential', + title: 'Account', + type: 'oauth-input', + serviceId: '{service}', // Must match OAuth provider + placeholder: 'Select account', + required: true, +} +``` + +### Selectors (with dynamic options) +```typescript +// Channel selector (Slack, Discord, etc.) +{ + id: 'channel', + title: 'Channel', + type: 'channel-selector', + serviceId: '{service}', + placeholder: 'Select channel', + dependsOn: ['credential'], +} + +// Project selector (Jira, etc.) +{ + id: 'project', + title: 'Project', + type: 'project-selector', + serviceId: '{service}', + dependsOn: ['credential'], +} + +// File selector (Google Drive, etc.) +{ + id: 'file', + title: 'File', + type: 'file-selector', + serviceId: '{service}', + mimeType: 'application/pdf', + dependsOn: ['credential'], +} + +// User selector +{ + id: 'user', + title: 'User', + type: 'user-selector', + serviceId: '{service}', + dependsOn: ['credential'], +} +``` + +### Other Types +```typescript +// Switch/toggle +{ id: 'enabled', type: 'switch' } + +// Slider +{ id: 'temperature', title: 'Temperature', type: 'slider', min: 0, max: 2, step: 0.1 } + +// Table (key-value pairs) +{ id: 'headers', title: 'Headers', type: 'table', columns: ['Key', 'Value'] } + +// File upload +{ + id: 'files', + title: 'Attachments', + type: 'file-upload', + multiple: true, + acceptedTypes: 'image/*,application/pdf', +} +``` + +## Condition Syntax + +Controls when a field is shown based on other field values. + +### Simple Condition +```typescript +condition: { field: 'operation', value: 'create' } +// Shows when operation === 'create' +``` + +### Multiple Values (OR) +```typescript +condition: { field: 'operation', value: ['create', 'update'] } +// Shows when operation is 'create' OR 'update' +``` + +### Negation +```typescript +condition: { field: 'operation', value: 'delete', not: true } +// Shows when operation !== 'delete' +``` + +### Compound (AND) +```typescript +condition: { + field: 'operation', + value: 'send', + and: { + field: 'type', + value: 'dm', + not: true, + } +} +// Shows when operation === 'send' AND type !== 'dm' +``` + +### Complex Example +```typescript +condition: { + field: 'operation', + value: ['list', 'search'], + not: true, + and: { + field: 'authMethod', + value: 'oauth', + } +} +// Shows when operation NOT in ['list', 'search'] AND authMethod === 'oauth' +``` + +## DependsOn Pattern + +Controls when a field is enabled and when its options are refetched. + +### Simple Array (all must be set) +```typescript +dependsOn: ['credential'] +// Enabled only when credential has a value +// Options refetch when credential changes + +dependsOn: ['credential', 'projectId'] +// Enabled only when BOTH have values +``` + +### Complex (all + any) +```typescript +dependsOn: { + all: ['authMethod'], // All must be set + any: ['credential', 'apiKey'] // At least one must be set +} +// Enabled when authMethod is set AND (credential OR apiKey is set) +``` + +## Required Pattern + +Can be boolean or condition-based. + +### Simple Boolean +```typescript +required: true +required: false +``` + +### Conditional Required +```typescript +required: { field: 'operation', value: 'create' } +// Required only when operation === 'create' + +required: { field: 'operation', value: ['create', 'update'] } +// Required when operation is 'create' OR 'update' +``` + +## Mode Pattern (Basic vs Advanced) + +Controls which UI view shows the field. + +### Mode Options +- `'basic'` - Only in basic view (default UI) +- `'advanced'` - Only in advanced view +- `'both'` - Both views (default if not specified) +- `'trigger'` - Only in trigger configuration + +### canonicalParamId Pattern + +Maps multiple UI fields to a single serialized parameter: + +```typescript +// Basic mode: Visual selector +{ + id: 'channel', + title: 'Channel', + type: 'channel-selector', + mode: 'basic', + canonicalParamId: 'channel', // Both map to 'channel' param + dependsOn: ['credential'], +} + +// Advanced mode: Manual input +{ + id: 'channelId', + title: 'Channel ID', + type: 'short-input', + mode: 'advanced', + canonicalParamId: 'channel', // Both map to 'channel' param + placeholder: 'Enter channel ID manually', +} +``` + +**How it works:** +- In basic mode: `channel` selector value → `params.channel` +- In advanced mode: `channelId` input value → `params.channel` +- The serializer consolidates based on current mode + +**Critical constraints:** +- `canonicalParamId` must NOT match any other subblock's `id` in the same block (causes conflicts) +- `canonicalParamId` must be unique per block (only one basic/advanced pair per canonicalParamId) +- ONLY use `canonicalParamId` to link basic/advanced mode alternatives for the same logical parameter +- Do NOT use it for any other purpose + +## WandConfig Pattern + +Enables AI-assisted field generation. + +```typescript +{ + id: 'query', + title: 'Query', + type: 'code', + language: 'json', + wandConfig: { + enabled: true, + prompt: 'Generate a query based on the user request. Return ONLY the JSON.', + placeholder: 'Describe what you want to query...', + generationType: 'json-object', // Optional: affects AI behavior + maintainHistory: true, // Optional: keeps conversation context + }, +} +``` + +### Generation Types +- `'javascript-function-body'` - JS code generation +- `'json-object'` - Raw JSON (adds "no markdown" instruction) +- `'json-schema'` - JSON Schema definitions +- `'sql-query'` - SQL statements +- `'timestamp'` - Adds current date/time context + +## Tools Configuration + +### Simple Tool Selector +```typescript +tools: { + access: ['service_create', 'service_read', 'service_update'], + config: { + tool: (params) => `service_${params.operation}`, + }, +} +``` + +### With Parameter Transformation +```typescript +tools: { + access: ['service_action'], + config: { + tool: (params) => 'service_action', + params: (params) => ({ + id: params.resourceId, + data: typeof params.data === 'string' ? JSON.parse(params.data) : params.data, + }), + }, +} +``` + +### V2 Versioned Tool Selector +```typescript +import { createVersionedToolSelector } from '@/blocks/utils' + +tools: { + access: [ + 'service_create_v2', + 'service_read_v2', + 'service_update_v2', + ], + config: { + tool: createVersionedToolSelector({ + baseToolSelector: (params) => `service_${params.operation}`, + suffix: '_v2', + fallbackToolId: 'service_create_v2', + }), + }, +} +``` + +## Outputs Definition + +**IMPORTANT:** Block outputs have a simpler schema than tool outputs. Block outputs do NOT support: +- `optional: true` - This is only for tool outputs +- `items` property - This is only for tool outputs with array types + +Block outputs only support: +- `type` - The data type ('string', 'number', 'boolean', 'json', 'array') +- `description` - Human readable description +- Nested object structure (for complex types) + +```typescript +outputs: { + // Simple outputs + id: { type: 'string', description: 'Resource ID' }, + success: { type: 'boolean', description: 'Whether operation succeeded' }, + + // Use type: 'json' for complex objects or arrays (NOT type: 'array' with items) + items: { type: 'json', description: 'List of items' }, + metadata: { type: 'json', description: 'Response metadata' }, + + // Nested outputs (for structured data) + user: { + id: { type: 'string', description: 'User ID' }, + name: { type: 'string', description: 'User name' }, + email: { type: 'string', description: 'User email' }, + }, +} +``` + +## V2 Block Pattern + +When creating V2 blocks (alongside legacy V1): + +```typescript +// V1 Block - mark as legacy +export const ServiceBlock: BlockConfig = { + type: 'service', + name: 'Service (Legacy)', + hideFromToolbar: true, // Hide from toolbar + // ... rest of config +} + +// V2 Block - visible, uses V2 tools +export const ServiceV2Block: BlockConfig = { + type: 'service_v2', + name: 'Service', // Clean name + hideFromToolbar: false, // Visible + subBlocks: ServiceBlock.subBlocks, // Reuse UI + tools: { + access: ServiceBlock.tools?.access?.map(id => `${id}_v2`) || [], + config: { + tool: createVersionedToolSelector({ + baseToolSelector: (params) => (ServiceBlock.tools?.config as any)?.tool(params), + suffix: '_v2', + fallbackToolId: 'service_default_v2', + }), + params: ServiceBlock.tools?.config?.params, + }, + }, + outputs: { + // Flat, API-aligned outputs (not wrapped in content/metadata) + }, +} +``` + +## Registering Blocks + +After creating the block, remind the user to: +1. Import in `apps/sim/blocks/registry.ts` +2. Add to the `registry` object (alphabetically): + +```typescript +import { ServiceBlock } from '@/blocks/blocks/service' + +export const registry: Record = { + // ... existing blocks ... + service: ServiceBlock, +} +``` + +## Complete Example + +```typescript +import { ServiceIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' + +export const ServiceBlock: BlockConfig = { + type: 'service', + name: 'Service', + description: 'Integrate with Service API', + longDescription: 'Full description for documentation...', + docsLink: 'https://docs.sim.ai/tools/service', + category: 'tools', + bgColor: '#FF6B6B', + icon: ServiceIcon, + authMode: AuthMode.OAuth, + + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Create', id: 'create' }, + { label: 'Read', id: 'read' }, + { label: 'Update', id: 'update' }, + { label: 'Delete', id: 'delete' }, + ], + value: () => 'create', + }, + { + id: 'credential', + title: 'Service Account', + type: 'oauth-input', + serviceId: 'service', + placeholder: 'Select account', + required: true, + }, + { + id: 'resourceId', + title: 'Resource ID', + type: 'short-input', + placeholder: 'Enter resource ID', + condition: { field: 'operation', value: ['read', 'update', 'delete'] }, + required: { field: 'operation', value: ['read', 'update', 'delete'] }, + }, + { + id: 'name', + title: 'Name', + type: 'short-input', + placeholder: 'Resource name', + condition: { field: 'operation', value: ['create', 'update'] }, + required: { field: 'operation', value: 'create' }, + }, + ], + + tools: { + access: ['service_create', 'service_read', 'service_update', 'service_delete'], + config: { + tool: (params) => `service_${params.operation}`, + }, + }, + + outputs: { + id: { type: 'string', description: 'Resource ID' }, + name: { type: 'string', description: 'Resource name' }, + createdAt: { type: 'string', description: 'Creation timestamp' }, + }, +} +``` + +## Connecting Blocks with Triggers + +If the service supports webhooks, connect the block to its triggers. + +```typescript +import { getTrigger } from '@/triggers' + +export const ServiceBlock: BlockConfig = { + // ... basic config ... + + triggers: { + enabled: true, + available: ['service_event_a', 'service_event_b', 'service_webhook'], + }, + + subBlocks: [ + // Tool subBlocks first... + { id: 'operation', /* ... */ }, + + // Then spread trigger subBlocks + ...getTrigger('service_event_a').subBlocks, + ...getTrigger('service_event_b').subBlocks, + ...getTrigger('service_webhook').subBlocks, + ], +} +``` + +See the `/add-trigger` skill for creating triggers. + +## Checklist Before Finishing + +- [ ] All subBlocks have `id`, `title` (except switch), and `type` +- [ ] Conditions use correct syntax (field, value, not, and) +- [ ] DependsOn set for fields that need other values +- [ ] Required fields marked correctly (boolean or condition) +- [ ] OAuth inputs have correct `serviceId` +- [ ] Tools.access lists all tool IDs +- [ ] Tools.config.tool returns correct tool ID +- [ ] Outputs match tool outputs +- [ ] Block registered in registry.ts +- [ ] If triggers exist: `triggers` config set, trigger subBlocks spread diff --git a/.claude/commands/add-integration.md b/.claude/commands/add-integration.md new file mode 100644 index 0000000000..017bebcffb --- /dev/null +++ b/.claude/commands/add-integration.md @@ -0,0 +1,450 @@ +--- +description: Add a complete integration to Sim Studio (tools, block, icon, registration) +argument-hint: [api-docs-url] +--- + +# Add Integration Skill + +You are an expert at adding complete integrations to Sim Studio. This skill orchestrates the full process of adding a new service integration. + +## Overview + +Adding an integration involves these steps in order: +1. **Research** - Read the service's API documentation +2. **Create Tools** - Build tool configurations for each API operation +3. **Create Block** - Build the block UI configuration +4. **Add Icon** - Add the service's brand icon +5. **Create Triggers** (optional) - If the service supports webhooks +6. **Register** - Register tools, block, and triggers in their registries +7. **Generate Docs** - Run the docs generation script + +## Step 1: Research the API + +Before writing any code: +1. Use Context7 to find official documentation: `mcp__plugin_context7_context7__resolve-library-id` +2. Or use WebFetch to read API docs directly +3. Identify: + - Authentication method (OAuth, API Key, both) + - Available operations (CRUD, search, etc.) + - Required vs optional parameters + - Response structures + +## Step 2: Create Tools + +### Directory Structure +``` +apps/sim/tools/{service}/ +├── index.ts # Barrel exports +├── types.ts # TypeScript interfaces +├── {action1}.ts # Tool for action 1 +├── {action2}.ts # Tool for action 2 +└── ... +``` + +### Key Patterns + +**types.ts:** +```typescript +import type { ToolResponse } from '@/tools/types' + +export interface {Service}{Action}Params { + accessToken: string // For OAuth services + // OR + apiKey: string // For API key services + + requiredParam: string + optionalParam?: string +} + +export interface {Service}Response extends ToolResponse { + output: { + // Define output structure + } +} +``` + +**Tool file pattern:** +```typescript +export const {service}{Action}Tool: ToolConfig = { + id: '{service}_{action}', + name: '{Service} {Action}', + description: '...', + version: '1.0.0', + + oauth: { required: true, provider: '{service}' }, // If OAuth + + params: { + accessToken: { type: 'string', required: true, visibility: 'hidden', description: '...' }, + // ... other params + }, + + request: { url, method, headers, body }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + field: data.field ?? null, // Always handle nullables + }, + } + }, + + outputs: { /* ... */ }, +} +``` + +### Critical Rules +- `visibility: 'hidden'` for OAuth tokens +- `visibility: 'user-only'` for API keys and user credentials +- `visibility: 'user-or-llm'` for operation parameters +- Always use `?? null` for nullable API response fields +- Always use `?? []` for optional array fields +- Set `optional: true` for outputs that may not exist +- Never output raw JSON dumps - extract meaningful fields + +## Step 3: Create Block + +### File Location +`apps/sim/blocks/blocks/{service}.ts` + +### Block Structure +```typescript +import { {Service}Icon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' + +export const {Service}Block: BlockConfig = { + type: '{service}', + name: '{Service}', + description: '...', + longDescription: '...', + docsLink: 'https://docs.sim.ai/tools/{service}', + category: 'tools', + bgColor: '#HEXCOLOR', + icon: {Service}Icon, + authMode: AuthMode.OAuth, // or AuthMode.ApiKey + + subBlocks: [ + // Operation dropdown + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + options: [ + { label: 'Operation 1', id: 'action1' }, + { label: 'Operation 2', id: 'action2' }, + ], + value: () => 'action1', + }, + // Credential field + { + id: 'credential', + title: '{Service} Account', + type: 'oauth-input', + serviceId: '{service}', + required: true, + }, + // Conditional fields per operation + // ... + ], + + tools: { + access: ['{service}_action1', '{service}_action2'], + config: { + tool: (params) => `{service}_${params.operation}`, + }, + }, + + outputs: { /* ... */ }, +} +``` + +### Key SubBlock Patterns + +**Condition-based visibility:** +```typescript +{ + id: 'resourceId', + title: 'Resource ID', + type: 'short-input', + condition: { field: 'operation', value: ['read', 'update', 'delete'] }, + required: { field: 'operation', value: ['read', 'update', 'delete'] }, +} +``` + +**DependsOn for cascading selectors:** +```typescript +{ + id: 'project', + type: 'project-selector', + dependsOn: ['credential'], +}, +{ + id: 'issue', + type: 'file-selector', + dependsOn: ['credential', 'project'], +} +``` + +**Basic/Advanced mode for dual UX:** +```typescript +// Basic: Visual selector +{ + id: 'channel', + type: 'channel-selector', + mode: 'basic', + canonicalParamId: 'channel', + dependsOn: ['credential'], +}, +// Advanced: Manual input +{ + id: 'channelId', + type: 'short-input', + mode: 'advanced', + canonicalParamId: 'channel', +} +``` + +**Critical:** +- `canonicalParamId` must NOT match any other subblock's `id`, must be unique per block, and should only be used to link basic/advanced alternatives for the same parameter. +- `mode` only controls UI visibility, NOT serialization. Without `canonicalParamId`, both basic and advanced field values would be sent. +- Every subblock `id` must be unique within the block. Duplicate IDs cause conflicts even with different conditions. + +## Step 4: Add Icon + +### File Location +`apps/sim/components/icons.tsx` + +### Pattern +```typescript +export function {Service}Icon(props: SVGProps) { + return ( + + {/* SVG paths from brand assets */} + + ) +} +``` + +### Finding Icons +1. Check the service's brand/press kit page +2. Download SVG logo +3. Convert to React component +4. Ensure it accepts and spreads props + +## Step 5: Create Triggers (Optional) + +If the service supports webhooks, create triggers using the generic `buildTriggerSubBlocks` helper. + +### Directory Structure +``` +apps/sim/triggers/{service}/ +├── index.ts # Barrel exports +├── utils.ts # Trigger options, setup instructions, extra fields +├── {event_a}.ts # Primary trigger (includes dropdown) +├── {event_b}.ts # Secondary triggers (no dropdown) +└── webhook.ts # Generic webhook (optional) +``` + +### Key Pattern + +```typescript +import { buildTriggerSubBlocks } from '@/triggers' +import { {service}TriggerOptions, {service}SetupInstructions, build{Service}ExtraFields } from './utils' + +// Primary trigger - includeDropdown: true +export const {service}EventATrigger: TriggerConfig = { + id: '{service}_event_a', + subBlocks: buildTriggerSubBlocks({ + triggerId: '{service}_event_a', + triggerOptions: {service}TriggerOptions, + includeDropdown: true, // Only for primary trigger! + setupInstructions: {service}SetupInstructions('Event A'), + extraFields: build{Service}ExtraFields('{service}_event_a'), + }), + // ... +} + +// Secondary triggers - no dropdown +export const {service}EventBTrigger: TriggerConfig = { + id: '{service}_event_b', + subBlocks: buildTriggerSubBlocks({ + triggerId: '{service}_event_b', + triggerOptions: {service}TriggerOptions, + // No includeDropdown! + setupInstructions: {service}SetupInstructions('Event B'), + extraFields: build{Service}ExtraFields('{service}_event_b'), + }), + // ... +} +``` + +### Connect to Block +```typescript +import { getTrigger } from '@/triggers' + +export const {Service}Block: BlockConfig = { + triggers: { + enabled: true, + available: ['{service}_event_a', '{service}_event_b'], + }, + subBlocks: [ + // Tool fields... + ...getTrigger('{service}_event_a').subBlocks, + ...getTrigger('{service}_event_b').subBlocks, + ], +} +``` + +See `/add-trigger` skill for complete documentation. + +## Step 6: Register Everything + +### Tools Registry (`apps/sim/tools/registry.ts`) + +```typescript +// Add import (alphabetically) +import { + {service}Action1Tool, + {service}Action2Tool, +} from '@/tools/{service}' + +// Add to tools object (alphabetically) +export const tools: Record = { + // ... existing tools ... + {service}_action1: {service}Action1Tool, + {service}_action2: {service}Action2Tool, +} +``` + +### Block Registry (`apps/sim/blocks/registry.ts`) + +```typescript +// Add import (alphabetically) +import { {Service}Block } from '@/blocks/blocks/{service}' + +// Add to registry (alphabetically) +export const registry: Record = { + // ... existing blocks ... + {service}: {Service}Block, +} +``` + +### Trigger Registry (`apps/sim/triggers/registry.ts`) - If triggers exist + +```typescript +// Add import (alphabetically) +import { + {service}EventATrigger, + {service}EventBTrigger, + {service}WebhookTrigger, +} from '@/triggers/{service}' + +// Add to TRIGGER_REGISTRY (alphabetically) +export const TRIGGER_REGISTRY: TriggerRegistry = { + // ... existing triggers ... + {service}_event_a: {service}EventATrigger, + {service}_event_b: {service}EventBTrigger, + {service}_webhook: {service}WebhookTrigger, +} +``` + +## Step 7: Generate Docs + +Run the documentation generator: +```bash +bun run scripts/generate-docs.ts +``` + +This creates `apps/docs/content/docs/en/tools/{service}.mdx` + +## V2 Integration Pattern + +If creating V2 versions (API-aligned outputs): + +1. **V2 Tools** - Add `_v2` suffix, version `2.0.0`, flat outputs +2. **V2 Block** - Add `_v2` type, use `createVersionedToolSelector` +3. **V1 Block** - Add `(Legacy)` to name, set `hideFromToolbar: true` +4. **Registry** - Register both versions + +```typescript +// In registry +{service}: {Service}Block, // V1 (legacy, hidden) +{service}_v2: {Service}V2Block, // V2 (visible) +``` + +## Complete Checklist + +### Tools +- [ ] Created `tools/{service}/` directory +- [ ] Created `types.ts` with all interfaces +- [ ] Created tool file for each operation +- [ ] All params have correct visibility +- [ ] All nullable fields use `?? null` +- [ ] All optional outputs have `optional: true` +- [ ] Created `index.ts` barrel export +- [ ] Registered all tools in `tools/registry.ts` + +### Block +- [ ] Created `blocks/blocks/{service}.ts` +- [ ] Defined operation dropdown with all operations +- [ ] Added credential field (oauth-input or short-input) +- [ ] Added conditional fields per operation +- [ ] Set up dependsOn for cascading selectors +- [ ] Configured tools.access with all tool IDs +- [ ] Configured tools.config.tool selector +- [ ] Defined outputs matching tool outputs +- [ ] Registered block in `blocks/registry.ts` +- [ ] If triggers: set `triggers.enabled` and `triggers.available` +- [ ] If triggers: spread trigger subBlocks with `getTrigger()` + +### Icon +- [ ] Added icon to `components/icons.tsx` +- [ ] Icon spreads props correctly + +### Triggers (if service supports webhooks) +- [ ] Created `triggers/{service}/` directory +- [ ] Created `utils.ts` with options, instructions, and extra fields helpers +- [ ] Primary trigger uses `includeDropdown: true` +- [ ] Secondary triggers do NOT have `includeDropdown` +- [ ] All triggers use `buildTriggerSubBlocks` helper +- [ ] Created `index.ts` barrel export +- [ ] Registered all triggers in `triggers/registry.ts` + +### Docs +- [ ] Ran `bun run scripts/generate-docs.ts` +- [ ] Verified docs file created + +## Example Command + +When the user asks to add an integration: + +``` +User: Add a Stripe integration + +You: I'll add the Stripe integration. Let me: + +1. First, research the Stripe API using Context7 +2. Create the tools for key operations (payments, subscriptions, etc.) +3. Create the block with operation dropdown +4. Add the Stripe icon +5. Register everything +6. Generate docs + +[Proceed with implementation...] +``` + +## Common Gotchas + +1. **OAuth serviceId must match** - The `serviceId` in oauth-input must match the OAuth provider configuration +2. **Tool IDs are snake_case** - `stripe_create_payment`, not `stripeCreatePayment` +3. **Block type is snake_case** - `type: 'stripe'`, not `type: 'Stripe'` +4. **Alphabetical ordering** - Keep imports and registry entries alphabetically sorted +5. **Required can be conditional** - Use `required: { field: 'op', value: 'create' }` instead of always true +6. **DependsOn clears options** - When a dependency changes, selector options are refetched diff --git a/.claude/commands/add-tools.md b/.claude/commands/add-tools.md new file mode 100644 index 0000000000..d4dfee8706 --- /dev/null +++ b/.claude/commands/add-tools.md @@ -0,0 +1,284 @@ +--- +description: Create tool configurations for a Sim Studio integration by reading API docs +argument-hint: [api-docs-url] +--- + +# Add Tools Skill + +You are an expert at creating tool configurations for Sim Studio integrations. Your job is to read API documentation and create properly structured tool files. + +## Your Task + +When the user asks you to create tools for a service: +1. Use Context7 or WebFetch to read the service's API documentation +2. Create the tools directory structure +3. Generate properly typed tool configurations + +## Directory Structure + +Create files in `apps/sim/tools/{service}/`: +``` +tools/{service}/ +├── index.ts # Barrel export +├── types.ts # Parameter & response types +└── {action}.ts # Individual tool files (one per operation) +``` + +## Tool Configuration Structure + +Every tool MUST follow this exact structure: + +```typescript +import type { {ServiceName}{Action}Params } from '@/tools/{service}/types' +import type { ToolConfig } from '@/tools/types' + +interface {ServiceName}{Action}Response { + success: boolean + output: { + // Define output structure here + } +} + +export const {serviceName}{Action}Tool: ToolConfig< + {ServiceName}{Action}Params, + {ServiceName}{Action}Response +> = { + id: '{service}_{action}', // snake_case, matches tool name + name: '{Service} {Action}', // Human readable + description: 'Brief description', // One sentence + version: '1.0.0', + + // OAuth config (if service uses OAuth) + oauth: { + required: true, + provider: '{service}', // Must match OAuth provider ID + }, + + params: { + // Hidden params (system-injected) + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + // User-only params (credentials, IDs user must provide) + someId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'The ID of the resource', + }, + // User-or-LLM params (can be provided by user OR computed by LLM) + query: { + type: 'string', + required: false, // Use false for optional + visibility: 'user-or-llm', + description: 'Search query', + }, + }, + + request: { + url: (params) => `https://api.service.com/v1/resource/${params.id}`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + // Request body - only for POST/PUT/PATCH + // Trim ID fields to prevent copy-paste whitespace errors: + // userId: params.userId?.trim(), + }), + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + // Map API response to output + // Use ?? null for nullable fields + // Use ?? [] for optional arrays + }, + } + }, + + outputs: { + // Define each output field + }, +} +``` + +## Critical Rules for Parameters + +### Visibility Options +- `'hidden'` - System-injected (OAuth tokens, internal params). User never sees. +- `'user-only'` - User must provide (credentials, account-specific IDs) +- `'user-or-llm'` - User provides OR LLM can compute (search queries, content, filters) + +### Parameter Types +- `'string'` - Text values +- `'number'` - Numeric values +- `'boolean'` - True/false +- `'json'` - Complex objects (NOT 'object', use 'json') +- `'file'` - Single file +- `'file[]'` - Multiple files + +### Required vs Optional +- Always explicitly set `required: true` or `required: false` +- Optional params should have `required: false` + +## Critical Rules for Outputs + +### Output Types +- `'string'`, `'number'`, `'boolean'` - Primitives +- `'json'` - Complex objects (use this, NOT 'object') +- `'array'` - Arrays with `items` property +- `'object'` - Objects with `properties` property + +### Optional Outputs +Add `optional: true` for fields that may not exist in the response: +```typescript +closedAt: { + type: 'string', + description: 'When the issue was closed', + optional: true, +}, +``` + +### Nested Properties +For complex outputs, define nested structure: +```typescript +metadata: { + type: 'json', + description: 'Response metadata', + properties: { + id: { type: 'string', description: 'Unique ID' }, + status: { type: 'string', description: 'Current status' }, + count: { type: 'number', description: 'Total count' }, + }, +}, + +items: { + type: 'array', + description: 'List of items', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Item ID' }, + name: { type: 'string', description: 'Item name' }, + }, + }, +}, +``` + +## Critical Rules for transformResponse + +### Handle Nullable Fields +ALWAYS use `?? null` for fields that may be undefined: +```typescript +transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { + id: data.id, + title: data.title, + body: data.body ?? null, // May be undefined + assignee: data.assignee ?? null, // May be undefined + labels: data.labels ?? [], // Default to empty array + closedAt: data.closed_at ?? null, // May be undefined + }, + } +} +``` + +### Never Output Raw JSON Dumps +DON'T do this: +```typescript +output: { + data: data, // BAD - raw JSON dump +} +``` + +DO this instead - extract meaningful fields: +```typescript +output: { + id: data.id, + name: data.name, + status: data.status, + metadata: { + createdAt: data.created_at, + updatedAt: data.updated_at, + }, +} +``` + +## Types File Pattern + +Create `types.ts` with interfaces for all params and responses: + +```typescript +import type { ToolResponse } from '@/tools/types' + +// Parameter interfaces +export interface {Service}{Action}Params { + accessToken: string + requiredField: string + optionalField?: string +} + +// Response interfaces (extend ToolResponse) +export interface {Service}{Action}Response extends ToolResponse { + output: { + field1: string + field2: number + optionalField?: string | null + } +} +``` + +## Index.ts Barrel Export Pattern + +```typescript +// Export all tools +export { serviceTool1 } from './{action1}' +export { serviceTool2 } from './{action2}' + +// Export types +export * from './types' +``` + +## Registering Tools + +After creating tools, remind the user to: +1. Import tools in `apps/sim/tools/registry.ts` +2. Add to the `tools` object with snake_case keys: +```typescript +import { serviceActionTool } from '@/tools/{service}' + +export const tools = { + // ... existing tools ... + {service}_{action}: serviceActionTool, +} +``` + +## V2 Tool Pattern + +If creating V2 tools (API-aligned outputs), use `_v2` suffix: +- Tool ID: `{service}_{action}_v2` +- Variable name: `{action}V2Tool` +- Version: `'2.0.0'` +- Outputs: Flat, API-aligned (no content/metadata wrapper) + +## Checklist Before Finishing + +- [ ] All params have explicit `required: true` or `required: false` +- [ ] All params have appropriate `visibility` +- [ ] All nullable response fields use `?? null` +- [ ] All optional outputs have `optional: true` +- [ ] No raw JSON dumps in outputs +- [ ] Types file has all interfaces +- [ ] Index.ts exports all tools +- [ ] Tool IDs use snake_case diff --git a/.claude/commands/add-trigger.md b/.claude/commands/add-trigger.md new file mode 100644 index 0000000000..461563ab0c --- /dev/null +++ b/.claude/commands/add-trigger.md @@ -0,0 +1,656 @@ +--- +description: Create webhook triggers for a Sim Studio integration using the generic trigger builder +argument-hint: +--- + +# Add Trigger Skill + +You are an expert at creating webhook triggers for Sim Studio. You understand the trigger system, the generic `buildTriggerSubBlocks` helper, and how triggers connect to blocks. + +## Your Task + +When the user asks you to create triggers for a service: +1. Research what webhook events the service supports +2. Create the trigger files using the generic builder +3. Register triggers and connect them to the block + +## Directory Structure + +``` +apps/sim/triggers/{service}/ +├── index.ts # Barrel exports +├── utils.ts # Service-specific helpers (trigger options, setup instructions, extra fields) +├── {event_a}.ts # Primary trigger (includes dropdown) +├── {event_b}.ts # Secondary trigger (no dropdown) +├── {event_c}.ts # Secondary trigger (no dropdown) +└── webhook.ts # Generic webhook trigger (optional, for "all events") +``` + +## Step 1: Create utils.ts + +This file contains service-specific helpers used by all triggers. + +```typescript +import type { SubBlockConfig } from '@/blocks/types' +import type { TriggerOutput } from '@/triggers/types' + +/** + * Dropdown options for the trigger type selector. + * These appear in the primary trigger's dropdown. + */ +export const {service}TriggerOptions = [ + { label: 'Event A', id: '{service}_event_a' }, + { label: 'Event B', id: '{service}_event_b' }, + { label: 'Event C', id: '{service}_event_c' }, + { label: 'Generic Webhook (All Events)', id: '{service}_webhook' }, +] + +/** + * Generates HTML setup instructions for the trigger. + * Displayed to users to help them configure webhooks in the external service. + */ +export function {service}SetupInstructions(eventType: string): string { + const instructions = [ + 'Copy the Webhook URL above', + 'Go to {Service} Settings > Webhooks', + 'Click Add Webhook', + 'Paste the webhook URL', + `Select the ${eventType} event type`, + 'Save the webhook configuration', + 'Click "Save" above to activate your trigger', + ] + + return instructions + .map((instruction, index) => + `
${index + 1}. ${instruction}
` + ) + .join('') +} + +/** + * Service-specific extra fields to add to triggers. + * These are inserted between webhookUrl and triggerSave. + */ +export function build{Service}ExtraFields(triggerId: string): SubBlockConfig[] { + return [ + { + id: 'projectId', + title: 'Project ID (Optional)', + type: 'short-input', + placeholder: 'Leave empty for all projects', + description: 'Optionally filter to a specific project', + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + ] +} + +/** + * Build outputs for this trigger type. + * Outputs define what data is available to downstream blocks. + */ +export function build{Service}Outputs(): Record { + return { + eventType: { type: 'string', description: 'The type of event that triggered this workflow' }, + resourceId: { type: 'string', description: 'ID of the affected resource' }, + timestamp: { type: 'string', description: 'When the event occurred (ISO 8601)' }, + // Nested outputs for complex data + resource: { + id: { type: 'string', description: 'Resource ID' }, + name: { type: 'string', description: 'Resource name' }, + status: { type: 'string', description: 'Current status' }, + }, + webhook: { type: 'json', description: 'Full webhook payload' }, + } +} +``` + +## Step 2: Create the Primary Trigger + +The **primary trigger** is the first one listed. It MUST include `includeDropdown: true` so users can switch between trigger types. + +```typescript +import { {Service}Icon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + build{Service}ExtraFields, + build{Service}Outputs, + {service}SetupInstructions, + {service}TriggerOptions, +} from '@/triggers/{service}/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * {Service} Event A Trigger + * + * This is the PRIMARY trigger - it includes the dropdown for selecting trigger type. + */ +export const {service}EventATrigger: TriggerConfig = { + id: '{service}_event_a', + name: '{Service} Event A', + provider: '{service}', + description: 'Trigger workflow when Event A occurs', + version: '1.0.0', + icon: {Service}Icon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: '{service}_event_a', + triggerOptions: {service}TriggerOptions, + includeDropdown: true, // PRIMARY TRIGGER - includes dropdown + setupInstructions: {service}SetupInstructions('Event A'), + extraFields: build{Service}ExtraFields('{service}_event_a'), + }), + + outputs: build{Service}Outputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} +``` + +## Step 3: Create Secondary Triggers + +Secondary triggers do NOT include the dropdown (it's already in the primary trigger). + +```typescript +import { {Service}Icon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import { + build{Service}ExtraFields, + build{Service}Outputs, + {service}SetupInstructions, + {service}TriggerOptions, +} from '@/triggers/{service}/utils' +import type { TriggerConfig } from '@/triggers/types' + +/** + * {Service} Event B Trigger + */ +export const {service}EventBTrigger: TriggerConfig = { + id: '{service}_event_b', + name: '{Service} Event B', + provider: '{service}', + description: 'Trigger workflow when Event B occurs', + version: '1.0.0', + icon: {Service}Icon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: '{service}_event_b', + triggerOptions: {service}TriggerOptions, + // NO includeDropdown - secondary trigger + setupInstructions: {service}SetupInstructions('Event B'), + extraFields: build{Service}ExtraFields('{service}_event_b'), + }), + + outputs: build{Service}Outputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} +``` + +## Step 4: Create index.ts Barrel Export + +```typescript +export { {service}EventATrigger } from './event_a' +export { {service}EventBTrigger } from './event_b' +export { {service}EventCTrigger } from './event_c' +export { {service}WebhookTrigger } from './webhook' +``` + +## Step 5: Register Triggers + +### Trigger Registry (`apps/sim/triggers/registry.ts`) + +```typescript +// Add import +import { + {service}EventATrigger, + {service}EventBTrigger, + {service}EventCTrigger, + {service}WebhookTrigger, +} from '@/triggers/{service}' + +// Add to TRIGGER_REGISTRY +export const TRIGGER_REGISTRY: TriggerRegistry = { + // ... existing triggers ... + {service}_event_a: {service}EventATrigger, + {service}_event_b: {service}EventBTrigger, + {service}_event_c: {service}EventCTrigger, + {service}_webhook: {service}WebhookTrigger, +} +``` + +## Step 6: Connect Triggers to Block + +In the block file (`apps/sim/blocks/blocks/{service}.ts`): + +```typescript +import { {Service}Icon } from '@/components/icons' +import { getTrigger } from '@/triggers' +import type { BlockConfig } from '@/blocks/types' + +export const {Service}Block: BlockConfig = { + type: '{service}', + name: '{Service}', + // ... other config ... + + // Enable triggers and list available trigger IDs + triggers: { + enabled: true, + available: [ + '{service}_event_a', + '{service}_event_b', + '{service}_event_c', + '{service}_webhook', + ], + }, + + subBlocks: [ + // Regular tool subBlocks first + { id: 'operation', /* ... */ }, + { id: 'credential', /* ... */ }, + // ... other tool fields ... + + // Then spread ALL trigger subBlocks + ...getTrigger('{service}_event_a').subBlocks, + ...getTrigger('{service}_event_b').subBlocks, + ...getTrigger('{service}_event_c').subBlocks, + ...getTrigger('{service}_webhook').subBlocks, + ], + + // ... tools config ... +} +``` + +## Automatic Webhook Registration (Preferred) + +If the service's API supports programmatic webhook creation, implement automatic webhook registration instead of requiring users to manually configure webhooks. This provides a much better user experience. + +### When to Use Automatic Registration + +Check the service's API documentation for endpoints like: +- `POST /webhooks` or `POST /hooks` - Create webhook +- `DELETE /webhooks/{id}` - Delete webhook + +Services that support this pattern include: Grain, Lemlist, Calendly, Airtable, Webflow, Typeform, etc. + +### Implementation Steps + +#### 1. Add API Key to Extra Fields + +Update your `build{Service}ExtraFields` function to include an API key field: + +```typescript +export function build{Service}ExtraFields(triggerId: string): SubBlockConfig[] { + return [ + { + id: 'apiKey', + title: 'API Key', + type: 'short-input', + placeholder: 'Enter your {Service} API key', + description: 'Required to create the webhook in {Service}.', + password: true, + required: true, + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + // Other optional fields (e.g., campaign filter, project filter) + { + id: 'projectId', + title: 'Project ID (Optional)', + type: 'short-input', + placeholder: 'Leave empty for all projects', + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + ] +} +``` + +#### 2. Update Setup Instructions for Automatic Creation + +Change instructions to indicate automatic webhook creation: + +```typescript +export function {service}SetupInstructions(eventType: string): string { + const instructions = [ + 'Enter your {Service} API Key above.', + 'You can find your API key in {Service} at Settings > API.', + `Click "Save Configuration" to automatically create the webhook in {Service} for ${eventType} events.`, + 'The webhook will be automatically deleted when you remove this trigger.', + ] + + return instructions + .map((instruction, index) => + `
${index + 1}. ${instruction}
` + ) + .join('') +} +``` + +#### 3. Add Webhook Creation to API Route + +In `apps/sim/app/api/webhooks/route.ts`, add provider-specific logic after the database save: + +```typescript +// --- {Service} specific logic --- +if (savedWebhook && provider === '{service}') { + logger.info(`[${requestId}] {Service} provider detected. Creating webhook subscription.`) + try { + const result = await create{Service}WebhookSubscription( + { + id: savedWebhook.id, + path: savedWebhook.path, + providerConfig: savedWebhook.providerConfig, + }, + requestId + ) + + if (result) { + // Update the webhook record with the external webhook ID + const updatedConfig = { + ...(savedWebhook.providerConfig as Record), + externalId: result.id, + } + await db + .update(webhook) + .set({ + providerConfig: updatedConfig, + updatedAt: new Date(), + }) + .where(eq(webhook.id, savedWebhook.id)) + + savedWebhook.providerConfig = updatedConfig + logger.info(`[${requestId}] Successfully created {Service} webhook`, { + externalHookId: result.id, + webhookId: savedWebhook.id, + }) + } + } catch (err) { + logger.error( + `[${requestId}] Error creating {Service} webhook subscription, rolling back webhook`, + err + ) + await db.delete(webhook).where(eq(webhook.id, savedWebhook.id)) + return NextResponse.json( + { + error: 'Failed to create webhook in {Service}', + details: err instanceof Error ? err.message : 'Unknown error', + }, + { status: 500 } + ) + } +} +// --- End {Service} specific logic --- +``` + +Then add the helper function at the end of the file: + +```typescript +async function create{Service}WebhookSubscription( + webhookData: any, + requestId: string +): Promise<{ id: string } | undefined> { + try { + const { path, providerConfig } = webhookData + const { apiKey, triggerId, projectId } = providerConfig || {} + + if (!apiKey) { + throw new Error('{Service} API Key is required.') + } + + // Map trigger IDs to service event types + const eventTypeMap: Record = { + {service}_event_a: 'eventA', + {service}_event_b: 'eventB', + {service}_webhook: undefined, // Generic - no filter + } + + const eventType = eventTypeMap[triggerId] + const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}` + + const requestBody: Record = { + url: notificationUrl, + } + + if (eventType) { + requestBody.eventType = eventType + } + + if (projectId) { + requestBody.projectId = projectId + } + + const response = await fetch('https://api.{service}.com/webhooks', { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }) + + const responseBody = await response.json() + + if (!response.ok) { + const errorMessage = responseBody.message || 'Unknown API error' + let userFriendlyMessage = 'Failed to create webhook in {Service}' + + if (response.status === 401) { + userFriendlyMessage = 'Invalid API Key. Please verify and try again.' + } else if (errorMessage) { + userFriendlyMessage = `{Service} error: ${errorMessage}` + } + + throw new Error(userFriendlyMessage) + } + + return { id: responseBody.id } + } catch (error: any) { + logger.error(`Exception during {Service} webhook creation`, { error: error.message }) + throw error + } +} +``` + +#### 4. Add Webhook Deletion to Provider Subscriptions + +In `apps/sim/lib/webhooks/provider-subscriptions.ts`: + +1. Add a logger: +```typescript +const {service}Logger = createLogger('{Service}Webhook') +``` + +2. Add the delete function: +```typescript +export async function delete{Service}Webhook(webhook: any, requestId: string): Promise { + try { + const config = getProviderConfig(webhook) + const apiKey = config.apiKey as string | undefined + const externalId = config.externalId as string | undefined + + if (!apiKey || !externalId) { + {service}Logger.warn(`[${requestId}] Missing apiKey or externalId, skipping cleanup`) + return + } + + const response = await fetch(`https://api.{service}.com/webhooks/${externalId}`, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${apiKey}`, + }, + }) + + if (!response.ok && response.status !== 404) { + {service}Logger.warn(`[${requestId}] Failed to delete webhook (non-fatal): ${response.status}`) + } else { + {service}Logger.info(`[${requestId}] Successfully deleted webhook ${externalId}`) + } + } catch (error) { + {service}Logger.warn(`[${requestId}] Error deleting webhook (non-fatal)`, error) + } +} +``` + +3. Add to `cleanupExternalWebhook`: +```typescript +export async function cleanupExternalWebhook(...): Promise { + // ... existing providers ... + } else if (webhook.provider === '{service}') { + await delete{Service}Webhook(webhook, requestId) + } +} +``` + +### Key Points for Automatic Registration + +- **API Key visibility**: Always use `password: true` for API key fields +- **Error handling**: Roll back the database webhook if external creation fails +- **External ID storage**: Save the external webhook ID in `providerConfig.externalId` +- **Graceful cleanup**: Don't fail webhook deletion if cleanup fails (use non-fatal logging) +- **User-friendly errors**: Map HTTP status codes to helpful error messages + +## The buildTriggerSubBlocks Helper + +This is the generic helper from `@/triggers` that creates consistent trigger subBlocks. + +### Function Signature + +```typescript +interface BuildTriggerSubBlocksOptions { + triggerId: string // e.g., 'service_event_a' + triggerOptions: Array<{ label: string; id: string }> // Dropdown options + includeDropdown?: boolean // true only for primary trigger + setupInstructions: string // HTML instructions + extraFields?: SubBlockConfig[] // Service-specific fields + webhookPlaceholder?: string // Custom placeholder text +} + +function buildTriggerSubBlocks(options: BuildTriggerSubBlocksOptions): SubBlockConfig[] +``` + +### What It Creates + +The helper creates this structure: +1. **Dropdown** (only if `includeDropdown: true`) - Trigger type selector +2. **Webhook URL** - Read-only field with copy button +3. **Extra Fields** - Your service-specific fields (filters, options, etc.) +4. **Save Button** - Activates the trigger +5. **Instructions** - Setup guide for users + +All fields automatically have: +- `mode: 'trigger'` - Only shown in trigger mode +- `condition: { field: 'selectedTriggerId', value: triggerId }` - Only shown when this trigger is selected + +## Trigger Outputs + +Trigger outputs use the same schema as block outputs (NOT tool outputs). + +**Supported:** +- `type` and `description` for simple fields +- Nested object structure for complex data + +**NOT Supported:** +- `optional: true` (tool outputs only) +- `items` property (tool outputs only) + +```typescript +export function buildOutputs(): Record { + return { + // Simple fields + eventType: { type: 'string', description: 'Event type' }, + timestamp: { type: 'string', description: 'When it occurred' }, + + // Complex data - use type: 'json' + payload: { type: 'json', description: 'Full event payload' }, + + // Nested structure + resource: { + id: { type: 'string', description: 'Resource ID' }, + name: { type: 'string', description: 'Resource name' }, + }, + } +} +``` + +## Generic Webhook Trigger Pattern + +For services with many event types, create a generic webhook that accepts all events: + +```typescript +export const {service}WebhookTrigger: TriggerConfig = { + id: '{service}_webhook', + name: '{Service} Webhook (All Events)', + // ... + + subBlocks: buildTriggerSubBlocks({ + triggerId: '{service}_webhook', + triggerOptions: {service}TriggerOptions, + setupInstructions: {service}SetupInstructions('All Events'), + extraFields: [ + // Event type filter (optional) + { + id: 'eventTypes', + title: 'Event Types', + type: 'dropdown', + multiSelect: true, + options: [ + { label: 'Event A', id: 'event_a' }, + { label: 'Event B', id: 'event_b' }, + ], + placeholder: 'Leave empty for all events', + mode: 'trigger', + condition: { field: 'selectedTriggerId', value: '{service}_webhook' }, + }, + // Plus any other service-specific fields + ...build{Service}ExtraFields('{service}_webhook'), + ], + }), +} +``` + +## Checklist Before Finishing + +### Utils +- [ ] Created `{service}TriggerOptions` array with all trigger IDs +- [ ] Created `{service}SetupInstructions` function with clear steps +- [ ] Created `build{Service}ExtraFields` for service-specific fields +- [ ] Created output builders for each trigger type + +### Triggers +- [ ] Primary trigger has `includeDropdown: true` +- [ ] Secondary triggers do NOT have `includeDropdown` +- [ ] All triggers use `buildTriggerSubBlocks` helper +- [ ] All triggers have proper outputs defined +- [ ] Created `index.ts` barrel export + +### Registration +- [ ] All triggers imported in `triggers/registry.ts` +- [ ] All triggers added to `TRIGGER_REGISTRY` +- [ ] Block has `triggers.enabled: true` +- [ ] Block has all trigger IDs in `triggers.available` +- [ ] Block spreads all trigger subBlocks: `...getTrigger('id').subBlocks` + +### Automatic Webhook Registration (if supported) +- [ ] Added API key field to `build{Service}ExtraFields` with `password: true` +- [ ] Updated setup instructions for automatic webhook creation +- [ ] Added provider-specific logic to `apps/sim/app/api/webhooks/route.ts` +- [ ] Added `create{Service}WebhookSubscription` helper function +- [ ] Added `delete{Service}Webhook` function to `provider-subscriptions.ts` +- [ ] Added provider to `cleanupExternalWebhook` function + +### Testing +- [ ] Run `bun run type-check` to verify no TypeScript errors +- [ ] Restart dev server to pick up new triggers +- [ ] Test trigger UI shows correctly in the block +- [ ] Test automatic webhook creation works (if applicable) diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index de0ab92021..2c46036dbf 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -1853,6 +1853,23 @@ export function LinearIcon(props: React.SVGProps) { ) } +export function LemlistIcon(props: SVGProps) { + return ( + + + + + + ) +} + export function TelegramIcon(props: SVGProps) { return ( = { circleback: CirclebackIcon, clay: ClayIcon, confluence: ConfluenceIcon, - cursor: CursorIcon, + cursor_v2: CursorIcon, datadog: DatadogIcon, discord: DiscordIcon, dropbox: DropboxIcon, @@ -149,10 +150,10 @@ export const blockTypeToIconMap: Record = { file: DocumentIcon, firecrawl: FirecrawlIcon, fireflies: FirefliesIcon, - github: GithubIcon, + github_v2: GithubIcon, gitlab: GitLabIcon, - gmail: GmailIcon, - google_calendar: GoogleCalendarIcon, + gmail_v2: GmailIcon, + google_calendar_v2: GoogleCalendarIcon, google_docs: GoogleDocsIcon, google_drive: GoogleDriveIcon, google_forms: GoogleFormsIcon, @@ -170,12 +171,13 @@ export const blockTypeToIconMap: Record = { image_generator: ImageIcon, imap: MailServerIcon, incidentio: IncidentioIcon, - intercom: IntercomIcon, + intercom_v2: IntercomIcon, jina: JinaAIIcon, jira: JiraIcon, jira_service_management: JiraServiceManagementIcon, kalshi: KalshiIcon, knowledge: PackageSearchIcon, + lemlist: LemlistIcon, linear: LinearIcon, linkedin: LinkedInIcon, linkup: LinkupIcon, @@ -190,7 +192,7 @@ export const blockTypeToIconMap: Record = { mongodb: MongoDBIcon, mysql: MySQLIcon, neo4j: Neo4jIcon, - notion: NotionIcon, + notion_v2: NotionIcon, onedrive: MicrosoftOneDriveIcon, openai: OpenAIIcon, outlook: OutlookIcon, @@ -226,7 +228,6 @@ export const blockTypeToIconMap: Record = { supabase: SupabaseIcon, tavily: TavilyIcon, telegram: TelegramIcon, - thinking: BrainIcon, translate: TranslateIcon, trello: TrelloIcon, tts: TTSIcon, diff --git a/apps/docs/content/docs/de/enterprise/index.mdx b/apps/docs/content/docs/de/enterprise/index.mdx index 109b196491..82682f260b 100644 --- a/apps/docs/content/docs/de/enterprise/index.mdx +++ b/apps/docs/content/docs/de/enterprise/index.mdx @@ -70,6 +70,7 @@ Für selbst gehostete Bereitstellungen können Enterprise-Funktionen über Umgeb |----------|-------------| | `SSO_ENABLED`, `NEXT_PUBLIC_SSO_ENABLED` | Single Sign-On mit SAML/OIDC | | `CREDENTIAL_SETS_ENABLED`, `NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED` | Polling-Gruppen für E-Mail-Trigger | +| `DISABLE_INVITATIONS`, `NEXT_PUBLIC_DISABLE_INVITATIONS` | Workspace-/Organisations-Einladungen global deaktivieren | BYOK ist nur im gehosteten Sim Studio verfügbar. Selbst gehostete Deployments konfigurieren AI-Provider-Schlüssel direkt über Umgebungsvariablen. diff --git a/apps/docs/content/docs/en/enterprise/index.mdx b/apps/docs/content/docs/en/enterprise/index.mdx index 3e5acdf5e2..29abe99844 100644 --- a/apps/docs/content/docs/en/enterprise/index.mdx +++ b/apps/docs/content/docs/en/enterprise/index.mdx @@ -17,7 +17,7 @@ Define permission groups to control what features and integrations team members - **Allowed Model Providers** - Restrict which AI providers users can access (OpenAI, Anthropic, Google, etc.) - **Allowed Blocks** - Control which workflow blocks are available -- **Platform Settings** - Hide Knowledge Base, disable MCP tools, or disable custom tools +- **Platform Settings** - Hide Knowledge Base, disable MCP tools, disable custom tools, or disable invitations ### Setup @@ -31,33 +31,6 @@ Define permission groups to control what features and integrations team members --- -## Bring Your Own Key (BYOK) - -Use your own API keys for AI model providers instead of Sim Studio's hosted keys. - -### Supported Providers - -| Provider | Usage | -|----------|-------| -| OpenAI | Knowledge Base embeddings, Agent block | -| Anthropic | Agent block | -| Google | Agent block | -| Mistral | Knowledge Base OCR | - -### Setup - -1. Navigate to **Settings** → **BYOK** in your workspace -2. Click **Add Key** for your provider -3. Enter your API key and save - - - BYOK keys are encrypted at rest. Only organization admins and owners can manage keys. - - -When configured, workflows use your key instead of Sim Studio's hosted keys. If removed, workflows automatically fall back to hosted keys. - ---- - ## Single Sign-On (SSO) Enterprise authentication with SAML 2.0 and OIDC support for centralized identity management. @@ -95,6 +68,7 @@ For self-hosted deployments, enterprise features can be enabled via environment | `ACCESS_CONTROL_ENABLED`, `NEXT_PUBLIC_ACCESS_CONTROL_ENABLED` | Permission groups for access restrictions | | `SSO_ENABLED`, `NEXT_PUBLIC_SSO_ENABLED` | Single Sign-On with SAML/OIDC | | `CREDENTIAL_SETS_ENABLED`, `NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED` | Polling Groups for email triggers | +| `DISABLE_INVITATIONS`, `NEXT_PUBLIC_DISABLE_INVITATIONS` | Globally disable workspace/organization invitations | ### Organization Management @@ -114,7 +88,23 @@ curl -X POST https://your-instance/api/v1/admin/organizations/{orgId}/members \ -d '{"userId": "user-id-here", "role": "admin"}' ``` +### Workspace Members + +When invitations are disabled, use the Admin API to manage workspace memberships directly: + +```bash +# Add a user to a workspace +curl -X POST https://your-instance/api/v1/admin/workspaces/{workspaceId}/members \ + -H "x-admin-key: YOUR_ADMIN_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"userId": "user-id-here", "permissions": "write"}' + +# Remove a user from a workspace +curl -X DELETE "https://your-instance/api/v1/admin/workspaces/{workspaceId}/members?userId=user-id-here" \ + -H "x-admin-key: YOUR_ADMIN_API_KEY" +``` + ### Notes - Enabling `ACCESS_CONTROL_ENABLED` automatically enables organizations, as access control requires organization membership. -- BYOK is only available on hosted Sim Studio. Self-hosted deployments configure AI provider keys directly via environment variables. +- When `DISABLE_INVITATIONS` is set, users cannot send invitations. Use the Admin API to manage workspace and organization memberships instead. diff --git a/apps/docs/content/docs/en/execution/costs.mdx b/apps/docs/content/docs/en/execution/costs.mdx index dce00ace96..a376a97bb7 100644 --- a/apps/docs/content/docs/en/execution/costs.mdx +++ b/apps/docs/content/docs/en/execution/costs.mdx @@ -106,7 +106,28 @@ The model breakdown shows: ## Bring Your Own Key (BYOK) -You can use your own API keys for hosted models (OpenAI, Anthropic, Google, Mistral) in **Settings → BYOK** to pay base prices. Keys are encrypted and apply workspace-wide. +Use your own API keys for AI model providers instead of Sim Studio's hosted keys to pay base prices with no markup. + +### Supported Providers + +| Provider | Usage | +|----------|-------| +| OpenAI | Knowledge Base embeddings, Agent block | +| Anthropic | Agent block | +| Google | Agent block | +| Mistral | Knowledge Base OCR | + +### Setup + +1. Navigate to **Settings** → **BYOK** in your workspace +2. Click **Add Key** for your provider +3. Enter your API key and save + + + BYOK keys are encrypted at rest. Only workspace admins can manage keys. + + +When configured, workflows use your key instead of Sim Studio's hosted keys. If removed, workflows automatically fall back to hosted keys with the multiplier. ## Cost Optimization Strategies diff --git a/apps/docs/content/docs/en/tools/ahrefs.mdx b/apps/docs/content/docs/en/tools/ahrefs.mdx index 82941def99..1ad2148757 100644 --- a/apps/docs/content/docs/en/tools/ahrefs.mdx +++ b/apps/docs/content/docs/en/tools/ahrefs.mdx @@ -198,8 +198,3 @@ Get a list of broken backlinks pointing to a target domain or URL. Useful for id | `brokenBacklinks` | array | List of broken backlinks | - -## Notes - -- Category: `tools` -- Type: `ahrefs` diff --git a/apps/docs/content/docs/en/tools/airtable.mdx b/apps/docs/content/docs/en/tools/airtable.mdx index cf924cd53d..8986c0e2f5 100644 --- a/apps/docs/content/docs/en/tools/airtable.mdx +++ b/apps/docs/content/docs/en/tools/airtable.mdx @@ -128,8 +128,3 @@ Update multiple existing records in an Airtable table | `records` | json | Array of updated Airtable records | - -## Notes - -- Category: `tools` -- Type: `airtable` diff --git a/apps/docs/content/docs/en/tools/apify.mdx b/apps/docs/content/docs/en/tools/apify.mdx index 2ba9276b1c..518bb0a7fb 100644 --- a/apps/docs/content/docs/en/tools/apify.mdx +++ b/apps/docs/content/docs/en/tools/apify.mdx @@ -86,8 +86,3 @@ Run an APIFY actor asynchronously with polling for long-running tasks | `items` | array | Dataset items \(if completed\) | - -## Notes - -- Category: `tools` -- Type: `apify` diff --git a/apps/docs/content/docs/en/tools/apollo.mdx b/apps/docs/content/docs/en/tools/apollo.mdx index 4b9e7b1483..7fc5863ee1 100644 --- a/apps/docs/content/docs/en/tools/apollo.mdx +++ b/apps/docs/content/docs/en/tools/apollo.mdx @@ -61,7 +61,9 @@ Search Apollo | Parameter | Type | Description | | --------- | ---- | ----------- | | `people` | json | Array of people matching the search criteria | -| `metadata` | json | Pagination information including page, per_page, and total_entries | +| `page` | number | Current page number | +| `per_page` | number | Results per page | +| `total_entries` | number | Total matching entries | ### `apollo_people_enrich` @@ -86,7 +88,7 @@ Enrich data for a single person using Apollo | Parameter | Type | Description | | --------- | ---- | ----------- | | `person` | json | Enriched person data from Apollo | -| `metadata` | json | Enrichment metadata including enriched status | +| `enriched` | boolean | Whether the person was successfully enriched | ### `apollo_people_bulk_enrich` @@ -106,7 +108,8 @@ Enrich data for up to 10 people at once using Apollo | Parameter | Type | Description | | --------- | ---- | ----------- | | `people` | json | Array of enriched people data | -| `metadata` | json | Bulk enrichment metadata including total and enriched counts | +| `total` | number | Total number of people processed | +| `enriched` | number | Number of people successfully enriched | ### `apollo_organization_search` @@ -129,7 +132,9 @@ Search Apollo | Parameter | Type | Description | | --------- | ---- | ----------- | | `organizations` | json | Array of organizations matching the search criteria | -| `metadata` | json | Pagination information including page, per_page, and total_entries | +| `page` | number | Current page number | +| `per_page` | number | Results per page | +| `total_entries` | number | Total matching entries | ### `apollo_organization_enrich` @@ -148,7 +153,7 @@ Enrich data for a single organization using Apollo | Parameter | Type | Description | | --------- | ---- | ----------- | | `organization` | json | Enriched organization data from Apollo | -| `metadata` | json | Enrichment metadata including enriched status | +| `enriched` | boolean | Whether the organization was successfully enriched | ### `apollo_organization_bulk_enrich` @@ -166,7 +171,8 @@ Enrich data for up to 10 organizations at once using Apollo | Parameter | Type | Description | | --------- | ---- | ----------- | | `organizations` | json | Array of enriched organization data | -| `metadata` | json | Bulk enrichment metadata including total and enriched counts | +| `total` | number | Total number of organizations processed | +| `enriched` | number | Number of organizations successfully enriched | ### `apollo_contact_create` @@ -189,7 +195,7 @@ Create a new contact in your Apollo database | Parameter | Type | Description | | --------- | ---- | ----------- | | `contact` | json | Created contact data from Apollo | -| `metadata` | json | Creation metadata including created status | +| `created` | boolean | Whether the contact was successfully created | ### `apollo_contact_update` @@ -213,7 +219,7 @@ Update an existing contact in your Apollo database | Parameter | Type | Description | | --------- | ---- | ----------- | | `contact` | json | Updated contact data from Apollo | -| `metadata` | json | Update metadata including updated status | +| `updated` | boolean | Whether the contact was successfully updated | ### `apollo_contact_search` @@ -234,7 +240,7 @@ Search your team | Parameter | Type | Description | | --------- | ---- | ----------- | | `contacts` | json | Array of contacts matching the search criteria | -| `metadata` | json | Pagination information including page, per_page, and total_entries | +| `pagination` | json | Pagination information | ### `apollo_contact_bulk_create` @@ -254,7 +260,9 @@ Create up to 100 contacts at once in your Apollo database. Supports deduplicatio | --------- | ---- | ----------- | | `created_contacts` | json | Array of newly created contacts | | `existing_contacts` | json | Array of existing contacts \(when deduplication is enabled\) | -| `metadata` | json | Bulk creation metadata including counts of created and existing contacts | +| `total_submitted` | number | Total number of contacts submitted | +| `created` | number | Number of contacts successfully created | +| `existing` | number | Number of existing contacts found | ### `apollo_contact_bulk_update` @@ -273,7 +281,9 @@ Update up to 100 existing contacts at once in your Apollo database. Each contact | --------- | ---- | ----------- | | `updated_contacts` | json | Array of successfully updated contacts | | `failed_contacts` | json | Array of contacts that failed to update | -| `metadata` | json | Bulk update metadata including counts of updated and failed contacts | +| `total_submitted` | number | Total number of contacts submitted | +| `updated` | number | Number of contacts successfully updated | +| `failed` | number | Number of contacts that failed to update | ### `apollo_account_create` @@ -294,7 +304,7 @@ Create a new account (company) in your Apollo database | Parameter | Type | Description | | --------- | ---- | ----------- | | `account` | json | Created account data from Apollo | -| `metadata` | json | Creation metadata including created status | +| `created` | boolean | Whether the account was successfully created | ### `apollo_account_update` @@ -316,7 +326,7 @@ Update an existing account in your Apollo database | Parameter | Type | Description | | --------- | ---- | ----------- | | `account` | json | Updated account data from Apollo | -| `metadata` | json | Update metadata including updated status | +| `updated` | boolean | Whether the account was successfully updated | ### `apollo_account_search` @@ -338,7 +348,7 @@ Search your team | Parameter | Type | Description | | --------- | ---- | ----------- | | `accounts` | json | Array of accounts matching the search criteria | -| `metadata` | json | Pagination information including page, per_page, and total_entries | +| `pagination` | json | Pagination information | ### `apollo_account_bulk_create` @@ -357,7 +367,9 @@ Create up to 100 accounts at once in your Apollo database. Note: Apollo does not | --------- | ---- | ----------- | | `created_accounts` | json | Array of newly created accounts | | `failed_accounts` | json | Array of accounts that failed to create | -| `metadata` | json | Bulk creation metadata including counts of created and failed accounts | +| `total_submitted` | number | Total number of accounts submitted | +| `created` | number | Number of accounts successfully created | +| `failed` | number | Number of accounts that failed to create | ### `apollo_account_bulk_update` @@ -376,7 +388,9 @@ Update up to 1000 existing accounts at once in your Apollo database (higher limi | --------- | ---- | ----------- | | `updated_accounts` | json | Array of successfully updated accounts | | `failed_accounts` | json | Array of accounts that failed to update | -| `metadata` | json | Bulk update metadata including counts of updated and failed accounts | +| `total_submitted` | number | Total number of accounts submitted | +| `updated` | number | Number of accounts successfully updated | +| `failed` | number | Number of accounts that failed to update | ### `apollo_opportunity_create` @@ -400,7 +414,7 @@ Create a new deal for an account in your Apollo database (master key required) | Parameter | Type | Description | | --------- | ---- | ----------- | | `opportunity` | json | Created opportunity data from Apollo | -| `metadata` | json | Creation metadata including created status | +| `created` | boolean | Whether the opportunity was successfully created | ### `apollo_opportunity_search` @@ -423,7 +437,9 @@ Search and list all deals/opportunities in your team | Parameter | Type | Description | | --------- | ---- | ----------- | | `opportunities` | json | Array of opportunities matching the search criteria | -| `metadata` | json | Pagination information including page, per_page, and total_entries | +| `page` | number | Current page number | +| `per_page` | number | Results per page | +| `total_entries` | number | Total matching entries | ### `apollo_opportunity_get` @@ -441,7 +457,7 @@ Retrieve complete details of a specific deal/opportunity by ID | Parameter | Type | Description | | --------- | ---- | ----------- | | `opportunity` | json | Complete opportunity data from Apollo | -| `metadata` | json | Retrieval metadata including found status | +| `found` | boolean | Whether the opportunity was found | ### `apollo_opportunity_update` @@ -465,7 +481,7 @@ Update an existing deal/opportunity in your Apollo database | Parameter | Type | Description | | --------- | ---- | ----------- | | `opportunity` | json | Updated opportunity data from Apollo | -| `metadata` | json | Update metadata including updated status | +| `updated` | boolean | Whether the opportunity was successfully updated | ### `apollo_sequence_search` @@ -486,7 +502,9 @@ Search for sequences/campaigns in your team | Parameter | Type | Description | | --------- | ---- | ----------- | | `sequences` | json | Array of sequences/campaigns matching the search criteria | -| `metadata` | json | Pagination information including page, per_page, and total_entries | +| `page` | number | Current page number | +| `per_page` | number | Results per page | +| `total_entries` | number | Total matching entries | ### `apollo_sequence_add_contacts` @@ -507,7 +525,8 @@ Add contacts to an Apollo sequence | Parameter | Type | Description | | --------- | ---- | ----------- | | `contacts_added` | json | Array of contact IDs added to the sequence | -| `metadata` | json | Sequence metadata including sequence_id and total_added count | +| `sequence_id` | string | ID of the sequence contacts were added to | +| `total_added` | number | Total number of contacts added | ### `apollo_task_create` @@ -530,7 +549,7 @@ Create a new task in Apollo | Parameter | Type | Description | | --------- | ---- | ----------- | | `task` | json | Created task data from Apollo | -| `metadata` | json | Creation metadata including created status | +| `created` | boolean | Whether the task was successfully created | ### `apollo_task_search` @@ -552,7 +571,7 @@ Search for tasks in Apollo | Parameter | Type | Description | | --------- | ---- | ----------- | | `tasks` | json | Array of tasks matching the search criteria | -| `metadata` | json | Pagination information including page, per_page, and total_entries | +| `pagination` | json | Pagination information | ### `apollo_email_accounts` @@ -569,11 +588,6 @@ Get list of team | Parameter | Type | Description | | --------- | ---- | ----------- | | `email_accounts` | json | Array of team email accounts linked in Apollo | -| `metadata` | json | Metadata including total count of email accounts | +| `total` | number | Total count of email accounts | - -## Notes - -- Category: `tools` -- Type: `apollo` diff --git a/apps/docs/content/docs/en/tools/arxiv.mdx b/apps/docs/content/docs/en/tools/arxiv.mdx index 0baf62b67c..acb1e6b0f3 100644 --- a/apps/docs/content/docs/en/tools/arxiv.mdx +++ b/apps/docs/content/docs/en/tools/arxiv.mdx @@ -87,8 +87,3 @@ Search for papers by a specific author on ArXiv. | `authorPapers` | json | Array of papers authored by the specified author | - -## Notes - -- Category: `tools` -- Type: `arxiv` diff --git a/apps/docs/content/docs/en/tools/asana.mdx b/apps/docs/content/docs/en/tools/asana.mdx index 87686e065c..0c3823c93d 100644 --- a/apps/docs/content/docs/en/tools/asana.mdx +++ b/apps/docs/content/docs/en/tools/asana.mdx @@ -162,8 +162,3 @@ Add a comment (story) to an Asana task | `created_by` | object | Comment author details | - -## Notes - -- Category: `tools` -- Type: `asana` diff --git a/apps/docs/content/docs/en/tools/browser_use.mdx b/apps/docs/content/docs/en/tools/browser_use.mdx index 7e2b0f680c..6510894dce 100644 --- a/apps/docs/content/docs/en/tools/browser_use.mdx +++ b/apps/docs/content/docs/en/tools/browser_use.mdx @@ -58,8 +58,3 @@ Runs a browser automation task using BrowserUse | `steps` | json | Execution steps taken | - -## Notes - -- Category: `tools` -- Type: `browser_use` diff --git a/apps/docs/content/docs/en/tools/calendly.mdx b/apps/docs/content/docs/en/tools/calendly.mdx index d1772f84cd..ac6fdbb70c 100644 --- a/apps/docs/content/docs/en/tools/calendly.mdx +++ b/apps/docs/content/docs/en/tools/calendly.mdx @@ -170,8 +170,3 @@ Cancel a scheduled event | `resource` | object | Cancellation details | - -## Notes - -- Category: `tools` -- Type: `calendly` diff --git a/apps/docs/content/docs/en/tools/circleback.mdx b/apps/docs/content/docs/en/tools/circleback.mdx index 04810df96d..662ac1561f 100644 --- a/apps/docs/content/docs/en/tools/circleback.mdx +++ b/apps/docs/content/docs/en/tools/circleback.mdx @@ -57,8 +57,3 @@ Receive meeting notes, action items, transcripts, and recordings when meetings a - -## Notes - -- Category: `triggers` -- Type: `circleback` diff --git a/apps/docs/content/docs/en/tools/clay.mdx b/apps/docs/content/docs/en/tools/clay.mdx index 97ea882cd4..2dca8e9390 100644 --- a/apps/docs/content/docs/en/tools/clay.mdx +++ b/apps/docs/content/docs/en/tools/clay.mdx @@ -63,8 +63,3 @@ Populate Clay with data from a JSON file. Enables direct communication and notif | `metadata` | object | Webhook response metadata | - -## Notes - -- Category: `tools` -- Type: `clay` diff --git a/apps/docs/content/docs/en/tools/confluence.mdx b/apps/docs/content/docs/en/tools/confluence.mdx index 031ef7f908..bb8453eceb 100644 --- a/apps/docs/content/docs/en/tools/confluence.mdx +++ b/apps/docs/content/docs/en/tools/confluence.mdx @@ -354,8 +354,3 @@ List all Confluence spaces accessible to the user. | `spaces` | array | List of spaces | - -## Notes - -- Category: `tools` -- Type: `confluence` diff --git a/apps/docs/content/docs/en/tools/cursor.mdx b/apps/docs/content/docs/en/tools/cursor.mdx index 77bab828b0..00c82c5f26 100644 --- a/apps/docs/content/docs/en/tools/cursor.mdx +++ b/apps/docs/content/docs/en/tools/cursor.mdx @@ -1,49 +1,22 @@ --- title: Cursor -description: Launch and manage Cursor cloud agents to work on GitHub repositories +description: Agent identifier --- import { BlockInfoCard } from "@/components/ui/block-info-card" -{/* MANUAL-CONTENT-START:intro */} -[Cursor](https://www.cursor.so/) is an AI IDE and cloud-based platform that lets you launch and manage powerful AI agents able to work directly on your GitHub repositories. Cursor agents can automate development tasks, enhance your team's productivity, and collaborate with you by making code changes, responding to natural language instructions, and maintaining conversation history about their activities. - -With Cursor, you can: - -- **Launch cloud agents for codebases**: Instantly create new AI agents that work on your repositories in the cloud -- **Delegate coding tasks using natural language**: Guide agents with written instructions, amendments, and clarifications -- **Monitor progress and outputs**: Retrieve agent status, view detailed results, and inspect current or completed tasks -- **Access full conversation history**: Review all prompts and AI responses for transparency and auditability -- **Control and manage agent lifecycle**: List active agents, terminate agents, and manage API-based agent launches and follow-ups - -In Sim, the Cursor integration enables your agents and workflows to interact programmatically with Cursor cloud agents. This means you can use Sim to: - -- List all cloud agents and browse their current state (`cursor_list_agents`) -- Retrieve up-to-date status and outputs for any agent (`cursor_get_agent`) -- View the full conversation history for any coding agent (`cursor_get_conversation`) -- Add follow-up instructions or new prompts to a running agent -- Manage and terminate agents as needed - -This integration helps you combine the flexible intelligence of Sim agents with the powerful development automation capabilities of Cursor, making it possible to scale AI-driven development across your projects. -{/* MANUAL-CONTENT-END */} - - -## Usage Instructions - -Interact with Cursor Cloud Agents API to launch AI agents that can work on your GitHub repositories. Supports launching agents, adding follow-up instructions, checking status, viewing conversations, and managing agent lifecycle. - ## Tools ### `cursor_list_agents` -List all cloud agents for the authenticated user with optional pagination. +List all cloud agents for the authenticated user with optional pagination. Returns API-aligned fields only. #### Input @@ -57,12 +30,12 @@ List all cloud agents for the authenticated user with optional pagination. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Human-readable list of agents | -| `metadata` | object | Agent list metadata | +| `agents` | array | Array of agent objects | +| `nextCursor` | string | Pagination cursor for next page | ### `cursor_get_agent` -Retrieve the current status and results of a cloud agent. +Retrieve the current status and results of a cloud agent. Returns API-aligned fields only. #### Input @@ -75,12 +48,17 @@ Retrieve the current status and results of a cloud agent. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Human-readable agent details | -| `metadata` | object | Agent metadata | +| `id` | string | Agent ID | +| `name` | string | Agent name | +| `status` | string | Agent status | +| `source` | json | Source repository info | +| `target` | json | Target branch/PR info | +| `summary` | string | Agent summary | +| `createdAt` | string | Creation timestamp | ### `cursor_get_conversation` -Retrieve the conversation history of a cloud agent, including all user prompts and assistant responses. +Retrieve the conversation history of a cloud agent, including all user prompts and assistant responses. Returns API-aligned fields only. #### Input @@ -93,12 +71,12 @@ Retrieve the conversation history of a cloud agent, including all user prompts a | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Human-readable conversation history | -| `metadata` | object | Conversation metadata | +| `id` | string | Agent ID | +| `messages` | array | Array of conversation messages | ### `cursor_launch_agent` -Start a new cloud agent to work on a GitHub repository with the given instructions. +Start a new cloud agent to work on a GitHub repository with the given instructions. Returns API-aligned fields only. #### Input @@ -119,12 +97,12 @@ Start a new cloud agent to work on a GitHub repository with the given instructio | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Success message with agent details | -| `metadata` | object | Launch result metadata | +| `id` | string | Agent ID | +| `url` | string | Agent URL | ### `cursor_add_followup` -Add a follow-up instruction to an existing cloud agent. +Add a follow-up instruction to an existing cloud agent. Returns API-aligned fields only. #### Input @@ -139,12 +117,11 @@ Add a follow-up instruction to an existing cloud agent. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Success message | -| `metadata` | object | Result metadata | +| `id` | string | Agent ID | ### `cursor_stop_agent` -Stop a running cloud agent. This pauses the agent without deleting it. +Stop a running cloud agent. Returns API-aligned fields only. #### Input @@ -157,12 +134,11 @@ Stop a running cloud agent. This pauses the agent without deleting it. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Success message | -| `metadata` | object | Result metadata | +| `id` | string | Agent ID | ### `cursor_delete_agent` -Permanently delete a cloud agent. This action cannot be undone. +Permanently delete a cloud agent. Returns API-aligned fields only. #### Input @@ -175,12 +151,6 @@ Permanently delete a cloud agent. This action cannot be undone. | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Success message | -| `metadata` | object | Result metadata | - - +| `id` | string | Agent ID | -## Notes -- Category: `tools` -- Type: `cursor` diff --git a/apps/docs/content/docs/en/tools/datadog.mdx b/apps/docs/content/docs/en/tools/datadog.mdx index 45978068a8..25c9fe784f 100644 --- a/apps/docs/content/docs/en/tools/datadog.mdx +++ b/apps/docs/content/docs/en/tools/datadog.mdx @@ -300,8 +300,3 @@ Cancel a scheduled downtime. | `success` | boolean | Whether the downtime was successfully canceled | - -## Notes - -- Category: `tools` -- Type: `datadog` diff --git a/apps/docs/content/docs/en/tools/discord.mdx b/apps/docs/content/docs/en/tools/discord.mdx index 208f962713..27c77f10f5 100644 --- a/apps/docs/content/docs/en/tools/discord.mdx +++ b/apps/docs/content/docs/en/tools/discord.mdx @@ -735,8 +735,3 @@ Delete a Discord webhook | `message` | string | Success or error message | - -## Notes - -- Category: `tools` -- Type: `discord` diff --git a/apps/docs/content/docs/en/tools/dropbox.mdx b/apps/docs/content/docs/en/tools/dropbox.mdx index 399d06a002..49a6ce22e8 100644 --- a/apps/docs/content/docs/en/tools/dropbox.mdx +++ b/apps/docs/content/docs/en/tools/dropbox.mdx @@ -217,8 +217,3 @@ Search for files and folders in Dropbox | `matches` | array | Search results | - -## Notes - -- Category: `tools` -- Type: `dropbox` diff --git a/apps/docs/content/docs/en/tools/duckduckgo.mdx b/apps/docs/content/docs/en/tools/duckduckgo.mdx index baf9f7d6e8..3530d15339 100644 --- a/apps/docs/content/docs/en/tools/duckduckgo.mdx +++ b/apps/docs/content/docs/en/tools/duckduckgo.mdx @@ -61,8 +61,3 @@ Search the web using DuckDuckGo Instant Answers API. Returns instant answers, ab | `relatedTopics` | array | Array of related topics with URLs and descriptions | - -## Notes - -- Category: `tools` -- Type: `duckduckgo` diff --git a/apps/docs/content/docs/en/tools/dynamodb.mdx b/apps/docs/content/docs/en/tools/dynamodb.mdx index ff0c9efd2a..03550a30d5 100644 --- a/apps/docs/content/docs/en/tools/dynamodb.mdx +++ b/apps/docs/content/docs/en/tools/dynamodb.mdx @@ -37,7 +37,7 @@ This integration empowers Sim agents to automate data management tasks within yo ## Usage Instructions -Integrate Amazon DynamoDB into workflows. Supports Get, Put, Query, Scan, Update, and Delete operations on DynamoDB tables. +Integrate Amazon DynamoDB into workflows. Supports Get, Put, Query, Scan, Update, Delete, and Introspect operations on DynamoDB tables. @@ -185,9 +185,25 @@ Delete an item from a DynamoDB table | --------- | ---- | ----------- | | `message` | string | Operation status message | +### `dynamodb_introspect` +Introspect DynamoDB to list tables or get detailed schema information for a specific table + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `region` | string | Yes | AWS region \(e.g., us-east-1\) | +| `accessKeyId` | string | Yes | AWS access key ID | +| `secretAccessKey` | string | Yes | AWS secret access key | +| `tableName` | string | No | Optional table name to get detailed schema. If not provided, lists all tables. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `tables` | array | List of table names in the region | +| `tableDetails` | object | Detailed schema information for a specific table | -## Notes -- Category: `tools` -- Type: `dynamodb` diff --git a/apps/docs/content/docs/en/tools/elasticsearch.mdx b/apps/docs/content/docs/en/tools/elasticsearch.mdx index 6c2fd618f6..d806a37586 100644 --- a/apps/docs/content/docs/en/tools/elasticsearch.mdx +++ b/apps/docs/content/docs/en/tools/elasticsearch.mdx @@ -362,9 +362,27 @@ Get comprehensive statistics about the Elasticsearch cluster. | `nodes` | object | Node statistics including count and versions | | `indices` | object | Index statistics including document count and store size | +### `elasticsearch_list_indices` +List all indices in the Elasticsearch cluster with their health, status, and statistics. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `deploymentType` | string | Yes | Deployment type: self_hosted or cloud | +| `host` | string | No | Elasticsearch host URL \(for self-hosted\) | +| `cloudId` | string | No | Elastic Cloud ID \(for cloud deployments\) | +| `authMethod` | string | Yes | Authentication method: api_key or basic_auth | +| `apiKey` | string | No | Elasticsearch API key | +| `username` | string | No | Username for basic auth | +| `password` | string | No | Password for basic auth | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Summary message about the indices | +| `indices` | json | Array of index information objects | -## Notes -- Category: `tools` -- Type: `elasticsearch` diff --git a/apps/docs/content/docs/en/tools/elevenlabs.mdx b/apps/docs/content/docs/en/tools/elevenlabs.mdx index 5c7515d5f0..108bce0f3f 100644 --- a/apps/docs/content/docs/en/tools/elevenlabs.mdx +++ b/apps/docs/content/docs/en/tools/elevenlabs.mdx @@ -54,8 +54,3 @@ Convert TTS using ElevenLabs voices | `audioFile` | file | The generated audio file | - -## Notes - -- Category: `tools` -- Type: `elevenlabs` diff --git a/apps/docs/content/docs/en/tools/exa.mdx b/apps/docs/content/docs/en/tools/exa.mdx index a864a2fdda..b9200f332d 100644 --- a/apps/docs/content/docs/en/tools/exa.mdx +++ b/apps/docs/content/docs/en/tools/exa.mdx @@ -148,8 +148,3 @@ Perform comprehensive research using AI to generate detailed reports with citati | `research` | array | Comprehensive research findings with citations and summaries | - -## Notes - -- Category: `tools` -- Type: `exa` diff --git a/apps/docs/content/docs/en/tools/file.mdx b/apps/docs/content/docs/en/tools/file.mdx index feb0021b63..b90c163bdb 100644 --- a/apps/docs/content/docs/en/tools/file.mdx +++ b/apps/docs/content/docs/en/tools/file.mdx @@ -52,8 +52,3 @@ Parse one or more uploaded files or files from URLs (text, PDF, CSV, images, etc | `combinedContent` | string | Combined content of all parsed files | - -## Notes - -- Category: `tools` -- Type: `file` diff --git a/apps/docs/content/docs/en/tools/firecrawl.mdx b/apps/docs/content/docs/en/tools/firecrawl.mdx index b38686bc11..ab76814c8f 100644 --- a/apps/docs/content/docs/en/tools/firecrawl.mdx +++ b/apps/docs/content/docs/en/tools/firecrawl.mdx @@ -176,8 +176,3 @@ Autonomous web data extraction agent. Searches and gathers information based on | `sources` | object | Array of source URLs used by the agent | - -## Notes - -- Category: `tools` -- Type: `firecrawl` diff --git a/apps/docs/content/docs/en/tools/fireflies.mdx b/apps/docs/content/docs/en/tools/fireflies.mdx index 6eb599337f..cdc57bc9fa 100644 --- a/apps/docs/content/docs/en/tools/fireflies.mdx +++ b/apps/docs/content/docs/en/tools/fireflies.mdx @@ -231,8 +231,3 @@ List all contacts from your Fireflies.ai meetings | `contacts` | array | List of contacts from meetings | - -## Notes - -- Category: `tools` -- Type: `fireflies` diff --git a/apps/docs/content/docs/en/tools/github.mdx b/apps/docs/content/docs/en/tools/github.mdx index 09ed416f87..bd08207b07 100644 --- a/apps/docs/content/docs/en/tools/github.mdx +++ b/apps/docs/content/docs/en/tools/github.mdx @@ -1,1177 +1,15 @@ --- title: GitHub -description: Interact with GitHub or trigger workflows from GitHub events +description: Operation result data (API-aligned) --- import { BlockInfoCard } from "@/components/ui/block-info-card" -{/* MANUAL-CONTENT-START:intro */} -[GitHub](https://github.com/) is the world's leading platform for software development and version control using Git. It provides a collaborative environment where developers can host and review code, manage projects, and build software together. -With GitHub, you can: -- **Host repositories**: Store your code in public or private repositories with version control -- **Collaborate on code**: Use pull requests to propose changes, review code, and merge contributions -- **Track issues**: Create, assign, and manage issues to organize work and track bugs -- **Automate workflows**: Use GitHub Actions to build, test, and deploy code automatically -- **Manage projects**: Organize work with project boards, milestones, and task tracking -- **Document code**: Create and maintain documentation with GitHub Pages and wikis -In Sim, the GitHub integration enables your agents to interact directly with GitHub repositories and workflows. This allows for powerful automation scenarios such as code review assistance, pull request management, issue tracking, and repository exploration. Your agents can fetch repository data, analyze code changes, post comments on pull requests, and perform other GitHub operations programmatically. This integration bridges the gap between your AI workflows and your development processes, enabling seamless collaboration between your agents and your development team. -{/* MANUAL-CONTENT-END */} - - -## Usage Instructions - -Integrate Github into the workflow. Can get get PR details, create PR comment, get repository info, and get latest commit. Can be used in trigger mode to trigger a workflow when a PR is created, commented on, or a commit is pushed. - - - -## Tools - -### `github_pr` - -Fetch PR details including diff and files changed - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner | -| `repo` | string | Yes | Repository name | -| `pullNumber` | number | Yes | Pull request number | -| `apiKey` | string | Yes | GitHub API token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable PR summary | -| `metadata` | object | Detailed PR metadata including file changes | - -### `github_comment` - -Create comments on GitHub PRs - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner | -| `repo` | string | Yes | Repository name | -| `body` | string | Yes | Comment content | -| `pullNumber` | number | Yes | Pull request number | -| `path` | string | No | File path for review comment | -| `position` | number | No | Line number for review comment | -| `commentType` | string | No | Type of comment \(pr_comment or file_comment\) | -| `line` | number | No | Line number for review comment | -| `side` | string | No | Side of the diff \(LEFT or RIGHT\) | -| `commitId` | string | No | The SHA of the commit to comment on | -| `apiKey` | string | Yes | GitHub API token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable comment confirmation | -| `metadata` | object | Comment metadata | - -### `github_repo_info` - -Retrieve comprehensive GitHub repository metadata including stars, forks, issues, and primary language. Supports both public and private repositories with optional authentication. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable repository summary | -| `metadata` | object | Repository metadata | - -### `github_latest_commit` - -Retrieve the latest commit from a GitHub repository - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `branch` | string | No | Branch name \(defaults to the repository's default branch\) | -| `apiKey` | string | Yes | GitHub API token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable commit summary | -| `metadata` | object | Commit metadata | - -### `github_issue_comment` - -Create a comment on a GitHub issue - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner | -| `repo` | string | Yes | Repository name | -| `issue_number` | number | Yes | Issue number | -| `body` | string | Yes | Comment content | -| `apiKey` | string | Yes | GitHub API token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable comment confirmation | -| `metadata` | object | Comment metadata | - -### `github_list_issue_comments` - -List all comments on a GitHub issue - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner | -| `repo` | string | Yes | Repository name | -| `issue_number` | number | Yes | Issue number | -| `since` | string | No | Only show comments updated after this ISO 8601 timestamp | -| `per_page` | number | No | Number of results per page \(max 100\) | -| `page` | number | No | Page number | -| `apiKey` | string | Yes | GitHub API token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable comments summary | -| `metadata` | object | Comments list metadata | - -### `github_update_comment` - -Update an existing comment on a GitHub issue or pull request - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner | -| `repo` | string | Yes | Repository name | -| `comment_id` | number | Yes | Comment ID | -| `body` | string | Yes | Updated comment content | -| `apiKey` | string | Yes | GitHub API token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable update confirmation | -| `metadata` | object | Updated comment metadata | - -### `github_delete_comment` - -Delete a comment on a GitHub issue or pull request - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner | -| `repo` | string | Yes | Repository name | -| `comment_id` | number | Yes | Comment ID | -| `apiKey` | string | Yes | GitHub API token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable deletion confirmation | -| `metadata` | object | Deletion result metadata | - -### `github_list_pr_comments` - -List all review comments on a GitHub pull request - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner | -| `repo` | string | Yes | Repository name | -| `pullNumber` | number | Yes | Pull request number | -| `sort` | string | No | Sort by created or updated | -| `direction` | string | No | Sort direction \(asc or desc\) | -| `since` | string | No | Only show comments updated after this ISO 8601 timestamp | -| `per_page` | number | No | Number of results per page \(max 100\) | -| `page` | number | No | Page number | -| `apiKey` | string | Yes | GitHub API token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable review comments summary | -| `metadata` | object | Review comments list metadata | - -### `github_create_pr` - -Create a new pull request in a GitHub repository - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner | -| `repo` | string | Yes | Repository name | -| `title` | string | Yes | Pull request title | -| `head` | string | Yes | The name of the branch where your changes are implemented | -| `base` | string | Yes | The name of the branch you want the changes pulled into | -| `body` | string | No | Pull request description \(Markdown\) | -| `draft` | boolean | No | Create as draft pull request | -| `apiKey` | string | Yes | GitHub API token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable PR creation confirmation | -| `metadata` | object | Pull request metadata | - -### `github_update_pr` - -Update an existing pull request in a GitHub repository - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner | -| `repo` | string | Yes | Repository name | -| `pullNumber` | number | Yes | Pull request number | -| `title` | string | No | New pull request title | -| `body` | string | No | New pull request description \(Markdown\) | -| `state` | string | No | New state \(open or closed\) | -| `base` | string | No | New base branch name | -| `apiKey` | string | Yes | GitHub API token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable PR update confirmation | -| `metadata` | object | Updated pull request metadata | - -### `github_merge_pr` - -Merge a pull request in a GitHub repository - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner | -| `repo` | string | Yes | Repository name | -| `pullNumber` | number | Yes | Pull request number | -| `commit_title` | string | No | Title for the merge commit | -| `commit_message` | string | No | Extra detail to append to merge commit message | -| `merge_method` | string | No | Merge method: merge, squash, or rebase | -| `apiKey` | string | Yes | GitHub API token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable merge confirmation | -| `metadata` | object | Merge result metadata | - -### `github_list_prs` - -List pull requests in a GitHub repository - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner | -| `repo` | string | Yes | Repository name | -| `state` | string | No | Filter by state: open, closed, or all | -| `head` | string | No | Filter by head user or branch name \(format: user:ref-name or organization:ref-name\) | -| `base` | string | No | Filter by base branch name | -| `sort` | string | No | Sort by: created, updated, popularity, or long-running | -| `direction` | string | No | Sort direction: asc or desc | -| `per_page` | number | No | Results per page \(max 100\) | -| `page` | number | No | Page number | -| `apiKey` | string | Yes | GitHub API token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable list of pull requests | -| `metadata` | object | Pull requests list metadata | - -### `github_get_pr_files` - -Get the list of files changed in a pull request - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner | -| `repo` | string | Yes | Repository name | -| `pullNumber` | number | Yes | Pull request number | -| `per_page` | number | No | Results per page \(max 100\) | -| `page` | number | No | Page number | -| `apiKey` | string | Yes | GitHub API token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable list of files changed in PR | -| `metadata` | object | PR files metadata | - -### `github_close_pr` - -Close a pull request in a GitHub repository - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner | -| `repo` | string | Yes | Repository name | -| `pullNumber` | number | Yes | Pull request number | -| `apiKey` | string | Yes | GitHub API token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable PR close confirmation | -| `metadata` | object | Closed pull request metadata | - -### `github_request_reviewers` - -Request reviewers for a pull request - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner | -| `repo` | string | Yes | Repository name | -| `pullNumber` | number | Yes | Pull request number | -| `reviewers` | string | Yes | Comma-separated list of user logins to request reviews from | -| `team_reviewers` | string | No | Comma-separated list of team slugs to request reviews from | -| `apiKey` | string | Yes | GitHub API token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable reviewer request confirmation | -| `metadata` | object | Requested reviewers metadata | - -### `github_get_file_content` - -Get the content of a file from a GitHub repository. Supports files up to 1MB. Content is returned decoded and human-readable. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `path` | string | Yes | Path to the file in the repository \(e.g., "src/index.ts"\) | -| `ref` | string | No | Branch name, tag, or commit SHA \(defaults to repository default branch\) | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable file information with content preview | -| `metadata` | object | File metadata including name, path, SHA, size, and URLs | - -### `github_create_file` - -Create a new file in a GitHub repository. The file content will be automatically Base64 encoded. Supports files up to 1MB. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `path` | string | Yes | Path where the file will be created \(e.g., "src/newfile.ts"\) | -| `message` | string | Yes | Commit message for this file creation | -| `content` | string | Yes | File content \(plain text, will be Base64 encoded automatically\) | -| `branch` | string | No | Branch to create the file in \(defaults to repository default branch\) | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable file creation confirmation | -| `metadata` | object | File and commit metadata | - -### `github_update_file` - -Update an existing file in a GitHub repository. Requires the file SHA. Content will be automatically Base64 encoded. Supports files up to 1MB. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `path` | string | Yes | Path to the file to update \(e.g., "src/index.ts"\) | -| `message` | string | Yes | Commit message for this file update | -| `content` | string | Yes | New file content \(plain text, will be Base64 encoded automatically\) | -| `sha` | string | Yes | The blob SHA of the file being replaced \(get from github_get_file_content\) | -| `branch` | string | No | Branch to update the file in \(defaults to repository default branch\) | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable file update confirmation | -| `metadata` | object | Updated file and commit metadata | - -### `github_delete_file` - -Delete a file from a GitHub repository. Requires the file SHA. This operation cannot be undone through the API. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `path` | string | Yes | Path to the file to delete \(e.g., "src/oldfile.ts"\) | -| `message` | string | Yes | Commit message for this file deletion | -| `sha` | string | Yes | The blob SHA of the file being deleted \(get from github_get_file_content\) | -| `branch` | string | No | Branch to delete the file from \(defaults to repository default branch\) | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable file deletion confirmation | -| `metadata` | object | Deletion confirmation and commit metadata | - -### `github_get_tree` - -Get the contents of a directory in a GitHub repository. Returns a list of files and subdirectories. Use empty path or omit to get root directory contents. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `path` | string | No | Directory path \(e.g., "src/components"\). Leave empty for root directory. | -| `ref` | string | No | Branch name, tag, or commit SHA \(defaults to repository default branch\) | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable directory tree listing | -| `metadata` | object | Directory contents metadata | - -### `github_list_branches` - -List all branches in a GitHub repository. Optionally filter by protected status and control pagination. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `protected` | boolean | No | Filter branches by protection status | -| `per_page` | number | No | Number of results per page \(max 100, default 30\) | -| `page` | number | No | Page number for pagination \(default 1\) | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable list of branches | -| `metadata` | object | Branch list metadata | - -### `github_get_branch` - -Get detailed information about a specific branch in a GitHub repository, including commit details and protection status. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `branch` | string | Yes | Branch name | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable branch details | -| `metadata` | object | Branch metadata | - -### `github_create_branch` - -Create a new branch in a GitHub repository by creating a git reference pointing to a specific commit SHA. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `branch` | string | Yes | Name of the branch to create | -| `sha` | string | Yes | Commit SHA to point the branch to | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable branch creation confirmation | -| `metadata` | object | Git reference metadata | - -### `github_delete_branch` - -Delete a branch from a GitHub repository by removing its git reference. Protected branches cannot be deleted. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `branch` | string | Yes | Name of the branch to delete | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable deletion confirmation | -| `metadata` | object | Deletion metadata | - -### `github_get_branch_protection` - -Get the branch protection rules for a specific branch, including status checks, review requirements, and restrictions. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `branch` | string | Yes | Branch name | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable branch protection summary | -| `metadata` | object | Branch protection configuration | - -### `github_update_branch_protection` - -Update branch protection rules for a specific branch, including status checks, review requirements, admin enforcement, and push restrictions. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `branch` | string | Yes | Branch name | -| `required_status_checks` | object | Yes | Required status check configuration \(null to disable\). Object with strict \(boolean\) and contexts \(string array\) | -| `enforce_admins` | boolean | Yes | Whether to enforce restrictions for administrators | -| `required_pull_request_reviews` | object | Yes | PR review requirements \(null to disable\). Object with optional required_approving_review_count, dismiss_stale_reviews, require_code_owner_reviews | -| `restrictions` | object | Yes | Push restrictions \(null to disable\). Object with users \(string array\) and teams \(string array\) | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable branch protection update summary | -| `metadata` | object | Updated branch protection configuration | - -### `github_create_issue` - -Create a new issue in a GitHub repository - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner | -| `repo` | string | Yes | Repository name | -| `title` | string | Yes | Issue title | -| `body` | string | No | Issue description/body | -| `assignees` | string | No | Comma-separated list of usernames to assign to this issue | -| `labels` | string | No | Comma-separated list of label names to add to this issue | -| `milestone` | number | No | Milestone number to associate with this issue | -| `apiKey` | string | Yes | GitHub API token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable issue creation confirmation | -| `metadata` | object | Issue metadata | - -### `github_update_issue` - -Update an existing issue in a GitHub repository - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner | -| `repo` | string | Yes | Repository name | -| `issue_number` | number | Yes | Issue number | -| `title` | string | No | New issue title | -| `body` | string | No | New issue description/body | -| `state` | string | No | Issue state \(open or closed\) | -| `labels` | array | No | Array of label names \(replaces all existing labels\) | -| `assignees` | array | No | Array of usernames \(replaces all existing assignees\) | -| `apiKey` | string | Yes | GitHub API token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable issue update confirmation | -| `metadata` | object | Updated issue metadata | - -### `github_list_issues` - -List issues in a GitHub repository. Note: This includes pull requests as PRs are considered issues in GitHub - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner | -| `repo` | string | Yes | Repository name | -| `state` | string | No | Filter by state: open, closed, or all \(default: open\) | -| `assignee` | string | No | Filter by assignee username | -| `creator` | string | No | Filter by creator username | -| `labels` | string | No | Comma-separated list of label names to filter by | -| `sort` | string | No | Sort by: created, updated, or comments \(default: created\) | -| `direction` | string | No | Sort direction: asc or desc \(default: desc\) | -| `per_page` | number | No | Results per page \(max 100, default: 30\) | -| `page` | number | No | Page number \(default: 1\) | -| `apiKey` | string | Yes | GitHub API token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable list of issues | -| `metadata` | object | Issues list metadata | - -### `github_get_issue` - -Get detailed information about a specific issue in a GitHub repository - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner | -| `repo` | string | Yes | Repository name | -| `issue_number` | number | Yes | Issue number | -| `apiKey` | string | Yes | GitHub API token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable issue details | -| `metadata` | object | Detailed issue metadata | - -### `github_close_issue` - -Close an issue in a GitHub repository - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner | -| `repo` | string | Yes | Repository name | -| `issue_number` | number | Yes | Issue number | -| `state_reason` | string | No | Reason for closing: completed or not_planned | -| `apiKey` | string | Yes | GitHub API token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable issue close confirmation | -| `metadata` | object | Closed issue metadata | - -### `github_add_labels` - -Add labels to an issue in a GitHub repository - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner | -| `repo` | string | Yes | Repository name | -| `issue_number` | number | Yes | Issue number | -| `labels` | string | Yes | Comma-separated list of label names to add to the issue | -| `apiKey` | string | Yes | GitHub API token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable labels confirmation | -| `metadata` | object | Labels metadata | - -### `github_remove_label` - -Remove a label from an issue in a GitHub repository - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner | -| `repo` | string | Yes | Repository name | -| `issue_number` | number | Yes | Issue number | -| `name` | string | Yes | Label name to remove | -| `apiKey` | string | Yes | GitHub API token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable label removal confirmation | -| `metadata` | object | Remaining labels metadata | - -### `github_add_assignees` - -Add assignees to an issue in a GitHub repository - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner | -| `repo` | string | Yes | Repository name | -| `issue_number` | number | Yes | Issue number | -| `assignees` | string | Yes | Comma-separated list of usernames to assign to the issue | -| `apiKey` | string | Yes | GitHub API token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable assignees confirmation | -| `metadata` | object | Updated issue metadata with assignees | - -### `github_create_release` - -Create a new release for a GitHub repository. Specify tag name, target commit, title, description, and whether it should be a draft or prerelease. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `tag_name` | string | Yes | The name of the tag for this release | -| `target_commitish` | string | No | Specifies the commitish value that determines where the Git tag is created from. Can be any branch or commit SHA. Defaults to the repository default branch. | -| `name` | string | No | The name of the release | -| `body` | string | No | Text describing the contents of the release \(markdown supported\) | -| `draft` | boolean | No | true to create a draft \(unpublished\) release, false to create a published one | -| `prerelease` | boolean | No | true to identify the release as a prerelease, false to identify as a full release | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable release creation summary | -| `metadata` | object | Release metadata including download URLs | - -### `github_update_release` - -Update an existing GitHub release. Modify tag name, target commit, title, description, draft status, or prerelease status. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `release_id` | number | Yes | The unique identifier of the release | -| `tag_name` | string | No | The name of the tag | -| `target_commitish` | string | No | Specifies the commitish value for where the tag is created from | -| `name` | string | No | The name of the release | -| `body` | string | No | Text describing the contents of the release \(markdown supported\) | -| `draft` | boolean | No | true to set as draft, false to publish | -| `prerelease` | boolean | No | true to identify as a prerelease, false for a full release | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable release update summary | -| `metadata` | object | Updated release metadata including download URLs | - -### `github_list_releases` - -List all releases for a GitHub repository. Returns release information including tags, names, and download URLs. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `per_page` | number | No | Number of results per page \(max 100\) | -| `page` | number | No | Page number of the results to fetch | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable list of releases with summary | -| `metadata` | object | Releases metadata | - -### `github_get_release` - -Get detailed information about a specific GitHub release by ID. Returns release metadata including assets and download URLs. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `release_id` | number | Yes | The unique identifier of the release | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable release details | -| `metadata` | object | Release metadata including download URLs | - -### `github_delete_release` - -Delete a GitHub release by ID. This permanently removes the release but does not delete the associated Git tag. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `release_id` | number | Yes | The unique identifier of the release to delete | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable deletion confirmation | -| `metadata` | object | Deletion result metadata | - -### `github_list_workflows` - -List all workflows in a GitHub repository. Returns workflow details including ID, name, path, state, and badge URL. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `per_page` | number | No | Number of results per page \(default: 30, max: 100\) | -| `page` | number | No | Page number of results to fetch \(default: 1\) | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable workflows summary | -| `metadata` | object | Workflows metadata | - -### `github_get_workflow` - -Get details of a specific GitHub Actions workflow by ID or filename. Returns workflow information including name, path, state, and badge URL. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `workflow_id` | string | Yes | Workflow ID \(number\) or workflow filename \(e.g., "main.yaml"\) | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable workflow details | -| `metadata` | object | Workflow metadata | - -### `github_trigger_workflow` - -Trigger a workflow dispatch event for a GitHub Actions workflow. The workflow must have a workflow_dispatch trigger configured. Returns 204 No Content on success. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `workflow_id` | string | Yes | Workflow ID \(number\) or workflow filename \(e.g., "main.yaml"\) | -| `ref` | string | Yes | Git reference \(branch or tag name\) to run the workflow on | -| `inputs` | object | No | Input keys and values configured in the workflow file | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Confirmation message | -| `metadata` | object | Empty metadata object \(204 No Content response\) | - -### `github_list_workflow_runs` - -List workflow runs for a repository. Supports filtering by actor, branch, event, and status. Returns run details including status, conclusion, and links. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `actor` | string | No | Filter by user who triggered the workflow | -| `branch` | string | No | Filter by branch name | -| `event` | string | No | Filter by event type \(e.g., push, pull_request, workflow_dispatch\) | -| `status` | string | No | Filter by status \(queued, in_progress, completed, waiting, requested, pending\) | -| `per_page` | number | No | Number of results per page \(default: 30, max: 100\) | -| `page` | number | No | Page number of results to fetch \(default: 1\) | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable workflow runs summary | -| `metadata` | object | Workflow runs metadata | - -### `github_get_workflow_run` - -Get detailed information about a specific workflow run by ID. Returns status, conclusion, timing, and links to the run. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `run_id` | number | Yes | Workflow run ID | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable workflow run details | -| `metadata` | object | Workflow run metadata | - -### `github_cancel_workflow_run` - -Cancel a workflow run. Returns 202 Accepted if cancellation is initiated, or 409 Conflict if the run cannot be cancelled (already completed). - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `run_id` | number | Yes | Workflow run ID to cancel | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Cancellation status message | -| `metadata` | object | Cancellation metadata | - -### `github_rerun_workflow` - -Rerun a workflow run. Optionally enable debug logging for the rerun. Returns 201 Created on success. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner` | string | Yes | Repository owner \(user or organization\) | -| `repo` | string | Yes | Repository name | -| `run_id` | number | Yes | Workflow run ID to rerun | -| `enable_debug_logging` | boolean | No | Enable debug logging for the rerun \(default: false\) | -| `apiKey` | string | Yes | GitHub Personal Access Token | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Rerun confirmation message | -| `metadata` | object | Rerun metadata | - -### `github_list_projects` - -List GitHub Projects V2 for an organization or user. Returns up to 20 projects with their details including ID, title, number, URL, and status. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner_type` | string | Yes | Owner type: "org" for organization or "user" for user | -| `owner_login` | string | Yes | Organization or user login name | -| `apiKey` | string | Yes | GitHub Personal Access Token with project read permissions | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable list of projects | -| `metadata` | object | Projects metadata | - -### `github_get_project` - -Get detailed information about a specific GitHub Project V2 by its number. Returns project details including ID, title, description, URL, and status. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner_type` | string | Yes | Owner type: "org" for organization or "user" for user | -| `owner_login` | string | Yes | Organization or user login name | -| `project_number` | number | Yes | Project number | -| `apiKey` | string | Yes | GitHub Personal Access Token with project read permissions | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable project details | -| `metadata` | object | Project metadata | - -### `github_create_project` - -Create a new GitHub Project V2. Requires the owner Node ID (not login name). Returns the created project with ID, title, and URL. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `owner_id` | string | Yes | Owner Node ID \(format: PVT_... or MDQ6...\). Use GitHub GraphQL API to get this ID from organization or user login. | -| `title` | string | Yes | Project title | -| `apiKey` | string | Yes | GitHub Personal Access Token with project write permissions | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable confirmation message | -| `metadata` | object | Created project metadata | - -### `github_update_project` - -Update an existing GitHub Project V2. Can update title, description, visibility (public), or status (closed). Requires the project Node ID. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `project_id` | string | Yes | Project Node ID \(format: PVT_...\) | -| `title` | string | No | New project title | -| `shortDescription` | string | No | New project short description | -| `project_public` | boolean | No | Set project visibility \(true = public, false = private\) | -| `closed` | boolean | No | Set project status \(true = closed, false = open\) | -| `apiKey` | string | Yes | GitHub Personal Access Token with project write permissions | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable confirmation message | -| `metadata` | object | Updated project metadata | - -### `github_delete_project` - -Delete a GitHub Project V2. This action is permanent and cannot be undone. Requires the project Node ID. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `project_id` | string | Yes | Project Node ID \(format: PVT_...\) | -| `apiKey` | string | Yes | GitHub Personal Access Token with project admin permissions | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `content` | string | Human-readable confirmation message | -| `metadata` | object | Deleted project metadata | - - - -## Notes - -- Category: `tools` -- Type: `github` diff --git a/apps/docs/content/docs/en/tools/gitlab.mdx b/apps/docs/content/docs/en/tools/gitlab.mdx index f5b71f443d..e1b68a35ef 100644 --- a/apps/docs/content/docs/en/tools/gitlab.mdx +++ b/apps/docs/content/docs/en/tools/gitlab.mdx @@ -427,8 +427,3 @@ Cancel a running GitLab pipeline | `pipeline` | object | The cancelled GitLab pipeline | - -## Notes - -- Category: `tools` -- Type: `gitlab` diff --git a/apps/docs/content/docs/en/tools/gmail.mdx b/apps/docs/content/docs/en/tools/gmail.mdx index 6b01273671..3accf3bbee 100644 --- a/apps/docs/content/docs/en/tools/gmail.mdx +++ b/apps/docs/content/docs/en/tools/gmail.mdx @@ -1,50 +1,22 @@ --- title: Gmail -description: Send, read, search, and move Gmail messages or trigger workflows from Gmail events +description: Gmail message ID --- import { BlockInfoCard } from "@/components/ui/block-info-card" -{/* MANUAL-CONTENT-START:intro */} -[Gmail](https://gmail.com) is Google's popular email service that provides a robust platform for sending, receiving, and managing email communications. With over 1.8 billion active users worldwide, Gmail offers a feature-rich experience with powerful search capabilities, organizational tools, and integration options. - -With Gmail, you can: - -- **Send and receive emails**: Communicate with contacts through a clean, intuitive interface -- **Organize messages**: Use labels, folders, and filters to keep your inbox organized -- **Search efficiently**: Find specific messages quickly with Google's powerful search technology -- **Automate workflows**: Create filters and rules to automatically process incoming emails -- **Access from anywhere**: Use Gmail across devices with synchronized content and settings -- **Integrate with other services**: Connect with Google Calendar, Drive, and other productivity tools - -In Sim, the Gmail integration enables your agents to fully manage emails programmatically with comprehensive automation capabilities. This allows for powerful automation scenarios such as sending notifications, processing incoming messages, extracting information from emails, and managing communication workflows at scale. Your agents can: - -- **Compose and send**: Create personalized emails with attachments and send to recipients -- **Read and search**: Find specific messages using Gmail's query syntax and extract content -- **Organize intelligently**: Mark messages as read/unread, archive or unarchive emails, and manage labels -- **Clean up inbox**: Delete messages, move emails between labels, and maintain inbox zero -- **Trigger workflows**: Listen for new emails in real-time, enabling responsive workflows that react to incoming messages - -This integration bridges the gap between your AI workflows and email communications, enabling seamless interaction with one of the world's most widely used communication platforms. Whether you're automating customer support responses, processing receipts, managing subscriptions, or coordinating team communications, the Gmail integration provides all the tools you need for comprehensive email automation. -{/* MANUAL-CONTENT-END */} - - -## Usage Instructions - -Integrate Gmail into the workflow. Can send, read, search, and move emails. Can be used in trigger mode to trigger a workflow when a new email is received. - ## Tools ### `gmail_send` -Send emails using Gmail +Send emails using Gmail. Returns API-aligned fields only. #### Input @@ -64,12 +36,13 @@ Send emails using Gmail | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Success message | -| `metadata` | object | Email metadata | +| `id` | string | Gmail message ID | +| `threadId` | string | Gmail thread ID | +| `labelIds` | array | Email labels | ### `gmail_draft` -Draft emails using Gmail +Draft emails using Gmail. Returns API-aligned fields only. #### Input @@ -89,12 +62,14 @@ Draft emails using Gmail | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Success message | -| `metadata` | object | Draft metadata | +| `draftId` | string | Draft ID | +| `messageId` | string | Gmail message ID for the draft | +| `threadId` | string | Gmail thread ID | +| `labelIds` | array | Email labels | ### `gmail_read` -Read emails from Gmail +Read emails from Gmail. Returns API-aligned fields only. #### Input @@ -110,13 +85,22 @@ Read emails from Gmail | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Text content of the email | -| `metadata` | json | Metadata of the email | -| `attachments` | file[] | Attachments of the email | +| `id` | string | Gmail message ID | +| `threadId` | string | Gmail thread ID | +| `labelIds` | array | Email labels | +| `from` | string | Sender email address | +| `to` | string | Recipient email address | +| `subject` | string | Email subject | +| `date` | string | Email date | +| `body` | string | Email body text \(best-effort plain text\) | +| `hasAttachments` | boolean | Whether the email has attachments | +| `attachmentCount` | number | Number of attachments | +| `attachments` | file[] | Downloaded attachments \(if enabled\) | +| `results` | json | Summary results when reading multiple messages | ### `gmail_search` -Search emails in Gmail +Search emails in Gmail. Returns API-aligned fields only. #### Input @@ -129,12 +113,11 @@ Search emails in Gmail | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Search results summary | -| `metadata` | object | Search metadata | +| `results` | json | Array of search results | ### `gmail_move` -Move emails between Gmail labels/folders +Move emails between labels/folders in Gmail. Returns API-aligned fields only. #### Input @@ -148,12 +131,13 @@ Move emails between Gmail labels/folders | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Success message | -| `metadata` | object | Email metadata | +| `id` | string | Gmail message ID | +| `threadId` | string | Gmail thread ID | +| `labelIds` | array | Email labels | ### `gmail_mark_read` -Mark a Gmail message as read +Mark a Gmail message as read. Returns API-aligned fields only. #### Input @@ -165,12 +149,13 @@ Mark a Gmail message as read | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Success message | -| `metadata` | object | Email metadata | +| `id` | string | Gmail message ID | +| `threadId` | string | Gmail thread ID | +| `labelIds` | array | Updated email labels | ### `gmail_mark_unread` -Mark a Gmail message as unread +Mark a Gmail message as unread. Returns API-aligned fields only. #### Input @@ -182,12 +167,13 @@ Mark a Gmail message as unread | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Success message | -| `metadata` | object | Email metadata | +| `id` | string | Gmail message ID | +| `threadId` | string | Gmail thread ID | +| `labelIds` | array | Updated email labels | ### `gmail_archive` -Archive a Gmail message (remove from inbox) +Archive a Gmail message (remove from inbox). Returns API-aligned fields only. #### Input @@ -199,12 +185,13 @@ Archive a Gmail message (remove from inbox) | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Success message | -| `metadata` | object | Email metadata | +| `id` | string | Gmail message ID | +| `threadId` | string | Gmail thread ID | +| `labelIds` | array | Updated email labels | ### `gmail_unarchive` -Unarchive a Gmail message (move back to inbox) +Unarchive a Gmail message (move back to inbox). Returns API-aligned fields only. #### Input @@ -216,12 +203,13 @@ Unarchive a Gmail message (move back to inbox) | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Success message | -| `metadata` | object | Email metadata | +| `id` | string | Gmail message ID | +| `threadId` | string | Gmail thread ID | +| `labelIds` | array | Updated email labels | ### `gmail_delete` -Delete a Gmail message (move to trash) +Delete a Gmail message (move to trash). Returns API-aligned fields only. #### Input @@ -233,12 +221,13 @@ Delete a Gmail message (move to trash) | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Success message | -| `metadata` | object | Email metadata | +| `id` | string | Gmail message ID | +| `threadId` | string | Gmail thread ID | +| `labelIds` | array | Updated email labels | ### `gmail_add_label` -Add label(s) to a Gmail message +Add label(s) to a Gmail message. Returns API-aligned fields only. #### Input @@ -251,12 +240,13 @@ Add label(s) to a Gmail message | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Success message | -| `metadata` | object | Email metadata | +| `id` | string | Gmail message ID | +| `threadId` | string | Gmail thread ID | +| `labelIds` | array | Updated email labels | ### `gmail_remove_label` -Remove label(s) from a Gmail message +Remove label(s) from a Gmail message. Returns API-aligned fields only. #### Input @@ -269,12 +259,8 @@ Remove label(s) from a Gmail message | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Success message | -| `metadata` | object | Email metadata | - - +| `id` | string | Gmail message ID | +| `threadId` | string | Gmail thread ID | +| `labelIds` | array | Updated email labels | -## Notes -- Category: `tools` -- Type: `gmail` diff --git a/apps/docs/content/docs/en/tools/google_calendar.mdx b/apps/docs/content/docs/en/tools/google_calendar.mdx index b38eb3d995..8a6523a8dd 100644 --- a/apps/docs/content/docs/en/tools/google_calendar.mdx +++ b/apps/docs/content/docs/en/tools/google_calendar.mdx @@ -1,41 +1,22 @@ --- title: Google Calendar -description: Manage Google Calendar events +description: Event ID --- import { BlockInfoCard } from "@/components/ui/block-info-card" -{/* MANUAL-CONTENT-START:intro */} -[Google Calendar](https://calendar.google.com) is Google's powerful calendar and scheduling service that provides a comprehensive platform for managing events, meetings, and appointments. With seamless integration across Google's ecosystem and widespread adoption, Google Calendar offers robust features for both personal and professional scheduling needs. - -With Google Calendar, you can: - -- **Create and manage events**: Schedule meetings, appointments, and reminders with detailed information -- **Send calendar invites**: Automatically notify and coordinate with attendees through email invitations -- **Natural language event creation**: Quickly add events using conversational language like "Meeting with John tomorrow at 3pm" -- **View and search events**: Easily find and access your scheduled events across multiple calendars -- **Manage multiple calendars**: Organize different types of events across various calendars - -In Sim, the Google Calendar integration enables your agents to programmatically create, read, and manage calendar events. This allows for powerful automation scenarios such as scheduling meetings, sending calendar invites, checking availability, and managing event details. Your agents can create events with natural language input, send automated calendar invitations to attendees, retrieve event information, and list upcoming events. This integration bridges the gap between your AI workflows and calendar management, enabling seamless scheduling automation and coordination with one of the world's most widely used calendar platforms. -{/* MANUAL-CONTENT-END */} - - -## Usage Instructions - -Integrate Google Calendar into the workflow. Can create, read, update, and list calendar events. - ## Tools ### `google_calendar_create` -Create a new event in Google Calendar +Create a new event in Google Calendar. Returns API-aligned fields only. #### Input @@ -55,12 +36,21 @@ Create a new event in Google Calendar | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Event creation confirmation message | -| `metadata` | json | Created event metadata including ID, status, and details | +| `id` | string | Event ID | +| `htmlLink` | string | Event link | +| `status` | string | Event status | +| `summary` | string | Event title | +| `description` | string | Event description | +| `location` | string | Event location | +| `start` | json | Event start | +| `end` | json | Event end | +| `attendees` | json | Event attendees | +| `creator` | json | Event creator | +| `organizer` | json | Event organizer | ### `google_calendar_list` -List events from Google Calendar +List events from Google Calendar. Returns API-aligned fields only. #### Input @@ -76,12 +66,13 @@ List events from Google Calendar | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Summary of found events count | -| `metadata` | json | List of events with pagination tokens and event details | +| `nextPageToken` | string | Next page token | +| `timeZone` | string | Calendar time zone | +| `events` | json | List of events | ### `google_calendar_get` -Get a specific event from Google Calendar +Get a specific event from Google Calendar. Returns API-aligned fields only. #### Input @@ -94,12 +85,21 @@ Get a specific event from Google Calendar | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Event retrieval confirmation message | -| `metadata` | json | Event details including ID, status, times, and attendees | +| `id` | string | Event ID | +| `htmlLink` | string | Event link | +| `status` | string | Event status | +| `summary` | string | Event title | +| `description` | string | Event description | +| `location` | string | Event location | +| `start` | json | Event start | +| `end` | json | Event end | +| `attendees` | json | Event attendees | +| `creator` | json | Event creator | +| `organizer` | json | Event organizer | ### `google_calendar_quick_add` -Create events from natural language text +Create events from natural language text. Returns API-aligned fields only. #### Input @@ -114,12 +114,21 @@ Create events from natural language text | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Event creation confirmation message from natural language | -| `metadata` | json | Created event metadata including parsed details | +| `id` | string | Event ID | +| `htmlLink` | string | Event link | +| `status` | string | Event status | +| `summary` | string | Event title | +| `description` | string | Event description | +| `location` | string | Event location | +| `start` | json | Event start | +| `end` | json | Event end | +| `attendees` | json | Event attendees | +| `creator` | json | Event creator | +| `organizer` | json | Event organizer | ### `google_calendar_invite` -Invite attendees to an existing Google Calendar event +Invite attendees to an existing Google Calendar event. Returns API-aligned fields only. #### Input @@ -135,12 +144,16 @@ Invite attendees to an existing Google Calendar event | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Attendee invitation confirmation message with email delivery status | -| `metadata` | json | Updated event metadata including attendee list and details | - - +| `id` | string | Event ID | +| `htmlLink` | string | Event link | +| `status` | string | Event status | +| `summary` | string | Event title | +| `description` | string | Event description | +| `location` | string | Event location | +| `start` | json | Event start | +| `end` | json | Event end | +| `attendees` | json | Event attendees | +| `creator` | json | Event creator | +| `organizer` | json | Event organizer | -## Notes -- Category: `tools` -- Type: `google_calendar` diff --git a/apps/docs/content/docs/en/tools/google_docs.mdx b/apps/docs/content/docs/en/tools/google_docs.mdx index 06973e14c0..e9b9af71b9 100644 --- a/apps/docs/content/docs/en/tools/google_docs.mdx +++ b/apps/docs/content/docs/en/tools/google_docs.mdx @@ -126,8 +126,3 @@ Create a new Google Docs document | `metadata` | json | Created document metadata including ID, title, and URL | - -## Notes - -- Category: `tools` -- Type: `google_docs` diff --git a/apps/docs/content/docs/en/tools/google_drive.mdx b/apps/docs/content/docs/en/tools/google_drive.mdx index 54b3ed3bc3..15c184e0a0 100644 --- a/apps/docs/content/docs/en/tools/google_drive.mdx +++ b/apps/docs/content/docs/en/tools/google_drive.mdx @@ -96,13 +96,13 @@ Download a file from Google Drive with complete metadata (exports Google Workspa | `fileId` | string | Yes | The ID of the file to download | | `mimeType` | string | No | The MIME type to export Google Workspace files to \(optional\) | | `fileName` | string | No | Optional filename override | -| `includeRevisions` | boolean | No | Whether to include revision history in the metadata \(default: true\) | +| `includeRevisions` | boolean | No | Whether to include revision history in the metadata \(default: true, returns first 100 revisions\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `file` | object | Downloaded file stored in execution files | +| `file` | object | Downloaded file data | ### `google_drive_list` @@ -125,8 +125,3 @@ List files and folders in Google Drive with complete metadata | `files` | array | Array of file metadata objects from Google Drive | - -## Notes - -- Category: `tools` -- Type: `google_drive` diff --git a/apps/docs/content/docs/en/tools/google_forms.mdx b/apps/docs/content/docs/en/tools/google_forms.mdx index f5f84e9158..52ee3053ab 100644 --- a/apps/docs/content/docs/en/tools/google_forms.mdx +++ b/apps/docs/content/docs/en/tools/google_forms.mdx @@ -49,8 +49,3 @@ Integrate Google Forms into your workflow. Provide a Form ID to list responses, | `data` | json | Response or list of responses | - -## Notes - -- Category: `tools` -- Type: `google_forms` diff --git a/apps/docs/content/docs/en/tools/google_groups.mdx b/apps/docs/content/docs/en/tools/google_groups.mdx index e6dc141c4a..793bc90134 100644 --- a/apps/docs/content/docs/en/tools/google_groups.mdx +++ b/apps/docs/content/docs/en/tools/google_groups.mdx @@ -216,8 +216,3 @@ Check if a user is a member of a Google Group | `isMember` | boolean | Whether the user is a member of the group | - -## Notes - -- Category: `tools` -- Type: `google_groups` diff --git a/apps/docs/content/docs/en/tools/google_search.mdx b/apps/docs/content/docs/en/tools/google_search.mdx index 43cf08eee3..17a599c9d6 100644 --- a/apps/docs/content/docs/en/tools/google_search.mdx +++ b/apps/docs/content/docs/en/tools/google_search.mdx @@ -65,8 +65,3 @@ Search the web with the Custom Search API | `items` | array | Array of search results from Google | - -## Notes - -- Category: `tools` -- Type: `google_search` diff --git a/apps/docs/content/docs/en/tools/google_sheets.mdx b/apps/docs/content/docs/en/tools/google_sheets.mdx index 7efd523066..8d93c7a045 100644 --- a/apps/docs/content/docs/en/tools/google_sheets.mdx +++ b/apps/docs/content/docs/en/tools/google_sheets.mdx @@ -176,8 +176,3 @@ Append data to the end of a Google Sheets spreadsheet | `metadata` | json | Spreadsheet metadata including ID and URL | - -## Notes - -- Category: `tools` -- Type: `google_sheets` diff --git a/apps/docs/content/docs/en/tools/google_slides.mdx b/apps/docs/content/docs/en/tools/google_slides.mdx index ebe08628f9..b8a290b01b 100644 --- a/apps/docs/content/docs/en/tools/google_slides.mdx +++ b/apps/docs/content/docs/en/tools/google_slides.mdx @@ -178,8 +178,3 @@ Generate a thumbnail image of a specific slide in a Google Slides presentation | `metadata` | json | Operation metadata including presentation ID and page object ID | - -## Notes - -- Category: `tools` -- Type: `google_slides` diff --git a/apps/docs/content/docs/en/tools/google_vault.mdx b/apps/docs/content/docs/en/tools/google_vault.mdx index 39714d1122..a9b39df483 100644 --- a/apps/docs/content/docs/en/tools/google_vault.mdx +++ b/apps/docs/content/docs/en/tools/google_vault.mdx @@ -157,8 +157,3 @@ List matters, or get a specific matter if matterId is provided | `nextPageToken` | string | Token for fetching next page of results | - -## Notes - -- Category: `tools` -- Type: `google_vault` diff --git a/apps/docs/content/docs/en/tools/grafana.mdx b/apps/docs/content/docs/en/tools/grafana.mdx index 6654b1c127..656370efd5 100644 --- a/apps/docs/content/docs/en/tools/grafana.mdx +++ b/apps/docs/content/docs/en/tools/grafana.mdx @@ -502,8 +502,3 @@ Create a new folder in Grafana | `version` | number | Version number of the folder | - -## Notes - -- Category: `tools` -- Type: `grafana` diff --git a/apps/docs/content/docs/en/tools/grain.mdx b/apps/docs/content/docs/en/tools/grain.mdx index 7df544d8ae..8d223acb16 100644 --- a/apps/docs/content/docs/en/tools/grain.mdx +++ b/apps/docs/content/docs/en/tools/grain.mdx @@ -218,8 +218,3 @@ Delete a webhook by ID | `success` | boolean | True when webhook was successfully deleted | - -## Notes - -- Category: `tools` -- Type: `grain` diff --git a/apps/docs/content/docs/en/tools/greptile.mdx b/apps/docs/content/docs/en/tools/greptile.mdx index ba775dd2ae..726243bfcc 100644 --- a/apps/docs/content/docs/en/tools/greptile.mdx +++ b/apps/docs/content/docs/en/tools/greptile.mdx @@ -134,8 +134,3 @@ Check the indexing status of a repository. Use this to verify if a repository is | `sha` | string | Git commit SHA of the indexed version | - -## Notes - -- Category: `tools` -- Type: `greptile` diff --git a/apps/docs/content/docs/en/tools/hubspot.mdx b/apps/docs/content/docs/en/tools/hubspot.mdx index a1e64a9cec..b7029b819a 100644 --- a/apps/docs/content/docs/en/tools/hubspot.mdx +++ b/apps/docs/content/docs/en/tools/hubspot.mdx @@ -51,7 +51,7 @@ Retrieve all users from HubSpot account | Parameter | Type | Description | | --------- | ---- | ----------- | | `users` | array | Array of HubSpot user objects | -| `metadata` | object | Operation metadata | +| `totalItems` | number | Total number of users returned | | `success` | boolean | Operation success status | ### `hubspot_list_contacts` @@ -73,7 +73,7 @@ Retrieve all contacts from HubSpot account with pagination support | --------- | ---- | ----------- | | `contacts` | array | Array of HubSpot contact objects | | `paging` | object | Pagination information | -| `metadata` | object | Operation metadata | +| `metadata` | object | Metadata with totalReturned and hasMore | | `success` | boolean | Operation success status | ### `hubspot_get_contact` @@ -94,7 +94,7 @@ Retrieve a single contact by ID or email from HubSpot | Parameter | Type | Description | | --------- | ---- | ----------- | | `contact` | object | HubSpot contact object with properties | -| `metadata` | object | Operation metadata | +| `contactId` | string | The retrieved contact ID | | `success` | boolean | Operation success status | ### `hubspot_create_contact` @@ -113,7 +113,7 @@ Create a new contact in HubSpot. Requires at least one of: email, firstname, or | Parameter | Type | Description | | --------- | ---- | ----------- | | `contact` | object | Created HubSpot contact object | -| `metadata` | object | Operation metadata | +| `contactId` | string | The created contact ID | | `success` | boolean | Operation success status | ### `hubspot_update_contact` @@ -133,7 +133,7 @@ Update an existing contact in HubSpot by ID or email | Parameter | Type | Description | | --------- | ---- | ----------- | | `contact` | object | Updated HubSpot contact object | -| `metadata` | object | Operation metadata | +| `contactId` | string | The updated contact ID | | `success` | boolean | Operation success status | ### `hubspot_search_contacts` @@ -158,7 +158,7 @@ Search for contacts in HubSpot using filters, sorting, and queries | `contacts` | array | Array of matching HubSpot contact objects | | `total` | number | Total number of matching contacts | | `paging` | object | Pagination information | -| `metadata` | object | Operation metadata | +| `metadata` | object | Metadata with totalReturned and hasMore | | `success` | boolean | Operation success status | ### `hubspot_list_companies` @@ -180,7 +180,7 @@ Retrieve all companies from HubSpot account with pagination support | --------- | ---- | ----------- | | `companies` | array | Array of HubSpot company objects | | `paging` | object | Pagination information | -| `metadata` | object | Operation metadata | +| `metadata` | object | Metadata with totalReturned and hasMore | | `success` | boolean | Operation success status | ### `hubspot_get_company` @@ -201,7 +201,7 @@ Retrieve a single company by ID or domain from HubSpot | Parameter | Type | Description | | --------- | ---- | ----------- | | `company` | object | HubSpot company object with properties | -| `metadata` | object | Operation metadata | +| `companyId` | string | The retrieved company ID | | `success` | boolean | Operation success status | ### `hubspot_create_company` @@ -220,7 +220,7 @@ Create a new company in HubSpot | Parameter | Type | Description | | --------- | ---- | ----------- | | `company` | object | Created HubSpot company object | -| `metadata` | object | Operation metadata | +| `companyId` | string | The created company ID | | `success` | boolean | Operation success status | ### `hubspot_update_company` @@ -240,7 +240,7 @@ Update an existing company in HubSpot by ID or domain | Parameter | Type | Description | | --------- | ---- | ----------- | | `company` | object | Updated HubSpot company object | -| `metadata` | object | Operation metadata | +| `companyId` | string | The updated company ID | | `success` | boolean | Operation success status | ### `hubspot_search_companies` @@ -265,7 +265,7 @@ Search for companies in HubSpot using filters, sorting, and queries | `companies` | array | Array of matching HubSpot company objects | | `total` | number | Total number of matching companies | | `paging` | object | Pagination information | -| `metadata` | object | Operation metadata | +| `metadata` | object | Metadata with totalReturned and hasMore | | `success` | boolean | Operation success status | ### `hubspot_list_deals` @@ -287,12 +287,7 @@ Retrieve all deals from HubSpot account with pagination support | --------- | ---- | ----------- | | `deals` | array | Array of HubSpot deal objects | | `paging` | object | Pagination information | -| `metadata` | object | Operation metadata | +| `metadata` | object | Metadata with totalReturned and hasMore | | `success` | boolean | Operation success status | - -## Notes - -- Category: `tools` -- Type: `hubspot` diff --git a/apps/docs/content/docs/en/tools/huggingface.mdx b/apps/docs/content/docs/en/tools/huggingface.mdx index 1a3a30976b..c6b0cafecb 100644 --- a/apps/docs/content/docs/en/tools/huggingface.mdx +++ b/apps/docs/content/docs/en/tools/huggingface.mdx @@ -56,8 +56,3 @@ Generate completions using Hugging Face Inference API | `output` | object | Chat completion results | - -## Notes - -- Category: `tools` -- Type: `huggingface` diff --git a/apps/docs/content/docs/en/tools/hunter.mdx b/apps/docs/content/docs/en/tools/hunter.mdx index c654af1a3e..6b837329bd 100644 --- a/apps/docs/content/docs/en/tools/hunter.mdx +++ b/apps/docs/content/docs/en/tools/hunter.mdx @@ -190,8 +190,3 @@ Returns the total number of email addresses found for a domain or company. | `seniority` | object | Breakdown of email addresses by seniority level \(junior, senior, executive\) | - -## Notes - -- Category: `tools` -- Type: `hunter` diff --git a/apps/docs/content/docs/en/tools/image_generator.mdx b/apps/docs/content/docs/en/tools/image_generator.mdx index 8d068ef23a..3d59c450c9 100644 --- a/apps/docs/content/docs/en/tools/image_generator.mdx +++ b/apps/docs/content/docs/en/tools/image_generator.mdx @@ -60,8 +60,3 @@ Generate images using OpenAI | `output` | object | Generated image data | - -## Notes - -- Category: `tools` -- Type: `image_generator` diff --git a/apps/docs/content/docs/en/tools/imap.mdx b/apps/docs/content/docs/en/tools/imap.mdx index af53c8200f..37bb0016a8 100644 --- a/apps/docs/content/docs/en/tools/imap.mdx +++ b/apps/docs/content/docs/en/tools/imap.mdx @@ -33,8 +33,3 @@ Connect to any email server via IMAP protocol to trigger workflows when new emai - -## Notes - -- Category: `triggers` -- Type: `imap` diff --git a/apps/docs/content/docs/en/tools/incidentio.mdx b/apps/docs/content/docs/en/tools/incidentio.mdx index 9c0edaf4bc..ee44247b5f 100644 --- a/apps/docs/content/docs/en/tools/incidentio.mdx +++ b/apps/docs/content/docs/en/tools/incidentio.mdx @@ -841,8 +841,3 @@ Delete an escalation path in incident.io | `message` | string | Success message | - -## Notes - -- Category: `tools` -- Type: `incidentio` diff --git a/apps/docs/content/docs/en/tools/intercom.mdx b/apps/docs/content/docs/en/tools/intercom.mdx index 760e4f46e1..b84c02d974 100644 --- a/apps/docs/content/docs/en/tools/intercom.mdx +++ b/apps/docs/content/docs/en/tools/intercom.mdx @@ -1,41 +1,22 @@ --- title: Intercom -description: Manage contacts, companies, conversations, tickets, and messages in Intercom +description: Contact object with id, type, role, email, phone, name, external_id, created_at, updated_at --- import { BlockInfoCard } from "@/components/ui/block-info-card" -{/* MANUAL-CONTENT-START:intro */} -[Intercom](https://www.intercom.com/) is a leading customer communications platform that enables you to manage and automate your interactions with contacts, companies, conversations, tickets, and messages—all in one place. The Intercom integration in Sim lets your agents programmatically manage customer relationships, support requests, and conversations directly from your automated workflows. - -With the Intercom tools, you can: - -- **Contacts Management:** Create, get, update, list, search, and delete contacts—automate your CRM processes and keep your customer records up-to-date. -- **Company Management:** Create new companies, retrieve company details, and list all companies related to your users or business clients. -- **Conversation Handling:** Get, list, reply to, and search through conversations—allowing agents to track ongoing support threads, provide answers, and automate follow-up actions. -- **Ticket Management:** Create and retrieve tickets programmatically, helping you automate customer service, support issue tracking, and workflow escalations. -- **Send Messages:** Trigger messages to users or leads for onboarding, support, or marketing, all from within your workflow automation. - -By integrating Intercom tools into Sim, you empower your workflows to communicate directly with your users, automate customer support processes, manage leads, and streamline communications at scale. Whether you need to create new contacts, keep customer data synchronized, manage support tickets, or send personalized engagement messages, the Intercom tools provide a comprehensive way to manage customer interactions as part of your intelligent automations. -{/* MANUAL-CONTENT-END */} - - -## Usage Instructions - -Integrate Intercom into the workflow. Can create, get, update, list, search, and delete contacts; create, get, and list companies; get, list, reply, and search conversations; create and get tickets; and create messages. - ## Tools ### `intercom_create_contact` -Create a new contact in Intercom with email, external_id, or role +Create a new contact in Intercom with email, external_id, or role. Returns API-aligned fields only. #### Input @@ -62,7 +43,7 @@ Create a new contact in Intercom with email, external_id, or role ### `intercom_get_contact` -Get a single contact by ID from Intercom +Get a single contact by ID from Intercom. Returns API-aligned fields only. #### Input @@ -78,7 +59,7 @@ Get a single contact by ID from Intercom ### `intercom_update_contact` -Update an existing contact in Intercom +Update an existing contact in Intercom. Returns API-aligned fields only. #### Input @@ -143,7 +124,7 @@ Search for contacts in Intercom using a query ### `intercom_delete_contact` -Delete a contact from Intercom by ID +Delete a contact from Intercom by ID. Returns API-aligned fields only. #### Input @@ -157,7 +138,6 @@ Delete a contact from Intercom by ID | --------- | ---- | ----------- | | `id` | string | ID of deleted contact | | `deleted` | boolean | Whether the contact was deleted | -| `metadata` | object | Operation metadata | ### `intercom_create_company` @@ -277,7 +257,7 @@ Reply to a conversation as an admin in Intercom ### `intercom_search_conversations` -Search for conversations in Intercom using a query +Search for conversations in Intercom using a query. Returns API-aligned fields only. #### Input @@ -297,7 +277,7 @@ Search for conversations in Intercom using a query ### `intercom_create_ticket` -Create a new ticket in Intercom +Create a new ticket in Intercom. Returns API-aligned fields only. #### Input @@ -319,7 +299,7 @@ Create a new ticket in Intercom ### `intercom_get_ticket` -Retrieve a single ticket by ID from Intercom +Retrieve a single ticket by ID from Intercom. Returns API-aligned fields only. #### Input @@ -335,7 +315,7 @@ Retrieve a single ticket by ID from Intercom ### `intercom_create_message` -Create and send a new admin-initiated message in Intercom +Create and send a new admin-initiated message in Intercom. Returns API-aligned fields only. #### Input @@ -358,8 +338,3 @@ Create and send a new admin-initiated message in Intercom | `message` | object | Created message object | - -## Notes - -- Category: `tools` -- Type: `intercom` diff --git a/apps/docs/content/docs/en/tools/jina.mdx b/apps/docs/content/docs/en/tools/jina.mdx index ec20f9ff85..c037d00817 100644 --- a/apps/docs/content/docs/en/tools/jina.mdx +++ b/apps/docs/content/docs/en/tools/jina.mdx @@ -93,8 +93,3 @@ Search the web and return top 5 results with LLM-friendly content. Each result i | `results` | array | Array of search results, each containing title, description, url, and LLM-friendly content | - -## Notes - -- Category: `tools` -- Type: `jina` diff --git a/apps/docs/content/docs/en/tools/jira.mdx b/apps/docs/content/docs/en/tools/jira.mdx index 7018f79b6e..b2a296e3e8 100644 --- a/apps/docs/content/docs/en/tools/jira.mdx +++ b/apps/docs/content/docs/en/tools/jira.mdx @@ -555,8 +555,3 @@ Get Jira users. If an account ID is provided, returns a single user. Otherwise, | `maxResults` | number | Maximum results per page | - -## Notes - -- Category: `tools` -- Type: `jira` diff --git a/apps/docs/content/docs/en/tools/jira_service_management.mdx b/apps/docs/content/docs/en/tools/jira_service_management.mdx index 2b3d57a2b1..a068f3a65b 100644 --- a/apps/docs/content/docs/en/tools/jira_service_management.mdx +++ b/apps/docs/content/docs/en/tools/jira_service_management.mdx @@ -483,8 +483,3 @@ Approve or decline an approval request in Jira Service Management | `success` | boolean | Whether the operation succeeded | - -## Notes - -- Category: `tools` -- Type: `jira_service_management` diff --git a/apps/docs/content/docs/en/tools/kalshi.mdx b/apps/docs/content/docs/en/tools/kalshi.mdx index 199ec05425..0757348b18 100644 --- a/apps/docs/content/docs/en/tools/kalshi.mdx +++ b/apps/docs/content/docs/en/tools/kalshi.mdx @@ -382,8 +382,3 @@ Modify the price or quantity of an existing order on Kalshi | `order` | object | The amended order object | - -## Notes - -- Category: `tools` -- Type: `kalshi` diff --git a/apps/docs/content/docs/en/tools/knowledge.mdx b/apps/docs/content/docs/en/tools/knowledge.mdx index fa5208cf33..00aca66cd8 100644 --- a/apps/docs/content/docs/en/tools/knowledge.mdx +++ b/apps/docs/content/docs/en/tools/knowledge.mdx @@ -108,8 +108,3 @@ Create a new document in a knowledge base | `data` | object | Information about the created document | - -## Notes - -- Category: `blocks` -- Type: `knowledge` diff --git a/apps/docs/content/docs/en/tools/lemlist.mdx b/apps/docs/content/docs/en/tools/lemlist.mdx new file mode 100644 index 0000000000..c3b38bb720 --- /dev/null +++ b/apps/docs/content/docs/en/tools/lemlist.mdx @@ -0,0 +1,95 @@ +--- +title: Lemlist +description: Manage outreach activities, leads, and send emails via Lemlist +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + +## Usage Instructions + +Integrate Lemlist into your workflow. Retrieve campaign activities and replies, get lead information, and send emails through the Lemlist inbox. + + + +## Tools + +### `lemlist_get_activities` + +Retrieves campaign activities and steps performed, including email opens, clicks, replies, and other events. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Lemlist API key | +| `type` | string | No | Filter by activity type \(e.g., emailOpened, emailClicked, emailReplied, paused\) | +| `campaignId` | string | No | Filter by campaign ID | +| `leadId` | string | No | Filter by lead ID | +| `isFirst` | boolean | No | Filter for first activity only | +| `limit` | number | No | Number of results per request \(max 100, default 100\) | +| `offset` | number | No | Number of records to skip for pagination | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `activities` | array | List of activities | + +### `lemlist_get_lead` + +Retrieves lead information by email address or lead ID. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Lemlist API key | +| `email` | string | No | Lead email address \(use either email or id\) | +| `id` | string | No | Lead ID \(use either email or id\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `_id` | string | Lead ID | +| `email` | string | Lead email address | +| `firstName` | string | Lead first name | +| `lastName` | string | Lead last name | +| `companyName` | string | Company name | +| `jobTitle` | string | Job title | +| `companyDomain` | string | Company domain | +| `isPaused` | boolean | Whether the lead is paused | +| `campaignId` | string | Campaign ID the lead belongs to | +| `contactId` | string | Contact ID | +| `emailStatus` | string | Email deliverability status | + +### `lemlist_send_email` + +Sends an email to a contact through the Lemlist inbox. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Lemlist API key | +| `sendUserId` | string | Yes | Identifier for the user sending the message | +| `sendUserEmail` | string | Yes | Email address of the sender | +| `sendUserMailboxId` | string | Yes | Mailbox identifier for the sender | +| `contactId` | string | Yes | Recipient contact identifier | +| `leadId` | string | Yes | Associated lead identifier | +| `subject` | string | Yes | Email subject line | +| `message` | string | Yes | Email message body in HTML format | +| `cc` | json | No | Array of CC email addresses | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `ok` | boolean | Whether the email was sent successfully | + + diff --git a/apps/docs/content/docs/en/tools/linear.mdx b/apps/docs/content/docs/en/tools/linear.mdx index c0d6daa5fd..78c95912d9 100644 --- a/apps/docs/content/docs/en/tools/linear.mdx +++ b/apps/docs/content/docs/en/tools/linear.mdx @@ -1471,8 +1471,3 @@ List all project statuses in Linear | `projectStatuses` | array | List of project statuses | - -## Notes - -- Category: `tools` -- Type: `linear` diff --git a/apps/docs/content/docs/en/tools/linkedin.mdx b/apps/docs/content/docs/en/tools/linkedin.mdx index 78d5374357..7fd37685b5 100644 --- a/apps/docs/content/docs/en/tools/linkedin.mdx +++ b/apps/docs/content/docs/en/tools/linkedin.mdx @@ -81,8 +81,3 @@ Retrieve your LinkedIn profile information | `error` | string | Error message if operation failed | - -## Notes - -- Category: `tools` -- Type: `linkedin` diff --git a/apps/docs/content/docs/en/tools/linkup.mdx b/apps/docs/content/docs/en/tools/linkup.mdx index ea503c5141..e6b0c3eb3d 100644 --- a/apps/docs/content/docs/en/tools/linkup.mdx +++ b/apps/docs/content/docs/en/tools/linkup.mdx @@ -60,8 +60,3 @@ Search the web for information using Linkup | `sources` | array | Array of sources used to compile the answer, each containing name, url, and snippet | - -## Notes - -- Category: `tools` -- Type: `linkup` diff --git a/apps/docs/content/docs/en/tools/mailchimp.mdx b/apps/docs/content/docs/en/tools/mailchimp.mdx index 4a5efcb821..7ce7ef5bfb 100644 --- a/apps/docs/content/docs/en/tools/mailchimp.mdx +++ b/apps/docs/content/docs/en/tools/mailchimp.mdx @@ -70,8 +70,8 @@ Retrieve a list of audiences (lists) from Mailchimp | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Audiences data and metadata | +| `success` | boolean | Whether the audiences were successfully retrieved | +| `output` | object | Audiences data | ### `mailchimp_get_audience` @@ -88,8 +88,8 @@ Retrieve details of a specific audience (list) from Mailchimp | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Audience data and metadata | +| `success` | boolean | Whether the audience was successfully retrieved | +| `output` | object | Audience data | ### `mailchimp_create_audience` @@ -150,8 +150,7 @@ Delete an audience (list) from Mailchimp | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Deletion confirmation | +| `success` | boolean | Whether the audience was successfully deleted | ### `mailchimp_get_members` @@ -171,8 +170,8 @@ Retrieve a list of members from a Mailchimp audience | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Members data and metadata | +| `success` | boolean | Whether the members were successfully retrieved | +| `output` | object | Members data | ### `mailchimp_get_member` @@ -190,8 +189,8 @@ Retrieve details of a specific member from a Mailchimp audience | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Member data and metadata | +| `success` | boolean | Whether the member was successfully retrieved | +| `output` | object | Member data | ### `mailchimp_add_member` @@ -277,8 +276,7 @@ Delete a member from a Mailchimp audience | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Deletion confirmation | +| `success` | boolean | Whether the member was successfully deleted | ### `mailchimp_archive_member` @@ -338,8 +336,8 @@ Retrieve a list of campaigns from Mailchimp | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Campaigns data and metadata | +| `success` | boolean | Whether the campaigns were successfully retrieved | +| `output` | object | Campaigns data | ### `mailchimp_get_campaign` @@ -356,8 +354,8 @@ Retrieve details of a specific campaign from Mailchimp | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Campaign data and metadata | +| `success` | boolean | Whether the campaign was successfully retrieved | +| `output` | object | Campaign data | ### `mailchimp_create_campaign` @@ -414,8 +412,7 @@ Delete a campaign from Mailchimp | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Deletion confirmation | +| `success` | boolean | Whether the campaign was successfully deleted | ### `mailchimp_send_campaign` @@ -451,8 +448,7 @@ Schedule a Mailchimp campaign to be sent at a specific time | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Schedule confirmation | +| `success` | boolean | Whether the campaign was successfully scheduled | ### `mailchimp_unschedule_campaign` @@ -505,7 +501,7 @@ Retrieve the HTML and plain-text content for a Mailchimp campaign | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | +| `success` | boolean | Whether the campaign content was successfully retrieved | | `output` | object | Campaign content data | ### `mailchimp_set_campaign_content` @@ -545,8 +541,8 @@ Retrieve a list of automations from Mailchimp | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Automations data and metadata | +| `success` | boolean | Whether the automations were successfully retrieved | +| `output` | object | Automations data | ### `mailchimp_get_automation` @@ -563,8 +559,8 @@ Retrieve details of a specific automation from Mailchimp | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Automation data and metadata | +| `success` | boolean | Whether the automation was successfully retrieved | +| `output` | object | Automation data | ### `mailchimp_start_automation` @@ -638,8 +634,8 @@ Retrieve a list of templates from Mailchimp | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Templates data and metadata | +| `success` | boolean | Whether the templates were successfully retrieved | +| `output` | object | Templates data | ### `mailchimp_get_template` @@ -656,8 +652,8 @@ Retrieve details of a specific template from Mailchimp | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Template data and metadata | +| `success` | boolean | Whether the template was successfully retrieved | +| `output` | object | Template data | ### `mailchimp_create_template` @@ -713,8 +709,7 @@ Delete a template from Mailchimp | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Deletion confirmation | +| `success` | boolean | Whether the template was successfully deleted | ### `mailchimp_get_campaign_reports` @@ -732,8 +727,8 @@ Retrieve a list of campaign reports from Mailchimp | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Campaign reports data and metadata | +| `success` | boolean | Whether the campaign reports were successfully retrieved | +| `output` | object | Campaign reports data | ### `mailchimp_get_campaign_report` @@ -750,8 +745,8 @@ Retrieve the report for a specific campaign from Mailchimp | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Campaign report data and metadata | +| `success` | boolean | Whether the campaign report was successfully retrieved | +| `output` | object | Campaign report data | ### `mailchimp_get_segments` @@ -770,8 +765,8 @@ Retrieve a list of segments from a Mailchimp audience | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Segments data and metadata | +| `success` | boolean | Whether the segments were successfully retrieved | +| `output` | object | Segments data | ### `mailchimp_get_segment` @@ -789,8 +784,8 @@ Retrieve details of a specific segment from a Mailchimp audience | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Segment data and metadata | +| `success` | boolean | Whether the segment was successfully retrieved | +| `output` | object | Segment data | ### `mailchimp_create_segment` @@ -849,8 +844,7 @@ Delete a segment from a Mailchimp audience | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Deletion confirmation | +| `success` | boolean | Whether the segment was successfully deleted | ### `mailchimp_get_segment_members` @@ -870,8 +864,8 @@ Retrieve members of a specific segment from a Mailchimp audience | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Segment members data and metadata | +| `success` | boolean | Whether the segment members were successfully retrieved | +| `output` | object | Segment members data | ### `mailchimp_add_segment_member` @@ -929,8 +923,8 @@ Retrieve tags associated with a member in a Mailchimp audience | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Member tags data and metadata | +| `success` | boolean | Whether the member tags were successfully retrieved | +| `output` | object | Member tags data | ### `mailchimp_add_member_tags` @@ -989,8 +983,8 @@ Retrieve a list of merge fields from a Mailchimp audience | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Merge fields data and metadata | +| `success` | boolean | Whether the merge fields were successfully retrieved | +| `output` | object | Merge fields data | ### `mailchimp_get_merge_field` @@ -1008,8 +1002,8 @@ Retrieve details of a specific merge field from a Mailchimp audience | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Merge field data and metadata | +| `success` | boolean | Whether the merge field was successfully retrieved | +| `output` | object | Merge field data | ### `mailchimp_create_merge_field` @@ -1067,8 +1061,7 @@ Delete a merge field from a Mailchimp audience | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Deletion confirmation | +| `success` | boolean | Whether the merge field was successfully deleted | ### `mailchimp_get_interest_categories` @@ -1087,8 +1080,8 @@ Retrieve a list of interest categories from a Mailchimp audience | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Interest categories data and metadata | +| `success` | boolean | Whether the interest categories were successfully retrieved | +| `output` | object | Interest categories data | ### `mailchimp_get_interest_category` @@ -1106,8 +1099,8 @@ Retrieve details of a specific interest category from a Mailchimp audience | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Interest category data and metadata | +| `success` | boolean | Whether the interest category was successfully retrieved | +| `output` | object | Interest category data | ### `mailchimp_create_interest_category` @@ -1165,8 +1158,7 @@ Delete an interest category from a Mailchimp audience | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Deletion confirmation | +| `success` | boolean | Whether the interest category was successfully deleted | ### `mailchimp_get_interests` @@ -1186,8 +1178,8 @@ Retrieve a list of interests from an interest category in a Mailchimp audience | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Interests data and metadata | +| `success` | boolean | Whether the interests were successfully retrieved | +| `output` | object | Interests data | ### `mailchimp_get_interest` @@ -1206,8 +1198,8 @@ Retrieve details of a specific interest from an interest category in a Mailchimp | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Interest data and metadata | +| `success` | boolean | Whether the interest was successfully retrieved | +| `output` | object | Interest data | ### `mailchimp_create_interest` @@ -1267,8 +1259,7 @@ Delete an interest from an interest category in a Mailchimp audience | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Deletion confirmation | +| `success` | boolean | Whether the interest was successfully deleted | ### `mailchimp_get_landing_pages` @@ -1286,8 +1277,8 @@ Retrieve a list of landing pages from Mailchimp | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Landing pages data and metadata | +| `success` | boolean | Whether the landing pages were successfully retrieved | +| `output` | object | Landing pages data | ### `mailchimp_get_landing_page` @@ -1304,8 +1295,8 @@ Retrieve details of a specific landing page from Mailchimp | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Landing page data and metadata | +| `success` | boolean | Whether the landing page was successfully retrieved | +| `output` | object | Landing page data | ### `mailchimp_create_landing_page` @@ -1360,8 +1351,7 @@ Delete a landing page from Mailchimp | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Deletion confirmation | +| `success` | boolean | Whether the landing page was successfully deleted | ### `mailchimp_publish_landing_page` @@ -1415,8 +1405,8 @@ Retrieve a list of batch operations from Mailchimp | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Batch operations data and metadata | +| `success` | boolean | Whether the batch operations were successfully retrieved | +| `output` | object | Batch operations data | ### `mailchimp_get_batch_operation` @@ -1433,8 +1423,8 @@ Retrieve details of a specific batch operation from Mailchimp | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Batch operation data and metadata | +| `success` | boolean | Whether the batch operation was successfully retrieved | +| `output` | object | Batch operation data | ### `mailchimp_create_batch_operation` @@ -1469,12 +1459,6 @@ Delete a batch operation from Mailchimp | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Operation success status | -| `output` | object | Deletion confirmation | - - +| `success` | boolean | Whether the batch operation was successfully deleted | -## Notes -- Category: `tools` -- Type: `mailchimp` diff --git a/apps/docs/content/docs/en/tools/mailgun.mdx b/apps/docs/content/docs/en/tools/mailgun.mdx index 9fb05cfab7..0d29cc9c55 100644 --- a/apps/docs/content/docs/en/tools/mailgun.mdx +++ b/apps/docs/content/docs/en/tools/mailgun.mdx @@ -214,8 +214,3 @@ Get details of a specific domain | `domain` | json | Domain details | - -## Notes - -- Category: `tools` -- Type: `mailgun` diff --git a/apps/docs/content/docs/en/tools/mem0.mdx b/apps/docs/content/docs/en/tools/mem0.mdx index ff94317490..680c130771 100644 --- a/apps/docs/content/docs/en/tools/mem0.mdx +++ b/apps/docs/content/docs/en/tools/mem0.mdx @@ -96,8 +96,3 @@ Retrieve memories from Mem0 by ID or filter criteria | `ids` | array | Array of memory IDs that were retrieved | - -## Notes - -- Category: `tools` -- Type: `mem0` diff --git a/apps/docs/content/docs/en/tools/memory.mdx b/apps/docs/content/docs/en/tools/memory.mdx index d2990a74ba..9fb09ade4a 100644 --- a/apps/docs/content/docs/en/tools/memory.mdx +++ b/apps/docs/content/docs/en/tools/memory.mdx @@ -97,8 +97,3 @@ Delete memories by conversationId. | `error` | string | Error message if operation failed | - -## Notes - -- Category: `blocks` -- Type: `memory` diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index 8a2bca881a..e489efd205 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -51,6 +51,7 @@ "jira_service_management", "kalshi", "knowledge", + "lemlist", "linear", "linkedin", "linkup", @@ -101,7 +102,6 @@ "supabase", "tavily", "telegram", - "thinking", "translate", "trello", "tts", diff --git a/apps/docs/content/docs/en/tools/microsoft_excel.mdx b/apps/docs/content/docs/en/tools/microsoft_excel.mdx index f260e0653f..b7a2a3fb0d 100644 --- a/apps/docs/content/docs/en/tools/microsoft_excel.mdx +++ b/apps/docs/content/docs/en/tools/microsoft_excel.mdx @@ -112,8 +112,3 @@ Create a new worksheet (sheet) in a Microsoft Excel workbook | `worksheet` | object | Details of the newly created worksheet | - -## Notes - -- Category: `tools` -- Type: `microsoft_excel` diff --git a/apps/docs/content/docs/en/tools/microsoft_planner.mdx b/apps/docs/content/docs/en/tools/microsoft_planner.mdx index 3d26c3ab52..ef8e7b94dc 100644 --- a/apps/docs/content/docs/en/tools/microsoft_planner.mdx +++ b/apps/docs/content/docs/en/tools/microsoft_planner.mdx @@ -294,8 +294,3 @@ Update task details including description, checklist items, and references in Mi | `metadata` | object | Metadata including taskId | - -## Notes - -- Category: `tools` -- Type: `microsoft_planner` diff --git a/apps/docs/content/docs/en/tools/microsoft_teams.mdx b/apps/docs/content/docs/en/tools/microsoft_teams.mdx index 0f211e440a..fdeb0de40c 100644 --- a/apps/docs/content/docs/en/tools/microsoft_teams.mdx +++ b/apps/docs/content/docs/en/tools/microsoft_teams.mdx @@ -337,8 +337,3 @@ List all members of a Microsoft Teams channel | `memberCount` | number | Total number of members | - -## Notes - -- Category: `tools` -- Type: `microsoft_teams` diff --git a/apps/docs/content/docs/en/tools/mistral_parse.mdx b/apps/docs/content/docs/en/tools/mistral_parse.mdx index 56f0597e99..e25f83099f 100644 --- a/apps/docs/content/docs/en/tools/mistral_parse.mdx +++ b/apps/docs/content/docs/en/tools/mistral_parse.mdx @@ -59,8 +59,3 @@ Parse PDF documents using Mistral OCR API | `metadata` | object | Processing metadata including jobId, fileType, pageCount, and usage info | - -## Notes - -- Category: `tools` -- Type: `mistral_parse` diff --git a/apps/docs/content/docs/en/tools/mongodb.mdx b/apps/docs/content/docs/en/tools/mongodb.mdx index 6b2a3d7bdc..dc69d59d1c 100644 --- a/apps/docs/content/docs/en/tools/mongodb.mdx +++ b/apps/docs/content/docs/en/tools/mongodb.mdx @@ -172,9 +172,28 @@ Execute MongoDB aggregation pipeline | `documents` | array | Array of documents returned from aggregation | | `documentCount` | number | Number of documents returned | +### `mongodb_introspect` +Introspect MongoDB database to list databases, collections, and indexes + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | MongoDB server hostname or IP address | +| `port` | number | Yes | MongoDB server port \(default: 27017\) | +| `database` | string | No | Database name to introspect \(optional - if not provided, lists all databases\) | +| `username` | string | No | MongoDB username | +| `password` | string | No | MongoDB password | +| `authSource` | string | No | Authentication database | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `databases` | array | Array of database names | +| `collections` | array | Array of collection info with name, type, document count, and indexes | -## Notes -- Category: `tools` -- Type: `mongodb` diff --git a/apps/docs/content/docs/en/tools/mysql.mdx b/apps/docs/content/docs/en/tools/mysql.mdx index 391dd5126b..1d9d6c1d88 100644 --- a/apps/docs/content/docs/en/tools/mysql.mdx +++ b/apps/docs/content/docs/en/tools/mysql.mdx @@ -157,9 +157,27 @@ Execute raw SQL query on MySQL database | `rows` | array | Array of rows returned from the query | | `rowCount` | number | Number of rows affected | +### `mysql_introspect` +Introspect MySQL database schema to retrieve table structures, columns, and relationships + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | MySQL server hostname or IP address | +| `port` | number | Yes | MySQL server port \(default: 3306\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `tables` | array | Array of table schemas with columns, keys, and indexes | +| `databases` | array | List of available databases on the server | -## Notes -- Category: `tools` -- Type: `mysql` diff --git a/apps/docs/content/docs/en/tools/neo4j.mdx b/apps/docs/content/docs/en/tools/neo4j.mdx index f3f9bce9d0..661ff7c574 100644 --- a/apps/docs/content/docs/en/tools/neo4j.mdx +++ b/apps/docs/content/docs/en/tools/neo4j.mdx @@ -168,9 +168,31 @@ Execute arbitrary Cypher queries on Neo4j graph database for complex operations | `recordCount` | number | Number of records returned | | `summary` | json | Execution summary with timing and counters | +### `neo4j_introspect` +Introspect a Neo4j database to discover its schema including node labels, relationship types, properties, constraints, and indexes. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | Neo4j server hostname or IP address | +| `port` | number | Yes | Neo4j server port \(default: 7687 for Bolt protocol\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | Neo4j username | +| `password` | string | Yes | Neo4j password | +| `encryption` | string | No | Connection encryption mode \(enabled, disabled\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `labels` | array | Array of node labels in the database | +| `relationshipTypes` | array | Array of relationship types in the database | +| `nodeSchemas` | array | Array of node schemas with their properties | +| `relationshipSchemas` | array | Array of relationship schemas with their properties | +| `constraints` | array | Array of database constraints | +| `indexes` | array | Array of database indexes | -## Notes -- Category: `tools` -- Type: `neo4j` diff --git a/apps/docs/content/docs/en/tools/notion.mdx b/apps/docs/content/docs/en/tools/notion.mdx index dd580a41da..418df58559 100644 --- a/apps/docs/content/docs/en/tools/notion.mdx +++ b/apps/docs/content/docs/en/tools/notion.mdx @@ -6,36 +6,10 @@ description: Manage Notion pages import { BlockInfoCard } from "@/components/ui/block-info-card" -{/* MANUAL-CONTENT-START:intro */} -[Notion](https://www.notion.so) is an all-in-one workspace that combines notes, documents, wikis, and project management tools into a single platform. It offers a flexible and customizable environment where users can create, organize, and collaborate on content in various formats. - -With Notion, you can: - -- **Create versatile content**: Build documents, wikis, databases, kanban boards, calendars, and more -- **Organize information**: Structure content hierarchically with nested pages and powerful databases -- **Collaborate seamlessly**: Share workspaces and pages with team members for real-time collaboration -- **Customize your workspace**: Design your ideal workflow with flexible templates and building blocks -- **Connect information**: Link between pages and databases to create a knowledge network -- **Access anywhere**: Use Notion across web, desktop, and mobile platforms with automatic syncing - -In Sim, the Notion integration enables your agents to interact directly with your Notion workspace programmatically. This allows for powerful automation scenarios such as knowledge management, content creation, and information retrieval. Your agents can: - -- **Read Notion pages**: Extract content and metadata from any Notion page. -- **Read Notion databases**: Retrieve database structure and information. -- **Write to pages**: Append new content to existing Notion pages. -- **Create new pages**: Generate new Notion pages under a parent page, with custom titles and content. -- **Query databases**: Search and filter database entries using advanced filter and sort criteria. -- **Search workspace**: Search across your entire Notion workspace for pages or databases matching specific queries. -- **Create new databases**: Programmatically create new databases with custom properties and structure. - -This integration bridges the gap between your AI workflows and your knowledge base, enabling seamless documentation and information management. By connecting Sim with Notion, you can automate documentation processes, maintain up-to-date information repositories, generate reports, and organize information intelligently—all through your intelligent agents. -{/* MANUAL-CONTENT-END */} - - ## Usage Instructions Integrate with Notion into the workflow. Can read page, read database, create page, create database, append content, query database, and search workspace. @@ -58,8 +32,11 @@ Read content from a Notion page | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Page content in markdown format with headers, paragraphs, lists, and todos | -| `metadata` | object | Page metadata including title, URL, and timestamps | +| `content` | string | Page content in markdown format | +| `title` | string | Page title | +| `url` | string | Page URL | +| `created_time` | string | Creation timestamp | +| `last_edited_time` | string | Last edit timestamp | ### `notion_read_database` @@ -75,8 +52,12 @@ Read database information and structure from Notion | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Database information including title, properties schema, and metadata | -| `metadata` | object | Database metadata including title, ID, URL, timestamps, and properties schema | +| `id` | string | Database ID | +| `title` | string | Database title | +| `url` | string | Database URL | +| `created_time` | string | Creation timestamp | +| `last_edited_time` | string | Last edit timestamp | +| `properties` | object | Database properties schema | ### `notion_write` @@ -93,7 +74,7 @@ Append content to a Notion page | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Success message confirming content was appended to page | +| `appended` | boolean | Whether content was successfully appended | ### `notion_create_page` @@ -111,8 +92,11 @@ Create a new page in Notion | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Success message confirming page creation | -| `metadata` | object | Page metadata including title, page ID, URL, and timestamps | +| `id` | string | Page ID | +| `title` | string | Page title | +| `url` | string | Page URL | +| `created_time` | string | Creation timestamp | +| `last_edited_time` | string | Last edit timestamp | ### `notion_query_database` @@ -131,8 +115,7 @@ Query and filter Notion database entries with advanced filtering | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Formatted list of database entries with their properties | -| `metadata` | object | Query metadata including total results count, pagination info, and raw results array | +| `results` | array | Array of Notion page objects from the database | ### `notion_search` @@ -150,8 +133,7 @@ Search across all pages and databases in Notion workspace | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Formatted list of search results including pages and databases | -| `metadata` | object | Search metadata including total results count, pagination info, and raw results array | +| `results` | array | Array of search results \(pages and databases\) | ### `notion_create_database` @@ -163,18 +145,37 @@ Create a new database in Notion with custom properties | --------- | ---- | -------- | ----------- | | `parentId` | string | Yes | ID of the parent page where the database will be created | | `title` | string | Yes | Title for the new database | -| `properties` | string | No | Database properties as JSON object \(optional, will create a default "Name" property if empty\) | +| `properties` | json | No | Database properties as JSON object \(optional, will create a default "Name" property if empty\) | #### Output | Parameter | Type | Description | | --------- | ---- | ----------- | -| `content` | string | Success message with database details and properties list | -| `metadata` | object | Database metadata including ID, title, URL, creation time, and properties schema | +| `id` | string | Database ID | +| `title` | string | Database title | +| `url` | string | Database URL | +| `created_time` | string | Creation timestamp | +| `properties` | object | Database properties schema | +### `notion_add_database_row` +Add a new row to a Notion database with specified properties + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `databaseId` | string | Yes | ID of the database to add the row to | +| `properties` | json | Yes | Row properties as JSON object matching the database schema \(e.g., \{"Name": \{"title": \[\{"text": \{"content": "Task 1"\}\}\]\}, "Status": \{"select": \{"name": "Done"\}\}\}\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | string | Page/row ID | +| `url` | string | Page/row URL | +| `title` | string | Row title | +| `created_time` | string | Creation timestamp | +| `last_edited_time` | string | Last edit timestamp | -## Notes -- Category: `tools` -- Type: `notion` diff --git a/apps/docs/content/docs/en/tools/onedrive.mdx b/apps/docs/content/docs/en/tools/onedrive.mdx index d844cf32b5..5e8f4b04be 100644 --- a/apps/docs/content/docs/en/tools/onedrive.mdx +++ b/apps/docs/content/docs/en/tools/onedrive.mdx @@ -134,8 +134,3 @@ Delete a file or folder from OneDrive | `fileId` | string | The ID of the deleted file | - -## Notes - -- Category: `tools` -- Type: `onedrive` diff --git a/apps/docs/content/docs/en/tools/openai.mdx b/apps/docs/content/docs/en/tools/openai.mdx index e6790dd4cd..b7db750a0f 100644 --- a/apps/docs/content/docs/en/tools/openai.mdx +++ b/apps/docs/content/docs/en/tools/openai.mdx @@ -56,8 +56,3 @@ Generate embeddings from text using OpenAI | `output` | object | Embeddings generation results | - -## Notes - -- Category: `tools` -- Type: `openai` diff --git a/apps/docs/content/docs/en/tools/outlook.mdx b/apps/docs/content/docs/en/tools/outlook.mdx index c54a0b24c1..82af2bcff1 100644 --- a/apps/docs/content/docs/en/tools/outlook.mdx +++ b/apps/docs/content/docs/en/tools/outlook.mdx @@ -235,8 +235,3 @@ Copy an Outlook message to another folder | `destinationFolderId` | string | ID of the destination folder | - -## Notes - -- Category: `tools` -- Type: `outlook` diff --git a/apps/docs/content/docs/en/tools/parallel_ai.mdx b/apps/docs/content/docs/en/tools/parallel_ai.mdx index c395c57005..deaf9378e6 100644 --- a/apps/docs/content/docs/en/tools/parallel_ai.mdx +++ b/apps/docs/content/docs/en/tools/parallel_ai.mdx @@ -101,8 +101,3 @@ Conduct comprehensive deep research across the web using Parallel AI. Synthesize | `basis` | array | Citations and sources with reasoning and confidence levels | - -## Notes - -- Category: `tools` -- Type: `parallel_ai` diff --git a/apps/docs/content/docs/en/tools/perplexity.mdx b/apps/docs/content/docs/en/tools/perplexity.mdx index 53c5238fc3..63a97c40dc 100644 --- a/apps/docs/content/docs/en/tools/perplexity.mdx +++ b/apps/docs/content/docs/en/tools/perplexity.mdx @@ -83,8 +83,3 @@ Get ranked search results from Perplexity | `results` | array | Array of search results | - -## Notes - -- Category: `tools` -- Type: `perplexity` diff --git a/apps/docs/content/docs/en/tools/pinecone.mdx b/apps/docs/content/docs/en/tools/pinecone.mdx index 340f48da2f..cc052ef5db 100644 --- a/apps/docs/content/docs/en/tools/pinecone.mdx +++ b/apps/docs/content/docs/en/tools/pinecone.mdx @@ -142,8 +142,3 @@ Fetch vectors by ID from a Pinecone index | `matches` | array | Fetched vectors with ID, values, metadata, and score | - -## Notes - -- Category: `tools` -- Type: `pinecone` diff --git a/apps/docs/content/docs/en/tools/pipedrive.mdx b/apps/docs/content/docs/en/tools/pipedrive.mdx index 3f66973c8b..d561b80752 100644 --- a/apps/docs/content/docs/en/tools/pipedrive.mdx +++ b/apps/docs/content/docs/en/tools/pipedrive.mdx @@ -55,7 +55,7 @@ Retrieve all deals from Pipedrive with optional filters | Parameter | Type | Description | | --------- | ---- | ----------- | | `deals` | array | Array of deal objects from Pipedrive | -| `metadata` | object | Operation metadata | +| `metadata` | object | Pagination metadata for the response | | `success` | boolean | Operation success status | ### `pipedrive_get_deal` @@ -73,7 +73,6 @@ Retrieve detailed information about a specific deal | Parameter | Type | Description | | --------- | ---- | ----------- | | `deal` | object | Deal object with full details | -| `metadata` | object | Operation metadata | | `success` | boolean | Operation success status | ### `pipedrive_create_deal` @@ -99,7 +98,6 @@ Create a new deal in Pipedrive | Parameter | Type | Description | | --------- | ---- | ----------- | | `deal` | object | The created deal object | -| `metadata` | object | Operation metadata | | `success` | boolean | Operation success status | ### `pipedrive_update_deal` @@ -122,7 +120,6 @@ Update an existing deal in Pipedrive | Parameter | Type | Description | | --------- | ---- | ----------- | | `deal` | object | The updated deal object | -| `metadata` | object | Operation metadata | | `success` | boolean | Operation success status | ### `pipedrive_get_files` @@ -143,7 +140,7 @@ Retrieve files from Pipedrive with optional filters | Parameter | Type | Description | | --------- | ---- | ----------- | | `files` | array | Array of file objects from Pipedrive | -| `metadata` | object | Operation metadata | +| `total_items` | number | Total number of files returned | | `success` | boolean | Operation success status | ### `pipedrive_get_mail_messages` @@ -162,7 +159,7 @@ Retrieve mail threads from Pipedrive mailbox | Parameter | Type | Description | | --------- | ---- | ----------- | | `messages` | array | Array of mail thread objects from Pipedrive mailbox | -| `metadata` | object | Operation metadata | +| `total_items` | number | Total number of mail threads returned | | `success` | boolean | Operation success status | ### `pipedrive_get_mail_thread` @@ -180,7 +177,7 @@ Retrieve all messages from a specific mail thread | Parameter | Type | Description | | --------- | ---- | ----------- | | `messages` | array | Array of mail message objects from the thread | -| `metadata` | object | Operation metadata including thread ID | +| `metadata` | object | Thread and pagination metadata | | `success` | boolean | Operation success status | ### `pipedrive_get_pipelines` @@ -201,7 +198,7 @@ Retrieve all pipelines from Pipedrive | Parameter | Type | Description | | --------- | ---- | ----------- | | `pipelines` | array | Array of pipeline objects from Pipedrive | -| `metadata` | object | Operation metadata | +| `total_items` | number | Total number of pipelines returned | | `success` | boolean | Operation success status | ### `pipedrive_get_pipeline_deals` @@ -222,7 +219,7 @@ Retrieve all deals in a specific pipeline | Parameter | Type | Description | | --------- | ---- | ----------- | | `deals` | array | Array of deal objects from the pipeline | -| `metadata` | object | Operation metadata including pipeline ID | +| `metadata` | object | Pipeline and pagination metadata | | `success` | boolean | Operation success status | ### `pipedrive_get_projects` @@ -243,7 +240,7 @@ Retrieve all projects or a specific project from Pipedrive | --------- | ---- | ----------- | | `projects` | array | Array of project objects \(when listing all\) | | `project` | object | Single project object \(when project_id is provided\) | -| `metadata` | object | Operation metadata | +| `total_items` | number | Total number of projects returned | | `success` | boolean | Operation success status | ### `pipedrive_create_project` @@ -264,7 +261,6 @@ Create a new project in Pipedrive | Parameter | Type | Description | | --------- | ---- | ----------- | | `project` | object | The created project object | -| `metadata` | object | Operation metadata | | `success` | boolean | Operation success status | ### `pipedrive_get_activities` @@ -287,7 +283,7 @@ Retrieve activities (tasks) from Pipedrive with optional filters | Parameter | Type | Description | | --------- | ---- | ----------- | | `activities` | array | Array of activity objects from Pipedrive | -| `metadata` | object | Operation metadata | +| `total_items` | number | Total number of activities returned | | `success` | boolean | Operation success status | ### `pipedrive_create_activity` @@ -313,7 +309,6 @@ Create a new activity (task) in Pipedrive | Parameter | Type | Description | | --------- | ---- | ----------- | | `activity` | object | The created activity object | -| `metadata` | object | Operation metadata | | `success` | boolean | Operation success status | ### `pipedrive_update_activity` @@ -337,7 +332,6 @@ Update an existing activity (task) in Pipedrive | Parameter | Type | Description | | --------- | ---- | ----------- | | `activity` | object | The updated activity object | -| `metadata` | object | Operation metadata | | `success` | boolean | Operation success status | ### `pipedrive_get_leads` @@ -361,7 +355,7 @@ Retrieve all leads or a specific lead from Pipedrive | --------- | ---- | ----------- | | `leads` | array | Array of lead objects \(when listing all\) | | `lead` | object | Single lead object \(when lead_id is provided\) | -| `metadata` | object | Operation metadata | +| `total_items` | number | Total number of leads returned | | `success` | boolean | Operation success status | ### `pipedrive_create_lead` @@ -386,7 +380,6 @@ Create a new lead in Pipedrive | Parameter | Type | Description | | --------- | ---- | ----------- | | `lead` | object | The created lead object | -| `metadata` | object | Operation metadata | | `success` | boolean | Operation success status | ### `pipedrive_update_lead` @@ -412,7 +405,6 @@ Update an existing lead in Pipedrive | Parameter | Type | Description | | --------- | ---- | ----------- | | `lead` | object | The updated lead object | -| `metadata` | object | Operation metadata | | `success` | boolean | Operation success status | ### `pipedrive_delete_lead` @@ -430,12 +422,6 @@ Delete a specific lead from Pipedrive | Parameter | Type | Description | | --------- | ---- | ----------- | | `data` | object | Deletion confirmation data | -| `metadata` | object | Operation metadata | | `success` | boolean | Operation success status | - -## Notes - -- Category: `tools` -- Type: `pipedrive` diff --git a/apps/docs/content/docs/en/tools/polymarket.mdx b/apps/docs/content/docs/en/tools/polymarket.mdx index feda033a9c..6a6d17852d 100644 --- a/apps/docs/content/docs/en/tools/polymarket.mdx +++ b/apps/docs/content/docs/en/tools/polymarket.mdx @@ -333,8 +333,3 @@ Retrieve trade history from Polymarket | `trades` | array | Array of trade objects | - -## Notes - -- Category: `tools` -- Type: `polymarket` diff --git a/apps/docs/content/docs/en/tools/postgresql.mdx b/apps/docs/content/docs/en/tools/postgresql.mdx index db75cccf34..143a77a20d 100644 --- a/apps/docs/content/docs/en/tools/postgresql.mdx +++ b/apps/docs/content/docs/en/tools/postgresql.mdx @@ -157,9 +157,28 @@ Execute raw SQL query on PostgreSQL database | `rows` | array | Array of rows returned from the query | | `rowCount` | number | Number of rows affected | +### `postgresql_introspect` +Introspect PostgreSQL database schema to retrieve table structures, columns, and relationships + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `host` | string | Yes | PostgreSQL server hostname or IP address | +| `port` | number | Yes | PostgreSQL server port \(default: 5432\) | +| `database` | string | Yes | Database name to connect to | +| `username` | string | Yes | Database username | +| `password` | string | Yes | Database password | +| `ssl` | string | No | SSL connection mode \(disabled, required, preferred\) | +| `schema` | string | No | Schema to introspect \(default: public\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `tables` | array | Array of table schemas with columns, keys, and indexes | +| `schemas` | array | List of available schemas in the database | -## Notes -- Category: `tools` -- Type: `postgresql` diff --git a/apps/docs/content/docs/en/tools/posthog.mdx b/apps/docs/content/docs/en/tools/posthog.mdx index c7acf1fdbe..062a9c026e 100644 --- a/apps/docs/content/docs/en/tools/posthog.mdx +++ b/apps/docs/content/docs/en/tools/posthog.mdx @@ -77,7 +77,7 @@ Capture multiple events at once in PostHog. Use this for bulk event ingestion to | Parameter | Type | Description | | --------- | ---- | ----------- | | `status` | string | Status message indicating whether the batch was captured successfully | -| `eventsProcessed` | number | Number of events processed in the batch | +| `events_processed` | number | Number of events processed in the batch | ### `posthog_list_persons` @@ -600,9 +600,9 @@ Evaluate feature flags for a specific user or group. This is a public endpoint t | Parameter | Type | Description | | --------- | ---- | ----------- | -| `featureFlags` | object | Feature flag evaluations \(key-value pairs where values are boolean or string variants\) | -| `featureFlagPayloads` | object | Additional payloads attached to feature flags | -| `errorsWhileComputingFlags` | boolean | Whether there were errors while computing flags | +| `feature_flags` | object | Feature flag evaluations \(key-value pairs where values are boolean or string variants\) | +| `feature_flag_payloads` | object | Additional payloads attached to feature flags | +| `errors_while_computing_flags` | boolean | Whether there were errors while computing flags | ### `posthog_list_experiments` @@ -1086,8 +1086,3 @@ Get detailed information about a specific organization by ID. Returns comprehens | `organization` | object | Detailed organization information with settings and features | - -## Notes - -- Category: `tools` -- Type: `posthog` diff --git a/apps/docs/content/docs/en/tools/qdrant.mdx b/apps/docs/content/docs/en/tools/qdrant.mdx index a7ac197dd7..4e3271363a 100644 --- a/apps/docs/content/docs/en/tools/qdrant.mdx +++ b/apps/docs/content/docs/en/tools/qdrant.mdx @@ -108,8 +108,3 @@ Fetch points by ID from a Qdrant collection | `status` | string | Status of the fetch operation | - -## Notes - -- Category: `tools` -- Type: `qdrant` diff --git a/apps/docs/content/docs/en/tools/rds.mdx b/apps/docs/content/docs/en/tools/rds.mdx index 0ecf4d131b..ec1f31869e 100644 --- a/apps/docs/content/docs/en/tools/rds.mdx +++ b/apps/docs/content/docs/en/tools/rds.mdx @@ -165,9 +165,30 @@ Execute raw SQL on Amazon RDS using the Data API | `rows` | array | Array of rows returned or affected | | `rowCount` | number | Number of rows affected | +### `rds_introspect` +Introspect Amazon RDS Aurora database schema to retrieve table structures, columns, and relationships + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `region` | string | Yes | AWS region \(e.g., us-east-1\) | +| `accessKeyId` | string | Yes | AWS access key ID | +| `secretAccessKey` | string | Yes | AWS secret access key | +| `resourceArn` | string | Yes | ARN of the Aurora DB cluster | +| `secretArn` | string | Yes | ARN of the Secrets Manager secret containing DB credentials | +| `database` | string | No | Database name \(optional\) | +| `schema` | string | No | Schema to introspect \(default: public for PostgreSQL, database name for MySQL\) | +| `engine` | string | No | Database engine \(aurora-postgresql or aurora-mysql\). Auto-detected if not provided. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `engine` | string | Detected database engine type | +| `tables` | array | Array of table schemas with columns, keys, and indexes | +| `schemas` | array | List of available schemas in the database | -## Notes -- Category: `tools` -- Type: `rds` diff --git a/apps/docs/content/docs/en/tools/reddit.mdx b/apps/docs/content/docs/en/tools/reddit.mdx index 84435db722..537c2a0bd3 100644 --- a/apps/docs/content/docs/en/tools/reddit.mdx +++ b/apps/docs/content/docs/en/tools/reddit.mdx @@ -284,8 +284,3 @@ Subscribe or unsubscribe from a subreddit | `message` | string | Success or error message | - -## Notes - -- Category: `tools` -- Type: `reddit` diff --git a/apps/docs/content/docs/en/tools/resend.mdx b/apps/docs/content/docs/en/tools/resend.mdx index e6ff725ed7..eb0bc6a32d 100644 --- a/apps/docs/content/docs/en/tools/resend.mdx +++ b/apps/docs/content/docs/en/tools/resend.mdx @@ -58,8 +58,3 @@ Send an email using your own Resend API key and from address | `body` | string | Email body content | - -## Notes - -- Category: `tools` -- Type: `resend` diff --git a/apps/docs/content/docs/en/tools/s3.mdx b/apps/docs/content/docs/en/tools/s3.mdx index 3e964c7fd1..302e5dd756 100644 --- a/apps/docs/content/docs/en/tools/s3.mdx +++ b/apps/docs/content/docs/en/tools/s3.mdx @@ -147,8 +147,3 @@ Copy an object within or between AWS S3 buckets | `metadata` | object | Copy operation metadata | - -## Notes - -- Category: `tools` -- Type: `s3` diff --git a/apps/docs/content/docs/en/tools/salesforce.mdx b/apps/docs/content/docs/en/tools/salesforce.mdx index 870c6161d2..02fe188f7d 100644 --- a/apps/docs/content/docs/en/tools/salesforce.mdx +++ b/apps/docs/content/docs/en/tools/salesforce.mdx @@ -259,7 +259,7 @@ Get lead(s) from Salesforce | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success status | +| `success` | boolean | Operation success status | | `output` | object | Lead data | ### `salesforce_create_lead` @@ -286,8 +286,8 @@ Create a new lead | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success | -| `output` | object | Created lead | +| `success` | boolean | Operation success status | +| `output` | object | Created lead data | ### `salesforce_update_lead` @@ -314,8 +314,8 @@ Update an existing lead | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success | -| `output` | object | Updated lead | +| `success` | boolean | Operation success status | +| `output` | object | Updated lead data | ### `salesforce_delete_lead` @@ -333,8 +333,8 @@ Delete a lead | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success | -| `output` | object | Deleted lead | +| `success` | boolean | Operation success status | +| `output` | object | Deleted lead data | ### `salesforce_get_opportunities` @@ -355,7 +355,7 @@ Get opportunity(ies) from Salesforce | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success | +| `success` | boolean | Operation success status | | `output` | object | Opportunity data | ### `salesforce_create_opportunity` @@ -380,8 +380,8 @@ Create a new opportunity | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success | -| `output` | object | Created opportunity | +| `success` | boolean | Operation success status | +| `output` | object | Created opportunity data | ### `salesforce_update_opportunity` @@ -406,8 +406,8 @@ Update an existing opportunity | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success | -| `output` | object | Updated opportunity | +| `success` | boolean | Operation success status | +| `output` | object | Updated opportunity data | ### `salesforce_delete_opportunity` @@ -425,8 +425,8 @@ Delete an opportunity | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success | -| `output` | object | Deleted opportunity | +| `success` | boolean | Operation success status | +| `output` | object | Deleted opportunity data | ### `salesforce_get_cases` @@ -447,7 +447,7 @@ Get case(s) from Salesforce | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success | +| `success` | boolean | Operation success status | | `output` | object | Case data | ### `salesforce_create_case` @@ -472,8 +472,8 @@ Create a new case | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success | -| `output` | object | Created case | +| `success` | boolean | Operation success status | +| `output` | object | Created case data | ### `salesforce_update_case` @@ -495,8 +495,8 @@ Update an existing case | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success | -| `output` | object | Updated case | +| `success` | boolean | Operation success status | +| `output` | object | Updated case data | ### `salesforce_delete_case` @@ -514,8 +514,8 @@ Delete a case | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success | -| `output` | object | Deleted case | +| `success` | boolean | Operation success status | +| `output` | object | Deleted case data | ### `salesforce_get_tasks` @@ -536,7 +536,7 @@ Get task(s) from Salesforce | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success | +| `success` | boolean | Operation success status | | `output` | object | Task data | ### `salesforce_create_task` @@ -561,8 +561,8 @@ Create a new task | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success | -| `output` | object | Created task | +| `success` | boolean | Operation success status | +| `output` | object | Created task data | ### `salesforce_update_task` @@ -585,8 +585,8 @@ Update an existing task | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success | -| `output` | object | Updated task | +| `success` | boolean | Operation success status | +| `output` | object | Updated task data | ### `salesforce_delete_task` @@ -604,8 +604,8 @@ Delete a task | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success | -| `output` | object | Deleted task | +| `success` | boolean | Operation success status | +| `output` | object | Deleted task data | ### `salesforce_list_reports` @@ -624,7 +624,7 @@ Get a list of reports accessible by the current user | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success status | +| `success` | boolean | Operation success status | | `output` | object | Reports data | ### `salesforce_get_report` @@ -643,7 +643,7 @@ Get metadata and describe information for a specific report | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success status | +| `success` | boolean | Operation success status | | `output` | object | Report metadata | ### `salesforce_run_report` @@ -664,7 +664,7 @@ Execute a report and retrieve the results | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success status | +| `success` | boolean | Operation success status | | `output` | object | Report results | ### `salesforce_list_report_types` @@ -682,7 +682,7 @@ Get a list of available report types | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success status | +| `success` | boolean | Operation success status | | `output` | object | Report types data | ### `salesforce_list_dashboards` @@ -701,7 +701,7 @@ Get a list of dashboards accessible by the current user | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success status | +| `success` | boolean | Operation success status | | `output` | object | Dashboards data | ### `salesforce_get_dashboard` @@ -720,7 +720,7 @@ Get details and results for a specific dashboard | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success status | +| `success` | boolean | Operation success status | | `output` | object | Dashboard data | ### `salesforce_refresh_dashboard` @@ -739,7 +739,7 @@ Refresh a dashboard to get the latest data | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success status | +| `success` | boolean | Operation success status | | `output` | object | Refreshed dashboard data | ### `salesforce_query` @@ -758,7 +758,7 @@ Execute a custom SOQL query to retrieve data from Salesforce | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success status | +| `success` | boolean | Operation success status | | `output` | object | Query results | ### `salesforce_query_more` @@ -777,7 +777,7 @@ Retrieve additional query results using the nextRecordsUrl from a previous query | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success status | +| `success` | boolean | Operation success status | | `output` | object | Query results | ### `salesforce_describe_object` @@ -796,7 +796,7 @@ Get metadata and field information for a Salesforce object | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success status | +| `success` | boolean | Operation success status | | `output` | object | Object metadata | ### `salesforce_list_objects` @@ -814,12 +814,7 @@ Get a list of all available Salesforce objects | Parameter | Type | Description | | --------- | ---- | ----------- | -| `success` | boolean | Success status | +| `success` | boolean | Operation success status | | `output` | object | Objects list | - -## Notes - -- Category: `tools` -- Type: `salesforce` diff --git a/apps/docs/content/docs/en/tools/search.mdx b/apps/docs/content/docs/en/tools/search.mdx index 795956a565..882a18714d 100644 --- a/apps/docs/content/docs/en/tools/search.mdx +++ b/apps/docs/content/docs/en/tools/search.mdx @@ -52,8 +52,3 @@ Search the web. Returns the most relevant web results, including title, link, sn | `cost` | json | Cost information \($0.01\) | - -## Notes - -- Category: `tools` -- Type: `search` diff --git a/apps/docs/content/docs/en/tools/sendgrid.mdx b/apps/docs/content/docs/en/tools/sendgrid.mdx index a55b71077b..40aec3bb0e 100644 --- a/apps/docs/content/docs/en/tools/sendgrid.mdx +++ b/apps/docs/content/docs/en/tools/sendgrid.mdx @@ -400,8 +400,3 @@ Create a new version of an email template in SendGrid | `updatedAt` | string | Last update timestamp | - -## Notes - -- Category: `tools` -- Type: `sendgrid` diff --git a/apps/docs/content/docs/en/tools/sentry.mdx b/apps/docs/content/docs/en/tools/sentry.mdx index 905ff45dae..408a61a515 100644 --- a/apps/docs/content/docs/en/tools/sentry.mdx +++ b/apps/docs/content/docs/en/tools/sentry.mdx @@ -303,8 +303,3 @@ Create a deploy record for a Sentry release in a specific environment. Deploys t | `deploy` | object | The newly created deploy record | - -## Notes - -- Category: `tools` -- Type: `sentry` diff --git a/apps/docs/content/docs/en/tools/serper.mdx b/apps/docs/content/docs/en/tools/serper.mdx index 4671f54083..44496283f5 100644 --- a/apps/docs/content/docs/en/tools/serper.mdx +++ b/apps/docs/content/docs/en/tools/serper.mdx @@ -55,8 +55,3 @@ A powerful web search tool that provides access to Google search results through | `searchResults` | array | Search results with titles, links, snippets, and type-specific metadata \(date for news, rating for places, imageUrl for images\) | - -## Notes - -- Category: `tools` -- Type: `serper` diff --git a/apps/docs/content/docs/en/tools/servicenow.mdx b/apps/docs/content/docs/en/tools/servicenow.mdx index 8a1a8b71bb..879e808125 100644 --- a/apps/docs/content/docs/en/tools/servicenow.mdx +++ b/apps/docs/content/docs/en/tools/servicenow.mdx @@ -122,8 +122,3 @@ Delete a record from a ServiceNow table | `metadata` | json | Operation metadata | - -## Notes - -- Category: `tools` -- Type: `servicenow` diff --git a/apps/docs/content/docs/en/tools/sftp.mdx b/apps/docs/content/docs/en/tools/sftp.mdx index 3795d473b0..3cb8d08767 100644 --- a/apps/docs/content/docs/en/tools/sftp.mdx +++ b/apps/docs/content/docs/en/tools/sftp.mdx @@ -181,8 +181,3 @@ Create a directory on a remote SFTP server | `message` | string | Operation status message | - -## Notes - -- Category: `tools` -- Type: `sftp` diff --git a/apps/docs/content/docs/en/tools/sharepoint.mdx b/apps/docs/content/docs/en/tools/sharepoint.mdx index 70a24feed0..9b2aac048c 100644 --- a/apps/docs/content/docs/en/tools/sharepoint.mdx +++ b/apps/docs/content/docs/en/tools/sharepoint.mdx @@ -189,8 +189,3 @@ Upload files to a SharePoint document library | `uploadedFiles` | array | Array of uploaded file objects | - -## Notes - -- Category: `tools` -- Type: `sharepoint` diff --git a/apps/docs/content/docs/en/tools/shopify.mdx b/apps/docs/content/docs/en/tools/shopify.mdx index 327e935aee..2247af0e1b 100644 --- a/apps/docs/content/docs/en/tools/shopify.mdx +++ b/apps/docs/content/docs/en/tools/shopify.mdx @@ -442,8 +442,3 @@ Get a specific collection by ID, including its products. Use this to retrieve pr | `collection` | object | The collection details including its products | - -## Notes - -- Category: `tools` -- Type: `shopify` diff --git a/apps/docs/content/docs/en/tools/slack.mdx b/apps/docs/content/docs/en/tools/slack.mdx index 7eb927c811..8c806d4de8 100644 --- a/apps/docs/content/docs/en/tools/slack.mdx +++ b/apps/docs/content/docs/en/tools/slack.mdx @@ -283,8 +283,3 @@ Add an emoji reaction to a Slack message | `metadata` | object | Reaction metadata | - -## Notes - -- Category: `tools` -- Type: `slack` diff --git a/apps/docs/content/docs/en/tools/smtp.mdx b/apps/docs/content/docs/en/tools/smtp.mdx index 01075b7494..098deb7b2c 100644 --- a/apps/docs/content/docs/en/tools/smtp.mdx +++ b/apps/docs/content/docs/en/tools/smtp.mdx @@ -71,8 +71,3 @@ Send emails via SMTP server | `error` | string | Error message if sending failed | - -## Notes - -- Category: `tools` -- Type: `smtp` diff --git a/apps/docs/content/docs/en/tools/spotify.mdx b/apps/docs/content/docs/en/tools/spotify.mdx index 1b8cd3b0c7..04f823d812 100644 --- a/apps/docs/content/docs/en/tools/spotify.mdx +++ b/apps/docs/content/docs/en/tools/spotify.mdx @@ -1449,8 +1449,3 @@ Transfer playback to a different device. | `success` | boolean | Whether transfer was successful | - -## Notes - -- Category: `tools` -- Type: `spotify` diff --git a/apps/docs/content/docs/en/tools/sqs.mdx b/apps/docs/content/docs/en/tools/sqs.mdx index 0fe60e5425..36820cf018 100644 --- a/apps/docs/content/docs/en/tools/sqs.mdx +++ b/apps/docs/content/docs/en/tools/sqs.mdx @@ -61,8 +61,3 @@ Send a message to an Amazon SQS queue | `id` | string | Message ID | - -## Notes - -- Category: `tools` -- Type: `sqs` diff --git a/apps/docs/content/docs/en/tools/ssh.mdx b/apps/docs/content/docs/en/tools/ssh.mdx index 663ca7ac31..1f083937d2 100644 --- a/apps/docs/content/docs/en/tools/ssh.mdx +++ b/apps/docs/content/docs/en/tools/ssh.mdx @@ -392,8 +392,3 @@ Write or append content to a remote file | `message` | string | Operation status message | - -## Notes - -- Category: `tools` -- Type: `ssh` diff --git a/apps/docs/content/docs/en/tools/stagehand.mdx b/apps/docs/content/docs/en/tools/stagehand.mdx index 448a73b6c1..09585de157 100644 --- a/apps/docs/content/docs/en/tools/stagehand.mdx +++ b/apps/docs/content/docs/en/tools/stagehand.mdx @@ -81,8 +81,3 @@ Run an autonomous web agent to complete tasks and extract structured data | `agentResult` | object | Result from the Stagehand agent execution | - -## Notes - -- Category: `tools` -- Type: `stagehand` diff --git a/apps/docs/content/docs/en/tools/stripe.mdx b/apps/docs/content/docs/en/tools/stripe.mdx index 19c251647c..dad24ade66 100644 --- a/apps/docs/content/docs/en/tools/stripe.mdx +++ b/apps/docs/content/docs/en/tools/stripe.mdx @@ -282,7 +282,6 @@ Permanently delete a customer | --------- | ---- | ----------- | | `deleted` | boolean | Whether the customer was deleted | | `id` | string | The ID of the deleted customer | -| `metadata` | json | Deletion metadata | ### `stripe_list_customers` @@ -541,7 +540,6 @@ Permanently delete a draft invoice | --------- | ---- | ----------- | | `deleted` | boolean | Whether the invoice was deleted | | `id` | string | The ID of the deleted invoice | -| `metadata` | json | Deletion metadata | ### `stripe_finalize_invoice` @@ -856,7 +854,6 @@ Permanently delete a product | --------- | ---- | ----------- | | `deleted` | boolean | Whether the product was deleted | | `id` | string | The ID of the deleted product | -| `metadata` | json | Deletion metadata | ### `stripe_list_products` @@ -1035,8 +1032,3 @@ List all Events | `metadata` | json | List metadata including count and has_more | - -## Notes - -- Category: `tools` -- Type: `stripe` diff --git a/apps/docs/content/docs/en/tools/stt.mdx b/apps/docs/content/docs/en/tools/stt.mdx index d76afbbd31..db378e0db4 100644 --- a/apps/docs/content/docs/en/tools/stt.mdx +++ b/apps/docs/content/docs/en/tools/stt.mdx @@ -195,8 +195,3 @@ Transcribe audio to text using Google Gemini with multimodal capabilities | `confidence` | number | Overall confidence score | - -## Notes - -- Category: `tools` -- Type: `stt` diff --git a/apps/docs/content/docs/en/tools/supabase.mdx b/apps/docs/content/docs/en/tools/supabase.mdx index a6e285c9a8..5249cdbdbf 100644 --- a/apps/docs/content/docs/en/tools/supabase.mdx +++ b/apps/docs/content/docs/en/tools/supabase.mdx @@ -261,6 +261,25 @@ Call a PostgreSQL function in Supabase | `message` | string | Operation status message | | `results` | json | Result returned from the function | +### `supabase_introspect` + +Introspect Supabase database schema to get table structures, columns, and relationships + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `projectId` | string | Yes | Your Supabase project ID \(e.g., jdrkgepadsdopsntdlom\) | +| `schema` | string | No | Database schema to introspect \(defaults to all user schemas, commonly "public"\) | +| `apiKey` | string | Yes | Your Supabase service role secret key | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `message` | string | Operation status message | +| `tables` | array | Array of table schemas with columns, keys, and indexes | + ### `supabase_storage_upload` Upload a file to a Supabase storage bucket @@ -495,8 +514,3 @@ Create a temporary signed URL for a file in a Supabase storage bucket | `signedUrl` | string | The temporary signed URL to access the file | - -## Notes - -- Category: `tools` -- Type: `supabase` diff --git a/apps/docs/content/docs/en/tools/tavily.mdx b/apps/docs/content/docs/en/tools/tavily.mdx index 7c4ec1110b..dbf8bb1b77 100644 --- a/apps/docs/content/docs/en/tools/tavily.mdx +++ b/apps/docs/content/docs/en/tools/tavily.mdx @@ -147,8 +147,3 @@ Discover and visualize website structure using Tavily | `results` | array | Discovered URL | - -## Notes - -- Category: `tools` -- Type: `tavily` diff --git a/apps/docs/content/docs/en/tools/telegram.mdx b/apps/docs/content/docs/en/tools/telegram.mdx index eb8ba336b0..9955078ce6 100644 --- a/apps/docs/content/docs/en/tools/telegram.mdx +++ b/apps/docs/content/docs/en/tools/telegram.mdx @@ -197,8 +197,3 @@ Send documents (PDF, ZIP, DOC, etc.) to Telegram channels or users through the T | `data` | object | Telegram message data including document | - -## Notes - -- Category: `tools` -- Type: `telegram` diff --git a/apps/docs/content/docs/en/tools/thinking.mdx b/apps/docs/content/docs/en/tools/thinking.mdx deleted file mode 100644 index 917f510327..0000000000 --- a/apps/docs/content/docs/en/tools/thinking.mdx +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: Thinking -description: Forces model to outline its thought process. ---- - -import { BlockInfoCard } from "@/components/ui/block-info-card" - - - -{/* MANUAL-CONTENT-START:intro */} -The Thinking tool encourages AI models to engage in explicit reasoning before responding to complex queries. By providing a dedicated space for step-by-step analysis, this tool helps models break down problems, consider multiple perspectives, and arrive at more thoughtful conclusions. - -Research has shown that prompting language models to "think step by step" can significantly improve their reasoning capabilities. According to [Anthropic's research on Claude's Think tool](https://www.anthropic.com/engineering/claude-think-tool), when models are given space to work through their reasoning explicitly, they demonstrate: - -- **Improved problem-solving**: Breaking complex problems into manageable steps -- **Enhanced accuracy**: Reducing errors by carefully working through each component of a problem -- **Greater transparency**: Making the model's reasoning process visible and auditable -- **More nuanced responses**: Considering multiple angles before arriving at conclusions - -In Sim, the Thinking tool creates a structured opportunity for your agents to engage in this kind of deliberate reasoning. By incorporating thinking steps into your workflows, you can help your agents tackle complex tasks more effectively, avoid common reasoning pitfalls, and produce higher-quality outputs. This is particularly valuable for tasks involving multi-step reasoning, complex decision-making, or situations where accuracy is critical. -{/* MANUAL-CONTENT-END */} - - -## Usage Instructions - -Adds a step where the model explicitly outlines its thought process before proceeding. This can improve reasoning quality by encouraging step-by-step analysis. - - - -## Tools - -### `thinking_tool` - -Processes a provided thought/instruction, making it available for subsequent steps. - -#### Input - -| Parameter | Type | Required | Description | -| --------- | ---- | -------- | ----------- | -| `thought` | string | Yes | Your internal reasoning, analysis, or thought process. Use this to think through the problem step by step before responding. | - -#### Output - -| Parameter | Type | Description | -| --------- | ---- | ----------- | -| `acknowledgedThought` | string | The thought that was processed and acknowledged | - - - -## Notes - -- Category: `tools` -- Type: `thinking` diff --git a/apps/docs/content/docs/en/tools/translate.mdx b/apps/docs/content/docs/en/tools/translate.mdx index d28443a91b..3c3f7dc75d 100644 --- a/apps/docs/content/docs/en/tools/translate.mdx +++ b/apps/docs/content/docs/en/tools/translate.mdx @@ -66,8 +66,3 @@ Send a chat completion request to any supported LLM provider | `tokens` | object | Token usage information | - -## Notes - -- Category: `tools` -- Type: `translate` diff --git a/apps/docs/content/docs/en/tools/trello.mdx b/apps/docs/content/docs/en/tools/trello.mdx index ac924c5f04..91d76f752d 100644 --- a/apps/docs/content/docs/en/tools/trello.mdx +++ b/apps/docs/content/docs/en/tools/trello.mdx @@ -151,8 +151,3 @@ Add a comment to a Trello card | `comment` | object | The created comment object with id, text, date, and member creator | - -## Notes - -- Category: `tools` -- Type: `trello` diff --git a/apps/docs/content/docs/en/tools/tts.mdx b/apps/docs/content/docs/en/tools/tts.mdx index 99b380939a..f4c1ea4ccb 100644 --- a/apps/docs/content/docs/en/tools/tts.mdx +++ b/apps/docs/content/docs/en/tools/tts.mdx @@ -254,8 +254,3 @@ Convert text to speech using PlayHT (voice cloning) | `provider` | string | TTS provider used | - -## Notes - -- Category: `tools` -- Type: `tts` diff --git a/apps/docs/content/docs/en/tools/twilio_sms.mdx b/apps/docs/content/docs/en/tools/twilio_sms.mdx index e9170e4694..2b7f6f82ec 100644 --- a/apps/docs/content/docs/en/tools/twilio_sms.mdx +++ b/apps/docs/content/docs/en/tools/twilio_sms.mdx @@ -58,8 +58,3 @@ Send text messages to single or multiple recipients using the Twilio API. | `toNumber` | string | Phone number message was sent to | - -## Notes - -- Category: `tools` -- Type: `twilio_sms` diff --git a/apps/docs/content/docs/en/tools/twilio_voice.mdx b/apps/docs/content/docs/en/tools/twilio_voice.mdx index 34a44b5a76..892e0e7a2b 100644 --- a/apps/docs/content/docs/en/tools/twilio_voice.mdx +++ b/apps/docs/content/docs/en/tools/twilio_voice.mdx @@ -132,8 +132,3 @@ Retrieve call recording information and transcription (if enabled via TwiML). | `error` | string | Error message if retrieval failed | - -## Notes - -- Category: `tools` -- Type: `twilio_voice` diff --git a/apps/docs/content/docs/en/tools/typeform.mdx b/apps/docs/content/docs/en/tools/typeform.mdx index bd1d055905..7a2d918e23 100644 --- a/apps/docs/content/docs/en/tools/typeform.mdx +++ b/apps/docs/content/docs/en/tools/typeform.mdx @@ -214,8 +214,3 @@ Permanently delete a form and all its responses | `message` | string | Deletion confirmation message | - -## Notes - -- Category: `tools` -- Type: `typeform` diff --git a/apps/docs/content/docs/en/tools/video_generator.mdx b/apps/docs/content/docs/en/tools/video_generator.mdx index a56b59108a..7930ad7b2f 100644 --- a/apps/docs/content/docs/en/tools/video_generator.mdx +++ b/apps/docs/content/docs/en/tools/video_generator.mdx @@ -190,8 +190,3 @@ Generate videos using Fal.ai platform with access to multiple models including V | `jobId` | string | Job ID | - -## Notes - -- Category: `tools` -- Type: `video_generator` diff --git a/apps/docs/content/docs/en/tools/vision.mdx b/apps/docs/content/docs/en/tools/vision.mdx index 06c5f9155f..ade454f419 100644 --- a/apps/docs/content/docs/en/tools/vision.mdx +++ b/apps/docs/content/docs/en/tools/vision.mdx @@ -57,8 +57,3 @@ Process and analyze images using advanced vision models. Capable of understandin | `usage` | object | Detailed token usage breakdown | - -## Notes - -- Category: `tools` -- Type: `vision` diff --git a/apps/docs/content/docs/en/tools/wealthbox.mdx b/apps/docs/content/docs/en/tools/wealthbox.mdx index 6bfd998cd7..b1e5514985 100644 --- a/apps/docs/content/docs/en/tools/wealthbox.mdx +++ b/apps/docs/content/docs/en/tools/wealthbox.mdx @@ -144,8 +144,3 @@ Create or update a Wealthbox task | `output` | object | Created or updated task data and metadata | - -## Notes - -- Category: `tools` -- Type: `wealthbox` diff --git a/apps/docs/content/docs/en/tools/webflow.mdx b/apps/docs/content/docs/en/tools/webflow.mdx index 168f3eaf43..6e53139d05 100644 --- a/apps/docs/content/docs/en/tools/webflow.mdx +++ b/apps/docs/content/docs/en/tools/webflow.mdx @@ -132,8 +132,3 @@ Delete an item from a Webflow CMS collection | `metadata` | json | Metadata about the deletion | - -## Notes - -- Category: `tools` -- Type: `webflow` diff --git a/apps/docs/content/docs/en/tools/whatsapp.mdx b/apps/docs/content/docs/en/tools/whatsapp.mdx index bffed543e8..2c04af3ef7 100644 --- a/apps/docs/content/docs/en/tools/whatsapp.mdx +++ b/apps/docs/content/docs/en/tools/whatsapp.mdx @@ -55,8 +55,3 @@ Send WhatsApp messages | `timestamp` | string | Message send timestamp | - -## Notes - -- Category: `tools` -- Type: `whatsapp` diff --git a/apps/docs/content/docs/en/tools/wikipedia.mdx b/apps/docs/content/docs/en/tools/wikipedia.mdx index a22a2ca0f7..8a02918d9e 100644 --- a/apps/docs/content/docs/en/tools/wikipedia.mdx +++ b/apps/docs/content/docs/en/tools/wikipedia.mdx @@ -97,8 +97,3 @@ Get a random Wikipedia page. | `randomPage` | object | Random Wikipedia page data | - -## Notes - -- Category: `tools` -- Type: `wikipedia` diff --git a/apps/docs/content/docs/en/tools/wordpress.mdx b/apps/docs/content/docs/en/tools/wordpress.mdx index 94840bbe58..31592b8100 100644 --- a/apps/docs/content/docs/en/tools/wordpress.mdx +++ b/apps/docs/content/docs/en/tools/wordpress.mdx @@ -564,8 +564,3 @@ Search across all content types in WordPress.com (posts, pages, media) | `results` | array | Search results | - -## Notes - -- Category: `tools` -- Type: `wordpress` diff --git a/apps/docs/content/docs/en/tools/x.mdx b/apps/docs/content/docs/en/tools/x.mdx index 7b4f5ff0ae..bc5a98affd 100644 --- a/apps/docs/content/docs/en/tools/x.mdx +++ b/apps/docs/content/docs/en/tools/x.mdx @@ -108,8 +108,3 @@ Get user profile information | `user` | object | X user profile information | - -## Notes - -- Category: `tools` -- Type: `x` diff --git a/apps/docs/content/docs/en/tools/youtube.mdx b/apps/docs/content/docs/en/tools/youtube.mdx index 1c087fb640..dcc057caa7 100644 --- a/apps/docs/content/docs/en/tools/youtube.mdx +++ b/apps/docs/content/docs/en/tools/youtube.mdx @@ -194,8 +194,3 @@ Get comments from a YouTube video. | `items` | array | Array of comments from the video | - -## Notes - -- Category: `tools` -- Type: `youtube` diff --git a/apps/docs/content/docs/en/tools/zendesk.mdx b/apps/docs/content/docs/en/tools/zendesk.mdx index 3b826c0112..54c794d3f0 100644 --- a/apps/docs/content/docs/en/tools/zendesk.mdx +++ b/apps/docs/content/docs/en/tools/zendesk.mdx @@ -78,7 +78,6 @@ Retrieve a list of tickets from Zendesk with optional filtering | --------- | ---- | ----------- | | `tickets` | array | Array of ticket objects | | `paging` | object | Pagination information | -| `metadata` | object | Operation metadata | ### `zendesk_get_ticket` @@ -98,7 +97,7 @@ Get a single ticket by ID from Zendesk | Parameter | Type | Description | | --------- | ---- | ----------- | | `ticket` | object | Ticket object | -| `metadata` | object | Operation metadata | +| `ticket_id` | number | The ticket ID | ### `zendesk_create_ticket` @@ -127,7 +126,7 @@ Create a new ticket in Zendesk with support for custom fields | Parameter | Type | Description | | --------- | ---- | ----------- | | `ticket` | object | Created ticket object | -| `metadata` | object | Operation metadata | +| `ticket_id` | number | The created ticket ID | ### `zendesk_create_tickets_bulk` @@ -146,8 +145,8 @@ Create multiple tickets in Zendesk at once (max 100) | Parameter | Type | Description | | --------- | ---- | ----------- | -| `jobStatus` | object | Job status object | -| `metadata` | object | Operation metadata | +| `job_status` | object | Job status object | +| `job_id` | string | The bulk operation job ID | ### `zendesk_update_ticket` @@ -176,7 +175,7 @@ Update an existing ticket in Zendesk with support for custom fields | Parameter | Type | Description | | --------- | ---- | ----------- | | `ticket` | object | Updated ticket object | -| `metadata` | object | Operation metadata | +| `ticket_id` | number | The updated ticket ID | ### `zendesk_update_tickets_bulk` @@ -200,8 +199,8 @@ Update multiple tickets in Zendesk at once (max 100) | Parameter | Type | Description | | --------- | ---- | ----------- | -| `jobStatus` | object | Job status object | -| `metadata` | object | Operation metadata | +| `job_status` | object | Job status object | +| `job_id` | string | The bulk operation job ID | ### `zendesk_delete_ticket` @@ -221,7 +220,7 @@ Delete a ticket from Zendesk | Parameter | Type | Description | | --------- | ---- | ----------- | | `deleted` | boolean | Deletion success | -| `metadata` | object | Operation metadata | +| `ticket_id` | string | The deleted ticket ID | ### `zendesk_merge_tickets` @@ -242,8 +241,9 @@ Merge multiple tickets into a target ticket | Parameter | Type | Description | | --------- | ---- | ----------- | -| `jobStatus` | object | Job status object | -| `metadata` | object | Operation metadata | +| `job_status` | object | Job status object | +| `job_id` | string | The merge job ID | +| `target_ticket_id` | string | The target ticket ID that tickets were merged into | ### `zendesk_get_users` @@ -267,7 +267,6 @@ Retrieve a list of users from Zendesk with optional filtering | --------- | ---- | ----------- | | `users` | array | Array of user objects | | `paging` | object | Pagination information | -| `metadata` | object | Operation metadata | ### `zendesk_get_user` @@ -287,7 +286,7 @@ Get a single user by ID from Zendesk | Parameter | Type | Description | | --------- | ---- | ----------- | | `user` | object | User object | -| `metadata` | object | Operation metadata | +| `user_id` | number | The user ID | ### `zendesk_get_current_user` @@ -306,7 +305,7 @@ Get the currently authenticated user from Zendesk | Parameter | Type | Description | | --------- | ---- | ----------- | | `user` | object | Current user object | -| `metadata` | object | Operation metadata | +| `user_id` | number | The current user ID | ### `zendesk_search_users` @@ -330,7 +329,6 @@ Search for users in Zendesk using a query string | --------- | ---- | ----------- | | `users` | array | Array of user objects | | `paging` | object | Pagination information | -| `metadata` | object | Operation metadata | ### `zendesk_create_user` @@ -357,7 +355,7 @@ Create a new user in Zendesk | Parameter | Type | Description | | --------- | ---- | ----------- | | `user` | object | Created user object | -| `metadata` | object | Operation metadata | +| `user_id` | number | The created user ID | ### `zendesk_create_users_bulk` @@ -376,8 +374,8 @@ Create multiple users in Zendesk using bulk import | Parameter | Type | Description | | --------- | ---- | ----------- | -| `jobStatus` | object | Job status object | -| `metadata` | object | Operation metadata | +| `job_status` | object | Job status object | +| `job_id` | string | The bulk operation job ID | ### `zendesk_update_user` @@ -404,8 +402,8 @@ Update an existing user in Zendesk | Parameter | Type | Description | | --------- | ---- | ----------- | -| `user` | object | Updated user object | -| `metadata` | object | Operation metadata | +| `user` | json | Updated user object | +| `user_id` | number | The updated user ID | ### `zendesk_update_users_bulk` @@ -424,8 +422,8 @@ Update multiple users in Zendesk using bulk update | Parameter | Type | Description | | --------- | ---- | ----------- | -| `jobStatus` | object | Job status object | -| `metadata` | object | Operation metadata | +| `job_status` | object | Job status object | +| `job_id` | string | The bulk operation job ID | ### `zendesk_delete_user` @@ -445,7 +443,7 @@ Delete a user from Zendesk | Parameter | Type | Description | | --------- | ---- | ----------- | | `deleted` | boolean | Deletion success | -| `metadata` | object | Operation metadata | +| `user_id` | string | The deleted user ID | ### `zendesk_get_organizations` @@ -467,7 +465,6 @@ Retrieve a list of organizations from Zendesk | --------- | ---- | ----------- | | `organizations` | array | Array of organization objects | | `paging` | object | Pagination information | -| `metadata` | object | Operation metadata | ### `zendesk_get_organization` @@ -486,8 +483,8 @@ Get a single organization by ID from Zendesk | Parameter | Type | Description | | --------- | ---- | ----------- | -| `organization` | object | Organization object | -| `metadata` | object | Operation metadata | +| `organization` | json | Organization object | +| `organization_id` | number | The organization ID | ### `zendesk_autocomplete_organizations` @@ -510,7 +507,6 @@ Autocomplete organizations in Zendesk by name prefix (for name matching/autocomp | --------- | ---- | ----------- | | `organizations` | array | Array of organization objects | | `paging` | object | Pagination information | -| `metadata` | object | Operation metadata | ### `zendesk_create_organization` @@ -534,8 +530,8 @@ Create a new organization in Zendesk | Parameter | Type | Description | | --------- | ---- | ----------- | -| `organization` | object | Created organization object | -| `metadata` | object | Operation metadata | +| `organization` | json | Created organization object | +| `organization_id` | number | The created organization ID | ### `zendesk_create_organizations_bulk` @@ -554,8 +550,8 @@ Create multiple organizations in Zendesk using bulk import | Parameter | Type | Description | | --------- | ---- | ----------- | -| `jobStatus` | object | Job status object | -| `metadata` | object | Operation metadata | +| `job_status` | object | Job status object | +| `job_id` | string | The bulk operation job ID | ### `zendesk_update_organization` @@ -580,8 +576,8 @@ Update an existing organization in Zendesk | Parameter | Type | Description | | --------- | ---- | ----------- | -| `organization` | object | Updated organization object | -| `metadata` | object | Operation metadata | +| `organization` | json | Updated organization object | +| `organization_id` | number | The updated organization ID | ### `zendesk_delete_organization` @@ -600,8 +596,8 @@ Delete an organization from Zendesk | Parameter | Type | Description | | --------- | ---- | ----------- | -| `deleted` | boolean | Deletion success | -| `metadata` | object | Operation metadata | +| `deleted` | boolean | Whether the organization was successfully deleted | +| `organization_id` | string | The deleted organization ID | ### `zendesk_search` @@ -626,7 +622,6 @@ Unified search across tickets, users, and organizations in Zendesk | --------- | ---- | ----------- | | `results` | array | Array of result objects | | `paging` | object | Pagination information | -| `metadata` | object | Operation metadata | ### `zendesk_search_count` @@ -646,11 +641,5 @@ Count the number of search results matching a query in Zendesk | Parameter | Type | Description | | --------- | ---- | ----------- | | `count` | number | Number of matching results | -| `metadata` | object | Operation metadata | - - -## Notes -- Category: `tools` -- Type: `zendesk` diff --git a/apps/docs/content/docs/en/tools/zep.mdx b/apps/docs/content/docs/en/tools/zep.mdx index 4b53ed8f4a..675dc9d6de 100644 --- a/apps/docs/content/docs/en/tools/zep.mdx +++ b/apps/docs/content/docs/en/tools/zep.mdx @@ -211,8 +211,3 @@ List all conversation threads for a specific user | `totalCount` | number | Total number of threads returned | - -## Notes - -- Category: `tools` -- Type: `zep` diff --git a/apps/docs/content/docs/en/tools/zoom.mdx b/apps/docs/content/docs/en/tools/zoom.mdx index a1bc1b9eab..2185abf39a 100644 --- a/apps/docs/content/docs/en/tools/zoom.mdx +++ b/apps/docs/content/docs/en/tools/zoom.mdx @@ -250,8 +250,3 @@ List participants from a past Zoom meeting | `pageInfo` | object | Pagination information | - -## Notes - -- Category: `tools` -- Type: `zoom` diff --git a/apps/docs/content/docs/es/enterprise/index.mdx b/apps/docs/content/docs/es/enterprise/index.mdx index 48c3f59241..2137bb5366 100644 --- a/apps/docs/content/docs/es/enterprise/index.mdx +++ b/apps/docs/content/docs/es/enterprise/index.mdx @@ -70,6 +70,7 @@ Para implementaciones self-hosted, las funciones enterprise se pueden activar me |----------|-------------| | `SSO_ENABLED`, `NEXT_PUBLIC_SSO_ENABLED` | Inicio de sesión único con SAML/OIDC | | `CREDENTIAL_SETS_ENABLED`, `NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED` | Grupos de sondeo para activadores de correo electrónico | +| `DISABLE_INVITATIONS`, `NEXT_PUBLIC_DISABLE_INVITATIONS` | Desactivar globalmente invitaciones a espacios de trabajo/organizaciones | BYOK solo está disponible en Sim Studio alojado. Las implementaciones autoalojadas configuran las claves de proveedor de IA directamente a través de variables de entorno. diff --git a/apps/docs/content/docs/fr/enterprise/index.mdx b/apps/docs/content/docs/fr/enterprise/index.mdx index 46efa6b6ac..c3eb71122e 100644 --- a/apps/docs/content/docs/fr/enterprise/index.mdx +++ b/apps/docs/content/docs/fr/enterprise/index.mdx @@ -70,6 +70,7 @@ Pour les déploiements auto-hébergés, les fonctionnalités entreprise peuvent |----------|-------------| | `SSO_ENABLED`, `NEXT_PUBLIC_SSO_ENABLED` | Authentification unique avec SAML/OIDC | | `CREDENTIAL_SETS_ENABLED`, `NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED` | Groupes de sondage pour les déclencheurs d'e-mail | +| `DISABLE_INVITATIONS`, `NEXT_PUBLIC_DISABLE_INVITATIONS` | Désactiver globalement les invitations aux espaces de travail/organisations | BYOK est uniquement disponible sur Sim Studio hébergé. Les déploiements auto-hébergés configurent les clés de fournisseur d'IA directement via les variables d'environnement. diff --git a/apps/docs/content/docs/ja/enterprise/index.mdx b/apps/docs/content/docs/ja/enterprise/index.mdx index a08a5a51d5..1fc8dd6c5a 100644 --- a/apps/docs/content/docs/ja/enterprise/index.mdx +++ b/apps/docs/content/docs/ja/enterprise/index.mdx @@ -69,6 +69,7 @@ Sim Studioのホストキーの代わりに、AIモデルプロバイダー用 |----------|-------------| | `SSO_ENABLED`、`NEXT_PUBLIC_SSO_ENABLED` | SAML/OIDCによるシングルサインオン | | `CREDENTIAL_SETS_ENABLED`、`NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED` | メールトリガー用のポーリンググループ | +| `DISABLE_INVITATIONS`、`NEXT_PUBLIC_DISABLE_INVITATIONS` | ワークスペース/組織への招待をグローバルに無効化 | BYOKはホスト型Sim Studioでのみ利用可能です。セルフホスト型デプロイメントでは、環境変数を介してAIプロバイダーキーを直接設定します。 diff --git a/apps/docs/content/docs/zh/enterprise/index.mdx b/apps/docs/content/docs/zh/enterprise/index.mdx index 045a14ea4d..edef31636b 100644 --- a/apps/docs/content/docs/zh/enterprise/index.mdx +++ b/apps/docs/content/docs/zh/enterprise/index.mdx @@ -69,6 +69,7 @@ Sim Studio 企业版为需要更高安全性、合规性和管理能力的组织 |----------|-------------| | `SSO_ENABLED`,`NEXT_PUBLIC_SSO_ENABLED` | 使用 SAML/OIDC 的单点登录 | | `CREDENTIAL_SETS_ENABLED`,`NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED` | 用于邮件触发器的轮询组 | +| `DISABLE_INVITATIONS`,`NEXT_PUBLIC_DISABLE_INVITATIONS` | 全局禁用工作区/组织邀请 | BYOK 仅适用于托管版 Sim Studio。自托管部署需通过环境变量直接配置 AI 提供商密钥。 diff --git a/apps/sim/app/api/copilot/chat/route.ts b/apps/sim/app/api/copilot/chat/route.ts index 8d92bdbb7b..1c0067e835 100644 --- a/apps/sim/app/api/copilot/chat/route.ts +++ b/apps/sim/app/api/copilot/chat/route.ts @@ -21,6 +21,7 @@ import { env } from '@/lib/core/config/env' import { CopilotFiles } from '@/lib/uploads' import { createFileContent } from '@/lib/uploads/utils/file-utils' import { tools } from '@/tools/registry' +import { getLatestVersionTools, stripVersionSuffix } from '@/tools/utils' const logger = createLogger('CopilotChatAPI') @@ -411,11 +412,14 @@ export async function POST(req: NextRequest) { try { const { createUserToolSchema } = await import('@/tools/params') - integrationTools = Object.entries(tools).map(([toolId, toolConfig]) => { + const latestTools = getLatestVersionTools(tools) + + integrationTools = Object.entries(latestTools).map(([toolId, toolConfig]) => { const userSchema = createUserToolSchema(toolConfig) + const strippedName = stripVersionSuffix(toolId) return { - name: toolId, - description: toolConfig.description || toolConfig.name || toolId, + name: strippedName, + description: toolConfig.description || toolConfig.name || strippedName, input_schema: userSchema, defer_loading: true, // Anthropic Advanced Tool Use ...(toolConfig.oauth?.required && { diff --git a/apps/sim/app/api/copilot/execute-tool/route.ts b/apps/sim/app/api/copilot/execute-tool/route.ts index adb88071e3..b737b196de 100644 --- a/apps/sim/app/api/copilot/execute-tool/route.ts +++ b/apps/sim/app/api/copilot/execute-tool/route.ts @@ -17,7 +17,7 @@ import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils' import { REFERENCE } from '@/executor/constants' import { createEnvVarPattern } from '@/executor/utils/reference-validation' import { executeTool } from '@/tools' -import { getTool } from '@/tools/utils' +import { getTool, resolveToolId } from '@/tools/utils' const logger = createLogger('CopilotExecuteToolAPI') @@ -86,15 +86,17 @@ export async function POST(req: NextRequest) { const { toolCallId, toolName, arguments: toolArgs, workflowId } = ExecuteToolSchema.parse(body) + const resolvedToolName = resolveToolId(toolName) + logger.info(`[${tracker.requestId}] Executing tool`, { toolCallId, toolName, + resolvedToolName, workflowId, hasArgs: Object.keys(toolArgs).length > 0, }) - // Get tool config from registry - const toolConfig = getTool(toolName) + const toolConfig = getTool(resolvedToolName) if (!toolConfig) { // Find similar tool names to help debug const { tools: allTools } = await import('@/tools/registry') @@ -252,7 +254,7 @@ export async function POST(req: NextRequest) { hasApiKey: !!executionParams.apiKey, }) - const result = await executeTool(toolName, executionParams, true) + const result = await executeTool(resolvedToolName, executionParams, true) logger.info(`[${tracker.requestId}] Tool execution complete`, { toolName, diff --git a/apps/sim/app/api/organizations/[id]/invitations/route.ts b/apps/sim/app/api/organizations/[id]/invitations/route.ts index f72705e90e..124d709574 100644 --- a/apps/sim/app/api/organizations/[id]/invitations/route.ts +++ b/apps/sim/app/api/organizations/[id]/invitations/route.ts @@ -26,6 +26,10 @@ import { getBaseUrl } from '@/lib/core/utils/urls' import { sendEmail } from '@/lib/messaging/email/mailer' import { quickValidateEmail } from '@/lib/messaging/email/validation' import { hasWorkspaceAdminAccess } from '@/lib/workspaces/permissions/utils' +import { + InvitationsNotAllowedError, + validateInvitationsAllowed, +} from '@/executor/utils/permission-check' const logger = createLogger('OrganizationInvitations') @@ -116,6 +120,8 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } + await validateInvitationsAllowed(session.user.id) + const { id: organizationId } = await params const url = new URL(request.url) const validateOnly = url.searchParams.get('validate') === 'true' @@ -427,6 +433,10 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ }, }) } catch (error) { + if (error instanceof InvitationsNotAllowedError) { + return NextResponse.json({ error: error.message }, { status: 403 }) + } + logger.error('Failed to create organization invitations', { organizationId: (await params).id, error, @@ -486,10 +496,7 @@ export async function DELETE( and( eq(invitation.id, invitationId), eq(invitation.organizationId, organizationId), - or( - eq(invitation.status, 'pending'), - eq(invitation.status, 'rejected') // Allow cancelling rejected invitations too - ) + or(eq(invitation.status, 'pending'), eq(invitation.status, 'rejected')) ) ) .returning() diff --git a/apps/sim/app/api/tools/dynamodb/introspect/route.ts b/apps/sim/app/api/tools/dynamodb/introspect/route.ts new file mode 100644 index 0000000000..55db21aea9 --- /dev/null +++ b/apps/sim/app/api/tools/dynamodb/introspect/route.ts @@ -0,0 +1,73 @@ +import { randomUUID } from 'crypto' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createRawDynamoDBClient, describeTable, listTables } from '@/app/api/tools/dynamodb/utils' + +const logger = createLogger('DynamoDBIntrospectAPI') + +const IntrospectSchema = z.object({ + region: z.string().min(1, 'AWS region is required'), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + tableName: z.string().optional(), +}) + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = IntrospectSchema.parse(body) + + logger.info(`[${requestId}] Introspecting DynamoDB in region ${params.region}`) + + const client = createRawDynamoDBClient({ + region: params.region, + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + }) + + try { + const { tables } = await listTables(client) + + if (params.tableName) { + logger.info(`[${requestId}] Describing table: ${params.tableName}`) + const { tableDetails } = await describeTable(client, params.tableName) + + logger.info(`[${requestId}] Table description completed for '${params.tableName}'`) + + return NextResponse.json({ + message: `Table '${params.tableName}' described successfully.`, + tables, + tableDetails, + }) + } + + logger.info(`[${requestId}] Listed ${tables.length} tables`) + + return NextResponse.json({ + message: `Found ${tables.length} table(s) in region '${params.region}'.`, + tables, + }) + } finally { + client.destroy() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] DynamoDB introspection failed:`, error) + + return NextResponse.json( + { error: `DynamoDB introspection failed: ${errorMessage}` }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/dynamodb/utils.ts b/apps/sim/app/api/tools/dynamodb/utils.ts index 9641fb9b30..c71d8bbe6c 100644 --- a/apps/sim/app/api/tools/dynamodb/utils.ts +++ b/apps/sim/app/api/tools/dynamodb/utils.ts @@ -1,4 +1,4 @@ -import { DynamoDBClient } from '@aws-sdk/client-dynamodb' +import { DescribeTableCommand, DynamoDBClient, ListTablesCommand } from '@aws-sdk/client-dynamodb' import { DeleteCommand, DynamoDBDocumentClient, @@ -8,7 +8,7 @@ import { ScanCommand, UpdateCommand, } from '@aws-sdk/lib-dynamodb' -import type { DynamoDBConnectionConfig } from '@/tools/dynamodb/types' +import type { DynamoDBConnectionConfig, DynamoDBTableSchema } from '@/tools/dynamodb/types' export function createDynamoDBClient(config: DynamoDBConnectionConfig): DynamoDBDocumentClient { const client = new DynamoDBClient({ @@ -172,3 +172,99 @@ export async function deleteItem( await client.send(command) return { success: true } } + +/** + * Creates a raw DynamoDB client for operations that don't require DocumentClient + */ +export function createRawDynamoDBClient(config: DynamoDBConnectionConfig): DynamoDBClient { + return new DynamoDBClient({ + region: config.region, + credentials: { + accessKeyId: config.accessKeyId, + secretAccessKey: config.secretAccessKey, + }, + }) +} + +/** + * Lists all DynamoDB tables in the configured region + */ +export async function listTables(client: DynamoDBClient): Promise<{ tables: string[] }> { + const tables: string[] = [] + let exclusiveStartTableName: string | undefined + + do { + const command = new ListTablesCommand({ + ExclusiveStartTableName: exclusiveStartTableName, + }) + + const response = await client.send(command) + if (response.TableNames) { + tables.push(...response.TableNames) + } + exclusiveStartTableName = response.LastEvaluatedTableName + } while (exclusiveStartTableName) + + return { tables } +} + +/** + * Describes a specific DynamoDB table and returns its schema information + */ +export async function describeTable( + client: DynamoDBClient, + tableName: string +): Promise<{ tableDetails: DynamoDBTableSchema }> { + const command = new DescribeTableCommand({ + TableName: tableName, + }) + + const response = await client.send(command) + const table = response.Table + + if (!table) { + throw new Error(`Table '${tableName}' not found`) + } + + const tableDetails: DynamoDBTableSchema = { + tableName: table.TableName || tableName, + tableStatus: table.TableStatus || 'UNKNOWN', + keySchema: + table.KeySchema?.map((key) => ({ + attributeName: key.AttributeName || '', + keyType: (key.KeyType as 'HASH' | 'RANGE') || 'HASH', + })) || [], + attributeDefinitions: + table.AttributeDefinitions?.map((attr) => ({ + attributeName: attr.AttributeName || '', + attributeType: (attr.AttributeType as 'S' | 'N' | 'B') || 'S', + })) || [], + globalSecondaryIndexes: + table.GlobalSecondaryIndexes?.map((gsi) => ({ + indexName: gsi.IndexName || '', + keySchema: + gsi.KeySchema?.map((key) => ({ + attributeName: key.AttributeName || '', + keyType: (key.KeyType as 'HASH' | 'RANGE') || 'HASH', + })) || [], + projectionType: gsi.Projection?.ProjectionType || 'ALL', + indexStatus: gsi.IndexStatus || 'UNKNOWN', + })) || [], + localSecondaryIndexes: + table.LocalSecondaryIndexes?.map((lsi) => ({ + indexName: lsi.IndexName || '', + keySchema: + lsi.KeySchema?.map((key) => ({ + attributeName: key.AttributeName || '', + keyType: (key.KeyType as 'HASH' | 'RANGE') || 'HASH', + })) || [], + projectionType: lsi.Projection?.ProjectionType || 'ALL', + indexStatus: 'ACTIVE', + })) || [], + itemCount: Number(table.ItemCount) || 0, + tableSizeBytes: Number(table.TableSizeBytes) || 0, + billingMode: table.BillingModeSummary?.BillingMode || 'PROVISIONED', + } + + return { tableDetails } +} diff --git a/apps/sim/app/api/tools/google_calendar/calendars/route.ts b/apps/sim/app/api/tools/google_calendar/calendars/route.ts index f934d6dd41..0493825399 100644 --- a/apps/sim/app/api/tools/google_calendar/calendars/route.ts +++ b/apps/sim/app/api/tools/google_calendar/calendars/route.ts @@ -3,7 +3,6 @@ import { type NextRequest, NextResponse } from 'next/server' import { authorizeCredentialUse } from '@/lib/auth/credential-access' import { generateRequestId } from '@/lib/core/utils/request' import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils' -import { isUuidV4 } from '@/executor/constants' export const dynamic = 'force-dynamic' const logger = createLogger('GoogleCalendarAPI') @@ -34,16 +33,6 @@ export async function GET(request: NextRequest) { logger.warn(`[${requestId}] Missing credentialId parameter`) return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 }) } - - if (!isUuidV4(credentialId)) { - logger.warn(`[${requestId}] Invalid credentialId format`, { credentialId }) - return NextResponse.json({ error: 'Invalid credential ID format' }, { status: 400 }) - } - - if (workflowId && !isUuidV4(workflowId)) { - logger.warn(`[${requestId}] Invalid workflowId format`, { workflowId }) - return NextResponse.json({ error: 'Invalid workflow ID format' }, { status: 400 }) - } const authz = await authorizeCredentialUse(request, { credentialId, workflowId }) if (!authz.ok || !authz.credentialOwnerUserId) { return NextResponse.json({ error: authz.error || 'Unauthorized' }, { status: 403 }) diff --git a/apps/sim/app/api/tools/mongodb/introspect/route.ts b/apps/sim/app/api/tools/mongodb/introspect/route.ts new file mode 100644 index 0000000000..a095c13868 --- /dev/null +++ b/apps/sim/app/api/tools/mongodb/introspect/route.ts @@ -0,0 +1,73 @@ +import { randomUUID } from 'crypto' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createMongoDBConnection, executeIntrospect } from '../utils' + +const logger = createLogger('MongoDBIntrospectAPI') + +const IntrospectSchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive('Port must be a positive integer'), + database: z.string().optional(), + username: z.string().optional(), + password: z.string().optional(), + authSource: z.string().optional(), + ssl: z.enum(['disabled', 'required', 'preferred']).default('preferred'), +}) + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + let client = null + + try { + const body = await request.json() + const params = IntrospectSchema.parse(body) + + logger.info( + `[${requestId}] Introspecting MongoDB at ${params.host}:${params.port}${params.database ? `/${params.database}` : ''}` + ) + + client = await createMongoDBConnection({ + host: params.host, + port: params.port, + database: params.database || 'admin', + username: params.username, + password: params.password, + authSource: params.authSource, + ssl: params.ssl, + }) + + const result = await executeIntrospect(client, params.database) + + logger.info( + `[${requestId}] Introspection completed: ${result.databases.length} databases, ${result.collections.length} collections` + ) + + return NextResponse.json({ + message: result.message, + databases: result.databases, + collections: result.collections, + }) + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] MongoDB introspect failed:`, error) + + return NextResponse.json( + { error: `MongoDB introspect failed: ${errorMessage}` }, + { status: 500 } + ) + } finally { + if (client) { + await client.close() + } + } +} diff --git a/apps/sim/app/api/tools/mongodb/utils.ts b/apps/sim/app/api/tools/mongodb/utils.ts index 812cc3cd8e..4697a1cce0 100644 --- a/apps/sim/app/api/tools/mongodb/utils.ts +++ b/apps/sim/app/api/tools/mongodb/utils.ts @@ -1,5 +1,5 @@ import { MongoClient } from 'mongodb' -import type { MongoDBConnectionConfig } from '@/tools/mongodb/types' +import type { MongoDBCollectionInfo, MongoDBConnectionConfig } from '@/tools/mongodb/types' export async function createMongoDBConnection(config: MongoDBConnectionConfig) { const credentials = @@ -129,3 +129,59 @@ export function sanitizeCollectionName(name: string): string { } return name } + +/** + * Introspect MongoDB to get databases, collections, and indexes + */ +export async function executeIntrospect( + client: MongoClient, + database?: string +): Promise<{ + message: string + databases: string[] + collections: MongoDBCollectionInfo[] +}> { + const databases: string[] = [] + const collections: MongoDBCollectionInfo[] = [] + + if (database) { + databases.push(database) + const db = client.db(database) + const collectionList = await db.listCollections().toArray() + + for (const collInfo of collectionList) { + const coll = db.collection(collInfo.name) + const indexes = await coll.indexes() + const documentCount = await coll.estimatedDocumentCount() + + collections.push({ + name: collInfo.name, + type: collInfo.type || 'collection', + documentCount, + indexes: indexes.map((idx) => ({ + name: idx.name || '', + key: idx.key as Record, + unique: idx.unique || false, + sparse: idx.sparse, + })), + }) + } + } else { + const admin = client.db().admin() + const dbList = await admin.listDatabases() + + for (const dbInfo of dbList.databases) { + databases.push(dbInfo.name) + } + } + + const message = database + ? `Found ${collections.length} collections in database '${database}'` + : `Found ${databases.length} databases` + + return { + message, + databases, + collections, + } +} diff --git a/apps/sim/app/api/tools/mysql/introspect/route.ts b/apps/sim/app/api/tools/mysql/introspect/route.ts new file mode 100644 index 0000000000..5a9cb53c5f --- /dev/null +++ b/apps/sim/app/api/tools/mysql/introspect/route.ts @@ -0,0 +1,70 @@ +import { randomUUID } from 'crypto' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createMySQLConnection, executeIntrospect } from '@/app/api/tools/mysql/utils' + +const logger = createLogger('MySQLIntrospectAPI') + +const IntrospectSchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive('Port must be a positive integer'), + database: z.string().min(1, 'Database name is required'), + username: z.string().min(1, 'Username is required'), + password: z.string().min(1, 'Password is required'), + ssl: z.enum(['disabled', 'required', 'preferred']).default('preferred'), +}) + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = IntrospectSchema.parse(body) + + logger.info( + `[${requestId}] Introspecting MySQL schema on ${params.host}:${params.port}/${params.database}` + ) + + const connection = await createMySQLConnection({ + host: params.host, + port: params.port, + database: params.database, + username: params.username, + password: params.password, + ssl: params.ssl, + }) + + try { + const result = await executeIntrospect(connection, params.database) + + logger.info( + `[${requestId}] Introspection completed successfully, found ${result.tables.length} tables` + ) + + return NextResponse.json({ + message: `Schema introspection completed. Found ${result.tables.length} table(s) in database '${params.database}'.`, + tables: result.tables, + databases: result.databases, + }) + } finally { + await connection.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] MySQL introspection failed:`, error) + + return NextResponse.json( + { error: `MySQL introspection failed: ${errorMessage}` }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/mysql/utils.ts b/apps/sim/app/api/tools/mysql/utils.ts index 7738b3fc0d..44bab141b6 100644 --- a/apps/sim/app/api/tools/mysql/utils.ts +++ b/apps/sim/app/api/tools/mysql/utils.ts @@ -166,3 +166,146 @@ function sanitizeSingleIdentifier(identifier: string): string { return `\`${cleaned}\`` } + +export interface MySQLIntrospectionResult { + tables: Array<{ + name: string + database: string + columns: Array<{ + name: string + type: string + nullable: boolean + default: string | null + isPrimaryKey: boolean + isForeignKey: boolean + autoIncrement: boolean + references?: { + table: string + column: string + } + }> + primaryKey: string[] + foreignKeys: Array<{ + column: string + referencesTable: string + referencesColumn: string + }> + indexes: Array<{ + name: string + columns: string[] + unique: boolean + }> + }> + databases: string[] +} + +export async function executeIntrospect( + connection: mysql.Connection, + databaseName: string +): Promise { + const [databasesRows] = await connection.execute( + `SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA + WHERE SCHEMA_NAME NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys') + ORDER BY SCHEMA_NAME` + ) + const databases = databasesRows.map((row) => row.SCHEMA_NAME) + + const [tablesRows] = await connection.execute( + `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = ? AND TABLE_TYPE = 'BASE TABLE' + ORDER BY TABLE_NAME`, + [databaseName] + ) + + const tables = [] + + for (const tableRow of tablesRows) { + const tableName = tableRow.TABLE_NAME + + const [columnsRows] = await connection.execute( + `SELECT COLUMN_NAME, DATA_TYPE, COLUMN_TYPE, IS_NULLABLE, COLUMN_DEFAULT, EXTRA + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? + ORDER BY ORDINAL_POSITION`, + [databaseName, tableName] + ) + + const [pkRows] = await connection.execute( + `SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE + WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND CONSTRAINT_NAME = 'PRIMARY' + ORDER BY ORDINAL_POSITION`, + [databaseName, tableName] + ) + const primaryKeyColumns = pkRows.map((row) => row.COLUMN_NAME) + + const [fkRows] = await connection.execute( + `SELECT kcu.COLUMN_NAME, kcu.REFERENCED_TABLE_NAME, kcu.REFERENCED_COLUMN_NAME + FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu + WHERE kcu.TABLE_SCHEMA = ? AND kcu.TABLE_NAME = ? AND kcu.REFERENCED_TABLE_NAME IS NOT NULL`, + [databaseName, tableName] + ) + + const foreignKeys = fkRows.map((row) => ({ + column: row.COLUMN_NAME, + referencesTable: row.REFERENCED_TABLE_NAME, + referencesColumn: row.REFERENCED_COLUMN_NAME, + })) + + const fkColumnSet = new Set(foreignKeys.map((fk) => fk.column)) + + const [indexRows] = await connection.execute( + `SELECT INDEX_NAME, COLUMN_NAME, SEQ_IN_INDEX, NON_UNIQUE + FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND INDEX_NAME != 'PRIMARY' + ORDER BY INDEX_NAME, SEQ_IN_INDEX`, + [databaseName, tableName] + ) + + const indexMap = new Map() + for (const row of indexRows) { + const indexName = row.INDEX_NAME + if (!indexMap.has(indexName)) { + indexMap.set(indexName, { + name: indexName, + columns: [], + unique: row.NON_UNIQUE === 0, + }) + } + indexMap.get(indexName)!.columns.push(row.COLUMN_NAME) + } + const indexes = Array.from(indexMap.values()) + + const columns = columnsRows.map((col) => { + const columnName = col.COLUMN_NAME + const fk = foreignKeys.find((f) => f.column === columnName) + const isAutoIncrement = col.EXTRA?.toLowerCase().includes('auto_increment') || false + + return { + name: columnName, + type: col.COLUMN_TYPE || col.DATA_TYPE, + nullable: col.IS_NULLABLE === 'YES', + default: col.COLUMN_DEFAULT, + isPrimaryKey: primaryKeyColumns.includes(columnName), + isForeignKey: fkColumnSet.has(columnName), + autoIncrement: isAutoIncrement, + ...(fk && { + references: { + table: fk.referencesTable, + column: fk.referencesColumn, + }, + }), + } + }) + + tables.push({ + name: tableName, + database: databaseName, + columns, + primaryKey: primaryKeyColumns, + foreignKeys, + indexes, + }) + } + + return { tables, databases } +} diff --git a/apps/sim/app/api/tools/neo4j/introspect/route.ts b/apps/sim/app/api/tools/neo4j/introspect/route.ts new file mode 100644 index 0000000000..0543afb026 --- /dev/null +++ b/apps/sim/app/api/tools/neo4j/introspect/route.ts @@ -0,0 +1,199 @@ +import { randomUUID } from 'crypto' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createNeo4jDriver } from '@/app/api/tools/neo4j/utils' +import type { Neo4jNodeSchema, Neo4jRelationshipSchema } from '@/tools/neo4j/types' + +const logger = createLogger('Neo4jIntrospectAPI') + +const IntrospectSchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive('Port must be a positive integer'), + database: z.string().min(1, 'Database name is required'), + username: z.string().min(1, 'Username is required'), + password: z.string().min(1, 'Password is required'), + encryption: z.enum(['enabled', 'disabled']).default('disabled'), +}) + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + let driver = null + let session = null + + try { + const body = await request.json() + const params = IntrospectSchema.parse(body) + + logger.info( + `[${requestId}] Introspecting Neo4j database at ${params.host}:${params.port}/${params.database}` + ) + + driver = await createNeo4jDriver({ + host: params.host, + port: params.port, + database: params.database, + username: params.username, + password: params.password, + encryption: params.encryption, + }) + + session = driver.session({ database: params.database }) + + const labelsResult = await session.run( + 'CALL db.labels() YIELD label RETURN label ORDER BY label' + ) + const labels: string[] = labelsResult.records.map((record) => record.get('label') as string) + + const relationshipTypesResult = await session.run( + 'CALL db.relationshipTypes() YIELD relationshipType RETURN relationshipType ORDER BY relationshipType' + ) + const relationshipTypes: string[] = relationshipTypesResult.records.map( + (record) => record.get('relationshipType') as string + ) + + const nodeSchemas: Neo4jNodeSchema[] = [] + try { + const nodePropertiesResult = await session.run( + 'CALL db.schema.nodeTypeProperties() YIELD nodeLabels, propertyName, propertyTypes RETURN nodeLabels, propertyName, propertyTypes' + ) + + const nodePropertiesMap = new Map>() + + for (const record of nodePropertiesResult.records) { + const nodeLabels = record.get('nodeLabels') as string[] + const propertyName = record.get('propertyName') as string + const propertyTypes = record.get('propertyTypes') as string[] + + const labelKey = nodeLabels.join(':') + if (!nodePropertiesMap.has(labelKey)) { + nodePropertiesMap.set(labelKey, []) + } + nodePropertiesMap.get(labelKey)!.push({ name: propertyName, types: propertyTypes }) + } + + for (const [labelKey, properties] of nodePropertiesMap) { + nodeSchemas.push({ + label: labelKey, + properties, + }) + } + } catch (nodePropsError) { + logger.warn( + `[${requestId}] Could not fetch node properties (may not be supported in this Neo4j version): ${nodePropsError}` + ) + } + + const relationshipSchemas: Neo4jRelationshipSchema[] = [] + try { + const relPropertiesResult = await session.run( + 'CALL db.schema.relTypeProperties() YIELD relationshipType, propertyName, propertyTypes RETURN relationshipType, propertyName, propertyTypes' + ) + + const relPropertiesMap = new Map>() + + for (const record of relPropertiesResult.records) { + const relType = record.get('relationshipType') as string + const propertyName = record.get('propertyName') as string | null + const propertyTypes = record.get('propertyTypes') as string[] + + if (!relPropertiesMap.has(relType)) { + relPropertiesMap.set(relType, []) + } + if (propertyName) { + relPropertiesMap.get(relType)!.push({ name: propertyName, types: propertyTypes }) + } + } + + for (const [relType, properties] of relPropertiesMap) { + relationshipSchemas.push({ + type: relType, + properties, + }) + } + } catch (relPropsError) { + logger.warn( + `[${requestId}] Could not fetch relationship properties (may not be supported in this Neo4j version): ${relPropsError}` + ) + } + + const constraints: Array<{ + name: string + type: string + entityType: string + properties: string[] + }> = [] + try { + const constraintsResult = await session.run('SHOW CONSTRAINTS') + + for (const record of constraintsResult.records) { + const name = record.get('name') as string + const type = record.get('type') as string + const entityType = record.get('entityType') as string + const properties = (record.get('properties') as string[]) || [] + + constraints.push({ name, type, entityType, properties }) + } + } catch (constraintsError) { + logger.warn( + `[${requestId}] Could not fetch constraints (may not be supported in this Neo4j version): ${constraintsError}` + ) + } + + const indexes: Array<{ name: string; type: string; entityType: string; properties: string[] }> = + [] + try { + const indexesResult = await session.run('SHOW INDEXES') + + for (const record of indexesResult.records) { + const name = record.get('name') as string + const type = record.get('type') as string + const entityType = record.get('entityType') as string + const properties = (record.get('properties') as string[]) || [] + + indexes.push({ name, type, entityType, properties }) + } + } catch (indexesError) { + logger.warn( + `[${requestId}] Could not fetch indexes (may not be supported in this Neo4j version): ${indexesError}` + ) + } + + logger.info( + `[${requestId}] Introspection completed: ${labels.length} labels, ${relationshipTypes.length} relationship types, ${constraints.length} constraints, ${indexes.length} indexes` + ) + + return NextResponse.json({ + message: `Database introspection completed: found ${labels.length} labels, ${relationshipTypes.length} relationship types, ${nodeSchemas.length} node schemas, ${relationshipSchemas.length} relationship schemas, ${constraints.length} constraints, ${indexes.length} indexes`, + labels, + relationshipTypes, + nodeSchemas, + relationshipSchemas, + constraints, + indexes, + }) + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] Neo4j introspection failed:`, error) + + return NextResponse.json( + { error: `Neo4j introspection failed: ${errorMessage}` }, + { status: 500 } + ) + } finally { + if (session) { + await session.close() + } + if (driver) { + await driver.close() + } + } +} diff --git a/apps/sim/app/api/tools/postgresql/introspect/route.ts b/apps/sim/app/api/tools/postgresql/introspect/route.ts new file mode 100644 index 0000000000..3fc5e41fcf --- /dev/null +++ b/apps/sim/app/api/tools/postgresql/introspect/route.ts @@ -0,0 +1,71 @@ +import { randomUUID } from 'crypto' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createPostgresConnection, executeIntrospect } from '@/app/api/tools/postgresql/utils' + +const logger = createLogger('PostgreSQLIntrospectAPI') + +const IntrospectSchema = z.object({ + host: z.string().min(1, 'Host is required'), + port: z.coerce.number().int().positive('Port must be a positive integer'), + database: z.string().min(1, 'Database name is required'), + username: z.string().min(1, 'Username is required'), + password: z.string().min(1, 'Password is required'), + ssl: z.enum(['disabled', 'required', 'preferred']).default('preferred'), + schema: z.string().default('public'), +}) + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = IntrospectSchema.parse(body) + + logger.info( + `[${requestId}] Introspecting PostgreSQL schema on ${params.host}:${params.port}/${params.database}` + ) + + const sql = createPostgresConnection({ + host: params.host, + port: params.port, + database: params.database, + username: params.username, + password: params.password, + ssl: params.ssl, + }) + + try { + const result = await executeIntrospect(sql, params.schema) + + logger.info( + `[${requestId}] Introspection completed successfully, found ${result.tables.length} tables` + ) + + return NextResponse.json({ + message: `Schema introspection completed. Found ${result.tables.length} table(s) in schema '${params.schema}'.`, + tables: result.tables, + schemas: result.schemas, + }) + } finally { + await sql.end() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] PostgreSQL introspection failed:`, error) + + return NextResponse.json( + { error: `PostgreSQL introspection failed: ${errorMessage}` }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/postgresql/utils.ts b/apps/sim/app/api/tools/postgresql/utils.ts index 27e8b62cd1..eef711144d 100644 --- a/apps/sim/app/api/tools/postgresql/utils.ts +++ b/apps/sim/app/api/tools/postgresql/utils.ts @@ -187,3 +187,184 @@ export async function executeDelete( rowCount, } } + +export interface IntrospectionResult { + tables: Array<{ + name: string + schema: string + columns: Array<{ + name: string + type: string + nullable: boolean + default: string | null + isPrimaryKey: boolean + isForeignKey: boolean + references?: { + table: string + column: string + } + }> + primaryKey: string[] + foreignKeys: Array<{ + column: string + referencesTable: string + referencesColumn: string + }> + indexes: Array<{ + name: string + columns: string[] + unique: boolean + }> + }> + schemas: string[] +} + +export async function executeIntrospect( + sql: any, + schemaName = 'public' +): Promise { + const schemasResult = await sql` + SELECT schema_name + FROM information_schema.schemata + WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast') + ORDER BY schema_name + ` + const schemas = schemasResult.map((row: { schema_name: string }) => row.schema_name) + + const tablesResult = await sql` + SELECT table_name, table_schema + FROM information_schema.tables + WHERE table_schema = ${schemaName} + AND table_type = 'BASE TABLE' + ORDER BY table_name + ` + + const tables = [] + + for (const tableRow of tablesResult) { + const tableName = tableRow.table_name + const tableSchema = tableRow.table_schema + + const columnsResult = await sql` + SELECT + c.column_name, + c.data_type, + c.is_nullable, + c.column_default, + c.udt_name + FROM information_schema.columns c + WHERE c.table_schema = ${tableSchema} + AND c.table_name = ${tableName} + ORDER BY c.ordinal_position + ` + + const pkResult = await sql` + SELECT kcu.column_name + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + WHERE tc.constraint_type = 'PRIMARY KEY' + AND tc.table_schema = ${tableSchema} + AND tc.table_name = ${tableName} + ` + const primaryKeyColumns = pkResult.map((row: { column_name: string }) => row.column_name) + + const fkResult = await sql` + SELECT + kcu.column_name, + ccu.table_name AS foreign_table_name, + ccu.column_name AS foreign_column_name + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + JOIN information_schema.constraint_column_usage ccu + ON ccu.constraint_name = tc.constraint_name + AND ccu.table_schema = tc.table_schema + WHERE tc.constraint_type = 'FOREIGN KEY' + AND tc.table_schema = ${tableSchema} + AND tc.table_name = ${tableName} + ` + + const foreignKeys = fkResult.map( + (row: { column_name: string; foreign_table_name: string; foreign_column_name: string }) => ({ + column: row.column_name, + referencesTable: row.foreign_table_name, + referencesColumn: row.foreign_column_name, + }) + ) + + const fkColumnSet = new Set(foreignKeys.map((fk: { column: string }) => fk.column)) + + const indexesResult = await sql` + SELECT + i.relname AS index_name, + a.attname AS column_name, + ix.indisunique AS is_unique + FROM pg_class t + JOIN pg_index ix ON t.oid = ix.indrelid + JOIN pg_class i ON i.oid = ix.indexrelid + JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE t.relkind = 'r' + AND n.nspname = ${tableSchema} + AND t.relname = ${tableName} + AND NOT ix.indisprimary + ORDER BY i.relname, a.attnum + ` + + const indexMap = new Map() + for (const row of indexesResult) { + const indexName = row.index_name + if (!indexMap.has(indexName)) { + indexMap.set(indexName, { + name: indexName, + columns: [], + unique: row.is_unique, + }) + } + indexMap.get(indexName)!.columns.push(row.column_name) + } + const indexes = Array.from(indexMap.values()) + + const columns = columnsResult.map( + (col: { + column_name: string + data_type: string + is_nullable: string + column_default: string | null + udt_name: string + }) => { + const columnName = col.column_name + const fk = foreignKeys.find((f: { column: string }) => f.column === columnName) + + return { + name: columnName, + type: col.data_type === 'USER-DEFINED' ? col.udt_name : col.data_type, + nullable: col.is_nullable === 'YES', + default: col.column_default, + isPrimaryKey: primaryKeyColumns.includes(columnName), + isForeignKey: fkColumnSet.has(columnName), + ...(fk && { + references: { + table: fk.referencesTable, + column: fk.referencesColumn, + }, + }), + } + } + ) + + tables.push({ + name: tableName, + schema: tableSchema, + columns, + primaryKey: primaryKeyColumns, + foreignKeys, + indexes, + }) + } + + return { tables, schemas } +} diff --git a/apps/sim/app/api/tools/rds/introspect/route.ts b/apps/sim/app/api/tools/rds/introspect/route.ts new file mode 100644 index 0000000000..8eb46ed64b --- /dev/null +++ b/apps/sim/app/api/tools/rds/introspect/route.ts @@ -0,0 +1,80 @@ +import { randomUUID } from 'crypto' +import { createLogger } from '@sim/logger' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { createRdsClient, executeIntrospect, type RdsEngine } from '@/app/api/tools/rds/utils' + +const logger = createLogger('RDSIntrospectAPI') + +const IntrospectSchema = z.object({ + region: z.string().min(1, 'AWS region is required'), + accessKeyId: z.string().min(1, 'AWS access key ID is required'), + secretAccessKey: z.string().min(1, 'AWS secret access key is required'), + resourceArn: z.string().min(1, 'Resource ARN is required'), + secretArn: z.string().min(1, 'Secret ARN is required'), + database: z.string().optional(), + schema: z.string().optional(), + engine: z.enum(['aurora-postgresql', 'aurora-mysql']).optional(), +}) + +export async function POST(request: NextRequest) { + const requestId = randomUUID().slice(0, 8) + + try { + const body = await request.json() + const params = IntrospectSchema.parse(body) + + logger.info( + `[${requestId}] Introspecting RDS Aurora database${params.database ? ` (${params.database})` : ''}` + ) + + const client = createRdsClient({ + region: params.region, + accessKeyId: params.accessKeyId, + secretAccessKey: params.secretAccessKey, + resourceArn: params.resourceArn, + secretArn: params.secretArn, + database: params.database, + }) + + try { + const result = await executeIntrospect( + client, + params.resourceArn, + params.secretArn, + params.database, + params.schema, + params.engine as RdsEngine | undefined + ) + + logger.info( + `[${requestId}] Introspection completed successfully. Engine: ${result.engine}, found ${result.tables.length} tables` + ) + + return NextResponse.json({ + message: `Schema introspection completed. Engine: ${result.engine}. Found ${result.tables.length} table(s).`, + engine: result.engine, + tables: result.tables, + schemas: result.schemas, + }) + } finally { + client.destroy() + } + } catch (error) { + if (error instanceof z.ZodError) { + logger.warn(`[${requestId}] Invalid request data`, { errors: error.errors }) + return NextResponse.json( + { error: 'Invalid request data', details: error.errors }, + { status: 400 } + ) + } + + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred' + logger.error(`[${requestId}] RDS introspection failed:`, error) + + return NextResponse.json( + { error: `RDS introspection failed: ${errorMessage}` }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/rds/utils.ts b/apps/sim/app/api/tools/rds/utils.ts index d76e248ea3..ac78b83d87 100644 --- a/apps/sim/app/api/tools/rds/utils.ts +++ b/apps/sim/app/api/tools/rds/utils.ts @@ -241,3 +241,487 @@ export async function executeDelete( return executeStatement(client, resourceArn, secretArn, database, sql, parameters) } + +export type RdsEngine = 'aurora-postgresql' | 'aurora-mysql' + +export interface RdsIntrospectionResult { + engine: RdsEngine + tables: Array<{ + name: string + schema: string + columns: Array<{ + name: string + type: string + nullable: boolean + default: string | null + isPrimaryKey: boolean + isForeignKey: boolean + references?: { + table: string + column: string + } + }> + primaryKey: string[] + foreignKeys: Array<{ + column: string + referencesTable: string + referencesColumn: string + }> + indexes: Array<{ + name: string + columns: string[] + unique: boolean + }> + }> + schemas: string[] +} + +/** + * Detects the database engine by querying SELECT VERSION() + */ +export async function detectEngine( + client: RDSDataClient, + resourceArn: string, + secretArn: string, + database: string | undefined +): Promise { + const result = await executeStatement( + client, + resourceArn, + secretArn, + database, + 'SELECT VERSION()' + ) + + if (result.rows.length > 0) { + const versionRow = result.rows[0] as Record + const versionValue = Object.values(versionRow)[0] + const versionString = String(versionValue).toLowerCase() + + if (versionString.includes('postgresql') || versionString.includes('postgres')) { + return 'aurora-postgresql' + } + if (versionString.includes('mysql') || versionString.includes('mariadb')) { + return 'aurora-mysql' + } + } + + throw new Error('Unable to detect database engine. Please specify the engine parameter.') +} + +/** + * Introspects PostgreSQL schema using INFORMATION_SCHEMA + */ +async function introspectPostgresql( + client: RDSDataClient, + resourceArn: string, + secretArn: string, + database: string | undefined, + schemaName: string +): Promise { + const schemasResult = await executeStatement( + client, + resourceArn, + secretArn, + database, + `SELECT schema_name FROM information_schema.schemata + WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast') + ORDER BY schema_name` + ) + const schemas = schemasResult.rows.map((row) => (row as { schema_name: string }).schema_name) + + const tablesResult = await executeStatement( + client, + resourceArn, + secretArn, + database, + `SELECT table_name, table_schema + FROM information_schema.tables + WHERE table_schema = :schemaName + AND table_type = 'BASE TABLE' + ORDER BY table_name`, + [{ name: 'schemaName', value: { stringValue: schemaName } }] + ) + + const tables = [] + + for (const tableRow of tablesResult.rows) { + const row = tableRow as { table_name: string; table_schema: string } + const tableName = row.table_name + const tableSchema = row.table_schema + + const columnsResult = await executeStatement( + client, + resourceArn, + secretArn, + database, + `SELECT + c.column_name, + c.data_type, + c.is_nullable, + c.column_default, + c.udt_name + FROM information_schema.columns c + WHERE c.table_schema = :tableSchema + AND c.table_name = :tableName + ORDER BY c.ordinal_position`, + [ + { name: 'tableSchema', value: { stringValue: tableSchema } }, + { name: 'tableName', value: { stringValue: tableName } }, + ] + ) + + const pkResult = await executeStatement( + client, + resourceArn, + secretArn, + database, + `SELECT kcu.column_name + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + WHERE tc.constraint_type = 'PRIMARY KEY' + AND tc.table_schema = :tableSchema + AND tc.table_name = :tableName`, + [ + { name: 'tableSchema', value: { stringValue: tableSchema } }, + { name: 'tableName', value: { stringValue: tableName } }, + ] + ) + const primaryKeyColumns = pkResult.rows.map((r) => (r as { column_name: string }).column_name) + + const fkResult = await executeStatement( + client, + resourceArn, + secretArn, + database, + `SELECT + kcu.column_name, + ccu.table_name AS foreign_table_name, + ccu.column_name AS foreign_column_name + FROM information_schema.table_constraints tc + JOIN information_schema.key_column_usage kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema + JOIN information_schema.constraint_column_usage ccu + ON ccu.constraint_name = tc.constraint_name + AND ccu.table_schema = tc.table_schema + WHERE tc.constraint_type = 'FOREIGN KEY' + AND tc.table_schema = :tableSchema + AND tc.table_name = :tableName`, + [ + { name: 'tableSchema', value: { stringValue: tableSchema } }, + { name: 'tableName', value: { stringValue: tableName } }, + ] + ) + + const foreignKeys = fkResult.rows.map((r) => { + const fkRow = r as { + column_name: string + foreign_table_name: string + foreign_column_name: string + } + return { + column: fkRow.column_name, + referencesTable: fkRow.foreign_table_name, + referencesColumn: fkRow.foreign_column_name, + } + }) + + const fkColumnSet = new Set(foreignKeys.map((fk) => fk.column)) + + const indexesResult = await executeStatement( + client, + resourceArn, + secretArn, + database, + `SELECT + i.relname AS index_name, + a.attname AS column_name, + ix.indisunique AS is_unique + FROM pg_class t + JOIN pg_index ix ON t.oid = ix.indrelid + JOIN pg_class i ON i.oid = ix.indexrelid + JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey) + JOIN pg_namespace n ON n.oid = t.relnamespace + WHERE t.relkind = 'r' + AND n.nspname = :tableSchema + AND t.relname = :tableName + AND NOT ix.indisprimary + ORDER BY i.relname, a.attnum`, + [ + { name: 'tableSchema', value: { stringValue: tableSchema } }, + { name: 'tableName', value: { stringValue: tableName } }, + ] + ) + + const indexMap = new Map() + for (const idxRow of indexesResult.rows) { + const idx = idxRow as { index_name: string; column_name: string; is_unique: boolean } + const indexName = idx.index_name + if (!indexMap.has(indexName)) { + indexMap.set(indexName, { + name: indexName, + columns: [], + unique: idx.is_unique, + }) + } + indexMap.get(indexName)!.columns.push(idx.column_name) + } + const indexes = Array.from(indexMap.values()) + + const columns = columnsResult.rows.map((colRow) => { + const col = colRow as { + column_name: string + data_type: string + is_nullable: string + column_default: string | null + udt_name: string + } + const columnName = col.column_name + const fk = foreignKeys.find((f) => f.column === columnName) + + return { + name: columnName, + type: col.data_type === 'USER-DEFINED' ? col.udt_name : col.data_type, + nullable: col.is_nullable === 'YES', + default: col.column_default, + isPrimaryKey: primaryKeyColumns.includes(columnName), + isForeignKey: fkColumnSet.has(columnName), + ...(fk && { + references: { + table: fk.referencesTable, + column: fk.referencesColumn, + }, + }), + } + }) + + tables.push({ + name: tableName, + schema: tableSchema, + columns, + primaryKey: primaryKeyColumns, + foreignKeys, + indexes, + }) + } + + return { engine: 'aurora-postgresql', tables, schemas } +} + +/** + * Introspects MySQL schema using INFORMATION_SCHEMA + */ +async function introspectMysql( + client: RDSDataClient, + resourceArn: string, + secretArn: string, + database: string | undefined, + schemaName: string +): Promise { + const schemasResult = await executeStatement( + client, + resourceArn, + secretArn, + database, + `SELECT SCHEMA_NAME as schema_name FROM information_schema.SCHEMATA + WHERE SCHEMA_NAME NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys') + ORDER BY SCHEMA_NAME` + ) + const schemas = schemasResult.rows.map((row) => (row as { schema_name: string }).schema_name) + + const tablesResult = await executeStatement( + client, + resourceArn, + secretArn, + database, + `SELECT TABLE_NAME as table_name, TABLE_SCHEMA as table_schema + FROM information_schema.TABLES + WHERE TABLE_SCHEMA = :schemaName + AND TABLE_TYPE = 'BASE TABLE' + ORDER BY TABLE_NAME`, + [{ name: 'schemaName', value: { stringValue: schemaName } }] + ) + + const tables = [] + + for (const tableRow of tablesResult.rows) { + const row = tableRow as { table_name: string; table_schema: string } + const tableName = row.table_name + const tableSchema = row.table_schema + + const columnsResult = await executeStatement( + client, + resourceArn, + secretArn, + database, + `SELECT + COLUMN_NAME as column_name, + DATA_TYPE as data_type, + IS_NULLABLE as is_nullable, + COLUMN_DEFAULT as column_default, + COLUMN_TYPE as column_type, + COLUMN_KEY as column_key + FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = :tableSchema + AND TABLE_NAME = :tableName + ORDER BY ORDINAL_POSITION`, + [ + { name: 'tableSchema', value: { stringValue: tableSchema } }, + { name: 'tableName', value: { stringValue: tableName } }, + ] + ) + + const pkResult = await executeStatement( + client, + resourceArn, + secretArn, + database, + `SELECT COLUMN_NAME as column_name + FROM information_schema.KEY_COLUMN_USAGE + WHERE TABLE_SCHEMA = :tableSchema + AND TABLE_NAME = :tableName + AND CONSTRAINT_NAME = 'PRIMARY' + ORDER BY ORDINAL_POSITION`, + [ + { name: 'tableSchema', value: { stringValue: tableSchema } }, + { name: 'tableName', value: { stringValue: tableName } }, + ] + ) + const primaryKeyColumns = pkResult.rows.map((r) => (r as { column_name: string }).column_name) + + const fkResult = await executeStatement( + client, + resourceArn, + secretArn, + database, + `SELECT + kcu.COLUMN_NAME as column_name, + kcu.REFERENCED_TABLE_NAME as foreign_table_name, + kcu.REFERENCED_COLUMN_NAME as foreign_column_name + FROM information_schema.KEY_COLUMN_USAGE kcu + WHERE kcu.TABLE_SCHEMA = :tableSchema + AND kcu.TABLE_NAME = :tableName + AND kcu.REFERENCED_TABLE_NAME IS NOT NULL`, + [ + { name: 'tableSchema', value: { stringValue: tableSchema } }, + { name: 'tableName', value: { stringValue: tableName } }, + ] + ) + + const foreignKeys = fkResult.rows.map((r) => { + const fkRow = r as { + column_name: string + foreign_table_name: string + foreign_column_name: string + } + return { + column: fkRow.column_name, + referencesTable: fkRow.foreign_table_name, + referencesColumn: fkRow.foreign_column_name, + } + }) + + const fkColumnSet = new Set(foreignKeys.map((fk) => fk.column)) + + const indexesResult = await executeStatement( + client, + resourceArn, + secretArn, + database, + `SELECT + INDEX_NAME as index_name, + COLUMN_NAME as column_name, + NON_UNIQUE as non_unique + FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = :tableSchema + AND TABLE_NAME = :tableName + AND INDEX_NAME != 'PRIMARY' + ORDER BY INDEX_NAME, SEQ_IN_INDEX`, + [ + { name: 'tableSchema', value: { stringValue: tableSchema } }, + { name: 'tableName', value: { stringValue: tableName } }, + ] + ) + + const indexMap = new Map() + for (const idxRow of indexesResult.rows) { + const idx = idxRow as { index_name: string; column_name: string; non_unique: number } + const indexName = idx.index_name + if (!indexMap.has(indexName)) { + indexMap.set(indexName, { + name: indexName, + columns: [], + unique: idx.non_unique === 0, + }) + } + indexMap.get(indexName)!.columns.push(idx.column_name) + } + const indexes = Array.from(indexMap.values()) + + const columns = columnsResult.rows.map((colRow) => { + const col = colRow as { + column_name: string + data_type: string + is_nullable: string + column_default: string | null + column_type: string + column_key: string + } + const columnName = col.column_name + const fk = foreignKeys.find((f) => f.column === columnName) + + return { + name: columnName, + type: col.column_type || col.data_type, + nullable: col.is_nullable === 'YES', + default: col.column_default, + isPrimaryKey: col.column_key === 'PRI', + isForeignKey: fkColumnSet.has(columnName), + ...(fk && { + references: { + table: fk.referencesTable, + column: fk.referencesColumn, + }, + }), + } + }) + + tables.push({ + name: tableName, + schema: tableSchema, + columns, + primaryKey: primaryKeyColumns, + foreignKeys, + indexes, + }) + } + + return { engine: 'aurora-mysql', tables, schemas } +} + +/** + * Introspects RDS Aurora database schema with auto-detection of engine type + */ +export async function executeIntrospect( + client: RDSDataClient, + resourceArn: string, + secretArn: string, + database: string | undefined, + schemaName?: string, + engine?: RdsEngine +): Promise { + const detectedEngine = engine || (await detectEngine(client, resourceArn, secretArn, database)) + + if (detectedEngine === 'aurora-postgresql') { + const schema = schemaName || 'public' + return introspectPostgresql(client, resourceArn, secretArn, database, schema) + } + const schema = schemaName || database || '' + if (!schema) { + throw new Error('Schema or database name is required for MySQL introspection') + } + return introspectMysql(client, resourceArn, secretArn, database, schema) +} diff --git a/apps/sim/app/api/v1/admin/byok/route.ts b/apps/sim/app/api/v1/admin/byok/route.ts deleted file mode 100644 index 8144993122..0000000000 --- a/apps/sim/app/api/v1/admin/byok/route.ts +++ /dev/null @@ -1,199 +0,0 @@ -/** - * Admin BYOK Keys API - * - * GET /api/v1/admin/byok - * List all BYOK keys with optional filtering. - * - * Query Parameters: - * - organizationId?: string - Filter by organization ID (finds all workspaces billed to this org) - * - workspaceId?: string - Filter by specific workspace ID - * - * Response: { data: AdminBYOKKey[], pagination: PaginationMeta } - * - * DELETE /api/v1/admin/byok - * Delete BYOK keys for an organization or workspace. - * Used when an enterprise plan churns to clean up BYOK keys. - * - * Query Parameters: - * - organizationId: string - Delete all BYOK keys for workspaces billed to this org - * - workspaceId?: string - Delete keys for a specific workspace only (optional) - * - * Response: { success: true, deletedCount: number, workspacesAffected: string[] } - */ - -import { db } from '@sim/db' -import { user, workspace, workspaceBYOKKeys } from '@sim/db/schema' -import { createLogger } from '@sim/logger' -import { eq, inArray, sql } from 'drizzle-orm' -import { withAdminAuth } from '@/app/api/v1/admin/middleware' -import { - badRequestResponse, - internalErrorResponse, - singleResponse, -} from '@/app/api/v1/admin/responses' - -const logger = createLogger('AdminBYOKAPI') - -export interface AdminBYOKKey { - id: string - workspaceId: string - workspaceName: string - organizationId: string - providerId: string - createdAt: string - createdByUserId: string | null - createdByEmail: string | null -} - -export const GET = withAdminAuth(async (request) => { - const url = new URL(request.url) - const organizationId = url.searchParams.get('organizationId') - const workspaceId = url.searchParams.get('workspaceId') - - try { - let workspaceIds: string[] = [] - - if (workspaceId) { - workspaceIds = [workspaceId] - } else if (organizationId) { - const workspaces = await db - .select({ id: workspace.id }) - .from(workspace) - .where(eq(workspace.billedAccountUserId, organizationId)) - - workspaceIds = workspaces.map((w) => w.id) - } - - const query = db - .select({ - id: workspaceBYOKKeys.id, - workspaceId: workspaceBYOKKeys.workspaceId, - workspaceName: workspace.name, - organizationId: workspace.billedAccountUserId, - providerId: workspaceBYOKKeys.providerId, - createdAt: workspaceBYOKKeys.createdAt, - createdByUserId: workspaceBYOKKeys.createdBy, - createdByEmail: user.email, - }) - .from(workspaceBYOKKeys) - .innerJoin(workspace, eq(workspaceBYOKKeys.workspaceId, workspace.id)) - .leftJoin(user, eq(workspaceBYOKKeys.createdBy, user.id)) - - let keys - if (workspaceIds.length > 0) { - keys = await query.where(inArray(workspaceBYOKKeys.workspaceId, workspaceIds)) - } else { - keys = await query - } - - const formattedKeys: AdminBYOKKey[] = keys.map((k) => ({ - id: k.id, - workspaceId: k.workspaceId, - workspaceName: k.workspaceName, - organizationId: k.organizationId, - providerId: k.providerId, - createdAt: k.createdAt.toISOString(), - createdByUserId: k.createdByUserId, - createdByEmail: k.createdByEmail, - })) - - logger.info('Admin API: Listed BYOK keys', { - organizationId, - workspaceId, - count: formattedKeys.length, - }) - - return singleResponse({ - data: formattedKeys, - pagination: { - total: formattedKeys.length, - limit: formattedKeys.length, - offset: 0, - hasMore: false, - }, - }) - } catch (error) { - logger.error('Admin API: Failed to list BYOK keys', { error, organizationId, workspaceId }) - return internalErrorResponse('Failed to list BYOK keys') - } -}) - -export const DELETE = withAdminAuth(async (request) => { - const url = new URL(request.url) - const organizationId = url.searchParams.get('organizationId') - const workspaceId = url.searchParams.get('workspaceId') - const reason = url.searchParams.get('reason') || 'Enterprise plan churn cleanup' - - if (!organizationId && !workspaceId) { - return badRequestResponse('Either organizationId or workspaceId is required') - } - - try { - let workspaceIds: string[] = [] - - if (workspaceId) { - workspaceIds = [workspaceId] - } else if (organizationId) { - const workspaces = await db - .select({ id: workspace.id }) - .from(workspace) - .where(eq(workspace.billedAccountUserId, organizationId)) - - workspaceIds = workspaces.map((w) => w.id) - } - - if (workspaceIds.length === 0) { - logger.info('Admin API: No workspaces found for BYOK cleanup', { - organizationId, - workspaceId, - }) - return singleResponse({ - success: true, - deletedCount: 0, - workspacesAffected: [], - message: 'No workspaces found for the given organization/workspace ID', - }) - } - - const countResult = await db - .select({ count: sql`count(*)` }) - .from(workspaceBYOKKeys) - .where(inArray(workspaceBYOKKeys.workspaceId, workspaceIds)) - - const totalToDelete = Number(countResult[0]?.count ?? 0) - - if (totalToDelete === 0) { - logger.info('Admin API: No BYOK keys to delete', { - organizationId, - workspaceId, - workspaceIds, - }) - return singleResponse({ - success: true, - deletedCount: 0, - workspacesAffected: [], - message: 'No BYOK keys found for the specified workspaces', - }) - } - - await db.delete(workspaceBYOKKeys).where(inArray(workspaceBYOKKeys.workspaceId, workspaceIds)) - - logger.info('Admin API: Deleted BYOK keys', { - organizationId, - workspaceId, - workspaceIds, - deletedCount: totalToDelete, - reason, - }) - - return singleResponse({ - success: true, - deletedCount: totalToDelete, - workspacesAffected: workspaceIds, - reason, - }) - } catch (error) { - logger.error('Admin API: Failed to delete BYOK keys', { error, organizationId, workspaceId }) - return internalErrorResponse('Failed to delete BYOK keys') - } -}) diff --git a/apps/sim/app/api/v1/admin/index.ts b/apps/sim/app/api/v1/admin/index.ts index ad91e0c447..e76bece6eb 100644 --- a/apps/sim/app/api/v1/admin/index.ts +++ b/apps/sim/app/api/v1/admin/index.ts @@ -17,6 +17,12 @@ * Workspaces: * GET /api/v1/admin/workspaces - List all workspaces * GET /api/v1/admin/workspaces/:id - Get workspace details + * GET /api/v1/admin/workspaces/:id/members - List workspace members + * POST /api/v1/admin/workspaces/:id/members - Add/update workspace member + * DELETE /api/v1/admin/workspaces/:id/members?userId=X - Remove workspace member + * GET /api/v1/admin/workspaces/:id/members/:mid - Get workspace member details + * PATCH /api/v1/admin/workspaces/:id/members/:mid - Update workspace member permissions + * DELETE /api/v1/admin/workspaces/:id/members/:mid - Remove workspace member by ID * GET /api/v1/admin/workspaces/:id/workflows - List workspace workflows * DELETE /api/v1/admin/workspaces/:id/workflows - Delete all workspace workflows * GET /api/v1/admin/workspaces/:id/folders - List workspace folders @@ -53,10 +59,6 @@ * GET /api/v1/admin/subscriptions/:id - Get subscription details * DELETE /api/v1/admin/subscriptions/:id - Cancel subscription (?atPeriodEnd=true for scheduled) * - * BYOK Keys: - * GET /api/v1/admin/byok - List BYOK keys (?organizationId=X or ?workspaceId=X) - * DELETE /api/v1/admin/byok - Delete BYOK keys for org/workspace - * * Access Control (Permission Groups): * GET /api/v1/admin/access-control - List permission groups (?organizationId=X) * DELETE /api/v1/admin/access-control - Delete permission groups for org (?organizationId=X) @@ -99,6 +101,7 @@ export type { AdminWorkflowDetail, AdminWorkspace, AdminWorkspaceDetail, + AdminWorkspaceMember, DbMember, DbOrganization, DbSubscription, diff --git a/apps/sim/app/api/v1/admin/types.ts b/apps/sim/app/api/v1/admin/types.ts index fbc12ae7ec..114563a372 100644 --- a/apps/sim/app/api/v1/admin/types.ts +++ b/apps/sim/app/api/v1/admin/types.ts @@ -518,6 +518,22 @@ export interface AdminMemberDetail extends AdminMember { billingBlocked: boolean } +// ============================================================================= +// Workspace Member Types +// ============================================================================= + +export interface AdminWorkspaceMember { + id: string + workspaceId: string + userId: string + permissions: 'admin' | 'write' | 'read' + createdAt: string + updatedAt: string + userName: string + userEmail: string + userImage: string | null +} + // ============================================================================= // User Billing Types // ============================================================================= diff --git a/apps/sim/app/api/v1/admin/workspaces/[id]/members/[memberId]/route.ts b/apps/sim/app/api/v1/admin/workspaces/[id]/members/[memberId]/route.ts new file mode 100644 index 0000000000..49092d86df --- /dev/null +++ b/apps/sim/app/api/v1/admin/workspaces/[id]/members/[memberId]/route.ts @@ -0,0 +1,232 @@ +/** + * GET /api/v1/admin/workspaces/[id]/members/[memberId] + * + * Get workspace member details. + * + * Response: AdminSingleResponse + * + * PATCH /api/v1/admin/workspaces/[id]/members/[memberId] + * + * Update member permissions. + * + * Body: + * - permissions: 'admin' | 'write' | 'read' - New permission level + * + * Response: AdminSingleResponse + * + * DELETE /api/v1/admin/workspaces/[id]/members/[memberId] + * + * Remove member from workspace. + * + * Response: AdminSingleResponse<{ removed: true, memberId: string, userId: string }> + */ + +import { db } from '@sim/db' +import { permissions, user, workspace } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, eq } from 'drizzle-orm' +import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' +import { + badRequestResponse, + internalErrorResponse, + notFoundResponse, + singleResponse, +} from '@/app/api/v1/admin/responses' +import type { AdminWorkspaceMember } from '@/app/api/v1/admin/types' + +const logger = createLogger('AdminWorkspaceMemberDetailAPI') + +interface RouteParams { + id: string + memberId: string +} + +export const GET = withAdminAuthParams(async (_, context) => { + const { id: workspaceId, memberId } = await context.params + + try { + const [workspaceData] = await db + .select({ id: workspace.id }) + .from(workspace) + .where(eq(workspace.id, workspaceId)) + .limit(1) + + if (!workspaceData) { + return notFoundResponse('Workspace') + } + + const [memberData] = await db + .select({ + id: permissions.id, + userId: permissions.userId, + permissionType: permissions.permissionType, + createdAt: permissions.createdAt, + updatedAt: permissions.updatedAt, + userName: user.name, + userEmail: user.email, + userImage: user.image, + }) + .from(permissions) + .innerJoin(user, eq(permissions.userId, user.id)) + .where( + and( + eq(permissions.id, memberId), + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, workspaceId) + ) + ) + .limit(1) + + if (!memberData) { + return notFoundResponse('Workspace member') + } + + const data: AdminWorkspaceMember = { + id: memberData.id, + workspaceId, + userId: memberData.userId, + permissions: memberData.permissionType, + createdAt: memberData.createdAt.toISOString(), + updatedAt: memberData.updatedAt.toISOString(), + userName: memberData.userName, + userEmail: memberData.userEmail, + userImage: memberData.userImage, + } + + logger.info(`Admin API: Retrieved member ${memberId} from workspace ${workspaceId}`) + + return singleResponse(data) + } catch (error) { + logger.error('Admin API: Failed to get workspace member', { error, workspaceId, memberId }) + return internalErrorResponse('Failed to get workspace member') + } +}) + +export const PATCH = withAdminAuthParams(async (request, context) => { + const { id: workspaceId, memberId } = await context.params + + try { + const body = await request.json() + + if (!body.permissions || !['admin', 'write', 'read'].includes(body.permissions)) { + return badRequestResponse('permissions must be "admin", "write", or "read"') + } + + const [workspaceData] = await db + .select({ id: workspace.id }) + .from(workspace) + .where(eq(workspace.id, workspaceId)) + .limit(1) + + if (!workspaceData) { + return notFoundResponse('Workspace') + } + + const [existingMember] = await db + .select({ + id: permissions.id, + userId: permissions.userId, + permissionType: permissions.permissionType, + createdAt: permissions.createdAt, + }) + .from(permissions) + .where( + and( + eq(permissions.id, memberId), + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, workspaceId) + ) + ) + .limit(1) + + if (!existingMember) { + return notFoundResponse('Workspace member') + } + + const now = new Date() + + await db + .update(permissions) + .set({ permissionType: body.permissions, updatedAt: now }) + .where(eq(permissions.id, memberId)) + + const [userData] = await db + .select({ name: user.name, email: user.email, image: user.image }) + .from(user) + .where(eq(user.id, existingMember.userId)) + .limit(1) + + const data: AdminWorkspaceMember = { + id: existingMember.id, + workspaceId, + userId: existingMember.userId, + permissions: body.permissions, + createdAt: existingMember.createdAt.toISOString(), + updatedAt: now.toISOString(), + userName: userData?.name ?? '', + userEmail: userData?.email ?? '', + userImage: userData?.image ?? null, + } + + logger.info(`Admin API: Updated member ${memberId} permissions to ${body.permissions}`, { + workspaceId, + previousPermissions: existingMember.permissionType, + }) + + return singleResponse(data) + } catch (error) { + logger.error('Admin API: Failed to update workspace member', { error, workspaceId, memberId }) + return internalErrorResponse('Failed to update workspace member') + } +}) + +export const DELETE = withAdminAuthParams(async (_, context) => { + const { id: workspaceId, memberId } = await context.params + + try { + const [workspaceData] = await db + .select({ id: workspace.id }) + .from(workspace) + .where(eq(workspace.id, workspaceId)) + .limit(1) + + if (!workspaceData) { + return notFoundResponse('Workspace') + } + + const [existingMember] = await db + .select({ + id: permissions.id, + userId: permissions.userId, + }) + .from(permissions) + .where( + and( + eq(permissions.id, memberId), + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, workspaceId) + ) + ) + .limit(1) + + if (!existingMember) { + return notFoundResponse('Workspace member') + } + + await db.delete(permissions).where(eq(permissions.id, memberId)) + + logger.info(`Admin API: Removed member ${memberId} from workspace ${workspaceId}`, { + userId: existingMember.userId, + }) + + return singleResponse({ + removed: true, + memberId, + userId: existingMember.userId, + workspaceId, + }) + } catch (error) { + logger.error('Admin API: Failed to remove workspace member', { error, workspaceId, memberId }) + return internalErrorResponse('Failed to remove workspace member') + } +}) diff --git a/apps/sim/app/api/v1/admin/workspaces/[id]/members/route.ts b/apps/sim/app/api/v1/admin/workspaces/[id]/members/route.ts new file mode 100644 index 0000000000..687198506c --- /dev/null +++ b/apps/sim/app/api/v1/admin/workspaces/[id]/members/route.ts @@ -0,0 +1,298 @@ +/** + * GET /api/v1/admin/workspaces/[id]/members + * + * List all members of a workspace with their permission details. + * + * Query Parameters: + * - limit: number (default: 50, max: 250) + * - offset: number (default: 0) + * + * Response: AdminListResponse + * + * POST /api/v1/admin/workspaces/[id]/members + * + * Add a user to a workspace with a specific permission level. + * If the user already has permissions, updates their permission level. + * + * Body: + * - userId: string - User ID to add + * - permissions: 'admin' | 'write' | 'read' - Permission level + * + * Response: AdminSingleResponse + * + * DELETE /api/v1/admin/workspaces/[id]/members + * + * Remove a user from a workspace. + * + * Query Parameters: + * - userId: string - User ID to remove + * + * Response: AdminSingleResponse<{ removed: true }> + */ + +import crypto from 'crypto' +import { db } from '@sim/db' +import { permissions, user, workspace } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, count, eq } from 'drizzle-orm' +import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' +import { + badRequestResponse, + internalErrorResponse, + listResponse, + notFoundResponse, + singleResponse, +} from '@/app/api/v1/admin/responses' +import { + type AdminWorkspaceMember, + createPaginationMeta, + parsePaginationParams, +} from '@/app/api/v1/admin/types' + +const logger = createLogger('AdminWorkspaceMembersAPI') + +interface RouteParams { + id: string +} + +export const GET = withAdminAuthParams(async (request, context) => { + const { id: workspaceId } = await context.params + const url = new URL(request.url) + const { limit, offset } = parsePaginationParams(url) + + try { + const [workspaceData] = await db + .select({ id: workspace.id }) + .from(workspace) + .where(eq(workspace.id, workspaceId)) + .limit(1) + + if (!workspaceData) { + return notFoundResponse('Workspace') + } + + const [countResult, membersData] = await Promise.all([ + db + .select({ count: count() }) + .from(permissions) + .where(and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspaceId))), + db + .select({ + id: permissions.id, + userId: permissions.userId, + permissionType: permissions.permissionType, + createdAt: permissions.createdAt, + updatedAt: permissions.updatedAt, + userName: user.name, + userEmail: user.email, + userImage: user.image, + }) + .from(permissions) + .innerJoin(user, eq(permissions.userId, user.id)) + .where(and(eq(permissions.entityType, 'workspace'), eq(permissions.entityId, workspaceId))) + .orderBy(permissions.createdAt) + .limit(limit) + .offset(offset), + ]) + + const total = countResult[0].count + const data: AdminWorkspaceMember[] = membersData.map((m) => ({ + id: m.id, + workspaceId, + userId: m.userId, + permissions: m.permissionType, + createdAt: m.createdAt.toISOString(), + updatedAt: m.updatedAt.toISOString(), + userName: m.userName, + userEmail: m.userEmail, + userImage: m.userImage, + })) + + const pagination = createPaginationMeta(total, limit, offset) + + logger.info(`Admin API: Listed ${data.length} members for workspace ${workspaceId}`) + + return listResponse(data, pagination) + } catch (error) { + logger.error('Admin API: Failed to list workspace members', { error, workspaceId }) + return internalErrorResponse('Failed to list workspace members') + } +}) + +export const POST = withAdminAuthParams(async (request, context) => { + const { id: workspaceId } = await context.params + + try { + const body = await request.json() + + if (!body.userId || typeof body.userId !== 'string') { + return badRequestResponse('userId is required') + } + + if (!body.permissions || !['admin', 'write', 'read'].includes(body.permissions)) { + return badRequestResponse('permissions must be "admin", "write", or "read"') + } + + const [workspaceData] = await db + .select({ id: workspace.id, name: workspace.name }) + .from(workspace) + .where(eq(workspace.id, workspaceId)) + .limit(1) + + if (!workspaceData) { + return notFoundResponse('Workspace') + } + + const [userData] = await db + .select({ id: user.id, name: user.name, email: user.email, image: user.image }) + .from(user) + .where(eq(user.id, body.userId)) + .limit(1) + + if (!userData) { + return notFoundResponse('User') + } + + const [existingPermission] = await db + .select({ + id: permissions.id, + permissionType: permissions.permissionType, + createdAt: permissions.createdAt, + updatedAt: permissions.updatedAt, + }) + .from(permissions) + .where( + and( + eq(permissions.userId, body.userId), + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, workspaceId) + ) + ) + .limit(1) + + if (existingPermission) { + if (existingPermission.permissionType !== body.permissions) { + const now = new Date() + await db + .update(permissions) + .set({ permissionType: body.permissions, updatedAt: now }) + .where(eq(permissions.id, existingPermission.id)) + + logger.info( + `Admin API: Updated user ${body.userId} permissions in workspace ${workspaceId}`, + { + previousPermissions: existingPermission.permissionType, + newPermissions: body.permissions, + } + ) + + return singleResponse({ + id: existingPermission.id, + workspaceId, + userId: body.userId, + permissions: body.permissions as 'admin' | 'write' | 'read', + createdAt: existingPermission.createdAt.toISOString(), + updatedAt: now.toISOString(), + userName: userData.name, + userEmail: userData.email, + userImage: userData.image, + action: 'updated' as const, + }) + } + + return singleResponse({ + id: existingPermission.id, + workspaceId, + userId: body.userId, + permissions: existingPermission.permissionType, + createdAt: existingPermission.createdAt.toISOString(), + updatedAt: existingPermission.updatedAt.toISOString(), + userName: userData.name, + userEmail: userData.email, + userImage: userData.image, + action: 'already_member' as const, + }) + } + + const now = new Date() + const permissionId = crypto.randomUUID() + + await db.insert(permissions).values({ + id: permissionId, + userId: body.userId, + entityType: 'workspace', + entityId: workspaceId, + permissionType: body.permissions, + createdAt: now, + updatedAt: now, + }) + + logger.info(`Admin API: Added user ${body.userId} to workspace ${workspaceId}`, { + permissions: body.permissions, + permissionId, + }) + + return singleResponse({ + id: permissionId, + workspaceId, + userId: body.userId, + permissions: body.permissions as 'admin' | 'write' | 'read', + createdAt: now.toISOString(), + updatedAt: now.toISOString(), + userName: userData.name, + userEmail: userData.email, + userImage: userData.image, + action: 'created' as const, + }) + } catch (error) { + logger.error('Admin API: Failed to add workspace member', { error, workspaceId }) + return internalErrorResponse('Failed to add workspace member') + } +}) + +export const DELETE = withAdminAuthParams(async (request, context) => { + const { id: workspaceId } = await context.params + const url = new URL(request.url) + const userId = url.searchParams.get('userId') + + try { + if (!userId) { + return badRequestResponse('userId query parameter is required') + } + + const [workspaceData] = await db + .select({ id: workspace.id }) + .from(workspace) + .where(eq(workspace.id, workspaceId)) + .limit(1) + + if (!workspaceData) { + return notFoundResponse('Workspace') + } + + const [existingPermission] = await db + .select({ id: permissions.id }) + .from(permissions) + .where( + and( + eq(permissions.userId, userId), + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, workspaceId) + ) + ) + .limit(1) + + if (!existingPermission) { + return notFoundResponse('Workspace member') + } + + await db.delete(permissions).where(eq(permissions.id, existingPermission.id)) + + logger.info(`Admin API: Removed user ${userId} from workspace ${workspaceId}`) + + return singleResponse({ removed: true, userId, workspaceId }) + } catch (error) { + logger.error('Admin API: Failed to remove workspace member', { error, workspaceId, userId }) + return internalErrorResponse('Failed to remove workspace member') + } +}) diff --git a/apps/sim/app/api/wand/route.ts b/apps/sim/app/api/wand/route.ts index 69f79f4a9d..54f914a2b7 100644 --- a/apps/sim/app/api/wand/route.ts +++ b/apps/sim/app/api/wand/route.ts @@ -243,6 +243,11 @@ Use this context to calculate relative dates like "yesterday", "last week", "beg finalSystemPrompt += currentTimeContext } + if (generationType === 'json-object') { + finalSystemPrompt += + '\n\nIMPORTANT: Return ONLY the raw JSON object. Do NOT wrap it in markdown code blocks (no ```json or ```). Do NOT include any explanation or text before or after the JSON. The response must start with { and end with }.' + } + const messages: ChatMessage[] = [{ role: 'system', content: finalSystemPrompt }] messages.push(...history.filter((msg) => msg.role !== 'system')) diff --git a/apps/sim/app/api/webhooks/[id]/test-url/route.ts b/apps/sim/app/api/webhooks/[id]/test-url/route.ts deleted file mode 100644 index 7b27b2280c..0000000000 --- a/apps/sim/app/api/webhooks/[id]/test-url/route.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { db, webhook, workflow } from '@sim/db' -import { createLogger } from '@sim/logger' -import { eq } from 'drizzle-orm' -import { type NextRequest, NextResponse } from 'next/server' -import { getSession } from '@/lib/auth' -import { generateRequestId } from '@/lib/core/utils/request' -import { getBaseUrl } from '@/lib/core/utils/urls' -import { signTestWebhookToken } from '@/lib/webhooks/test-tokens' -import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' - -const logger = createLogger('MintWebhookTestUrlAPI') - -export const dynamic = 'force-dynamic' - -export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { - const requestId = generateRequestId() - try { - const session = await getSession() - if (!session?.user?.id) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - - const { id } = await params - const body = await request.json().catch(() => ({})) - const ttlSeconds = Math.max( - 60, - Math.min(60 * 60 * 24 * 30, Number(body?.ttlSeconds) || 60 * 60 * 24 * 7) - ) - - // Load webhook + workflow for permission check - const rows = await db - .select({ - webhook: webhook, - workflow: { - id: workflow.id, - userId: workflow.userId, - workspaceId: workflow.workspaceId, - }, - }) - .from(webhook) - .innerJoin(workflow, eq(webhook.workflowId, workflow.id)) - .where(eq(webhook.id, id)) - .limit(1) - - if (rows.length === 0) { - return NextResponse.json({ error: 'Webhook not found' }, { status: 404 }) - } - - const wf = rows[0].workflow - - // Permissions: owner OR workspace write/admin - let canMint = false - if (wf.userId === session.user.id) { - canMint = true - } else if (wf.workspaceId) { - const perm = await getUserEntityPermissions(session.user.id, 'workspace', wf.workspaceId) - if (perm === 'write' || perm === 'admin') { - canMint = true - } - } - - if (!canMint) { - logger.warn(`[${requestId}] User ${session.user.id} denied mint for webhook ${id}`) - return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) - } - - const token = await signTestWebhookToken(id, ttlSeconds) - const url = `${getBaseUrl()}/api/webhooks/test/${id}?token=${encodeURIComponent(token)}` - - logger.info(`[${requestId}] Minted test URL for webhook ${id}`) - return NextResponse.json({ - url, - expiresAt: new Date(Date.now() + ttlSeconds * 1000).toISOString(), - }) - } catch (error: any) { - logger.error('Error minting test webhook URL', error) - return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) - } -} diff --git a/apps/sim/app/api/webhooks/route.ts b/apps/sim/app/api/webhooks/route.ts index a26fe9933b..4e980646b9 100644 --- a/apps/sim/app/api/webhooks/route.ts +++ b/apps/sim/app/api/webhooks/route.ts @@ -793,6 +793,58 @@ export async function POST(request: NextRequest) { } // --- End Grain specific logic --- + // --- Lemlist specific logic --- + if (savedWebhook && provider === 'lemlist') { + logger.info( + `[${requestId}] Lemlist provider detected. Creating Lemlist webhook subscription.` + ) + try { + const lemlistResult = await createLemlistWebhookSubscription( + { + id: savedWebhook.id, + path: savedWebhook.path, + providerConfig: savedWebhook.providerConfig, + }, + requestId + ) + + if (lemlistResult) { + // Update the webhook record with the external Lemlist hook ID + const updatedConfig = { + ...(savedWebhook.providerConfig as Record), + externalId: lemlistResult.id, + } + await db + .update(webhook) + .set({ + providerConfig: updatedConfig, + updatedAt: new Date(), + }) + .where(eq(webhook.id, savedWebhook.id)) + + savedWebhook.providerConfig = updatedConfig + logger.info(`[${requestId}] Successfully created Lemlist webhook`, { + lemlistHookId: lemlistResult.id, + webhookId: savedWebhook.id, + }) + } + } catch (err) { + logger.error( + `[${requestId}] Error creating Lemlist webhook subscription, rolling back webhook`, + err + ) + await db.delete(webhook).where(eq(webhook.id, savedWebhook.id)) + return NextResponse.json( + { + error: 'Failed to create webhook in Lemlist', + details: err instanceof Error ? err.message : 'Unknown error', + }, + { status: 500 } + ) + } + } + // --- End Lemlist specific logic --- + if (!targetWebhookId && savedWebhook) { try { PlatformEvents.webhookCreated({ @@ -1316,3 +1368,116 @@ async function createGrainWebhookSubscription( throw error } } + +// Helper function to create the webhook subscription in Lemlist +async function createLemlistWebhookSubscription( + webhookData: any, + requestId: string +): Promise<{ id: string } | undefined> { + try { + const { path, providerConfig } = webhookData + const { apiKey, triggerId, campaignId } = providerConfig || {} + + if (!apiKey) { + logger.warn(`[${requestId}] Missing apiKey for Lemlist webhook creation.`, { + webhookId: webhookData.id, + }) + throw new Error( + 'Lemlist API Key is required. Please provide your Lemlist API Key in the trigger configuration.' + ) + } + + // Map trigger IDs to Lemlist event types + const eventTypeMap: Record = { + lemlist_email_replied: 'emailsReplied', + lemlist_linkedin_replied: 'linkedinReplied', + lemlist_interested: 'interested', + lemlist_not_interested: 'notInterested', + lemlist_email_opened: 'emailsOpened', + lemlist_email_clicked: 'emailsClicked', + lemlist_email_bounced: 'emailsBounced', + lemlist_email_sent: 'emailsSent', + lemlist_webhook: undefined, // Generic webhook - no type filter + } + + const eventType = eventTypeMap[triggerId] + + logger.info(`[${requestId}] Creating Lemlist webhook`, { + triggerId, + eventType, + hasCampaignId: !!campaignId, + webhookId: webhookData.id, + }) + + const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}` + + const lemlistApiUrl = 'https://api.lemlist.com/api/hooks' + + // Build request body + const requestBody: Record = { + targetUrl: notificationUrl, + } + + // Add event type if specified (omit for generic webhook to receive all events) + if (eventType) { + requestBody.type = eventType + } + + // Add campaign filter if specified + if (campaignId) { + requestBody.campaignId = campaignId + } + + // Lemlist uses Basic Auth with empty username and API key as password + const authString = Buffer.from(`:${apiKey}`).toString('base64') + + const lemlistResponse = await fetch(lemlistApiUrl, { + method: 'POST', + headers: { + Authorization: `Basic ${authString}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }) + + const responseBody = await lemlistResponse.json() + + if (!lemlistResponse.ok || responseBody.error) { + const errorMessage = responseBody.message || responseBody.error || 'Unknown Lemlist API error' + logger.error( + `[${requestId}] Failed to create webhook in Lemlist for webhook ${webhookData.id}. Status: ${lemlistResponse.status}`, + { message: errorMessage, response: responseBody } + ) + + let userFriendlyMessage = 'Failed to create webhook subscription in Lemlist' + if (lemlistResponse.status === 401) { + userFriendlyMessage = 'Invalid Lemlist API Key. Please verify your API Key is correct.' + } else if (lemlistResponse.status === 403) { + userFriendlyMessage = + 'Access denied. Please ensure your Lemlist API Key has appropriate permissions.' + } else if (errorMessage && errorMessage !== 'Unknown Lemlist API error') { + userFriendlyMessage = `Lemlist error: ${errorMessage}` + } + + throw new Error(userFriendlyMessage) + } + + logger.info( + `[${requestId}] Successfully created webhook in Lemlist for webhook ${webhookData.id}.`, + { + lemlistWebhookId: responseBody._id, + } + ) + + return { id: responseBody._id } + } catch (error: any) { + logger.error( + `[${requestId}] Exception during Lemlist webhook creation for webhook ${webhookData.id}.`, + { + message: error.message, + stack: error.stack, + } + ) + throw error + } +} diff --git a/apps/sim/app/api/webhooks/test/[id]/route.ts b/apps/sim/app/api/webhooks/test/[id]/route.ts deleted file mode 100644 index 46653c3bf7..0000000000 --- a/apps/sim/app/api/webhooks/test/[id]/route.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { createLogger } from '@sim/logger' -import { type NextRequest, NextResponse } from 'next/server' -import { generateRequestId } from '@/lib/core/utils/request' -import { - checkWebhookPreprocessing, - findWebhookAndWorkflow, - handleProviderChallenges, - parseWebhookBody, - queueWebhookExecution, - verifyProviderAuth, -} from '@/lib/webhooks/processor' -import { verifyTestWebhookToken } from '@/lib/webhooks/test-tokens' - -const logger = createLogger('WebhookTestReceiverAPI') - -export const dynamic = 'force-dynamic' -export const runtime = 'nodejs' - -export async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> }) { - const requestId = generateRequestId() - const webhookId = (await params).id - - logger.info(`[${requestId}] Test webhook request received for webhook ${webhookId}`) - - const parseResult = await parseWebhookBody(request, requestId) - if (parseResult instanceof NextResponse) { - return parseResult - } - - const { body, rawBody } = parseResult - - const challengeResponse = await handleProviderChallenges(body, request, requestId, '') - if (challengeResponse) { - return challengeResponse - } - - const url = new URL(request.url) - const token = url.searchParams.get('token') - - if (!token) { - logger.warn(`[${requestId}] Test webhook request missing token`) - return new NextResponse('Unauthorized', { status: 401 }) - } - - const isValid = await verifyTestWebhookToken(token, webhookId) - if (!isValid) { - logger.warn(`[${requestId}] Invalid test webhook token`) - return new NextResponse('Unauthorized', { status: 401 }) - } - - const result = await findWebhookAndWorkflow({ requestId, webhookId }) - if (!result) { - logger.warn(`[${requestId}] No active webhook found for id: ${webhookId}`) - return new NextResponse('Webhook not found', { status: 404 }) - } - - const { webhook: foundWebhook, workflow: foundWorkflow } = result - - const authError = await verifyProviderAuth( - foundWebhook, - foundWorkflow, - request, - rawBody, - requestId - ) - if (authError) { - return authError - } - - let preprocessError: NextResponse | null = null - try { - // Test webhooks skip deployment check but still enforce rate limits and usage limits - // They run on live/draft state to allow testing before deployment - preprocessError = await checkWebhookPreprocessing(foundWorkflow, foundWebhook, requestId, { - isTestMode: true, - }) - if (preprocessError) { - return preprocessError - } - } catch (error) { - logger.error(`[${requestId}] Unexpected error during webhook preprocessing`, { - error: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, - webhookId: foundWebhook.id, - workflowId: foundWorkflow.id, - }) - - if (foundWebhook.provider === 'microsoft-teams') { - return NextResponse.json( - { - type: 'message', - text: 'An unexpected error occurred during preprocessing', - }, - { status: 500 } - ) - } - - return NextResponse.json( - { error: 'An unexpected error occurred during preprocessing' }, - { status: 500 } - ) - } - - logger.info( - `[${requestId}] Executing TEST webhook for ${foundWebhook.provider} (workflow: ${foundWorkflow.id})` - ) - - return queueWebhookExecution(foundWebhook, foundWorkflow, body, request, { - requestId, - path: foundWebhook.path, - testMode: true, - executionTarget: 'live', - }) -} diff --git a/apps/sim/app/api/webhooks/test/route.ts b/apps/sim/app/api/webhooks/test/route.ts deleted file mode 100644 index bf3aece243..0000000000 --- a/apps/sim/app/api/webhooks/test/route.ts +++ /dev/null @@ -1,522 +0,0 @@ -import { db } from '@sim/db' -import { webhook } from '@sim/db/schema' -import { createLogger } from '@sim/logger' -import { eq } from 'drizzle-orm' -import { type NextRequest, NextResponse } from 'next/server' -import { generateRequestId } from '@/lib/core/utils/request' -import { getBaseUrl } from '@/lib/core/utils/urls' - -const logger = createLogger('WebhookTestAPI') - -export const dynamic = 'force-dynamic' - -export async function GET(request: NextRequest) { - const requestId = generateRequestId() - - try { - const { searchParams } = new URL(request.url) - const webhookId = searchParams.get('id') - - if (!webhookId) { - logger.warn(`[${requestId}] Missing webhook ID in test request`) - return NextResponse.json({ success: false, error: 'Webhook ID is required' }, { status: 400 }) - } - - logger.debug(`[${requestId}] Testing webhook with ID: ${webhookId}`) - - const webhooks = await db.select().from(webhook).where(eq(webhook.id, webhookId)).limit(1) - - if (webhooks.length === 0) { - logger.warn(`[${requestId}] Webhook not found: ${webhookId}`) - return NextResponse.json({ success: false, error: 'Webhook not found' }, { status: 404 }) - } - - const foundWebhook = webhooks[0] - const provider = foundWebhook.provider || 'generic' - const providerConfig = (foundWebhook.providerConfig as Record) || {} - - const webhookUrl = `${getBaseUrl()}/api/webhooks/trigger/${foundWebhook.path}` - - logger.info(`[${requestId}] Testing webhook for provider: ${provider}`, { - webhookId, - path: foundWebhook.path, - isActive: foundWebhook.isActive, - }) - - switch (provider) { - case 'whatsapp': { - const verificationToken = providerConfig.verificationToken - - if (!verificationToken) { - logger.warn(`[${requestId}] WhatsApp webhook missing verification token: ${webhookId}`) - return NextResponse.json( - { success: false, error: 'Webhook has no verification token' }, - { status: 400 } - ) - } - - const challenge = `test_${Date.now()}` - - const whatsappUrl = `${webhookUrl}?hub.mode=subscribe&hub.verify_token=${verificationToken}&hub.challenge=${challenge}` - - logger.debug(`[${requestId}] Testing WhatsApp webhook verification`, { - webhookId, - challenge, - }) - - const response = await fetch(whatsappUrl, { - headers: { - 'User-Agent': 'facebookplatform/1.0', - }, - }) - - const status = response.status - const contentType = response.headers.get('content-type') - const responseText = await response.text() - - const success = status === 200 && responseText === challenge - - if (success) { - logger.info(`[${requestId}] WhatsApp webhook verification successful: ${webhookId}`) - } else { - logger.warn(`[${requestId}] WhatsApp webhook verification failed: ${webhookId}`, { - status, - contentType, - responseTextLength: responseText.length, - }) - } - - return NextResponse.json({ - success, - webhook: { - id: foundWebhook.id, - url: webhookUrl, - verificationToken, - isActive: foundWebhook.isActive, - }, - test: { - status, - contentType, - responseText, - expectedStatus: 200, - expectedContentType: 'text/plain', - expectedResponse: challenge, - }, - message: success - ? 'Webhook configuration is valid. You can now use this URL in WhatsApp.' - : 'Webhook verification failed. Please check your configuration.', - diagnostics: { - statusMatch: status === 200 ? '✅ Status code is 200' : '❌ Status code should be 200', - contentTypeMatch: - contentType === 'text/plain' - ? '✅ Content-Type is text/plain' - : '❌ Content-Type should be text/plain', - bodyMatch: - responseText === challenge - ? '✅ Response body matches challenge' - : '❌ Response body should exactly match the challenge string', - }, - }) - } - - case 'telegram': { - const botToken = providerConfig.botToken - - if (!botToken) { - logger.warn(`[${requestId}] Telegram webhook missing configuration: ${webhookId}`) - return NextResponse.json( - { success: false, error: 'Webhook has incomplete configuration' }, - { status: 400 } - ) - } - - const testMessage = { - update_id: 12345, - message: { - message_id: 67890, - from: { - id: 123456789, - first_name: 'Test', - username: 'testbot', - }, - chat: { - id: 123456789, - first_name: 'Test', - username: 'testbot', - type: 'private', - }, - date: Math.floor(Date.now() / 1000), - text: 'This is a test message', - }, - } - - logger.debug(`[${requestId}] Testing Telegram webhook connection`, { - webhookId, - url: webhookUrl, - }) - - const response = await fetch(webhookUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'User-Agent': 'TelegramBot/1.0', - }, - body: JSON.stringify(testMessage), - }) - - const status = response.status - let responseText = '' - try { - responseText = await response.text() - } catch (_e) {} - - const success = status >= 200 && status < 300 - - if (success) { - logger.info(`[${requestId}] Telegram webhook test successful: ${webhookId}`) - } else { - logger.warn(`[${requestId}] Telegram webhook test failed: ${webhookId}`, { - status, - responseText, - }) - } - - let webhookInfo = null - try { - const webhookInfoUrl = `https://api.telegram.org/bot${botToken}/getWebhookInfo` - const infoResponse = await fetch(webhookInfoUrl, { - headers: { - 'User-Agent': 'TelegramBot/1.0', - }, - }) - if (infoResponse.ok) { - const infoJson = await infoResponse.json() - if (infoJson.ok) { - webhookInfo = infoJson.result - } - } - } catch (e) { - logger.warn(`[${requestId}] Failed to get Telegram webhook info`, e) - } - - const curlCommand = [ - `curl -X POST "${webhookUrl}"`, - `-H "Content-Type: application/json"`, - `-H "User-Agent: TelegramBot/1.0"`, - `-d '${JSON.stringify(testMessage, null, 2)}'`, - ].join(' \\\n') - - return NextResponse.json({ - success, - webhook: { - id: foundWebhook.id, - url: webhookUrl, - botToken: `${botToken.substring(0, 5)}...${botToken.substring(botToken.length - 5)}`, // Show partial token for security - isActive: foundWebhook.isActive, - }, - test: { - status, - responseText, - webhookInfo, - }, - message: success - ? 'Telegram webhook appears to be working. Any message sent to your bot will trigger the workflow.' - : 'Telegram webhook test failed. Please check server logs for more details.', - curlCommand, - info: 'To fix issues with Telegram webhooks getting 403 Forbidden responses, ensure the webhook request includes a User-Agent header.', - }) - } - - case 'github': { - const contentType = providerConfig.contentType || 'application/json' - - logger.info(`[${requestId}] GitHub webhook test successful: ${webhookId}`) - return NextResponse.json({ - success: true, - webhook: { - id: foundWebhook.id, - url: webhookUrl, - contentType, - isActive: foundWebhook.isActive, - }, - message: - 'GitHub webhook configuration is valid. Use this URL in your GitHub repository settings.', - setup: { - url: webhookUrl, - contentType, - events: ['push', 'pull_request', 'issues', 'issue_comment'], - }, - }) - } - - case 'stripe': { - logger.info(`[${requestId}] Stripe webhook test successful: ${webhookId}`) - return NextResponse.json({ - success: true, - webhook: { - id: foundWebhook.id, - url: webhookUrl, - isActive: foundWebhook.isActive, - }, - message: 'Stripe webhook configuration is valid. Use this URL in your Stripe dashboard.', - setup: { - url: webhookUrl, - events: [ - 'charge.succeeded', - 'invoice.payment_succeeded', - 'customer.subscription.created', - ], - }, - }) - } - - case 'generic': { - const token = providerConfig.token - const secretHeaderName = providerConfig.secretHeaderName - const requireAuth = providerConfig.requireAuth - const allowedIps = providerConfig.allowedIps - - let curlCommand = `curl -X POST "${webhookUrl}" -H "Content-Type: application/json"` - - if (requireAuth && token) { - if (secretHeaderName) { - curlCommand += ` -H "${secretHeaderName}: ${token}"` - } else { - curlCommand += ` -H "Authorization: Bearer ${token}"` - } - } - - curlCommand += ` -d '{"event":"test_event","timestamp":"${new Date().toISOString()}"}'` - - logger.info(`[${requestId}] General webhook test successful: ${webhookId}`) - return NextResponse.json({ - success: true, - webhook: { - id: foundWebhook.id, - url: webhookUrl, - isActive: foundWebhook.isActive, - }, - message: - 'General webhook configuration is valid. Use the URL and authentication details as needed.', - details: { - requireAuth: requireAuth || false, - hasToken: !!token, - hasCustomHeader: !!secretHeaderName, - customHeaderName: secretHeaderName, - hasIpRestrictions: Array.isArray(allowedIps) && allowedIps.length > 0, - }, - test: { - curlCommand, - headers: requireAuth - ? secretHeaderName - ? { [secretHeaderName]: token } - : { Authorization: `Bearer ${token}` } - : {}, - samplePayload: { - event: 'test_event', - timestamp: new Date().toISOString(), - }, - }, - }) - } - - case 'slack': { - const signingSecret = providerConfig.signingSecret - - if (!signingSecret) { - logger.warn(`[${requestId}] Slack webhook missing signing secret: ${webhookId}`) - return NextResponse.json( - { success: false, error: 'Webhook has no signing secret configured' }, - { status: 400 } - ) - } - - logger.info(`[${requestId}] Slack webhook test successful: ${webhookId}`) - return NextResponse.json({ - success: true, - webhook: { - id: foundWebhook.id, - url: webhookUrl, - isActive: foundWebhook.isActive, - }, - message: - 'Slack webhook configuration is valid. Use this URL in your Slack Event Subscriptions settings.', - setup: { - url: webhookUrl, - events: ['message.channels', 'reaction_added', 'app_mention'], - signingSecretConfigured: true, - }, - test: { - curlCommand: [ - `curl -X POST "${webhookUrl}"`, - `-H "Content-Type: application/json"`, - `-H "X-Slack-Request-Timestamp: $(date +%s)"`, - `-H "X-Slack-Signature: v0=$(date +%s)"`, - `-d '{"type":"event_callback","event":{"type":"message","channel":"C0123456789","user":"U0123456789","text":"Hello from Slack!","ts":"1234567890.123456"},"team_id":"T0123456789"}'`, - ].join(' \\\n'), - samplePayload: { - type: 'event_callback', - token: 'XXYYZZ', - team_id: 'T123ABC', - event: { - type: 'message', - user: 'U123ABC', - text: 'Hello from Slack!', - ts: '1234567890.1234', - }, - event_id: 'Ev123ABC', - }, - }, - }) - } - - case 'airtable': { - const baseId = providerConfig.baseId - const tableId = providerConfig.tableId - const webhookSecret = providerConfig.webhookSecret - - if (!baseId || !tableId) { - logger.warn(`[${requestId}] Airtable webhook missing Base ID or Table ID: ${webhookId}`) - return NextResponse.json( - { - success: false, - error: 'Webhook configuration is incomplete (missing Base ID or Table ID)', - }, - { status: 400 } - ) - } - - const samplePayload = { - webhook: { - id: 'whiYOUR_WEBHOOK_ID', - }, - base: { - id: baseId, - }, - payloadFormat: 'v0', - actionMetadata: { - source: 'tableOrViewChange', - sourceMetadata: {}, - }, - payloads: [ - { - timestamp: new Date().toISOString(), - baseTransactionNumber: Date.now(), - changedTablesById: { - [tableId]: { - changedRecordsById: { - recSAMPLEID1: { - current: { cellValuesByFieldId: { fldSAMPLEID: 'New Value' } }, - previous: { cellValuesByFieldId: { fldSAMPLEID: 'Old Value' } }, - }, - }, - changedFieldsById: {}, - changedViewsById: {}, - }, - }, - }, - ], - } - - let curlCommand = `curl -X POST "${webhookUrl}" -H "Content-Type: application/json"` - curlCommand += ` -d '${JSON.stringify(samplePayload, null, 2)}'` - - logger.info(`[${requestId}] Airtable webhook test successful: ${webhookId}`) - return NextResponse.json({ - success: true, - webhook: { - id: foundWebhook.id, - url: webhookUrl, - baseId: baseId, - tableId: tableId, - secretConfigured: !!webhookSecret, - isActive: foundWebhook.isActive, - }, - message: - 'Airtable webhook configuration appears valid. Use the sample curl command to manually send a test payload to your webhook URL.', - test: { - curlCommand: curlCommand, - samplePayload: samplePayload, - }, - }) - } - - case 'microsoft-teams': { - const hmacSecret = providerConfig.hmacSecret - - if (!hmacSecret) { - logger.warn(`[${requestId}] Microsoft Teams webhook missing HMAC secret: ${webhookId}`) - return NextResponse.json( - { success: false, error: 'Microsoft Teams webhook requires HMAC secret' }, - { status: 400 } - ) - } - - logger.info(`[${requestId}] Microsoft Teams webhook test successful: ${webhookId}`) - return NextResponse.json({ - success: true, - webhook: { - id: foundWebhook.id, - url: webhookUrl, - isActive: foundWebhook.isActive, - }, - message: 'Microsoft Teams outgoing webhook configuration is valid.', - setup: { - url: webhookUrl, - hmacSecretConfigured: !!hmacSecret, - instructions: [ - 'Create an outgoing webhook in Microsoft Teams', - 'Set the callback URL to the webhook URL above', - 'Copy the HMAC security token to the configuration', - 'Users can trigger the webhook by @mentioning it in Teams', - ], - }, - test: { - curlCommand: `curl -X POST "${webhookUrl}" \\ - -H "Content-Type: application/json" \\ - -H "Authorization: HMAC " \\ - -d '{"type":"message","text":"Hello from Microsoft Teams!","from":{"id":"test","name":"Test User"}}'`, - samplePayload: { - type: 'message', - id: '1234567890', - timestamp: new Date().toISOString(), - text: 'Hello Sim Bot!', - from: { - id: '29:1234567890abcdef', - name: 'Test User', - }, - conversation: { - id: '19:meeting_abcdef@thread.v2', - }, - }, - }, - }) - } - - default: { - logger.info(`[${requestId}] Generic webhook test successful: ${webhookId}`) - return NextResponse.json({ - success: true, - webhook: { - id: foundWebhook.id, - url: webhookUrl, - provider: foundWebhook.provider, - isActive: foundWebhook.isActive, - }, - message: - 'Webhook configuration is valid. You can use this URL to receive webhook events.', - }) - } - } - } catch (error: any) { - logger.error(`[${requestId}] Error testing webhook`, error) - return NextResponse.json( - { - success: false, - error: 'Test failed', - message: error.message, - }, - { status: 500 } - ) - } -} diff --git a/apps/sim/app/api/webhooks/trigger/[path]/route.ts b/apps/sim/app/api/webhooks/trigger/[path]/route.ts index f3aba1e7b4..ae11e476cf 100644 --- a/apps/sim/app/api/webhooks/trigger/[path]/route.ts +++ b/apps/sim/app/api/webhooks/trigger/[path]/route.ts @@ -152,7 +152,6 @@ export async function POST( const response = await queueWebhookExecution(foundWebhook, foundWorkflow, body, request, { requestId, path, - testMode: false, executionTarget: 'deployed', }) responses.push(response) diff --git a/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts b/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts index 84be273d12..246cc6b245 100644 --- a/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts +++ b/apps/sim/app/api/workspaces/[id]/byok-keys/route.ts @@ -6,8 +6,6 @@ import { nanoid } from 'nanoid' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' -import { isEnterpriseOrgAdminOrOwner } from '@/lib/billing/core/subscription' -import { isHosted } from '@/lib/core/config/feature-flags' import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption' import { generateRequestId } from '@/lib/core/utils/request' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' @@ -58,15 +56,6 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } - let byokEnabled = true - if (isHosted) { - byokEnabled = await isEnterpriseOrgAdminOrOwner(userId) - } - - if (!byokEnabled) { - return NextResponse.json({ keys: [], byokEnabled: false }) - } - const byokKeys = await db .select({ id: workspaceBYOKKeys.id, @@ -108,7 +97,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<{ }) ) - return NextResponse.json({ keys: formattedKeys, byokEnabled: true }) + return NextResponse.json({ keys: formattedKeys }) } catch (error: unknown) { logger.error(`[${requestId}] BYOK keys GET error`, error) return NextResponse.json( @@ -131,20 +120,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{ const userId = session.user.id - if (isHosted) { - const canManageBYOK = await isEnterpriseOrgAdminOrOwner(userId) - if (!canManageBYOK) { - logger.warn(`[${requestId}] User not authorized to manage BYOK keys`, { userId }) - return NextResponse.json( - { - error: - 'BYOK is an Enterprise-only feature. Only organization admins and owners can manage API keys.', - }, - { status: 403 } - ) - } - } - const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId) if (permission !== 'admin') { return NextResponse.json( @@ -245,20 +220,6 @@ export async function DELETE( const userId = session.user.id - if (isHosted) { - const canManageBYOK = await isEnterpriseOrgAdminOrOwner(userId) - if (!canManageBYOK) { - logger.warn(`[${requestId}] User not authorized to manage BYOK keys`, { userId }) - return NextResponse.json( - { - error: - 'BYOK is an Enterprise-only feature. Only organization admins and owners can manage API keys.', - }, - { status: 403 } - ) - } - } - const permission = await getUserEntityPermissions(userId, 'workspace', workspaceId) if (permission !== 'admin') { return NextResponse.json( diff --git a/apps/sim/app/api/workspaces/invitations/route.test.ts b/apps/sim/app/api/workspaces/invitations/route.test.ts index b47e7bf362..f56e9d0120 100644 --- a/apps/sim/app/api/workspaces/invitations/route.test.ts +++ b/apps/sim/app/api/workspaces/invitations/route.test.ts @@ -101,6 +101,16 @@ describe('Workspace Invitations API Route', () => { eq: vi.fn().mockImplementation((field, value) => ({ type: 'eq', field, value })), inArray: vi.fn().mockImplementation((field, values) => ({ type: 'inArray', field, values })), })) + + vi.doMock('@/executor/utils/permission-check', () => ({ + validateInvitationsAllowed: vi.fn().mockResolvedValue(undefined), + InvitationsNotAllowedError: class InvitationsNotAllowedError extends Error { + constructor() { + super('Invitations are not allowed based on your permission group settings') + this.name = 'InvitationsNotAllowedError' + } + }, + })) }) describe('GET /api/workspaces/invitations', () => { diff --git a/apps/sim/app/api/workspaces/invitations/route.ts b/apps/sim/app/api/workspaces/invitations/route.ts index 06ad14d34d..bd70b9dc9c 100644 --- a/apps/sim/app/api/workspaces/invitations/route.ts +++ b/apps/sim/app/api/workspaces/invitations/route.ts @@ -18,6 +18,10 @@ import { PlatformEvents } from '@/lib/core/telemetry' import { getBaseUrl } from '@/lib/core/utils/urls' import { sendEmail } from '@/lib/messaging/email/mailer' import { getFromEmailAddress } from '@/lib/messaging/email/utils' +import { + InvitationsNotAllowedError, + validateInvitationsAllowed, +} from '@/executor/utils/permission-check' export const dynamic = 'force-dynamic' @@ -76,6 +80,8 @@ export async function POST(req: NextRequest) { } try { + await validateInvitationsAllowed(session.user.id) + const { workspaceId, email, role = 'member', permission = 'read' } = await req.json() if (!workspaceId || !email) { @@ -213,6 +219,9 @@ export async function POST(req: NextRequest) { return NextResponse.json({ success: true, invitation: invitationData }) } catch (error) { + if (error instanceof InvitationsNotAllowedError) { + return NextResponse.json({ error: error.message }, { status: 403 }) + } logger.error('Error creating workspace invitation:', error) return NextResponse.json({ error: 'Failed to create invitation' }, { status: 500 }) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/pane-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/pane-context-menu.tsx index e7a98f1e8a..a5bba68b46 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/pane-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/pane-context-menu.tsx @@ -34,6 +34,7 @@ export function PaneContextMenu({ disableAdmin = false, canUndo = false, canRedo = false, + isInvitationsDisabled = false, }: PaneContextMenuProps) { return ( - {/* Admin action */} - - { - onInvite() - onClose() - }} - > - Invite to Workspace - + {/* Admin action - hidden when invitations are disabled */} + {!isInvitationsDisabled && ( + <> + + { + onInvite() + onClose() + }} + > + Invite to Workspace + + + )} ) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/types.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/types.ts index 53b5246cc4..ed0ecd26ee 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/types.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/types.ts @@ -94,4 +94,6 @@ export interface PaneContextMenuProps { canUndo?: boolean /** Whether redo is available */ canRedo?: boolean + /** Whether invitations are disabled (feature flag or permission group) */ + isInvitationsDisabled?: boolean } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer.tsx index dd9e45cfcc..dcc2dffd06 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer.tsx @@ -326,8 +326,8 @@ export default function CopilotMarkdownRenderer({ content }: CopilotMarkdownRend ), table: ({ children }: React.TableHTMLAttributes) => ( -
- +
+
{children}
@@ -346,12 +346,12 @@ export default function CopilotMarkdownRenderer({ content }: CopilotMarkdownRend ), th: ({ children }: React.ThHTMLAttributes) => ( - + {children} ), td: ({ children }: React.TdHTMLAttributes) => ( - + {children} ), diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block.tsx index e1bfda0baa..54c7042e75 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/thinking-block.tsx @@ -246,7 +246,7 @@ export function ThinkingBlock({ )} > {/* Render markdown during streaming with thinking text styling */} -
+
@@ -286,7 +286,7 @@ export function ThinkingBlock({ )} > {/* Use markdown renderer for completed content */} -
+
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx index 4f921c898e..c2fdf92647 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/tool-call/tool-call.tsx @@ -1827,7 +1827,8 @@ function getStateVerb(state: string): string { * e.g., "google_calendar_list_events" -> "Google Calendar List Events" */ function formatToolName(name: string): string { - return name + const baseName = name.replace(/_v\d+$/, '') + return baseName .split('_') .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(' ') diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx index fd0c69277e..b33427bab9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown.tsx @@ -240,7 +240,7 @@ const generateOutputPaths = (outputs: Record, prefix = ''): string[ } else if (typeof value === 'object' && value !== null) { if ('type' in value && typeof value.type === 'string') { const hasNestedProperties = - (value.type === 'object' && value.properties) || + ((value.type === 'object' || value.type === 'json') && value.properties) || (value.type === 'array' && value.items?.properties) || (value.type === 'array' && value.items && @@ -251,7 +251,7 @@ const generateOutputPaths = (outputs: Record, prefix = ''): string[ paths.push(currentPath) } - if (value.type === 'object' && value.properties) { + if ((value.type === 'object' || value.type === 'json') && value.properties) { paths.push(...generateOutputPaths(value.properties, currentPath)) } else if (value.type === 'array' && value.items?.properties) { paths.push(...generateOutputPaths(value.items.properties, currentPath)) @@ -303,8 +303,8 @@ const generateOutputPathsWithTypes = ( paths.push({ path: currentPath, type: 'array' }) const subPaths = generateOutputPathsWithTypes(value.items.properties, currentPath) paths.push(...subPaths) - } else if (value.type === 'object' && value.properties) { - paths.push({ path: currentPath, type: 'object' }) + } else if ((value.type === 'object' || value.type === 'json') && value.properties) { + paths.push({ path: currentPath, type: value.type }) const subPaths = generateOutputPathsWithTypes(value.properties, currentPath) paths.push(...subPaths) } else { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index 46228def69..26b125591e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -16,7 +16,7 @@ import { Switch, Tooltip, } from '@/components/emcn' -import { McpIcon } from '@/components/icons' +import { McpIcon, WorkflowIcon } from '@/components/icons' import { cn } from '@/lib/core/utils/cn' import { getIssueBadgeLabel, @@ -30,6 +30,7 @@ import { type OAuthProvider, type OAuthService, } from '@/lib/oauth' +import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { CheckboxList, Code, @@ -769,9 +770,10 @@ function WorkflowToolDeployBadge({ }) { const { isDeployed, needsRedeploy, isLoading, refetch } = useChildDeployment(workflowId) const [isDeploying, setIsDeploying] = useState(false) + const userPermissions = useUserPermissionsContext() const deployWorkflow = useCallback(async () => { - if (isDeploying || !workflowId) return + if (isDeploying || !workflowId || !userPermissions.canAdmin) return try { setIsDeploying(true) @@ -796,7 +798,7 @@ function WorkflowToolDeployBadge({ } finally { setIsDeploying(false) } - }, [isDeploying, workflowId, refetch, onDeploySuccess]) + }, [isDeploying, workflowId, refetch, onDeploySuccess, userPermissions.canAdmin]) if (isLoading || (isDeployed && !needsRedeploy)) { return null @@ -811,13 +813,13 @@ function WorkflowToolDeployBadge({ { e.stopPropagation() e.preventDefault() - if (!isDeploying) { + if (!isDeploying && userPermissions.canAdmin) { deployWorkflow() } }} @@ -826,7 +828,13 @@ function WorkflowToolDeployBadge({ - {!isDeployed ? 'Click to deploy' : 'Click to redeploy'} + + {!userPermissions.canAdmin + ? 'Admin permission required to deploy' + : !isDeployed + ? 'Click to deploy' + : 'Click to redeploy'} + ) @@ -933,6 +941,13 @@ export function ToolInput({ const forceRefreshMcpTools = useForceRefreshMcpTools() const openSettingsModal = useSettingsModalStore((state) => state.openModal) const mcpDataLoading = mcpLoading || mcpServersLoading + + // Fetch workflows for the Workflows section in the dropdown + const { data: workflowsList = [] } = useWorkflows(workspaceId, { syncRegistry: false }) + const availableWorkflows = useMemo( + () => workflowsList.filter((w) => w.id !== workflowId), + [workflowsList, workflowId] + ) const hasRefreshedRef = useRef(false) const hasMcpTools = selectedTools.some((tool) => tool.type === 'mcp') @@ -1016,6 +1031,7 @@ export function ToolInput({ const toolBlocks = useMemo(() => { const allToolBlocks = getAllBlocks().filter( (block) => + !block.hideFromToolbar && (block.category === 'tools' || block.type === 'api' || block.type === 'webhook_request' || @@ -1735,6 +1751,36 @@ export function ToolInput({ }) } + // Workflows section - shows available workflows that can be executed as tools + if (availableWorkflows.length > 0) { + groups.push({ + section: 'Workflows', + items: availableWorkflows.map((workflow) => ({ + label: workflow.name, + value: `workflow-${workflow.id}`, + iconElement: createToolIcon('#6366F1', WorkflowIcon), + onSelect: () => { + const newTool: StoredTool = { + type: 'workflow', + title: 'Workflow', + toolId: 'workflow_executor', + params: { + workflowId: workflow.id, + }, + isExpanded: true, + usageControl: 'auto', + } + setStoreValue([ + ...selectedTools.map((tool) => ({ ...tool, isExpanded: false })), + newTool, + ]) + setOpen(false) + }, + disabled: isPreview || disabled, + })), + }) + } + return groups }, [ customTools, @@ -1749,6 +1795,7 @@ export function ToolInput({ handleSelectTool, permissionConfig.disableCustomTools, permissionConfig.disableMcpTools, + availableWorkflows, ]) const toolRequiresOAuth = (toolId: string): boolean => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/trigger-save/trigger-save.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/trigger-save/trigger-save.tsx index b66463ca95..4fb9e306aa 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/trigger-save/trigger-save.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/trigger-save/trigger-save.tsx @@ -16,7 +16,6 @@ import { useWebhookManagement } from '@/hooks/use-webhook-management' import { useSubBlockStore } from '@/stores/workflows/subblock/store' import { getTrigger, isTriggerValid } from '@/triggers' import { SYSTEM_SUBBLOCK_IDS } from '@/triggers/constants' -import { ShortInput } from '../short-input/short-input' const logger = createLogger('TriggerSave') @@ -41,22 +40,6 @@ export function TriggerSave({ const [errorMessage, setErrorMessage] = useState(null) const [deleteStatus, setDeleteStatus] = useState<'idle' | 'deleting'>('idle') const [showDeleteDialog, setShowDeleteDialog] = useState(false) - const [isGeneratingTestUrl, setIsGeneratingTestUrl] = useState(false) - - const storedTestUrl = useSubBlockStore((state) => state.getValue(blockId, 'testUrl')) as - | string - | null - const storedTestUrlExpiresAt = useSubBlockStore((state) => - state.getValue(blockId, 'testUrlExpiresAt') - ) as string | null - - const isTestUrlExpired = useMemo(() => { - if (!storedTestUrlExpiresAt) return true - return new Date(storedTestUrlExpiresAt) < new Date() - }, [storedTestUrlExpiresAt]) - - const testUrl = isTestUrlExpired ? null : (storedTestUrl as string | null) - const testUrlExpiresAt = isTestUrlExpired ? null : (storedTestUrlExpiresAt as string | null) const effectiveTriggerId = useMemo(() => { if (triggerId && isTriggerValid(triggerId)) { @@ -86,9 +69,6 @@ export function TriggerSave({ const triggerDef = effectiveTriggerId && isTriggerValid(effectiveTriggerId) ? getTrigger(effectiveTriggerId) : null - const hasWebhookUrlDisplay = - triggerDef?.subBlocks.some((sb) => sb.id === 'webhookUrlDisplay') ?? false - const validateRequiredFields = useCallback( ( configToCheck: Record | null | undefined @@ -212,13 +192,6 @@ export function TriggerSave({ validateRequiredFields, ]) - useEffect(() => { - if (isTestUrlExpired && storedTestUrl) { - useSubBlockStore.getState().setValue(blockId, 'testUrl', null) - useSubBlockStore.getState().setValue(blockId, 'testUrlExpiresAt', null) - } - }, [blockId, isTestUrlExpired, storedTestUrl]) - const handleSave = async () => { if (isPreview || disabled) return @@ -278,34 +251,6 @@ export function TriggerSave({ } } - const generateTestUrl = async () => { - if (!webhookId) return - try { - setIsGeneratingTestUrl(true) - const res = await fetch(`/api/webhooks/${webhookId}/test-url`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({}), - }) - if (!res.ok) { - const err = await res.json().catch(() => ({})) - throw new Error(err?.error || 'Failed to generate test URL') - } - const json = await res.json() - useSubBlockStore.getState().setValue(blockId, 'testUrl', json.url) - useSubBlockStore.getState().setValue(blockId, 'testUrlExpiresAt', json.expiresAt) - collaborativeSetSubblockValue(blockId, 'testUrl', json.url) - collaborativeSetSubblockValue(blockId, 'testUrlExpiresAt', json.expiresAt) - } catch (e) { - logger.error('Failed to generate test webhook URL', { error: e }) - setErrorMessage( - e instanceof Error ? e.message : 'Failed to generate test URL. Please try again.' - ) - } finally { - setIsGeneratingTestUrl(false) - } - } - const handleDeleteClick = () => { if (isPreview || disabled || !webhookId) return setShowDeleteDialog(true) @@ -324,14 +269,9 @@ export function TriggerSave({ setSaveStatus('idle') setErrorMessage(null) - useSubBlockStore.getState().setValue(blockId, 'testUrl', null) - useSubBlockStore.getState().setValue(blockId, 'testUrlExpiresAt', null) - collaborativeSetSubblockValue(blockId, 'triggerPath', '') collaborativeSetSubblockValue(blockId, 'webhookId', null) collaborativeSetSubblockValue(blockId, 'triggerConfig', null) - collaborativeSetSubblockValue(blockId, 'testUrl', null) - collaborativeSetSubblockValue(blockId, 'testUrlExpiresAt', null) logger.info('Trigger configuration deleted successfully', { blockId, @@ -383,51 +323,6 @@ export function TriggerSave({ {errorMessage &&

{errorMessage}

} - {webhookId && hasWebhookUrlDisplay && ( -
-
- - Test Webhook URL - - -
- {testUrl ? ( - <> - - {testUrlExpiresAt && ( -

- Expires {new Date(testUrlExpiresAt).toLocaleString()} -

- )} - - ) : ( -

- Generate a temporary URL to test against the live (undeployed) workflow state. -

- )} -
- )} - Delete Trigger diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index 46b0451483..9e915ce18c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -108,7 +108,7 @@ export function Panel() { // Delete workflow hook const { isDeleting, handleDeleteWorkflow } = useDeleteWorkflow({ workspaceId, - getWorkflowIds: () => activeWorkflowId || '', + workflowIds: activeWorkflowId || '', isActive: true, onSuccess: () => setIsDeleteModalOpen(false), }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index fbd93d2491..b98eb7d390 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -35,7 +35,6 @@ import { getDependsOnFields } from '@/blocks/utils' import { useKnowledgeBase } from '@/hooks/kb/use-knowledge' import { useMcpServers, useMcpToolsQuery } from '@/hooks/queries/mcp' import { useCredentialName } from '@/hooks/queries/oauth-credentials' -import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' import { useSelectorDisplayName } from '@/hooks/use-selector-display-name' import { useVariablesStore } from '@/stores/panel' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' @@ -613,34 +612,6 @@ export const WorkflowBlock = memo(function WorkflowBlock({ [isDeploying, setDeploymentStatus, refetchDeployment] ) - const { collaborativeSetSubblockValue } = useCollaborativeWorkflow() - - /** - * Clear credential-dependent fields when credential changes to prevent - * stale data from persisting with new credentials. - */ - const prevCredRef = useRef(undefined) - useEffect(() => { - const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId - if (!activeWorkflowId) return - const current = useSubBlockStore.getState().workflowValues[activeWorkflowId]?.[id] - if (!current) return - const credValue = current.credential - const cred = - typeof credValue === 'object' && credValue !== null && 'value' in credValue - ? ((credValue as { value?: unknown }).value as string | undefined) - : (credValue as string | undefined) - if (prevCredRef.current !== cred) { - const hadPreviousCredential = prevCredRef.current !== undefined - prevCredRef.current = cred - if (hadPreviousCredential) { - const keys = Object.keys(current) - const dependentKeys = keys.filter((k) => k !== 'credential') - dependentKeys.forEach((k) => collaborativeSetSubblockValue(id, k, '')) - } - } - }, [id, collaborativeSetSubblockValue]) - const currentStoreBlock = currentWorkflow.getBlockById(id) const isStarterBlock = type === 'starter' @@ -1021,11 +992,11 @@ export const WorkflowBlock = memo(function WorkflowBlock({ { e.stopPropagation() - if (childWorkflowId && !isDeploying) { + if (childWorkflowId && !isDeploying && userPermissions.canAdmin) { deployWorkflow(childWorkflowId) } }} @@ -1035,7 +1006,11 @@ export const WorkflowBlock = memo(function WorkflowBlock({ - {!childIsDeployed ? 'Click to deploy' : 'Click to redeploy'} + {!userPermissions.canAdmin + ? 'Admin permission required to deploy' + : !childIsDeployed + ? 'Click to deploy' + : 'Click to redeploy'} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-ring-utils.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-ring-utils.ts index 1490d6040b..634d28a86e 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-ring-utils.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/utils/block-ring-utils.ts @@ -50,7 +50,7 @@ export function getBlockRingStyles(options: BlockRingOptions): { !isPending && !isDeletedBlock && diffStatus === 'new' && - 'ring-[var(--brand-tertiary)]', + 'ring-[var(--brand-tertiary-2)]', !isActive && !isPending && !isDeletedBlock && diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx index bbaad51aa8..2ac4f3cb72 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx @@ -64,6 +64,7 @@ import { getBlock } from '@/blocks' import { isAnnotationOnlyBlock } from '@/executor/constants' import { useWorkspaceEnvironment } from '@/hooks/queries/environment' import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow' +import { usePermissionConfig } from '@/hooks/use-permission-config' import { useStreamCleanup } from '@/hooks/use-stream-cleanup' import { useChatStore } from '@/stores/chat/store' import { useCopilotTrainingStore } from '@/stores/copilot-training/store' @@ -281,6 +282,9 @@ const WorkflowContent = React.memo(() => { // Panel open states for context menu const isVariablesOpen = useVariablesStore((state) => state.isOpen) const isChatOpen = useChatStore((state) => state.isChatOpen) + + // Permission config for invitation control + const { isInvitationsDisabled } = usePermissionConfig() const snapGrid: [number, number] = useMemo( () => [snapToGridSize, snapToGridSize], [snapToGridSize] @@ -3426,6 +3430,7 @@ const WorkflowContent = React.memo(() => { disableAdmin={!effectivePermissions.canAdmin} canUndo={canUndo} canRedo={canRedo} + isInvitationsDisabled={isInvitationsDisabled} /> )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx index ee43fefbf9..0d8fd312cc 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx @@ -182,7 +182,7 @@ export function SearchModal({ const allBlocks = getAllBlocks() const filteredAllBlocks = filterBlocks(allBlocks) return filteredAllBlocks - .filter((block) => block.category === 'tools') + .filter((block) => !block.hideFromToolbar && block.category === 'tools') .map( (block): ToolItem => ({ id: block.type, @@ -225,7 +225,7 @@ export function SearchModal({ const docsItems: DocItem[] = [] allBlocks.forEach((block) => { - if (block.docsLink) { + if (block.docsLink && !block.hideFromToolbar) { docsItems.push({ id: `docs-${block.type}`, name: block.name, diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx index 034f028290..fc7bf1dcda 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx @@ -342,6 +342,12 @@ export function AccessControl() { category: 'Logs', configKey: 'hideTraceSpans' as const, }, + { + id: 'disable-invitations', + label: 'Invitations', + category: 'Collaboration', + configKey: 'disableInvitations' as const, + }, ], [] ) @@ -869,7 +875,8 @@ export function AccessControl() { !editingConfig?.hideFilesTab && !editingConfig?.disableMcpTools && !editingConfig?.disableCustomTools && - !editingConfig?.hideTraceSpans + !editingConfig?.hideTraceSpans && + !editingConfig?.disableInvitations setEditingConfig((prev) => prev ? { @@ -883,6 +890,7 @@ export function AccessControl() { disableMcpTools: allVisible, disableCustomTools: allVisible, hideTraceSpans: allVisible, + disableInvitations: allVisible, } : prev ) @@ -896,7 +904,8 @@ export function AccessControl() { !editingConfig?.hideFilesTab && !editingConfig?.disableMcpTools && !editingConfig?.disableCustomTools && - !editingConfig?.hideTraceSpans + !editingConfig?.hideTraceSpans && + !editingConfig?.disableInvitations ? 'Deselect All' : 'Select All'} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/byok/byok.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/byok/byok.tsx index e06ab18c3b..867c128f60 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/byok/byok.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/byok/byok.tsx @@ -2,7 +2,7 @@ import { useState } from 'react' import { createLogger } from '@sim/logger' -import { Crown, Eye, EyeOff } from 'lucide-react' +import { Eye, EyeOff } from 'lucide-react' import { useParams } from 'next/navigation' import { Button, @@ -83,7 +83,6 @@ export function BYOK() { const { data, isLoading } = useBYOKKeys(workspaceId) const keys = data?.keys ?? [] - const byokEnabled = data?.byokEnabled ?? true const upsertKey = useUpsertBYOKKey() const deleteKey = useDeleteBYOKKey() @@ -98,31 +97,6 @@ export function BYOK() { return keys.find((k) => k.providerId === providerId) } - // Show enterprise-only gate if BYOK is not enabled - if (!isLoading && !byokEnabled) { - return ( -
-
- -
-
-

Enterprise Feature

-

- Bring Your Own Key (BYOK) is available exclusively on the Enterprise plan. Upgrade to - use your own API keys and eliminate the 2x cost multiplier. -

-
- -
- ) - } - const handleSave = async () => { if (!editingProvider || !apiKeyInput.trim()) return @@ -340,7 +314,7 @@ export function BYOK() { {PROVIDERS.find((p) => p.id === deleteConfirmProvider)?.name} {' '} - API key? This workspace will revert to using platform keys with the 2x multiplier. + API key? This workspace will revert to using platform hosted keys.

diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/team-management.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/team-management.tsx index 6f3f7e0b57..b78de6ed98 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/team-management.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/team-management/team-management.tsx @@ -33,11 +33,13 @@ import { } from '@/hooks/queries/organization' import { useSubscriptionData } from '@/hooks/queries/subscription' import { useAdminWorkspaces } from '@/hooks/queries/workspace' +import { usePermissionConfig } from '@/hooks/use-permission-config' const logger = createLogger('TeamManagement') export function TeamManagement() { const { data: session } = useSession() + const { isInvitationsDisabled } = usePermissionConfig() const { data: organizationsData } = useOrganizations() const activeOrganization = organizationsData?.activeOrganization @@ -385,8 +387,8 @@ export function TeamManagement() {
)} - {/* Action: Invite New Members */} - {adminOrOwner && ( + {/* Action: Invite New Members - hidden when invitations are disabled */} + {adminOrOwner && !isInvitationsDisabled && (
{ e.stopPropagation() - onColorChange(color) + setHexInput(color) }} className={cn( 'h-[20px] w-[20px] rounded-[4px]', - currentColor?.toLowerCase() === color.toLowerCase() && 'ring-1 ring-white' + hexInput.toLowerCase() === color.toLowerCase() && 'ring-1 ring-white' )} style={{ backgroundColor: color }} /> @@ -373,7 +373,7 @@ export function ContextMenu({ onKeyDown={handleHexKeyDown} onFocus={handleHexFocus} onClick={(e) => e.stopPropagation()} - className='h-[20px] min-w-0 flex-1 rounded-[4px] bg-[#363636] px-[6px] text-[11px] text-white uppercase focus:outline-none' + className='h-[20px] min-w-0 flex-1 rounded-[4px] bg-[#363636] px-[6px] text-[11px] text-white uppercase caret-white focus:outline-none' />
{/* Workspace Actions */}
- {/* Invite - hidden in collapsed mode */} - {!isCollapsed && ( + {/* Invite - hidden in collapsed mode or when invitations are disabled */} + {!isCollapsed && !isInvitationsDisabled && ( setIsInviteModalOpen(true)}> Invite diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx index 6acab60c56..cb4b7a5bf8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx @@ -2,10 +2,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' -import { ArrowDown, Database, HelpCircle, Layout, Plus, Search, Settings } from 'lucide-react' +import { Database, HelpCircle, Layout, Plus, Search, Settings } from 'lucide-react' import Link from 'next/link' import { useParams, usePathname, useRouter } from 'next/navigation' -import { Button, FolderPlus, Library, Tooltip } from '@/components/emcn' +import { Button, Download, FolderPlus, Library, Loader, Tooltip } from '@/components/emcn' import { useSession } from '@/lib/auth/auth-client' import { getEnv, isTruthy } from '@/lib/core/config/env' import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider' @@ -30,6 +30,7 @@ import { import { useDuplicateWorkspace, useExportWorkspace, + useImportWorkflow, useImportWorkspace, } from '@/app/workspace/[workspaceId]/w/hooks' import { usePermissionConfig } from '@/hooks/use-permission-config' @@ -85,9 +86,11 @@ export function Sidebar() { const isCollapsed = hasHydrated ? isCollapsedStore : false const isOnWorkflowPage = !!workflowId - const [isImporting, setIsImporting] = useState(false) const workspaceFileInputRef = useRef(null) + const { isImporting, handleFileChange: handleImportFileChange } = useImportWorkflow({ + workspaceId, + }) const { isImporting: isImportingWorkspace, handleImportWorkspace: importWorkspace } = useImportWorkspace() const { handleExportWorkspace: exportWorkspace } = useExportWorkspace() @@ -213,7 +216,7 @@ export function Sidebar() { }, [activeNavItemHref]) const { handleDuplicateWorkspace: duplicateWorkspace } = useDuplicateWorkspace({ - getWorkspaceId: () => workspaceId, + workspaceId, }) const searchModalWorkflows = useMemo( @@ -565,21 +568,31 @@ export function Sidebar() { Workflows
- - - - - -

{isImporting ? 'Importing workflow...' : 'Import workflow'}

-
-
+ {isImporting ? ( + + ) : ( + + + + + +

Import workflows

+
+
+ )}