mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-06 04:35:03 -05:00
Compare commits
4 Commits
v0.5.82
...
feat/skill
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92edf2f88a | ||
|
|
dfddb60cc5 | ||
|
|
6fea21c9aa | ||
|
|
84e77fe635 |
@@ -10,6 +10,7 @@
|
||||
"connections",
|
||||
"mcp",
|
||||
"copilot",
|
||||
"skills",
|
||||
"knowledgebase",
|
||||
"variables",
|
||||
"execution",
|
||||
|
||||
83
apps/docs/content/docs/en/skills/index.mdx
Normal file
83
apps/docs/content/docs/en/skills/index.mdx
Normal file
@@ -0,0 +1,83 @@
|
||||
---
|
||||
title: Agent Skills
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout'
|
||||
|
||||
Agent Skills are reusable packages of instructions that give your AI agents specialized capabilities. Based on the open [Agent Skills](https://agentskills.io) format, skills let you capture domain expertise, workflows, and best practices that agents can load on demand.
|
||||
|
||||
## How Skills Work
|
||||
|
||||
Skills use **progressive disclosure** to keep agent context lean:
|
||||
|
||||
1. **Discovery** — Only skill names and descriptions are included in the agent's system prompt (~50-100 tokens each)
|
||||
2. **Activation** — When the agent decides a skill is relevant, it calls the `load_skill` tool to load the full instructions into context
|
||||
3. **Execution** — The agent follows the loaded instructions to complete the task
|
||||
|
||||
This means you can attach many skills to an agent without bloating its context window. The agent only loads what it needs.
|
||||
|
||||
## Creating Skills
|
||||
|
||||
Go to **Settings** (gear icon) and select **Skills** under the Tools section.
|
||||
|
||||
Click **Add** to create a new skill with three fields:
|
||||
|
||||
| Field | Description |
|
||||
|-------|-------------|
|
||||
| **Name** | A kebab-case identifier (e.g. `sql-expert`, `code-reviewer`). Max 64 characters. |
|
||||
| **Description** | A short explanation of what the skill does and when to use it. This is what the agent reads to decide whether to activate the skill. Max 1024 characters. |
|
||||
| **Content** | The full skill instructions in markdown. This is loaded when the agent activates the skill. |
|
||||
|
||||
<Callout type="info">
|
||||
The description is critical — it's the only thing the agent sees before deciding to load a skill. Be specific about when and why the skill should be used.
|
||||
</Callout>
|
||||
|
||||
### Writing Good Skill Content
|
||||
|
||||
Skill content follows the same conventions as [SKILL.md files](https://agentskills.io/specification):
|
||||
|
||||
```markdown
|
||||
# SQL Expert
|
||||
|
||||
## When to use this skill
|
||||
Use when the user asks you to write, optimize, or debug SQL queries.
|
||||
|
||||
## Instructions
|
||||
1. Always ask which database engine (PostgreSQL, MySQL, SQLite)
|
||||
2. Use CTEs over subqueries for readability
|
||||
3. Add index recommendations when relevant
|
||||
4. Explain query plans for optimization requests
|
||||
|
||||
## Common Patterns
|
||||
...
|
||||
```
|
||||
|
||||
## Adding Skills to an Agent
|
||||
|
||||
Open any **Agent** block and find the **Skills** dropdown below the tools section. Select the skills you want the agent to have access to.
|
||||
|
||||
Selected skills appear as chips that you can click to edit or remove.
|
||||
|
||||
### What Happens at Runtime
|
||||
|
||||
When the workflow runs:
|
||||
|
||||
1. The agent's system prompt includes an `<available_skills>` section listing each skill's name and description
|
||||
2. A `load_skill` tool is automatically added to the agent's available tools
|
||||
3. When the agent determines a skill is relevant to the current task, it calls `load_skill` with the skill name
|
||||
4. The full skill content is returned as a tool response, giving the agent detailed instructions
|
||||
|
||||
This works across all supported LLM providers — the `load_skill` tool uses standard tool-calling, so no provider-specific configuration is needed.
|
||||
|
||||
## Tips
|
||||
|
||||
- **Keep descriptions actionable** — Instead of "Helps with SQL", write "Write optimized SQL queries for PostgreSQL, MySQL, and SQLite, including index recommendations and query plan analysis"
|
||||
- **One skill per domain** — A focused `sql-expert` skill works better than a broad `database-everything` skill
|
||||
- **Use markdown structure** — Headers, lists, and code blocks help the agent parse and follow instructions
|
||||
- **Test iteratively** — Run your workflow and check if the agent activates the skill when expected
|
||||
|
||||
## Learn More
|
||||
|
||||
- [Agent Skills specification](https://agentskills.io) — The open format for portable agent skills
|
||||
- [Example skills](https://github.com/anthropics/skills) — Browse community skill examples
|
||||
- [Best practices](https://agentskills.io/what-are-skills) — Writing effective skills
|
||||
@@ -320,7 +320,6 @@ Search for issues in Linear using full-text search
|
||||
| `teamId` | string | No | Filter by team ID |
|
||||
| `includeArchived` | boolean | No | Include archived issues in search results |
|
||||
| `first` | number | No | Number of results to return \(default: 50\) |
|
||||
| `after` | string | No | Cursor for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -755,10 +754,6 @@ List all labels in Linear workspace or team
|
||||
| ↳ `name` | string | Label name |
|
||||
| ↳ `color` | string | Label color \(hex\) |
|
||||
| ↳ `description` | string | Label description |
|
||||
| ↳ `isGroup` | boolean | Whether this label is a group |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) |
|
||||
| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) |
|
||||
| ↳ `team` | object | Team object |
|
||||
| ↳ `id` | string | Team ID |
|
||||
| ↳ `name` | string | Team name |
|
||||
@@ -785,10 +780,6 @@ Create a new label in Linear
|
||||
| ↳ `name` | string | Label name |
|
||||
| ↳ `color` | string | Label color \(hex\) |
|
||||
| ↳ `description` | string | Label description |
|
||||
| ↳ `isGroup` | boolean | Whether this label is a group |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) |
|
||||
| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) |
|
||||
| ↳ `team` | object | Team object |
|
||||
| ↳ `id` | string | Team ID |
|
||||
| ↳ `name` | string | Team name |
|
||||
@@ -815,10 +806,6 @@ Update an existing label in Linear
|
||||
| ↳ `name` | string | Label name |
|
||||
| ↳ `color` | string | Label color \(hex\) |
|
||||
| ↳ `description` | string | Label description |
|
||||
| ↳ `isGroup` | boolean | Whether this label is a group |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) |
|
||||
| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) |
|
||||
| ↳ `team` | object | Team object |
|
||||
| ↳ `id` | string | Team ID |
|
||||
| ↳ `name` | string | Team name |
|
||||
@@ -862,13 +849,9 @@ List all workflow states (statuses) in Linear
|
||||
| `states` | array | Array of workflow states |
|
||||
| ↳ `id` | string | State ID |
|
||||
| ↳ `name` | string | State name \(e.g., "Todo", "In Progress"\) |
|
||||
| ↳ `description` | string | State description |
|
||||
| ↳ `type` | string | State type \(triage, backlog, unstarted, started, completed, canceled\) |
|
||||
| ↳ `type` | string | State type \(unstarted, started, completed, canceled\) |
|
||||
| ↳ `color` | string | State color \(hex\) |
|
||||
| ↳ `position` | number | State position in workflow |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) |
|
||||
| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) |
|
||||
| ↳ `team` | object | Team object |
|
||||
| ↳ `id` | string | Team ID |
|
||||
| ↳ `name` | string | Team name |
|
||||
@@ -894,17 +877,11 @@ Create a new workflow state (status) in Linear
|
||||
| --------- | ---- | ----------- |
|
||||
| `state` | object | The created workflow state |
|
||||
| ↳ `id` | string | State ID |
|
||||
| ↳ `name` | string | State name \(e.g., "Todo", "In Progress"\) |
|
||||
| ↳ `description` | string | State description |
|
||||
| ↳ `type` | string | State type \(triage, backlog, unstarted, started, completed, canceled\) |
|
||||
| ↳ `color` | string | State color \(hex\) |
|
||||
| ↳ `position` | number | State position in workflow |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) |
|
||||
| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) |
|
||||
| ↳ `team` | object | Team object |
|
||||
| ↳ `id` | string | Team ID |
|
||||
| ↳ `name` | string | Team name |
|
||||
| ↳ `name` | string | State name |
|
||||
| ↳ `type` | string | State type |
|
||||
| ↳ `color` | string | State color |
|
||||
| ↳ `position` | number | State position |
|
||||
| ↳ `team` | object | Team this state belongs to |
|
||||
|
||||
### `linear_update_workflow_state`
|
||||
|
||||
@@ -926,17 +903,10 @@ Update an existing workflow state in Linear
|
||||
| --------- | ---- | ----------- |
|
||||
| `state` | object | The updated workflow state |
|
||||
| ↳ `id` | string | State ID |
|
||||
| ↳ `name` | string | State name \(e.g., "Todo", "In Progress"\) |
|
||||
| ↳ `description` | string | State description |
|
||||
| ↳ `type` | string | State type \(triage, backlog, unstarted, started, completed, canceled\) |
|
||||
| ↳ `color` | string | State color \(hex\) |
|
||||
| ↳ `position` | number | State position in workflow |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) |
|
||||
| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) |
|
||||
| ↳ `team` | object | Team object |
|
||||
| ↳ `id` | string | Team ID |
|
||||
| ↳ `name` | string | Team name |
|
||||
| ↳ `name` | string | State name |
|
||||
| ↳ `type` | string | State type |
|
||||
| ↳ `color` | string | State color |
|
||||
| ↳ `position` | number | State position |
|
||||
|
||||
### `linear_list_cycles`
|
||||
|
||||
@@ -965,7 +935,6 @@ List cycles (sprints/iterations) in Linear
|
||||
| ↳ `endsAt` | string | End date \(ISO 8601\) |
|
||||
| ↳ `completedAt` | string | Completion date \(ISO 8601\) |
|
||||
| ↳ `progress` | number | Progress percentage \(0-1\) |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `team` | object | Team object |
|
||||
| ↳ `id` | string | Team ID |
|
||||
| ↳ `name` | string | Team name |
|
||||
@@ -992,7 +961,6 @@ Get a single cycle by ID from Linear
|
||||
| ↳ `endsAt` | string | End date \(ISO 8601\) |
|
||||
| ↳ `completedAt` | string | Completion date \(ISO 8601\) |
|
||||
| ↳ `progress` | number | Progress percentage \(0-1\) |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `team` | object | Team object |
|
||||
| ↳ `id` | string | Team ID |
|
||||
| ↳ `name` | string | Team name |
|
||||
@@ -1018,14 +986,9 @@ Create a new cycle (sprint/iteration) in Linear
|
||||
| ↳ `id` | string | Cycle ID |
|
||||
| ↳ `number` | number | Cycle number |
|
||||
| ↳ `name` | string | Cycle name |
|
||||
| ↳ `startsAt` | string | Start date \(ISO 8601\) |
|
||||
| ↳ `endsAt` | string | End date \(ISO 8601\) |
|
||||
| ↳ `completedAt` | string | Completion date \(ISO 8601\) |
|
||||
| ↳ `progress` | number | Progress percentage \(0-1\) |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `team` | object | Team object |
|
||||
| ↳ `id` | string | Team ID |
|
||||
| ↳ `name` | string | Team name |
|
||||
| ↳ `startsAt` | string | Start date |
|
||||
| ↳ `endsAt` | string | End date |
|
||||
| ↳ `team` | object | Team this cycle belongs to |
|
||||
|
||||
### `linear_get_active_cycle`
|
||||
|
||||
@@ -1045,14 +1008,10 @@ Get the currently active cycle for a team
|
||||
| ↳ `id` | string | Cycle ID |
|
||||
| ↳ `number` | number | Cycle number |
|
||||
| ↳ `name` | string | Cycle name |
|
||||
| ↳ `startsAt` | string | Start date \(ISO 8601\) |
|
||||
| ↳ `endsAt` | string | End date \(ISO 8601\) |
|
||||
| ↳ `completedAt` | string | Completion date \(ISO 8601\) |
|
||||
| ↳ `progress` | number | Progress percentage \(0-1\) |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `team` | object | Team object |
|
||||
| ↳ `id` | string | Team ID |
|
||||
| ↳ `name` | string | Team name |
|
||||
| ↳ `startsAt` | string | Start date |
|
||||
| ↳ `endsAt` | string | End date |
|
||||
| ↳ `progress` | number | Progress percentage |
|
||||
| ↳ `team` | object | Team this cycle belongs to |
|
||||
|
||||
### `linear_create_attachment`
|
||||
|
||||
@@ -1375,12 +1334,8 @@ Create a new customer in Linear
|
||||
| ↳ `domains` | array | Associated domains |
|
||||
| ↳ `externalIds` | array | External IDs from other systems |
|
||||
| ↳ `logoUrl` | string | Logo URL |
|
||||
| ↳ `slugId` | string | Unique URL slug |
|
||||
| ↳ `approximateNeedCount` | number | Number of customer needs |
|
||||
| ↳ `revenue` | number | Annual revenue |
|
||||
| ↳ `size` | number | Organization size |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) |
|
||||
| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) |
|
||||
|
||||
### `linear_list_customers`
|
||||
@@ -1408,12 +1363,8 @@ List all customers in Linear
|
||||
| ↳ `domains` | array | Associated domains |
|
||||
| ↳ `externalIds` | array | External IDs from other systems |
|
||||
| ↳ `logoUrl` | string | Logo URL |
|
||||
| ↳ `slugId` | string | Unique URL slug |
|
||||
| ↳ `approximateNeedCount` | number | Number of customer needs |
|
||||
| ↳ `revenue` | number | Annual revenue |
|
||||
| ↳ `size` | number | Organization size |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) |
|
||||
| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) |
|
||||
|
||||
### `linear_create_customer_request`
|
||||
@@ -1529,12 +1480,8 @@ Get a single customer by ID in Linear
|
||||
| ↳ `domains` | array | Associated domains |
|
||||
| ↳ `externalIds` | array | External IDs from other systems |
|
||||
| ↳ `logoUrl` | string | Logo URL |
|
||||
| ↳ `slugId` | string | Unique URL slug |
|
||||
| ↳ `approximateNeedCount` | number | Number of customer needs |
|
||||
| ↳ `revenue` | number | Annual revenue |
|
||||
| ↳ `size` | number | Organization size |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) |
|
||||
| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) |
|
||||
|
||||
### `linear_update_customer`
|
||||
@@ -1566,12 +1513,8 @@ Update a customer in Linear
|
||||
| ↳ `domains` | array | Associated domains |
|
||||
| ↳ `externalIds` | array | External IDs from other systems |
|
||||
| ↳ `logoUrl` | string | Logo URL |
|
||||
| ↳ `slugId` | string | Unique URL slug |
|
||||
| ↳ `approximateNeedCount` | number | Number of customer needs |
|
||||
| ↳ `revenue` | number | Annual revenue |
|
||||
| ↳ `size` | number | Organization size |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) |
|
||||
| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) |
|
||||
|
||||
### `linear_delete_customer`
|
||||
@@ -1617,8 +1560,8 @@ Create a new customer status in Linear
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `name` | string | Yes | Customer status name |
|
||||
| `color` | string | Yes | Status color \(hex code\) |
|
||||
| `description` | string | No | Status description |
|
||||
| `displayName` | string | No | Display name for the status |
|
||||
| `description` | string | No | Status description |
|
||||
| `position` | number | No | Position in status list |
|
||||
|
||||
#### Output
|
||||
@@ -1628,12 +1571,11 @@ Create a new customer status in Linear
|
||||
| `customerStatus` | object | The created customer status |
|
||||
| ↳ `id` | string | Customer status ID |
|
||||
| ↳ `name` | string | Status name |
|
||||
| ↳ `displayName` | string | Display name |
|
||||
| ↳ `description` | string | Status description |
|
||||
| ↳ `color` | string | Status color \(hex\) |
|
||||
| ↳ `position` | number | Position in list |
|
||||
| ↳ `type` | string | Status type \(active, inactive\) |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last updated timestamp \(ISO 8601\) |
|
||||
| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) |
|
||||
|
||||
### `linear_update_customer_status`
|
||||
@@ -1647,8 +1589,8 @@ Update a customer status in Linear
|
||||
| `statusId` | string | Yes | Customer status ID to update |
|
||||
| `name` | string | No | Updated status name |
|
||||
| `color` | string | No | Updated status color |
|
||||
| `description` | string | No | Updated description |
|
||||
| `displayName` | string | No | Updated display name |
|
||||
| `description` | string | No | Updated description |
|
||||
| `position` | number | No | Updated position |
|
||||
|
||||
#### Output
|
||||
@@ -1656,15 +1598,6 @@ Update a customer status in Linear
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `customerStatus` | object | The updated customer status |
|
||||
| ↳ `id` | string | Customer status ID |
|
||||
| ↳ `name` | string | Status name |
|
||||
| ↳ `description` | string | Status description |
|
||||
| ↳ `color` | string | Status color \(hex\) |
|
||||
| ↳ `position` | number | Position in list |
|
||||
| ↳ `type` | string | Status type \(active, inactive\) |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last updated timestamp \(ISO 8601\) |
|
||||
| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) |
|
||||
|
||||
### `linear_delete_customer_status`
|
||||
|
||||
@@ -1690,25 +1623,19 @@ List all customer statuses in Linear
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `first` | number | No | Number of statuses to return \(default: 50\) |
|
||||
| `after` | string | No | Cursor for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `pageInfo` | object | Pagination information |
|
||||
| ↳ `hasNextPage` | boolean | Whether there are more results |
|
||||
| ↳ `endCursor` | string | Cursor for the next page |
|
||||
| `customerStatuses` | array | List of customer statuses |
|
||||
| ↳ `id` | string | Customer status ID |
|
||||
| ↳ `name` | string | Status name |
|
||||
| ↳ `displayName` | string | Display name |
|
||||
| ↳ `description` | string | Status description |
|
||||
| ↳ `color` | string | Status color \(hex\) |
|
||||
| ↳ `position` | number | Position in list |
|
||||
| ↳ `type` | string | Status type \(active, inactive\) |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last updated timestamp \(ISO 8601\) |
|
||||
| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) |
|
||||
|
||||
### `linear_create_customer_tier`
|
||||
@@ -1784,16 +1711,11 @@ List all customer tiers in Linear
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `first` | number | No | Number of tiers to return \(default: 50\) |
|
||||
| `after` | string | No | Cursor for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `pageInfo` | object | Pagination information |
|
||||
| ↳ `hasNextPage` | boolean | Whether there are more results |
|
||||
| ↳ `endCursor` | string | Cursor for the next page |
|
||||
| `customerTiers` | array | List of customer tiers |
|
||||
| ↳ `id` | string | Customer tier ID |
|
||||
| ↳ `name` | string | Tier name |
|
||||
@@ -1839,14 +1761,6 @@ Create a new project label in Linear
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `projectLabel` | object | The created project label |
|
||||
| ↳ `id` | string | Project label ID |
|
||||
| ↳ `name` | string | Label name |
|
||||
| ↳ `description` | string | Label description |
|
||||
| ↳ `color` | string | Label color \(hex\) |
|
||||
| ↳ `isGroup` | boolean | Whether this label is a group |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) |
|
||||
| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) |
|
||||
|
||||
### `linear_update_project_label`
|
||||
|
||||
@@ -1866,14 +1780,6 @@ Update a project label in Linear
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `projectLabel` | object | The updated project label |
|
||||
| ↳ `id` | string | Project label ID |
|
||||
| ↳ `name` | string | Label name |
|
||||
| ↳ `description` | string | Label description |
|
||||
| ↳ `color` | string | Label color \(hex\) |
|
||||
| ↳ `isGroup` | boolean | Whether this label is a group |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) |
|
||||
| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) |
|
||||
|
||||
### `linear_delete_project_label`
|
||||
|
||||
@@ -1900,25 +1806,12 @@ List all project labels in Linear
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | No | Optional project ID to filter labels for a specific project |
|
||||
| `first` | number | No | Number of labels to return \(default: 50\) |
|
||||
| `after` | string | No | Cursor for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `pageInfo` | object | Pagination information |
|
||||
| ↳ `hasNextPage` | boolean | Whether there are more results |
|
||||
| ↳ `endCursor` | string | Cursor for the next page |
|
||||
| `projectLabels` | array | List of project labels |
|
||||
| ↳ `id` | string | Project label ID |
|
||||
| ↳ `name` | string | Label name |
|
||||
| ↳ `description` | string | Label description |
|
||||
| ↳ `color` | string | Label color \(hex\) |
|
||||
| ↳ `isGroup` | boolean | Whether this label is a group |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last update timestamp \(ISO 8601\) |
|
||||
| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) |
|
||||
|
||||
### `linear_add_label_to_project`
|
||||
|
||||
@@ -1974,16 +1867,6 @@ Create a new project milestone in Linear
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `projectMilestone` | object | The created project milestone |
|
||||
| ↳ `id` | string | Project milestone ID |
|
||||
| ↳ `name` | string | Milestone name |
|
||||
| ↳ `description` | string | Milestone description |
|
||||
| ↳ `projectId` | string | Project ID |
|
||||
| ↳ `targetDate` | string | Target date \(YYYY-MM-DD\) |
|
||||
| ↳ `progress` | number | Progress percentage \(0-1\) |
|
||||
| ↳ `sortOrder` | number | Sort order within the project |
|
||||
| ↳ `status` | string | Milestone status \(done, next, overdue, unstarted\) |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) |
|
||||
|
||||
### `linear_update_project_milestone`
|
||||
|
||||
@@ -2003,16 +1886,6 @@ Update a project milestone in Linear
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `projectMilestone` | object | The updated project milestone |
|
||||
| ↳ `id` | string | Project milestone ID |
|
||||
| ↳ `name` | string | Milestone name |
|
||||
| ↳ `description` | string | Milestone description |
|
||||
| ↳ `projectId` | string | Project ID |
|
||||
| ↳ `targetDate` | string | Target date \(YYYY-MM-DD\) |
|
||||
| ↳ `progress` | number | Progress percentage \(0-1\) |
|
||||
| ↳ `sortOrder` | number | Sort order within the project |
|
||||
| ↳ `status` | string | Milestone status \(done, next, overdue, unstarted\) |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) |
|
||||
|
||||
### `linear_delete_project_milestone`
|
||||
|
||||
@@ -2039,27 +1912,12 @@ List all milestones for a project in Linear
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `projectId` | string | Yes | Project ID to list milestones for |
|
||||
| `first` | number | No | Number of milestones to return \(default: 50\) |
|
||||
| `after` | string | No | Cursor for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `pageInfo` | object | Pagination information |
|
||||
| ↳ `hasNextPage` | boolean | Whether there are more results |
|
||||
| ↳ `endCursor` | string | Cursor for the next page |
|
||||
| `projectMilestones` | array | List of project milestones |
|
||||
| ↳ `id` | string | Project milestone ID |
|
||||
| ↳ `name` | string | Milestone name |
|
||||
| ↳ `description` | string | Milestone description |
|
||||
| ↳ `projectId` | string | Project ID |
|
||||
| ↳ `targetDate` | string | Target date \(YYYY-MM-DD\) |
|
||||
| ↳ `progress` | number | Progress percentage \(0-1\) |
|
||||
| ↳ `sortOrder` | number | Sort order within the project |
|
||||
| ↳ `status` | string | Milestone status \(done, next, overdue, unstarted\) |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) |
|
||||
|
||||
### `linear_create_project_status`
|
||||
|
||||
@@ -2081,16 +1939,6 @@ Create a new project status in Linear
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `projectStatus` | object | The created project status |
|
||||
| ↳ `id` | string | Project status ID |
|
||||
| ↳ `name` | string | Status name |
|
||||
| ↳ `description` | string | Status description |
|
||||
| ↳ `color` | string | Status color \(hex\) |
|
||||
| ↳ `indefinite` | boolean | Whether this status is indefinite |
|
||||
| ↳ `position` | number | Position in list |
|
||||
| ↳ `type` | string | Status type \(backlog, planned, started, paused, completed, canceled\) |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last updated timestamp \(ISO 8601\) |
|
||||
| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) |
|
||||
|
||||
### `linear_update_project_status`
|
||||
|
||||
@@ -2112,16 +1960,6 @@ Update a project status in Linear
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `projectStatus` | object | The updated project status |
|
||||
| ↳ `id` | string | Project status ID |
|
||||
| ↳ `name` | string | Status name |
|
||||
| ↳ `description` | string | Status description |
|
||||
| ↳ `color` | string | Status color \(hex\) |
|
||||
| ↳ `indefinite` | boolean | Whether this status is indefinite |
|
||||
| ↳ `position` | number | Position in list |
|
||||
| ↳ `type` | string | Status type \(backlog, planned, started, paused, completed, canceled\) |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last updated timestamp \(ISO 8601\) |
|
||||
| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) |
|
||||
|
||||
### `linear_delete_project_status`
|
||||
|
||||
@@ -2147,26 +1985,11 @@ List all project statuses in Linear
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `first` | number | No | Number of statuses to return \(default: 50\) |
|
||||
| `after` | string | No | Cursor for pagination |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `pageInfo` | object | Pagination information |
|
||||
| ↳ `hasNextPage` | boolean | Whether there are more results |
|
||||
| ↳ `endCursor` | string | Cursor for the next page |
|
||||
| `projectStatuses` | array | List of project statuses |
|
||||
| ↳ `id` | string | Project status ID |
|
||||
| ↳ `name` | string | Status name |
|
||||
| ↳ `description` | string | Status description |
|
||||
| ↳ `color` | string | Status color \(hex\) |
|
||||
| ↳ `indefinite` | boolean | Whether this status is indefinite |
|
||||
| ↳ `position` | number | Position in list |
|
||||
| ↳ `type` | string | Status type \(backlog, planned, started, paused, completed, canceled\) |
|
||||
| ↳ `createdAt` | string | Creation timestamp \(ISO 8601\) |
|
||||
| ↳ `updatedAt` | string | Last updated timestamp \(ISO 8601\) |
|
||||
| ↳ `archivedAt` | string | Archive timestamp \(ISO 8601\) |
|
||||
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ const configSchema = z.object({
|
||||
hideFilesTab: z.boolean().optional(),
|
||||
disableMcpTools: z.boolean().optional(),
|
||||
disableCustomTools: z.boolean().optional(),
|
||||
disableSkills: z.boolean().optional(),
|
||||
hideTemplates: z.boolean().optional(),
|
||||
disableInvitations: z.boolean().optional(),
|
||||
hideDeployApi: z.boolean().optional(),
|
||||
|
||||
@@ -25,6 +25,7 @@ const configSchema = z.object({
|
||||
hideFilesTab: z.boolean().optional(),
|
||||
disableMcpTools: z.boolean().optional(),
|
||||
disableCustomTools: z.boolean().optional(),
|
||||
disableSkills: z.boolean().optional(),
|
||||
hideTemplates: z.boolean().optional(),
|
||||
disableInvitations: z.boolean().optional(),
|
||||
hideDeployApi: z.boolean().optional(),
|
||||
|
||||
182
apps/sim/app/api/skills/route.ts
Normal file
182
apps/sim/app/api/skills/route.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { db } from '@sim/db'
|
||||
import { skill } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, desc, eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { upsertSkills } from '@/lib/workflows/skills/operations'
|
||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||
|
||||
const logger = createLogger('SkillsAPI')
|
||||
|
||||
const SkillSchema = z.object({
|
||||
skills: z.array(
|
||||
z.object({
|
||||
id: z.string().optional(),
|
||||
name: z
|
||||
.string()
|
||||
.min(1, 'Skill name is required')
|
||||
.max(64)
|
||||
.regex(/^[a-z0-9]+(-[a-z0-9]+)*$/, 'Name must be kebab-case (e.g. my-skill)'),
|
||||
description: z.string().min(1, 'Description is required').max(1024),
|
||||
content: z.string().min(1, 'Content is required').max(50000, 'Content is too large'),
|
||||
})
|
||||
),
|
||||
workspaceId: z.string().optional(),
|
||||
})
|
||||
|
||||
/** GET - Fetch all skills for a workspace */
|
||||
export async function GET(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const workspaceId = searchParams.get('workspaceId')
|
||||
|
||||
try {
|
||||
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized skills access attempt`)
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const userId = authResult.userId
|
||||
|
||||
if (!workspaceId) {
|
||||
logger.warn(`[${requestId}] Missing workspaceId`)
|
||||
return NextResponse.json({ error: 'workspaceId is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const userPermission = await getUserEntityPermissions(userId, 'workspace', workspaceId)
|
||||
if (!userPermission) {
|
||||
logger.warn(`[${requestId}] User ${userId} does not have access to workspace ${workspaceId}`)
|
||||
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
|
||||
}
|
||||
|
||||
const result = await db
|
||||
.select()
|
||||
.from(skill)
|
||||
.where(eq(skill.workspaceId, workspaceId))
|
||||
.orderBy(desc(skill.createdAt))
|
||||
|
||||
return NextResponse.json({ data: result }, { status: 200 })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error fetching skills:`, error)
|
||||
return NextResponse.json({ error: 'Failed to fetch skills' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/** POST - Create or update skills */
|
||||
export async function POST(req: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkSessionOrInternalAuth(req, { requireWorkflowId: false })
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized skills update attempt`)
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const userId = authResult.userId
|
||||
const body = await req.json()
|
||||
|
||||
try {
|
||||
const { skills, workspaceId } = SkillSchema.parse(body)
|
||||
|
||||
if (!workspaceId) {
|
||||
logger.warn(`[${requestId}] Missing workspaceId in request body`)
|
||||
return NextResponse.json({ error: 'workspaceId is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const userPermission = await getUserEntityPermissions(userId, 'workspace', workspaceId)
|
||||
if (!userPermission || (userPermission !== 'admin' && userPermission !== 'write')) {
|
||||
logger.warn(
|
||||
`[${requestId}] User ${userId} does not have write permission for workspace ${workspaceId}`
|
||||
)
|
||||
return NextResponse.json({ error: 'Write permission required' }, { status: 403 })
|
||||
}
|
||||
|
||||
const resultSkills = await upsertSkills({
|
||||
skills,
|
||||
workspaceId,
|
||||
userId,
|
||||
requestId,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true, data: resultSkills })
|
||||
} catch (validationError) {
|
||||
if (validationError instanceof z.ZodError) {
|
||||
logger.warn(`[${requestId}] Invalid skills data`, {
|
||||
errors: validationError.errors,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{ error: 'Invalid request data', details: validationError.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
if (validationError instanceof Error && validationError.message.includes('already exists')) {
|
||||
return NextResponse.json({ error: validationError.message }, { status: 409 })
|
||||
}
|
||||
throw validationError
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error updating skills`, error)
|
||||
return NextResponse.json({ error: 'Failed to update skills' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
/** DELETE - Delete a skill by ID */
|
||||
export async function DELETE(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
const searchParams = request.nextUrl.searchParams
|
||||
const skillId = searchParams.get('id')
|
||||
const workspaceId = searchParams.get('workspaceId')
|
||||
|
||||
try {
|
||||
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||
if (!authResult.success || !authResult.userId) {
|
||||
logger.warn(`[${requestId}] Unauthorized skill deletion attempt`)
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const userId = authResult.userId
|
||||
|
||||
if (!skillId) {
|
||||
logger.warn(`[${requestId}] Missing skill ID for deletion`)
|
||||
return NextResponse.json({ error: 'Skill ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (!workspaceId) {
|
||||
logger.warn(`[${requestId}] Missing workspaceId for deletion`)
|
||||
return NextResponse.json({ error: 'workspaceId is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const userPermission = await getUserEntityPermissions(userId, 'workspace', workspaceId)
|
||||
if (!userPermission || (userPermission !== 'admin' && userPermission !== 'write')) {
|
||||
logger.warn(
|
||||
`[${requestId}] User ${userId} does not have write permission for workspace ${workspaceId}`
|
||||
)
|
||||
return NextResponse.json({ error: 'Write permission required' }, { status: 403 })
|
||||
}
|
||||
|
||||
const existingSkill = await db.select().from(skill).where(eq(skill.id, skillId)).limit(1)
|
||||
|
||||
if (existingSkill.length === 0) {
|
||||
logger.warn(`[${requestId}] Skill not found: ${skillId}`)
|
||||
return NextResponse.json({ error: 'Skill not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
if (existingSkill[0].workspaceId !== workspaceId) {
|
||||
logger.warn(`[${requestId}] Skill ${skillId} does not belong to workspace ${workspaceId}`)
|
||||
return NextResponse.json({ error: 'Skill not found' }, { status: 404 })
|
||||
}
|
||||
|
||||
await db.delete(skill).where(and(eq(skill.id, skillId), eq(skill.workspaceId, workspaceId)))
|
||||
|
||||
logger.info(`[${requestId}] Deleted skill: ${skillId}`)
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error deleting skill:`, error)
|
||||
return NextResponse.json({ error: 'Failed to delete skill' }, { status: 500 })
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ export { ResponseFormat } from './response/response-format'
|
||||
export { ScheduleInfo } from './schedule-info/schedule-info'
|
||||
export { SheetSelectorInput } from './sheet-selector/sheet-selector-input'
|
||||
export { ShortInput } from './short-input/short-input'
|
||||
export { SkillInput } from './skill-input/skill-input'
|
||||
export { SlackSelectorInput } from './slack-selector/slack-selector-input'
|
||||
export { SliderInput } from './slider-input/slider-input'
|
||||
export { InputFormat } from './starter/input-format'
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
'use client'
|
||||
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { Plus, XIcon } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import { Combobox, type ComboboxOptionGroup } from '@/components/emcn'
|
||||
import { AgentSkillsIcon } from '@/components/icons'
|
||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||
import { SkillModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/skills/components/skill-modal'
|
||||
import type { SkillDefinition } from '@/hooks/queries/skills'
|
||||
import { useSkills } from '@/hooks/queries/skills'
|
||||
import { usePermissionConfig } from '@/hooks/use-permission-config'
|
||||
|
||||
interface StoredSkill {
|
||||
skillId: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
interface SkillInputProps {
|
||||
blockId: string
|
||||
subBlockId: string
|
||||
isPreview?: boolean
|
||||
previewValue?: unknown
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export function SkillInput({
|
||||
blockId,
|
||||
subBlockId,
|
||||
isPreview,
|
||||
previewValue,
|
||||
disabled,
|
||||
}: SkillInputProps) {
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
|
||||
const { config: permissionConfig } = usePermissionConfig()
|
||||
const { data: workspaceSkills = [] } = useSkills(workspaceId)
|
||||
const [value, setValue] = useSubBlockValue<StoredSkill[]>(blockId, subBlockId)
|
||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||
const [editingSkill, setEditingSkill] = useState<SkillDefinition | null>(null)
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const selectedSkills: StoredSkill[] = useMemo(() => {
|
||||
if (isPreview && previewValue) {
|
||||
return Array.isArray(previewValue) ? previewValue : []
|
||||
}
|
||||
return Array.isArray(value) ? value : []
|
||||
}, [isPreview, previewValue, value])
|
||||
|
||||
const selectedIds = useMemo(() => new Set(selectedSkills.map((s) => s.skillId)), [selectedSkills])
|
||||
|
||||
const skillsDisabled = permissionConfig.disableSkills
|
||||
|
||||
const skillGroups = useMemo((): ComboboxOptionGroup[] => {
|
||||
const groups: ComboboxOptionGroup[] = []
|
||||
|
||||
if (!skillsDisabled) {
|
||||
groups.push({
|
||||
items: [
|
||||
{
|
||||
label: 'Create Skill',
|
||||
value: 'action-create-skill',
|
||||
icon: Plus,
|
||||
onSelect: () => {
|
||||
setShowCreateModal(true)
|
||||
setOpen(false)
|
||||
},
|
||||
disabled: isPreview,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
const availableSkills = workspaceSkills.filter((s) => !selectedIds.has(s.id))
|
||||
if (availableSkills.length > 0) {
|
||||
groups.push({
|
||||
section: 'Skills',
|
||||
items: availableSkills.map((s) => {
|
||||
return {
|
||||
label: s.name,
|
||||
value: `skill-${s.id}`,
|
||||
icon: AgentSkillsIcon,
|
||||
onSelect: () => {
|
||||
const newSkills: StoredSkill[] = [...selectedSkills, { skillId: s.id, name: s.name }]
|
||||
setValue(newSkills)
|
||||
setOpen(false)
|
||||
},
|
||||
}
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
return groups
|
||||
}, [workspaceSkills, selectedIds, selectedSkills, setValue, isPreview, skillsDisabled])
|
||||
|
||||
const handleRemove = useCallback(
|
||||
(skillId: string) => {
|
||||
const newSkills = selectedSkills.filter((s) => s.skillId !== skillId)
|
||||
setValue(newSkills)
|
||||
},
|
||||
[selectedSkills, setValue]
|
||||
)
|
||||
|
||||
const handleSkillSaved = useCallback(() => {
|
||||
setShowCreateModal(false)
|
||||
setEditingSkill(null)
|
||||
}, [])
|
||||
|
||||
const resolveSkillName = useCallback(
|
||||
(stored: StoredSkill): string => {
|
||||
const found = workspaceSkills.find((s) => s.id === stored.skillId)
|
||||
return found?.name ?? stored.name ?? stored.skillId
|
||||
},
|
||||
[workspaceSkills]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='w-full space-y-[8px]'>
|
||||
<Combobox
|
||||
options={[]}
|
||||
groups={skillGroups}
|
||||
placeholder='Add skill...'
|
||||
disabled={disabled}
|
||||
searchable
|
||||
searchPlaceholder='Search skills...'
|
||||
maxHeight={240}
|
||||
emptyMessage='No skills found'
|
||||
onOpenChange={setOpen}
|
||||
/>
|
||||
|
||||
{selectedSkills.length > 0 && (
|
||||
<div className='flex flex-wrap gap-[4px]'>
|
||||
{selectedSkills.map((stored) => {
|
||||
const fullSkill = workspaceSkills.find((s) => s.id === stored.skillId)
|
||||
return (
|
||||
<div
|
||||
key={stored.skillId}
|
||||
className='flex cursor-pointer items-center gap-[4px] rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[6px] py-[2px] font-medium text-[12px] text-[var(--text-secondary)] hover:bg-[var(--surface-6)]'
|
||||
onClick={() => {
|
||||
if (fullSkill && !disabled && !isPreview) {
|
||||
setEditingSkill(fullSkill)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AgentSkillsIcon className='h-[10px] w-[10px] text-[var(--text-tertiary)]' />
|
||||
<span className='max-w-[140px] truncate'>{resolveSkillName(stored)}</span>
|
||||
{!disabled && !isPreview && (
|
||||
<button
|
||||
type='button'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleRemove(stored.skillId)
|
||||
}}
|
||||
className='ml-[2px] rounded-[2px] p-[1px] text-[var(--text-tertiary)] hover:bg-[var(--surface-7)] hover:text-[var(--text-secondary)]'
|
||||
>
|
||||
<XIcon className='h-[10px] w-[10px]' />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SkillModal
|
||||
open={showCreateModal || !!editingSkill}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) {
|
||||
setShowCreateModal(false)
|
||||
setEditingSkill(null)
|
||||
}
|
||||
}}
|
||||
onSave={handleSkillSaved}
|
||||
initialValues={editingSkill ?? undefined}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
ScheduleInfo,
|
||||
SheetSelectorInput,
|
||||
ShortInput,
|
||||
SkillInput,
|
||||
SlackSelectorInput,
|
||||
SliderInput,
|
||||
Switch,
|
||||
@@ -687,6 +688,17 @@ function SubBlockComponent({
|
||||
/>
|
||||
)
|
||||
|
||||
case 'skill-input':
|
||||
return (
|
||||
<SkillInput
|
||||
blockId={blockId}
|
||||
subBlockId={config.id}
|
||||
isPreview={isPreview}
|
||||
previewValue={previewValue}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
)
|
||||
|
||||
case 'checkbox-list':
|
||||
return (
|
||||
<CheckboxList
|
||||
|
||||
@@ -491,13 +491,6 @@ export function useWorkflowExecution() {
|
||||
updateActiveBlocks(data.blockId, false)
|
||||
setBlockRunStatus(data.blockId, 'error')
|
||||
|
||||
executedBlockIds.add(data.blockId)
|
||||
accumulatedBlockStates.set(data.blockId, {
|
||||
output: { error: data.error },
|
||||
executed: true,
|
||||
executionTime: data.durationMs || 0,
|
||||
})
|
||||
|
||||
accumulatedBlockLogs.push(
|
||||
createBlockLogEntry(data, { success: false, output: {}, error: data.error })
|
||||
)
|
||||
|
||||
@@ -349,15 +349,7 @@ export function PreviewWorkflow({
|
||||
if (block.type === 'loop' || block.type === 'parallel') {
|
||||
const isSelected = selectedBlockId === blockId
|
||||
const dimensions = calculateContainerDimensions(blockId, workflowState.blocks)
|
||||
|
||||
// Check for direct error on the subflow block itself (e.g., loop resolution errors)
|
||||
// before falling back to children-derived status
|
||||
const directExecution = blockExecutionMap.get(blockId)
|
||||
const subflowExecutionStatus: ExecutionStatus | undefined =
|
||||
directExecution?.status === 'error'
|
||||
? 'error'
|
||||
: (getSubflowExecutionStatus(blockId) ??
|
||||
(directExecution ? (directExecution.status as ExecutionStatus) : undefined))
|
||||
const subflowExecutionStatus = getSubflowExecutionStatus(blockId)
|
||||
|
||||
nodeArray.push({
|
||||
id: blockId,
|
||||
|
||||
@@ -9,6 +9,7 @@ export { Files as FileUploads } from './files/files'
|
||||
export { General } from './general/general'
|
||||
export { Integrations } from './integrations/integrations'
|
||||
export { MCP } from './mcp/mcp'
|
||||
export { Skills } from './skills/skills'
|
||||
export { Subscription } from './subscription/subscription'
|
||||
export { TeamManagement } from './team-management/team-management'
|
||||
export { WorkflowMcpServers } from './workflow-mcp-servers/workflow-mcp-servers'
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
'use client'
|
||||
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Label,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
Textarea,
|
||||
} from '@/components/emcn'
|
||||
import type { SkillDefinition } from '@/hooks/queries/skills'
|
||||
import { useCreateSkill, useUpdateSkill } from '@/hooks/queries/skills'
|
||||
|
||||
interface SkillModalProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onSave: () => void
|
||||
onDelete?: (skillId: string) => void
|
||||
initialValues?: SkillDefinition
|
||||
}
|
||||
|
||||
const KEBAB_CASE_REGEX = /^[a-z0-9]+(-[a-z0-9]+)*$/
|
||||
|
||||
export function SkillModal({
|
||||
open,
|
||||
onOpenChange,
|
||||
onSave,
|
||||
onDelete,
|
||||
initialValues,
|
||||
}: SkillModalProps) {
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
|
||||
const createSkill = useCreateSkill()
|
||||
const updateSkill = useUpdateSkill()
|
||||
|
||||
const [name, setName] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [content, setContent] = useState('')
|
||||
const [formError, setFormError] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (initialValues) {
|
||||
setName(initialValues.name)
|
||||
setDescription(initialValues.description)
|
||||
setContent(initialValues.content)
|
||||
} else {
|
||||
setName('')
|
||||
setDescription('')
|
||||
setContent('')
|
||||
}
|
||||
setFormError('')
|
||||
}
|
||||
}, [open, initialValues])
|
||||
|
||||
const hasChanges = useMemo(() => {
|
||||
if (!initialValues) return true
|
||||
return (
|
||||
name !== initialValues.name ||
|
||||
description !== initialValues.description ||
|
||||
content !== initialValues.content
|
||||
)
|
||||
}, [name, description, content, initialValues])
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!name.trim()) {
|
||||
setFormError('Name is required')
|
||||
return
|
||||
}
|
||||
if (name.length > 64) {
|
||||
setFormError('Name must be 64 characters or less')
|
||||
return
|
||||
}
|
||||
if (!KEBAB_CASE_REGEX.test(name)) {
|
||||
setFormError('Name must be kebab-case (e.g. my-skill)')
|
||||
return
|
||||
}
|
||||
if (!description.trim()) {
|
||||
setFormError('Description is required')
|
||||
return
|
||||
}
|
||||
if (!content.trim()) {
|
||||
setFormError('Content is required')
|
||||
return
|
||||
}
|
||||
|
||||
setSaving(true)
|
||||
|
||||
try {
|
||||
if (initialValues) {
|
||||
await updateSkill.mutateAsync({
|
||||
workspaceId,
|
||||
skillId: initialValues.id,
|
||||
updates: { name, description, content },
|
||||
})
|
||||
} else {
|
||||
await createSkill.mutateAsync({
|
||||
workspaceId,
|
||||
skill: { name, description, content },
|
||||
})
|
||||
}
|
||||
onSave()
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error && error.message.includes('already exists')
|
||||
? error.message
|
||||
: 'Failed to save skill. Please try again.'
|
||||
setFormError(message)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={open} onOpenChange={onOpenChange}>
|
||||
<ModalContent size='xl'>
|
||||
<ModalHeader>{initialValues ? 'Edit Skill' : 'Create Skill'}</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className='flex flex-col gap-[16px]'>
|
||||
<div className='flex flex-col gap-[4px]'>
|
||||
<Label htmlFor='skill-name' className='font-medium text-[13px]'>
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
id='skill-name'
|
||||
placeholder='my-skill-name'
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
setName(e.target.value)
|
||||
if (formError) setFormError('')
|
||||
}}
|
||||
/>
|
||||
<span className='text-[11px] text-[var(--text-muted)]'>
|
||||
Lowercase letters, numbers, and hyphens (e.g. my-skill)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-[4px]'>
|
||||
<Label htmlFor='skill-description' className='font-medium text-[13px]'>
|
||||
Description
|
||||
</Label>
|
||||
<Input
|
||||
id='skill-description'
|
||||
placeholder='What this skill does and when to use it...'
|
||||
value={description}
|
||||
onChange={(e) => {
|
||||
setDescription(e.target.value)
|
||||
if (formError) setFormError('')
|
||||
}}
|
||||
maxLength={1024}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='flex flex-col gap-[4px]'>
|
||||
<Label htmlFor='skill-content' className='font-medium text-[13px]'>
|
||||
Content
|
||||
</Label>
|
||||
<Textarea
|
||||
id='skill-content'
|
||||
placeholder='Skill instructions in markdown...'
|
||||
value={content}
|
||||
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setContent(e.target.value)
|
||||
if (formError) setFormError('')
|
||||
}}
|
||||
className='min-h-[200px] resize-y font-mono text-[13px]'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{formError && <span className='text-[11px] text-[var(--text-error)]'>{formError}</span>}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter className='items-center justify-between'>
|
||||
{initialValues && onDelete ? (
|
||||
<Button variant='destructive' onClick={() => onDelete(initialValues.id)}>
|
||||
Delete
|
||||
</Button>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<div className='flex gap-2'>
|
||||
<Button variant='default' onClick={() => onOpenChange(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant='tertiary' onClick={handleSave} disabled={saving || !hasChanges}>
|
||||
{saving ? 'Saving...' : initialValues ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Plus, Search } from 'lucide-react'
|
||||
import { useParams } from 'next/navigation'
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
} from '@/components/emcn'
|
||||
import { Skeleton } from '@/components/ui'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import { SkillModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/skills/components/skill-modal'
|
||||
import type { SkillDefinition } from '@/hooks/queries/skills'
|
||||
import { useDeleteSkill, useSkills } from '@/hooks/queries/skills'
|
||||
|
||||
const logger = createLogger('SkillsSettings')
|
||||
|
||||
function SkillSkeleton() {
|
||||
return (
|
||||
<div className='flex items-center justify-between gap-[12px]'>
|
||||
<div className='flex min-w-0 flex-col justify-center gap-[1px]'>
|
||||
<Skeleton className='h-[14px] w-[100px]' />
|
||||
<Skeleton className='h-[13px] w-[200px]' />
|
||||
</div>
|
||||
<div className='flex flex-shrink-0 items-center gap-[8px]'>
|
||||
<Skeleton className='h-[30px] w-[40px] rounded-[4px]' />
|
||||
<Skeleton className='h-[30px] w-[54px] rounded-[4px]' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function Skills() {
|
||||
const params = useParams()
|
||||
const workspaceId = params.workspaceId as string
|
||||
|
||||
const { data: skills = [], isLoading, error, refetch: refetchSkills } = useSkills(workspaceId)
|
||||
const deleteSkillMutation = useDeleteSkill()
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [deletingSkills, setDeletingSkills] = useState<Set<string>>(new Set())
|
||||
const [editingSkill, setEditingSkill] = useState<SkillDefinition | null>(null)
|
||||
const [showAddForm, setShowAddForm] = useState(false)
|
||||
const [skillToDelete, setSkillToDelete] = useState<{ id: string; name: string } | null>(null)
|
||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
|
||||
|
||||
const filteredSkills = skills.filter((s) => {
|
||||
if (!searchTerm.trim()) return true
|
||||
const searchLower = searchTerm.toLowerCase()
|
||||
return (
|
||||
s.name.toLowerCase().includes(searchLower) ||
|
||||
s.description.toLowerCase().includes(searchLower)
|
||||
)
|
||||
})
|
||||
|
||||
const handleDeleteClick = (skillId: string) => {
|
||||
const s = skills.find((sk) => sk.id === skillId)
|
||||
if (!s) return
|
||||
|
||||
setSkillToDelete({ id: skillId, name: s.name })
|
||||
setShowDeleteDialog(true)
|
||||
}
|
||||
|
||||
const handleDeleteSkill = async () => {
|
||||
if (!skillToDelete) return
|
||||
|
||||
setDeletingSkills((prev) => new Set(prev).add(skillToDelete.id))
|
||||
setShowDeleteDialog(false)
|
||||
|
||||
try {
|
||||
await deleteSkillMutation.mutateAsync({
|
||||
workspaceId,
|
||||
skillId: skillToDelete.id,
|
||||
})
|
||||
logger.info(`Deleted skill: ${skillToDelete.id}`)
|
||||
} catch (error) {
|
||||
logger.error('Error deleting skill:', error)
|
||||
} finally {
|
||||
setDeletingSkills((prev) => {
|
||||
const next = new Set(prev)
|
||||
next.delete(skillToDelete.id)
|
||||
return next
|
||||
})
|
||||
setSkillToDelete(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSkillSaved = () => {
|
||||
setShowAddForm(false)
|
||||
setEditingSkill(null)
|
||||
refetchSkills()
|
||||
}
|
||||
|
||||
const hasSkills = skills && skills.length > 0
|
||||
const showEmptyState = !hasSkills && !showAddForm && !editingSkill
|
||||
const showNoResults = searchTerm.trim() && filteredSkills.length === 0 && skills.length > 0
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex h-full flex-col gap-[16px]'>
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-1 items-center gap-[8px] rounded-[8px] border border-[var(--border)] bg-transparent px-[8px] py-[5px] transition-colors duration-100 dark:bg-[var(--surface-4)] dark:hover:border-[var(--border-1)] dark:hover:bg-[var(--surface-5)]',
|
||||
isLoading && 'opacity-50'
|
||||
)}
|
||||
>
|
||||
<Search
|
||||
className='h-[14px] w-[14px] flex-shrink-0 text-[var(--text-tertiary)]'
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<Input
|
||||
placeholder='Search skills...'
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
disabled={isLoading}
|
||||
className='h-auto flex-1 border-0 bg-transparent p-0 font-base leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-100'
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={() => setShowAddForm(true)} disabled={isLoading} variant='tertiary'>
|
||||
<Plus className='mr-[6px] h-[13px] w-[13px]' />
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className='min-h-0 flex-1 overflow-y-auto'>
|
||||
{error ? (
|
||||
<div className='flex h-full flex-col items-center justify-center gap-[8px]'>
|
||||
<p className='text-[#DC2626] text-[11px] leading-tight dark:text-[#F87171]'>
|
||||
{error instanceof Error ? error.message : 'Failed to load skills'}
|
||||
</p>
|
||||
</div>
|
||||
) : isLoading ? (
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
<SkillSkeleton />
|
||||
<SkillSkeleton />
|
||||
<SkillSkeleton />
|
||||
</div>
|
||||
) : showEmptyState ? (
|
||||
<div className='flex h-full items-center justify-center text-[13px] text-[var(--text-muted)]'>
|
||||
Click "Add" above to get started
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex flex-col gap-[8px]'>
|
||||
{filteredSkills.map((s) => (
|
||||
<div key={s.id} className='flex items-center justify-between gap-[12px]'>
|
||||
<div className='flex min-w-0 flex-col justify-center gap-[1px]'>
|
||||
<span className='truncate font-medium text-[14px]'>{s.name}</span>
|
||||
<p className='truncate text-[13px] text-[var(--text-muted)]'>{s.description}</p>
|
||||
</div>
|
||||
<div className='flex flex-shrink-0 items-center gap-[8px]'>
|
||||
<Button variant='default' onClick={() => setEditingSkill(s)}>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant='ghost'
|
||||
onClick={() => handleDeleteClick(s.id)}
|
||||
disabled={deletingSkills.has(s.id)}
|
||||
>
|
||||
{deletingSkills.has(s.id) ? 'Deleting...' : 'Delete'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{showNoResults && (
|
||||
<div className='py-[16px] text-center text-[13px] text-[var(--text-muted)]'>
|
||||
No skills found matching "{searchTerm}"
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SkillModal
|
||||
open={showAddForm || !!editingSkill}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
setShowAddForm(false)
|
||||
setEditingSkill(null)
|
||||
}
|
||||
}}
|
||||
onSave={handleSkillSaved}
|
||||
onDelete={(skillId) => {
|
||||
setEditingSkill(null)
|
||||
handleDeleteClick(skillId)
|
||||
}}
|
||||
initialValues={editingSkill ?? undefined}
|
||||
/>
|
||||
|
||||
<Modal open={showDeleteDialog} onOpenChange={setShowDeleteDialog}>
|
||||
<ModalContent size='sm'>
|
||||
<ModalHeader>Delete Skill</ModalHeader>
|
||||
<ModalBody>
|
||||
<p className='text-[12px] text-[var(--text-secondary)]'>
|
||||
Are you sure you want to delete{' '}
|
||||
<span className='font-medium text-[var(--text-primary)]'>{skillToDelete?.name}</span>?{' '}
|
||||
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
|
||||
</p>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant='default' onClick={() => setShowDeleteDialog(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant='destructive' onClick={handleDeleteSkill}>
|
||||
Delete
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
SModalSidebarSection,
|
||||
SModalSidebarSectionTitle,
|
||||
} from '@/components/emcn'
|
||||
import { McpIcon } from '@/components/icons'
|
||||
import { AgentSkillsIcon, McpIcon } from '@/components/icons'
|
||||
import { useSession } from '@/lib/auth/auth-client'
|
||||
import { getSubscriptionStatus } from '@/lib/billing/client'
|
||||
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
General,
|
||||
Integrations,
|
||||
MCP,
|
||||
Skills,
|
||||
Subscription,
|
||||
TeamManagement,
|
||||
WorkflowMcpServers,
|
||||
@@ -93,6 +94,7 @@ type SettingsSection =
|
||||
| 'copilot'
|
||||
| 'mcp'
|
||||
| 'custom-tools'
|
||||
| 'skills'
|
||||
| 'workflow-mcp-servers'
|
||||
| 'debug'
|
||||
|
||||
@@ -156,6 +158,7 @@ const allNavigationItems: NavigationItem[] = [
|
||||
},
|
||||
{ id: 'integrations', label: 'Integrations', icon: Connections, section: 'tools' },
|
||||
{ id: 'custom-tools', label: 'Custom Tools', icon: Wrench, section: 'tools' },
|
||||
{ id: 'skills', label: 'Skills', icon: AgentSkillsIcon, section: 'tools' },
|
||||
{ id: 'mcp', label: 'MCP Tools', icon: McpIcon, section: 'tools' },
|
||||
{ id: 'environment', label: 'Environment', icon: FolderCode, section: 'system' },
|
||||
{ id: 'apikeys', label: 'API Keys', icon: Key, section: 'system' },
|
||||
@@ -265,6 +268,9 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
|
||||
if (item.id === 'custom-tools' && permissionConfig.disableCustomTools) {
|
||||
return false
|
||||
}
|
||||
if (item.id === 'skills' && permissionConfig.disableSkills) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Self-hosted override allows showing the item when not on hosted
|
||||
if (item.selfHostedOverride && !isHosted) {
|
||||
@@ -556,6 +562,7 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) {
|
||||
{effectiveActiveSection === 'copilot' && <Copilot />}
|
||||
{effectiveActiveSection === 'mcp' && <MCP initialServerId={pendingMcpServerId} />}
|
||||
{effectiveActiveSection === 'custom-tools' && <CustomTools />}
|
||||
{effectiveActiveSection === 'skills' && <Skills />}
|
||||
{effectiveActiveSection === 'workflow-mcp-servers' && <WorkflowMcpServers />}
|
||||
{effectiveActiveSection === 'debug' && <Debug />}
|
||||
</SModalMainBody>
|
||||
|
||||
@@ -21,7 +21,6 @@ import { executeWorkflowCore } from '@/lib/workflows/executor/execution-core'
|
||||
import { PauseResumeManager } from '@/lib/workflows/executor/human-in-the-loop-manager'
|
||||
import { loadDeployedWorkflowState } from '@/lib/workflows/persistence/utils'
|
||||
import { getWorkflowById } from '@/lib/workflows/utils'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { ExecutionSnapshot } from '@/executor/execution/snapshot'
|
||||
import type { ExecutionMetadata } from '@/executor/execution/types'
|
||||
import { hasExecutionResult } from '@/executor/utils/errors'
|
||||
@@ -75,21 +74,8 @@ async function processTriggerFileOutputs(
|
||||
logger.error(`[${context.requestId}] Error processing ${currentPath}:`, error)
|
||||
processed[key] = val
|
||||
}
|
||||
} else if (
|
||||
outputDef &&
|
||||
typeof outputDef === 'object' &&
|
||||
(outputDef.type === 'object' || outputDef.type === 'json') &&
|
||||
outputDef.properties
|
||||
) {
|
||||
// Explicit object schema with properties - recurse into properties
|
||||
processed[key] = await processTriggerFileOutputs(
|
||||
val,
|
||||
outputDef.properties,
|
||||
context,
|
||||
currentPath
|
||||
)
|
||||
} else if (outputDef && typeof outputDef === 'object' && !outputDef.type) {
|
||||
// Nested object in schema (flat pattern) - recurse with the nested schema
|
||||
// Nested object in schema - recurse with the nested schema
|
||||
processed[key] = await processTriggerFileOutputs(val, outputDef, context, currentPath)
|
||||
} else {
|
||||
// Not a file output - keep as is
|
||||
@@ -419,23 +405,11 @@ async function executeWebhookJobInternal(
|
||||
const rawSelectedTriggerId = triggerBlock?.subBlocks?.selectedTriggerId?.value
|
||||
const rawTriggerId = triggerBlock?.subBlocks?.triggerId?.value
|
||||
|
||||
let resolvedTriggerId = [rawSelectedTriggerId, rawTriggerId].find(
|
||||
const resolvedTriggerId = [rawSelectedTriggerId, rawTriggerId].find(
|
||||
(candidate): candidate is string =>
|
||||
typeof candidate === 'string' && isTriggerValid(candidate)
|
||||
)
|
||||
|
||||
if (!resolvedTriggerId) {
|
||||
const blockConfig = getBlock(triggerBlock.type)
|
||||
if (blockConfig?.category === 'triggers' && isTriggerValid(triggerBlock.type)) {
|
||||
resolvedTriggerId = triggerBlock.type
|
||||
} else if (triggerBlock.triggerMode && blockConfig?.triggers?.enabled) {
|
||||
const available = blockConfig.triggers?.available?.[0]
|
||||
if (available && isTriggerValid(available)) {
|
||||
resolvedTriggerId = available
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (resolvedTriggerId) {
|
||||
const triggerConfig = getTrigger(resolvedTriggerId)
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// Use the real registry module, not the global mock from vitest.setup.ts
|
||||
vi.unmock('@/blocks/registry')
|
||||
|
||||
import { generateRouterPrompt } from '@/blocks/blocks/router'
|
||||
@@ -15,7 +14,7 @@ import {
|
||||
} from '@/blocks/registry'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
|
||||
describe('Blocks Module', () => {
|
||||
describe.concurrent('Blocks Module', () => {
|
||||
describe('Registry', () => {
|
||||
it('should have a non-empty registry of blocks', () => {
|
||||
expect(Object.keys(registry).length).toBeGreaterThan(0)
|
||||
@@ -409,6 +408,7 @@ describe('Blocks Module', () => {
|
||||
'workflow-input-mapper',
|
||||
'text',
|
||||
'router-input',
|
||||
'skill-input',
|
||||
]
|
||||
|
||||
const blocks = getAllBlocks()
|
||||
|
||||
@@ -407,6 +407,12 @@ Return ONLY the JSON array.`,
|
||||
type: 'tool-input',
|
||||
defaultValue: [],
|
||||
},
|
||||
{
|
||||
id: 'skills',
|
||||
title: 'Skills',
|
||||
type: 'skill-input',
|
||||
defaultValue: [],
|
||||
},
|
||||
{
|
||||
id: 'apiKey',
|
||||
title: 'API Key',
|
||||
@@ -769,6 +775,7 @@ Example 3 (Array Input):
|
||||
description: 'Thinking level for models with extended thinking (Anthropic Claude, Gemini 3)',
|
||||
},
|
||||
tools: { type: 'json', description: 'Available tools configuration' },
|
||||
skills: { type: 'json', description: 'Selected skills configuration' },
|
||||
},
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Generated response content' },
|
||||
|
||||
@@ -810,29 +810,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
placeholder: 'Number of items to return (default: 50)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'linear_read_issues',
|
||||
'linear_search_issues',
|
||||
'linear_list_comments',
|
||||
'linear_list_projects',
|
||||
'linear_list_users',
|
||||
'linear_list_teams',
|
||||
'linear_list_labels',
|
||||
'linear_list_workflow_states',
|
||||
'linear_list_cycles',
|
||||
'linear_list_attachments',
|
||||
'linear_list_issue_relations',
|
||||
'linear_list_favorites',
|
||||
'linear_list_project_updates',
|
||||
'linear_list_notifications',
|
||||
'linear_list_customer_statuses',
|
||||
'linear_list_customer_tiers',
|
||||
'linear_list_customers',
|
||||
'linear_list_customer_requests',
|
||||
'linear_list_project_labels',
|
||||
'linear_list_project_milestones',
|
||||
'linear_list_project_statuses',
|
||||
],
|
||||
value: ['linear_list_favorites'],
|
||||
},
|
||||
},
|
||||
// Pagination - After (for list operations)
|
||||
@@ -843,29 +821,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
placeholder: 'Cursor for pagination',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: [
|
||||
'linear_read_issues',
|
||||
'linear_search_issues',
|
||||
'linear_list_comments',
|
||||
'linear_list_projects',
|
||||
'linear_list_users',
|
||||
'linear_list_teams',
|
||||
'linear_list_labels',
|
||||
'linear_list_workflow_states',
|
||||
'linear_list_cycles',
|
||||
'linear_list_attachments',
|
||||
'linear_list_issue_relations',
|
||||
'linear_list_favorites',
|
||||
'linear_list_project_updates',
|
||||
'linear_list_notifications',
|
||||
'linear_list_customers',
|
||||
'linear_list_customer_requests',
|
||||
'linear_list_customer_statuses',
|
||||
'linear_list_customer_tiers',
|
||||
'linear_list_project_labels',
|
||||
'linear_list_project_milestones',
|
||||
'linear_list_project_statuses',
|
||||
],
|
||||
value: ['linear_list_favorites'],
|
||||
},
|
||||
},
|
||||
// Project health (for project updates)
|
||||
@@ -1097,6 +1053,28 @@ Return ONLY the description text - no explanations.`,
|
||||
value: ['linear_create_customer_request', 'linear_update_customer_request'],
|
||||
},
|
||||
},
|
||||
// Pagination - first
|
||||
{
|
||||
id: 'first',
|
||||
title: 'Limit',
|
||||
type: 'short-input',
|
||||
placeholder: 'Number of items (default: 50)',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['linear_list_customers', 'linear_list_customer_requests'],
|
||||
},
|
||||
},
|
||||
// Pagination - after
|
||||
{
|
||||
id: 'after',
|
||||
title: 'After Cursor',
|
||||
type: 'short-input',
|
||||
placeholder: 'Cursor for pagination',
|
||||
condition: {
|
||||
field: 'operation',
|
||||
value: ['linear_list_customers', 'linear_list_customer_requests'],
|
||||
},
|
||||
},
|
||||
// Customer ID for get/update/delete/merge operations
|
||||
{
|
||||
id: 'customerIdTarget',
|
||||
@@ -1515,8 +1493,6 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
teamId: effectiveTeamId || undefined,
|
||||
projectId: effectiveProjectId || undefined,
|
||||
includeArchived: params.includeArchived,
|
||||
first: params.first ? Number(params.first) : undefined,
|
||||
after: params.after,
|
||||
}
|
||||
|
||||
case 'linear_get_issue':
|
||||
@@ -1582,8 +1558,6 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
query: params.query.trim(),
|
||||
teamId: effectiveTeamId,
|
||||
includeArchived: params.includeArchived,
|
||||
first: params.first ? Number(params.first) : undefined,
|
||||
after: params.after,
|
||||
}
|
||||
|
||||
case 'linear_add_label_to_issue':
|
||||
@@ -1633,8 +1607,6 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
return {
|
||||
...baseParams,
|
||||
issueId: params.issueId.trim(),
|
||||
first: params.first ? Number(params.first) : undefined,
|
||||
after: params.after,
|
||||
}
|
||||
|
||||
case 'linear_list_projects':
|
||||
@@ -1642,8 +1614,6 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
...baseParams,
|
||||
teamId: effectiveTeamId,
|
||||
includeArchived: params.includeArchived,
|
||||
first: params.first ? Number(params.first) : undefined,
|
||||
after: params.after,
|
||||
}
|
||||
|
||||
case 'linear_get_project':
|
||||
@@ -1695,12 +1665,6 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
|
||||
case 'linear_list_users':
|
||||
case 'linear_list_teams':
|
||||
return {
|
||||
...baseParams,
|
||||
first: params.first ? Number(params.first) : undefined,
|
||||
after: params.after,
|
||||
}
|
||||
|
||||
case 'linear_get_viewer':
|
||||
return baseParams
|
||||
|
||||
@@ -1708,8 +1672,6 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
return {
|
||||
...baseParams,
|
||||
teamId: effectiveTeamId,
|
||||
first: params.first ? Number(params.first) : undefined,
|
||||
after: params.after,
|
||||
}
|
||||
|
||||
case 'linear_create_label':
|
||||
@@ -1747,8 +1709,6 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
return {
|
||||
...baseParams,
|
||||
teamId: effectiveTeamId,
|
||||
first: params.first ? Number(params.first) : undefined,
|
||||
after: params.after,
|
||||
}
|
||||
|
||||
case 'linear_create_workflow_state':
|
||||
@@ -1778,8 +1738,6 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
return {
|
||||
...baseParams,
|
||||
teamId: effectiveTeamId,
|
||||
first: params.first ? Number(params.first) : undefined,
|
||||
after: params.after,
|
||||
}
|
||||
|
||||
case 'linear_get_cycle':
|
||||
@@ -1843,8 +1801,6 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
return {
|
||||
...baseParams,
|
||||
issueId: params.issueId.trim(),
|
||||
first: params.first ? Number(params.first) : undefined,
|
||||
after: params.after,
|
||||
}
|
||||
|
||||
case 'linear_update_attachment':
|
||||
@@ -1884,8 +1840,6 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
return {
|
||||
...baseParams,
|
||||
issueId: params.issueId.trim(),
|
||||
first: params.first ? Number(params.first) : undefined,
|
||||
after: params.after,
|
||||
}
|
||||
|
||||
case 'linear_delete_issue_relation':
|
||||
@@ -1932,16 +1886,10 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
return {
|
||||
...baseParams,
|
||||
projectId: effectiveProjectId,
|
||||
first: params.first ? Number(params.first) : undefined,
|
||||
after: params.after,
|
||||
}
|
||||
|
||||
case 'linear_list_notifications':
|
||||
return {
|
||||
...baseParams,
|
||||
first: params.first ? Number(params.first) : undefined,
|
||||
after: params.after,
|
||||
}
|
||||
return baseParams
|
||||
|
||||
case 'linear_update_notification':
|
||||
if (!params.notificationId?.trim()) {
|
||||
@@ -2070,9 +2018,9 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
return {
|
||||
...baseParams,
|
||||
name: params.statusName.trim(),
|
||||
displayName: params.statusDisplayName?.trim() || params.statusName.trim(),
|
||||
color: params.statusColor.trim(),
|
||||
description: params.statusDescription?.trim() || undefined,
|
||||
displayName: params.statusDisplayName?.trim() || undefined,
|
||||
}
|
||||
|
||||
case 'linear_update_customer_status':
|
||||
@@ -2083,9 +2031,9 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
...baseParams,
|
||||
statusId: params.statusId.trim(),
|
||||
name: params.statusName?.trim() || undefined,
|
||||
displayName: params.statusDisplayName?.trim() || undefined,
|
||||
color: params.statusColor?.trim() || undefined,
|
||||
description: params.statusDescription?.trim() || undefined,
|
||||
displayName: params.statusDisplayName?.trim() || undefined,
|
||||
}
|
||||
|
||||
case 'linear_delete_customer_status':
|
||||
@@ -2098,11 +2046,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
}
|
||||
|
||||
case 'linear_list_customer_statuses':
|
||||
return {
|
||||
...baseParams,
|
||||
first: params.first ? Number(params.first) : undefined,
|
||||
after: params.after,
|
||||
}
|
||||
return baseParams
|
||||
|
||||
// Customer Tier Operations
|
||||
case 'linear_create_customer_tier':
|
||||
@@ -2140,11 +2084,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
}
|
||||
|
||||
case 'linear_list_customer_tiers':
|
||||
return {
|
||||
...baseParams,
|
||||
first: params.first ? Number(params.first) : undefined,
|
||||
after: params.after,
|
||||
}
|
||||
return baseParams
|
||||
|
||||
// Project Management Operations
|
||||
case 'linear_delete_project':
|
||||
@@ -2195,8 +2135,6 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
return {
|
||||
...baseParams,
|
||||
projectId: effectiveProjectId || undefined,
|
||||
first: params.first ? Number(params.first) : undefined,
|
||||
after: params.after,
|
||||
}
|
||||
|
||||
case 'linear_add_label_to_project':
|
||||
@@ -2260,8 +2198,6 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
return {
|
||||
...baseParams,
|
||||
projectId: params.projectIdForMilestone.trim(),
|
||||
first: params.first ? Number(params.first) : undefined,
|
||||
after: params.after,
|
||||
}
|
||||
|
||||
// Project Status Operations
|
||||
@@ -2309,11 +2245,7 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
}
|
||||
|
||||
case 'linear_list_project_statuses':
|
||||
return {
|
||||
...baseParams,
|
||||
first: params.first ? Number(params.first) : undefined,
|
||||
after: params.after,
|
||||
}
|
||||
return baseParams
|
||||
|
||||
default:
|
||||
return baseParams
|
||||
@@ -2389,9 +2321,9 @@ Return ONLY the date string in YYYY-MM-DD format - no explanations, no quotes, n
|
||||
// Customer status and tier inputs
|
||||
statusId: { type: 'string', description: 'Status identifier' },
|
||||
statusName: { type: 'string', description: 'Status name' },
|
||||
statusDisplayName: { type: 'string', description: 'Status display name' },
|
||||
statusColor: { type: 'string', description: 'Status color in hex format' },
|
||||
statusDescription: { type: 'string', description: 'Status description' },
|
||||
statusDisplayName: { type: 'string', description: 'Status display name' },
|
||||
tierId: { type: 'string', description: 'Tier identifier' },
|
||||
tierName: { type: 'string', description: 'Tier name' },
|
||||
tierDisplayName: { type: 'string', description: 'Tier display name' },
|
||||
|
||||
@@ -42,7 +42,6 @@ export const WorkflowBlock: BlockConfig = {
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Execution success status' },
|
||||
childWorkflowName: { type: 'string', description: 'Child workflow name' },
|
||||
childWorkflowId: { type: 'string', description: 'Child workflow ID' },
|
||||
result: { type: 'json', description: 'Workflow execution result' },
|
||||
error: { type: 'string', description: 'Error message' },
|
||||
childTraceSpans: {
|
||||
|
||||
@@ -41,7 +41,6 @@ export const WorkflowInputBlock: BlockConfig = {
|
||||
outputs: {
|
||||
success: { type: 'boolean', description: 'Execution success status' },
|
||||
childWorkflowName: { type: 'string', description: 'Child workflow name' },
|
||||
childWorkflowId: { type: 'string', description: 'Child workflow ID' },
|
||||
result: { type: 'json', description: 'Workflow execution result' },
|
||||
error: { type: 'string', description: 'Error message' },
|
||||
childTraceSpans: {
|
||||
|
||||
@@ -51,6 +51,7 @@ export type SubBlockType =
|
||||
| 'code' // Code editor
|
||||
| 'switch' // Toggle button
|
||||
| 'tool-input' // Tool configuration
|
||||
| 'skill-input' // Skill selection for agent blocks
|
||||
| 'checkbox-list' // Multiple selection
|
||||
| 'grouped-checkbox-list' // Grouped, scrollable checkbox list with select all
|
||||
| 'condition-input' // Conditional logic
|
||||
|
||||
@@ -5436,3 +5436,24 @@ export function EnrichSoIcon(props: SVGProps<SVGSVGElement>) {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function AgentSkillsIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
width='24'
|
||||
height='24'
|
||||
viewBox='0 0 32 32'
|
||||
fill='none'
|
||||
>
|
||||
<path d='M16 0.5L29.4234 8.25V23.75L16 31.5L2.57661 23.75V8.25L16 0.5Z' fill='currentColor' />
|
||||
<path
|
||||
d='M16 6L24.6603 11V21L16 26L7.33975 21V11L16 6Z'
|
||||
fill='currentColor'
|
||||
stroke='var(--background, white)'
|
||||
strokeWidth='3'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -367,6 +367,12 @@ export function AccessControl() {
|
||||
category: 'Tools',
|
||||
configKey: 'disableCustomTools' as const,
|
||||
},
|
||||
{
|
||||
id: 'disable-skills',
|
||||
label: 'Skills',
|
||||
category: 'Tools',
|
||||
configKey: 'disableSkills' as const,
|
||||
},
|
||||
{
|
||||
id: 'hide-trace-spans',
|
||||
label: 'Trace Spans',
|
||||
@@ -950,6 +956,7 @@ export function AccessControl() {
|
||||
!editingConfig?.hideFilesTab &&
|
||||
!editingConfig?.disableMcpTools &&
|
||||
!editingConfig?.disableCustomTools &&
|
||||
!editingConfig?.disableSkills &&
|
||||
!editingConfig?.hideTraceSpans &&
|
||||
!editingConfig?.disableInvitations &&
|
||||
!editingConfig?.hideDeployApi &&
|
||||
@@ -969,6 +976,7 @@ export function AccessControl() {
|
||||
hideFilesTab: allVisible,
|
||||
disableMcpTools: allVisible,
|
||||
disableCustomTools: allVisible,
|
||||
disableSkills: allVisible,
|
||||
hideTraceSpans: allVisible,
|
||||
disableInvitations: allVisible,
|
||||
hideDeployApi: allVisible,
|
||||
@@ -989,6 +997,7 @@ export function AccessControl() {
|
||||
!editingConfig?.hideFilesTab &&
|
||||
!editingConfig?.disableMcpTools &&
|
||||
!editingConfig?.disableCustomTools &&
|
||||
!editingConfig?.disableSkills &&
|
||||
!editingConfig?.hideTraceSpans &&
|
||||
!editingConfig?.disableInvitations &&
|
||||
!editingConfig?.hideDeployApi &&
|
||||
|
||||
@@ -43,6 +43,13 @@ export class CustomToolsNotAllowedError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export class SkillsNotAllowedError extends Error {
|
||||
constructor() {
|
||||
super('Skills are not allowed based on your permission group settings')
|
||||
this.name = 'SkillsNotAllowedError'
|
||||
}
|
||||
}
|
||||
|
||||
export class InvitationsNotAllowedError extends Error {
|
||||
constructor() {
|
||||
super('Invitations are not allowed based on your permission group settings')
|
||||
@@ -201,6 +208,26 @@ export async function validateCustomToolsAllowed(
|
||||
}
|
||||
}
|
||||
|
||||
export async function validateSkillsAllowed(
|
||||
userId: string | undefined,
|
||||
ctx?: ExecutionContext
|
||||
): Promise<void> {
|
||||
if (!userId) {
|
||||
return
|
||||
}
|
||||
|
||||
const config = await getPermissionConfig(userId, ctx)
|
||||
|
||||
if (!config) {
|
||||
return
|
||||
}
|
||||
|
||||
if (config.disableSkills) {
|
||||
logger.warn('Skills blocked by permission group', { userId })
|
||||
throw new SkillsNotAllowedError()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if the user is allowed to send invitations.
|
||||
* Also checks the global feature flag.
|
||||
|
||||
@@ -2478,9 +2478,6 @@ describe('EdgeManager', () => {
|
||||
expect(readyNodes).toContain(otherBranchId)
|
||||
expect(readyNodes).not.toContain(sentinelStartId)
|
||||
|
||||
// sentinel_end should NOT be ready - it's on a fully deactivated path
|
||||
expect(readyNodes).not.toContain(sentinelEndId)
|
||||
|
||||
// afterLoop should NOT be ready - its incoming edge from sentinel_end should be deactivated
|
||||
expect(readyNodes).not.toContain(afterLoopId)
|
||||
|
||||
@@ -2548,84 +2545,6 @@ describe('EdgeManager', () => {
|
||||
expect(edgeManager.isNodeReady(afterParallelNode)).toBe(true)
|
||||
})
|
||||
|
||||
it('should not queue loop sentinel-end when upstream condition deactivates entire loop branch', () => {
|
||||
// Regression test for: upstream condition → (if) → ... many blocks ... → sentinel_start → body → sentinel_end
|
||||
// → (else) → exit_block
|
||||
// When condition takes "else", the deep cascade deactivation should NOT queue sentinel_end.
|
||||
// Previously, sentinel_end was flagged as a cascadeTarget (terminal control node) and
|
||||
// spuriously queued, causing it to attempt loop scope initialization and fail.
|
||||
|
||||
const conditionId = 'condition'
|
||||
const intermediateId = 'intermediate'
|
||||
const sentinelStartId = 'sentinel-start'
|
||||
const loopBodyId = 'loop-body'
|
||||
const sentinelEndId = 'sentinel-end'
|
||||
const afterLoopId = 'after-loop'
|
||||
const exitBlockId = 'exit-block'
|
||||
|
||||
const conditionNode = createMockNode(conditionId, [
|
||||
{ target: intermediateId, sourceHandle: 'condition-if' },
|
||||
{ target: exitBlockId, sourceHandle: 'condition-else' },
|
||||
])
|
||||
|
||||
const intermediateNode = createMockNode(
|
||||
intermediateId,
|
||||
[{ target: sentinelStartId }],
|
||||
[conditionId]
|
||||
)
|
||||
|
||||
const sentinelStartNode = createMockNode(
|
||||
sentinelStartId,
|
||||
[{ target: loopBodyId }],
|
||||
[intermediateId]
|
||||
)
|
||||
|
||||
const loopBodyNode = createMockNode(
|
||||
loopBodyId,
|
||||
[{ target: sentinelEndId }],
|
||||
[sentinelStartId]
|
||||
)
|
||||
|
||||
const sentinelEndNode = createMockNode(
|
||||
sentinelEndId,
|
||||
[
|
||||
{ target: sentinelStartId, sourceHandle: 'loop_continue' },
|
||||
{ target: afterLoopId, sourceHandle: 'loop_exit' },
|
||||
],
|
||||
[loopBodyId]
|
||||
)
|
||||
|
||||
const afterLoopNode = createMockNode(afterLoopId, [], [sentinelEndId])
|
||||
const exitBlockNode = createMockNode(exitBlockId, [], [conditionId])
|
||||
|
||||
const nodes = new Map<string, DAGNode>([
|
||||
[conditionId, conditionNode],
|
||||
[intermediateId, intermediateNode],
|
||||
[sentinelStartId, sentinelStartNode],
|
||||
[loopBodyId, loopBodyNode],
|
||||
[sentinelEndId, sentinelEndNode],
|
||||
[afterLoopId, afterLoopNode],
|
||||
[exitBlockId, exitBlockNode],
|
||||
])
|
||||
|
||||
const dag = createMockDAG(nodes)
|
||||
const edgeManager = new EdgeManager(dag)
|
||||
|
||||
const readyNodes = edgeManager.processOutgoingEdges(conditionNode, {
|
||||
selectedOption: 'else',
|
||||
})
|
||||
|
||||
// Only exitBlock should be ready
|
||||
expect(readyNodes).toContain(exitBlockId)
|
||||
|
||||
// Nothing on the deactivated path should be queued
|
||||
expect(readyNodes).not.toContain(intermediateId)
|
||||
expect(readyNodes).not.toContain(sentinelStartId)
|
||||
expect(readyNodes).not.toContain(loopBodyId)
|
||||
expect(readyNodes).not.toContain(sentinelEndId)
|
||||
expect(readyNodes).not.toContain(afterLoopId)
|
||||
})
|
||||
|
||||
it('should still correctly handle normal loop exit (not deactivate when loop runs)', () => {
|
||||
// When a loop actually executes and exits normally, after_loop should become ready
|
||||
const sentinelStartId = 'sentinel-start'
|
||||
|
||||
@@ -71,13 +71,7 @@ export class EdgeManager {
|
||||
|
||||
for (const targetId of cascadeTargets) {
|
||||
if (!readyNodes.includes(targetId) && !activatedTargets.includes(targetId)) {
|
||||
// Only queue cascade terminal control nodes when ALL outgoing edges from the
|
||||
// current node were deactivated (dead-end scenario). When some edges are
|
||||
// activated, terminal control nodes on deactivated branches should NOT be
|
||||
// queued - they will be reached through the normal activated path's completion.
|
||||
// This prevents loop/parallel sentinels on fully deactivated paths (e.g., an
|
||||
// upstream condition took a different branch) from being spuriously executed.
|
||||
if (activatedTargets.length === 0 && this.isTargetReady(targetId)) {
|
||||
if (this.isTargetReady(targetId)) {
|
||||
readyNodes.push(targetId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,9 +11,15 @@ import {
|
||||
validateCustomToolsAllowed,
|
||||
validateMcpToolsAllowed,
|
||||
validateModelProvider,
|
||||
validateSkillsAllowed,
|
||||
} from '@/ee/access-control/utils/permission-check'
|
||||
import { AGENT, BlockType, DEFAULTS, REFERENCE, stripCustomToolPrefix } from '@/executor/constants'
|
||||
import { memoryService } from '@/executor/handlers/agent/memory'
|
||||
import {
|
||||
buildLoadSkillTool,
|
||||
buildSkillsSystemPromptSection,
|
||||
resolveSkillMetadata,
|
||||
} from '@/executor/handlers/agent/skills-resolver'
|
||||
import type {
|
||||
AgentInputs,
|
||||
Message,
|
||||
@@ -57,8 +63,21 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
|
||||
const providerId = getProviderFromModel(model)
|
||||
const formattedTools = await this.formatTools(ctx, filteredInputs.tools || [])
|
||||
|
||||
// Resolve skill metadata for progressive disclosure
|
||||
const skillInputs = filteredInputs.skills ?? []
|
||||
let skillMetadata: Array<{ name: string; description: string }> = []
|
||||
if (skillInputs.length > 0 && ctx.workspaceId) {
|
||||
await validateSkillsAllowed(ctx.userId, ctx)
|
||||
skillMetadata = await resolveSkillMetadata(skillInputs, ctx.workspaceId)
|
||||
if (skillMetadata.length > 0) {
|
||||
const skillNames = skillMetadata.map((s) => s.name)
|
||||
formattedTools.push(buildLoadSkillTool(skillNames))
|
||||
}
|
||||
}
|
||||
|
||||
const streamingConfig = this.getStreamingConfig(ctx, block)
|
||||
const messages = await this.buildMessages(ctx, filteredInputs)
|
||||
const messages = await this.buildMessages(ctx, filteredInputs, skillMetadata)
|
||||
|
||||
const providerRequest = this.buildProviderRequest({
|
||||
ctx,
|
||||
@@ -723,7 +742,8 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
|
||||
private async buildMessages(
|
||||
ctx: ExecutionContext,
|
||||
inputs: AgentInputs
|
||||
inputs: AgentInputs,
|
||||
skillMetadata: Array<{ name: string; description: string }> = []
|
||||
): Promise<Message[] | undefined> {
|
||||
const messages: Message[] = []
|
||||
const memoryEnabled = inputs.memoryType && inputs.memoryType !== 'none'
|
||||
@@ -803,6 +823,20 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
messages.unshift(...systemMessages)
|
||||
}
|
||||
|
||||
// 8. Inject skill metadata into the system message (progressive disclosure)
|
||||
if (skillMetadata.length > 0) {
|
||||
const skillSection = buildSkillsSystemPromptSection(skillMetadata)
|
||||
const systemIdx = messages.findIndex((m) => m.role === 'system')
|
||||
if (systemIdx >= 0) {
|
||||
messages[systemIdx] = {
|
||||
...messages[systemIdx],
|
||||
content: messages[systemIdx].content + skillSection,
|
||||
}
|
||||
} else {
|
||||
messages.unshift({ role: 'system', content: skillSection.trim() })
|
||||
}
|
||||
}
|
||||
|
||||
return messages.length > 0 ? messages : undefined
|
||||
}
|
||||
|
||||
|
||||
122
apps/sim/executor/handlers/agent/skills-resolver.ts
Normal file
122
apps/sim/executor/handlers/agent/skills-resolver.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { db } from '@sim/db'
|
||||
import { skill } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq, inArray } from 'drizzle-orm'
|
||||
import type { SkillInput } from '@/executor/handlers/agent/types'
|
||||
|
||||
const logger = createLogger('SkillsResolver')
|
||||
|
||||
function escapeXml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
}
|
||||
|
||||
interface SkillMetadata {
|
||||
name: string
|
||||
description: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch skill metadata (name + description) for system prompt injection.
|
||||
* Only returns lightweight data so the LLM knows what skills are available.
|
||||
*/
|
||||
export async function resolveSkillMetadata(
|
||||
skillInputs: SkillInput[],
|
||||
workspaceId: string
|
||||
): Promise<SkillMetadata[]> {
|
||||
if (!skillInputs.length || !workspaceId) return []
|
||||
|
||||
const skillIds = skillInputs.map((s) => s.skillId)
|
||||
|
||||
try {
|
||||
const rows = await db
|
||||
.select({ name: skill.name, description: skill.description })
|
||||
.from(skill)
|
||||
.where(and(eq(skill.workspaceId, workspaceId), inArray(skill.id, skillIds)))
|
||||
|
||||
return rows
|
||||
} catch (error) {
|
||||
logger.error('Failed to resolve skill metadata', { error, skillIds, workspaceId })
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch full skill content for a load_skill tool response.
|
||||
* Called when the LLM decides a skill is relevant and invokes load_skill.
|
||||
*/
|
||||
export async function resolveSkillContent(
|
||||
skillName: string,
|
||||
workspaceId: string
|
||||
): Promise<string | null> {
|
||||
if (!skillName || !workspaceId) return null
|
||||
|
||||
try {
|
||||
const rows = await db
|
||||
.select({ content: skill.content, name: skill.name })
|
||||
.from(skill)
|
||||
.where(and(eq(skill.workspaceId, workspaceId), eq(skill.name, skillName)))
|
||||
.limit(1)
|
||||
|
||||
if (rows.length === 0) {
|
||||
logger.warn('Skill not found', { skillName, workspaceId })
|
||||
return null
|
||||
}
|
||||
|
||||
return rows[0].content
|
||||
} catch (error) {
|
||||
logger.error('Failed to resolve skill content', { error, skillName, workspaceId })
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the system prompt section that lists available skills.
|
||||
* Uses XML format per the agentskills.io integration guide.
|
||||
*/
|
||||
export function buildSkillsSystemPromptSection(skills: SkillMetadata[]): string {
|
||||
if (!skills.length) return ''
|
||||
|
||||
const skillEntries = skills
|
||||
.map(
|
||||
(s) =>
|
||||
` <skill name="${escapeXml(s.name)}">\n <description>${escapeXml(s.description)}</description>\n </skill>`
|
||||
)
|
||||
.join('\n')
|
||||
|
||||
return [
|
||||
'',
|
||||
'You have access to the following skills. Use the load_skill tool to activate a skill when relevant.',
|
||||
'',
|
||||
'<available_skills>',
|
||||
skillEntries,
|
||||
'</available_skills>',
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the load_skill tool definition for injection into the tools array.
|
||||
* Returns a ProviderToolConfig-compatible object so all providers can process it.
|
||||
*/
|
||||
export function buildLoadSkillTool(skillNames: string[]) {
|
||||
return {
|
||||
id: 'load_skill',
|
||||
name: 'load_skill',
|
||||
description: `Load a skill to get specialized instructions. Available skills: ${skillNames.join(', ')}`,
|
||||
params: {},
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
skill_name: {
|
||||
type: 'string',
|
||||
description: 'Name of the skill to load',
|
||||
enum: skillNames,
|
||||
},
|
||||
},
|
||||
required: ['skill_name'],
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,14 @@
|
||||
export interface SkillInput {
|
||||
skillId: string
|
||||
name?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export interface AgentInputs {
|
||||
model?: string
|
||||
responseFormat?: string | object
|
||||
tools?: ToolInput[]
|
||||
skills?: SkillInput[]
|
||||
// Legacy inputs (backward compatible)
|
||||
systemPrompt?: string
|
||||
userPrompt?: string | object
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import {
|
||||
extractFieldsFromSchema,
|
||||
parseResponseFormatSafely,
|
||||
} from '@/lib/core/utils/response-format'
|
||||
import { normalizeInputFormatValue } from '@/lib/workflows/input-format'
|
||||
import { isTriggerBehavior, normalizeName } from '@/executor/constants'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
@@ -47,53 +43,23 @@ function getInputFormatFields(block: SerializedBlock): OutputSchema {
|
||||
const schema: OutputSchema = {}
|
||||
for (const field of inputFormat) {
|
||||
if (!field.name) continue
|
||||
schema[field.name] = { type: field.type || 'any' }
|
||||
schema[field.name] = {
|
||||
type: (field.type || 'any') as 'string' | 'number' | 'boolean' | 'object' | 'array' | 'any',
|
||||
}
|
||||
}
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
function getEvaluatorMetricsSchema(block: SerializedBlock): OutputSchema | undefined {
|
||||
if (block.metadata?.id !== 'evaluator') return undefined
|
||||
|
||||
const metrics = block.config?.params?.metrics
|
||||
if (!Array.isArray(metrics) || metrics.length === 0) return undefined
|
||||
|
||||
const validMetrics = metrics.filter(
|
||||
(m: { name?: string }) => m?.name && typeof m.name === 'string'
|
||||
)
|
||||
if (validMetrics.length === 0) return undefined
|
||||
|
||||
const schema: OutputSchema = { ...(block.outputs as OutputSchema) }
|
||||
for (const metric of validMetrics) {
|
||||
schema[metric.name.toLowerCase()] = { type: 'number' }
|
||||
}
|
||||
return schema
|
||||
}
|
||||
|
||||
function getResponseFormatSchema(block: SerializedBlock): OutputSchema | undefined {
|
||||
const responseFormatValue = block.config?.params?.responseFormat
|
||||
if (!responseFormatValue) return undefined
|
||||
|
||||
const parsed = parseResponseFormatSafely(responseFormatValue, block.id)
|
||||
if (!parsed) return undefined
|
||||
|
||||
const fields = extractFieldsFromSchema(parsed)
|
||||
if (fields.length === 0) return undefined
|
||||
|
||||
const schema: OutputSchema = {}
|
||||
for (const field of fields) {
|
||||
schema[field.name] = { type: field.type || 'any' }
|
||||
}
|
||||
return schema
|
||||
}
|
||||
|
||||
export function getBlockSchema(
|
||||
block: SerializedBlock,
|
||||
toolConfig?: ToolConfig
|
||||
): OutputSchema | undefined {
|
||||
const blockType = block.metadata?.id
|
||||
|
||||
// For blocks that expose inputFormat as outputs, always merge them
|
||||
// This includes both triggers (start_trigger, generic_webhook) and
|
||||
// non-triggers (starter, human_in_the_loop) that have inputFormat
|
||||
if (
|
||||
blockType &&
|
||||
BLOCKS_WITH_INPUT_FORMAT_OUTPUTS.includes(
|
||||
@@ -108,16 +74,6 @@ export function getBlockSchema(
|
||||
}
|
||||
}
|
||||
|
||||
const evaluatorSchema = getEvaluatorMetricsSchema(block)
|
||||
if (evaluatorSchema) {
|
||||
return evaluatorSchema
|
||||
}
|
||||
|
||||
const responseFormatSchema = getResponseFormatSchema(block)
|
||||
if (responseFormatSchema) {
|
||||
return responseFormatSchema
|
||||
}
|
||||
|
||||
const isTrigger = isTriggerBehavior(block)
|
||||
|
||||
if (isTrigger && block.outputs && Object.keys(block.outputs).length > 0) {
|
||||
|
||||
292
apps/sim/hooks/queries/skills.ts
Normal file
292
apps/sim/hooks/queries/skills.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { keepPreviousData, useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { getQueryClient } from '@/app/_shell/providers/query-provider'
|
||||
|
||||
const logger = createLogger('SkillsQueries')
|
||||
const API_ENDPOINT = '/api/skills'
|
||||
|
||||
export interface SkillDefinition {
|
||||
id: string
|
||||
workspaceId: string | null
|
||||
userId: string | null
|
||||
name: string
|
||||
description: string
|
||||
content: string
|
||||
createdAt: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Query key factories for skills queries
|
||||
*/
|
||||
export const skillsKeys = {
|
||||
all: ['skills'] as const,
|
||||
lists: () => [...skillsKeys.all, 'list'] as const,
|
||||
list: (workspaceId: string) => [...skillsKeys.lists(), workspaceId] as const,
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract workspaceId from the current URL path
|
||||
*/
|
||||
function getWorkspaceIdFromUrl(): string | null {
|
||||
if (typeof window === 'undefined') return null
|
||||
const match = window.location.pathname.match(/^\/workspace\/([^/]+)/)
|
||||
return match?.[1] ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all skills from the query cache (for non-React code)
|
||||
*/
|
||||
export function getSkills(workspaceId?: string): SkillDefinition[] {
|
||||
if (typeof window === 'undefined') return []
|
||||
const wsId = workspaceId ?? getWorkspaceIdFromUrl()
|
||||
if (!wsId) return []
|
||||
const queryClient = getQueryClient()
|
||||
return queryClient.getQueryData<SkillDefinition[]>(skillsKeys.list(wsId)) ?? []
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific skill from the query cache by ID or name
|
||||
*/
|
||||
export function getSkill(identifier: string, workspaceId?: string): SkillDefinition | undefined {
|
||||
const skills = getSkills(workspaceId)
|
||||
return skills.find((s) => s.id === identifier || s.name === identifier)
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch skills for a workspace
|
||||
*/
|
||||
async function fetchSkills(workspaceId: string): Promise<SkillDefinition[]> {
|
||||
const response = await fetch(`${API_ENDPOINT}?workspaceId=${workspaceId}`)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}))
|
||||
throw new Error(errorData.error || `Failed to fetch skills: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const { data } = await response.json()
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error('Invalid response format')
|
||||
}
|
||||
|
||||
return data.map((s: Record<string, unknown>) => ({
|
||||
id: s.id as string,
|
||||
workspaceId: (s.workspaceId as string) ?? null,
|
||||
userId: (s.userId as string) ?? null,
|
||||
name: s.name as string,
|
||||
description: s.description as string,
|
||||
content: s.content as string,
|
||||
createdAt: (s.createdAt as string) ?? new Date().toISOString(),
|
||||
updatedAt: s.updatedAt as string | undefined,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to fetch skills for a workspace
|
||||
*/
|
||||
export function useSkills(workspaceId: string) {
|
||||
return useQuery<SkillDefinition[]>({
|
||||
queryKey: skillsKeys.list(workspaceId),
|
||||
queryFn: () => fetchSkills(workspaceId),
|
||||
enabled: !!workspaceId,
|
||||
staleTime: 60 * 1000,
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create skill mutation
|
||||
*/
|
||||
interface CreateSkillParams {
|
||||
workspaceId: string
|
||||
skill: {
|
||||
name: string
|
||||
description: string
|
||||
content: string
|
||||
}
|
||||
}
|
||||
|
||||
export function useCreateSkill() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ workspaceId, skill: s }: CreateSkillParams) => {
|
||||
logger.info(`Creating skill: ${s.name} in workspace ${workspaceId}`)
|
||||
|
||||
const response = await fetch(API_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
skills: [{ name: s.name, description: s.description, content: s.content }],
|
||||
workspaceId,
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to create skill')
|
||||
}
|
||||
|
||||
if (!data.data || !Array.isArray(data.data)) {
|
||||
throw new Error('Invalid API response: missing skills data')
|
||||
}
|
||||
|
||||
logger.info(`Created skill: ${s.name}`)
|
||||
return data.data
|
||||
},
|
||||
onSuccess: (_data, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: skillsKeys.list(variables.workspaceId) })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update skill mutation
|
||||
*/
|
||||
interface UpdateSkillParams {
|
||||
workspaceId: string
|
||||
skillId: string
|
||||
updates: {
|
||||
name?: string
|
||||
description?: string
|
||||
content?: string
|
||||
}
|
||||
}
|
||||
|
||||
export function useUpdateSkill() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ workspaceId, skillId, updates }: UpdateSkillParams) => {
|
||||
logger.info(`Updating skill: ${skillId} in workspace ${workspaceId}`)
|
||||
|
||||
const currentSkills = queryClient.getQueryData<SkillDefinition[]>(
|
||||
skillsKeys.list(workspaceId)
|
||||
)
|
||||
const currentSkill = currentSkills?.find((s) => s.id === skillId)
|
||||
|
||||
if (!currentSkill) {
|
||||
throw new Error('Skill not found')
|
||||
}
|
||||
|
||||
const response = await fetch(API_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
skills: [
|
||||
{
|
||||
id: skillId,
|
||||
name: updates.name ?? currentSkill.name,
|
||||
description: updates.description ?? currentSkill.description,
|
||||
content: updates.content ?? currentSkill.content,
|
||||
},
|
||||
],
|
||||
workspaceId,
|
||||
}),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to update skill')
|
||||
}
|
||||
|
||||
if (!data.data || !Array.isArray(data.data)) {
|
||||
throw new Error('Invalid API response: missing skills data')
|
||||
}
|
||||
|
||||
logger.info(`Updated skill: ${skillId}`)
|
||||
return data.data
|
||||
},
|
||||
onMutate: async ({ workspaceId, skillId, updates }) => {
|
||||
await queryClient.cancelQueries({ queryKey: skillsKeys.list(workspaceId) })
|
||||
|
||||
const previousSkills = queryClient.getQueryData<SkillDefinition[]>(
|
||||
skillsKeys.list(workspaceId)
|
||||
)
|
||||
|
||||
if (previousSkills) {
|
||||
queryClient.setQueryData<SkillDefinition[]>(
|
||||
skillsKeys.list(workspaceId),
|
||||
previousSkills.map((s) =>
|
||||
s.id === skillId
|
||||
? {
|
||||
...s,
|
||||
name: updates.name ?? s.name,
|
||||
description: updates.description ?? s.description,
|
||||
content: updates.content ?? s.content,
|
||||
}
|
||||
: s
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return { previousSkills }
|
||||
},
|
||||
onError: (_err, variables, context) => {
|
||||
if (context?.previousSkills) {
|
||||
queryClient.setQueryData(skillsKeys.list(variables.workspaceId), context.previousSkills)
|
||||
}
|
||||
},
|
||||
onSettled: (_data, _error, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: skillsKeys.list(variables.workspaceId) })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete skill mutation
|
||||
*/
|
||||
interface DeleteSkillParams {
|
||||
workspaceId: string
|
||||
skillId: string
|
||||
}
|
||||
|
||||
export function useDeleteSkill() {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ workspaceId, skillId }: DeleteSkillParams) => {
|
||||
logger.info(`Deleting skill: ${skillId}`)
|
||||
|
||||
const response = await fetch(`${API_ENDPOINT}?id=${skillId}&workspaceId=${workspaceId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || 'Failed to delete skill')
|
||||
}
|
||||
|
||||
logger.info(`Deleted skill: ${skillId}`)
|
||||
return data
|
||||
},
|
||||
onMutate: async ({ workspaceId, skillId }) => {
|
||||
await queryClient.cancelQueries({ queryKey: skillsKeys.list(workspaceId) })
|
||||
|
||||
const previousSkills = queryClient.getQueryData<SkillDefinition[]>(
|
||||
skillsKeys.list(workspaceId)
|
||||
)
|
||||
|
||||
if (previousSkills) {
|
||||
queryClient.setQueryData<SkillDefinition[]>(
|
||||
skillsKeys.list(workspaceId),
|
||||
previousSkills.filter((s) => s.id !== skillId)
|
||||
)
|
||||
}
|
||||
|
||||
return { previousSkills }
|
||||
},
|
||||
onError: (_err, variables, context) => {
|
||||
if (context?.previousSkills) {
|
||||
queryClient.setQueryData(skillsKeys.list(variables.workspaceId), context.previousSkills)
|
||||
}
|
||||
},
|
||||
onSettled: (_data, _error, variables) => {
|
||||
queryClient.invalidateQueries({ queryKey: skillsKeys.list(variables.workspaceId) })
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export interface PermissionGroupConfig {
|
||||
hideFilesTab: boolean
|
||||
disableMcpTools: boolean
|
||||
disableCustomTools: boolean
|
||||
disableSkills: boolean
|
||||
hideTemplates: boolean
|
||||
disableInvitations: boolean
|
||||
// Deploy Modal Tabs
|
||||
@@ -31,6 +32,7 @@ export const DEFAULT_PERMISSION_GROUP_CONFIG: PermissionGroupConfig = {
|
||||
hideFilesTab: false,
|
||||
disableMcpTools: false,
|
||||
disableCustomTools: false,
|
||||
disableSkills: false,
|
||||
hideTemplates: false,
|
||||
disableInvitations: false,
|
||||
hideDeployApi: false,
|
||||
@@ -59,6 +61,7 @@ export function parsePermissionGroupConfig(config: unknown): PermissionGroupConf
|
||||
hideFilesTab: typeof c.hideFilesTab === 'boolean' ? c.hideFilesTab : false,
|
||||
disableMcpTools: typeof c.disableMcpTools === 'boolean' ? c.disableMcpTools : false,
|
||||
disableCustomTools: typeof c.disableCustomTools === 'boolean' ? c.disableCustomTools : false,
|
||||
disableSkills: typeof c.disableSkills === 'boolean' ? c.disableSkills : false,
|
||||
hideTemplates: typeof c.hideTemplates === 'boolean' ? c.hideTemplates : false,
|
||||
disableInvitations: typeof c.disableInvitations === 'boolean' ? c.disableInvitations : false,
|
||||
hideDeployApi: typeof c.hideDeployApi === 'boolean' ? c.hideDeployApi : false,
|
||||
|
||||
@@ -527,113 +527,6 @@ export async function validateTwilioSignature(
|
||||
}
|
||||
}
|
||||
|
||||
const SLACK_FILE_HOSTS = new Set(['files.slack.com', 'files-pri.slack.com'])
|
||||
const SLACK_MAX_FILE_SIZE = 50 * 1024 * 1024 // 50 MB
|
||||
const SLACK_MAX_FILES = 10
|
||||
|
||||
/**
|
||||
* Downloads file attachments from Slack using the bot token.
|
||||
* Returns files in the format expected by WebhookAttachmentProcessor:
|
||||
* { name, data (base64 string), mimeType, size }
|
||||
*
|
||||
* Security:
|
||||
* - Validates each url_private against allowlisted Slack file hosts
|
||||
* - Uses validateUrlWithDNS + secureFetchWithPinnedIP to prevent SSRF
|
||||
* - Enforces per-file size limit and max file count
|
||||
*/
|
||||
async function downloadSlackFiles(
|
||||
rawFiles: any[],
|
||||
botToken: string
|
||||
): Promise<Array<{ name: string; data: string; mimeType: string; size: number }>> {
|
||||
const filesToProcess = rawFiles.slice(0, SLACK_MAX_FILES)
|
||||
const downloaded: Array<{ name: string; data: string; mimeType: string; size: number }> = []
|
||||
|
||||
for (const file of filesToProcess) {
|
||||
const urlPrivate = file.url_private as string | undefined
|
||||
if (!urlPrivate) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Validate the URL points to a known Slack file host
|
||||
let parsedUrl: URL
|
||||
try {
|
||||
parsedUrl = new URL(urlPrivate)
|
||||
} catch {
|
||||
logger.warn('Slack file has invalid url_private, skipping', { fileId: file.id })
|
||||
continue
|
||||
}
|
||||
|
||||
if (!SLACK_FILE_HOSTS.has(parsedUrl.hostname)) {
|
||||
logger.warn('Slack file url_private points to unexpected host, skipping', {
|
||||
fileId: file.id,
|
||||
hostname: sanitizeUrlForLog(urlPrivate),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip files that exceed the size limit
|
||||
const reportedSize = Number(file.size) || 0
|
||||
if (reportedSize > SLACK_MAX_FILE_SIZE) {
|
||||
logger.warn('Slack file exceeds size limit, skipping', {
|
||||
fileId: file.id,
|
||||
size: reportedSize,
|
||||
limit: SLACK_MAX_FILE_SIZE,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const urlValidation = await validateUrlWithDNS(urlPrivate, 'url_private')
|
||||
if (!urlValidation.isValid) {
|
||||
logger.warn('Slack file url_private failed DNS validation, skipping', {
|
||||
fileId: file.id,
|
||||
error: urlValidation.error,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
const response = await secureFetchWithPinnedIP(urlPrivate, urlValidation.resolvedIP!, {
|
||||
headers: { Authorization: `Bearer ${botToken}` },
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
logger.warn('Failed to download Slack file, skipping', {
|
||||
fileId: file.id,
|
||||
status: response.status,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
const buffer = Buffer.from(arrayBuffer)
|
||||
|
||||
// Verify the actual downloaded size doesn't exceed our limit
|
||||
if (buffer.length > SLACK_MAX_FILE_SIZE) {
|
||||
logger.warn('Downloaded Slack file exceeds size limit, skipping', {
|
||||
fileId: file.id,
|
||||
actualSize: buffer.length,
|
||||
limit: SLACK_MAX_FILE_SIZE,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
downloaded.push({
|
||||
name: file.name || 'download',
|
||||
data: buffer.toString('base64'),
|
||||
mimeType: file.mimetype || 'application/octet-stream',
|
||||
size: buffer.length,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error downloading Slack file, skipping', {
|
||||
fileId: file.id,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return downloaded
|
||||
}
|
||||
|
||||
/**
|
||||
* Format webhook input based on provider
|
||||
*/
|
||||
@@ -894,44 +787,43 @@ export async function formatWebhookInput(
|
||||
}
|
||||
|
||||
if (foundWebhook.provider === 'slack') {
|
||||
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
|
||||
const botToken = providerConfig.botToken as string | undefined
|
||||
const includeFiles = Boolean(providerConfig.includeFiles)
|
||||
const event = body?.event
|
||||
|
||||
const rawEvent = body?.event
|
||||
|
||||
if (!rawEvent) {
|
||||
logger.warn('Unknown Slack event type', {
|
||||
type: body?.type,
|
||||
hasEvent: false,
|
||||
bodyKeys: Object.keys(body || {}),
|
||||
})
|
||||
if (event && body?.type === 'event_callback') {
|
||||
return {
|
||||
event: {
|
||||
event_type: event.type || '',
|
||||
channel: event.channel || '',
|
||||
channel_name: '',
|
||||
user: event.user || '',
|
||||
user_name: '',
|
||||
text: event.text || '',
|
||||
timestamp: event.ts || event.event_ts || '',
|
||||
thread_ts: event.thread_ts || '',
|
||||
team_id: body.team_id || event.team || '',
|
||||
event_id: body.event_id || '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const rawFiles: any[] = rawEvent?.files ?? []
|
||||
const hasFiles = rawFiles.length > 0
|
||||
|
||||
let files: any[] = []
|
||||
if (hasFiles && includeFiles && botToken) {
|
||||
files = await downloadSlackFiles(rawFiles, botToken)
|
||||
} else if (hasFiles && includeFiles && !botToken) {
|
||||
logger.warn('Slack message has files and includeFiles is enabled, but no bot token provided')
|
||||
}
|
||||
logger.warn('Unknown Slack event type', {
|
||||
type: body?.type,
|
||||
hasEvent: !!body?.event,
|
||||
bodyKeys: Object.keys(body || {}),
|
||||
})
|
||||
|
||||
return {
|
||||
event: {
|
||||
event_type: rawEvent?.type || body?.type || 'unknown',
|
||||
channel: rawEvent?.channel || '',
|
||||
event_type: body?.event?.type || body?.type || 'unknown',
|
||||
channel: body?.event?.channel || '',
|
||||
channel_name: '',
|
||||
user: rawEvent?.user || '',
|
||||
user: body?.event?.user || '',
|
||||
user_name: '',
|
||||
text: rawEvent?.text || '',
|
||||
timestamp: rawEvent?.ts || rawEvent?.event_ts || '',
|
||||
thread_ts: rawEvent?.thread_ts || '',
|
||||
team_id: body?.team_id || rawEvent?.team || '',
|
||||
text: body?.event?.text || '',
|
||||
timestamp: body?.event?.ts || '',
|
||||
thread_ts: body?.event?.thread_ts || '',
|
||||
team_id: body?.team_id || '',
|
||||
event_id: body?.event_id || '',
|
||||
hasFiles,
|
||||
files,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
100
apps/sim/lib/workflows/skills/operations.ts
Normal file
100
apps/sim/lib/workflows/skills/operations.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { db } from '@sim/db'
|
||||
import { skill } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, desc, eq, ne } from 'drizzle-orm'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
const logger = createLogger('SkillsOperations')
|
||||
|
||||
/**
|
||||
* Internal function to create/update skills.
|
||||
* Can be called from API routes or internal services.
|
||||
*/
|
||||
export async function upsertSkills(params: {
|
||||
skills: Array<{
|
||||
id?: string
|
||||
name: string
|
||||
description: string
|
||||
content: string
|
||||
}>
|
||||
workspaceId: string
|
||||
userId: string
|
||||
requestId?: string
|
||||
}) {
|
||||
const { skills, workspaceId, userId, requestId = generateRequestId() } = params
|
||||
|
||||
return await db.transaction(async (tx) => {
|
||||
for (const s of skills) {
|
||||
const nowTime = new Date()
|
||||
|
||||
if (s.id) {
|
||||
const existingSkill = await tx
|
||||
.select()
|
||||
.from(skill)
|
||||
.where(and(eq(skill.id, s.id), eq(skill.workspaceId, workspaceId)))
|
||||
.limit(1)
|
||||
|
||||
if (existingSkill.length > 0) {
|
||||
if (s.name !== existingSkill[0].name) {
|
||||
const nameConflict = await tx
|
||||
.select({ id: skill.id })
|
||||
.from(skill)
|
||||
.where(
|
||||
and(eq(skill.workspaceId, workspaceId), eq(skill.name, s.name), ne(skill.id, s.id))
|
||||
)
|
||||
.limit(1)
|
||||
|
||||
if (nameConflict.length > 0) {
|
||||
throw new Error(`A skill with the name "${s.name}" already exists in this workspace`)
|
||||
}
|
||||
}
|
||||
|
||||
await tx
|
||||
.update(skill)
|
||||
.set({
|
||||
name: s.name,
|
||||
description: s.description,
|
||||
content: s.content,
|
||||
updatedAt: nowTime,
|
||||
})
|
||||
.where(and(eq(skill.id, s.id), eq(skill.workspaceId, workspaceId)))
|
||||
|
||||
logger.info(`[${requestId}] Updated skill ${s.id}`)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
const duplicateName = await tx
|
||||
.select()
|
||||
.from(skill)
|
||||
.where(and(eq(skill.workspaceId, workspaceId), eq(skill.name, s.name)))
|
||||
.limit(1)
|
||||
|
||||
if (duplicateName.length > 0) {
|
||||
throw new Error(`A skill with the name "${s.name}" already exists in this workspace`)
|
||||
}
|
||||
|
||||
await tx.insert(skill).values({
|
||||
id: nanoid(),
|
||||
workspaceId,
|
||||
userId,
|
||||
name: s.name,
|
||||
description: s.description,
|
||||
content: s.content,
|
||||
createdAt: nowTime,
|
||||
updatedAt: nowTime,
|
||||
})
|
||||
|
||||
logger.info(`[${requestId}] Created skill "${s.name}"`)
|
||||
}
|
||||
|
||||
const resultSkills = await tx
|
||||
.select()
|
||||
.from(skill)
|
||||
.where(eq(skill.workspaceId, workspaceId))
|
||||
.orderBy(desc(skill.createdAt))
|
||||
|
||||
return resultSkills
|
||||
})
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { parseMcpToolId } from '@/lib/mcp/utils'
|
||||
import { isCustomTool, isMcpTool } from '@/executor/constants'
|
||||
import { resolveSkillContent } from '@/executor/handlers/agent/skills-resolver'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import type { ErrorInfo } from '@/tools/error-extractors'
|
||||
import { extractErrorMessage } from '@/tools/error-extractors'
|
||||
@@ -218,6 +219,31 @@ export async function executeTool(
|
||||
// Normalize tool ID to strip resource suffixes (e.g., workflow_executor_<uuid> -> workflow_executor)
|
||||
const normalizedToolId = normalizeToolId(toolId)
|
||||
|
||||
// Handle load_skill tool for agent skills progressive disclosure
|
||||
if (normalizedToolId === 'load_skill') {
|
||||
const skillName = params.skill_name
|
||||
const workspaceId = params._context?.workspaceId
|
||||
if (!skillName || !workspaceId) {
|
||||
return {
|
||||
success: false,
|
||||
output: { error: 'Missing skill_name or workspace context' },
|
||||
error: 'Missing skill_name or workspace context',
|
||||
}
|
||||
}
|
||||
const content = await resolveSkillContent(skillName, workspaceId)
|
||||
if (!content) {
|
||||
return {
|
||||
success: false,
|
||||
output: { error: `Skill "${skillName}" not found` },
|
||||
error: `Skill "${skillName}" not found`,
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
output: { content },
|
||||
}
|
||||
}
|
||||
|
||||
// If it's a custom tool, use the async version with workflowId
|
||||
if (isCustomTool(normalizedToolId)) {
|
||||
const workflowId = params._context?.workflowId
|
||||
|
||||
@@ -131,12 +131,8 @@ export const linearCreateCustomerTool: ToolConfig<
|
||||
domains
|
||||
externalIds
|
||||
logoUrl
|
||||
slugId
|
||||
approximateNeedCount
|
||||
revenue
|
||||
size
|
||||
createdAt
|
||||
updatedAt
|
||||
archivedAt
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,18 +32,18 @@ export const linearCreateCustomerStatusTool: ToolConfig<
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Status color (hex code)',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Status description',
|
||||
},
|
||||
displayName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Display name for the status',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Status description',
|
||||
},
|
||||
position: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
@@ -70,12 +70,12 @@ export const linearCreateCustomerStatusTool: ToolConfig<
|
||||
color: params.color,
|
||||
}
|
||||
|
||||
if (params.description != null && params.description !== '') {
|
||||
input.description = params.description
|
||||
}
|
||||
if (params.displayName != null && params.displayName !== '') {
|
||||
input.displayName = params.displayName
|
||||
}
|
||||
if (params.description != null && params.description !== '') {
|
||||
input.description = params.description
|
||||
}
|
||||
if (params.position != null) {
|
||||
input.position = params.position
|
||||
}
|
||||
@@ -88,12 +88,11 @@ export const linearCreateCustomerStatusTool: ToolConfig<
|
||||
status {
|
||||
id
|
||||
name
|
||||
displayName
|
||||
description
|
||||
color
|
||||
position
|
||||
type
|
||||
createdAt
|
||||
updatedAt
|
||||
archivedAt
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { LinearCreateCycleParams, LinearCreateCycleResponse } from '@/tools/linear/types'
|
||||
import { CYCLE_FULL_OUTPUT_PROPERTIES } from '@/tools/linear/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const linearCreateCycleTool: ToolConfig<LinearCreateCycleParams, LinearCreateCycleResponse> =
|
||||
@@ -73,9 +72,7 @@ export const linearCreateCycleTool: ToolConfig<LinearCreateCycleParams, LinearCr
|
||||
name
|
||||
startsAt
|
||||
endsAt
|
||||
completedAt
|
||||
progress
|
||||
createdAt
|
||||
team {
|
||||
id
|
||||
name
|
||||
@@ -123,7 +120,14 @@ export const linearCreateCycleTool: ToolConfig<LinearCreateCycleParams, LinearCr
|
||||
cycle: {
|
||||
type: 'object',
|
||||
description: 'The created cycle',
|
||||
properties: CYCLE_FULL_OUTPUT_PROPERTIES,
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Cycle ID' },
|
||||
number: { type: 'number', description: 'Cycle number' },
|
||||
name: { type: 'string', description: 'Cycle name' },
|
||||
startsAt: { type: 'string', description: 'Start date' },
|
||||
endsAt: { type: 'string', description: 'End date' },
|
||||
team: { type: 'object', description: 'Team this cycle belongs to' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -73,10 +73,6 @@ export const linearCreateLabelTool: ToolConfig<LinearCreateLabelParams, LinearCr
|
||||
name
|
||||
color
|
||||
description
|
||||
isGroup
|
||||
createdAt
|
||||
updatedAt
|
||||
archivedAt
|
||||
team {
|
||||
id
|
||||
name
|
||||
|
||||
@@ -2,7 +2,6 @@ import type {
|
||||
LinearCreateProjectLabelParams,
|
||||
LinearCreateProjectLabelResponse,
|
||||
} from '@/tools/linear/types'
|
||||
import { PROJECT_LABEL_OUTPUT_PROPERTIES } from '@/tools/linear/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const linearCreateProjectLabelTool: ToolConfig<
|
||||
@@ -94,7 +93,6 @@ export const linearCreateProjectLabelTool: ToolConfig<
|
||||
color
|
||||
isGroup
|
||||
createdAt
|
||||
updatedAt
|
||||
archivedAt
|
||||
}
|
||||
}
|
||||
@@ -139,7 +137,6 @@ export const linearCreateProjectLabelTool: ToolConfig<
|
||||
projectLabel: {
|
||||
type: 'object',
|
||||
description: 'The created project label',
|
||||
properties: PROJECT_LABEL_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type {
|
||||
LinearCreateProjectMilestoneParams,
|
||||
LinearCreateProjectMilestoneResponse,
|
||||
} from '@/tools/linear/types'
|
||||
import { PROJECT_MILESTONE_OUTPUT_PROPERTIES } from '@/tools/linear/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const linearCreateProjectMilestoneTool: ToolConfig<
|
||||
@@ -80,15 +79,10 @@ export const linearCreateProjectMilestoneTool: ToolConfig<
|
||||
id
|
||||
name
|
||||
description
|
||||
projectId
|
||||
targetDate
|
||||
progress
|
||||
sortOrder
|
||||
status
|
||||
createdAt
|
||||
archivedAt
|
||||
project {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,15 +114,10 @@ export const linearCreateProjectMilestoneTool: ToolConfig<
|
||||
}
|
||||
}
|
||||
|
||||
const milestone = result.projectMilestone
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
projectMilestone: {
|
||||
...milestone,
|
||||
projectId: milestone.project?.id ?? null,
|
||||
project: undefined,
|
||||
},
|
||||
projectMilestone: result.projectMilestone,
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -137,7 +126,6 @@ export const linearCreateProjectMilestoneTool: ToolConfig<
|
||||
projectMilestone: {
|
||||
type: 'object',
|
||||
description: 'The created project milestone',
|
||||
properties: PROJECT_MILESTONE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type {
|
||||
LinearCreateProjectStatusParams,
|
||||
LinearCreateProjectStatusResponse,
|
||||
} from '@/tools/linear/types'
|
||||
import { PROJECT_STATUS_OUTPUT_PROPERTIES } from '@/tools/linear/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const linearCreateProjectStatusTool: ToolConfig<
|
||||
@@ -98,9 +97,7 @@ export const linearCreateProjectStatusTool: ToolConfig<
|
||||
color
|
||||
indefinite
|
||||
position
|
||||
type
|
||||
createdAt
|
||||
updatedAt
|
||||
archivedAt
|
||||
}
|
||||
}
|
||||
@@ -145,7 +142,6 @@ export const linearCreateProjectStatusTool: ToolConfig<
|
||||
projectStatus: {
|
||||
type: 'object',
|
||||
description: 'The created project status',
|
||||
properties: PROJECT_STATUS_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type {
|
||||
LinearCreateWorkflowStateParams,
|
||||
LinearCreateWorkflowStateResponse,
|
||||
} from '@/tools/linear/types'
|
||||
import { WORKFLOW_STATE_OUTPUT_PROPERTIES } from '@/tools/linear/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const linearCreateWorkflowStateTool: ToolConfig<
|
||||
@@ -95,13 +94,9 @@ export const linearCreateWorkflowStateTool: ToolConfig<
|
||||
workflowState {
|
||||
id
|
||||
name
|
||||
description
|
||||
type
|
||||
color
|
||||
position
|
||||
createdAt
|
||||
updatedAt
|
||||
archivedAt
|
||||
team {
|
||||
id
|
||||
name
|
||||
@@ -149,7 +144,14 @@ export const linearCreateWorkflowStateTool: ToolConfig<
|
||||
state: {
|
||||
type: 'object',
|
||||
description: 'The created workflow state',
|
||||
properties: WORKFLOW_STATE_OUTPUT_PROPERTIES,
|
||||
properties: {
|
||||
id: { type: 'string', description: 'State ID' },
|
||||
name: { type: 'string', description: 'State name' },
|
||||
type: { type: 'string', description: 'State type' },
|
||||
color: { type: 'string', description: 'State color' },
|
||||
position: { type: 'number', description: 'State position' },
|
||||
team: { type: 'object', description: 'Team this state belongs to' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { LinearGetActiveCycleParams, LinearGetActiveCycleResponse } from '@/tools/linear/types'
|
||||
import { CYCLE_FULL_OUTPUT_PROPERTIES } from '@/tools/linear/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const linearGetActiveCycleTool: ToolConfig<
|
||||
@@ -49,7 +48,6 @@ export const linearGetActiveCycleTool: ToolConfig<
|
||||
endsAt
|
||||
completedAt
|
||||
progress
|
||||
createdAt
|
||||
team {
|
||||
id
|
||||
name
|
||||
@@ -95,7 +93,15 @@ export const linearGetActiveCycleTool: ToolConfig<
|
||||
cycle: {
|
||||
type: 'object',
|
||||
description: 'The active cycle (null if no active cycle)',
|
||||
properties: CYCLE_FULL_OUTPUT_PROPERTIES,
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Cycle ID' },
|
||||
number: { type: 'number', description: 'Cycle number' },
|
||||
name: { type: 'string', description: 'Cycle name' },
|
||||
startsAt: { type: 'string', description: 'Start date' },
|
||||
endsAt: { type: 'string', description: 'End date' },
|
||||
progress: { type: 'number', description: 'Progress percentage' },
|
||||
team: { type: 'object', description: 'Team this cycle belongs to' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -44,12 +44,8 @@ export const linearGetCustomerTool: ToolConfig<LinearGetCustomerParams, LinearGe
|
||||
domains
|
||||
externalIds
|
||||
logoUrl
|
||||
slugId
|
||||
approximateNeedCount
|
||||
revenue
|
||||
size
|
||||
createdAt
|
||||
updatedAt
|
||||
archivedAt
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ export const linearGetCycleTool: ToolConfig<LinearGetCycleParams, LinearGetCycle
|
||||
endsAt
|
||||
completedAt
|
||||
progress
|
||||
createdAt
|
||||
team {
|
||||
id
|
||||
name
|
||||
|
||||
@@ -88,7 +88,7 @@ export const linearListCustomerRequestsTool: ToolConfig<
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
first: params.first ? Number(params.first) : 50,
|
||||
first: params.first || 50,
|
||||
after: params.after,
|
||||
includeArchived: params.includeArchived || false,
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ import type {
|
||||
LinearListCustomerStatusesParams,
|
||||
LinearListCustomerStatusesResponse,
|
||||
} from '@/tools/linear/types'
|
||||
import { CUSTOMER_STATUS_OUTPUT_PROPERTIES, PAGE_INFO_OUTPUT } from '@/tools/linear/types'
|
||||
import { CUSTOMER_STATUS_OUTPUT_PROPERTIES } from '@/tools/linear/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const linearListCustomerStatusesTool: ToolConfig<
|
||||
@@ -19,20 +19,7 @@ export const linearListCustomerStatusesTool: ToolConfig<
|
||||
provider: 'linear',
|
||||
},
|
||||
|
||||
params: {
|
||||
first: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of statuses to return (default: 50)',
|
||||
},
|
||||
after: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Cursor for pagination',
|
||||
},
|
||||
},
|
||||
params: {},
|
||||
|
||||
request: {
|
||||
url: 'https://api.linear.app/graphql',
|
||||
@@ -46,32 +33,23 @@ export const linearListCustomerStatusesTool: ToolConfig<
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}
|
||||
},
|
||||
body: (params) => ({
|
||||
body: () => ({
|
||||
query: `
|
||||
query CustomerStatuses($first: Int, $after: String) {
|
||||
customerStatuses(first: $first, after: $after) {
|
||||
query CustomerStatuses {
|
||||
customerStatuses {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
displayName
|
||||
description
|
||||
color
|
||||
position
|
||||
type
|
||||
createdAt
|
||||
updatedAt
|
||||
archivedAt
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
first: params.first ? Number(params.first) : 50,
|
||||
after: params.after,
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -86,15 +64,10 @@ export const linearListCustomerStatusesTool: ToolConfig<
|
||||
}
|
||||
}
|
||||
|
||||
const result = data.data.customerStatuses
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
customerStatuses: result.nodes,
|
||||
pageInfo: {
|
||||
hasNextPage: result.pageInfo.hasNextPage,
|
||||
endCursor: result.pageInfo.endCursor,
|
||||
},
|
||||
customerStatuses: data.data.customerStatuses.nodes,
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -108,6 +81,5 @@ export const linearListCustomerStatusesTool: ToolConfig<
|
||||
properties: CUSTOMER_STATUS_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
pageInfo: PAGE_INFO_OUTPUT,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type {
|
||||
LinearListCustomerTiersParams,
|
||||
LinearListCustomerTiersResponse,
|
||||
} from '@/tools/linear/types'
|
||||
import { CUSTOMER_TIER_OUTPUT_PROPERTIES, PAGE_INFO_OUTPUT } from '@/tools/linear/types'
|
||||
import { CUSTOMER_TIER_OUTPUT_PROPERTIES } from '@/tools/linear/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const linearListCustomerTiersTool: ToolConfig<
|
||||
@@ -19,20 +19,7 @@ export const linearListCustomerTiersTool: ToolConfig<
|
||||
provider: 'linear',
|
||||
},
|
||||
|
||||
params: {
|
||||
first: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of tiers to return (default: 50)',
|
||||
},
|
||||
after: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Cursor for pagination',
|
||||
},
|
||||
},
|
||||
params: {},
|
||||
|
||||
request: {
|
||||
url: 'https://api.linear.app/graphql',
|
||||
@@ -46,10 +33,10 @@ export const linearListCustomerTiersTool: ToolConfig<
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}
|
||||
},
|
||||
body: (params) => ({
|
||||
body: () => ({
|
||||
query: `
|
||||
query CustomerTiers($first: Int, $after: String) {
|
||||
customerTiers(first: $first, after: $after) {
|
||||
query CustomerTiers {
|
||||
customerTiers {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
@@ -60,17 +47,9 @@ export const linearListCustomerTiersTool: ToolConfig<
|
||||
createdAt
|
||||
archivedAt
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
first: params.first ? Number(params.first) : 50,
|
||||
after: params.after,
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -85,15 +64,10 @@ export const linearListCustomerTiersTool: ToolConfig<
|
||||
}
|
||||
}
|
||||
|
||||
const result = data.data.customerTiers
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
customerTiers: result.nodes,
|
||||
pageInfo: {
|
||||
hasNextPage: result.pageInfo.hasNextPage,
|
||||
endCursor: result.pageInfo.endCursor,
|
||||
},
|
||||
customerTiers: data.data.customerTiers.nodes,
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -107,6 +81,5 @@ export const linearListCustomerTiersTool: ToolConfig<
|
||||
properties: CUSTOMER_TIER_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
pageInfo: PAGE_INFO_OUTPUT,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -59,12 +59,8 @@ export const linearListCustomersTool: ToolConfig<
|
||||
domains
|
||||
externalIds
|
||||
logoUrl
|
||||
slugId
|
||||
approximateNeedCount
|
||||
revenue
|
||||
size
|
||||
createdAt
|
||||
updatedAt
|
||||
archivedAt
|
||||
}
|
||||
pageInfo {
|
||||
@@ -75,7 +71,7 @@ export const linearListCustomersTool: ToolConfig<
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
first: params.first ? Number(params.first) : 50,
|
||||
first: params.first || 50,
|
||||
after: params.after,
|
||||
includeArchived: params.includeArchived || false,
|
||||
},
|
||||
|
||||
@@ -64,7 +64,6 @@ export const linearListCyclesTool: ToolConfig<LinearListCyclesParams, LinearList
|
||||
endsAt
|
||||
completedAt
|
||||
progress
|
||||
createdAt
|
||||
team {
|
||||
id
|
||||
name
|
||||
|
||||
@@ -61,10 +61,6 @@ export const linearListLabelsTool: ToolConfig<LinearListLabelsParams, LinearList
|
||||
name
|
||||
color
|
||||
description
|
||||
isGroup
|
||||
createdAt
|
||||
updatedAt
|
||||
archivedAt
|
||||
team {
|
||||
id
|
||||
name
|
||||
|
||||
@@ -2,7 +2,6 @@ import type {
|
||||
LinearListProjectLabelsParams,
|
||||
LinearListProjectLabelsResponse,
|
||||
} from '@/tools/linear/types'
|
||||
import { PAGE_INFO_OUTPUT, PROJECT_LABEL_OUTPUT_PROPERTIES } from '@/tools/linear/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const linearListProjectLabelsTool: ToolConfig<
|
||||
@@ -26,18 +25,6 @@ export const linearListProjectLabelsTool: ToolConfig<
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Optional project ID to filter labels for a specific project',
|
||||
},
|
||||
first: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of labels to return (default: 50)',
|
||||
},
|
||||
after: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Cursor for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
@@ -53,14 +40,15 @@ export const linearListProjectLabelsTool: ToolConfig<
|
||||
}
|
||||
},
|
||||
body: (params) => {
|
||||
// If projectId is provided, query the specific project's labels
|
||||
if (params.projectId?.trim()) {
|
||||
return {
|
||||
query: `
|
||||
query ProjectWithLabels($id: String!, $first: Int, $after: String) {
|
||||
query ProjectWithLabels($id: String!) {
|
||||
project(id: $id) {
|
||||
id
|
||||
name
|
||||
labels(first: $first, after: $after) {
|
||||
labels {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
@@ -68,29 +56,23 @@ export const linearListProjectLabelsTool: ToolConfig<
|
||||
color
|
||||
isGroup
|
||||
createdAt
|
||||
updatedAt
|
||||
archivedAt
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
id: params.projectId.trim(),
|
||||
first: params.first ? Number(params.first) : 50,
|
||||
after: params.after,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, list all project labels
|
||||
return {
|
||||
query: `
|
||||
query ProjectLabels($first: Int, $after: String) {
|
||||
projectLabels(first: $first, after: $after) {
|
||||
query ProjectLabels {
|
||||
projectLabels {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
@@ -98,20 +80,11 @@ export const linearListProjectLabelsTool: ToolConfig<
|
||||
color
|
||||
isGroup
|
||||
createdAt
|
||||
updatedAt
|
||||
archivedAt
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
first: params.first ? Number(params.first) : 50,
|
||||
after: params.after,
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -127,29 +100,21 @@ export const linearListProjectLabelsTool: ToolConfig<
|
||||
}
|
||||
}
|
||||
|
||||
// Handle project-specific query response
|
||||
if (data.data.project) {
|
||||
const result = data.data.project.labels
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
projectLabels: result.nodes,
|
||||
pageInfo: {
|
||||
hasNextPage: result.pageInfo.hasNextPage,
|
||||
endCursor: result.pageInfo.endCursor,
|
||||
},
|
||||
projectLabels: data.data.project.labels.nodes,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const result = data.data.projectLabels
|
||||
// Handle global projectLabels query response
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
projectLabels: result.nodes,
|
||||
pageInfo: {
|
||||
hasNextPage: result.pageInfo.hasNextPage,
|
||||
endCursor: result.pageInfo.endCursor,
|
||||
},
|
||||
projectLabels: data.data.projectLabels.nodes,
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -158,11 +123,6 @@ export const linearListProjectLabelsTool: ToolConfig<
|
||||
projectLabels: {
|
||||
type: 'array',
|
||||
description: 'List of project labels',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: PROJECT_LABEL_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
pageInfo: PAGE_INFO_OUTPUT,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type {
|
||||
LinearListProjectMilestonesParams,
|
||||
LinearListProjectMilestonesResponse,
|
||||
} from '@/tools/linear/types'
|
||||
import { PAGE_INFO_OUTPUT, PROJECT_MILESTONE_OUTPUT_PROPERTIES } from '@/tools/linear/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const linearListProjectMilestonesTool: ToolConfig<
|
||||
@@ -26,18 +25,6 @@ export const linearListProjectMilestonesTool: ToolConfig<
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Project ID to list milestones for',
|
||||
},
|
||||
first: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of milestones to return (default: 50)',
|
||||
},
|
||||
after: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Cursor for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
@@ -54,26 +41,17 @@ export const linearListProjectMilestonesTool: ToolConfig<
|
||||
},
|
||||
body: (params) => ({
|
||||
query: `
|
||||
query Project($id: String!, $first: Int, $after: String) {
|
||||
query Project($id: String!) {
|
||||
project(id: $id) {
|
||||
projectMilestones(first: $first, after: $after) {
|
||||
projectMilestones {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
description
|
||||
projectId
|
||||
targetDate
|
||||
progress
|
||||
sortOrder
|
||||
status
|
||||
createdAt
|
||||
archivedAt
|
||||
project {
|
||||
id
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -81,8 +59,6 @@ export const linearListProjectMilestonesTool: ToolConfig<
|
||||
`,
|
||||
variables: {
|
||||
id: params.projectId,
|
||||
first: params.first ? Number(params.first) : 50,
|
||||
after: params.after,
|
||||
},
|
||||
}),
|
||||
},
|
||||
@@ -98,20 +74,10 @@ export const linearListProjectMilestonesTool: ToolConfig<
|
||||
}
|
||||
}
|
||||
|
||||
const result = data.data.project?.projectMilestones
|
||||
const milestones = (result?.nodes || []).map((node: Record<string, unknown>) => ({
|
||||
...node,
|
||||
projectId: (node.project as Record<string, string>)?.id ?? null,
|
||||
project: undefined,
|
||||
}))
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
projectMilestones: milestones,
|
||||
pageInfo: {
|
||||
hasNextPage: result?.pageInfo?.hasNextPage ?? false,
|
||||
endCursor: result?.pageInfo?.endCursor,
|
||||
},
|
||||
projectMilestones: data.data.project?.projectMilestones?.nodes || [],
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -120,11 +86,6 @@ export const linearListProjectMilestonesTool: ToolConfig<
|
||||
projectMilestones: {
|
||||
type: 'array',
|
||||
description: 'List of project milestones',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: PROJECT_MILESTONE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
pageInfo: PAGE_INFO_OUTPUT,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type {
|
||||
LinearListProjectStatusesParams,
|
||||
LinearListProjectStatusesResponse,
|
||||
} from '@/tools/linear/types'
|
||||
import { PAGE_INFO_OUTPUT, PROJECT_STATUS_OUTPUT_PROPERTIES } from '@/tools/linear/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const linearListProjectStatusesTool: ToolConfig<
|
||||
@@ -19,20 +18,7 @@ export const linearListProjectStatusesTool: ToolConfig<
|
||||
provider: 'linear',
|
||||
},
|
||||
|
||||
params: {
|
||||
first: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of statuses to return (default: 50)',
|
||||
},
|
||||
after: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Cursor for pagination',
|
||||
},
|
||||
},
|
||||
params: {},
|
||||
|
||||
request: {
|
||||
url: 'https://api.linear.app/graphql',
|
||||
@@ -46,10 +32,10 @@ export const linearListProjectStatusesTool: ToolConfig<
|
||||
Authorization: `Bearer ${params.accessToken}`,
|
||||
}
|
||||
},
|
||||
body: (params) => ({
|
||||
body: () => ({
|
||||
query: `
|
||||
query ProjectStatuses($first: Int, $after: String) {
|
||||
projectStatuses(first: $first, after: $after) {
|
||||
query ProjectStatuses {
|
||||
projectStatuses {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
@@ -57,22 +43,12 @@ export const linearListProjectStatusesTool: ToolConfig<
|
||||
color
|
||||
indefinite
|
||||
position
|
||||
type
|
||||
createdAt
|
||||
updatedAt
|
||||
archivedAt
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
first: params.first ? Number(params.first) : 50,
|
||||
after: params.after,
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -87,15 +63,10 @@ export const linearListProjectStatusesTool: ToolConfig<
|
||||
}
|
||||
}
|
||||
|
||||
const result = data.data.projectStatuses
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
projectStatuses: result.nodes,
|
||||
pageInfo: {
|
||||
hasNextPage: result.pageInfo.hasNextPage,
|
||||
endCursor: result.pageInfo.endCursor,
|
||||
},
|
||||
projectStatuses: data.data.projectStatuses.nodes,
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -104,11 +75,6 @@ export const linearListProjectStatusesTool: ToolConfig<
|
||||
projectStatuses: {
|
||||
type: 'array',
|
||||
description: 'List of project statuses',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: PROJECT_STATUS_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
pageInfo: PAGE_INFO_OUTPUT,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ export const linearListProjectsTool: ToolConfig<
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
first: params.first ? Number(params.first) : 50,
|
||||
first: params.first || 50,
|
||||
after: params.after,
|
||||
includeArchived: params.includeArchived || false,
|
||||
},
|
||||
|
||||
@@ -65,13 +65,9 @@ export const linearListWorkflowStatesTool: ToolConfig<
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
description
|
||||
type
|
||||
color
|
||||
position
|
||||
createdAt
|
||||
updatedAt
|
||||
archivedAt
|
||||
team {
|
||||
id
|
||||
name
|
||||
|
||||
@@ -41,12 +41,6 @@ export const linearSearchIssuesTool: ToolConfig<
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of results to return (default: 50)',
|
||||
},
|
||||
after: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Cursor for pagination',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
@@ -69,8 +63,8 @@ export const linearSearchIssuesTool: ToolConfig<
|
||||
|
||||
return {
|
||||
query: `
|
||||
query SearchIssues($term: String!, $filter: IssueFilter, $first: Int, $after: String, $includeArchived: Boolean) {
|
||||
searchIssues(term: $term, filter: $filter, first: $first, after: $after, includeArchived: $includeArchived) {
|
||||
query SearchIssues($term: String!, $filter: IssueFilter, $first: Int, $includeArchived: Boolean) {
|
||||
searchIssues(term: $term, filter: $filter, first: $first, includeArchived: $includeArchived) {
|
||||
nodes {
|
||||
id
|
||||
title
|
||||
@@ -117,8 +111,7 @@ export const linearSearchIssuesTool: ToolConfig<
|
||||
variables: {
|
||||
term: params.query,
|
||||
filter: Object.keys(filter).length > 0 ? filter : undefined,
|
||||
first: params.first ? Number(params.first) : 50,
|
||||
after: params.after,
|
||||
first: params.first || 50,
|
||||
includeArchived: params.includeArchived || false,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -112,10 +112,6 @@ export const LABEL_FULL_OUTPUT_PROPERTIES = {
|
||||
name: { type: 'string', description: 'Label name' },
|
||||
color: { type: 'string', description: 'Label color (hex)' },
|
||||
description: { type: 'string', description: 'Label description' },
|
||||
isGroup: { type: 'boolean', description: 'Whether this label is a group' },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp (ISO 8601)' },
|
||||
updatedAt: { type: 'string', description: 'Last update timestamp (ISO 8601)' },
|
||||
archivedAt: { type: 'string', description: 'Archive timestamp (ISO 8601)' },
|
||||
team: TEAM_OUTPUT,
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
@@ -148,7 +144,6 @@ export const CYCLE_FULL_OUTPUT_PROPERTIES = {
|
||||
endsAt: { type: 'string', description: 'End date (ISO 8601)' },
|
||||
completedAt: { type: 'string', description: 'Completion date (ISO 8601)' },
|
||||
progress: { type: 'number', description: 'Progress percentage (0-1)' },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp (ISO 8601)' },
|
||||
team: TEAM_OUTPUT,
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
@@ -282,16 +277,9 @@ export const ATTACHMENT_OUTPUT_PROPERTIES = {
|
||||
export const WORKFLOW_STATE_OUTPUT_PROPERTIES = {
|
||||
id: { type: 'string', description: 'State ID' },
|
||||
name: { type: 'string', description: 'State name (e.g., "Todo", "In Progress")' },
|
||||
description: { type: 'string', description: 'State description' },
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'State type (triage, backlog, unstarted, started, completed, canceled)',
|
||||
},
|
||||
type: { type: 'string', description: 'State type (unstarted, started, completed, canceled)' },
|
||||
color: { type: 'string', description: 'State color (hex)' },
|
||||
position: { type: 'number', description: 'State position in workflow' },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp (ISO 8601)' },
|
||||
updatedAt: { type: 'string', description: 'Last update timestamp (ISO 8601)' },
|
||||
archivedAt: { type: 'string', description: 'Archive timestamp (ISO 8601)' },
|
||||
team: TEAM_OUTPUT,
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
@@ -355,12 +343,8 @@ export const CUSTOMER_OUTPUT_PROPERTIES = {
|
||||
items: { type: 'string', description: 'External ID' },
|
||||
},
|
||||
logoUrl: { type: 'string', description: 'Logo URL' },
|
||||
slugId: { type: 'string', description: 'Unique URL slug' },
|
||||
approximateNeedCount: { type: 'number', description: 'Number of customer needs' },
|
||||
revenue: { type: 'number', description: 'Annual revenue' },
|
||||
size: { type: 'number', description: 'Organization size' },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp (ISO 8601)' },
|
||||
updatedAt: { type: 'string', description: 'Last update timestamp (ISO 8601)' },
|
||||
archivedAt: { type: 'string', description: 'Archive timestamp (ISO 8601)' },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
@@ -394,12 +378,11 @@ export const CUSTOMER_NEED_OUTPUT_PROPERTIES = {
|
||||
export const CUSTOMER_STATUS_OUTPUT_PROPERTIES = {
|
||||
id: { type: 'string', description: 'Customer status ID' },
|
||||
name: { type: 'string', description: 'Status name' },
|
||||
displayName: { type: 'string', description: 'Display name' },
|
||||
description: { type: 'string', description: 'Status description' },
|
||||
color: { type: 'string', description: 'Status color (hex)' },
|
||||
position: { type: 'number', description: 'Position in list' },
|
||||
type: { type: 'string', description: 'Status type (active, inactive)' },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp (ISO 8601)' },
|
||||
updatedAt: { type: 'string', description: 'Last updated timestamp (ISO 8601)' },
|
||||
archivedAt: { type: 'string', description: 'Archive timestamp (ISO 8601)' },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
@@ -427,7 +410,6 @@ export const PROJECT_LABEL_OUTPUT_PROPERTIES = {
|
||||
color: { type: 'string', description: 'Label color (hex)' },
|
||||
isGroup: { type: 'boolean', description: 'Whether this label is a group' },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp (ISO 8601)' },
|
||||
updatedAt: { type: 'string', description: 'Last update timestamp (ISO 8601)' },
|
||||
archivedAt: { type: 'string', description: 'Archive timestamp (ISO 8601)' },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
@@ -440,9 +422,6 @@ export const PROJECT_MILESTONE_OUTPUT_PROPERTIES = {
|
||||
description: { type: 'string', description: 'Milestone description' },
|
||||
projectId: { type: 'string', description: 'Project ID' },
|
||||
targetDate: { type: 'string', description: 'Target date (YYYY-MM-DD)' },
|
||||
progress: { type: 'number', description: 'Progress percentage (0-1)' },
|
||||
sortOrder: { type: 'number', description: 'Sort order within the project' },
|
||||
status: { type: 'string', description: 'Milestone status (done, next, overdue, unstarted)' },
|
||||
createdAt: { type: 'string', description: 'Creation timestamp (ISO 8601)' },
|
||||
archivedAt: { type: 'string', description: 'Archive timestamp (ISO 8601)' },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
@@ -465,12 +444,7 @@ export const PROJECT_STATUS_OUTPUT_PROPERTIES = {
|
||||
color: { type: 'string', description: 'Status color (hex)' },
|
||||
indefinite: { type: 'boolean', description: 'Whether this status is indefinite' },
|
||||
position: { type: 'number', description: 'Position in list' },
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'Status type (backlog, planned, started, paused, completed, canceled)',
|
||||
},
|
||||
createdAt: { type: 'string', description: 'Creation timestamp (ISO 8601)' },
|
||||
updatedAt: { type: 'string', description: 'Last updated timestamp (ISO 8601)' },
|
||||
archivedAt: { type: 'string', description: 'Archive timestamp (ISO 8601)' },
|
||||
} as const satisfies Record<string, OutputProperty>
|
||||
|
||||
@@ -613,10 +587,6 @@ export interface LinearLabel {
|
||||
name: string
|
||||
color: string
|
||||
description?: string
|
||||
isGroup: boolean
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
archivedAt?: string
|
||||
team?: {
|
||||
id: string
|
||||
name: string
|
||||
@@ -626,13 +596,9 @@ export interface LinearLabel {
|
||||
export interface LinearWorkflowState {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
type: string
|
||||
color: string
|
||||
position: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
archivedAt?: string
|
||||
team: {
|
||||
id: string
|
||||
name: string
|
||||
@@ -647,7 +613,6 @@ export interface LinearCycle {
|
||||
endsAt: string
|
||||
completedAt?: string
|
||||
progress: number
|
||||
createdAt: string
|
||||
team: {
|
||||
id: string
|
||||
name: string
|
||||
@@ -745,7 +710,6 @@ export interface LinearSearchIssuesParams {
|
||||
teamId?: string
|
||||
includeArchived?: boolean
|
||||
first?: number
|
||||
after?: string
|
||||
accessToken?: string
|
||||
}
|
||||
|
||||
@@ -1241,7 +1205,7 @@ export interface LinearAttachment {
|
||||
subtitle?: string
|
||||
url: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
updatedAt?: string
|
||||
}
|
||||
|
||||
export interface LinearCreateAttachmentResponse extends ToolResponse {
|
||||
@@ -1402,12 +1366,8 @@ export interface LinearCustomer {
|
||||
domains: string[]
|
||||
externalIds: string[]
|
||||
logoUrl?: string
|
||||
slugId: string
|
||||
approximateNeedCount: number
|
||||
revenue?: number
|
||||
size?: number
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
archivedAt?: string
|
||||
}
|
||||
|
||||
@@ -1582,12 +1542,11 @@ export interface LinearMergeCustomersResponse extends ToolResponse {
|
||||
export interface LinearCustomerStatus {
|
||||
id: string
|
||||
name: string
|
||||
displayName: string
|
||||
description?: string
|
||||
color: string
|
||||
position: number
|
||||
type: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
archivedAt?: string
|
||||
}
|
||||
|
||||
@@ -1634,18 +1593,12 @@ export interface LinearDeleteCustomerStatusResponse extends ToolResponse {
|
||||
}
|
||||
|
||||
export interface LinearListCustomerStatusesParams {
|
||||
first?: number
|
||||
after?: string
|
||||
accessToken?: string
|
||||
}
|
||||
|
||||
export interface LinearListCustomerStatusesResponse extends ToolResponse {
|
||||
output: {
|
||||
customerStatuses?: LinearCustomerStatus[]
|
||||
pageInfo?: {
|
||||
hasNextPage: boolean
|
||||
endCursor?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1705,18 +1658,12 @@ export interface LinearDeleteCustomerTierResponse extends ToolResponse {
|
||||
}
|
||||
|
||||
export interface LinearListCustomerTiersParams {
|
||||
first?: number
|
||||
after?: string
|
||||
accessToken?: string
|
||||
}
|
||||
|
||||
export interface LinearListCustomerTiersResponse extends ToolResponse {
|
||||
output: {
|
||||
customerTiers?: LinearCustomerTier[]
|
||||
pageInfo?: {
|
||||
hasNextPage: boolean
|
||||
endCursor?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1729,7 +1676,6 @@ export interface LinearProjectLabel {
|
||||
color?: string
|
||||
isGroup: boolean
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
archivedAt?: string
|
||||
}
|
||||
|
||||
@@ -1774,19 +1720,13 @@ export interface LinearDeleteProjectLabelResponse extends ToolResponse {
|
||||
}
|
||||
|
||||
export interface LinearListProjectLabelsParams {
|
||||
projectId?: string
|
||||
first?: number
|
||||
after?: string
|
||||
accessToken?: string
|
||||
projectId?: string
|
||||
}
|
||||
|
||||
export interface LinearListProjectLabelsResponse extends ToolResponse {
|
||||
output: {
|
||||
projectLabels?: LinearProjectLabel[]
|
||||
pageInfo?: {
|
||||
hasNextPage: boolean
|
||||
endCursor?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1824,9 +1764,6 @@ export interface LinearProjectMilestone {
|
||||
description?: string
|
||||
projectId: string
|
||||
targetDate?: string
|
||||
progress: number
|
||||
sortOrder: number
|
||||
status: string
|
||||
createdAt: string
|
||||
archivedAt?: string
|
||||
}
|
||||
@@ -1872,18 +1809,12 @@ export interface LinearDeleteProjectMilestoneResponse extends ToolResponse {
|
||||
|
||||
export interface LinearListProjectMilestonesParams {
|
||||
projectId: string
|
||||
first?: number
|
||||
after?: string
|
||||
accessToken?: string
|
||||
}
|
||||
|
||||
export interface LinearListProjectMilestonesResponse extends ToolResponse {
|
||||
output: {
|
||||
projectMilestones?: LinearProjectMilestone[]
|
||||
pageInfo?: {
|
||||
hasNextPage: boolean
|
||||
endCursor?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1896,9 +1827,7 @@ export interface LinearProjectStatus {
|
||||
color: string
|
||||
indefinite: boolean
|
||||
position: number
|
||||
type: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
archivedAt?: string
|
||||
}
|
||||
|
||||
@@ -1946,18 +1875,12 @@ export interface LinearDeleteProjectStatusResponse extends ToolResponse {
|
||||
}
|
||||
|
||||
export interface LinearListProjectStatusesParams {
|
||||
first?: number
|
||||
after?: string
|
||||
accessToken?: string
|
||||
}
|
||||
|
||||
export interface LinearListProjectStatusesResponse extends ToolResponse {
|
||||
output: {
|
||||
projectStatuses?: LinearProjectStatus[]
|
||||
pageInfo?: {
|
||||
hasNextPage: boolean
|
||||
endCursor?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,6 @@ export const linearUpdateAttachmentTool: ToolConfig<
|
||||
title
|
||||
subtitle
|
||||
url
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,12 +137,8 @@ export const linearUpdateCustomerTool: ToolConfig<
|
||||
domains
|
||||
externalIds
|
||||
logoUrl
|
||||
slugId
|
||||
approximateNeedCount
|
||||
revenue
|
||||
size
|
||||
createdAt
|
||||
updatedAt
|
||||
archivedAt
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type {
|
||||
LinearUpdateCustomerStatusParams,
|
||||
LinearUpdateCustomerStatusResponse,
|
||||
} from '@/tools/linear/types'
|
||||
import { CUSTOMER_STATUS_OUTPUT_PROPERTIES } from '@/tools/linear/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const linearUpdateCustomerStatusTool: ToolConfig<
|
||||
@@ -38,18 +37,18 @@ export const linearUpdateCustomerStatusTool: ToolConfig<
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Updated status color',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Updated description',
|
||||
},
|
||||
displayName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Updated display name',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Updated description',
|
||||
},
|
||||
position: {
|
||||
type: 'number',
|
||||
required: false,
|
||||
@@ -79,12 +78,12 @@ export const linearUpdateCustomerStatusTool: ToolConfig<
|
||||
if (params.color != null && params.color !== '') {
|
||||
input.color = params.color
|
||||
}
|
||||
if (params.description != null && params.description !== '') {
|
||||
input.description = params.description
|
||||
}
|
||||
if (params.displayName != null && params.displayName !== '') {
|
||||
input.displayName = params.displayName
|
||||
}
|
||||
if (params.description != null && params.description !== '') {
|
||||
input.description = params.description
|
||||
}
|
||||
if (params.position != null) {
|
||||
input.position = params.position
|
||||
}
|
||||
@@ -97,12 +96,11 @@ export const linearUpdateCustomerStatusTool: ToolConfig<
|
||||
customerStatus {
|
||||
id
|
||||
name
|
||||
displayName
|
||||
description
|
||||
color
|
||||
position
|
||||
type
|
||||
createdAt
|
||||
updatedAt
|
||||
archivedAt
|
||||
}
|
||||
}
|
||||
@@ -140,7 +138,6 @@ export const linearUpdateCustomerStatusTool: ToolConfig<
|
||||
customerStatus: {
|
||||
type: 'object',
|
||||
description: 'The updated customer status',
|
||||
properties: CUSTOMER_STATUS_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -71,10 +71,6 @@ export const linearUpdateLabelTool: ToolConfig<LinearUpdateLabelParams, LinearUp
|
||||
name
|
||||
color
|
||||
description
|
||||
isGroup
|
||||
createdAt
|
||||
updatedAt
|
||||
archivedAt
|
||||
team {
|
||||
id
|
||||
name
|
||||
|
||||
@@ -2,7 +2,6 @@ import type {
|
||||
LinearUpdateProjectLabelParams,
|
||||
LinearUpdateProjectLabelResponse,
|
||||
} from '@/tools/linear/types'
|
||||
import { PROJECT_LABEL_OUTPUT_PROPERTIES } from '@/tools/linear/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const linearUpdateProjectLabelTool: ToolConfig<
|
||||
@@ -83,7 +82,6 @@ export const linearUpdateProjectLabelTool: ToolConfig<
|
||||
color
|
||||
isGroup
|
||||
createdAt
|
||||
updatedAt
|
||||
archivedAt
|
||||
}
|
||||
}
|
||||
@@ -121,7 +119,6 @@ export const linearUpdateProjectLabelTool: ToolConfig<
|
||||
projectLabel: {
|
||||
type: 'object',
|
||||
description: 'The updated project label',
|
||||
properties: PROJECT_LABEL_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type {
|
||||
LinearUpdateProjectMilestoneParams,
|
||||
LinearUpdateProjectMilestoneResponse,
|
||||
} from '@/tools/linear/types'
|
||||
import { PROJECT_MILESTONE_OUTPUT_PROPERTIES } from '@/tools/linear/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const linearUpdateProjectMilestoneTool: ToolConfig<
|
||||
@@ -80,15 +79,10 @@ export const linearUpdateProjectMilestoneTool: ToolConfig<
|
||||
id
|
||||
name
|
||||
description
|
||||
projectId
|
||||
targetDate
|
||||
progress
|
||||
sortOrder
|
||||
status
|
||||
createdAt
|
||||
archivedAt
|
||||
project {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -113,23 +107,10 @@ export const linearUpdateProjectMilestoneTool: ToolConfig<
|
||||
}
|
||||
|
||||
const result = data.data.projectMilestoneUpdate
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Project milestone update was not successful',
|
||||
output: {},
|
||||
}
|
||||
}
|
||||
|
||||
const milestone = result.projectMilestone
|
||||
return {
|
||||
success: true,
|
||||
success: result.success,
|
||||
output: {
|
||||
projectMilestone: {
|
||||
...milestone,
|
||||
projectId: milestone.project?.id ?? null,
|
||||
project: undefined,
|
||||
},
|
||||
projectMilestone: result.projectMilestone,
|
||||
},
|
||||
}
|
||||
},
|
||||
@@ -138,7 +119,6 @@ export const linearUpdateProjectMilestoneTool: ToolConfig<
|
||||
projectMilestone: {
|
||||
type: 'object',
|
||||
description: 'The updated project milestone',
|
||||
properties: PROJECT_MILESTONE_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type {
|
||||
LinearUpdateProjectStatusParams,
|
||||
LinearUpdateProjectStatusResponse,
|
||||
} from '@/tools/linear/types'
|
||||
import { PROJECT_STATUS_OUTPUT_PROPERTIES } from '@/tools/linear/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const linearUpdateProjectStatusTool: ToolConfig<
|
||||
@@ -101,9 +100,7 @@ export const linearUpdateProjectStatusTool: ToolConfig<
|
||||
color
|
||||
indefinite
|
||||
position
|
||||
type
|
||||
createdAt
|
||||
updatedAt
|
||||
archivedAt
|
||||
}
|
||||
}
|
||||
@@ -141,7 +138,6 @@ export const linearUpdateProjectStatusTool: ToolConfig<
|
||||
projectStatus: {
|
||||
type: 'object',
|
||||
description: 'The updated project status',
|
||||
properties: PROJECT_STATUS_OUTPUT_PROPERTIES,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import type {
|
||||
LinearUpdateWorkflowStateParams,
|
||||
LinearUpdateWorkflowStateResponse,
|
||||
} from '@/tools/linear/types'
|
||||
import { WORKFLOW_STATE_OUTPUT_PROPERTIES } from '@/tools/linear/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const linearUpdateWorkflowStateTool: ToolConfig<
|
||||
@@ -88,13 +87,9 @@ export const linearUpdateWorkflowStateTool: ToolConfig<
|
||||
workflowState {
|
||||
id
|
||||
name
|
||||
description
|
||||
type
|
||||
color
|
||||
position
|
||||
createdAt
|
||||
updatedAt
|
||||
archivedAt
|
||||
team {
|
||||
id
|
||||
name
|
||||
@@ -143,7 +138,13 @@ export const linearUpdateWorkflowStateTool: ToolConfig<
|
||||
state: {
|
||||
type: 'object',
|
||||
description: 'The updated workflow state',
|
||||
properties: WORKFLOW_STATE_OUTPUT_PROPERTIES,
|
||||
properties: {
|
||||
id: { type: 'string', description: 'State ID' },
|
||||
name: { type: 'string', description: 'State name' },
|
||||
type: { type: 'string', description: 'State type' },
|
||||
color: { type: 'string', description: 'State color' },
|
||||
position: { type: 'number', description: 'State position' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -30,27 +30,6 @@ export const slackWebhookTrigger: TriggerConfig = {
|
||||
required: true,
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'botToken',
|
||||
title: 'Bot Token',
|
||||
type: 'short-input',
|
||||
placeholder: 'xoxb-...',
|
||||
description:
|
||||
'The bot token from your Slack app. Required for downloading files attached to messages.',
|
||||
password: true,
|
||||
required: false,
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'includeFiles',
|
||||
title: 'Include File Attachments',
|
||||
type: 'switch',
|
||||
defaultValue: false,
|
||||
description:
|
||||
'Download and include file attachments from messages. Requires a bot token with files:read scope.',
|
||||
required: false,
|
||||
mode: 'trigger',
|
||||
},
|
||||
{
|
||||
id: 'triggerSave',
|
||||
title: '',
|
||||
@@ -67,10 +46,9 @@ export const slackWebhookTrigger: TriggerConfig = {
|
||||
'Go to <a href="https://api.slack.com/apps" target="_blank" rel="noopener noreferrer" class="text-muted-foreground underline transition-colors hover:text-muted-foreground/80">Slack Apps page</a>',
|
||||
'If you don\'t have an app:<br><ul class="mt-1 ml-5 list-disc"><li>Create an app from scratch</li><li>Give it a name and select your workspace</li></ul>',
|
||||
'Go to "Basic Information", find the "Signing Secret", and paste it in the field above.',
|
||||
'Go to "OAuth & Permissions" and add bot token scopes:<br><ul class="mt-1 ml-5 list-disc"><li><code>app_mentions:read</code> - For viewing messages that tag your bot with an @</li><li><code>chat:write</code> - To send messages to channels your bot is a part of</li><li><code>files:read</code> - To access files and images shared in messages</li></ul>',
|
||||
'Go to "OAuth & Permissions" and add bot token scopes:<br><ul class="mt-1 ml-5 list-disc"><li><code>app_mentions:read</code> - For viewing messages that tag your bot with an @</li><li><code>chat:write</code> - To send messages to channels your bot is a part of</li></ul>',
|
||||
'Go to "Event Subscriptions":<br><ul class="mt-1 ml-5 list-disc"><li>Enable events</li><li>Under "Subscribe to Bot Events", add <code>app_mention</code> to listen to messages that mention your bot</li><li>Paste the Webhook URL above into the "Request URL" field</li></ul>',
|
||||
'Go to "Install App" in the left sidebar and install the app into your desired Slack workspace and channel.',
|
||||
'Copy the "Bot User OAuth Token" (starts with <code>xoxb-</code>) and paste it in the Bot Token field above to enable file downloads.',
|
||||
'Save changes in both Slack and here.',
|
||||
]
|
||||
.map(
|
||||
@@ -128,15 +106,6 @@ export const slackWebhookTrigger: TriggerConfig = {
|
||||
type: 'string',
|
||||
description: 'Unique event identifier',
|
||||
},
|
||||
hasFiles: {
|
||||
type: 'boolean',
|
||||
description: 'Whether the message has file attachments',
|
||||
},
|
||||
files: {
|
||||
type: 'file[]',
|
||||
description:
|
||||
'File attachments downloaded from the message (if includeFiles is enabled and bot token is provided)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
15
packages/db/migrations/0152_parallel_frog_thor.sql
Normal file
15
packages/db/migrations/0152_parallel_frog_thor.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE "skill" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"workspace_id" text,
|
||||
"user_id" text,
|
||||
"name" text NOT NULL,
|
||||
"description" text NOT NULL,
|
||||
"content" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "skill" ADD CONSTRAINT "skill_workspace_id_workspace_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "public"."workspace"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "skill" ADD CONSTRAINT "skill_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "skill_workspace_id_idx" ON "skill" USING btree ("workspace_id");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "skill_workspace_name_unique" ON "skill" USING btree ("workspace_id","name");
|
||||
10619
packages/db/migrations/meta/0152_snapshot.json
Normal file
10619
packages/db/migrations/meta/0152_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1058,6 +1058,13 @@
|
||||
"when": 1770239332381,
|
||||
"tag": "0151_stale_screwball",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 152,
|
||||
"version": "7",
|
||||
"when": 1770336289511,
|
||||
"tag": "0152_parallel_frog_thor",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -743,6 +743,27 @@ export const customTools = pgTable(
|
||||
})
|
||||
)
|
||||
|
||||
export const skill = pgTable(
|
||||
'skill',
|
||||
{
|
||||
id: text('id').primaryKey(),
|
||||
workspaceId: text('workspace_id').references(() => workspace.id, { onDelete: 'cascade' }),
|
||||
userId: text('user_id').references(() => user.id, { onDelete: 'set null' }),
|
||||
name: text('name').notNull(),
|
||||
description: text('description').notNull(),
|
||||
content: text('content').notNull(),
|
||||
createdAt: timestamp('created_at').notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at').notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
workspaceIdIdx: index('skill_workspace_id_idx').on(table.workspaceId),
|
||||
workspaceNameUnique: uniqueIndex('skill_workspace_name_unique').on(
|
||||
table.workspaceId,
|
||||
table.name
|
||||
),
|
||||
})
|
||||
)
|
||||
|
||||
export const subscription = pgTable(
|
||||
'subscription',
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user