everruns_integrations_cursor/
lib.rs1pub mod client;
10pub mod connection;
11mod tools;
12
13use everruns_core::capabilities::{
14 Capability, CapabilityLocalization, CapabilityStatus, IntegrationPlugin, RiskLevel,
15};
16use everruns_core::connector::ConnectorPlugin;
17use everruns_core::tool_narration::ToolNarrationPhase;
18use everruns_core::tool_types::{ToolCall, ToolDefinition};
19use everruns_core::tools::Tool;
20
21use connection::CursorConnector;
22use tools::{
23 CursorAddFollowupTool, CursorDeleteAgentTool, CursorGetAgentTool, CursorGetConversationTool,
24 CursorKeyInfoTool, CursorLaunchAgentTool, CursorListAgentsTool, CursorListModelsTool,
25 CursorListRepositoriesTool,
26};
27
28inventory::submit! {
29 IntegrationPlugin {
30 experimental_only: false,
31 feature_flag: None,
32 factory: || Box::new(CursorCapability),
33 }
34}
35
36inventory::submit! {
37 ConnectorPlugin {
38 experimental_only: false,
39 factory: || Box::new(CursorConnector),
40 }
41}
42
43pub const CURSOR_API_BASE: &str = "https://api.cursor.com";
44pub const CURSOR_API_KEY_SECRET: &str = "CURSOR_API_KEY";
45pub const CURSOR_CONNECTION_PROVIDER: &str = "cursor";
46pub const CURSOR_API_BASE_ENV: &str = "EVERRUNS_CURSOR_API_BASE";
47
48pub struct CursorCapability;
49
50impl Capability for CursorCapability {
51 fn id(&self) -> &str {
52 "cursor"
53 }
54
55 fn name(&self) -> &str {
56 "Cursor"
57 }
58
59 fn description(&self) -> &str {
60 "Create and manage Cursor Cloud Agents that work asynchronously on GitHub repositories."
61 }
62
63 fn localizations(&self) -> Vec<CapabilityLocalization> {
64 vec![CapabilityLocalization::text(
65 "uk",
66 "Cursor",
67 "Створюйте агентів Cursor Cloud Agents, що асинхронно працюють із репозиторіями \
68 GitHub, і керуйте ними.",
69 )]
70 }
71
72 fn status(&self) -> CapabilityStatus {
73 CapabilityStatus::Available
74 }
75
76 fn icon(&self) -> Option<&str> {
77 Some("code")
78 }
79
80 fn category(&self) -> Option<&str> {
81 Some("Execution")
82 }
83
84 fn risk_level(&self) -> RiskLevel {
85 RiskLevel::High
86 }
87
88 fn system_prompt_addition(&self) -> Option<&str> {
89 Some(
90 "Delegate async coding work to Cursor Cloud Agents when a remote repository task should run outside this session. Launch with a precise repo, base ref, task, and optional branch; track status, send follow-ups while running, inspect the transcript before summarizing, and delete agent records only when asked. Omit model unless the user specifies one.",
91 )
92 }
93
94 fn tools(&self) -> Vec<Box<dyn Tool>> {
95 vec![
96 Box::new(CursorLaunchAgentTool),
97 Box::new(CursorGetAgentTool),
98 Box::new(CursorListAgentsTool),
99 Box::new(CursorAddFollowupTool),
100 Box::new(CursorGetConversationTool),
101 Box::new(CursorDeleteAgentTool),
102 Box::new(CursorListModelsTool),
103 Box::new(CursorListRepositoriesTool),
104 Box::new(CursorKeyInfoTool),
105 ]
106 }
107
108 fn narrate(
109 &self,
110 _tool_def: Option<&ToolDefinition>,
111 tool_call: &ToolCall,
112 phase: ToolNarrationPhase,
113 _locale: Option<&str>,
114 _ctx: everruns_core::tool_narration::ToolNarrationContext<'_>,
115 ) -> Option<String> {
116 let target = tool_call
117 .arguments
118 .get("name")
119 .or_else(|| tool_call.arguments.get("agent_id"))
120 .or_else(|| tool_call.arguments.get("repository"))
121 .and_then(|v| v.as_str())
122 .map(truncate_target);
123
124 let (started, completed, failed) = match tool_call.name.as_str() {
125 "cursor_launch_agent" => (
126 "Starting Cursor agent",
127 "Started Cursor agent",
128 "Failed to start Cursor agent",
129 ),
130 "cursor_get_agent" => (
131 "Checking Cursor agent",
132 "Checked Cursor agent",
133 "Failed to check Cursor agent",
134 ),
135 "cursor_list_agents" => (
136 "Listing Cursor agents",
137 "Listed Cursor agents",
138 "Failed to list Cursor agents",
139 ),
140 "cursor_add_followup" => (
141 "Sending Cursor follow-up",
142 "Sent Cursor follow-up",
143 "Failed to send Cursor follow-up",
144 ),
145 "cursor_get_conversation" => (
146 "Reading Cursor conversation",
147 "Read Cursor conversation",
148 "Failed to read Cursor conversation",
149 ),
150 "cursor_delete_agent" => (
151 "Deleting Cursor agent",
152 "Deleted Cursor agent",
153 "Failed to delete Cursor agent",
154 ),
155 "cursor_list_models" => (
156 "Listing Cursor models",
157 "Listed Cursor models",
158 "Failed to list Cursor models",
159 ),
160 "cursor_list_repositories" => (
161 "Listing Cursor repositories",
162 "Listed Cursor repositories",
163 "Failed to list Cursor repositories",
164 ),
165 "cursor_key_info" => (
166 "Checking Cursor connection",
167 "Checked Cursor connection",
168 "Failed to check Cursor connection",
169 ),
170 _ => return None,
171 };
172
173 let verb = match phase {
174 ToolNarrationPhase::Started | ToolNarrationPhase::Waiting => started,
175 ToolNarrationPhase::Completed => completed,
176 ToolNarrationPhase::Failed => failed,
177 };
178
179 Some(match target {
180 Some(target) => format!("{verb}: {target}"),
181 None => verb.to_string(),
182 })
183 }
184}
185
186fn truncate_target(value: &str) -> String {
187 const MAX_CHARS: usize = 48;
188 let clean = value.trim();
189 if clean.chars().count() <= MAX_CHARS {
190 return clean.to_string();
191 }
192 format!("{}...", clean.chars().take(MAX_CHARS).collect::<String>())
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn capability_metadata() {
201 let cap = CursorCapability;
202 assert_eq!(cap.id(), "cursor");
203 assert_eq!(cap.name(), "Cursor");
204 assert_eq!(cap.status(), CapabilityStatus::Available);
205 assert_eq!(cap.icon(), Some("code"));
206 assert_eq!(cap.category(), Some("Execution"));
207 assert_eq!(cap.risk_level(), RiskLevel::High);
208 }
209
210 #[test]
211 fn capability_has_all_tools() {
212 let cap = CursorCapability;
213 let tools = cap.tools();
214 let names: Vec<&str> = tools.iter().map(|t| t.name()).collect();
215 assert_eq!(tools.len(), 9);
216 assert!(names.contains(&"cursor_launch_agent"));
217 assert!(names.contains(&"cursor_add_followup"));
218 assert!(names.contains(&"cursor_get_conversation"));
219 assert!(names.contains(&"cursor_list_repositories"));
220 }
221
222 #[tokio::test]
223 async fn system_prompt_within_budget() {
224 let cap = CursorCapability;
225 let ctx = everruns_core::capabilities::SystemPromptContext::without_file_store(
226 everruns_core::SessionId::new(),
227 );
228 let prompt = cap.system_prompt_contribution(&ctx).await.unwrap();
229 assert!(prompt.len() <= 475, "prompt is {} bytes", prompt.len());
230 }
231}