Skip to content

Commit adfcb67

Browse files
authored
feat(cursor): add list artifacts and download artifact tools (#3970)
* feat(cursor): add list artifacts and download artifact tools * fix(cursor): resolve build errors in cursor block and download artifact types - Remove invalid wandConfig with unsupported generationType 'json-array' from promptImages subBlock - Remove invalid 'optional' property from summary output definition - Split DownloadArtifactResponse into v1 (content/metadata) and v2 (file) response types * fix(cursor): address PR review feedback - Remove redundant Array.isArray guards in list_artifacts.ts - Pass through actual HTTP status on presigned URL download failure instead of hardcoded 400
1 parent f9a7c45 commit adfcb67

File tree

15 files changed

+540
-9
lines changed

15 files changed

+540
-9
lines changed

apps/docs/content/docs/en/tools/cursor.mdx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ List all cloud agents for the authenticated user with optional pagination. Retur
4545
| `apiKey` | string | Yes | Cursor API key |
4646
| `limit` | number | No | Number of agents to return \(default: 20, max: 100\) |
4747
| `cursor` | string | No | Pagination cursor from previous response |
48+
| `prUrl` | string | No | Filter agents by pull request URL |
4849

4950
#### Output
5051

@@ -173,4 +174,41 @@ Permanently delete a cloud agent. Returns API-aligned fields only.
173174
| --------- | ---- | ----------- |
174175
| `id` | string | Agent ID |
175176

177+
### `cursor_list_artifacts`
178+
179+
List generated artifact files for a cloud agent. Returns API-aligned fields only.
180+
181+
#### Input
182+
183+
| Parameter | Type | Required | Description |
184+
| --------- | ---- | -------- | ----------- |
185+
| `apiKey` | string | Yes | Cursor API key |
186+
| `agentId` | string | Yes | Unique identifier for the cloud agent \(e.g., bc_abc123\) |
187+
188+
#### Output
189+
190+
| Parameter | Type | Description |
191+
| --------- | ---- | ----------- |
192+
| `artifacts` | array | List of artifact files |
193+
|`path` | string | Artifact file path |
194+
|`size` | number | File size in bytes |
195+
196+
### `cursor_download_artifact`
197+
198+
Download a generated artifact file from a cloud agent. Returns the file for execution storage.
199+
200+
#### Input
201+
202+
| Parameter | Type | Required | Description |
203+
| --------- | ---- | -------- | ----------- |
204+
| `apiKey` | string | Yes | Cursor API key |
205+
| `agentId` | string | Yes | Unique identifier for the cloud agent \(e.g., bc_abc123\) |
206+
| `path` | string | Yes | Absolute path of the artifact to download \(e.g., /src/index.ts\) |
207+
208+
#### Output
209+
210+
| Parameter | Type | Description |
211+
| --------- | ---- | ----------- |
212+
| `file` | file | Downloaded artifact file stored in execution files |
213+
176214

apps/sim/app/(landing)/integrations/data/integrations.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2327,9 +2327,17 @@
23272327
{
23282328
"name": "Delete Agent",
23292329
"description": "Permanently delete a cloud agent. This action cannot be undone."
2330+
},
2331+
{
2332+
"name": "List Artifacts",
2333+
"description": "List generated artifact files for a cloud agent."
2334+
},
2335+
{
2336+
"name": "Download Artifact",
2337+
"description": "Download a generated artifact file from a cloud agent."
23302338
}
23312339
],
2332-
"operationCount": 7,
2340+
"operationCount": 9,
23332341
"triggers": [],
23342342
"triggerCount": 0,
23352343
"authType": "api-key",
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import { createLogger } from '@sim/logger'
2+
import { type NextRequest, NextResponse } from 'next/server'
3+
import { z } from 'zod'
4+
import { checkInternalAuth } from '@/lib/auth/hybrid'
5+
import {
6+
secureFetchWithPinnedIP,
7+
validateUrlWithDNS,
8+
} from '@/lib/core/security/input-validation.server'
9+
import { generateRequestId } from '@/lib/core/utils/request'
10+
11+
export const dynamic = 'force-dynamic'
12+
13+
const logger = createLogger('CursorDownloadArtifactAPI')
14+
15+
const DownloadArtifactSchema = z.object({
16+
apiKey: z.string().min(1, 'API key is required'),
17+
agentId: z.string().min(1, 'Agent ID is required'),
18+
path: z.string().min(1, 'Artifact path is required'),
19+
})
20+
21+
export async function POST(request: NextRequest) {
22+
const requestId = generateRequestId()
23+
24+
try {
25+
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
26+
27+
if (!authResult.success) {
28+
logger.warn(
29+
`[${requestId}] Unauthorized Cursor download artifact attempt: ${authResult.error}`
30+
)
31+
return NextResponse.json(
32+
{ success: false, error: authResult.error || 'Authentication required' },
33+
{ status: 401 }
34+
)
35+
}
36+
37+
logger.info(
38+
`[${requestId}] Authenticated Cursor download artifact request via ${authResult.authType}`,
39+
{
40+
userId: authResult.userId,
41+
}
42+
)
43+
44+
const body = await request.json()
45+
const { apiKey, agentId, path } = DownloadArtifactSchema.parse(body)
46+
47+
const authHeader = `Basic ${Buffer.from(`${apiKey}:`).toString('base64')}`
48+
49+
logger.info(`[${requestId}] Requesting presigned URL for artifact`, { agentId, path })
50+
51+
const artifactResponse = await fetch(
52+
`https://api.cursor.com/v0/agents/${encodeURIComponent(agentId)}/artifacts/download?path=${encodeURIComponent(path)}`,
53+
{
54+
method: 'GET',
55+
headers: {
56+
Authorization: authHeader,
57+
},
58+
}
59+
)
60+
61+
if (!artifactResponse.ok) {
62+
const errorText = await artifactResponse.text().catch(() => '')
63+
logger.error(`[${requestId}] Failed to get artifact presigned URL`, {
64+
status: artifactResponse.status,
65+
error: errorText,
66+
})
67+
return NextResponse.json(
68+
{
69+
success: false,
70+
error: errorText || `Failed to get artifact URL (${artifactResponse.status})`,
71+
},
72+
{ status: artifactResponse.status }
73+
)
74+
}
75+
76+
const artifactData = await artifactResponse.json()
77+
const downloadUrl = artifactData.url || artifactData.downloadUrl || artifactData.presignedUrl
78+
79+
if (!downloadUrl) {
80+
logger.error(`[${requestId}] No download URL in artifact response`, { artifactData })
81+
return NextResponse.json(
82+
{ success: false, error: 'No download URL returned for artifact' },
83+
{ status: 400 }
84+
)
85+
}
86+
87+
const urlValidation = await validateUrlWithDNS(downloadUrl, 'downloadUrl')
88+
if (!urlValidation.isValid) {
89+
return NextResponse.json({ success: false, error: urlValidation.error }, { status: 400 })
90+
}
91+
92+
logger.info(`[${requestId}] Downloading artifact from presigned URL`, { agentId, path })
93+
94+
const downloadResponse = await secureFetchWithPinnedIP(
95+
downloadUrl,
96+
urlValidation.resolvedIP!,
97+
{}
98+
)
99+
100+
if (!downloadResponse.ok) {
101+
logger.error(`[${requestId}] Failed to download artifact content`, {
102+
status: downloadResponse.status,
103+
statusText: downloadResponse.statusText,
104+
})
105+
return NextResponse.json(
106+
{
107+
success: false,
108+
error: `Failed to download artifact content (${downloadResponse.status}: ${downloadResponse.statusText})`,
109+
},
110+
{ status: downloadResponse.status }
111+
)
112+
}
113+
114+
const contentType = downloadResponse.headers.get('content-type') || 'application/octet-stream'
115+
const arrayBuffer = await downloadResponse.arrayBuffer()
116+
const fileBuffer = Buffer.from(arrayBuffer)
117+
118+
const fileName = path.split('/').pop() || 'artifact'
119+
120+
logger.info(`[${requestId}] Artifact downloaded successfully`, {
121+
agentId,
122+
path,
123+
name: fileName,
124+
size: fileBuffer.length,
125+
mimeType: contentType,
126+
})
127+
128+
return NextResponse.json({
129+
success: true,
130+
output: {
131+
file: {
132+
name: fileName,
133+
mimeType: contentType,
134+
data: fileBuffer.toString('base64'),
135+
size: fileBuffer.length,
136+
},
137+
},
138+
})
139+
} catch (error) {
140+
logger.error(`[${requestId}] Error downloading Cursor artifact:`, error)
141+
return NextResponse.json(
142+
{ success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' },
143+
{ status: 500 }
144+
)
145+
}
146+
}

apps/sim/blocks/blocks/cursor.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export const CursorBlock: BlockConfig<CursorResponse> = {
3131
{ label: 'List Agents', id: 'cursor_list_agents' },
3232
{ label: 'Stop Agent', id: 'cursor_stop_agent' },
3333
{ label: 'Delete Agent', id: 'cursor_delete_agent' },
34+
{ label: 'List Artifacts', id: 'cursor_list_artifacts' },
35+
{ label: 'Download Artifact', id: 'cursor_download_artifact' },
3436
],
3537
value: () => 'cursor_launch_agent',
3638
},
@@ -48,6 +50,7 @@ export const CursorBlock: BlockConfig<CursorResponse> = {
4850
type: 'short-input',
4951
placeholder: 'main (optional)',
5052
condition: { field: 'operation', value: 'cursor_launch_agent' },
53+
mode: 'advanced',
5154
},
5255
{
5356
id: 'promptText',
@@ -57,19 +60,29 @@ export const CursorBlock: BlockConfig<CursorResponse> = {
5760
condition: { field: 'operation', value: 'cursor_launch_agent' },
5861
required: true,
5962
},
63+
{
64+
id: 'promptImages',
65+
title: 'Prompt Images',
66+
type: 'long-input',
67+
placeholder: '[{"data": "base64...", "dimension": {"width": 1024, "height": 768}}]',
68+
condition: { field: 'operation', value: ['cursor_launch_agent', 'cursor_add_followup'] },
69+
mode: 'advanced',
70+
},
6071
{
6172
id: 'model',
6273
title: 'Model',
6374
type: 'short-input',
6475
placeholder: 'Auto-selection by default',
6576
condition: { field: 'operation', value: 'cursor_launch_agent' },
77+
mode: 'advanced',
6678
},
6779
{
6880
id: 'branchName',
6981
title: 'Branch Name',
7082
type: 'short-input',
7183
placeholder: 'Custom branch name (optional)',
7284
condition: { field: 'operation', value: 'cursor_launch_agent' },
85+
mode: 'advanced',
7386
},
7487
{
7588
id: 'autoCreatePr',
@@ -82,12 +95,14 @@ export const CursorBlock: BlockConfig<CursorResponse> = {
8295
title: 'Open as Cursor GitHub App',
8396
type: 'switch',
8497
condition: { field: 'operation', value: 'cursor_launch_agent' },
98+
mode: 'advanced',
8599
},
86100
{
87101
id: 'skipReviewerRequest',
88102
title: 'Skip Reviewer Request',
89103
type: 'switch',
90104
condition: { field: 'operation', value: 'cursor_launch_agent' },
105+
mode: 'advanced',
91106
},
92107
{
93108
id: 'agentId',
@@ -102,10 +117,20 @@ export const CursorBlock: BlockConfig<CursorResponse> = {
102117
'cursor_add_followup',
103118
'cursor_stop_agent',
104119
'cursor_delete_agent',
120+
'cursor_list_artifacts',
121+
'cursor_download_artifact',
105122
],
106123
},
107124
required: true,
108125
},
126+
{
127+
id: 'path',
128+
title: 'Artifact Path',
129+
type: 'short-input',
130+
placeholder: '/opt/cursor/artifacts/screenshot.png',
131+
condition: { field: 'operation', value: 'cursor_download_artifact' },
132+
required: true,
133+
},
109134
{
110135
id: 'followupPromptText',
111136
title: 'Follow-up Prompt',
@@ -114,19 +139,29 @@ export const CursorBlock: BlockConfig<CursorResponse> = {
114139
condition: { field: 'operation', value: 'cursor_add_followup' },
115140
required: true,
116141
},
142+
{
143+
id: 'prUrl',
144+
title: 'PR URL Filter',
145+
type: 'short-input',
146+
placeholder: 'Filter by pull request URL (optional)',
147+
condition: { field: 'operation', value: 'cursor_list_agents' },
148+
mode: 'advanced',
149+
},
117150
{
118151
id: 'limit',
119152
title: 'Limit',
120153
type: 'short-input',
121154
placeholder: '20 (default, max 100)',
122155
condition: { field: 'operation', value: 'cursor_list_agents' },
156+
mode: 'advanced',
123157
},
124158
{
125159
id: 'cursor',
126160
title: 'Pagination Cursor',
127161
type: 'short-input',
128162
placeholder: 'Cursor from previous response',
129163
condition: { field: 'operation', value: 'cursor_list_agents' },
164+
mode: 'advanced',
130165
},
131166
{
132167
id: 'apiKey',
@@ -146,6 +181,8 @@ export const CursorBlock: BlockConfig<CursorResponse> = {
146181
'cursor_add_followup',
147182
'cursor_stop_agent',
148183
'cursor_delete_agent',
184+
'cursor_list_artifacts',
185+
'cursor_download_artifact',
149186
],
150187
config: {
151188
tool: (params) => params.operation || 'cursor_launch_agent',
@@ -157,15 +194,20 @@ export const CursorBlock: BlockConfig<CursorResponse> = {
157194
ref: { type: 'string', description: 'Branch, tag, or commit reference' },
158195
promptText: { type: 'string', description: 'Instruction text for the agent' },
159196
followupPromptText: { type: 'string', description: 'Follow-up instruction text for the agent' },
160-
promptImages: { type: 'string', description: 'JSON array of image objects' },
197+
promptImages: {
198+
type: 'string',
199+
description: 'JSON array of image objects with base64 data and dimensions',
200+
},
161201
model: { type: 'string', description: 'Model to use (empty for auto-selection)' },
162202
branchName: { type: 'string', description: 'Custom branch name' },
163203
autoCreatePr: { type: 'boolean', description: 'Auto-create PR when done' },
164204
openAsCursorGithubApp: { type: 'boolean', description: 'Open PR as Cursor GitHub App' },
165205
skipReviewerRequest: { type: 'boolean', description: 'Skip reviewer request' },
166206
agentId: { type: 'string', description: 'Agent identifier' },
207+
prUrl: { type: 'string', description: 'Filter agents by pull request URL' },
167208
limit: { type: 'number', description: 'Number of results to return' },
168209
cursor: { type: 'string', description: 'Pagination cursor' },
210+
path: { type: 'string', description: 'Absolute path of the artifact to download' },
169211
apiKey: { type: 'string', description: 'Cursor API key' },
170212
},
171213
outputs: {
@@ -192,6 +234,8 @@ export const CursorV2Block: BlockConfig<CursorResponse> = {
192234
'cursor_add_followup_v2',
193235
'cursor_stop_agent_v2',
194236
'cursor_delete_agent_v2',
237+
'cursor_list_artifacts_v2',
238+
'cursor_download_artifact_v2',
195239
],
196240
config: {
197241
tool: createVersionedToolSelector({
@@ -213,5 +257,7 @@ export const CursorV2Block: BlockConfig<CursorResponse> = {
213257
agents: { type: 'json', description: 'Array of agent objects (list operation)' },
214258
nextCursor: { type: 'string', description: 'Pagination cursor (list operation)' },
215259
messages: { type: 'json', description: 'Conversation messages (get conversation operation)' },
260+
artifacts: { type: 'json', description: 'List of artifact files (list artifacts operation)' },
261+
file: { type: 'file', description: 'Downloaded artifact file (download artifact operation)' },
216262
},
217263
}

apps/sim/tools/cursor/add_followup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const addFollowupBase = {
3030
},
3131
request: {
3232
url: (params: AddFollowupParams) =>
33-
`https://api.cursor.com/v0/agents/${params.agentId}/followup`,
33+
`https://api.cursor.com/v0/agents/${params.agentId.trim()}/followup`,
3434
method: 'POST',
3535
headers: (params: AddFollowupParams) => ({
3636
'Content-Type': 'application/json',

apps/sim/tools/cursor/delete_agent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const deleteAgentBase = {
1717
},
1818
},
1919
request: {
20-
url: (params: DeleteAgentParams) => `https://api.cursor.com/v0/agents/${params.agentId}`,
20+
url: (params: DeleteAgentParams) => `https://api.cursor.com/v0/agents/${params.agentId.trim()}`,
2121
method: 'DELETE',
2222
headers: (params: DeleteAgentParams) => ({
2323
Authorization: `Basic ${Buffer.from(`${params.apiKey}:`).toString('base64')}`,

0 commit comments

Comments
 (0)