From 5ec11a27d89387989da1740900ab39f62c285428 Mon Sep 17 00:00:00 2001 From: Surya Prashanth Date: Thu, 29 Jan 2026 19:22:37 +0530 Subject: [PATCH 1/2] feat: add agent docs and make oauth app docs more beginner friendly --- docs/.vitepress/config.mts | 34 +- docs/dev-tools/agents/best-practices.md | 395 +++++++ docs/dev-tools/agents/building-an-agent.md | 580 +++++++++ docs/dev-tools/agents/overview.md | 140 +++ .../agents/signals-content-payload.md | 828 +++++++++++++ docs/dev-tools/build-plane-app.md | 1043 ++++++++--------- package.json | 2 + pnpm-lock.yaml | 985 ++++++++++++++++ 8 files changed, 3442 insertions(+), 565 deletions(-) create mode 100644 docs/dev-tools/agents/best-practices.md create mode 100644 docs/dev-tools/agents/building-an-agent.md create mode 100644 docs/dev-tools/agents/overview.md create mode 100644 docs/dev-tools/agents/signals-content-payload.md diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 69fa5c5e..3acbcbe8 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,12 +1,32 @@ import { defineConfig } from 'vitepress' import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs' +import { withMermaid } from 'vitepress-plugin-mermaid' -export default defineConfig({ +export default withMermaid(defineConfig({ markdown: { config(md) { md.use(tabsMarkdownPlugin) } }, + mermaid: { + // Mermaid configuration options + }, + vite: { + optimizeDeps: { + include: [ + 'mermaid', + '@braintree/sanitize-url', + 'dayjs', + 'cytoscape', + 'cytoscape-cose-bilkent', + 'd3', + 'khroma', + 'dagre-d3-es', + 'lodash-es', + 'dompurify', + ], + }, + }, title: 'Plane developer documentation', description: 'Self-host Plane, integrate with our API, configure webhooks, and extend your project management platform. Complete guides for developers building on Plane.', @@ -571,6 +591,16 @@ export default defineConfig({ text: 'Build and extend Plane', items: [ { text: 'Build Plane App', link: '/dev-tools/build-plane-app' }, + { + text: 'Agents', + collapsed: false, + items: [ + { text: 'Overview', link: '/dev-tools/agents/overview' }, + { text: 'Building an Agent', link: '/dev-tools/agents/building-an-agent' }, + { text: 'Best Practices', link: '/dev-tools/agents/best-practices' }, + { text: 'Signals & Content Payload', link: '/dev-tools/agents/signals-content-payload' } + ] + }, { text: 'Webhooks', link: '/dev-tools/intro-webhooks' }, { text: 'MCP Server', link: '/dev-tools/mcp-server' } ] @@ -599,4 +629,4 @@ export default defineConfig({ copyright: 'Copyright © 2024 Plane' }*/ } -}) +})) diff --git a/docs/dev-tools/agents/best-practices.md b/docs/dev-tools/agents/best-practices.md new file mode 100644 index 00000000..c070a9b2 --- /dev/null +++ b/docs/dev-tools/agents/best-practices.md @@ -0,0 +1,395 @@ +--- +title: Best Practices +description: Guidelines for building responsive, user-friendly Plane agents that provide a seamless experience. +--- + +# Best Practices + +::: info +Plane Agents are currently in **Beta**. Please send any feedback to support@plane.so. +::: + +## Overview + +Building a great agent experience requires thoughtful design around responsiveness, error handling, and user communication. This guide covers best practices to ensure your agent feels native to Plane and provides a seamless experience for users. + +## Sending Immediate Thought Activity + +When your agent receives a webhook, users are waiting for a response. The most important best practice is to **acknowledge the request immediately**. + +### Why Immediate Acknowledgment Matters + +- Users see that your agent is active and processing their request +- Prevents the Agent Run from being marked as `stale` (5-minute timeout) +- Builds trust that the agent received and understood the request +- Provides visual feedback during potentially long processing times + +### Implementation + +Send a `thought` activity within the first few seconds of receiving a webhook: + +:::tabs key:language +== TypeScript {#typescript} + +```typescript +import { PlaneClient } from '@makeplane/plane-node-sdk'; + +async function handleWebhook(webhook: AgentRunActivityWebhook, credentials: { bot_token: string; workspace_slug: string }) { + const planeClient = new PlaneClient({ + baseUrl: process.env.PLANE_API_URL || 'https://api.plane.so', + accessToken: credentials.bot_token, + }); + + const agentRunId = webhook.agent_run.id; + + // IMMEDIATELY acknowledge receipt + await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { + type: 'thought', + content: { type: 'thought', body: 'Received your request. Analyzing...' }, + }); + + // Now proceed with actual processing + // This can take longer since user knows agent is working + const result = await processRequest(webhook); + + // ... rest of the logic +} +``` + +== Python {#python} + +```python +from plane import PlaneClient +from plane.models.agent_runs import CreateAgentRunActivity + +def handle_webhook(webhook: dict, credentials: dict): + plane_client = PlaneClient( + base_url=os.getenv("PLANE_API_URL", "https://api.plane.so"), + access_token=credentials["bot_token"], + ) + + agent_run_id = webhook["agent_run"]["id"] + + # IMMEDIATELY acknowledge receipt + plane_client.agent_runs.activities.create( + workspace_slug=credentials["workspace_slug"], + run_id=agent_run_id, + data=CreateAgentRunActivity( + type="thought", + content={"type": "thought", "body": "Received your request. Analyzing..."}, + ), + ) + + # Now proceed with actual processing + result = process_request(webhook) + + # ... rest of the logic +``` +::: + +### Thought Activity Best Practices + +- Keep thoughts concise but informative +- Update thoughts as you progress through different stages +- Use thoughts to explain what the agent is doing, not technical details + +**Good examples:** +- "Analyzing your question about project timelines..." +- "Searching for relevant work items..." +- "Preparing response with the requested data..." + +**Avoid:** +- "Initializing LLM context with temperature 0.7..." +- "Executing database query SELECT * FROM..." +- Generic messages like "Working..." repeated multiple times + +## Acknowledging Important Signals + +Signals communicate user intent beyond the message content. Your agent **must** handle the `stop` signal appropriately. + +### The Stop Signal + +When a user wants to stop an agent run, Plane sends a `stop` signal with the activity. Your agent should: + +1. **Recognize the signal immediately** +2. **Stop any ongoing processing** +3. **Send a confirmation response** + +:::tabs key:language +== TypeScript {#typescript} + +```typescript +async function handleWebhook(webhook: AgentRunActivityWebhook, credentials: { bot_token: string; workspace_slug: string }) { + const planeClient = new PlaneClient({ + baseUrl: process.env.PLANE_API_URL || 'https://api.plane.so', + accessToken: credentials.bot_token, + }); + + const signal = webhook.agent_run_activity.signal; + const agentRunId = webhook.agent_run.id; + + // ALWAYS check for stop signal first + if (signal === 'stop') { + // Cancel any ongoing work + cancelOngoingTasks(agentRunId); + + // Acknowledge the stop + await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { + type: 'response', + content: { + type: 'response', + body: "Understood. I've stopped processing your previous request.", + }, + }); + + return; // Exit early + } + + // Continue with normal processing... +} +``` + +== Python {#python} + +```python +def handle_webhook(webhook: dict, credentials: dict): + plane_client = PlaneClient( + base_url=os.getenv("PLANE_API_URL", "https://api.plane.so"), + access_token=credentials["bot_token"], + ) + + signal = webhook["agent_run_activity"]["signal"] + agent_run_id = webhook["agent_run"]["id"] + + # ALWAYS check for stop signal first + if signal == "stop": + # Cancel any ongoing work + cancel_ongoing_tasks(agent_run_id) + + # Acknowledge the stop + plane_client.agent_runs.activities.create( + workspace_slug=credentials["workspace_slug"], + run_id=agent_run_id, + data=CreateAgentRunActivity( + type="response", + content={ + "type": "response", + "body": "Understood. I've stopped processing your previous request.", + }, + ), + ) + + return # Exit early + + # Continue with normal processing... +``` +::: + +### Signal Considerations + +| Signal | How to Handle | +|--------|---------------| +| `continue` | Default behavior, proceed with processing | +| `stop` | Immediately halt and confirm | + +## Progress Communication + +For long-running tasks, keep users informed with progress updates. + +### Multi-Step Operations + +When your agent performs multiple steps, send thought activities for each: + +```typescript +// Step 1: Acknowledge +await createThought("Understanding your request..."); + +// Step 2: First action +await createAction("searchDocuments", { query: userQuery }); +const searchResults = await searchDocuments(userQuery); + +// Step 3: Processing +await createThought("Found relevant information. Analyzing..."); + +// Step 4: Additional work +await createAction("generateSummary", { data: searchResults }); +const summary = await generateSummary(searchResults); + +// Step 5: Final response +await createResponse(`Here's what I found: ${summary}`); +``` + +### Avoiding Information Overload + +While progress updates are important, too many can be overwhelming: + +- **Don't** send a thought for every internal function call +- **Do** send thoughts for user-meaningful milestones +- **Don't** expose technical implementation details +- **Do** explain what value is being created for the user + +## Error Handling + +Graceful error handling is crucial for a good user experience. + +### Always Catch and Report Errors + +```typescript +async function handleWebhook(webhook: AgentRunActivityWebhook, credentials: { bot_token: string; workspace_slug: string }) { + const planeClient = new PlaneClient({ + baseUrl: process.env.PLANE_API_URL || 'https://api.plane.so', + accessToken: credentials.bot_token, + }); + + const agentRunId = webhook.agent_run.id; + + try { + await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { + type: 'thought', + content: { type: 'thought', body: 'Processing your request...' }, + }); + + // Your logic here... + const result = await processRequest(webhook); + + await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { + type: 'response', + content: { type: 'response', body: result }, + }); + + } catch (error) { + // ALWAYS inform the user about errors + await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { + type: 'error', + content: { + type: 'error', + body: getUserFriendlyErrorMessage(error), + }, + }); + } +} + +function getUserFriendlyErrorMessage(error: Error): string { + // Map technical errors to user-friendly messages + if (error.message.includes('rate limit')) { + return "I'm receiving too many requests right now. Please try again in a few minutes."; + } + if (error.message.includes('timeout')) { + return 'The operation took too long. Please try a simpler request or try again later.'; + } + // Generic fallback + return 'I encountered an unexpected error. Please try again or contact support if the issue persists.'; +} +``` + +### Error Message Guidelines + +**Do:** +- Use clear, non-technical language +- Suggest next steps when possible +- Be honest about what went wrong (at a high level) + +**Don't:** +- Expose stack traces or technical details +- Blame the user for errors +- Leave users without any feedback + +## Handling Conversation Context + +For multi-turn conversations, maintain context from previous activities. + +### Fetching Previous Activities + +```typescript +// Get all activities for context +const activities = await planeClient.agentRuns.activities.list( + credentials.workspace_slug, + agentRunId +); + +// Build conversation history +const history = activities.results + .filter(a => a.type === 'prompt' || a.type === 'response') + .map(a => ({ + role: a.type === 'prompt' ? 'user' : 'assistant', + content: a.content.body, + })); + +// Use history in your LLM call or logic +const response = await processWithContext(newPrompt, history); +``` + +### Context Best Practices + +- Retrieve relevant history, not every single activity +- Filter to meaningful exchanges (prompts and responses) +- Consider summarizing long histories to save tokens/processing +- Don't assume infinite context availability + +## Rate Limiting and Timeouts + +Be mindful of Plane's API limits and your own processing time. + +### Stale Run Prevention + +Agent Runs are marked as `stale` after 5 minutes of inactivity. For long operations: + +```typescript +async function longRunningTask(agentRunId: string) { + const HEARTBEAT_INTERVAL = 60000; // 1 minute + + const heartbeat = setInterval(async () => { + await createThought("Still working on your request..."); + }, HEARTBEAT_INTERVAL); + + try { + const result = await performLongOperation(); + return result; + } finally { + clearInterval(heartbeat); + } +} +``` + +### Webhook Response Time + +- Return HTTP 200 from your webhook handler quickly (within seconds) +- Process the actual agent logic asynchronously +- Don't block the webhook response waiting for LLM calls + +```typescript +// Good: Respond immediately, process async +app.post("/webhook", async (req, res) => { + res.status(200).json({ received: true }); + + // Process in background + processWebhookAsync(req.body).catch(console.error); +}); +``` + +## Summary Checklist + +**Responsiveness** +- Send thought within seconds of webhook +- Return webhook response quickly +- Send heartbeats for long operations + +**Signal Handling** +- Always check for `stop` signal first +- Handle all signal types appropriately +- Confirm when stopping + +**Error Handling** +- Wrap processing in try/catch +- Always send error activity on failure +- Use friendly error messages + +**User Experience** +- Progress updates for long tasks +- Clear, non-technical communication +- Maintain conversation context + +## Next Steps + +- Learn about [Signals & Content Payload](/dev-tools/agents/signals-content-payload) for advanced activity handling +- Review the [Building an Agent](/dev-tools/agents/building-an-agent) guide for implementation details diff --git a/docs/dev-tools/agents/building-an-agent.md b/docs/dev-tools/agents/building-an-agent.md new file mode 100644 index 00000000..8bdb3e5c --- /dev/null +++ b/docs/dev-tools/agents/building-an-agent.md @@ -0,0 +1,580 @@ +--- +title: Building an Agent +description: Step-by-step guide to creating a Plane agent, including OAuth setup, webhook handling, and activity creation. +--- + +# Building an Agent + +::: info +Plane Agents are currently in **Beta**. Please send any feedback to support@plane.so. +::: + +## Prerequisites + +Before building an agent, make sure you have completed the following: + +1. **Build a Plane App** — Follow the [Build a Plane App](/dev-tools/build-plane-app) guide to understand OAuth flows, deployment, and webhook handling. + +2. **Get Your Bot Token** — Complete the [Bot Token Flow](/dev-tools/build-plane-app#bot-token-flow) to obtain a `bot_token` for your agent. This token is used for all API calls. + +3. **Set Up Webhook Handling** — Ensure your server can [receive and verify webhooks](/dev-tools/build-plane-app#handling-webhooks) from Plane. + +::: info +This guide assumes you have a working OAuth app with webhook handling. If not, complete the [Build a Plane App](/dev-tools/build-plane-app) guide first. +::: + +## Creating an Agent + +Building a Plane agent involves three main steps: + +1. Create an OAuth application with agent capabilities enabled +2. Implement the OAuth flow to install your agent in workspaces +3. Handle webhooks and create activities to respond to users + +### OAuth App Creation + +To create an agent, you first need to [register an OAuth application](/dev-tools/build-plane-app#create-an-oauth-application) with the **Enable App Mentions** checkbox enabled. + +1. Navigate to `https://app.plane.so//settings/integrations/` +2. Click on **Build your own** button +3. Fill out the required details: + - **Setup URL**: The URL users are redirected to when installing your app + - **Redirect URIs**: Where Plane sends the authorization code after consent + - **Webhook URL Endpoint**: Your service's webhook endpoint for receiving events +4. **Enable the "Enable App Mentions" checkbox** — This is required for agents +5. Save and securely store your **Client ID** and **Client Secret** + +::: info +The "Enable App Mentions" checkbox is what transforms a regular OAuth app into an agent that can be @mentioned in work items. +::: + +### Setting Is Mentionable + +When you enable app mentions during OAuth app creation, your application becomes mentionable in work item comments. This means: + +- Users will see your agent in the mention picker when typing `@` +- Your agent can be assigned or delegated work items +- Webhooks will be triggered when users interact with your agent + +After installation, your agent appears alongside workspace members in the mention autocomplete. + +## Agent Interaction + +Once your agent is installed via the [OAuth consent flow](/dev-tools/build-plane-app#bot-token-flow) and users start mentioning it, you need to handle the interactions through Agent Runs and Activities. + +### AgentRun + +An **AgentRun** tracks a complete interaction session between a user and your agent. + +#### Key Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | UUID | Unique identifier for the agent run | +| `agent_user` | UUID | The bot user ID representing your agent | +| `issue` | UUID | The work item where the interaction started | +| `project` | UUID | The project containing the work item | +| `workspace` | UUID | The workspace where the agent is installed | +| `comment` | UUID | The comment thread for this run | +| `source_comment` | UUID | The original comment that triggered the run | +| `creator` | UUID | The user who initiated the run | +| `status` | String | Current status (`created`, `in_progress`, `awaiting`, `completed`, `stopping`, `stopped`, `failed`, `stale`) | +| `started_at` | DateTime | When the run started | +| `ended_at` | DateTime | When the run ended (if applicable) | +| `stopped_at` | DateTime | When a stop was requested | +| `stopped_by` | UUID | User who requested the stop | +| `external_link` | URL | Optional link to external dashboard/logs | +| `error_metadata` | JSON | Error details if the run failed | +| `type` | String | Type of run (currently `comment_thread`) | + +### AgentRunActivity + +An **AgentRunActivity** represents a single message or action within an Agent Run. + +#### Key Fields + +| Field | Type | Description | +|-------|------|-------------| +| `id` | UUID | Unique identifier for the activity | +| `agent_run` | UUID | The parent Agent Run | +| `type` | String | Activity type (`prompt`, `thought`, `action`, `response`, `elicitation`, `error`) | +| `content` | JSON | The activity content (structure varies by type) | +| `content_metadata` | JSON | Additional metadata about the content | +| `ephemeral` | Boolean | If true, the activity is temporary and won't create a comment | +| `signal` | String | Signal for how to handle the activity (`continue`, `stop`, `auth_request`, `select`) | +| `signal_metadata` | JSON | Additional signal data | +| `actor` | UUID | The user or bot that created the activity | +| `comment` | UUID | Associated comment (for non-ephemeral activities) | + +### Creating Activities + +Your agent communicates back to users by creating activities. We recommend using the official SDKs which provide typed helpers and insulate your code from API changes. + +#### Install the SDK + +:::tabs key:language +== Node.js {#nodejs} + +```bash +npm install @makeplane/plane-node-sdk +``` + +== Python {#python} + +```bash +pip install plane-sdk +``` +::: + +#### Activity Examples + +:::tabs key:language +== TypeScript {#typescript} + +```typescript +import { PlaneClient } from '@makeplane/plane-node-sdk'; + +// Initialize the client with your bot token +const planeClient = new PlaneClient({ + baseUrl: process.env.PLANE_API_URL || 'https://api.plane.so', + accessToken: botToken, +}); + +// Create a thought activity (ephemeral - shows agent's reasoning) +await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { + type: 'thought', + content: { + type: 'thought', + body: "Analyzing the user's request about weather data...", + }, +}); + +// Create an action activity (ephemeral - shows tool usage) +await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { + type: 'action', + content: { + type: 'action', + action: 'getWeather', + parameters: { location: 'San Francisco' }, + }, +}); + +// Create a response activity (creates a visible comment) +await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { + type: 'response', + content: { + type: 'response', + body: 'The weather in San Francisco is currently 68°F with partly cloudy skies.', + }, + signal: 'continue', +}); + +// Create an elicitation activity (asks user for input) +await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { + type: 'elicitation', + content: { + type: 'elicitation', + body: 'Which city would you like me to check the weather for?', + }, +}); + +// Create an error activity +await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { + type: 'error', + content: { + type: 'error', + body: 'Unable to fetch weather data. Please try again later.', + }, +}); +``` + +== Python {#python} + +```python +from plane import PlaneClient +from plane.models.agent_runs import CreateAgentRunActivity + +# Initialize the client with your bot token +plane_client = PlaneClient( + base_url=os.getenv("PLANE_API_URL", "https://api.plane.so"), + access_token=bot_token, +) + +# Create a thought activity (ephemeral - shows agent's reasoning) +plane_client.agent_runs.activities.create( + workspace_slug=workspace_slug, + run_id=agent_run_id, + data=CreateAgentRunActivity( + type="thought", + content={ + "type": "thought", + "body": "Analyzing the user's request about weather data...", + }, + ), +) + +# Create an action activity (ephemeral - shows tool usage) +plane_client.agent_runs.activities.create( + workspace_slug=workspace_slug, + run_id=agent_run_id, + data=CreateAgentRunActivity( + type="action", + content={ + "type": "action", + "action": "getWeather", + "parameters": {"location": "San Francisco"}, + }, + ), +) + +# Create a response activity (creates a visible comment) +plane_client.agent_runs.activities.create( + workspace_slug=workspace_slug, + run_id=agent_run_id, + data=CreateAgentRunActivity( + type="response", + content={ + "type": "response", + "body": "The weather in San Francisco is currently 68°F with partly cloudy skies.", + }, + signal="continue", + ), +) + +# Create an elicitation activity (asks user for input) +plane_client.agent_runs.activities.create( + workspace_slug=workspace_slug, + run_id=agent_run_id, + data=CreateAgentRunActivity( + type="elicitation", + content={ + "type": "elicitation", + "body": "Which city would you like me to check the weather for?", + }, + ), +) + +# Create an error activity +plane_client.agent_runs.activities.create( + workspace_slug=workspace_slug, + run_id=agent_run_id, + data=CreateAgentRunActivity( + type="error", + content={ + "type": "error", + "body": "Unable to fetch weather data. Please try again later.", + }, + ), +) +``` +::: + +### Content Payload Types + +The `content` field structure varies based on the activity type: + +::: details Thought +Internal reasoning from the agent. Automatically marked as ephemeral. + +```json +{ + "type": "thought", + "body": "The user is asking about weather data for their location." +} +``` +::: + +::: details Action +A tool invocation. Automatically marked as ephemeral. You can include results after execution. + +```json +{ + "type": "action", + "action": "searchDatabase", + "parameters": { + "query": "weather API", + "limit": "10" + } +} +``` + +With result: + +```json +{ + "type": "action", + "action": "searchDatabase", + "parameters": { + "query": "weather API", + "result": "Found 3 matching records" + } +} +``` +::: + +::: details Response +A final response to the user. Creates a comment reply. + +```json +{ + "type": "response", + "body": "Here's the weather forecast for San Francisco..." +} +``` +::: + +::: details Elicitation +A question requesting user input. Creates a comment and sets run to `awaiting`. + +```json +{ + "type": "elicitation", + "body": "Could you please specify which date range you're interested in?" +} +``` +::: + +::: details Error +An error message. Creates a comment and sets run to `failed`. + +```json +{ + "type": "error", + "body": "I encountered an error while processing your request." +} +``` +::: + +### Signals + +Signals provide additional context about how an activity should be interpreted: + +| Signal | Description | +|--------|-------------| +| `continue` | Default signal, indicates the conversation can continue | +| `stop` | User requested to stop the agent run | +| `auth_request` | Agent needs user to authenticate with an external service | +| `select` | Agent is presenting options for user to select from | + +See [Signals & Content Payload](/dev-tools/agents/signals-content-payload) for detailed information. + +### Ephemeral Activities + +Activities with `ephemeral: true` are temporary and don't create comments. They're useful for showing agent progress without cluttering the conversation. + +The following activity types are automatically marked as ephemeral: +- `thought` +- `action` +- `error` + +Ephemeral activities are displayed temporarily in the UI and replaced when the next activity arrives. + +## AgentRun Webhooks + +Your agent receives webhooks when users interact with it. There are two main webhook events: + +### AgentRun Create Webhook + +Triggered when a new Agent Run is created (user first mentions your agent). + +**Event:** `agent_run_create` + +**Payload:** + +```json +{ + "action": "created", + "agent_run": { + "id": "uuid", + "agent_user": "uuid", + "issue": "uuid", + "project": "uuid", + "workspace": "uuid", + "status": "created", + "type": "comment_thread", + "started_at": "2025-01-15T10:30:00Z" + }, + "agent_user_id": "uuid", + "app_client_id": "your-client-id", + "issue_id": "uuid", + "project_id": "uuid", + "workspace_id": "uuid", + "comment_id": "uuid", + "type": "agent_run" +} +``` + +### AgentRun Activity Webhook + +Triggered when a user sends a prompt to your agent (initial mention or follow-up). + +**Event:** `agent_run_user_prompt` + +**Payload:** + +```json +{ + "action": "prompted", + "agent_run_activity": { + "id": "uuid", + "agent_run": "uuid", + "type": "prompt", + "content": { + "type": "prompt", + "body": "What's the weather like in San Francisco?" + }, + "ephemeral": false, + "signal": "continue", + "actor": "uuid", + "workspace": "uuid" + }, + "agent_run": { + "id": "uuid", + "agent_user": "uuid", + "issue": "uuid", + "project": "uuid", + "workspace": "uuid", + "status": "in_progress" + }, + "agent_user_id": "uuid", + "app_client_id": "your-client-id", + "comment_id": "uuid", + "issue_id": "uuid", + "project_id": "uuid", + "workspace_id": "uuid", + "type": "agent_run_activity" +} +``` + +### Handling Webhooks + +Here's a complete example of handling agent webhooks using the SDKs: + +:::tabs key:language +== TypeScript {#typescript} + +```typescript +import { PlaneClient } from '@makeplane/plane-node-sdk'; + +interface AgentRunActivityWebhook { + action: string; + agent_run_activity: { + id: string; + content: { type: string; body?: string }; + signal: string; + }; + agent_run: { + id: string; + status: string; + }; + workspace_id: string; + project_id: string; + type: string; +} + +async function handleWebhook( + webhook: AgentRunActivityWebhook, + credentials: { bot_token: string; workspace_slug: string } +) { + // Only handle agent_run_activity webhooks + if (webhook.type !== 'agent_run_activity') { + return; + } + + const planeClient = new PlaneClient({ + baseUrl: process.env.PLANE_API_URL || 'https://api.plane.so', + accessToken: credentials.bot_token, + }); + + const agentRunId = webhook.agent_run.id; + const userPrompt = webhook.agent_run_activity.content.body || ''; + const signal = webhook.agent_run_activity.signal; + + // Check for stop signal + if (signal === 'stop') { + await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { + type: 'response', + content: { type: 'response', body: 'Stopping as requested.' }, + }); + return; + } + + // Send initial thought (ephemeral - shows processing status) + await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { + type: 'thought', + content: { type: 'thought', body: 'Processing your request...' }, + }); + + // Process the request (implement your logic here) + const response = await processUserRequest(userPrompt); + + // Send the response (creates a visible comment) + await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { + type: 'response', + content: { type: 'response', body: response }, + }); +} +``` + +== Python {#python} + +```python +from plane import PlaneClient +from plane.models.agent_runs import CreateAgentRunActivity + +def handle_webhook(webhook: dict, credentials: dict): + """Handle incoming agent webhook.""" + # Only handle agent_run_activity webhooks + if webhook.get("type") != "agent_run_activity": + return + + plane_client = PlaneClient( + base_url=os.getenv("PLANE_API_URL", "https://api.plane.so"), + access_token=credentials["bot_token"], + ) + + agent_run_id = webhook["agent_run"]["id"] + user_prompt = webhook["agent_run_activity"]["content"].get("body", "") + signal = webhook["agent_run_activity"]["signal"] + + # Check for stop signal + if signal == "stop": + plane_client.agent_runs.activities.create( + workspace_slug=credentials["workspace_slug"], + run_id=agent_run_id, + data=CreateAgentRunActivity( + type="response", + content={"type": "response", "body": "Stopping as requested."}, + ), + ) + return + + # Send initial thought (ephemeral - shows processing status) + plane_client.agent_runs.activities.create( + workspace_slug=credentials["workspace_slug"], + run_id=agent_run_id, + data=CreateAgentRunActivity( + type="thought", + content={"type": "thought", "body": "Processing your request..."}, + ), + ) + + # Process the request (implement your logic here) + response = process_user_request(user_prompt) + + # Send the response (creates a visible comment) + plane_client.agent_runs.activities.create( + workspace_slug=credentials["workspace_slug"], + run_id=agent_run_id, + data=CreateAgentRunActivity( + type="response", + content={"type": "response", "body": response}, + ), + ) +``` +::: + +## Next Steps + +- Learn about [Best Practices](/dev-tools/agents/best-practices) for building responsive agents +- Explore [Signals & Content Payload](/dev-tools/agents/signals-content-payload) for advanced activity handling diff --git a/docs/dev-tools/agents/overview.md b/docs/dev-tools/agents/overview.md new file mode 100644 index 00000000..f62a4865 --- /dev/null +++ b/docs/dev-tools/agents/overview.md @@ -0,0 +1,140 @@ +--- +title: Agents Overview +description: Learn how to build AI agents that integrate with Plane workspaces, enabling automated task handling, intelligent responses, and seamless collaboration. +--- + +# Agents Overview + +::: info +Plane Agents are currently in **Beta**. Please send any feedback to support@plane.so. +::: + +## What are Agents in Plane? + +Agents in Plane are AI-powered applications that can interact with your workspace similar to how human users do. They can be @mentioned in work item comments, receive prompts from users, and respond with intelligent actions. Agents enable automation and AI assistance directly within your project management workflow. + +Key capabilities of Plane agents: + +- **Mentionable** — Users can @mention agents in work item comments to trigger interactions +- **Contextual awareness** — Agents receive full context about the work item, project, and conversation +- **Activity tracking** — All agent interactions are tracked through the Agent Run system +- **Real-time responses** — Agents can send thoughts, actions, and responses back to users + +## Agent Installation + +Agents are installed as OAuth applications in your Plane workspace. When you create an OAuth app with the **Enable App Mentions** option enabled, it becomes an agent that users can mention and interact with. + +### Installation Flow + +1. A workspace admin installs your agent via the OAuth consent flow +2. Plane creates a **bot user** for your agent in that workspace +3. The bot user ID is returned in the app installation details +4. Users can now @mention your agent in work item comments + +When installed, agents appear in the mention picker alongside regular workspace members. Users can tag them in comments just like any team member. + +::: info +Agents installed in your workspace do not count as billable users. +::: + +## Agent Run Lifecycle + +The Agent Run system tracks the complete lifecycle of an agent interaction, from when a user mentions the agent to when the agent completes its task. + +### What is an Agent Run? + +An **Agent Run** represents a single interaction session between a user and an agent. When a user @mentions an agent in a comment, Plane automatically creates an Agent Run to track: + +- The triggering comment and work item +- The conversation thread between user and agent +- All activities (thoughts, actions, responses) from the agent +- The current status of the interaction + +### What is an Agent Run Activity? + +An **Agent Run Activity** is a single unit of communication within an Agent Run. Activities can be: + +- **Prompt** — A message from a user to the agent +- **Thought** — Internal reasoning from the agent (ephemeral) +- **Action** — A tool invocation by the agent (ephemeral) +- **Response** — A final response from the agent (creates a comment) +- **Elicitation** — A question from the agent requesting user input +- **Error** — An error message from the agent + +### How Agent Run Works + +The Agent Run flow consists of three main phases: + +```mermaid +sequenceDiagram + participant User + participant Plane + participant Agent + + User->>Plane: @mentions agent in comment + Plane->>Plane: Create AgentRun + Activity (prompt) + Plane->>Agent: Send webhook (agent_run_activity) + + loop Agent Processing + Agent->>Plane: Create Activity (thought/action) + Agent->>Agent: Process and reason + end + + Agent->>Plane: Create Activity (response) + Plane->>User: Show response as comment +``` + +#### Phase 1: Trigger + +1. User @mentions the agent in a work item comment +2. Plane detects the mention and creates a new **Agent Run** +3. An **Agent Run Activity** is created with the user's prompt +4. The Agent Run status is set to `created` + +#### Phase 2: Webhook + +1. Plane triggers a webhook to your agent's webhook URL +2. The webhook payload includes: + - The Agent Run details + - The triggering activity (user prompt) + - Work item and project context + - Workspace information + +#### Phase 3: Agent Response + +1. Your agent processes the webhook and starts working +2. The agent sends activities back to Plane via the API: + - `thought` activities to show reasoning (ephemeral, don't create comments) + - `action` activities to show tool usage (ephemeral) + - `response` or `elicitation` when complete (creates a comment) +3. Plane updates the Agent Run status based on activities +4. Non-ephemeral activities (response, elicitation) create comment replies visible to users + +### Agent Run States + +Agent Runs transition through various states based on activities: + +| Status | Description | +|--------|-------------| +| `created` | The run has been initiated but not yet started processing | +| `in_progress` | The agent is actively processing the request | +| `awaiting` | The agent is waiting for additional input from the user | +| `completed` | The agent has successfully finished processing | +| `stopping` | A stop request has been received and is being processed | +| `stopped` | The run has been successfully stopped | +| `failed` | The run encountered an error and cannot continue | +| `stale` | The run has not been updated in 5 minutes and is considered stale | + +### Continuing a Conversation + +When a user replies to an agent's response: + +1. If an active Agent Run exists for that thread, the reply is added as a new activity +2. The webhook is triggered again with the updated context +3. The agent can continue the conversation with full history + +This enables multi-turn conversations where users and agents can have back-and-forth interactions. + +## Next Steps + +Ready to build your own agent? Continue to [Building an Agent](/dev-tools/agents/building-an-agent) to learn how to create and deploy your first Plane agent. diff --git a/docs/dev-tools/agents/signals-content-payload.md b/docs/dev-tools/agents/signals-content-payload.md new file mode 100644 index 00000000..ed80b127 --- /dev/null +++ b/docs/dev-tools/agents/signals-content-payload.md @@ -0,0 +1,828 @@ +--- +title: Signals & Content Payload +description: Detailed reference for activity signals and content payload structures in Plane agents. +--- + +# Signals & Content Payload + +::: info +Plane Agents are currently in **Beta**. Please send any feedback to support@plane.so. +::: + +## Overview + +Agent activities consist of two key components: + +1. **Content** — The message or action being communicated +2. **Signal** — Metadata indicating how the activity should be interpreted + +Understanding these components is essential for building agents that communicate effectively with users. + +## Signals + +Signals are metadata that modify how an activity should be interpreted or handled. They provide additional context about the sender's intent—guiding how the activity should be processed or responded to. + +### Available Signals + +| Signal | Description | Use Case | +|--------|-------------|----------| +| `continue` | Default signal, indicates normal flow | Standard responses, ongoing conversation | +| `stop` | User requested to stop the agent | Cancellation, abort operations | +| `auth_request` | Agent needs external authentication | OAuth flows, API key collection | +| `select` | Agent presenting options for selection | Multiple choice questions | + +### Signal: `continue` + +The default signal for most activities. Indicates normal conversation flow where the agent can continue processing. + +```json +{ + "type": "response", + "content": { + "type": "response", + "body": "Here's the information you requested." + }, + "signal": "continue" +} +``` + +### Signal: `stop` + +Sent by Plane when a user requests to stop the agent. Your agent should: + +1. Immediately halt any ongoing processing +2. Clean up resources if needed +3. Send a confirmation response + +**Incoming webhook with stop signal:** + +```json +{ + "action": "prompted", + "agent_run_activity": { + "type": "prompt", + "content": { + "type": "prompt", + "body": "Stop" + }, + "signal": "stop" + } +} +``` + +**Handling the stop signal:** + +```typescript +if (webhook.agent_run_activity.signal === 'stop') { + // Cancel ongoing work + await cancelAllPendingTasks(); + + // Acknowledge the stop - this transitions run to "stopped" status + await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { + type: 'response', + content: { + type: 'response', + body: "I've stopped working on your request.", + }, + }); + + return; +} +``` + +### Signal: `auth_request` + +Used when your agent needs the user to authenticate with an external service. Requires a URL in the `signal_metadata`. + +**Creating an auth request:** + +```typescript +await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { + type: 'elicitation', + content: { + type: 'elicitation', + body: 'I need access to your GitHub account to proceed. Please authenticate using the link below.', + }, + signal: 'auth_request', + signal_metadata: { + url: 'https://your-agent.com/auth/github?session=abc123', + }, +}); +``` + +**Requirements:** +- The URL must start with `https://` +- The URL should be a secure endpoint on your agent's server +- After authentication, redirect the user back or notify completion + +### Signal: `select` + +Used when presenting options for the user to choose from. Useful for disambiguation or multi-choice scenarios. + +```typescript +await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { + type: 'elicitation', + content: { + type: 'elicitation', + body: 'Which project would you like me to search?\n\n1. Frontend App\n2. Backend API\n3. Mobile App', + }, + signal: 'select', + signal_metadata: { + options: [ + { id: 'frontend', label: 'Frontend App' }, + { id: 'backend', label: 'Backend API' }, + { id: 'mobile', label: 'Mobile App' }, + ], + }, +}); +``` + +## Content Payload Types + +The `content` field contains the actual message or action. Its structure varies based on the activity type. + +### Type: `thought` + +Internal reasoning or progress updates from the agent. Automatically marked as ephemeral (won't create a comment). + +**Structure:** + +```typescript +interface ThoughtContent { + type: "thought"; + body: string; // The thought message +} +``` + +**Example:** + +```json +{ + "type": "thought", + "body": "The user is asking about deployment status. I'll check the CI/CD pipeline." +} +``` + +**Creating a thought activity:** + +:::tabs key:language +== TypeScript {#typescript} + +```typescript +await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { + type: 'thought', + content: { + type: 'thought', + body: 'Analyzing the codebase for potential issues...', + }, +}); +``` + +== Python {#python} + +```python +from plane.models.agent_runs import CreateAgentRunActivity + +plane_client.agent_runs.activities.create( + workspace_slug=credentials["workspace_slug"], + run_id=agent_run_id, + data=CreateAgentRunActivity( + type="thought", + content={ + "type": "thought", + "body": "Analyzing the codebase for potential issues...", + }, + ), +) +``` +::: + +**Best practices for thoughts:** +- Keep them concise and user-meaningful +- Use to show progress, not internal implementation +- Update as you move through stages of processing + +### Type: `action` + +Describes a tool invocation or external action. Automatically marked as ephemeral. + +**Structure:** + +```typescript +interface ActionContent { + type: "action"; + action: string; // Name of the tool/action + parameters: { // Key-value pairs of parameters + [key: string]: string; + }; +} +``` + +**Example - Starting an action:** + +```json +{ + "type": "action", + "action": "searchDatabase", + "parameters": { + "query": "bug reports", + "status": "open" + } +} +``` + +**Example - Action with result:** + +```json +{ + "type": "action", + "action": "searchDatabase", + "parameters": { + "query": "bug reports", + "status": "open", + "result": "Found 12 matching work items" + } +} +``` + +**Creating action activities:** + +:::tabs key:language +== TypeScript {#typescript} + +```typescript +// Before executing the action +await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { + type: 'action', + content: { + type: 'action', + action: 'fetchWeather', + parameters: { + location: 'San Francisco', + }, + }, +}); + +// Execute the actual action +const weatherData = await fetchWeather('San Francisco'); + +// After execution, report the result +await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentRunId, { + type: 'action', + content: { + type: 'action', + action: 'fetchWeather', + parameters: { + location: 'San Francisco', + result: `Temperature: ${weatherData.temp}°F, Conditions: ${weatherData.conditions}`, + }, + }, + content_metadata: { + result: weatherData, // Store full result in metadata + }, +}); +``` + +== Python {#python} + +```python +from plane.models.agent_runs import CreateAgentRunActivity + +# Before executing the action +plane_client.agent_runs.activities.create( + workspace_slug=credentials["workspace_slug"], + run_id=agent_run_id, + data=CreateAgentRunActivity( + type="action", + content={ + "type": "action", + "action": "fetchWeather", + "parameters": {"location": "San Francisco"}, + }, + ), +) + +# Execute the actual action +weather_data = fetch_weather("San Francisco") + +# After execution, report the result +plane_client.agent_runs.activities.create( + workspace_slug=credentials["workspace_slug"], + run_id=agent_run_id, + data=CreateAgentRunActivity( + type="action", + content={ + "type": "action", + "action": "fetchWeather", + "parameters": { + "location": "San Francisco", + "result": f"Temperature: {weather_data['temp']}°F, Conditions: {weather_data['conditions']}", + }, + }, + content_metadata={"result": weather_data}, + ), +) +``` +::: + +**Parameter requirements:** +- All parameter keys must be strings +- All parameter values must be strings +- Use `content_metadata` to store complex/structured data + +### Type: `response` + +A final response to the user. Creates a comment reply visible to users. + +**Structure:** + +```typescript +interface ResponseContent { + type: "response"; + body: string; // The response message (supports Markdown) +} +``` + +**Example:** + +```json +{ + "type": "response", + "body": "Based on my analysis, here are the top 3 issues affecting your sprint:\n\n1. **AUTH-123**: Login timeout affecting 15% of users\n2. **API-456**: Rate limiting too aggressive\n3. **UI-789**: Dashboard loading slowly\n\nWould you like me to provide more details on any of these?" +} +``` + +**Creating a response:** + +:::tabs key:language +== TypeScript {#typescript} + +```typescript +await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { + type: "response", + content: { + type: "response", + body: "Here's the weather in San Francisco:\n\n**68°F** - Partly Cloudy\n\nExpect mild conditions throughout the day.", + }, + signal: "continue", +}); +``` + +== Python {#python} + +```python +from plane.models.agent_runs import CreateAgentRunActivity + +plane_client.agent_runs.activities.create( + workspace_slug=workspace_slug, + run_id=agent_run_id, + data=CreateAgentRunActivity( + type="response", + content={ + "type": "response", + "body": "Here's the weather in San Francisco:\n\n**68°F** - Partly Cloudy\n\nExpect mild conditions throughout the day.", + }, + signal="continue", + ), +) +``` +::: + +**Response best practices:** +- Use Markdown for formatting +- Be clear and concise +- Include relevant context +- End with a call-to-action if appropriate + +### Type: `elicitation` + +Requests clarification or input from the user. Creates a comment and sets the Agent Run status to `awaiting`. + +**Structure:** + +```typescript +interface ElicitationContent { + type: "elicitation"; + body: string; // The question or request (supports Markdown) +} +``` + +**Example:** + +```json +{ + "type": "elicitation", + "body": "I found multiple projects matching your query. Which one would you like me to focus on?\n\n1. Project Alpha (12 open work items)\n2. Project Beta (8 open work items)\n3. Project Gamma (23 open work items)" +} +``` + +**Creating an elicitation:** + +:::tabs key:language +== TypeScript {#typescript} + +```typescript +await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { + type: "elicitation", + content: { + type: "elicitation", + body: "To generate the report, I need a few details:\n\n- What date range should I cover?\n- Should I include completed work items or only open ones?", + }, +}); +``` + +== Python {#python} + +```python +from plane.models.agent_runs import CreateAgentRunActivity + +plane_client.agent_runs.activities.create( + workspace_slug=workspace_slug, + run_id=agent_run_id, + data=CreateAgentRunActivity( + type="elicitation", + content={ + "type": "elicitation", + "body": "To generate the report, I need a few details:\n\n- What date range should I cover?\n- Should I include completed work items or only open ones?", + }, + ), +) +``` +::: + +**Elicitation best practices:** +- Ask specific, answerable questions +- Provide options when possible +- Don't ask too many questions at once +- Consider using `select` signal for multiple choice + +### Type: `error` + +Reports an error or failure. Creates a comment and sets the Agent Run status to `failed`. + +**Structure:** + +```typescript +interface ErrorContent { + type: "error"; + body: string; // The error message (supports Markdown) +} +``` + +**Example:** + +```json +{ + "type": "error", + "body": "I couldn't complete your request due to a connection issue with the external service. Please try again in a few minutes." +} +``` + +**Creating an error activity:** + +:::tabs key:language +== TypeScript {#typescript} + +```typescript +await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { + type: "error", + content: { + type: "error", + body: "I was unable to access the GitHub repository. Please ensure the integration is properly configured.", + }, + signal_metadata: { + error_code: "GITHUB_ACCESS_DENIED", + retryable: true, + }, +}); +``` + +== Python {#python} + +```python +from plane.models.agent_runs import CreateAgentRunActivity + +plane_client.agent_runs.activities.create( + workspace_slug=workspace_slug, + run_id=agent_run_id, + data=CreateAgentRunActivity( + type="error", + content={ + "type": "error", + "body": "I was unable to access the GitHub repository. Please ensure the integration is properly configured.", + }, + signal_metadata={ + "error_code": "GITHUB_ACCESS_DENIED", + "retryable": True, + }, + ), +) +``` +::: + +**Error best practices:** +- Use friendly, non-technical language +- Suggest next steps when possible +- Store detailed error info in `signal_metadata` +- Don't expose stack traces or sensitive information + +### Type: `prompt` + +This type is **user-generated only**. Your agent cannot create prompt activities—they're created by Plane when a user sends a message. + +**Structure:** + +```typescript +interface PromptContent { + type: "prompt"; + body: string; // The user's message +} +``` + +**Example received in webhook:** + +```json +{ + "type": "prompt", + "body": "Can you check the status of our deployment pipeline?" +} +``` + +## Ephemeral Activities + +Ephemeral activities are temporary and won't create comment replies. They're useful for showing agent progress without cluttering the conversation thread. + +### Automatically Ephemeral Types + +The following activity types are automatically marked as ephemeral: +- `thought` +- `action` +- `error` + +### Ephemeral Behavior + +- Ephemeral activities appear temporarily in the Agent UI +- They're replaced when the next activity arrives +- They don't create permanent comment replies +- Useful for real-time progress updates + +### Visual Example + +``` +User: @WeatherBot What's the weather in Tokyo? + +[Ephemeral - disappears when next activity arrives] +Analyzing your request... + +[Ephemeral - disappears when next activity arrives] +getCoordinates("Tokyo") + +[Ephemeral - disappears when next activity arrives] +getWeather(35.6762, 139.6503) → 72°F, Clear + +[Permanent - stays as comment] +The weather in Tokyo is currently 72°F with clear skies. +``` + +## Content Metadata + +Use `content_metadata` to store additional structured data about an activity: + +:::tabs key:language +== TypeScript {#typescript} + +```typescript +await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { + type: "action", + content: { + type: "action", + action: "analyzeCode", + parameters: { + file: "src/index.ts", + result: "Found 3 potential issues", + }, + }, + content_metadata: { + analysis_results: { + issues: [ + { line: 42, severity: "warning", message: "Unused variable" }, + { line: 78, severity: "error", message: "Type mismatch" }, + { line: 156, severity: "info", message: "Consider refactoring" }, + ], + processing_time_ms: 1250, + }, + }, +}); +``` + +== Python {#python} + +```python +from plane.models.agent_runs import CreateAgentRunActivity + +plane_client.agent_runs.activities.create( + workspace_slug=workspace_slug, + run_id=agent_run_id, + data=CreateAgentRunActivity( + type="action", + content={ + "type": "action", + "action": "analyzeCode", + "parameters": { + "file": "src/index.ts", + "result": "Found 3 potential issues", + }, + }, + content_metadata={ + "analysis_results": { + "issues": [ + {"line": 42, "severity": "warning", "message": "Unused variable"}, + {"line": 78, "severity": "error", "message": "Type mismatch"}, + {"line": 156, "severity": "info", "message": "Consider refactoring"}, + ], + "processing_time_ms": 1250, + }, + }, + ), +) +``` +::: + +## Signal Metadata + +Use `signal_metadata` to provide additional context for signals: + +```typescript +// Auth request with URL +{ + signal: "auth_request", + signal_metadata: { + url: "https://your-agent.com/auth/connect?session=xyz", + provider: "github", + scopes: ["repo", "read:user"], + } +} + +// Select with options +{ + signal: "select", + signal_metadata: { + options: [ + { id: "opt1", label: "Option 1", description: "First choice" }, + { id: "opt2", label: "Option 2", description: "Second choice" }, + ], + allow_multiple: false, + } +} + +// Error with details +{ + signal: "continue", + signal_metadata: { + error_code: "RATE_LIMIT_EXCEEDED", + retry_after: 60, + retryable: true, + } +} +``` + +## Complete Activity Creation Reference + +Here's a comprehensive example showing all activity types: + +:::tabs key:language +== TypeScript {#typescript} + +```typescript +import { PlaneClient } from '@makeplane/plane-node-sdk'; + +const planeClient = new PlaneClient({ + baseUrl: process.env.PLANE_API_URL || 'https://api.plane.so', + accessToken: botToken, +}); + +// 1. Thought - Show reasoning (ephemeral) +await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { + type: "thought", + content: { type: "thought", body: "Analyzing the request..." }, +}); + +// 2. Action - Tool invocation (ephemeral) +await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { + type: "action", + content: { + type: "action", + action: "searchWorkItems", + parameters: { query: "bug", status: "open" }, + }, +}); + +// 3. Response - Final answer (creates comment) +await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { + type: "response", + content: { type: "response", body: "Found 5 open bugs." }, + signal: "continue", +}); + +// 4. Elicitation - Ask for input (creates comment, awaits response) +await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { + type: "elicitation", + content: { type: "elicitation", body: "Which bug should I prioritize?" }, + signal: "select", + signal_metadata: { + options: [ + { id: "bug-1", label: "AUTH-123: Login timeout" }, + { id: "bug-2", label: "API-456: Rate limiting" }, + ], + }, +}); + +// 5. Error - Report failure (creates comment) +await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { + type: "error", + content: { type: "error", body: "Unable to access the database." }, + signal_metadata: { error_code: "DB_CONNECTION_FAILED" }, +}); +``` + +== Python {#python} + +```python +from plane import PlaneClient +from plane.models.agent_runs import CreateAgentRunActivity + +plane_client = PlaneClient( + base_url=os.getenv("PLANE_API_URL", "https://api.plane.so"), + access_token=bot_token, +) + +# 1. Thought - Show reasoning (ephemeral) +plane_client.agent_runs.activities.create( + workspace_slug=workspace_slug, + run_id=agent_run_id, + data=CreateAgentRunActivity( + type="thought", + content={"type": "thought", "body": "Analyzing the request..."}, + ), +) + +# 2. Action - Tool invocation (ephemeral) +plane_client.agent_runs.activities.create( + workspace_slug=workspace_slug, + run_id=agent_run_id, + data=CreateAgentRunActivity( + type="action", + content={ + "type": "action", + "action": "searchWorkItems", + "parameters": {"query": "bug", "status": "open"}, + }, + ), +) + +# 3. Response - Final answer (creates comment) +plane_client.agent_runs.activities.create( + workspace_slug=workspace_slug, + run_id=agent_run_id, + data=CreateAgentRunActivity( + type="response", + content={"type": "response", "body": "Found 5 open bugs."}, + signal="continue", + ), +) + +# 4. Elicitation - Ask for input (creates comment, awaits response) +plane_client.agent_runs.activities.create( + workspace_slug=workspace_slug, + run_id=agent_run_id, + data=CreateAgentRunActivity( + type="elicitation", + content={"type": "elicitation", "body": "Which bug should I prioritize?"}, + signal="select", + signal_metadata={ + "options": [ + {"id": "bug-1", "label": "AUTH-123: Login timeout"}, + {"id": "bug-2", "label": "API-456: Rate limiting"}, + ], + }, + ), +) + +# 5. Error - Report failure (creates comment) +plane_client.agent_runs.activities.create( + workspace_slug=workspace_slug, + run_id=agent_run_id, + data=CreateAgentRunActivity( + type="error", + content={"type": "error", "body": "Unable to access the database."}, + signal_metadata={"error_code": "DB_CONNECTION_FAILED"}, + ), +) +``` +::: + +## Next Steps + +- Review [Best Practices](/dev-tools/agents/best-practices) for building responsive agents +- See [Building an Agent](/dev-tools/agents/building-an-agent) for implementation examples +- Check the [Overview](/dev-tools/agents/overview) for Agent Run lifecycle details diff --git a/docs/dev-tools/build-plane-app.md b/docs/dev-tools/build-plane-app.md index a5eb7f3a..8fb3c6c9 100644 --- a/docs/dev-tools/build-plane-app.md +++ b/docs/dev-tools/build-plane-app.md @@ -1,716 +1,633 @@ --- -title: Build a Plane app -description: Create custom Plane apps and integrations using OAuth 2.0. Learn how to build webhooks, automate workflows, and extend Plane with third-party tools and services. -keywords: plane app development, plane oauth, plane webhooks, plane integration, build plane app, plane api integration, oauth 2.0 plane +title: Build a Plane App +description: Build and integrate an app with Plane using OAuth 2.0 authentication. --- +# Build a Plane App -# Build a Plane app +::: info +Plane apps are currently in **Beta**. Please send any feedback to support@plane.so. +::: -## Introduction -Plane apps seamlessly integrate tools and services with Plane so you can -use them without ever leaving your Workspace. Apps are conveniently available -from our [marketplace](https://plane.so/marketplace/integrations), helping you -stay focused and productive. +## Overview -## Why Build a Plane App? +Plane uses OAuth 2.0 to allow applications to access workspace data on behalf of users or as an autonomous bot. This guide covers how to register your app and implement the OAuth flow. -**Stop doing manual work** -Plane integrations eliminate repetitive tasks like copying updates between -tools, creating work items from support tickets, and generating status reports. -Instead of spending hours on administrative work, let your app handle it -automatically. +## Create an OAuth Application -**Connect everything you already use** -Your team probably uses dozens of different tools. Plane apps create a unified -workflow by connecting your favorite CRM, time tracking app, CI/CD pipelines, -communication tools, and more, together into Plane. One change in Plane can -trigger updates across your entire tech stack. +1. Navigate to **Workspace Settings** → **Integrations** (`https://app.plane.so//settings/integrations/`) +2. Click **Build your own** +3. Fill in the required details: -**Build exactly what you need** -Unlike rigid SaaS platforms, Plane's open core nature means you can create -integrations that fit your specific workflow. +| Field | Description | +|-------|-------------| +| **App Name** | Display name shown to users | +| **Setup URL** | Entry point when users install your app. Your app redirects users to Plane's consent screen from here. | +| **Redirect URI** | Callback URL where Plane sends users after they approve access, along with the authorization code. | +| **Webhook URL** | Endpoint for receiving event notifications | -## Prerequisites +4. For agents that respond to @mentions, enable **"Enable App Mentions"** +5. Save and store your **Client ID** and **Client Secret** securely -- A [Plane](https://app.plane.so) workspace -- Admin access to your workspace settings -- Familiarity with OAuth 2.0 concepts (authorization code flow) -- A backend server to handle OAuth token exchange +::: warning +Never expose your Client Secret in client-side code or commit it to version control. +::: -## High-Level Workflow +## Choose Your Flow -1. [Register your app on Plane developer portal](/dev-tools/build-plane-app#registering-your-app) -2. [Implement OAuth flow](/dev-tools/build-plane-app#implement-oauth-flow) -3. [Obtain and store access tokens securely](/dev-tools/build-plane-app#obtain-and-store-access-tokens-securely) -4. [Make authenticated API requests to Plane](/dev-tools/build-plane-app#make-authenticated-api-requests-to-plane) -5. [Handle token refresh](/dev-tools/build-plane-app#handle-token-refresh) +Plane supports two OAuth flows: -## Registering Your App +| Flow | Use When | Token Type | +|------|----------|------------| +| **Bot Token** (Client Credentials) | Agents, webhooks, automation, background tasks | `bot_token` | +| **User Token** (Authorization Code) | Actions on behalf of a specific user | `access_token` | -To build an OAuth application with Plane: +::: info +Most integrations should use the **Bot Token flow**. Use User Token only when you need to perform actions as a specific user. +::: -1. Navigate to `https://app.plane.so//settings/integrations/`. -2. Click on the **Build your own** button. -3. Fill out the form with the required details: +--- - - **Setup URL**: Provide the URL that users will be redirected to when they click "Install" from the marketplace or from the app listing. This URL should initiate the OAuth flow for your application. - - **Redirect URIs**: Provide the URIs where Plane will send the authorization code after the user consents to the app. - - **Webhook URL Endpoint(Optional)**: Your service's webhook endpoint. Plane will send an HTTP `POST` request to this endpoint upon every change to the workspace in which your app was installed. - - **Contact Details**: Add your email or other contact information. - - **Organization Details(Optional)**: Optionally include your contact email, privacy policy URL, terms of service URL, and any other relevant information. This helps Plane validate and approve your application should you choose to [list in the marketplace](#listing-your-app-on-plane-marketplace). +## Bot Token Flow -4. If you're building an agent (with or without using Plane's ADK) capable of performing operations when assigned or mentioned, enable the **Enable App Mentions** checkbox during app creation. -5. Once the app is created, securely store the generated **Client ID** and **Client Secret**. You will need these credentials to interact with Plane's API during the OAuth flow and for making authenticated API requests. +Use this flow for agents, webhook handlers, and automation that acts autonomously. -## Implement OAuth Flow +```mermaid +sequenceDiagram + participant User + participant Plane + participant YourApp -### Generating Consent URL (Optional) + User->>YourApp: Clicks "Install" + YourApp->>Plane: Redirects to consent screen + Plane->>User: Shows consent screen + User->>Plane: Approves + Plane->>YourApp: Redirects with app_installation_id + YourApp->>Plane: POST /auth/o/token/ (client_credentials) + Plane->>YourApp: Returns bot_token + YourApp->>YourApp: Store credentials +``` -This step is optional. This is needed only if the app should be installed from outside Plane's environment, the developer needs to generate the consent URL using the client ID generated during their app creation flow. +### 1. Redirect to Authorization -If this flow needs to be triggered from Plane marketplace as well, then provide the URL in "Setup URL" field on application create screen to redirect the user from marketplace on clicking "Install App" button. +When a user clicks "Install", redirect them to Plane's consent screen: -Below are sample implementations: +``` +GET https://api.plane.so/auth/o/authorize-app/ + ?client_id=YOUR_CLIENT_ID + &response_type=code + &redirect_uri=https://your-app.com/callback +``` -:::tabs key:language +### 2. Handle the Callback -== Python {#python} +After the user approves, Plane redirects to your Redirect URI with: -```python -import os -from urllib.parse import urlencode +| Parameter | Description | +|-----------|-------------| +| `app_installation_id` | Unique identifier for this installation | +| `code` | Authorization code (not used in bot flow) | + +### 3. Exchange for Bot Token + +``` +POST https://api.plane.so/auth/o/token/ +Content-Type: application/x-www-form-urlencoded +Authorization: Basic base64(client_id:client_secret) + +grant_type=client_credentials +&app_installation_id=APP_INSTALLATION_ID +``` + +**Response:** -params = { - "client_id": os.getenv("PLANE_CLIENT_ID"), - "response_type": "code", - "redirect_uri": os.getenv("PLANE_REDIRECT_URI"), - # Optional: include state if needed +```json +{ + "access_token": "pln_bot_xxxxxxxxxxxx", + "token_type": "Bearer", + "expires_in": 86400 } +``` + +### 4. Get Workspace Details -consent_url = f"https://api.plane.so/auth/o/authorize-app/?{urlencode(params)}" +``` +GET https://api.plane.so/auth/o/app-installation/?id=APP_INSTALLATION_ID +Authorization: Bearer YOUR_BOT_TOKEN ``` -== TypeScript {#typescript} +**Response:** -```typescript -import { URLSearchParams } from 'url'; +```json +[ + { + "id": "installation-uuid", + "workspace": "workspace-uuid", + "workspace_detail": { + "name": "My Workspace", + "slug": "my-workspace" + }, + "app_bot": "bot-user-uuid", + "status": "installed" + } +] +``` -const params = new URLSearchParams({ - client_id: process.env.PLANE_CLIENT_ID!, - response_type: "code", - redirect_uri: process.env.PLANE_REDIRECT_URI!, - // Optional: include state if needed -}); +Store the `workspace_detail.slug` for API calls and `app_installation_id` for token refresh. + +### 5. Refresh Bot Token + +Bot tokens expire. Request a new one using the stored `app_installation_id`: -const consentUrl = `https://api.plane.so/auth/o/authorize-app/?${params.toString()}`; ``` -::: +POST https://api.plane.so/auth/o/token/ +Content-Type: application/x-www-form-urlencoded +Authorization: Basic base64(client_id:client_secret) + +grant_type=client_credentials +&app_installation_id=APP_INSTALLATION_ID +``` + +--- -There are two types of authenticated actions your application can perform: +## User Token Flow -1. **User-authorized actions**: Actions performed on behalf of a user after they grant permission to your app via OAuth. -2. **App-authorized actions**: Actions that the app can perform independently within the workspace where it is installed (such as responding to webhooks or automation triggers). +Use this flow when your app needs to act on behalf of a specific user. -For both these flows, Plane will make a GET request to the Redirect URI with parameters as mentioned in the following sections. +```mermaid +sequenceDiagram + participant User + participant Plane + participant YourApp -We will describe how to configure and use each type in the following sections. + User->>YourApp: Clicks "Connect" + YourApp->>Plane: Redirects to consent screen + Plane->>User: Shows consent screen + User->>Plane: Approves + Plane->>YourApp: Redirects with code + YourApp->>Plane: POST /auth/o/token/ (authorization_code) + Plane->>YourApp: Returns access_token + refresh_token + YourApp->>YourApp: Store tokens for user +``` + +### 1. Redirect to Authorization + +``` +GET https://api.plane.so/auth/o/authorize-app/ + ?client_id=YOUR_CLIENT_ID + &response_type=code + &redirect_uri=https://your-app.com/callback + &state=RANDOM_STATE_VALUE +``` -### App-Authorized Actions (Client Credentials Flow) +::: info +Include a random `state` parameter to prevent CSRF attacks. Verify it matches when handling the callback. +::: -When the app is installed, Plane will send both a `code` and an `app_installation_id` as part of the callback to the Redirect URI provided during consent URL generation. You can use this `app_installation_id` to request a bot token for your app. +### 2. Handle the Callback -Plane will make a GET request to the Redirect URI with below parameters: +After approval, Plane redirects to your Redirect URI with: | Parameter | Description | |-----------|-------------| -| code | Authorization code (present but not used for client credentials flow) | -| app_installation_id | The unique identifier for the app installation in the workspace | +| `code` | Authorization code to exchange for tokens | +| `state` | Your state parameter (verify this matches) | -#### Examples +### 3. Exchange Code for Tokens -:::tabs key:language -== Python {#python} +``` +POST https://api.plane.so/auth/o/token/ +Content-Type: application/x-www-form-urlencoded + +grant_type=authorization_code +&code=AUTHORIZATION_CODE +&client_id=YOUR_CLIENT_ID +&client_secret=YOUR_CLIENT_SECRET +&redirect_uri=https://your-app.com/callback +``` -```python -import os -import time -from plane.oauth.api import OAuthApi -from plane.oauth.models import OAuthConfig - -# Initialize OAuth API -def get_oauth_api(): - oauth_config = OAuthConfig( - client_id=os.getenv("PLANE_CLIENT_ID"), - client_secret=os.getenv("PLANE_CLIENT_SECRET"), - redirect_uri=os.getenv("PLANE_REDIRECT_URI"), - ) - return OAuthApi( - oauth_config=oauth_config, - base_url=os.getenv("PLANE_BASE_URL", "https://api.plane.so"), - ) - -# Get bot token using app installation ID -oauth_api = get_oauth_api() -token_response = oauth_api.get_bot_token(app_installation_id) - -# Get app installation details -app_installations = oauth_api.get_app_installations( - token_response.access_token, - app_installation_id, -) +**Response:** + +```json +{ + "access_token": "pln_xxxxxxxxxxxx", + "refresh_token": "pln_refresh_xxxxxxxxxxxx", + "token_type": "Bearer", + "expires_in": 86400 +} +``` -if not app_installations: - raise Exception(f"No app installations found for app installation ID {app_installation_id}") +### 4. Refresh User Token -app_installation = app_installations[0] -bot_token = token_response.access_token -expires_in = token_response.expires_in ``` +POST https://api.plane.so/auth/o/token/ +Content-Type: application/x-www-form-urlencoded -== TypeScript {#typescript} +grant_type=refresh_token +&refresh_token=YOUR_REFRESH_TOKEN +&client_id=YOUR_CLIENT_ID +&client_secret=YOUR_CLIENT_SECRET +``` -```typescript -import axios from 'axios'; +--- -// Prepare basic auth header using client_id and client_secret -const clientId = "your_client_id"; -const clientSecret = "your_client_secret"; -const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); - -// Prepare request data -const payload = { - grant_type: "client_credentials", - app_installation_id: appInstallationId -}; - -// Make a POST request to fetch bot token -const response = await axios.post( - "https://api.plane.so/auth/o/token/", - payload, - { - headers: { - Authorization: `Basic ${basicAuth}`, - "Content-Type": "application/x-www-form-urlencoded" - } - } -); +## Making API Requests + +Include the token in the `Authorization` header: -// Parse the response -const responseData = response.data; -const botToken = responseData.access_token; -const expiresIn = responseData.expires_in; // Token expiry in seconds +``` +GET https://api.plane.so/api/v1/workspaces/{workspace_slug}/projects/ +Authorization: Bearer YOUR_TOKEN ``` -::: +See the [API Reference](/api-reference/introduction) for available endpoints. -### User-Authorized Actions (Authorization Code Flow) +--- -In this flow, your app exchanges the `code` received as a query parameter on the callback (to your Redirect URI) for an access token and refresh token. The access token is short-lived and must be refreshed using the refresh token when it expires. Both tokens should be securely stored. +## Handling Webhooks -Plane will make a GET request to the Redirect URI with below parameters: +When events occur in Plane, webhooks are sent to your Webhook URL. -| Parameter | Description | Required | -|-----------|-------------|----------| -| code | The authorization code that can be exchanged for an access token | Yes | -| state | The state parameter that was passed in the authorization request | No | +### Webhook Headers +| Header | Description | +|--------|-------------| +| `X-Plane-Delivery` | Unique delivery ID | +| `X-Plane-Event` | Event type (e.g., `issue`, `issue_comment`) | +| `X-Plane-Signature` | HMAC-SHA256 signature for verification | -#### Examples +### Verify Signature + +Always verify the `X-Plane-Signature` header: :::tabs key:language == Python {#python} ```python -from plane.oauth.api import OAuthApi -from plane.oauth.models import OAuthConfig - -# Initialize OAuth API -oauth_api = get_oauth_api() # Using the helper function from above - -# Exchange authorization code for access and refresh tokens -code = "authorization_code_from_callback" -token_response = oauth_api.exchange_code_for_token( - code, - "authorization_code", -) +import hmac +import hashlib -# Parse the response -access_token = token_response.access_token -refresh_token = token_response.refresh_token -expires_in = token_response.expires_in +def verify_signature(payload: bytes, signature: str, secret: str) -> bool: + expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest() + return hmac.compare_digest(expected, signature) ``` == TypeScript {#typescript} ```typescript -import axios from 'axios'; - -// Exchange authorization code for access and refresh tokens -const code = "authorization_code_from_callback"; -const clientId = "your_client_id"; -const clientSecret = "your_client_secret"; -const redirectUri = "your_redirect_uri"; - -const payload = { - grant_type: "authorization_code", - code: code, - client_id: clientId, - client_secret: clientSecret, - redirect_uri: redirectUri -}; - -const response = await axios.post( - "https://api.plane.so/auth/o/token/", - payload, - { - headers: { - "Content-Type": "application/x-www-form-urlencoded" - } - } -); +import crypto from 'crypto'; -// Parse the response -const responseData = response.data; -const accessToken = responseData.access_token; -const refreshToken = responseData.refresh_token; -const expiresIn = responseData.expires_in; +function verifySignature(payload: string, signature: string, secret: string): boolean { + const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex'); + return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)); +} ``` ::: -### Fetching App Installation Details +### Webhook Payload -In both user-authorized and app-authorized flows, the `app_installation_id` identifies the app's installation within a specific workspace. It is recommended that developers fetch workspace details after OAuth is successfully completed. Plane provides an `app-installation` endpoint that works with both types of tokens. +```json +{ + "event": "issue", + "action": "created", + "webhook_id": "webhook-uuid", + "workspace_id": "workspace-uuid", + "data": { ... }, + "activity": { + "actor": { "id": "user-uuid", "display_name": "John Doe" } + } +} +``` -#### Examples +See [Webhook Events](/dev-tools/intro-webhooks) for all event types. -:::tabs key:language -== Python {#python} +--- -```python -# Using the OAuth API to fetch app installation details -oauth_api = get_oauth_api() -app_installations = oauth_api.get_app_installations( - token, # Either access token or bot token - app_installation_id, -) +## Local Development + +For local development, use [ngrok](https://ngrok.com) to expose your server: -if app_installations: - workspace_details = app_installations[0] - print(f"Workspace: {workspace_details.workspace_detail.name}") - print(f"Workspace Slug: {workspace_details.workspace_detail.slug}") - print(f"Bot User ID: {workspace_details.app_bot}") +```bash +ngrok http 3000 ``` +Use the generated URL (e.g., `https://abc123.ngrok.io`) for your Setup URL, Redirect URI, and Webhook URL. -== TypeScript {#typescript} +::: info +Free ngrok URLs change on restart. Update your app settings when the URL changes. +::: +--- -```typescript -import axios from 'axios'; +## SDKs -// Set authorization header with either access token or bot token -const headers = { - Authorization: `Bearer ${token}`, -}; +Official SDKs provide OAuth helpers and typed API clients: -// Make GET request to fetch installation/workspace details -const response = await axios.get( - `https://api.plane.so/auth/o/app-installation/?id=${app_installation_id}`, - { headers } -); +| Language | Package | +|----------|---------| +| Node.js | [@makeplane/plane-node-sdk](https://www.npmjs.com/package/@makeplane/plane-node-sdk) | +| Python | [plane-sdk](https://pypi.org/project/plane-sdk/) | -const workspaceDetails = response.data[0]; +```bash +npm install @makeplane/plane-node-sdk +# or +pip install plane-sdk ``` -::: -#### Sample Response - -The app installation endpoint returns an **array** of installation objects. Typically, you'll want to use the first element `[0]`: - -```json -[ - { - "id": "34b97361-8636-43dc-953e-90deedc8498f", - "workspace_detail": { - "name": "sandbox", - "slug": "sandbox", - "id": "7a2e5944-c117-4a7d-b5f4-058fe705d7d1", - "logo_url": null - }, - "created_at": "2025-05-16T13:50:27.865821Z", - "updated_at": "2025-06-23T08:57:26.976742Z", - "deleted_at": null, - "status": "installed", - "workspace": "7a2e5944-c117-4a7d-b5f4-058fe705d7d1", - "application": "ab235529-388a-4f51-a55a-78272251f5f1", - "installed_by": "63333ab1-c605-42fc-82f7-5cd86799eca1", - "app_bot": "7286aaa7-9250-4851-a520-29c904fd7654", // Bot user ID for your app in this workspace - "webhook": "b1f4b7f1-51e8-4919-a84c-0b1143b51d2c" - } -] -``` +::: details SDK OAuth Helper Methods -**Key fields to note:** -- `app_bot`: The bot user ID that represents your app in the workspace -- `workspace_detail.slug`: The workspace slug needed for API calls -- `workspace`: The workspace ID for identifying the workspace -- `status`: Should be "installed" for active installations +**Node.js:** +```typescript +import { OAuthClient } from '@makeplane/plane-node-sdk'; -## Webhook Payload Structure +const oauth = new OAuthClient({ + clientId: 'your_client_id', + clientSecret: 'your_client_secret', + redirectUri: 'https://your-app.com/callback', +}); -When Plane sends webhooks to your application, the payload contains information about the event that occurred. Understanding this structure is crucial for processing webhooks effectively. +// Generate authorization URL +const authUrl = oauth.getAuthorizationUrl('code', 'state'); -### General Webhook Structure +// Exchange for bot token +const token = await oauth.getBotToken(appInstallationId); -All webhooks follow this general structure: +// Exchange code for user token +const userToken = await oauth.exchangeCodeForToken(code); -```json -{ - "event": "string", // Type of event (e.g., "issue", "issue_comment", "project") - "action": "string", // Action performed (e.g., "created", "updated", "deleted") - "webhook_id": "string", // Unique identifier for the webhook - "workspace_id": "string", // ID of the workspace where the event occurred - "data": { // Event-specific data (varies by event type) - "id": "string", - // ... other fields specific to the event - }, - "activity": { // Information about who performed the action - "actor": { - "id": "string", - "first_name": "string", - "last_name": "string", - "email": "string", - "avatar": "string", - "display_name": "string" - }, - "field": "string", // Field that was changed (for update events) - "new_value": "any", // New value (for update events) - "old_value": "any" // Previous value (for update events) - } -} +// Refresh user token +const newToken = await oauth.getRefreshToken(refreshToken); ``` -### Common Event Types +**Python:** +```python +from plane.client import OAuthClient -#### Issue Comment Events +oauth = OAuthClient( + client_id="your_client_id", + client_secret="your_client_secret", +) -When someone comments on an issue: +# Generate authorization URL +auth_url = oauth.get_authorization_url(redirect_uri="...", state="state") -```json -{ - "event": "issue_comment", - "action": "created", - "webhook_id": "uuid", - "workspace_id": "uuid", - "data": { - "id": "comment_id", - "issue": "issue_id", - "project": "project_id", - "workspace": "workspace_id", - "comment_html": "

Full HTML comment content

", - "comment_stripped": "Plain text comment content", - "created_at": "2025-01-01T00:00:00Z", - "updated_at": "2025-01-01T00:00:00Z" - }, - "activity": { - "actor": { - "id": "user_id", - "display_name": "User Name", - "email": "user@example.com" - } - } -} -``` - -#### Issue Events +# Exchange for bot token +token = oauth.get_client_credentials_token(app_installation_id=app_installation_id) -When an issue is created, updated, or deleted: +# Exchange code for user token +user_token = oauth.exchange_code(code=code, redirect_uri=redirect_uri) -```json -{ - "event": "issue", - "action": "updated", - "webhook_id": "uuid", - "workspace_id": "uuid", - "data": { - "id": "issue_id", - "name": "Issue Title", - "description_html": "

Issue description

", - "priority": "high", - "project": "project_id", - "workspace": "workspace_id", - "assignees": ["user_id_1", "user_id_2"], - "labels": ["label_id_1"], - "state": { - "id": "state_id", - "name": "In Progress", - "color": "#f39c12" - }, - "created_at": "2025-01-01T00:00:00Z", - "updated_at": "2025-01-01T00:00:00Z" - }, - "activity": { - "actor": { - "id": "user_id", - "display_name": "User Name" - }, - "field": "priority", - "new_value": "high", - "old_value": "medium" - } -} +# Refresh user token +new_token = oauth.refresh_token(refresh_token) ``` -### Processing Webhooks +::: -Here's how to process webhooks in your application: +--- +## Next Steps -:::tabs key:language -== Python {#python} +- [Build an Agent](/dev-tools/agents/overview) - Create AI agents that respond to @mentions +- [API Reference](/api-reference/introduction) - Explore the full Plane API +- [Webhook Events](/dev-tools/intro-webhooks) - All webhook event types +- [Example: PRD Agent](https://github.com/makeplane/prd-agent) - Complete agent implementation -```python -from typing import Dict, Any -from pydantic import ValidationError +--- -def handle_webhook(payload_data: Dict[str, Any]): - """Process incoming webhook from Plane with Pydantic validation""" - try: - # Validate webhook payload using Pydantic models - webhook = WebhookEvent(**payload_data) - print(f"Received {webhook.event} {webhook.action} event") - - # Get workspace credentials (implement your own storage) - credentials = get_credentials_for_workspace(webhook.workspace_id) - if not credentials: - raise Exception(f"No credentials found for workspace {webhook.workspace_id}") - - # Process specific event types with validated data - if webhook.event == 'issue_comment' and webhook.action == 'created': - comment_data = CommentEventData(**webhook.data) - comment_text = comment_data.comment_stripped or "" - - if '/your-command' in comment_text: - process_command(comment_data, credentials) - - elif webhook.event == 'issue' and webhook.action == 'updated': - issue_data = IssueEventData(**webhook.data) - - if webhook.activity.field == 'assignees': - handle_assignment_change(issue_data, credentials) - - except ValidationError as e: - print(f"Invalid webhook payload: {e}") - except Exception as e: - print(f"Error processing webhook: {e}") - -def process_command(comment_data: CommentEventData, credentials): - """Process custom commands from issue comments""" - from plane.api import WorkItemsApi - from plane.models import PatchedIssueRequest - - # Use the Plane API to respond to commands - # Implementation depends on your specific command logic - pass - -def handle_assignment_change(issue_data: IssueEventData, credentials): - """Handle issue assignment changes""" - # Your custom logic for handling assignments - pass -``` +## Complete Examples -== TypeScript {#typescript} +::: details TypeScript (Express) - Full Implementation ```typescript -interface WebhookPayload { - event: string; - action: string; - webhook_id: string; - workspace_id: string; - data: any; - activity: { - actor: { - id: string; - display_name: string; - email?: string; - }; - field?: string; - new_value?: any; - old_value?: any; - }; -} -// Process incoming webhook -async function handleWebhook(payload: WebhookPayload) { - console.log(`Received ${payload.event} ${payload.action} event`); - - // Get workspace credentials - const credentials = await getCredentialsForWorkspace(payload.workspace_id); - if (!credentials) { - throw new Error(`No credentials found for workspace ${payload.workspace_id}`); - } - - // Process specific event types - if (payload.event === 'issue_comment' && payload.action === 'created') { - const comment = payload.data.comment_stripped; - if (comment.includes('/your-command')) { - // Handle your custom command - await processCommand(payload.data, credentials); - } +import express from 'express'; +import axios from 'axios'; +import crypto from 'crypto'; + +const app = express(); + +const CLIENT_ID = process.env.PLANE_CLIENT_ID!; +const CLIENT_SECRET = process.env.PLANE_CLIENT_SECRET!; +const REDIRECT_URI = process.env.PLANE_REDIRECT_URI!; +const WEBHOOK_SECRET = process.env.PLANE_WEBHOOK_SECRET!; +const PLANE_API_URL = process.env.PLANE_API_URL || 'https://api.plane.so'; + +// In-memory storage (use a database in production) +const installations = new Map(); + +// Setup URL - redirect to Plane's consent screen +app.get('/oauth/setup', (req, res) => { + const params = new URLSearchParams({ + client_id: CLIENT_ID, + response_type: 'code', + redirect_uri: REDIRECT_URI, + }); + res.redirect(`${PLANE_API_URL}/auth/o/authorize-app/?${params}`); +}); + +// OAuth callback - exchange app_installation_id for bot token +app.get('/oauth/callback', async (req, res) => { + const appInstallationId = req.query.app_installation_id as string; + + if (!appInstallationId) { + return res.status(400).send('Missing app_installation_id'); } -} -``` -::: -## Obtain and store access tokens securely + try { + const basicAuth = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64'); + + // Exchange for bot token + const tokenRes = await axios.post( + `${PLANE_API_URL}/auth/o/token/`, + new URLSearchParams({ + grant_type: 'client_credentials', + app_installation_id: appInstallationId, + }).toString(), + { + headers: { + 'Authorization': `Basic ${basicAuth}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + } + ); + + const botToken = tokenRes.data.access_token; -Once you have obtained the access token, you can use it to make authenticated API requests to Plane. -Store the access token and refresh token securely in your database. + // Get workspace details + const installRes = await axios.get( + `${PLANE_API_URL}/auth/o/app-installation/?id=${appInstallationId}`, + { headers: { 'Authorization': `Bearer ${botToken}` } } + ); + const installation = installRes.data[0]; + const workspaceId = installation.workspace; + const workspaceSlug = installation.workspace_detail.slug; -## Make authenticated API requests to Plane + // Store credentials + installations.set(workspaceId, { botToken, workspaceSlug, appInstallationId }); -For making authenticated API requests to Plane, you can use the access token obtained from the OAuth flow. + console.log(`Installed in workspace: ${workspaceSlug}`); + res.send('Installation successful! You can close this window.'); -API reference is available at [https://docs.plane.so/api-reference](https://docs.plane.so/api-reference). + } catch (error) { + console.error('OAuth error:', error); + res.status(500).send('Installation failed'); + } +}); -We have official SDKs for the following languages to simplify the OAuth flow and make it easier to call Plane's API. +// Webhook handler +app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => { + const signature = req.headers['x-plane-signature'] as string; + const payload = req.body.toString(); -| Language | Package Link | Source Code | -|----------|---------|-------------| -| Node.js | [npm i @makeplane/plane-node-sdk](https://www.npmjs.com/package/@makeplane/plane-node-sdk) | [plane-node-sdk](https://github.com/makeplane/plane-node-sdk) | -| Python | [pip install plane-sdk](https://pypi.org/project/plane-sdk/) | [plane-python-sdk](https://github.com/makeplane/plane-python-sdk) | + // Verify signature + const expected = crypto.createHmac('sha256', WEBHOOK_SECRET).update(payload).digest('hex'); + if (!crypto.timingSafeEqual(Buffer.from(signature || ''), Buffer.from(expected))) { + return res.status(403).send('Invalid signature'); + } -## Handle Token Refresh + const event = JSON.parse(payload); + console.log(`Received: ${event.event} ${event.action}`); -Token refresh works differently depending on the type of token you're using: + // Get credentials for this workspace + const creds = installations.get(event.workspace_id); + if (creds) { + // Process the event with creds.botToken + } -### Bot Token Refresh (Client Credentials Flow) + res.status(200).send('OK'); +}); -Bot tokens obtained through the client credentials flow don't use refresh tokens. Instead, when a bot token expires, you simply request a new one using the same `app_installation_id`: +app.listen(3000, () => console.log('Server running on http://localhost:3000')); +``` +::: -:::tabs key:language -== Python {#python} +::: details Python (Flask) - Full Implementation ```python -# When bot token expires, request a new one using the same app_installation_id -from plane.oauth.api import OAuthApi - -def refresh_bot_token(app_installation_id: str): - """Refresh an expired bot token""" - oauth_api = get_oauth_api() # Using helper function from earlier examples - - # Get new bot token using the same app_installation_id - token_response = oauth_api.get_bot_token(app_installation_id) - - # Store the new token securely in your database - new_bot_token = token_response.access_token - expires_in = token_response.expires_in - - return new_bot_token, expires_in - -# Usage example -new_token, expires_in = refresh_bot_token(app_installation_id) -``` +import os +import hmac +import hashlib +import base64 +import requests as http_requests +from flask import Flask, request, redirect +from urllib.parse import urlencode -== TypeScript {#typescript} +app = Flask(__name__) -```typescript -// When bot token expires, request a new one using the same app_installation_id -import axios from 'axios'; +CLIENT_ID = os.getenv("PLANE_CLIENT_ID") +CLIENT_SECRET = os.getenv("PLANE_CLIENT_SECRET") +REDIRECT_URI = os.getenv("PLANE_REDIRECT_URI") +WEBHOOK_SECRET = os.getenv("PLANE_WEBHOOK_SECRET") +PLANE_API_URL = os.getenv("PLANE_API_URL", "https://api.plane.so") -const clientId = "your_client_id"; -const clientSecret = "your_client_secret"; -const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); +# In-memory storage (use a database in production) +installations = {} -const payload = { - grant_type: "client_credentials", - app_installation_id: appInstallationId // Same ID used during initial setup -}; -const response = await axios.post( - "https://api.plane.so/auth/o/token/", - payload, - { - headers: { - Authorization: `Basic ${basicAuth}`, - "Content-Type": "application/x-www-form-urlencoded" - } - } -); +@app.route("/oauth/setup") +def oauth_setup(): + """Redirect to Plane's consent screen.""" + params = urlencode({ + "client_id": CLIENT_ID, + "response_type": "code", + "redirect_uri": REDIRECT_URI, + }) + return redirect(f"{PLANE_API_URL}/auth/o/authorize-app/?{params}") -// Parse the response -const responseData = response.data; -const newBotToken = responseData.access_token; -const expiresIn = responseData.expires_in; -``` -::: -### User Token Refresh (Authorization Code Flow) +@app.route("/oauth/callback") +def oauth_callback(): + """Exchange app_installation_id for bot token.""" + app_installation_id = request.args.get("app_installation_id") -When user access tokens expire, you can use the refresh token to get a new access token: + if not app_installation_id: + return "Missing app_installation_id", 400 + try: + # Exchange for bot token + credentials = f"{CLIENT_ID}:{CLIENT_SECRET}" + basic_auth = base64.b64encode(credentials.encode()).decode() + + token_response = http_requests.post( + f"{PLANE_API_URL}/auth/o/token/", + data={ + "grant_type": "client_credentials", + "app_installation_id": app_installation_id, + }, + headers={ + "Authorization": f"Basic {basic_auth}", + "Content-Type": "application/x-www-form-urlencoded", + }, + ) + token_response.raise_for_status() + bot_token = token_response.json()["access_token"] + + # Get workspace details + install_response = http_requests.get( + f"{PLANE_API_URL}/auth/o/app-installation/", + params={"id": app_installation_id}, + headers={"Authorization": f"Bearer {bot_token}"}, + ) + install_response.raise_for_status() + installation = install_response.json()[0] + + workspace_id = installation["workspace"] + workspace_slug = installation["workspace_detail"]["slug"] + + # Store credentials + installations[workspace_id] = { + "bot_token": bot_token, + "workspace_slug": workspace_slug, + "app_installation_id": app_installation_id, + } + + print(f"Installed in workspace: {workspace_slug}") + return "Installation successful! You can close this window." -#### Examples + except Exception as e: + print(f"OAuth error: {e}") + return "Installation failed", 500 -:::tabs key:language -== Python {#python} -```python -# When access token expires, use refresh token to get a new access token -from plane.oauth.api import OAuthApi - -def refresh_user_token(refresh_token: str): - """Refresh an expired user access token""" - oauth_api = get_oauth_api() # Using helper function from earlier examples - - # Use refresh token to get new access token - token_response = oauth_api.exchange_code_for_token( - refresh_token, - "refresh_token", - ) - - # Store the new tokens securely - new_access_token = token_response.access_token - new_refresh_token = token_response.refresh_token # May be the same or new - expires_in = token_response.expires_in - - return new_access_token, new_refresh_token, expires_in - -# Usage example -new_access_token, new_refresh_token, expires_in = refresh_user_token(stored_refresh_token) -``` +@app.route("/webhook", methods=["POST"]) +def webhook(): + """Handle incoming webhooks.""" + signature = request.headers.get("X-Plane-Signature", "") + payload = request.get_data() -== TypeScript {#typescript} + # Verify signature + expected = hmac.new( + WEBHOOK_SECRET.encode(), payload, hashlib.sha256 + ).hexdigest() -```typescript -// When access token expires, use refresh token to get a new access token -const refreshPayload = { - grant_type: "refresh_token", - refresh_token: refreshToken, - client_id: clientId, - client_secret: clientSecret -}; - -const refreshResponse = await axios.post( - "https://api.plane.so/auth/o/token/", - refreshPayload, - { - headers: { - "Content-Type": "application/x-www-form-urlencoded" - } - } -); + if not hmac.compare_digest(expected, signature): + return "Invalid signature", 403 + + event = request.get_json() + print(f"Received: {event['event']} {event['action']}") -// Parse the refresh response -const refreshResponseData = refreshResponse.data; -const accessToken = refreshResponseData.access_token; + # Get credentials for this workspace + creds = installations.get(event["workspace_id"]) + if creds: + # Process the event with creds["bot_token"] + pass + + return "OK", 200 + + +if __name__ == "__main__": + app.run(port=3000) ``` + ::: -## Listing Your App on Plane Marketplace +--- -Apps built using the OAuth flow can be listed on the Plane Marketplace: [https://plane.so/marketplace/integrations](https://plane.so/marketplace/integrations) +## Publish to Marketplace -To list your app, please contact the Plane team at [**support@plane.so**](mailto:support@plane.so). \ No newline at end of file +Apps can be listed on the [Plane Marketplace](https://plane.so/marketplace/integrations). Contact [support@plane.so](mailto:support@plane.so) to list your app. diff --git a/package.json b/package.json index deb97995..0bb406a8 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,11 @@ "devDependencies": { "@tailwindcss/postcss": "^4.1.18", "autoprefixer": "^10.4.23", + "mermaid": "^11.12.2", "postcss": "^8.5.6", "tailwindcss": "^4.1.18", "vitepress": "^1.6.4", + "vitepress-plugin-mermaid": "^2.0.17", "vitepress-plugin-tabs": "^0.7.3", "vue": "^3.5.26" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b77b1c27..4178c8f1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,9 @@ importers: autoprefixer: specifier: ^10.4.23 version: 10.4.23(postcss@8.5.6) + mermaid: + specifier: ^11.12.2 + version: 11.12.2 postcss: specifier: ^8.5.6 version: 8.5.6 @@ -27,6 +30,9 @@ importers: vitepress: specifier: ^1.6.4 version: 1.6.4(@algolia/client-search@5.47.0)(lightningcss@1.30.2)(postcss@8.5.6)(search-insights@2.17.3) + vitepress-plugin-mermaid: + specifier: ^2.0.17 + version: 2.0.17(mermaid@11.12.2)(vitepress@1.6.4(@algolia/client-search@5.47.0)(lightningcss@1.30.2)(postcss@8.5.6)(search-insights@2.17.3)) vitepress-plugin-tabs: specifier: ^0.7.3 version: 0.7.3(vitepress@1.6.4(@algolia/client-search@5.47.0)(lightningcss@1.30.2)(postcss@8.5.6)(search-insights@2.17.3))(vue@3.5.27) @@ -116,6 +122,9 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + '@babel/helper-string-parser@7.27.1': resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} @@ -133,6 +142,27 @@ packages: resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} engines: {node: '>=6.9.0'} + '@braintree/sanitize-url@6.0.4': + resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==} + + '@braintree/sanitize-url@7.1.1': + resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==} + + '@chevrotain/cst-dts-gen@11.0.3': + resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} + + '@chevrotain/gast@11.0.3': + resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==} + + '@chevrotain/regexp-to-ast@11.0.3': + resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==} + + '@chevrotain/types@11.0.3': + resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==} + + '@chevrotain/utils@11.0.3': + resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + '@docsearch/css@3.8.2': resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==} @@ -300,6 +330,9 @@ packages: '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + '@iconify/utils@3.1.0': + resolution: {integrity: sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -316,6 +349,12 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@mermaid-js/mermaid-mindmap@9.3.0': + resolution: {integrity: sha512-IhtYSVBBRYviH1Ehu8gk69pMDF8DSRqXBRDMWrEfHoaMruHeaP2DXA3PBnuwsMaCdPQhlUUcy/7DBLAEIXvCAw==} + + '@mermaid-js/parser@0.6.3': + resolution: {integrity: sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==} + '@rollup/rollup-android-arm-eabi@4.56.0': resolution: {integrity: sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==} cpu: [arm] @@ -553,9 +592,105 @@ packages: '@tailwindcss/postcss@4.1.18': resolution: {integrity: sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==} + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.7': + resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.8': + resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -571,6 +706,9 @@ packages: '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -675,6 +813,11 @@ packages: '@vueuse/shared@12.8.2': resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + algoliasearch@5.47.0: resolution: {integrity: sha512-AGtz2U7zOV4DlsuYV84tLp2tBbA7RPtLA44jbVH4TTpDcc1dIWmULjHSsunlhscbzDydnjuFlNhflR3nV4VJaQ==} engines: {node: '>= 14.0.0'} @@ -710,16 +853,203 @@ packages: character-entities-legacy@3.0.0: resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + chevrotain-allstar@0.3.1: + resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} + peerDependencies: + chevrotain: ^11.0.0 + + chevrotain@11.0.3: + resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==} + comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + copy-anything@4.0.5: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.33.1: + resolution: {integrity: sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==} + engines: {node: '>=0.10'} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.13: + resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==} + + dayjs@1.11.19: + resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==} + + delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -731,6 +1061,9 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} + electron-to-chromium@1.5.277: resolution: {integrity: sha512-wKXFZw4erWmmOz5N/grBoJ2XrNJGDFMu2+W5ACHza5rHtvsqrK4gb6rnLC7XxKB9WlJ+RmyQatuEXmtm86xbnw==} @@ -771,6 +1104,9 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + hast-util-to-html@9.0.5: resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} @@ -783,6 +1119,17 @@ packages: html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + is-what@5.5.0: resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} engines: {node: '>=18'} @@ -791,6 +1138,23 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + katex@0.16.28: + resolution: {integrity: sha512-YHzO7721WbmAL6Ov1uzN/l5mY5WWWhJBSW+jq4tkfZfsxmo1hu6frS0EOswvjBUnWE6NtjEs48SFn5CQESRLZg==} + hasBin: true + + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + + langium@3.3.1: + resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==} + engines: {node: '>=16.0.0'} + + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + lightningcss-android-arm64@1.30.2: resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} engines: {node: '>= 12.0.0'} @@ -861,6 +1225,12 @@ packages: resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash-es@4.17.23: + resolution: {integrity: sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==} + lucide-vue-next@0.562.0: resolution: {integrity: sha512-LN0BLGKMFulv0lnfK29r14DcngRUhIqdcaL0zXTt2o0oS9odlrjCGaU3/X9hIihOjjN8l8e+Y9G/famcNYaI7Q==} peerDependencies: @@ -872,9 +1242,17 @@ packages: mark.js@8.11.1: resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} + marked@16.4.2: + resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} + engines: {node: '>= 20'} + hasBin: true + mdast-util-to-hast@13.2.1: resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + mermaid@11.12.2: + resolution: {integrity: sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w==} + micromark-util-character@2.1.1: resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} @@ -896,6 +1274,9 @@ packages: mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -904,15 +1285,36 @@ packages: node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + non-layered-tidy-tree-layout@2.0.2: + resolution: {integrity: sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==} + oniguruma-to-es@3.1.1: resolution: {integrity: sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==} + package-manager-detector@1.6.0: + resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} + + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -938,11 +1340,23 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + rollup@4.56.0: resolution: {integrity: sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + search-insights@2.17.3: resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} @@ -963,6 +1377,9 @@ packages: stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + stylis@4.3.6: + resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} + superjson@2.2.6: resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} engines: {node: '>=16'} @@ -977,9 +1394,20 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} @@ -1001,6 +1429,10 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -1038,6 +1470,12 @@ packages: terser: optional: true + vitepress-plugin-mermaid@2.0.17: + resolution: {integrity: sha512-IUzYpwf61GC6k0XzfmAmNrLvMi9TRrVRMsUyCA8KNXhg/mQ1VqWnO0/tBVPiX5UoKF1mDUwqn5QV4qAJl6JnUg==} + peerDependencies: + mermaid: 10 || 11 + vitepress: ^1.0.0 || ^1.0.0-alpha + vitepress-plugin-tabs@0.7.3: resolution: {integrity: sha512-CkUz49UrTLcVOszuiHIA7ZBvfsg9RluRkFjRG1KvCg/NwuOTLZwcBRv7vBB3vMlDp0bWXIFOIwdI7bE93cV3Hw==} peerDependencies: @@ -1056,6 +1494,26 @@ packages: postcss: optional: true + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + vue@3.5.27: resolution: {integrity: sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==} peerDependencies: @@ -1183,6 +1641,11 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.6.0 + tinyexec: 1.0.2 + '@babel/helper-string-parser@7.27.1': {} '@babel/helper-validator-identifier@7.28.5': {} @@ -1196,6 +1659,28 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@braintree/sanitize-url@6.0.4': + optional: true + + '@braintree/sanitize-url@7.1.1': {} + + '@chevrotain/cst-dts-gen@11.0.3': + dependencies: + '@chevrotain/gast': 11.0.3 + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/gast@11.0.3': + dependencies: + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/regexp-to-ast@11.0.3': {} + + '@chevrotain/types@11.0.3': {} + + '@chevrotain/utils@11.0.3': {} + '@docsearch/css@3.8.2': {} '@docsearch/js@3.8.2(@algolia/client-search@5.47.0)(search-insights@2.17.3)': @@ -1295,6 +1780,12 @@ snapshots: '@iconify/types@2.0.0': {} + '@iconify/utils@3.1.0': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@iconify/types': 2.0.0 + mlly: 1.8.0 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1314,6 +1805,21 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@mermaid-js/mermaid-mindmap@9.3.0': + dependencies: + '@braintree/sanitize-url': 6.0.4 + cytoscape: 3.33.1 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1) + cytoscape-fcose: 2.2.0(cytoscape@3.33.1) + d3: 7.9.0 + khroma: 2.1.0 + non-layered-tidy-tree-layout: 2.0.2 + optional: true + + '@mermaid-js/parser@0.6.3': + dependencies: + langium: 3.3.1 + '@rollup/rollup-android-arm-eabi@4.56.0': optional: true @@ -1498,8 +2004,127 @@ snapshots: postcss: 8.5.6 tailwindcss: 4.1.18 + '@types/d3-array@3.2.2': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.2 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.7': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.8': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.7 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.8 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + '@types/estree@1.0.8': {} + '@types/geojson@7946.0.16': {} + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -1517,6 +2142,9 @@ snapshots: '@types/mdurl@2.0.0': {} + '@types/trusted-types@2.0.7': + optional: true + '@types/unist@3.0.3': {} '@types/web-bluetooth@0.0.21': {} @@ -1627,6 +2255,8 @@ snapshots: transitivePeerDependencies: - typescript + acorn@8.15.0: {} + algoliasearch@5.47.0: dependencies: '@algolia/abtesting': 1.13.0 @@ -1673,14 +2303,232 @@ snapshots: character-entities-legacy@3.0.0: {} + chevrotain-allstar@0.3.1(chevrotain@11.0.3): + dependencies: + chevrotain: 11.0.3 + lodash-es: 4.17.23 + + chevrotain@11.0.3: + dependencies: + '@chevrotain/cst-dts-gen': 11.0.3 + '@chevrotain/gast': 11.0.3 + '@chevrotain/regexp-to-ast': 11.0.3 + '@chevrotain/types': 11.0.3 + '@chevrotain/utils': 11.0.3 + lodash-es: 4.17.21 + comma-separated-tokens@2.0.3: {} + commander@7.2.0: {} + + commander@8.3.0: {} + + confbox@0.1.8: {} + copy-anything@4.0.5: dependencies: is-what: 5.5.0 + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + csstype@3.2.3: {} + cytoscape-cose-bilkent@4.1.0(cytoscape@3.33.1): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.33.1 + + cytoscape-fcose@2.2.0(cytoscape@3.33.1): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.33.1 + + cytoscape@3.33.1: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.0.1 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.13: + dependencies: + d3: 7.9.0 + lodash-es: 4.17.23 + + dayjs@1.11.19: {} + + delaunator@5.0.1: + dependencies: + robust-predicates: 3.0.2 + dequal@2.0.3: {} detect-libc@2.1.2: {} @@ -1689,6 +2537,10 @@ snapshots: dependencies: dequal: 2.0.3 + dompurify@3.3.1: + optionalDependencies: + '@types/trusted-types': 2.0.7 + electron-to-chromium@1.5.277: {} emoji-regex-xs@1.0.0: {} @@ -1741,6 +2593,8 @@ snapshots: graceful-fs@4.2.11: {} + hachure-fill@0.5.2: {} + hast-util-to-html@9.0.5: dependencies: '@types/hast': 3.0.4 @@ -1763,10 +2617,36 @@ snapshots: html-void-elements@3.0.0: {} + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + internmap@1.0.1: {} + + internmap@2.0.3: {} + is-what@5.5.0: {} jiti@2.6.1: {} + katex@0.16.28: + dependencies: + commander: 8.3.0 + + khroma@2.1.0: {} + + langium@3.3.1: + dependencies: + chevrotain: 11.0.3 + chevrotain-allstar: 0.3.1(chevrotain@11.0.3) + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + lightningcss-android-arm64@1.30.2: optional: true @@ -1816,6 +2696,10 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 + lodash-es@4.17.21: {} + + lodash-es@4.17.23: {} + lucide-vue-next@0.562.0(vue@3.5.27): dependencies: vue: 3.5.27 @@ -1826,6 +2710,8 @@ snapshots: mark.js@8.11.1: {} + marked@16.4.2: {} + mdast-util-to-hast@13.2.1: dependencies: '@types/hast': 3.0.4 @@ -1838,6 +2724,29 @@ snapshots: unist-util-visit: 5.1.0 vfile: 6.0.3 + mermaid@11.12.2: + dependencies: + '@braintree/sanitize-url': 7.1.1 + '@iconify/utils': 3.1.0 + '@mermaid-js/parser': 0.6.3 + '@types/d3': 7.4.3 + cytoscape: 3.33.1 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.33.1) + cytoscape-fcose: 2.2.0(cytoscape@3.33.1) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.13 + dayjs: 1.11.19 + dompurify: 3.3.1 + katex: 0.16.28 + khroma: 2.1.0 + lodash-es: 4.17.23 + marked: 16.4.2 + roughjs: 4.6.6 + stylis: 4.3.6 + ts-dedent: 2.2.0 + uuid: 11.1.0 + micromark-util-character@2.1.1: dependencies: micromark-util-symbol: 2.0.1 @@ -1859,20 +2768,49 @@ snapshots: mitt@3.0.1: {} + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.3 + nanoid@3.3.11: {} node-releases@2.0.27: {} + non-layered-tidy-tree-layout@2.0.2: + optional: true + oniguruma-to-es@3.1.1: dependencies: emoji-regex-xs: 1.0.0 regex: 6.1.0 regex-recursion: 6.0.2 + package-manager-detector@1.6.0: {} + + path-data-parser@0.1.0: {} + + pathe@2.0.3: {} + perfect-debounce@1.0.0: {} picocolors@1.1.1: {} + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + postcss-value-parser@4.2.0: {} postcss@8.5.6: @@ -1897,6 +2835,8 @@ snapshots: rfdc@1.4.1: {} + robust-predicates@3.0.2: {} + rollup@4.56.0: dependencies: '@types/estree': 1.0.8 @@ -1928,6 +2868,17 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.56.0 fsevents: 2.3.3 + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + + rw@1.3.3: {} + + safer-buffer@2.1.2: {} + search-insights@2.17.3: {} shiki@2.5.0: @@ -1952,6 +2903,8 @@ snapshots: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 + stylis@4.3.6: {} + superjson@2.2.6: dependencies: copy-anything: 4.0.5 @@ -1962,8 +2915,14 @@ snapshots: tapable@2.3.0: {} + tinyexec@1.0.2: {} + trim-lines@3.0.1: {} + ts-dedent@2.2.0: {} + + ufo@1.6.3: {} + unist-util-is@6.0.1: dependencies: '@types/unist': 3.0.3 @@ -1993,6 +2952,8 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + uuid@11.1.0: {} + vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -2012,6 +2973,13 @@ snapshots: fsevents: 2.3.3 lightningcss: 1.30.2 + vitepress-plugin-mermaid@2.0.17(mermaid@11.12.2)(vitepress@1.6.4(@algolia/client-search@5.47.0)(lightningcss@1.30.2)(postcss@8.5.6)(search-insights@2.17.3)): + dependencies: + mermaid: 11.12.2 + vitepress: 1.6.4(@algolia/client-search@5.47.0)(lightningcss@1.30.2)(postcss@8.5.6)(search-insights@2.17.3) + optionalDependencies: + '@mermaid-js/mermaid-mindmap': 9.3.0 + vitepress-plugin-tabs@0.7.3(vitepress@1.6.4(@algolia/client-search@5.47.0)(lightningcss@1.30.2)(postcss@8.5.6)(search-insights@2.17.3))(vue@3.5.27): dependencies: vitepress: 1.6.4(@algolia/client-search@5.47.0)(lightningcss@1.30.2)(postcss@8.5.6)(search-insights@2.17.3) @@ -2066,6 +3034,23 @@ snapshots: - typescript - universal-cookie + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-uri@3.0.8: {} + vue@3.5.27: dependencies: '@vue/compiler-dom': 3.5.27 From 21c243a17d321b854fa7871bd140e7d761e5e102 Mon Sep 17 00:00:00 2001 From: danciaclara Date: Thu, 29 Jan 2026 20:42:19 +0530 Subject: [PATCH 2/2] fixed headings --- docs/.vitepress/config.mts | 6 +-- docs/dev-tools/agents/best-practices.md | 50 +++++++++---------- docs/dev-tools/agents/building-an-agent.md | 38 +++++++------- docs/dev-tools/agents/overview.md | 20 ++++---- .../agents/signals-content-payload.md | 22 ++++---- 5 files changed, 68 insertions(+), 68 deletions(-) diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 3acbcbe8..a79e6d04 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -596,9 +596,9 @@ export default withMermaid(defineConfig({ collapsed: false, items: [ { text: 'Overview', link: '/dev-tools/agents/overview' }, - { text: 'Building an Agent', link: '/dev-tools/agents/building-an-agent' }, - { text: 'Best Practices', link: '/dev-tools/agents/best-practices' }, - { text: 'Signals & Content Payload', link: '/dev-tools/agents/signals-content-payload' } + { text: 'Building an agent', link: '/dev-tools/agents/building-an-agent' }, + { text: 'Best practices', link: '/dev-tools/agents/best-practices' }, + { text: 'Signals and content payload', link: '/dev-tools/agents/signals-content-payload' } ] }, { text: 'Webhooks', link: '/dev-tools/intro-webhooks' }, diff --git a/docs/dev-tools/agents/best-practices.md b/docs/dev-tools/agents/best-practices.md index c070a9b2..53782976 100644 --- a/docs/dev-tools/agents/best-practices.md +++ b/docs/dev-tools/agents/best-practices.md @@ -1,9 +1,9 @@ --- -title: Best Practices +title: Best practices description: Guidelines for building responsive, user-friendly Plane agents that provide a seamless experience. --- -# Best Practices +# Best practices ::: info Plane Agents are currently in **Beta**. Please send any feedback to support@plane.so. @@ -13,11 +13,11 @@ Plane Agents are currently in **Beta**. Please send any feedback to support@plan Building a great agent experience requires thoughtful design around responsiveness, error handling, and user communication. This guide covers best practices to ensure your agent feels native to Plane and provides a seamless experience for users. -## Sending Immediate Thought Activity +## Sending immediate thought activity When your agent receives a webhook, users are waiting for a response. The most important best practice is to **acknowledge the request immediately**. -### Why Immediate Acknowledgment Matters +### Why immediate acknowledgment matters - Users see that your agent is active and processing their request - Prevents the Agent Run from being marked as `stale` (5-minute timeout) @@ -87,7 +87,7 @@ def handle_webhook(webhook: dict, credentials: dict): ``` ::: -### Thought Activity Best Practices +### Thought activity best practices - Keep thoughts concise but informative - Update thoughts as you progress through different stages @@ -103,11 +103,11 @@ def handle_webhook(webhook: dict, credentials: dict): - "Executing database query SELECT * FROM..." - Generic messages like "Working..." repeated multiple times -## Acknowledging Important Signals +## Acknowledging important signals Signals communicate user intent beyond the message content. Your agent **must** handle the `stop` signal appropriately. -### The Stop Signal +### The stop signal When a user wants to stop an agent run, Plane sends a `stop` signal with the activity. Your agent should: @@ -185,18 +185,18 @@ def handle_webhook(webhook: dict, credentials: dict): ``` ::: -### Signal Considerations +### Signal considerations | Signal | How to Handle | |--------|---------------| | `continue` | Default behavior, proceed with processing | | `stop` | Immediately halt and confirm | -## Progress Communication +## Progress communication For long-running tasks, keep users informed with progress updates. -### Multi-Step Operations +### Multi-step operations When your agent performs multiple steps, send thought activities for each: @@ -219,7 +219,7 @@ const summary = await generateSummary(searchResults); await createResponse(`Here's what I found: ${summary}`); ``` -### Avoiding Information Overload +### Avoiding information overload While progress updates are important, too many can be overwhelming: @@ -228,11 +228,11 @@ While progress updates are important, too many can be overwhelming: - **Don't** expose technical implementation details - **Do** explain what value is being created for the user -## Error Handling +## Error handling Graceful error handling is crucial for a good user experience. -### Always Catch and Report Errors +### Always catch and report errors ```typescript async function handleWebhook(webhook: AgentRunActivityWebhook, credentials: { bot_token: string; workspace_slug: string }) { @@ -282,7 +282,7 @@ function getUserFriendlyErrorMessage(error: Error): string { } ``` -### Error Message Guidelines +### Error message guidelines **Do:** - Use clear, non-technical language @@ -294,11 +294,11 @@ function getUserFriendlyErrorMessage(error: Error): string { - Blame the user for errors - Leave users without any feedback -## Handling Conversation Context +## Handling conversation context For multi-turn conversations, maintain context from previous activities. -### Fetching Previous Activities +### Fetching previous activities ```typescript // Get all activities for context @@ -319,18 +319,18 @@ const history = activities.results const response = await processWithContext(newPrompt, history); ``` -### Context Best Practices +### Context best practices - Retrieve relevant history, not every single activity - Filter to meaningful exchanges (prompts and responses) - Consider summarizing long histories to save tokens/processing - Don't assume infinite context availability -## Rate Limiting and Timeouts +## Rate limiting and timeouts Be mindful of Plane's API limits and your own processing time. -### Stale Run Prevention +### Stale run prevention Agent Runs are marked as `stale` after 5 minutes of inactivity. For long operations: @@ -351,7 +351,7 @@ async function longRunningTask(agentRunId: string) { } ``` -### Webhook Response Time +### Webhook response time - Return HTTP 200 from your webhook handler quickly (within seconds) - Process the actual agent logic asynchronously @@ -367,29 +367,29 @@ app.post("/webhook", async (req, res) => { }); ``` -## Summary Checklist +## Summary checklist **Responsiveness** - Send thought within seconds of webhook - Return webhook response quickly - Send heartbeats for long operations -**Signal Handling** +**Signal handling** - Always check for `stop` signal first - Handle all signal types appropriately - Confirm when stopping -**Error Handling** +**Error handling** - Wrap processing in try/catch - Always send error activity on failure - Use friendly error messages -**User Experience** +**User experience** - Progress updates for long tasks - Clear, non-technical communication - Maintain conversation context -## Next Steps +## Next steps - Learn about [Signals & Content Payload](/dev-tools/agents/signals-content-payload) for advanced activity handling - Review the [Building an Agent](/dev-tools/agents/building-an-agent) guide for implementation details diff --git a/docs/dev-tools/agents/building-an-agent.md b/docs/dev-tools/agents/building-an-agent.md index 8bdb3e5c..c1d3d059 100644 --- a/docs/dev-tools/agents/building-an-agent.md +++ b/docs/dev-tools/agents/building-an-agent.md @@ -3,7 +3,7 @@ title: Building an Agent description: Step-by-step guide to creating a Plane agent, including OAuth setup, webhook handling, and activity creation. --- -# Building an Agent +# Building an agent ::: info Plane Agents are currently in **Beta**. Please send any feedback to support@plane.so. @@ -13,17 +13,17 @@ Plane Agents are currently in **Beta**. Please send any feedback to support@plan Before building an agent, make sure you have completed the following: -1. **Build a Plane App** — Follow the [Build a Plane App](/dev-tools/build-plane-app) guide to understand OAuth flows, deployment, and webhook handling. +1. **Build a Plane app** — Follow the [Build a Plane App](/dev-tools/build-plane-app) guide to understand OAuth flows, deployment, and webhook handling. -2. **Get Your Bot Token** — Complete the [Bot Token Flow](/dev-tools/build-plane-app#bot-token-flow) to obtain a `bot_token` for your agent. This token is used for all API calls. +2. **Get your bot token** — Complete the [Bot Token Flow](/dev-tools/build-plane-app#bot-token-flow) to obtain a `bot_token` for your agent. This token is used for all API calls. -3. **Set Up Webhook Handling** — Ensure your server can [receive and verify webhooks](/dev-tools/build-plane-app#handling-webhooks) from Plane. +3. **Set up webhook handling** — Ensure your server can [receive and verify webhooks](/dev-tools/build-plane-app#handling-webhooks) from Plane. ::: info This guide assumes you have a working OAuth app with webhook handling. If not, complete the [Build a Plane App](/dev-tools/build-plane-app) guide first. ::: -## Creating an Agent +## Creating an agent Building a Plane agent involves three main steps: @@ -31,7 +31,7 @@ Building a Plane agent involves three main steps: 2. Implement the OAuth flow to install your agent in workspaces 3. Handle webhooks and create activities to respond to users -### OAuth App Creation +### OAuth app creation To create an agent, you first need to [register an OAuth application](/dev-tools/build-plane-app#create-an-oauth-application) with the **Enable App Mentions** checkbox enabled. @@ -48,7 +48,7 @@ To create an agent, you first need to [register an OAuth application](/dev-tools The "Enable App Mentions" checkbox is what transforms a regular OAuth app into an agent that can be @mentioned in work items. ::: -### Setting Is Mentionable +### Setting is mentionable When you enable app mentions during OAuth app creation, your application becomes mentionable in work item comments. This means: @@ -58,7 +58,7 @@ When you enable app mentions during OAuth app creation, your application becomes After installation, your agent appears alongside workspace members in the mention autocomplete. -## Agent Interaction +## Agent interaction Once your agent is installed via the [OAuth consent flow](/dev-tools/build-plane-app#bot-token-flow) and users start mentioning it, you need to handle the interactions through Agent Runs and Activities. @@ -66,7 +66,7 @@ Once your agent is installed via the [OAuth consent flow](/dev-tools/build-plane An **AgentRun** tracks a complete interaction session between a user and your agent. -#### Key Fields +#### Key fields | Field | Type | Description | |-------|------|-------------| @@ -91,7 +91,7 @@ An **AgentRun** tracks a complete interaction session between a user and your ag An **AgentRunActivity** represents a single message or action within an Agent Run. -#### Key Fields +#### Key fields | Field | Type | Description | |-------|------|-------------| @@ -106,7 +106,7 @@ An **AgentRunActivity** represents a single message or action within an Agent Ru | `actor` | UUID | The user or bot that created the activity | | `comment` | UUID | Associated comment (for non-ephemeral activities) | -### Creating Activities +### Creating activities Your agent communicates back to users by creating activities. We recommend using the official SDKs which provide typed helpers and insulate your code from API changes. @@ -126,7 +126,7 @@ pip install plane-sdk ``` ::: -#### Activity Examples +#### Activity examples :::tabs key:language == TypeScript {#typescript} @@ -269,7 +269,7 @@ plane_client.agent_runs.activities.create( ``` ::: -### Content Payload Types +### Content payload types The `content` field structure varies based on the activity type: @@ -358,7 +358,7 @@ Signals provide additional context about how an activity should be interpreted: See [Signals & Content Payload](/dev-tools/agents/signals-content-payload) for detailed information. -### Ephemeral Activities +### Ephemeral activities Activities with `ephemeral: true` are temporary and don't create comments. They're useful for showing agent progress without cluttering the conversation. @@ -369,11 +369,11 @@ The following activity types are automatically marked as ephemeral: Ephemeral activities are displayed temporarily in the UI and replaced when the next activity arrives. -## AgentRun Webhooks +## AgentRun webhooks Your agent receives webhooks when users interact with it. There are two main webhook events: -### AgentRun Create Webhook +### AgentRun create webhook Triggered when a new Agent Run is created (user first mentions your agent). @@ -404,7 +404,7 @@ Triggered when a new Agent Run is created (user first mentions your agent). } ``` -### AgentRun Activity Webhook +### AgentRun activity webhook Triggered when a user sends a prompt to your agent (initial mention or follow-up). @@ -446,7 +446,7 @@ Triggered when a user sends a prompt to your agent (initial mention or follow-up } ``` -### Handling Webhooks +### Handling webhooks Here's a complete example of handling agent webhooks using the SDKs: @@ -574,7 +574,7 @@ def handle_webhook(webhook: dict, credentials: dict): ``` ::: -## Next Steps +## Next steps - Learn about [Best Practices](/dev-tools/agents/best-practices) for building responsive agents - Explore [Signals & Content Payload](/dev-tools/agents/signals-content-payload) for advanced activity handling diff --git a/docs/dev-tools/agents/overview.md b/docs/dev-tools/agents/overview.md index f62a4865..d3ae9716 100644 --- a/docs/dev-tools/agents/overview.md +++ b/docs/dev-tools/agents/overview.md @@ -3,13 +3,13 @@ title: Agents Overview description: Learn how to build AI agents that integrate with Plane workspaces, enabling automated task handling, intelligent responses, and seamless collaboration. --- -# Agents Overview +# Agents overview ::: info Plane Agents are currently in **Beta**. Please send any feedback to support@plane.so. ::: -## What are Agents in Plane? +## What are agents in Plane? Agents in Plane are AI-powered applications that can interact with your workspace similar to how human users do. They can be @mentioned in work item comments, receive prompts from users, and respond with intelligent actions. Agents enable automation and AI assistance directly within your project management workflow. @@ -20,11 +20,11 @@ Key capabilities of Plane agents: - **Activity tracking** — All agent interactions are tracked through the Agent Run system - **Real-time responses** — Agents can send thoughts, actions, and responses back to users -## Agent Installation +## Agent installation Agents are installed as OAuth applications in your Plane workspace. When you create an OAuth app with the **Enable App Mentions** option enabled, it becomes an agent that users can mention and interact with. -### Installation Flow +### Installation flow 1. A workspace admin installs your agent via the OAuth consent flow 2. Plane creates a **bot user** for your agent in that workspace @@ -37,7 +37,7 @@ When installed, agents appear in the mention picker alongside regular workspace Agents installed in your workspace do not count as billable users. ::: -## Agent Run Lifecycle +## Agent Run lifecycle The Agent Run system tracks the complete lifecycle of an agent interaction, from when a user mentions the agent to when the agent completes its task. @@ -61,7 +61,7 @@ An **Agent Run Activity** is a single unit of communication within an Agent Run. - **Elicitation** — A question from the agent requesting user input - **Error** — An error message from the agent -### How Agent Run Works +### How Agent Run works The Agent Run flow consists of three main phases: @@ -100,7 +100,7 @@ sequenceDiagram - Work item and project context - Workspace information -#### Phase 3: Agent Response +#### Phase 3: Agent response 1. Your agent processes the webhook and starts working 2. The agent sends activities back to Plane via the API: @@ -110,7 +110,7 @@ sequenceDiagram 3. Plane updates the Agent Run status based on activities 4. Non-ephemeral activities (response, elicitation) create comment replies visible to users -### Agent Run States +### Agent Run states Agent Runs transition through various states based on activities: @@ -125,7 +125,7 @@ Agent Runs transition through various states based on activities: | `failed` | The run encountered an error and cannot continue | | `stale` | The run has not been updated in 5 minutes and is considered stale | -### Continuing a Conversation +### Continuing a conversation When a user replies to an agent's response: @@ -135,6 +135,6 @@ When a user replies to an agent's response: This enables multi-turn conversations where users and agents can have back-and-forth interactions. -## Next Steps +## Next steps Ready to build your own agent? Continue to [Building an Agent](/dev-tools/agents/building-an-agent) to learn how to create and deploy your first Plane agent. diff --git a/docs/dev-tools/agents/signals-content-payload.md b/docs/dev-tools/agents/signals-content-payload.md index ed80b127..5edacafd 100644 --- a/docs/dev-tools/agents/signals-content-payload.md +++ b/docs/dev-tools/agents/signals-content-payload.md @@ -3,7 +3,7 @@ title: Signals & Content Payload description: Detailed reference for activity signals and content payload structures in Plane agents. --- -# Signals & Content Payload +# Signals and content payload ::: info Plane Agents are currently in **Beta**. Please send any feedback to support@plane.so. @@ -22,7 +22,7 @@ Understanding these components is essential for building agents that communicate Signals are metadata that modify how an activity should be interpreted or handled. They provide additional context about the sender's intent—guiding how the activity should be processed or responded to. -### Available Signals +### Available signals | Signal | Description | Use Case | |--------|-------------|----------| @@ -137,7 +137,7 @@ await planeClient.agentRuns.activities.create(credentials.workspace_slug, agentR }); ``` -## Content Payload Types +## Content payload types The `content` field contains the actual message or action. Its structure varies based on the activity type. @@ -548,25 +548,25 @@ interface PromptContent { } ``` -## Ephemeral Activities +## Ephemeral activities Ephemeral activities are temporary and won't create comment replies. They're useful for showing agent progress without cluttering the conversation thread. -### Automatically Ephemeral Types +### Automatically ephemeral types The following activity types are automatically marked as ephemeral: - `thought` - `action` - `error` -### Ephemeral Behavior +### Ephemeral behavior - Ephemeral activities appear temporarily in the Agent UI - They're replaced when the next activity arrives - They don't create permanent comment replies - Useful for real-time progress updates -### Visual Example +### Visual example ``` User: @WeatherBot What's the weather in Tokyo? @@ -584,7 +584,7 @@ getWeather(35.6762, 139.6503) → 72°F, Clear The weather in Tokyo is currently 72°F with clear skies. ``` -## Content Metadata +## Content metadata Use `content_metadata` to store additional structured data about an activity: @@ -648,7 +648,7 @@ plane_client.agent_runs.activities.create( ``` ::: -## Signal Metadata +## Signal metadata Use `signal_metadata` to provide additional context for signals: @@ -686,7 +686,7 @@ Use `signal_metadata` to provide additional context for signals: } ``` -## Complete Activity Creation Reference +## Complete activity creation reference Here's a comprehensive example showing all activity types: @@ -821,7 +821,7 @@ plane_client.agent_runs.activities.create( ``` ::: -## Next Steps +## Next steps - Review [Best Practices](/dev-tools/agents/best-practices) for building responsive agents - See [Building an Agent](/dev-tools/agents/building-an-agent) for implementation examples