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:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,33 @@
{
"extends": [
"../../../../.eslintrc.base.json"
],
"ignorePatterns": [
"!**/*"
],
"overrides": [
{
"files": [
"*.ts",
"*.tsx",
"*.js",
"*.jsx"
],
"rules": {}
},
{
"files": [
"*.ts",
"*.tsx"
],
"rules": {}
},
{
"files": [
"*.js",
"*.jsx"
],
"rules": {}
}
]
}

View File

@@ -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.

View File

@@ -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"
}
}

View File

@@ -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}"
]
}
}
}

View File

@@ -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,
],
});

View File

@@ -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 }
);
},
});

View File

@@ -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
);
},
});

View File

@@ -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
);
},
});

View File

@@ -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
);
},
});

View File

@@ -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>;

View File

@@ -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;
}

View File

@@ -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.',
};
}
},
});

View File

@@ -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 });
},
});

View File

@@ -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];
},
});

View File

@@ -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 });
},
});

View File

@@ -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 });
},
});

View File

@@ -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,
});
},
});

View File

@@ -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"
}
]
}

View File

@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"]
}