mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
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
This commit is contained in:
@@ -45,6 +45,7 @@ List all cloud agents for the authenticated user with optional pagination. Retur
|
||||
| `apiKey` | string | Yes | Cursor API key |
|
||||
| `limit` | number | No | Number of agents to return \(default: 20, max: 100\) |
|
||||
| `cursor` | string | No | Pagination cursor from previous response |
|
||||
| `prUrl` | string | No | Filter agents by pull request URL |
|
||||
|
||||
#### Output
|
||||
|
||||
@@ -173,4 +174,41 @@ Permanently delete a cloud agent. Returns API-aligned fields only.
|
||||
| --------- | ---- | ----------- |
|
||||
| `id` | string | Agent ID |
|
||||
|
||||
### `cursor_list_artifacts`
|
||||
|
||||
List generated artifact files for a cloud agent. Returns API-aligned fields only.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Cursor API key |
|
||||
| `agentId` | string | Yes | Unique identifier for the cloud agent \(e.g., bc_abc123\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `artifacts` | array | List of artifact files |
|
||||
| ↳ `path` | string | Artifact file path |
|
||||
| ↳ `size` | number | File size in bytes |
|
||||
|
||||
### `cursor_download_artifact`
|
||||
|
||||
Download a generated artifact file from a cloud agent. Returns the file for execution storage.
|
||||
|
||||
#### Input
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `apiKey` | string | Yes | Cursor API key |
|
||||
| `agentId` | string | Yes | Unique identifier for the cloud agent \(e.g., bc_abc123\) |
|
||||
| `path` | string | Yes | Absolute path of the artifact to download \(e.g., /src/index.ts\) |
|
||||
|
||||
#### Output
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| --------- | ---- | ----------- |
|
||||
| `file` | file | Downloaded artifact file stored in execution files |
|
||||
|
||||
|
||||
|
||||
@@ -2327,9 +2327,17 @@
|
||||
{
|
||||
"name": "Delete Agent",
|
||||
"description": "Permanently delete a cloud agent. This action cannot be undone."
|
||||
},
|
||||
{
|
||||
"name": "List Artifacts",
|
||||
"description": "List generated artifact files for a cloud agent."
|
||||
},
|
||||
{
|
||||
"name": "Download Artifact",
|
||||
"description": "Download a generated artifact file from a cloud agent."
|
||||
}
|
||||
],
|
||||
"operationCount": 7,
|
||||
"operationCount": 9,
|
||||
"triggers": [],
|
||||
"triggerCount": 0,
|
||||
"authType": "api-key",
|
||||
|
||||
146
apps/sim/app/api/tools/cursor/download-artifact/route.ts
Normal file
146
apps/sim/app/api/tools/cursor/download-artifact/route.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import {
|
||||
secureFetchWithPinnedIP,
|
||||
validateUrlWithDNS,
|
||||
} from '@/lib/core/security/input-validation.server'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('CursorDownloadArtifactAPI')
|
||||
|
||||
const DownloadArtifactSchema = z.object({
|
||||
apiKey: z.string().min(1, 'API key is required'),
|
||||
agentId: z.string().min(1, 'Agent ID is required'),
|
||||
path: z.string().min(1, 'Artifact path is required'),
|
||||
})
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const requestId = generateRequestId()
|
||||
|
||||
try {
|
||||
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||
|
||||
if (!authResult.success) {
|
||||
logger.warn(
|
||||
`[${requestId}] Unauthorized Cursor download artifact attempt: ${authResult.error}`
|
||||
)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: authResult.error || 'Authentication required' },
|
||||
{ status: 401 }
|
||||
)
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[${requestId}] Authenticated Cursor download artifact request via ${authResult.authType}`,
|
||||
{
|
||||
userId: authResult.userId,
|
||||
}
|
||||
)
|
||||
|
||||
const body = await request.json()
|
||||
const { apiKey, agentId, path } = DownloadArtifactSchema.parse(body)
|
||||
|
||||
const authHeader = `Basic ${Buffer.from(`${apiKey}:`).toString('base64')}`
|
||||
|
||||
logger.info(`[${requestId}] Requesting presigned URL for artifact`, { agentId, path })
|
||||
|
||||
const artifactResponse = await fetch(
|
||||
`https://api.cursor.com/v0/agents/${encodeURIComponent(agentId)}/artifacts/download?path=${encodeURIComponent(path)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: authHeader,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!artifactResponse.ok) {
|
||||
const errorText = await artifactResponse.text().catch(() => '')
|
||||
logger.error(`[${requestId}] Failed to get artifact presigned URL`, {
|
||||
status: artifactResponse.status,
|
||||
error: errorText,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: errorText || `Failed to get artifact URL (${artifactResponse.status})`,
|
||||
},
|
||||
{ status: artifactResponse.status }
|
||||
)
|
||||
}
|
||||
|
||||
const artifactData = await artifactResponse.json()
|
||||
const downloadUrl = artifactData.url || artifactData.downloadUrl || artifactData.presignedUrl
|
||||
|
||||
if (!downloadUrl) {
|
||||
logger.error(`[${requestId}] No download URL in artifact response`, { artifactData })
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'No download URL returned for artifact' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const urlValidation = await validateUrlWithDNS(downloadUrl, 'downloadUrl')
|
||||
if (!urlValidation.isValid) {
|
||||
return NextResponse.json({ success: false, error: urlValidation.error }, { status: 400 })
|
||||
}
|
||||
|
||||
logger.info(`[${requestId}] Downloading artifact from presigned URL`, { agentId, path })
|
||||
|
||||
const downloadResponse = await secureFetchWithPinnedIP(
|
||||
downloadUrl,
|
||||
urlValidation.resolvedIP!,
|
||||
{}
|
||||
)
|
||||
|
||||
if (!downloadResponse.ok) {
|
||||
logger.error(`[${requestId}] Failed to download artifact content`, {
|
||||
status: downloadResponse.status,
|
||||
statusText: downloadResponse.statusText,
|
||||
})
|
||||
return NextResponse.json(
|
||||
{
|
||||
success: false,
|
||||
error: `Failed to download artifact content (${downloadResponse.status}: ${downloadResponse.statusText})`,
|
||||
},
|
||||
{ status: downloadResponse.status }
|
||||
)
|
||||
}
|
||||
|
||||
const contentType = downloadResponse.headers.get('content-type') || 'application/octet-stream'
|
||||
const arrayBuffer = await downloadResponse.arrayBuffer()
|
||||
const fileBuffer = Buffer.from(arrayBuffer)
|
||||
|
||||
const fileName = path.split('/').pop() || 'artifact'
|
||||
|
||||
logger.info(`[${requestId}] Artifact downloaded successfully`, {
|
||||
agentId,
|
||||
path,
|
||||
name: fileName,
|
||||
size: fileBuffer.length,
|
||||
mimeType: contentType,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
file: {
|
||||
name: fileName,
|
||||
mimeType: contentType,
|
||||
data: fileBuffer.toString('base64'),
|
||||
size: fileBuffer.length,
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error downloading Cursor artifact:`, error)
|
||||
return NextResponse.json(
|
||||
{ success: false, error: error instanceof Error ? error.message : 'Unknown error occurred' },
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,8 @@ export const CursorBlock: BlockConfig<CursorResponse> = {
|
||||
{ label: 'List Agents', id: 'cursor_list_agents' },
|
||||
{ label: 'Stop Agent', id: 'cursor_stop_agent' },
|
||||
{ label: 'Delete Agent', id: 'cursor_delete_agent' },
|
||||
{ label: 'List Artifacts', id: 'cursor_list_artifacts' },
|
||||
{ label: 'Download Artifact', id: 'cursor_download_artifact' },
|
||||
],
|
||||
value: () => 'cursor_launch_agent',
|
||||
},
|
||||
@@ -48,6 +50,7 @@ export const CursorBlock: BlockConfig<CursorResponse> = {
|
||||
type: 'short-input',
|
||||
placeholder: 'main (optional)',
|
||||
condition: { field: 'operation', value: 'cursor_launch_agent' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'promptText',
|
||||
@@ -57,12 +60,21 @@ export const CursorBlock: BlockConfig<CursorResponse> = {
|
||||
condition: { field: 'operation', value: 'cursor_launch_agent' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'promptImages',
|
||||
title: 'Prompt Images',
|
||||
type: 'long-input',
|
||||
placeholder: '[{"data": "base64...", "dimension": {"width": 1024, "height": 768}}]',
|
||||
condition: { field: 'operation', value: ['cursor_launch_agent', 'cursor_add_followup'] },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'model',
|
||||
title: 'Model',
|
||||
type: 'short-input',
|
||||
placeholder: 'Auto-selection by default',
|
||||
condition: { field: 'operation', value: 'cursor_launch_agent' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'branchName',
|
||||
@@ -70,6 +82,7 @@ export const CursorBlock: BlockConfig<CursorResponse> = {
|
||||
type: 'short-input',
|
||||
placeholder: 'Custom branch name (optional)',
|
||||
condition: { field: 'operation', value: 'cursor_launch_agent' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'autoCreatePr',
|
||||
@@ -82,12 +95,14 @@ export const CursorBlock: BlockConfig<CursorResponse> = {
|
||||
title: 'Open as Cursor GitHub App',
|
||||
type: 'switch',
|
||||
condition: { field: 'operation', value: 'cursor_launch_agent' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'skipReviewerRequest',
|
||||
title: 'Skip Reviewer Request',
|
||||
type: 'switch',
|
||||
condition: { field: 'operation', value: 'cursor_launch_agent' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'agentId',
|
||||
@@ -102,10 +117,20 @@ export const CursorBlock: BlockConfig<CursorResponse> = {
|
||||
'cursor_add_followup',
|
||||
'cursor_stop_agent',
|
||||
'cursor_delete_agent',
|
||||
'cursor_list_artifacts',
|
||||
'cursor_download_artifact',
|
||||
],
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'path',
|
||||
title: 'Artifact Path',
|
||||
type: 'short-input',
|
||||
placeholder: '/opt/cursor/artifacts/screenshot.png',
|
||||
condition: { field: 'operation', value: 'cursor_download_artifact' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'followupPromptText',
|
||||
title: 'Follow-up Prompt',
|
||||
@@ -114,12 +139,21 @@ export const CursorBlock: BlockConfig<CursorResponse> = {
|
||||
condition: { field: 'operation', value: 'cursor_add_followup' },
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
id: 'prUrl',
|
||||
title: 'PR URL Filter',
|
||||
type: 'short-input',
|
||||
placeholder: 'Filter by pull request URL (optional)',
|
||||
condition: { field: 'operation', value: 'cursor_list_agents' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'limit',
|
||||
title: 'Limit',
|
||||
type: 'short-input',
|
||||
placeholder: '20 (default, max 100)',
|
||||
condition: { field: 'operation', value: 'cursor_list_agents' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'cursor',
|
||||
@@ -127,6 +161,7 @@ export const CursorBlock: BlockConfig<CursorResponse> = {
|
||||
type: 'short-input',
|
||||
placeholder: 'Cursor from previous response',
|
||||
condition: { field: 'operation', value: 'cursor_list_agents' },
|
||||
mode: 'advanced',
|
||||
},
|
||||
{
|
||||
id: 'apiKey',
|
||||
@@ -146,6 +181,8 @@ export const CursorBlock: BlockConfig<CursorResponse> = {
|
||||
'cursor_add_followup',
|
||||
'cursor_stop_agent',
|
||||
'cursor_delete_agent',
|
||||
'cursor_list_artifacts',
|
||||
'cursor_download_artifact',
|
||||
],
|
||||
config: {
|
||||
tool: (params) => params.operation || 'cursor_launch_agent',
|
||||
@@ -157,15 +194,20 @@ export const CursorBlock: BlockConfig<CursorResponse> = {
|
||||
ref: { type: 'string', description: 'Branch, tag, or commit reference' },
|
||||
promptText: { type: 'string', description: 'Instruction text for the agent' },
|
||||
followupPromptText: { type: 'string', description: 'Follow-up instruction text for the agent' },
|
||||
promptImages: { type: 'string', description: 'JSON array of image objects' },
|
||||
promptImages: {
|
||||
type: 'string',
|
||||
description: 'JSON array of image objects with base64 data and dimensions',
|
||||
},
|
||||
model: { type: 'string', description: 'Model to use (empty for auto-selection)' },
|
||||
branchName: { type: 'string', description: 'Custom branch name' },
|
||||
autoCreatePr: { type: 'boolean', description: 'Auto-create PR when done' },
|
||||
openAsCursorGithubApp: { type: 'boolean', description: 'Open PR as Cursor GitHub App' },
|
||||
skipReviewerRequest: { type: 'boolean', description: 'Skip reviewer request' },
|
||||
agentId: { type: 'string', description: 'Agent identifier' },
|
||||
prUrl: { type: 'string', description: 'Filter agents by pull request URL' },
|
||||
limit: { type: 'number', description: 'Number of results to return' },
|
||||
cursor: { type: 'string', description: 'Pagination cursor' },
|
||||
path: { type: 'string', description: 'Absolute path of the artifact to download' },
|
||||
apiKey: { type: 'string', description: 'Cursor API key' },
|
||||
},
|
||||
outputs: {
|
||||
@@ -192,6 +234,8 @@ export const CursorV2Block: BlockConfig<CursorResponse> = {
|
||||
'cursor_add_followup_v2',
|
||||
'cursor_stop_agent_v2',
|
||||
'cursor_delete_agent_v2',
|
||||
'cursor_list_artifacts_v2',
|
||||
'cursor_download_artifact_v2',
|
||||
],
|
||||
config: {
|
||||
tool: createVersionedToolSelector({
|
||||
@@ -213,5 +257,7 @@ export const CursorV2Block: BlockConfig<CursorResponse> = {
|
||||
agents: { type: 'json', description: 'Array of agent objects (list operation)' },
|
||||
nextCursor: { type: 'string', description: 'Pagination cursor (list operation)' },
|
||||
messages: { type: 'json', description: 'Conversation messages (get conversation operation)' },
|
||||
artifacts: { type: 'json', description: 'List of artifact files (list artifacts operation)' },
|
||||
file: { type: 'file', description: 'Downloaded artifact file (download artifact operation)' },
|
||||
},
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ const addFollowupBase = {
|
||||
},
|
||||
request: {
|
||||
url: (params: AddFollowupParams) =>
|
||||
`https://api.cursor.com/v0/agents/${params.agentId}/followup`,
|
||||
`https://api.cursor.com/v0/agents/${params.agentId.trim()}/followup`,
|
||||
method: 'POST',
|
||||
headers: (params: AddFollowupParams) => ({
|
||||
'Content-Type': 'application/json',
|
||||
|
||||
@@ -17,7 +17,7 @@ const deleteAgentBase = {
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params: DeleteAgentParams) => `https://api.cursor.com/v0/agents/${params.agentId}`,
|
||||
url: (params: DeleteAgentParams) => `https://api.cursor.com/v0/agents/${params.agentId.trim()}`,
|
||||
method: 'DELETE',
|
||||
headers: (params: DeleteAgentParams) => ({
|
||||
Authorization: `Basic ${Buffer.from(`${params.apiKey}:`).toString('base64')}`,
|
||||
|
||||
113
apps/sim/tools/cursor/download_artifact.ts
Normal file
113
apps/sim/tools/cursor/download_artifact.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import type {
|
||||
DownloadArtifactParams,
|
||||
DownloadArtifactResponse,
|
||||
DownloadArtifactV2Response,
|
||||
} from '@/tools/cursor/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const downloadArtifactBase = {
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Cursor API key',
|
||||
},
|
||||
agentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Unique identifier for the cloud agent (e.g., bc_abc123)',
|
||||
},
|
||||
path: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Absolute path of the artifact to download (e.g., /src/index.ts)',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: '/api/tools/cursor/download-artifact',
|
||||
method: 'POST',
|
||||
headers: () => ({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params: DownloadArtifactParams) => ({
|
||||
apiKey: params.apiKey,
|
||||
agentId: params.agentId?.trim(),
|
||||
path: params.path?.trim(),
|
||||
}),
|
||||
},
|
||||
} satisfies Pick<ToolConfig<DownloadArtifactParams, any>, 'params' | 'request'>
|
||||
|
||||
export const downloadArtifactTool: ToolConfig<DownloadArtifactParams, DownloadArtifactResponse> = {
|
||||
id: 'cursor_download_artifact',
|
||||
name: 'Cursor Download Artifact',
|
||||
description: 'Download a generated artifact file from a cloud agent.',
|
||||
version: '1.0.0',
|
||||
|
||||
...downloadArtifactBase,
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Failed to download artifact')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content: `Downloaded artifact: ${data.output.file.name}`,
|
||||
metadata: data.output.file,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Human-readable download result' },
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Downloaded file metadata',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'File name' },
|
||||
mimeType: { type: 'string', description: 'MIME type' },
|
||||
size: { type: 'number', description: 'File size in bytes' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const downloadArtifactV2Tool: ToolConfig<
|
||||
DownloadArtifactParams,
|
||||
DownloadArtifactV2Response
|
||||
> = {
|
||||
...downloadArtifactBase,
|
||||
id: 'cursor_download_artifact_v2',
|
||||
name: 'Cursor Download Artifact',
|
||||
description:
|
||||
'Download a generated artifact file from a cloud agent. Returns the file for execution storage.',
|
||||
version: '2.0.0',
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error || 'Failed to download artifact')
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
file: data.output.file,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
file: {
|
||||
type: 'file',
|
||||
description: 'Downloaded artifact file stored in execution files',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -17,7 +17,7 @@ const getAgentBase = {
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params: GetAgentParams) => `https://api.cursor.com/v0/agents/${params.agentId}`,
|
||||
url: (params: GetAgentParams) => `https://api.cursor.com/v0/agents/${params.agentId.trim()}`,
|
||||
method: 'GET',
|
||||
headers: (params: GetAgentParams) => ({
|
||||
Authorization: `Basic ${Buffer.from(`${params.apiKey}:`).toString('base64')}`,
|
||||
|
||||
@@ -18,7 +18,7 @@ const getConversationBase = {
|
||||
},
|
||||
request: {
|
||||
url: (params: GetConversationParams) =>
|
||||
`https://api.cursor.com/v0/agents/${params.agentId}/conversation`,
|
||||
`https://api.cursor.com/v0/agents/${params.agentId.trim()}/conversation`,
|
||||
method: 'GET',
|
||||
headers: (params: GetConversationParams) => ({
|
||||
Authorization: `Basic ${Buffer.from(`${params.apiKey}:`).toString('base64')}`,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { addFollowupTool, addFollowupV2Tool } from '@/tools/cursor/add_followup'
|
||||
import { deleteAgentTool, deleteAgentV2Tool } from '@/tools/cursor/delete_agent'
|
||||
import { downloadArtifactTool, downloadArtifactV2Tool } from '@/tools/cursor/download_artifact'
|
||||
import { getAgentTool, getAgentV2Tool } from '@/tools/cursor/get_agent'
|
||||
import { getConversationTool, getConversationV2Tool } from '@/tools/cursor/get_conversation'
|
||||
import { launchAgentTool, launchAgentV2Tool } from '@/tools/cursor/launch_agent'
|
||||
import { listAgentsTool, listAgentsV2Tool } from '@/tools/cursor/list_agents'
|
||||
import { listArtifactsTool, listArtifactsV2Tool } from '@/tools/cursor/list_artifacts'
|
||||
import { stopAgentTool, stopAgentV2Tool } from '@/tools/cursor/stop_agent'
|
||||
|
||||
export const cursorListAgentsTool = listAgentsTool
|
||||
@@ -13,6 +15,8 @@ export const cursorLaunchAgentTool = launchAgentTool
|
||||
export const cursorAddFollowupTool = addFollowupTool
|
||||
export const cursorStopAgentTool = stopAgentTool
|
||||
export const cursorDeleteAgentTool = deleteAgentTool
|
||||
export const cursorDownloadArtifactTool = downloadArtifactTool
|
||||
export const cursorListArtifactsTool = listArtifactsTool
|
||||
|
||||
export const cursorListAgentsV2Tool = listAgentsV2Tool
|
||||
export const cursorGetAgentV2Tool = getAgentV2Tool
|
||||
@@ -21,3 +25,5 @@ export const cursorLaunchAgentV2Tool = launchAgentV2Tool
|
||||
export const cursorAddFollowupV2Tool = addFollowupV2Tool
|
||||
export const cursorStopAgentV2Tool = stopAgentV2Tool
|
||||
export const cursorDeleteAgentV2Tool = deleteAgentV2Tool
|
||||
export const cursorDownloadArtifactV2Tool = downloadArtifactV2Tool
|
||||
export const cursorListArtifactsV2Tool = listArtifactsV2Tool
|
||||
|
||||
@@ -21,12 +21,19 @@ const listAgentsBase = {
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Pagination cursor from previous response',
|
||||
},
|
||||
prUrl: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter agents by pull request URL',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params: ListAgentsParams) => {
|
||||
const url = new URL('https://api.cursor.com/v0/agents')
|
||||
if (params.limit) url.searchParams.set('limit', String(params.limit))
|
||||
if (params.cursor) url.searchParams.set('cursor', params.cursor)
|
||||
if (params.prUrl) url.searchParams.set('prUrl', params.prUrl)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
@@ -53,7 +60,7 @@ export const listAgentsTool: ToolConfig<ListAgentsParams, ListAgentsResponse> =
|
||||
content: `Found ${data.agents.length} agents`,
|
||||
metadata: {
|
||||
agents: data.agents,
|
||||
nextCursor: data.nextCursor,
|
||||
nextCursor: data.nextCursor ?? null,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
113
apps/sim/tools/cursor/list_artifacts.ts
Normal file
113
apps/sim/tools/cursor/list_artifacts.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import type { ListArtifactsParams, ListArtifactsResponse } from '@/tools/cursor/types'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
const listArtifactsBase = {
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Cursor API key',
|
||||
},
|
||||
agentId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Unique identifier for the cloud agent (e.g., bc_abc123)',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params: ListArtifactsParams) =>
|
||||
`https://api.cursor.com/v0/agents/${params.agentId.trim()}/artifacts`,
|
||||
method: 'GET',
|
||||
headers: (params: ListArtifactsParams) => ({
|
||||
Authorization: `Basic ${Buffer.from(`${params.apiKey}:`).toString('base64')}`,
|
||||
}),
|
||||
},
|
||||
} satisfies Pick<ToolConfig<ListArtifactsParams, any>, 'params' | 'request'>
|
||||
|
||||
export const listArtifactsTool: ToolConfig<ListArtifactsParams, ListArtifactsResponse> = {
|
||||
id: 'cursor_list_artifacts',
|
||||
name: 'Cursor List Artifacts',
|
||||
description: 'List generated artifact files for a cloud agent.',
|
||||
version: '1.0.0',
|
||||
|
||||
...listArtifactsBase,
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
const artifacts = data.artifacts ?? []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
content: `Found ${artifacts.length} artifact(s)`,
|
||||
metadata: {
|
||||
artifacts,
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
content: { type: 'string', description: 'Human-readable artifact count' },
|
||||
metadata: {
|
||||
type: 'object',
|
||||
description: 'Artifacts metadata',
|
||||
properties: {
|
||||
artifacts: {
|
||||
type: 'array',
|
||||
description: 'List of artifacts',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'Artifact file path' },
|
||||
size: { type: 'number', description: 'File size in bytes', optional: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
interface ListArtifactsV2Response {
|
||||
success: boolean
|
||||
output: {
|
||||
artifacts: Array<{ path: string; size?: number }>
|
||||
}
|
||||
}
|
||||
|
||||
export const listArtifactsV2Tool: ToolConfig<ListArtifactsParams, ListArtifactsV2Response> = {
|
||||
...listArtifactsBase,
|
||||
id: 'cursor_list_artifacts_v2',
|
||||
name: 'Cursor List Artifacts',
|
||||
description: 'List generated artifact files for a cloud agent. Returns API-aligned fields only.',
|
||||
version: '2.0.0',
|
||||
|
||||
transformResponse: async (response) => {
|
||||
const data = await response.json()
|
||||
const artifacts = data.artifacts ?? []
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
artifacts: Array.isArray(artifacts) ? artifacts : [],
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
artifacts: {
|
||||
type: 'array',
|
||||
description: 'List of artifact files',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
path: { type: 'string', description: 'Artifact file path' },
|
||||
size: { type: 'number', description: 'File size in bytes', optional: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -17,7 +17,8 @@ const stopAgentBase = {
|
||||
},
|
||||
},
|
||||
request: {
|
||||
url: (params: StopAgentParams) => `https://api.cursor.com/v0/agents/${params.agentId}/stop`,
|
||||
url: (params: StopAgentParams) =>
|
||||
`https://api.cursor.com/v0/agents/${params.agentId.trim()}/stop`,
|
||||
method: 'POST',
|
||||
headers: (params: StopAgentParams) => ({
|
||||
Authorization: `Basic ${Buffer.from(`${params.apiKey}:`).toString('base64')}`,
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface BaseCursorParams {
|
||||
export interface ListAgentsParams extends BaseCursorParams {
|
||||
limit?: number
|
||||
cursor?: string
|
||||
prUrl?: string
|
||||
}
|
||||
|
||||
export interface GetAgentParams extends BaseCursorParams {
|
||||
@@ -60,7 +61,7 @@ interface AgentTarget {
|
||||
interface AgentMetadata {
|
||||
id: string
|
||||
name: string
|
||||
status: 'RUNNING' | 'FINISHED' | 'STOPPED' | 'FAILED'
|
||||
status: 'CREATING' | 'RUNNING' | 'FINISHED' | 'STOPPED' | 'FAILED'
|
||||
source: AgentSource
|
||||
target: AgentTarget
|
||||
summary?: string
|
||||
@@ -174,6 +175,47 @@ export interface ListRepositoriesResponse extends ToolResponse {
|
||||
}
|
||||
}
|
||||
|
||||
export interface ListArtifactsParams extends BaseCursorParams {
|
||||
agentId: string
|
||||
}
|
||||
|
||||
export interface ArtifactMetadata {
|
||||
path: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
export interface ListArtifactsResponse extends ToolResponse {
|
||||
output: {
|
||||
content: string
|
||||
metadata: {
|
||||
artifacts: ArtifactMetadata[]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface DownloadArtifactParams extends BaseCursorParams {
|
||||
agentId: string
|
||||
path: string
|
||||
}
|
||||
|
||||
export interface DownloadArtifactResponse extends ToolResponse {
|
||||
output: {
|
||||
content: string
|
||||
metadata: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
export interface DownloadArtifactV2Response extends ToolResponse {
|
||||
output: {
|
||||
file: {
|
||||
name: string
|
||||
mimeType: string
|
||||
data: string
|
||||
size: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type CursorResponse =
|
||||
| ListAgentsResponse
|
||||
| GetAgentResponse
|
||||
@@ -185,3 +227,6 @@ export type CursorResponse =
|
||||
| GetApiKeyInfoResponse
|
||||
| ListModelsResponse
|
||||
| ListRepositoriesResponse
|
||||
| ListArtifactsResponse
|
||||
| DownloadArtifactResponse
|
||||
| DownloadArtifactV2Response
|
||||
|
||||
@@ -346,6 +346,8 @@ import {
|
||||
cursorAddFollowupV2Tool,
|
||||
cursorDeleteAgentTool,
|
||||
cursorDeleteAgentV2Tool,
|
||||
cursorDownloadArtifactTool,
|
||||
cursorDownloadArtifactV2Tool,
|
||||
cursorGetAgentTool,
|
||||
cursorGetAgentV2Tool,
|
||||
cursorGetConversationTool,
|
||||
@@ -354,6 +356,8 @@ import {
|
||||
cursorLaunchAgentV2Tool,
|
||||
cursorListAgentsTool,
|
||||
cursorListAgentsV2Tool,
|
||||
cursorListArtifactsTool,
|
||||
cursorListArtifactsV2Tool,
|
||||
cursorStopAgentTool,
|
||||
cursorStopAgentV2Tool,
|
||||
} from '@/tools/cursor'
|
||||
@@ -4123,6 +4127,10 @@ export const tools: Record<string, ToolConfig> = {
|
||||
cursor_stop_agent_v2: cursorStopAgentV2Tool,
|
||||
cursor_delete_agent: cursorDeleteAgentTool,
|
||||
cursor_delete_agent_v2: cursorDeleteAgentV2Tool,
|
||||
cursor_download_artifact: cursorDownloadArtifactTool,
|
||||
cursor_download_artifact_v2: cursorDownloadArtifactV2Tool,
|
||||
cursor_list_artifacts: cursorListArtifactsTool,
|
||||
cursor_list_artifacts_v2: cursorListArtifactsV2Tool,
|
||||
trello_list_lists: trelloListListsTool,
|
||||
trello_list_cards: trelloListCardsTool,
|
||||
trello_create_card: trelloCreateCardTool,
|
||||
|
||||
Reference in New Issue
Block a user