Add Activepieces integration for workflow automation
- Add Activepieces fork with SmoothSchedule custom piece - Create integrations app with Activepieces service layer - Add embed token endpoint for iframe integration - Create Automations page with embedded workflow builder - Add sidebar visibility fix for embed mode - Add list inactive customers endpoint to Public API - Include SmoothSchedule triggers: event created/updated/cancelled - Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"extends": [
|
||||
"../../../../.eslintrc.base.json"
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"!**/*"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.tsx",
|
||||
"*.js",
|
||||
"*.jsx"
|
||||
],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.ts",
|
||||
"*.tsx"
|
||||
],
|
||||
"rules": {}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.js",
|
||||
"*.jsx"
|
||||
],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
# pieces-cursor
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Building
|
||||
|
||||
Run `nx build pieces-cursor` to build the library.
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@activepieces/piece-cursor",
|
||||
"version": "0.0.1",
|
||||
"type": "commonjs",
|
||||
"main": "./src/index.js",
|
||||
"types": "./src/index.d.ts",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "pieces-cursor",
|
||||
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "packages/pieces/community/cursor/src",
|
||||
"projectType": "library",
|
||||
"release": {
|
||||
"version": {
|
||||
"manifestRootsToUpdate": [
|
||||
"dist/{projectRoot}"
|
||||
],
|
||||
"currentVersionResolver": "git-tag",
|
||||
"fallbackCurrentVersionResolver": "disk"
|
||||
}
|
||||
},
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/js:tsc",
|
||||
"outputs": [
|
||||
"{options.outputPath}"
|
||||
],
|
||||
"options": {
|
||||
"outputPath": "dist/packages/pieces/community/cursor",
|
||||
"tsConfig": "packages/pieces/community/cursor/tsconfig.lib.json",
|
||||
"packageJson": "packages/pieces/community/cursor/package.json",
|
||||
"main": "packages/pieces/community/cursor/src/index.ts",
|
||||
"assets": [
|
||||
"packages/pieces/community/cursor/*.md",
|
||||
{
|
||||
"input": "packages/pieces/community/cursor/src/i18n",
|
||||
"output": "./src/i18n",
|
||||
"glob": "**/!(i18n.json)"
|
||||
}
|
||||
],
|
||||
"buildableProjectDepsInPackageJsonType": "dependencies",
|
||||
"updateBuildableProjectDepsInPackageJson": true
|
||||
},
|
||||
"dependsOn": [
|
||||
"prebuild",
|
||||
"^build"
|
||||
]
|
||||
},
|
||||
"nx-release-publish": {
|
||||
"options": {
|
||||
"packageRoot": "dist/{projectRoot}"
|
||||
}
|
||||
},
|
||||
"prebuild": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"cwd": "packages/pieces/community/cursor",
|
||||
"command": "bun install --no-save --silent"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
"outputs": [
|
||||
"{options.outputFile}"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { createPiece } from "@activepieces/pieces-framework";
|
||||
import { PieceCategory } from "@activepieces/shared";
|
||||
import { cursorAuth } from "./lib/common/auth";
|
||||
import { addFollowupInstruction } from "./lib/actions/add-followup-instruction";
|
||||
import { launchAgent } from "./lib/actions/launch-agent";
|
||||
import { findAgentStatus } from "./lib/actions/find-agent-status";
|
||||
import { deleteAgent } from "./lib/actions/delete-agent";
|
||||
import { newAgentTrigger } from "./lib/triggers/new-agent";
|
||||
import { agentStatusEqualsTrigger } from "./lib/triggers/agent-status-equals";
|
||||
import { agentPullRequestCreatedTrigger } from "./lib/triggers/agent-pull-request-created";
|
||||
import { newAgentConversationMessageTrigger } from "./lib/triggers/new-agent-conversation-message";
|
||||
import { agentStatusChangedWebhookTrigger } from "./lib/triggers/agent-status-changed-webhook";
|
||||
|
||||
export const cursor = createPiece({
|
||||
displayName: "Cursor",
|
||||
description: "AI-powered code editor with cloud agents that can work on your repositories. Launch agents, monitor their status, and automate code-related tasks.",
|
||||
auth: cursorAuth,
|
||||
minimumSupportedRelease: '0.36.1',
|
||||
logoUrl: "https://cdn.activepieces.com/pieces/cursor.png",
|
||||
categories: [PieceCategory.DEVELOPER_TOOLS],
|
||||
authors: ["onyedikachi-david"],
|
||||
actions: [
|
||||
launchAgent,
|
||||
addFollowupInstruction,
|
||||
findAgentStatus,
|
||||
deleteAgent,
|
||||
],
|
||||
triggers: [
|
||||
newAgentTrigger,
|
||||
agentStatusEqualsTrigger,
|
||||
agentPullRequestCreatedTrigger,
|
||||
newAgentConversationMessageTrigger,
|
||||
agentStatusChangedWebhookTrigger,
|
||||
],
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
import { createAction, Property, ApFile } from '@activepieces/pieces-framework';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { cursorAuth } from '../common/auth';
|
||||
import { makeCursorRequest } from '../common/client';
|
||||
import { agentDropdown } from '../common/props';
|
||||
|
||||
interface ImageItem {
|
||||
image: ApFile;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export const addFollowupInstruction = createAction({
|
||||
auth: cursorAuth,
|
||||
name: 'add_followup_instruction',
|
||||
displayName: 'Add Followup Instruction to Agent',
|
||||
description: 'Adds follow-up instructions to a running cloud agent',
|
||||
props: {
|
||||
agentId: agentDropdown,
|
||||
text: Property.LongText({
|
||||
displayName: 'Instruction Text',
|
||||
description: 'The follow-up instruction text for the agent',
|
||||
required: true,
|
||||
}),
|
||||
images: Property.Array({
|
||||
displayName: 'Images',
|
||||
description: 'Optional images to include with the instruction (max 5)',
|
||||
required: false,
|
||||
properties: {
|
||||
image: Property.File({
|
||||
displayName: 'Image',
|
||||
description: 'Image file',
|
||||
required: true,
|
||||
}),
|
||||
width: Property.Number({
|
||||
displayName: 'Width',
|
||||
description: 'Image width in pixels',
|
||||
required: true,
|
||||
}),
|
||||
height: Property.Number({
|
||||
displayName: 'Height',
|
||||
description: 'Image height in pixels',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { agentId, text, images } = context.propsValue;
|
||||
|
||||
const imageItems = (images as ImageItem[]) ?? [];
|
||||
|
||||
if (imageItems.length > 5) {
|
||||
throw new Error('Maximum 5 images allowed');
|
||||
}
|
||||
|
||||
const prompt: any = {
|
||||
text,
|
||||
};
|
||||
|
||||
if (imageItems.length > 0) {
|
||||
prompt.images = imageItems.map((item) => ({
|
||||
data: item.image.base64,
|
||||
dimension: {
|
||||
width: item.width,
|
||||
height: item.height,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
return await makeCursorRequest(
|
||||
context.auth,
|
||||
`/v0/agents/${agentId}/followup`,
|
||||
HttpMethod.POST,
|
||||
{ prompt }
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createAction } from '@activepieces/pieces-framework';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { cursorAuth } from '../common/auth';
|
||||
import { makeCursorRequest } from '../common/client';
|
||||
import { agentDropdown } from '../common/props';
|
||||
|
||||
export const deleteAgent = createAction({
|
||||
auth: cursorAuth,
|
||||
name: 'delete_agent',
|
||||
displayName: 'Delete Agent',
|
||||
description: 'Delete a cloud agent. This action is permanent and cannot be undone.',
|
||||
props: {
|
||||
agentId: agentDropdown,
|
||||
},
|
||||
async run(context) {
|
||||
const { agentId } = context.propsValue;
|
||||
|
||||
return await makeCursorRequest(
|
||||
context.auth,
|
||||
`/v0/agents/${agentId}`,
|
||||
HttpMethod.DELETE
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createAction } from '@activepieces/pieces-framework';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { cursorAuth } from '../common/auth';
|
||||
import { makeCursorRequest } from '../common/client';
|
||||
import { agentDropdown } from '../common/props';
|
||||
|
||||
export const findAgentStatus = createAction({
|
||||
auth: cursorAuth,
|
||||
name: 'find_agent_status',
|
||||
displayName: 'Find Agent Status',
|
||||
description: 'Retrieve the current status and results of a cloud agent',
|
||||
props: {
|
||||
agentId: agentDropdown,
|
||||
},
|
||||
async run(context) {
|
||||
const { agentId } = context.propsValue;
|
||||
|
||||
return await makeCursorRequest(
|
||||
context.auth,
|
||||
`/v0/agents/${agentId}`,
|
||||
HttpMethod.GET
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import { createAction, Property, ApFile } from '@activepieces/pieces-framework';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { cursorAuth } from '../common/auth';
|
||||
import { makeCursorRequest } from '../common/client';
|
||||
|
||||
interface ImageItem {
|
||||
image: ApFile;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export const launchAgent = createAction({
|
||||
auth: cursorAuth,
|
||||
name: 'launch_agent',
|
||||
displayName: 'Launch Agent',
|
||||
description: 'Start a new cloud agent to work on your repository',
|
||||
props: {
|
||||
promptText: Property.LongText({
|
||||
displayName: 'Task Prompt',
|
||||
description: 'The instruction text for the agent',
|
||||
required: true,
|
||||
}),
|
||||
images: Property.Array({
|
||||
displayName: 'Images',
|
||||
description: 'Optional images to include with the prompt (max 5)',
|
||||
required: false,
|
||||
properties: {
|
||||
image: Property.File({
|
||||
displayName: 'Image',
|
||||
description: 'Image file',
|
||||
required: true,
|
||||
}),
|
||||
width: Property.Number({
|
||||
displayName: 'Width',
|
||||
description: 'Image width in pixels',
|
||||
required: true,
|
||||
}),
|
||||
height: Property.Number({
|
||||
displayName: 'Height',
|
||||
description: 'Image height in pixels',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
}),
|
||||
model: Property.ShortText({
|
||||
displayName: 'Model',
|
||||
description: 'The LLM to use (e.g., claude-4-sonnet). Leave empty to use the default model.',
|
||||
required: false,
|
||||
}),
|
||||
repository: Property.ShortText({
|
||||
displayName: 'Repository URL',
|
||||
description: 'GitHub repository URL (e.g., https://github.com/your-org/your-repo)',
|
||||
required: true,
|
||||
}),
|
||||
ref: Property.ShortText({
|
||||
displayName: 'Base Branch/Ref',
|
||||
description: 'Git ref (branch name, tag, or commit hash) to use as the base branch',
|
||||
required: false,
|
||||
}),
|
||||
autoCreatePr: Property.Checkbox({
|
||||
displayName: 'Auto Create Pull Request',
|
||||
description: 'Automatically create a pull request when the agent completes',
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
}),
|
||||
openAsCursorGithubApp: Property.Checkbox({
|
||||
displayName: 'Open as Cursor GitHub App',
|
||||
description: 'Open the pull request as the Cursor GitHub App instead of as the user. Only applies if auto-create PR is enabled.',
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
}),
|
||||
skipReviewerRequest: Property.Checkbox({
|
||||
displayName: 'Skip Reviewer Request',
|
||||
description: 'Skip adding the user as a reviewer to the pull request. Only applies if auto-create PR is enabled and opened as Cursor GitHub App.',
|
||||
required: false,
|
||||
defaultValue: false,
|
||||
}),
|
||||
branchName: Property.ShortText({
|
||||
displayName: 'Custom Branch Name',
|
||||
description: 'Custom branch name for the agent to create',
|
||||
required: false,
|
||||
}),
|
||||
webhookUrl: Property.ShortText({
|
||||
displayName: 'Webhook URL',
|
||||
description: 'URL to receive webhook notifications about agent status changes',
|
||||
required: false,
|
||||
}),
|
||||
webhookSecret: Property.ShortText({
|
||||
displayName: 'Webhook Secret',
|
||||
description: 'Secret key for webhook payload verification (minimum 32 characters)',
|
||||
required: false,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const {
|
||||
promptText,
|
||||
images,
|
||||
model,
|
||||
repository,
|
||||
ref,
|
||||
autoCreatePr,
|
||||
openAsCursorGithubApp,
|
||||
skipReviewerRequest,
|
||||
branchName,
|
||||
webhookUrl,
|
||||
webhookSecret,
|
||||
} = context.propsValue;
|
||||
|
||||
const imageItems = (images as ImageItem[]) ?? [];
|
||||
|
||||
if (imageItems.length > 5) {
|
||||
throw new Error('Maximum 5 images allowed');
|
||||
}
|
||||
|
||||
const prompt: any = {
|
||||
text: promptText,
|
||||
};
|
||||
|
||||
if (imageItems.length > 0) {
|
||||
prompt.images = imageItems.map((item) => ({
|
||||
data: item.image.base64,
|
||||
dimension: {
|
||||
width: item.width,
|
||||
height: item.height,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
const source: any = {
|
||||
repository,
|
||||
};
|
||||
|
||||
if (ref) {
|
||||
source.ref = ref;
|
||||
}
|
||||
|
||||
const body: any = {
|
||||
prompt,
|
||||
source,
|
||||
};
|
||||
|
||||
if (model) {
|
||||
body.model = model;
|
||||
}
|
||||
|
||||
const target: any = {};
|
||||
let hasTarget = false;
|
||||
|
||||
if (autoCreatePr !== undefined) {
|
||||
target.autoCreatePr = autoCreatePr;
|
||||
hasTarget = true;
|
||||
}
|
||||
|
||||
if (openAsCursorGithubApp !== undefined) {
|
||||
target.openAsCursorGithubApp = openAsCursorGithubApp;
|
||||
hasTarget = true;
|
||||
}
|
||||
|
||||
if (skipReviewerRequest !== undefined) {
|
||||
target.skipReviewerRequest = skipReviewerRequest;
|
||||
hasTarget = true;
|
||||
}
|
||||
|
||||
if (branchName) {
|
||||
target.branchName = branchName;
|
||||
hasTarget = true;
|
||||
}
|
||||
|
||||
if (hasTarget) {
|
||||
body.target = target;
|
||||
}
|
||||
|
||||
if (webhookUrl) {
|
||||
const webhook: any = {
|
||||
url: webhookUrl,
|
||||
};
|
||||
|
||||
if (webhookSecret) {
|
||||
if (webhookSecret.length < 32) {
|
||||
throw new Error('Webhook secret must be at least 32 characters long');
|
||||
}
|
||||
webhook.secret = webhookSecret;
|
||||
}
|
||||
|
||||
body.webhook = webhook;
|
||||
}
|
||||
|
||||
return await makeCursorRequest(
|
||||
context.auth,
|
||||
'/v0/agents',
|
||||
HttpMethod.POST,
|
||||
body
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { AppConnectionValueForAuthProperty, PieceAuth } from '@activepieces/pieces-framework';
|
||||
|
||||
export const cursorAuth = PieceAuth.SecretText({
|
||||
displayName: 'API Key',
|
||||
description: `
|
||||
User API Keys provide secure, programmatic access to your Cursor account, including the Cloud Agent API.
|
||||
|
||||
To get your API Key:
|
||||
1. Go to cursor.com/dashboard?tab=cloud-agents
|
||||
2. Scroll to the "User API Keys" section
|
||||
3. Click "+ New API Key"
|
||||
4. Copy the generated key (format: key_...)
|
||||
|
||||
Treat your API key like a password: keep it secure and never share it publicly.
|
||||
Note: The Cloud Agent API is in beta.
|
||||
`,
|
||||
required: true,
|
||||
});
|
||||
|
||||
export type CursorAuth = AppConnectionValueForAuthProperty<typeof cursorAuth>;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { httpClient, HttpMethod, AuthenticationType } from '@activepieces/pieces-common';
|
||||
import { CursorAuth } from './auth';
|
||||
|
||||
const BASE_URL = 'https://api.cursor.com';
|
||||
|
||||
export async function makeCursorRequest<T = any>(
|
||||
auth: CursorAuth,
|
||||
endpoint: string,
|
||||
method: HttpMethod = HttpMethod.GET,
|
||||
body?: unknown,
|
||||
queryParams?: Record<string, any>
|
||||
): Promise<T> {
|
||||
const response = await httpClient.sendRequest({
|
||||
method,
|
||||
url: `${BASE_URL}${endpoint}`,
|
||||
authentication: {
|
||||
type: AuthenticationType.BASIC,
|
||||
username: auth.secret_text,
|
||||
password: '',
|
||||
},
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body,
|
||||
queryParams,
|
||||
});
|
||||
|
||||
return response.body as T;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Property } from '@activepieces/pieces-framework';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { cursorAuth, CursorAuth } from './auth';
|
||||
import { makeCursorRequest } from './client';
|
||||
|
||||
interface Agent {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
source?: {
|
||||
repository?: string;
|
||||
ref?: string;
|
||||
};
|
||||
target?: {
|
||||
branchName?: string;
|
||||
url?: string;
|
||||
prUrl?: string;
|
||||
};
|
||||
summary?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface ListAgentsResponse {
|
||||
agents: Agent[];
|
||||
nextCursor?: string;
|
||||
}
|
||||
|
||||
export const agentDropdown = Property.Dropdown({
|
||||
auth: cursorAuth,
|
||||
displayName: 'Agent',
|
||||
description: 'Select a cloud agent',
|
||||
required: true,
|
||||
refreshers: [],
|
||||
options: async ({ auth }) => {
|
||||
if (!auth) {
|
||||
return {
|
||||
disabled: true,
|
||||
placeholder: 'Connect your Cursor account first',
|
||||
options: [],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await makeCursorRequest<ListAgentsResponse>(
|
||||
auth as CursorAuth,
|
||||
'/v0/agents',
|
||||
HttpMethod.GET,
|
||||
undefined,
|
||||
{ limit: 100 }
|
||||
);
|
||||
|
||||
if (!response.agents || response.agents.length === 0) {
|
||||
return {
|
||||
options: [],
|
||||
placeholder: 'No agents found',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
options: response.agents.map((agent) => ({
|
||||
label: `${agent.name} (${agent.status})`,
|
||||
value: agent.id,
|
||||
})),
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
disabled: true,
|
||||
options: [],
|
||||
placeholder: 'Failed to load agents. Check your API key.',
|
||||
};
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
import { createTrigger, TriggerStrategy, PiecePropValueSchema, AppConnectionValueForAuthProperty } from '@activepieces/pieces-framework';
|
||||
import { DedupeStrategy, Polling, pollingHelper, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { cursorAuth } from '../common/auth';
|
||||
import { makeCursorRequest } from '../common/client';
|
||||
|
||||
interface Agent {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
source?: {
|
||||
repository?: string;
|
||||
ref?: string;
|
||||
};
|
||||
target?: {
|
||||
branchName?: string;
|
||||
url?: string;
|
||||
prUrl?: string;
|
||||
autoCreatePr?: boolean;
|
||||
openAsCursorGithubApp?: boolean;
|
||||
skipReviewerRequest?: boolean;
|
||||
};
|
||||
summary?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface ListAgentsResponse {
|
||||
agents: Agent[];
|
||||
nextCursor?: string;
|
||||
}
|
||||
|
||||
const polling: Polling<AppConnectionValueForAuthProperty<typeof cursorAuth>, Record<string, never>> = {
|
||||
strategy: DedupeStrategy.TIMEBASED,
|
||||
items: async ({ auth, lastFetchEpochMS, store }) => {
|
||||
const isTest = lastFetchEpochMS === 0;
|
||||
const limit = isTest ? 5 : 100;
|
||||
|
||||
try {
|
||||
const response = await makeCursorRequest<ListAgentsResponse>(
|
||||
auth,
|
||||
'/v0/agents',
|
||||
HttpMethod.GET,
|
||||
undefined,
|
||||
{ limit }
|
||||
);
|
||||
|
||||
if (!response.agents || response.agents.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const storedAgentsWithPRs = await store.get<string>('agents_with_prs');
|
||||
const agentsWithPRsSet = new Set<string>(
|
||||
storedAgentsWithPRs ? JSON.parse(storedAgentsWithPRs) : []
|
||||
);
|
||||
|
||||
const newPRs: Array<{ epochMilliSeconds: number; data: Agent }> = [];
|
||||
const updatedAgentsWithPRs = new Set<string>(agentsWithPRsSet);
|
||||
|
||||
for (const agent of response.agents) {
|
||||
if (agent.target?.prUrl) {
|
||||
if (!agentsWithPRsSet.has(agent.id)) {
|
||||
const epochMilliSeconds = Date.parse(agent.createdAt);
|
||||
newPRs.push({
|
||||
epochMilliSeconds,
|
||||
data: agent,
|
||||
});
|
||||
}
|
||||
updatedAgentsWithPRs.add(agent.id);
|
||||
} else {
|
||||
updatedAgentsWithPRs.delete(agent.id);
|
||||
}
|
||||
}
|
||||
|
||||
await store.put('agents_with_prs', JSON.stringify(Array.from(updatedAgentsWithPRs)));
|
||||
|
||||
if (isTest) {
|
||||
return newPRs;
|
||||
}
|
||||
|
||||
return newPRs.filter((item) => item.epochMilliSeconds > lastFetchEpochMS);
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const agentPullRequestCreatedTrigger = createTrigger({
|
||||
auth: cursorAuth,
|
||||
name: 'agent_pull_request_created',
|
||||
displayName: 'Agent Pull Request Created',
|
||||
description: 'Triggers when a background agent creates a pull request',
|
||||
type: TriggerStrategy.POLLING,
|
||||
props: {},
|
||||
sampleData: {
|
||||
id: 'bc_abc123',
|
||||
name: 'Add README Documentation',
|
||||
status: 'FINISHED',
|
||||
source: {
|
||||
repository: 'https://github.com/your-org/your-repo',
|
||||
ref: 'main',
|
||||
},
|
||||
target: {
|
||||
branchName: 'cursor/add-readme-1234',
|
||||
url: 'https://cursor.com/agents?id=bc_abc123',
|
||||
prUrl: 'https://github.com/your-org/your-repo/pull/1234',
|
||||
autoCreatePr: true,
|
||||
openAsCursorGithubApp: false,
|
||||
skipReviewerRequest: false,
|
||||
},
|
||||
summary: 'Added README.md with installation instructions and usage examples',
|
||||
createdAt: '2024-01-15T10:30:00Z',
|
||||
},
|
||||
async onEnable(context) {
|
||||
const { store, auth, propsValue } = context;
|
||||
await store.put('agents_with_prs', JSON.stringify([]));
|
||||
await pollingHelper.onEnable(polling, { store, auth, propsValue });
|
||||
},
|
||||
async onDisable(context) {
|
||||
const { store, auth, propsValue } = context;
|
||||
await store.delete('agents_with_prs');
|
||||
await pollingHelper.onDisable(polling, { store, auth, propsValue });
|
||||
},
|
||||
async test(context) {
|
||||
const { store, auth, propsValue, files } = context;
|
||||
return await pollingHelper.test(polling, { store, auth, propsValue, files });
|
||||
},
|
||||
async run(context) {
|
||||
const { store, auth, propsValue, files } = context;
|
||||
return await pollingHelper.poll(polling, { store, auth, propsValue, files });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
import { createTrigger, TriggerStrategy, Property } from '@activepieces/pieces-framework';
|
||||
import { cursorAuth } from '../common/auth';
|
||||
import crypto from 'crypto';
|
||||
|
||||
interface WebhookPayload {
|
||||
event: string;
|
||||
timestamp: string;
|
||||
id: string;
|
||||
status: string;
|
||||
source?: {
|
||||
repository?: string;
|
||||
ref?: string;
|
||||
};
|
||||
target?: {
|
||||
url?: string;
|
||||
branchName?: string;
|
||||
prUrl?: string;
|
||||
};
|
||||
summary?: string;
|
||||
}
|
||||
|
||||
function verifyWebhookSignature(
|
||||
secret: string,
|
||||
rawBody: string | Buffer,
|
||||
signatureHeader: string | undefined
|
||||
): boolean {
|
||||
if (!signatureHeader || !secret) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const signature = signatureHeader.replace('sha256=', '');
|
||||
|
||||
const expectedSignature = crypto
|
||||
.createHmac('sha256', secret)
|
||||
.update(rawBody)
|
||||
.digest('hex');
|
||||
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(signature, 'hex'),
|
||||
Buffer.from(expectedSignature, 'hex')
|
||||
);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const agentStatusChangedWebhookTrigger = createTrigger({
|
||||
auth: cursorAuth,
|
||||
name: 'agent_status_changed_webhook',
|
||||
displayName: 'Agent Status Changed (Webhook)',
|
||||
description: 'Triggers when an agent status changes (ERROR or FINISHED). Configure this webhook URL when launching an agent.',
|
||||
type: TriggerStrategy.WEBHOOK,
|
||||
props: {
|
||||
webhookSecret: Property.ShortText({
|
||||
displayName: 'Webhook Secret',
|
||||
description: 'Optional webhook secret for signature verification. Must match the secret used when launching the agent.',
|
||||
required: false,
|
||||
}),
|
||||
},
|
||||
sampleData: {
|
||||
event: 'statusChange',
|
||||
timestamp: '2024-01-15T10:30:00Z',
|
||||
id: 'bc_abc123',
|
||||
status: 'FINISHED',
|
||||
source: {
|
||||
repository: 'https://github.com/your-org/your-repo',
|
||||
ref: 'main',
|
||||
},
|
||||
target: {
|
||||
url: 'https://cursor.com/agents?id=bc_abc123',
|
||||
branchName: 'cursor/add-readme-1234',
|
||||
prUrl: 'https://github.com/your-org/your-repo/pull/1234',
|
||||
},
|
||||
summary: 'Added README.md with installation instructions',
|
||||
},
|
||||
async onEnable(context) {
|
||||
if (context.propsValue.webhookSecret) {
|
||||
await context.store.put('webhook_secret', context.propsValue.webhookSecret);
|
||||
}
|
||||
},
|
||||
async onDisable(context) {
|
||||
await context.store.delete('webhook_secret');
|
||||
},
|
||||
async run(context) {
|
||||
const headers = context.payload.headers || {};
|
||||
const rawBody = context.payload.rawBody;
|
||||
const signatureHeader = headers['x-webhook-signature'] || headers['X-Webhook-Signature'];
|
||||
const eventHeader = headers['x-webhook-event'] || headers['X-Webhook-Event'];
|
||||
|
||||
const webhookSecret = context.propsValue.webhookSecret ||
|
||||
(await context.store.get<string>('webhook_secret'));
|
||||
|
||||
if (webhookSecret) {
|
||||
if (!signatureHeader) {
|
||||
throw new Error('Webhook signature header is missing. This request may not be from Cursor.');
|
||||
}
|
||||
|
||||
if (!verifyWebhookSignature(webhookSecret, rawBody as string, signatureHeader)) {
|
||||
throw new Error('Webhook signature verification failed. This request may not be from Cursor.');
|
||||
}
|
||||
}
|
||||
|
||||
const payload = context.payload.body as WebhookPayload;
|
||||
|
||||
if (eventHeader !== 'statusChange' && payload.event !== 'statusChange') {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (payload.status !== 'ERROR' && payload.status !== 'FINISHED') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [payload];
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import { createTrigger, TriggerStrategy, PiecePropValueSchema, Property, AppConnectionValueForAuthProperty } from '@activepieces/pieces-framework';
|
||||
import { DedupeStrategy, Polling, pollingHelper, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { cursorAuth } from '../common/auth';
|
||||
import { makeCursorRequest } from '../common/client';
|
||||
import { agentDropdown } from '../common/props';
|
||||
|
||||
interface Agent {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
source?: {
|
||||
repository?: string;
|
||||
ref?: string;
|
||||
};
|
||||
target?: {
|
||||
branchName?: string;
|
||||
url?: string;
|
||||
prUrl?: string;
|
||||
autoCreatePr?: boolean;
|
||||
openAsCursorGithubApp?: boolean;
|
||||
skipReviewerRequest?: boolean;
|
||||
};
|
||||
summary?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ label: 'CREATING', value: 'CREATING' },
|
||||
{ label: 'RUNNING', value: 'RUNNING' },
|
||||
{ label: 'FINISHED', value: 'FINISHED' },
|
||||
{ label: 'FAILED', value: 'FAILED' },
|
||||
];
|
||||
|
||||
const polling: Polling< AppConnectionValueForAuthProperty<typeof cursorAuth>,
|
||||
{ agentId: string; status: string }
|
||||
> = {
|
||||
strategy: DedupeStrategy.TIMEBASED,
|
||||
items: async ({ auth, propsValue, store }) => {
|
||||
const { agentId, status: targetStatus } = propsValue;
|
||||
const storeKey = `agent_status_${agentId}`;
|
||||
|
||||
try {
|
||||
const agent = await makeCursorRequest<Agent>(
|
||||
auth,
|
||||
`/v0/agents/${agentId}`,
|
||||
HttpMethod.GET
|
||||
);
|
||||
|
||||
const lastStatus = await store.get<string>(storeKey);
|
||||
|
||||
if (agent.status === targetStatus && agent.status !== lastStatus) {
|
||||
await store.put(storeKey, agent.status);
|
||||
|
||||
return [
|
||||
{
|
||||
epochMilliSeconds: Date.parse(agent.createdAt),
|
||||
data: agent,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (agent.status !== lastStatus) {
|
||||
await store.put(storeKey, agent.status);
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const agentStatusEqualsTrigger = createTrigger({
|
||||
auth: cursorAuth,
|
||||
name: 'agent_status_equals',
|
||||
displayName: 'Agent Status Equals',
|
||||
description: 'Triggers when a Cursor agent has the specified status (e.g., "FINISHED", "FAILED")',
|
||||
type: TriggerStrategy.POLLING,
|
||||
props: {
|
||||
agentId: agentDropdown,
|
||||
status: Property.StaticDropdown({
|
||||
displayName: 'Status',
|
||||
description: 'The status to watch for',
|
||||
required: true,
|
||||
options: {
|
||||
options: STATUS_OPTIONS,
|
||||
},
|
||||
}),
|
||||
},
|
||||
sampleData: {
|
||||
id: 'bc_abc123',
|
||||
name: 'Add README Documentation',
|
||||
status: 'FINISHED',
|
||||
source: {
|
||||
repository: 'https://github.com/your-org/your-repo',
|
||||
ref: 'main',
|
||||
},
|
||||
target: {
|
||||
branchName: 'cursor/add-readme-1234',
|
||||
url: 'https://cursor.com/agents?id=bc_abc123',
|
||||
prUrl: 'https://github.com/your-org/your-repo/pull/1234',
|
||||
autoCreatePr: false,
|
||||
openAsCursorGithubApp: false,
|
||||
skipReviewerRequest: false,
|
||||
},
|
||||
summary: 'Added README.md with installation instructions and usage examples',
|
||||
createdAt: '2024-01-15T10:30:00Z',
|
||||
},
|
||||
async onEnable(context) {
|
||||
const { store, auth, propsValue } = context;
|
||||
const { agentId } = propsValue;
|
||||
const storeKey = `agent_status_${agentId}`;
|
||||
|
||||
try {
|
||||
const agent = await makeCursorRequest<Agent>(
|
||||
auth,
|
||||
`/v0/agents/${agentId}`,
|
||||
HttpMethod.GET
|
||||
);
|
||||
await store.put(storeKey, agent.status);
|
||||
} catch (error) {
|
||||
await store.put(storeKey, null);
|
||||
}
|
||||
|
||||
await pollingHelper.onEnable(polling, { store, auth, propsValue });
|
||||
},
|
||||
async onDisable(context) {
|
||||
const { store, auth, propsValue } = context;
|
||||
const { agentId } = propsValue;
|
||||
const storeKey = `agent_status_${agentId}`;
|
||||
|
||||
await store.delete(storeKey);
|
||||
await pollingHelper.onDisable(polling, { store, auth, propsValue });
|
||||
},
|
||||
async test(context) {
|
||||
const { store, auth, propsValue, files } = context;
|
||||
return await pollingHelper.test(polling, { store, auth, propsValue, files });
|
||||
},
|
||||
async run(context) {
|
||||
const { store, auth, propsValue, files } = context;
|
||||
return await pollingHelper.poll(polling, { store, auth, propsValue, files });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { createTrigger, TriggerStrategy, PiecePropValueSchema, AppConnectionValueForAuthProperty } from '@activepieces/pieces-framework';
|
||||
import { DedupeStrategy, Polling, pollingHelper, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { cursorAuth } from '../common/auth';
|
||||
import { makeCursorRequest } from '../common/client';
|
||||
import { agentDropdown } from '../common/props';
|
||||
|
||||
interface ConversationMessage {
|
||||
id: string;
|
||||
type: 'user_message' | 'assistant_message';
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface AgentConversationResponse {
|
||||
messages: ConversationMessage[];
|
||||
}
|
||||
|
||||
const polling: Polling<
|
||||
AppConnectionValueForAuthProperty<typeof cursorAuth>,
|
||||
{ agentId: string }
|
||||
> = {
|
||||
strategy: DedupeStrategy.LAST_ITEM,
|
||||
items: async ({ auth, propsValue, lastItemId }) => {
|
||||
const { agentId } = propsValue;
|
||||
|
||||
try {
|
||||
const response = await makeCursorRequest<AgentConversationResponse>(
|
||||
auth,
|
||||
`/v0/agents/${agentId}/conversation`,
|
||||
HttpMethod.GET
|
||||
);
|
||||
|
||||
if (!response.messages || response.messages.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const messages = [...response.messages].reverse();
|
||||
|
||||
return messages.map((message) => ({
|
||||
id: message.id,
|
||||
data: message,
|
||||
}));
|
||||
} catch (error) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const newAgentConversationMessageTrigger = createTrigger({
|
||||
auth: cursorAuth,
|
||||
name: 'new_agent_conversation_message',
|
||||
displayName: 'New Agent Conversation Message',
|
||||
description: 'Triggers when a new message appears in a specific agent\'s conversation',
|
||||
type: TriggerStrategy.POLLING,
|
||||
props: {
|
||||
agentId: agentDropdown,
|
||||
},
|
||||
sampleData: {
|
||||
id: 'msg_001',
|
||||
type: 'user_message',
|
||||
text: 'Add a README.md file with installation instructions',
|
||||
},
|
||||
async onEnable(context) {
|
||||
const { store, auth, propsValue } = context;
|
||||
await pollingHelper.onEnable(polling, { store, auth, propsValue });
|
||||
},
|
||||
async onDisable(context) {
|
||||
const { store, auth, propsValue } = context;
|
||||
await pollingHelper.onDisable(polling, { store, auth, propsValue });
|
||||
},
|
||||
async test(context) {
|
||||
const { store, auth, propsValue, files } = context;
|
||||
return await pollingHelper.test(polling, { store, auth, propsValue, files });
|
||||
},
|
||||
async run(context) {
|
||||
const { store, auth, propsValue, files } = context;
|
||||
return await pollingHelper.poll(polling, { store, auth, propsValue, files });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { createTrigger, TriggerStrategy, PiecePropValueSchema, AppConnectionValueForAuthProperty } from '@activepieces/pieces-framework';
|
||||
import { DedupeStrategy, Polling, pollingHelper, HttpMethod } from '@activepieces/pieces-common';
|
||||
import { cursorAuth } from '../common/auth';
|
||||
import { makeCursorRequest } from '../common/client';
|
||||
|
||||
interface Agent {
|
||||
id: string;
|
||||
name: string;
|
||||
status: string;
|
||||
source?: {
|
||||
repository?: string;
|
||||
ref?: string;
|
||||
};
|
||||
target?: {
|
||||
branchName?: string;
|
||||
url?: string;
|
||||
prUrl?: string;
|
||||
autoCreatePr?: boolean;
|
||||
openAsCursorGithubApp?: boolean;
|
||||
skipReviewerRequest?: boolean;
|
||||
};
|
||||
summary?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface ListAgentsResponse {
|
||||
agents: Agent[];
|
||||
nextCursor?: string;
|
||||
}
|
||||
|
||||
const polling: Polling<AppConnectionValueForAuthProperty<typeof cursorAuth>, Record<string, never>> = {
|
||||
strategy: DedupeStrategy.TIMEBASED,
|
||||
items: async ({ auth, lastFetchEpochMS }) => {
|
||||
const isTest = lastFetchEpochMS === 0;
|
||||
|
||||
const limit = isTest ? 5 : 100;
|
||||
|
||||
const response = await makeCursorRequest<ListAgentsResponse>(
|
||||
auth,
|
||||
'/v0/agents',
|
||||
HttpMethod.GET,
|
||||
undefined,
|
||||
{ limit }
|
||||
);
|
||||
|
||||
if (!response.agents || response.agents.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return response.agents
|
||||
.map((agent) => {
|
||||
const epochMilliSeconds = Date.parse(agent.createdAt);
|
||||
|
||||
return {
|
||||
epochMilliSeconds,
|
||||
data: agent,
|
||||
};
|
||||
})
|
||||
.filter((item) => {
|
||||
if (isTest) {
|
||||
return true;
|
||||
}
|
||||
return item.epochMilliSeconds > lastFetchEpochMS;
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const newAgentTrigger = createTrigger({
|
||||
auth: cursorAuth,
|
||||
name: 'new_agent',
|
||||
displayName: 'New Agent',
|
||||
description: 'Triggers when a new Cursor agent is created or when agent status changes',
|
||||
type: TriggerStrategy.POLLING,
|
||||
props: {},
|
||||
sampleData: {
|
||||
id: 'bc_abc123',
|
||||
name: 'Add README Documentation',
|
||||
status: 'FINISHED',
|
||||
source: {
|
||||
repository: 'https://github.com/your-org/your-repo',
|
||||
ref: 'main',
|
||||
},
|
||||
target: {
|
||||
branchName: 'cursor/add-readme-1234',
|
||||
url: 'https://cursor.com/agents?id=bc_abc123',
|
||||
prUrl: 'https://github.com/your-org/your-repo/pull/1234',
|
||||
autoCreatePr: false,
|
||||
openAsCursorGithubApp: false,
|
||||
skipReviewerRequest: false,
|
||||
},
|
||||
summary: 'Added README.md with installation instructions and usage examples',
|
||||
createdAt: '2024-01-15T10:30:00Z',
|
||||
},
|
||||
onEnable: async (context) => {
|
||||
await pollingHelper.onEnable(polling, {
|
||||
auth: context.auth,
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
});
|
||||
},
|
||||
onDisable: async (context) => {
|
||||
await pollingHelper.onDisable(polling, {
|
||||
auth: context.auth,
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
});
|
||||
},
|
||||
test: async (context) => {
|
||||
return await pollingHelper.test(polling, {
|
||||
auth: context.auth,
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
files: context.files,
|
||||
});
|
||||
},
|
||||
run: async (context) => {
|
||||
return await pollingHelper.poll(polling, {
|
||||
auth: context.auth,
|
||||
store: context.store,
|
||||
propsValue: context.propsValue,
|
||||
files: context.files,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"importHelpers": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noPropertyAccessFromIndexSignature": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user