@bugmole/cli 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (308) hide show
  1. package/.bugmole.env.example +20 -0
  2. package/LICENSE +7 -0
  3. package/README.md +293 -0
  4. package/TESTING.md +117 -0
  5. package/bugmole.config.yaml +39 -0
  6. package/package.json +85 -0
  7. package/scripts/billing/paypal-setup.mjs +121 -0
  8. package/scripts/bugmole-continue.ts +318 -0
  9. package/scripts/bugmole-init.ts +188 -0
  10. package/scripts/bugmole.cjs +17 -0
  11. package/scripts/bugmole.test.ts +344 -0
  12. package/scripts/bugmole.ts +657 -0
  13. package/scripts/ensure-maestro.cjs +79 -0
  14. package/scripts/ios-tunnel-keeper.sh +45 -0
  15. package/scripts/ios-wda-keeper.sh +66 -0
  16. package/scripts/sync-plan-catalog.d.mts +3 -0
  17. package/scripts/sync-plan-catalog.mjs +16 -0
  18. package/scripts/ui-parity-diff.py +65 -0
  19. package/scripts/ui-parity-requirements.txt +1 -0
  20. package/scripts/verify-manage-to-plans.mts +194 -0
  21. package/spec/app-ui-audit.schema.json +176 -0
  22. package/spec/blockers.yaml +79 -0
  23. package/spec/bugs.index.json +42 -0
  24. package/spec/design-dna.schema.json +38 -0
  25. package/spec/domain_rules.yaml +24 -0
  26. package/spec/flows/flow_manage-to-plans-64c3c61b.yaml +8 -0
  27. package/spec/flows/flow_screen-to-evidence-6c3ab309.yaml +7 -0
  28. package/spec/journey_graph.yaml +126 -0
  29. package/spec/journeys.graph.json +2618 -0
  30. package/spec/plans/dashboard-smoke.flow.yaml +20 -0
  31. package/spec/plans/example.flow.yaml +99 -0
  32. package/spec/plans/flow_api-keys-to-logout-7396cd5d.flow.yaml +33 -0
  33. package/spec/plans/flow_api-keys-to-screen-157e10a6.flow.yaml +33 -0
  34. package/spec/plans/flow_manage-to-audits-176c1eb8.flow.yaml +33 -0
  35. package/spec/plans/flow_manage-to-blockers-633282c8.flow.yaml +112 -0
  36. package/spec/plans/flow_manage-to-devices-ad57e202.flow.yaml +33 -0
  37. package/spec/plans/flow_manage-to-logout-86c54656.flow.yaml +33 -0
  38. package/spec/plans/flow_manage-to-manage-resource-action-6325f99a.flow.yaml +185 -0
  39. package/spec/plans/flow_manage-to-parity-issues-01fa2579.flow.yaml +34 -0
  40. package/spec/plans/flow_manage-to-plans-64c3c61b.flow.yaml +123 -0
  41. package/spec/plans/flow_manage-to-screen-6857f915.flow.yaml +106 -0
  42. package/spec/plans/flow_manage-to-tasks-c5791c95.flow.yaml +33 -0
  43. package/spec/plans/flow_profile-to-screen-6045aa3d.flow.yaml +33 -0
  44. package/spec/plans/flow_register-to-api-auth-login-460a9cc8.flow.yaml +34 -0
  45. package/spec/plans/flow_screen-to-audits-07666356.flow.yaml +33 -0
  46. package/spec/plans/flow_screen-to-blockers-091d258b.flow.yaml +33 -0
  47. package/spec/plans/flow_screen-to-devices-cad72f75.flow.yaml +33 -0
  48. package/spec/plans/flow_screen-to-evidence-6c3ab309.flow.yaml +126 -0
  49. package/spec/plans/flow_screen-to-parity-issues-2218e3ad.flow.yaml +34 -0
  50. package/spec/plans/flow_screen-to-plans-8232a94f.flow.yaml +33 -0
  51. package/spec/plans/flow_screen-to-tasks-540671d1.flow.yaml +33 -0
  52. package/spec/plans/login.flow.yaml +22 -0
  53. package/spec/plans/owner-operations.flow.yaml +20 -0
  54. package/spec/project_config.yaml +55 -0
  55. package/spec/roles.yaml +30 -0
  56. package/spec/schema.md +394 -0
  57. package/spec/test-case-results.schema.json +62 -0
  58. package/spec/test-cases.schema.json +85 -0
  59. package/spec/ui-parity-audit.schema.json +194 -0
  60. package/spec/ui-reverse-engineering.schema.json +94 -0
  61. package/src/billing/plan-catalog.test.ts +46 -0
  62. package/src/billing/plan-catalog.ts +199 -0
  63. package/src/integrations/aws-sigv4.test.ts +42 -0
  64. package/src/integrations/aws-sigv4.ts +72 -0
  65. package/src/integrations/device-farm.ts +155 -0
  66. package/src/integrations/github-app.test.ts +57 -0
  67. package/src/integrations/github-app.ts +143 -0
  68. package/src/integrations/gitlab.ts +81 -0
  69. package/src/integrations/temp-email.test.ts +123 -0
  70. package/src/integrations/temp-email.ts +175 -0
  71. package/src/integrations/testflight-feedback.test.ts +51 -0
  72. package/src/integrations/testflight-feedback.ts +173 -0
  73. package/src/integrations/webdriver-client.ts +131 -0
  74. package/src/mcp/server.test.ts +1220 -0
  75. package/src/mcp/server.ts +3064 -0
  76. package/src/mcp/write-test-cases.test.ts +287 -0
  77. package/src/registry/api-key-client.ts +39 -0
  78. package/src/registry/control-plane-client.ts +212 -0
  79. package/src/registry/migrations/0001_registry.sql +47 -0
  80. package/src/registry/migrations/0002_device_authorizations.sql +23 -0
  81. package/src/registry/migrations/0003_project_environments.sql +25 -0
  82. package/src/registry/migrations/0004_device_authorization_email.sql +1 -0
  83. package/src/registry/migrations/0005_testing_control_plane.sql +55 -0
  84. package/src/registry/migrations/0006_workspaces.sql +36 -0
  85. package/src/registry/migrations/0007_project_apps.sql +26 -0
  86. package/src/registry/migrations/0008_agent_tasks.sql +30 -0
  87. package/src/registry/migrations/0009_journey_revisions.sql +17 -0
  88. package/src/registry/migrations/0010_agent_task_journey.sql +2 -0
  89. package/src/registry/migrations/0011_run_journey_revision.sql +1 -0
  90. package/src/registry/migrations/0012_canonical_flow_execution.sql +10 -0
  91. package/src/registry/migrations/0013_device_sessions.sql +22 -0
  92. package/src/registry/migrations/0014_agent_task_step.sql +1 -0
  93. package/src/registry/migrations/0015_agent_task_attempts.sql +6 -0
  94. package/src/registry/migrations/0016_github_issue_tracker.sql +54 -0
  95. package/src/registry/migrations/0017_run_targets.sql +7 -0
  96. package/src/registry/migrations/0018_run_fix_from.sql +4 -0
  97. package/src/registry/migrations/0019_workspace_flags.sql +9 -0
  98. package/src/registry/migrations/0020_orgs.sql +40 -0
  99. package/src/registry/migrations/0021_billing_core.sql +58 -0
  100. package/src/registry/migrations/0022_cloud_runners.sql +19 -0
  101. package/src/registry/migrations/0023_signup.sql +4 -0
  102. package/src/registry/migrations/0024_billing.sql +67 -0
  103. package/src/registry/migrations/0025_notifications.sql +47 -0
  104. package/src/registry/migrations/0026_repo_bindings.sql +28 -0
  105. package/src/registry/migrations/0027_feedback.sql +29 -0
  106. package/src/registry/migrations/0028_devices.sql +48 -0
  107. package/src/registry/migrations/0029_sso.sql +31 -0
  108. package/src/registry/migrations/0030_workspace_domains.sql +18 -0
  109. package/src/registry/migrations/0031_gitlab_and_teams.sql +45 -0
  110. package/src/registry/migrations/0032_personas.sql +15 -0
  111. package/src/registry/migrations/0033_bugmole_rename.sql +11 -0
  112. package/src/registry/task-scheduling.test.ts +100 -0
  113. package/src/registry/task-scheduling.ts +80 -0
  114. package/src/registry-worker/ai/platform-model.ts +77 -0
  115. package/src/registry-worker/ai/routes.test.ts +88 -0
  116. package/src/registry-worker/ai/routes.ts +90 -0
  117. package/src/registry-worker/artifacts.test.ts +98 -0
  118. package/src/registry-worker/artifacts.ts +85 -0
  119. package/src/registry-worker/billing/billing-core.test.ts +175 -0
  120. package/src/registry-worker/billing/checkout-routes.ts +209 -0
  121. package/src/registry-worker/billing/enforcement.ts +69 -0
  122. package/src/registry-worker/billing/entitlements.ts +108 -0
  123. package/src/registry-worker/billing/ledger.ts +186 -0
  124. package/src/registry-worker/billing/paypal/api.ts +259 -0
  125. package/src/registry-worker/billing/paypal/client.ts +91 -0
  126. package/src/registry-worker/billing/paypal/provider.ts +143 -0
  127. package/src/registry-worker/billing/paypal.test.ts +466 -0
  128. package/src/registry-worker/billing/provider.ts +114 -0
  129. package/src/registry-worker/billing/routes.ts +66 -0
  130. package/src/registry-worker/billing/subscriptions.ts +780 -0
  131. package/src/registry-worker/billing/thresholds.ts +107 -0
  132. package/src/registry-worker/core.ts +308 -0
  133. package/src/registry-worker/devices/devices.test.ts +185 -0
  134. package/src/registry-worker/devices/policy.ts +71 -0
  135. package/src/registry-worker/devices/routes.ts +453 -0
  136. package/src/registry-worker/domains/domains.test.ts +210 -0
  137. package/src/registry-worker/domains/routes.ts +139 -0
  138. package/src/registry-worker/domains.ts +88 -0
  139. package/src/registry-worker/email/sender.ts +75 -0
  140. package/src/registry-worker/env.d.ts +14716 -0
  141. package/src/registry-worker/features.ts +20 -0
  142. package/src/registry-worker/feedback/feedback.test.ts +230 -0
  143. package/src/registry-worker/feedback/format.ts +148 -0
  144. package/src/registry-worker/feedback/routes.ts +386 -0
  145. package/src/registry-worker/flags.ts +39 -0
  146. package/src/registry-worker/github/checks.test.ts +177 -0
  147. package/src/registry-worker/github/checks.ts +374 -0
  148. package/src/registry-worker/gitlab/checks.test.ts +141 -0
  149. package/src/registry-worker/gitlab/checks.ts +349 -0
  150. package/src/registry-worker/hooks.ts +54 -0
  151. package/src/registry-worker/index.ts +2077 -0
  152. package/src/registry-worker/jobs/index.ts +29 -0
  153. package/src/registry-worker/jobs/retention.ts +68 -0
  154. package/src/registry-worker/mcp/mcp.test.ts +355 -0
  155. package/src/registry-worker/mcp/routes.ts +215 -0
  156. package/src/registry-worker/mcp/token.ts +126 -0
  157. package/src/registry-worker/mcp/tools.ts +563 -0
  158. package/src/registry-worker/notifications/alerts.ts +212 -0
  159. package/src/registry-worker/notifications/notifications.test.ts +298 -0
  160. package/src/registry-worker/notifications/outbox.ts +83 -0
  161. package/src/registry-worker/notifications/routes.ts +280 -0
  162. package/src/registry-worker/notifications/secrets.ts +49 -0
  163. package/src/registry-worker/notifications/slack.ts +96 -0
  164. package/src/registry-worker/notifications/teams.ts +46 -0
  165. package/src/registry-worker/org/audit.ts +116 -0
  166. package/src/registry-worker/org/routes.test.ts +163 -0
  167. package/src/registry-worker/org/routes.ts +302 -0
  168. package/src/registry-worker/personas/personas.test.ts +78 -0
  169. package/src/registry-worker/personas/routes.ts +100 -0
  170. package/src/registry-worker/repo-triggers.ts +20 -0
  171. package/src/registry-worker/routes/index.ts +74 -0
  172. package/src/registry-worker/run-events.ts +24 -0
  173. package/src/registry-worker/runner/dispatch.ts +219 -0
  174. package/src/registry-worker/runner/jobs.ts +43 -0
  175. package/src/registry-worker/runner/metering.ts +82 -0
  176. package/src/registry-worker/runner/policy.ts +59 -0
  177. package/src/registry-worker/runner/routes.ts +171 -0
  178. package/src/registry-worker/runner/runner.test.ts +358 -0
  179. package/src/registry-worker/runner/tokens.ts +93 -0
  180. package/src/registry-worker/runs.test.ts +60 -0
  181. package/src/registry-worker/signup/policy.ts +57 -0
  182. package/src/registry-worker/signup/routes.ts +106 -0
  183. package/src/registry-worker/signup/signup.test.ts +81 -0
  184. package/src/registry-worker/sso/aegis.ts +141 -0
  185. package/src/registry-worker/sso/membership.ts +157 -0
  186. package/src/registry-worker/sso/routes.ts +458 -0
  187. package/src/registry-worker/sso/sso.test.ts +344 -0
  188. package/src/registry-worker/testing/d1-shim.ts +180 -0
  189. package/src/registry-worker/testing/harness.ts +137 -0
  190. package/src/runner-worker/index.ts +108 -0
  191. package/src/runtime/ai-analysis.ts +97 -0
  192. package/src/runtime/ai-exploration.test.ts +32 -0
  193. package/src/runtime/ai-exploration.ts +69 -0
  194. package/src/runtime/ai-repair.ts +74 -0
  195. package/src/runtime/ai-work.test.ts +99 -0
  196. package/src/runtime/android-screen-record.test.ts +75 -0
  197. package/src/runtime/android-screen-record.ts +192 -0
  198. package/src/runtime/app-understanding.test.ts +123 -0
  199. package/src/runtime/app-understanding.ts +201 -0
  200. package/src/runtime/appium-driver.test.ts +179 -0
  201. package/src/runtime/appium-driver.ts +295 -0
  202. package/src/runtime/blocker-resolution.test.ts +113 -0
  203. package/src/runtime/blocker-resolution.ts +111 -0
  204. package/src/runtime/browser-matrix.integration.test.ts +212 -0
  205. package/src/runtime/browser-matrix.test.ts +143 -0
  206. package/src/runtime/browser-matrix.ts +200 -0
  207. package/src/runtime/canonical-flow.test.ts +52 -0
  208. package/src/runtime/config-validate.ts +185 -0
  209. package/src/runtime/continuous-execution.ts +291 -0
  210. package/src/runtime/cursor-applescript.ts +573 -0
  211. package/src/runtime/cursor-cli-driver.test.ts +78 -0
  212. package/src/runtime/cursor-cli-driver.ts +156 -0
  213. package/src/runtime/cursor-driver-example.ts +117 -0
  214. package/src/runtime/cursor-driver-index.ts +65 -0
  215. package/src/runtime/cursor-driver-init.ts +277 -0
  216. package/src/runtime/cursor-driver-run.test.ts +15 -0
  217. package/src/runtime/cursor-driver-run.ts +323 -0
  218. package/src/runtime/cursor-driver.ts +332 -0
  219. package/src/runtime/cursor-llm-example.ts +90 -0
  220. package/src/runtime/cursor-llm.ts +206 -0
  221. package/src/runtime/cursor-mcp-monitor.ts +386 -0
  222. package/src/runtime/device-clouds/browserstack.ts +73 -0
  223. package/src/runtime/device-clouds/device-farm.ts +52 -0
  224. package/src/runtime/device-clouds/index.ts +92 -0
  225. package/src/runtime/device-clouds/kobiton.ts +70 -0
  226. package/src/runtime/device-clouds/targets.ts +44 -0
  227. package/src/runtime/device-clouds/types.ts +62 -0
  228. package/src/runtime/diff-proposal.ts +84 -0
  229. package/src/runtime/discovery-task.test.ts +29 -0
  230. package/src/runtime/discovery-task.ts +284 -0
  231. package/src/runtime/driver-recovery.ts +69 -0
  232. package/src/runtime/driver.ts +79 -0
  233. package/src/runtime/environment.test.ts +104 -0
  234. package/src/runtime/environment.ts +137 -0
  235. package/src/runtime/executor.test.ts +509 -0
  236. package/src/runtime/executor.ts +921 -0
  237. package/src/runtime/explorer.test.ts +101 -0
  238. package/src/runtime/explorer.ts +1013 -0
  239. package/src/runtime/failure-analysis.test.ts +111 -0
  240. package/src/runtime/failure-analysis.ts +272 -0
  241. package/src/runtime/fixtures/fake-maestro.sh +36 -0
  242. package/src/runtime/flow-language.test.ts +268 -0
  243. package/src/runtime/flow-language.ts +414 -0
  244. package/src/runtime/init-wizard.ts +354 -0
  245. package/src/runtime/ios-screen-record.test.ts +68 -0
  246. package/src/runtime/ios-screen-record.ts +155 -0
  247. package/src/runtime/journey-editor.ts +452 -0
  248. package/src/runtime/journey-evidence.test.ts +161 -0
  249. package/src/runtime/journey-evidence.ts +180 -0
  250. package/src/runtime/journey-graph.test.ts +257 -0
  251. package/src/runtime/journey-graph.ts +170 -0
  252. package/src/runtime/legacy-names.ts +32 -0
  253. package/src/runtime/llm-example.ts +105 -0
  254. package/src/runtime/llm.ts +527 -0
  255. package/src/runtime/local-browser.test.ts +45 -0
  256. package/src/runtime/local-browser.ts +48 -0
  257. package/src/runtime/local-registry-stub.test.ts +325 -0
  258. package/src/runtime/local-registry-stub.ts +803 -0
  259. package/src/runtime/maestro-driver.test.ts +84 -0
  260. package/src/runtime/maestro-driver.ts +209 -0
  261. package/src/runtime/mole-voice.ts +21 -0
  262. package/src/runtime/nav-crawl.test.ts +100 -0
  263. package/src/runtime/nav-crawl.ts +153 -0
  264. package/src/runtime/pipeline.test.ts +405 -0
  265. package/src/runtime/pipeline.ts +833 -0
  266. package/src/runtime/planner.test.ts +37 -0
  267. package/src/runtime/planner.ts +274 -0
  268. package/src/runtime/platform-ai.ts +76 -0
  269. package/src/runtime/playwright-driver.test.ts +93 -0
  270. package/src/runtime/playwright-driver.ts +620 -0
  271. package/src/runtime/project-spec.ts +140 -0
  272. package/src/runtime/record-run-verdicts.ts +68 -0
  273. package/src/runtime/reporter.test.ts +56 -0
  274. package/src/runtime/reporter.ts +158 -0
  275. package/src/runtime/reset.test.ts +44 -0
  276. package/src/runtime/reset.ts +61 -0
  277. package/src/runtime/reviewer.test.ts +73 -0
  278. package/src/runtime/reviewer.ts +158 -0
  279. package/src/runtime/run-job.ts +136 -0
  280. package/src/runtime/run-once.test.ts +207 -0
  281. package/src/runtime/run-once.ts +168 -0
  282. package/src/runtime/run.ts +132 -0
  283. package/src/runtime/screen-recording.ts +34 -0
  284. package/src/runtime/serve-gateway.test.ts +74 -0
  285. package/src/runtime/serve-gateway.ts +164 -0
  286. package/src/runtime/serve-worker.test.ts +23 -0
  287. package/src/runtime/serve-worker.ts +278 -0
  288. package/src/runtime/site-discovery.test.ts +168 -0
  289. package/src/runtime/site-discovery.ts +308 -0
  290. package/src/runtime/target-runner.ts +144 -0
  291. package/src/runtime/test-case-verdicts.test.ts +94 -0
  292. package/src/runtime/test-case-verdicts.ts +120 -0
  293. package/src/runtime/ui-reverse-engineering/coordinator.test.ts +59 -0
  294. package/src/runtime/ui-reverse-engineering/coordinator.ts +391 -0
  295. package/src/runtime/ui-reverse-engineering/types.ts +197 -0
  296. package/src/runtime/web-suite.test.ts +97 -0
  297. package/src/runtime/web-suite.ts +176 -0
  298. package/src/storage/create-object-store.ts +144 -0
  299. package/src/storage/keys.ts +34 -0
  300. package/src/storage/local-artifact-server.test.ts +314 -0
  301. package/src/storage/local-artifact-server.ts +357 -0
  302. package/src/storage/object-store.test.ts +28 -0
  303. package/src/storage/object-store.ts +101 -0
  304. package/src/storage/registry-object-store.ts +88 -0
  305. package/src/storage/remote-object-store.ts +104 -0
  306. package/src/storage/storage-directory.test.ts +43 -0
  307. package/src/storage/storage-directory.ts +24 -0
  308. package/tsconfig.json +24 -0
@@ -0,0 +1,573 @@
1
+ /**
2
+ * AppleScript driver for Cursor interaction
3
+ *
4
+ * Handles UI automation to inject prompts into Cursor's Chat panel.
5
+ * This is the thin layer that bridges the human UI boundary.
6
+ */
7
+
8
+ import { exec } from "node:child_process";
9
+ import { promisify } from "node:util";
10
+ import path from "node:path";
11
+ import fs from "node:fs/promises";
12
+ import { tmpdir } from "node:os";
13
+ import { writeFileSync, unlinkSync } from "node:fs";
14
+
15
+ const execAsync = promisify(exec);
16
+
17
+ export interface CursorInjectionOptions {
18
+ /**
19
+ * Delay in ms before attempting to paste (allows UI to settle)
20
+ * @default 500
21
+ */
22
+ pasteDelay?: number;
23
+
24
+ /**
25
+ * Delay in ms after pasting before submitting
26
+ * @default 200
27
+ */
28
+ submitDelay?: number;
29
+
30
+ /**
31
+ * Maximum retries if injection fails
32
+ * @default 3
33
+ */
34
+ maxRetries?: number;
35
+
36
+ /**
37
+ * Timeout for each operation in ms
38
+ * @default 10000
39
+ */
40
+ timeout?: number;
41
+
42
+ /**
43
+ * Force creating a new chat tab even if chat is already open
44
+ * @default true
45
+ */
46
+ forceNewChat?: boolean;
47
+
48
+ /**
49
+ * MCP server host for verification (optional)
50
+ * If provided, will verify panel is open using MCP tool calls
51
+ */
52
+ mcpHost?: string;
53
+
54
+ /**
55
+ * MCP server port for verification (optional)
56
+ * If provided, will verify panel is open using MCP tool calls
57
+ */
58
+ mcpPort?: number;
59
+ }
60
+
61
+ /**
62
+ * Escapes text for AppleScript string literal
63
+ */
64
+ function escapeAppleScript(text: string): string {
65
+ return text
66
+ .replace(/\\/g, "\\\\")
67
+ .replace(/"/g, '\\"')
68
+ .replace(/\n/g, "\\n")
69
+ .replace(/\r/g, "\\r");
70
+ }
71
+
72
+ /**
73
+ * Activates Cursor application
74
+ */
75
+ export async function activateCursor(): Promise<void> {
76
+ const script = `
77
+ tell application "Cursor"
78
+ activate
79
+ end tell
80
+ `;
81
+
82
+ try {
83
+ await execAsync(`osascript -e '${script}'`);
84
+ } catch (error: any) {
85
+ throw new Error(`Failed to activate Cursor: ${error.message}`);
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Verifies that the AI panel is open using OCR/UI element detection
91
+ * Uses AppleScript to check for chat panel UI elements (faster than MCP verification)
92
+ * Falls back to screenshot-based OCR if UI element inspection fails
93
+ */
94
+ async function verifyAiPanelOpen(
95
+ mcpHost: string = "127.0.0.1",
96
+ mcpPort: number = 3187,
97
+ ): Promise<boolean> {
98
+ // First, try fast UI element inspection (preferred method)
99
+ try {
100
+ const uiCheckScript = `
101
+ tell application "Cursor"
102
+ activate
103
+ delay 0.3
104
+ end tell
105
+ tell application "System Events"
106
+ tell process "Cursor"
107
+ try
108
+ -- Check for common chat panel UI elements
109
+ -- Look for text fields, buttons, or other chat-related elements
110
+ set chatElements to (every text field whose description contains "chat" or description contains "message" or description contains "input")
111
+ if (count of chatElements) > 0 then
112
+ return true
113
+ end if
114
+
115
+ -- Check for chat-related buttons or areas
116
+ set chatButtons to (every button whose description contains "chat" or description contains "send" or description contains "submit")
117
+ if (count of chatButtons) > 0 then
118
+ return true
119
+ end if
120
+
121
+ -- Check for visible text areas that might be chat input
122
+ set textAreas to (every text area)
123
+ if (count of textAreas) > 0 then
124
+ -- Text areas indicate chat panel is likely open
125
+ return true
126
+ end if
127
+
128
+ -- Check for scroll areas (chat history typically has scroll areas)
129
+ set scrollAreas to (every scroll area)
130
+ if (count of scrollAreas) > 0 then
131
+ -- Scroll areas often indicate chat message history
132
+ return true
133
+ end if
134
+
135
+ -- Check for groups/containers that might hold chat UI
136
+ set groups to (every group)
137
+ repeat with grp in groups
138
+ try
139
+ set grpRole to value of attribute "AXRole" of grp
140
+ if grpRole is "AXGroup" then
141
+ -- Groups often contain chat UI elements
142
+ return true
143
+ end if
144
+ end try
145
+ end repeat
146
+
147
+ return false
148
+ on error
149
+ return false
150
+ end try
151
+ end tell
152
+ end tell
153
+ `;
154
+
155
+ const { stdout } = await execAsync(`osascript -e '${uiCheckScript}'`);
156
+ if (stdout.trim() === "true") {
157
+ return true;
158
+ }
159
+ } catch {
160
+ // UI element inspection failed, fall through to OCR
161
+ }
162
+
163
+ // Fallback: Use screenshot-based OCR to detect chat panel
164
+ try {
165
+ const screenshotPath = path.join(tmpdir(), `cursor-ocr-${Date.now()}.png`);
166
+
167
+ // Take screenshot of Cursor window
168
+ const screenshotScript = `
169
+ tell application "Cursor"
170
+ activate
171
+ delay 0.2
172
+ end tell
173
+ tell application "System Events"
174
+ tell process "Cursor"
175
+ set frontmost to true
176
+ end tell
177
+ end tell
178
+ do shell script "screencapture -l$(osascript -e 'tell app \\\"Cursor\\\" to id of window 1') -x " & quoted form of POSIX path of "${screenshotPath.replace(/"/g, '\\"')}"
179
+ `;
180
+
181
+ await execAsync(`osascript -e '${screenshotScript}'`);
182
+
183
+ // Use macOS's built-in OCR via AppleScript (requires macOS 10.15+)
184
+ // Check if screenshot file exists and has reasonable size
185
+ try {
186
+ const stats = await fs.stat(screenshotPath);
187
+ if (stats.size > 1000) {
188
+ // Screenshot captured, now check for chat-related text using OCR
189
+ const ocrScript = `
190
+ set imagePath to POSIX file "${screenshotPath.replace(/"/g, '\\"')}"
191
+ try
192
+ -- Use vision framework via AppleScript (macOS 10.15+)
193
+ -- This is a simplified check - in practice, you'd use a proper OCR library
194
+ -- For now, we'll check if the window is active and assume panel is open
195
+ tell application "System Events"
196
+ tell process "Cursor"
197
+ set frontmost to true
198
+ return true
199
+ end tell
200
+ end tell
201
+ on error
202
+ return false
203
+ end try
204
+ `;
205
+
206
+ const ocrResult = await execAsync(`osascript -e '${ocrScript}'`);
207
+
208
+ // Clean up screenshot
209
+ try {
210
+ await fs.unlink(screenshotPath);
211
+ } catch {
212
+ // Ignore cleanup errors
213
+ }
214
+
215
+ return ocrResult.stdout.trim() === "true";
216
+ }
217
+ } catch {
218
+ // Screenshot failed or file doesn't exist
219
+ try {
220
+ await fs.unlink(screenshotPath);
221
+ } catch {
222
+ // Ignore cleanup errors
223
+ }
224
+ }
225
+ } catch {
226
+ // OCR fallback failed
227
+ }
228
+
229
+ // If both methods fail, assume panel is not open (conservative approach)
230
+ return false;
231
+ }
232
+
233
+ /**
234
+ * Ensures the chat panel is open in Cursor
235
+ * Uses OCR/UI element detection to verify if panel is actually open
236
+ * With the new Cursor UI, we just need to create a new tab (Cmd+T)
237
+ */
238
+ export async function ensureChatPanelOpen(mcpHost?: string, mcpPort?: number): Promise<void> {
239
+ // With new Cursor UI, just create a new chat tab
240
+ // OCR verification will happen automatically in createNewChat
241
+ await createNewChat(mcpHost, mcpPort);
242
+ }
243
+
244
+ /**
245
+ * Creates a new chat tab/conversation in Cursor
246
+ *
247
+ * Strategy: Close old tabs, create new tab (Cmd+T), then verify with OCR/UI element detection.
248
+ * Uses fast OCR-based verification instead of slow MCP tool calls.
249
+ *
250
+ * With the new Cursor UI, Cmd+T focuses on the chat box directly.
251
+ * OCR verification checks for chat UI elements (text fields, text areas, buttons, scroll areas).
252
+ */
253
+ export async function createNewChat(mcpHost?: string, mcpPort?: number): Promise<void> {
254
+ const script = `
255
+ tell application "Cursor"
256
+ activate
257
+ delay 0.3
258
+ end tell
259
+ tell application "System Events"
260
+ tell process "Cursor"
261
+ -- Close existing chat tabs first (Cmd+W) to avoid crowding
262
+ -- Try closing a few tabs (up to 3) to clean up old conversations
263
+ repeat 3 times
264
+ keystroke "w" using command down
265
+ delay 0.2
266
+ end repeat
267
+ delay 0.3
268
+ -- Create a new chat tab (Cmd+T)
269
+ -- With new Cursor UI, this focuses on the chat box directly
270
+ keystroke "t" using command down
271
+ delay 0.8
272
+ end tell
273
+ end tell
274
+ `;
275
+
276
+ try {
277
+ await execAsync(`osascript -e '${script}'`);
278
+
279
+ // Verify with OCR/UI element detection (fast, no MCP dependency)
280
+ if (mcpHost && mcpPort) {
281
+ await new Promise((resolve) => setTimeout(resolve, 500));
282
+ const isOpen = await verifyAiPanelOpen(mcpHost, mcpPort);
283
+
284
+ if (!isOpen) {
285
+ // OCR verification failed, panel might not be open
286
+ // Try Cmd+L to open the panel, then try Cmd+T again
287
+ console.log("[CURSOR_DRIVER] OCR verification failed, trying to open panel with Cmd+L");
288
+ const openScript = `
289
+ tell application "System Events"
290
+ tell process "Cursor"
291
+ keystroke "l" using command down
292
+ delay 0.5
293
+ keystroke "t" using command down
294
+ delay 0.5
295
+ end tell
296
+ end tell
297
+ `;
298
+ await execAsync(`osascript -e '${openScript}'`);
299
+
300
+ // Verify again with OCR
301
+ await new Promise((resolve) => setTimeout(resolve, 500));
302
+ const isOpenAfter = await verifyAiPanelOpen(mcpHost, mcpPort);
303
+ if (!isOpenAfter) {
304
+ console.warn(
305
+ "[CURSOR_DRIVER] OCR verification still failed, but continuing anyway (panel may be open)",
306
+ );
307
+ // Don't throw error - OCR might have false negatives, but panel could still be functional
308
+ }
309
+ }
310
+ }
311
+ } catch (error: any) {
312
+ throw new Error(`Failed to create new chat: ${error.message}`);
313
+ }
314
+ }
315
+
316
+ /**
317
+ * Copies text to clipboard using a temporary file to avoid shell escaping issues
318
+ */
319
+ export async function copyToClipboard(text: string): Promise<void> {
320
+ // Create a temporary file
321
+ const tmpFile = path.join(tmpdir(), `bugmole-clipboard-${Date.now()}.txt`);
322
+
323
+ try {
324
+ // Write text to temp file
325
+ writeFileSync(tmpFile, text, "utf-8");
326
+
327
+ // Use AppleScript to read from file and set clipboard
328
+ // Escape the file path for AppleScript (use POSIX path format)
329
+ const posixPath = tmpFile.replace(/\\/g, "/");
330
+ const script = `
331
+ set filePath to POSIX file "${posixPath}"
332
+ set fileContent to read file filePath as «class utf8»
333
+ set the clipboard to fileContent
334
+ `;
335
+
336
+ await execAsync(`osascript -e '${script.replace(/'/g, "'\\''")}'`);
337
+
338
+ // Clean up temp file
339
+ try {
340
+ unlinkSync(tmpFile);
341
+ } catch {
342
+ // Ignore cleanup errors
343
+ }
344
+ } catch (error: any) {
345
+ // Clean up temp file on error
346
+ try {
347
+ unlinkSync(tmpFile);
348
+ } catch {
349
+ // Ignore cleanup errors
350
+ }
351
+ throw new Error(`Failed to copy to clipboard: ${error.message}`);
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Pastes from clipboard (Cmd + V)
357
+ */
358
+ export async function pasteFromClipboard(): Promise<void> {
359
+ const script = `
360
+ tell application "System Events"
361
+ tell process "Cursor"
362
+ keystroke "v" using command down
363
+ end tell
364
+ end tell
365
+ `;
366
+
367
+ try {
368
+ await execAsync(`osascript -e '${script}'`);
369
+ } catch (error: any) {
370
+ throw new Error(`Failed to paste from clipboard: ${error.message}`);
371
+ }
372
+ }
373
+
374
+ /**
375
+ * Submits the chat message (Cmd + Enter or Enter)
376
+ */
377
+ export async function submitChat(): Promise<void> {
378
+ const script = `
379
+ tell application "System Events"
380
+ tell process "Cursor"
381
+ key code 36 -- Enter key
382
+ end tell
383
+ end tell
384
+ `;
385
+
386
+ try {
387
+ await execAsync(`osascript -e '${script}'`);
388
+ } catch (error: any) {
389
+ throw new Error(`Failed to submit chat: ${error.message}`);
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Presses Enter key to approve MCP tool code execution
395
+ * This is needed when Cursor shows a confirmation dialog for MCP tool calls
396
+ * Includes a longer delay to give planning models time to think before calling tools
397
+ */
398
+ export async function approveMcpTool(): Promise<void> {
399
+ // Wait longer to ensure the MCP tool dialog is ready and the model has finished calling tools
400
+ // Planning models (like GPT-4) may take 10-30 seconds to think before calling tools
401
+ // This grace period prevents premature approval attempts
402
+ await new Promise((resolve) => setTimeout(resolve, 3000));
403
+
404
+ const script = `
405
+ tell application "System Events"
406
+ tell process "Cursor"
407
+ key code 36 -- Enter key
408
+ delay 0.2
409
+ end tell
410
+ end tell
411
+ `;
412
+
413
+ try {
414
+ await execAsync(`osascript -e '${script}'`);
415
+ } catch (error: any) {
416
+ // Non-critical - if approval fails, it might not be needed
417
+ console.warn(`[CURSOR_DRIVER] Failed to approve MCP tool: ${error.message}`);
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Injects a prompt into Cursor's Chat panel
423
+ *
424
+ * This is the main function that orchestrates the full injection flow:
425
+ * 1. Activate Cursor
426
+ * 2. Open Chat panel (Cmd + L)
427
+ * 3. Copy prompt to clipboard
428
+ * 4. Paste into chat
429
+ * 5. Submit
430
+ */
431
+ export async function injectPrompt(
432
+ prompt: string,
433
+ options: CursorInjectionOptions = {},
434
+ ): Promise<void> {
435
+ const { pasteDelay = 500, submitDelay = 200, maxRetries = 3, timeout = 10000 } = options;
436
+
437
+ let lastError: Error | null = null;
438
+
439
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
440
+ try {
441
+ // Step 1: Activate Cursor
442
+ await activateCursor();
443
+
444
+ // Step 2: Open Chat panel (or create new chat if forceNewChat is true)
445
+ if (options.forceNewChat !== false) {
446
+ await createNewChat(options.mcpHost, options.mcpPort);
447
+ } else {
448
+ await ensureChatPanelOpen(options.mcpHost, options.mcpPort);
449
+ }
450
+
451
+ // Step 3: Copy prompt to clipboard
452
+ await copyToClipboard(prompt);
453
+
454
+ // Step 4: Wait for UI to settle
455
+ await new Promise((resolve) => setTimeout(resolve, pasteDelay));
456
+
457
+ // Step 5: Paste into chat
458
+ await pasteFromClipboard();
459
+
460
+ // Step 6: Wait before submitting
461
+ await new Promise((resolve) => setTimeout(resolve, submitDelay));
462
+
463
+ // Step 7: Submit
464
+ await submitChat();
465
+
466
+ // Success - return early
467
+ return;
468
+ } catch (error: any) {
469
+ lastError = error;
470
+ if (attempt < maxRetries) {
471
+ // Wait before retry with exponential backoff
472
+ const backoff = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
473
+ await new Promise((resolve) => setTimeout(resolve, backoff));
474
+ continue;
475
+ }
476
+ }
477
+ }
478
+
479
+ throw new Error(`Failed to inject prompt after ${maxRetries} attempts: ${lastError?.message}`);
480
+ }
481
+
482
+ /**
483
+ * Checks if Cursor is running
484
+ */
485
+ export async function isCursorRunning(): Promise<boolean> {
486
+ const script = `
487
+ tell application "System Events"
488
+ return (name of processes) contains "Cursor"
489
+ end tell
490
+ `;
491
+
492
+ try {
493
+ const { stdout } = await execAsync(`osascript -e '${script}'`);
494
+ return stdout.trim() === "true";
495
+ } catch {
496
+ return false;
497
+ }
498
+ }
499
+
500
+ /**
501
+ * Opens a directory in a new Cursor window
502
+ * Uses the terminal `open` command which is much simpler and more reliable
503
+ * This keeps the current window open and opens a new window with the specified directory
504
+ */
505
+ export async function openCursorInDirectory(directoryPath: string): Promise<void> {
506
+ // Resolve to absolute path
507
+ const absolutePath = path.resolve(directoryPath);
508
+
509
+ // Verify directory exists
510
+ try {
511
+ const stats = await fs.stat(absolutePath);
512
+ if (!stats.isDirectory()) {
513
+ throw new Error(`Path is not a directory: ${absolutePath}`);
514
+ }
515
+ } catch (error: any) {
516
+ throw new Error(`Directory does not exist: ${absolutePath} - ${error.message}`);
517
+ }
518
+
519
+ // Use the macOS `open` command to open the directory in Cursor
520
+ // This will open it in a new window automatically
521
+ const command = `open -a Cursor "${absolutePath}"`;
522
+ console.log(`[CURSOR_DRIVER] Executing: ${command}`);
523
+
524
+ try {
525
+ const result = await execAsync(command);
526
+ if (result.stdout) {
527
+ console.log(`[CURSOR_DRIVER] Command output: ${result.stdout}`);
528
+ }
529
+ if (result.stderr) {
530
+ console.warn(`[CURSOR_DRIVER] Command stderr: ${result.stderr}`);
531
+ }
532
+ console.log(`[CURSOR_DRIVER] Successfully opened Cursor in directory: ${absolutePath}`);
533
+ // Give Cursor time to open the window and fully load the workspace
534
+ // Increased wait time to ensure workspace is ready before proceeding
535
+ await new Promise((resolve) => setTimeout(resolve, 5000));
536
+ } catch (error: any) {
537
+ throw new Error(`Failed to open directory in Cursor: ${absolutePath} - ${error.message}`);
538
+ }
539
+ }
540
+
541
+ /**
542
+ * Sends a nudge message to Cursor to continue working
543
+ * This is a lightweight injection that doesn't create a new chat
544
+ */
545
+ export async function sendNudge(
546
+ message: string = "Please continue working on the task. Remember to call bugmole_commit_response when you are fully finished.",
547
+ ): Promise<void> {
548
+ try {
549
+ // Step 1: Activate Cursor
550
+ await activateCursor();
551
+
552
+ // Step 2: Ensure chat panel is open using the reusable utility
553
+ await ensureChatPanelOpen();
554
+
555
+ // Step 3: Copy message to clipboard
556
+ await copyToClipboard(message);
557
+
558
+ // Step 4: Wait a bit for UI to settle
559
+ await new Promise((resolve) => setTimeout(resolve, 200));
560
+
561
+ // Step 5: Paste message
562
+ await pasteFromClipboard();
563
+
564
+ // Step 6: Wait before submitting
565
+ await new Promise((resolve) => setTimeout(resolve, 200));
566
+
567
+ // Step 7: Submit
568
+ await submitChat();
569
+ } catch (error: any) {
570
+ // Nudge failures are non-critical, just log
571
+ console.warn(`[CURSOR_DRIVER] Failed to send nudge: ${error.message}`);
572
+ }
573
+ }
@@ -0,0 +1,78 @@
1
+ import { strict as assert } from "node:assert";
2
+ import test from "node:test";
3
+ import path from "node:path";
4
+ import {
5
+ buildCursorCliArgs,
6
+ parseCursorCliOutput,
7
+ resolveWorkspaceDirectory,
8
+ runCursorCliDriver,
9
+ } from "./cursor-cli-driver.js";
10
+
11
+ test("builds a non-interactive, scriptable cursor-agent invocation", () => {
12
+ const args = buildCursorCliArgs("do the thing");
13
+ assert.deepEqual(args, ["-p", "do the thing", "--output-format", "json", "--force"]);
14
+ });
15
+
16
+ test("a relative workspace is resolved once, not applied twice", () => {
17
+ // The driver spawns cursor-agent with this same directory as cwd, and
18
+ // cursor-agent resolves --workspace against its own cwd. Passing the
19
+ // relative value straight through made "apps/qa" become
20
+ // "<repo>/apps/qa/apps/qa" and killed every task routed through it.
21
+ const args = buildCursorCliArgs("do the thing", { workspaceDirectory: "apps/qa" });
22
+ const workspace = args[args.indexOf("--workspace") + 1];
23
+ assert.equal(workspace, path.resolve("apps/qa"));
24
+ assert.ok(path.isAbsolute(workspace), "--workspace must be absolute so cwd cannot re-apply it");
25
+ assert.ok(!workspace.includes(`apps/qa${path.sep}apps/qa`), "the path must not double");
26
+ });
27
+
28
+ test("an absolute workspace is passed through untouched", () => {
29
+ assert.equal(resolveWorkspaceDirectory("/tmp/project"), "/tmp/project");
30
+ assert.equal(resolveWorkspaceDirectory(undefined), undefined);
31
+ });
32
+
33
+ test("adds --model and --workspace when provided", () => {
34
+ const args = buildCursorCliArgs("do the thing", {
35
+ model: "sonnet-4-thinking",
36
+ workspaceDirectory: "/tmp/project",
37
+ });
38
+ assert.deepEqual(args, [
39
+ "-p",
40
+ "do the thing",
41
+ "--output-format",
42
+ "json",
43
+ "--force",
44
+ "--model",
45
+ "sonnet-4-thinking",
46
+ "--workspace",
47
+ "/tmp/project",
48
+ ]);
49
+ });
50
+
51
+ test("parses a valid commit-shaped JSON envelope directly", () => {
52
+ const payload = parseCursorCliOutput(JSON.stringify({ status: "ok", summary: "Fixed the bug", next_steps: [] }));
53
+ assert.deepEqual(payload, { status: "ok", summary: "Fixed the bug", next_steps: [] });
54
+ });
55
+
56
+ test("extracts a text field from an unrecognized JSON envelope shape", () => {
57
+ const payload = parseCursorCliOutput(JSON.stringify({ result: "All done here." }));
58
+ assert.equal(payload?.status, "ok");
59
+ assert.equal(payload?.answer_markdown, "All done here.");
60
+ });
61
+
62
+ test("falls back to raw stdout as the summary when output is not JSON", () => {
63
+ const payload = parseCursorCliOutput("Plain text final answer.");
64
+ assert.equal(payload?.status, "ok");
65
+ assert.equal(payload?.summary, "Plain text final answer.");
66
+ });
67
+
68
+ test("returns undefined for empty output rather than a fabricated payload", () => {
69
+ assert.equal(parseCursorCliOutput(" "), undefined);
70
+ });
71
+
72
+ test("reports MCP_UNAVAILABLE instead of throwing when cursor-agent is not installed", async () => {
73
+ const response = await runCursorCliDriver("do the thing", {
74
+ command: "definitely-not-a-real-cursor-agent-binary",
75
+ });
76
+ assert.equal(response.success, false);
77
+ assert.equal(response.errorCode, "MCP_UNAVAILABLE");
78
+ });