@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.
- package/.bugmole.env.example +20 -0
- package/LICENSE +7 -0
- package/README.md +293 -0
- package/TESTING.md +117 -0
- package/bugmole.config.yaml +39 -0
- package/package.json +85 -0
- package/scripts/billing/paypal-setup.mjs +121 -0
- package/scripts/bugmole-continue.ts +318 -0
- package/scripts/bugmole-init.ts +188 -0
- package/scripts/bugmole.cjs +17 -0
- package/scripts/bugmole.test.ts +344 -0
- package/scripts/bugmole.ts +657 -0
- package/scripts/ensure-maestro.cjs +79 -0
- package/scripts/ios-tunnel-keeper.sh +45 -0
- package/scripts/ios-wda-keeper.sh +66 -0
- package/scripts/sync-plan-catalog.d.mts +3 -0
- package/scripts/sync-plan-catalog.mjs +16 -0
- package/scripts/ui-parity-diff.py +65 -0
- package/scripts/ui-parity-requirements.txt +1 -0
- package/scripts/verify-manage-to-plans.mts +194 -0
- package/spec/app-ui-audit.schema.json +176 -0
- package/spec/blockers.yaml +79 -0
- package/spec/bugs.index.json +42 -0
- package/spec/design-dna.schema.json +38 -0
- package/spec/domain_rules.yaml +24 -0
- package/spec/flows/flow_manage-to-plans-64c3c61b.yaml +8 -0
- package/spec/flows/flow_screen-to-evidence-6c3ab309.yaml +7 -0
- package/spec/journey_graph.yaml +126 -0
- package/spec/journeys.graph.json +2618 -0
- package/spec/plans/dashboard-smoke.flow.yaml +20 -0
- package/spec/plans/example.flow.yaml +99 -0
- package/spec/plans/flow_api-keys-to-logout-7396cd5d.flow.yaml +33 -0
- package/spec/plans/flow_api-keys-to-screen-157e10a6.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-audits-176c1eb8.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-blockers-633282c8.flow.yaml +112 -0
- package/spec/plans/flow_manage-to-devices-ad57e202.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-logout-86c54656.flow.yaml +33 -0
- package/spec/plans/flow_manage-to-manage-resource-action-6325f99a.flow.yaml +185 -0
- package/spec/plans/flow_manage-to-parity-issues-01fa2579.flow.yaml +34 -0
- package/spec/plans/flow_manage-to-plans-64c3c61b.flow.yaml +123 -0
- package/spec/plans/flow_manage-to-screen-6857f915.flow.yaml +106 -0
- package/spec/plans/flow_manage-to-tasks-c5791c95.flow.yaml +33 -0
- package/spec/plans/flow_profile-to-screen-6045aa3d.flow.yaml +33 -0
- package/spec/plans/flow_register-to-api-auth-login-460a9cc8.flow.yaml +34 -0
- package/spec/plans/flow_screen-to-audits-07666356.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-blockers-091d258b.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-devices-cad72f75.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-evidence-6c3ab309.flow.yaml +126 -0
- package/spec/plans/flow_screen-to-parity-issues-2218e3ad.flow.yaml +34 -0
- package/spec/plans/flow_screen-to-plans-8232a94f.flow.yaml +33 -0
- package/spec/plans/flow_screen-to-tasks-540671d1.flow.yaml +33 -0
- package/spec/plans/login.flow.yaml +22 -0
- package/spec/plans/owner-operations.flow.yaml +20 -0
- package/spec/project_config.yaml +55 -0
- package/spec/roles.yaml +30 -0
- package/spec/schema.md +394 -0
- package/spec/test-case-results.schema.json +62 -0
- package/spec/test-cases.schema.json +85 -0
- package/spec/ui-parity-audit.schema.json +194 -0
- package/spec/ui-reverse-engineering.schema.json +94 -0
- package/src/billing/plan-catalog.test.ts +46 -0
- package/src/billing/plan-catalog.ts +199 -0
- package/src/integrations/aws-sigv4.test.ts +42 -0
- package/src/integrations/aws-sigv4.ts +72 -0
- package/src/integrations/device-farm.ts +155 -0
- package/src/integrations/github-app.test.ts +57 -0
- package/src/integrations/github-app.ts +143 -0
- package/src/integrations/gitlab.ts +81 -0
- package/src/integrations/temp-email.test.ts +123 -0
- package/src/integrations/temp-email.ts +175 -0
- package/src/integrations/testflight-feedback.test.ts +51 -0
- package/src/integrations/testflight-feedback.ts +173 -0
- package/src/integrations/webdriver-client.ts +131 -0
- package/src/mcp/server.test.ts +1220 -0
- package/src/mcp/server.ts +3064 -0
- package/src/mcp/write-test-cases.test.ts +287 -0
- package/src/registry/api-key-client.ts +39 -0
- package/src/registry/control-plane-client.ts +212 -0
- package/src/registry/migrations/0001_registry.sql +47 -0
- package/src/registry/migrations/0002_device_authorizations.sql +23 -0
- package/src/registry/migrations/0003_project_environments.sql +25 -0
- package/src/registry/migrations/0004_device_authorization_email.sql +1 -0
- package/src/registry/migrations/0005_testing_control_plane.sql +55 -0
- package/src/registry/migrations/0006_workspaces.sql +36 -0
- package/src/registry/migrations/0007_project_apps.sql +26 -0
- package/src/registry/migrations/0008_agent_tasks.sql +30 -0
- package/src/registry/migrations/0009_journey_revisions.sql +17 -0
- package/src/registry/migrations/0010_agent_task_journey.sql +2 -0
- package/src/registry/migrations/0011_run_journey_revision.sql +1 -0
- package/src/registry/migrations/0012_canonical_flow_execution.sql +10 -0
- package/src/registry/migrations/0013_device_sessions.sql +22 -0
- package/src/registry/migrations/0014_agent_task_step.sql +1 -0
- package/src/registry/migrations/0015_agent_task_attempts.sql +6 -0
- package/src/registry/migrations/0016_github_issue_tracker.sql +54 -0
- package/src/registry/migrations/0017_run_targets.sql +7 -0
- package/src/registry/migrations/0018_run_fix_from.sql +4 -0
- package/src/registry/migrations/0019_workspace_flags.sql +9 -0
- package/src/registry/migrations/0020_orgs.sql +40 -0
- package/src/registry/migrations/0021_billing_core.sql +58 -0
- package/src/registry/migrations/0022_cloud_runners.sql +19 -0
- package/src/registry/migrations/0023_signup.sql +4 -0
- package/src/registry/migrations/0024_billing.sql +67 -0
- package/src/registry/migrations/0025_notifications.sql +47 -0
- package/src/registry/migrations/0026_repo_bindings.sql +28 -0
- package/src/registry/migrations/0027_feedback.sql +29 -0
- package/src/registry/migrations/0028_devices.sql +48 -0
- package/src/registry/migrations/0029_sso.sql +31 -0
- package/src/registry/migrations/0030_workspace_domains.sql +18 -0
- package/src/registry/migrations/0031_gitlab_and_teams.sql +45 -0
- package/src/registry/migrations/0032_personas.sql +15 -0
- package/src/registry/migrations/0033_bugmole_rename.sql +11 -0
- package/src/registry/task-scheduling.test.ts +100 -0
- package/src/registry/task-scheduling.ts +80 -0
- package/src/registry-worker/ai/platform-model.ts +77 -0
- package/src/registry-worker/ai/routes.test.ts +88 -0
- package/src/registry-worker/ai/routes.ts +90 -0
- package/src/registry-worker/artifacts.test.ts +98 -0
- package/src/registry-worker/artifacts.ts +85 -0
- package/src/registry-worker/billing/billing-core.test.ts +175 -0
- package/src/registry-worker/billing/checkout-routes.ts +209 -0
- package/src/registry-worker/billing/enforcement.ts +69 -0
- package/src/registry-worker/billing/entitlements.ts +108 -0
- package/src/registry-worker/billing/ledger.ts +186 -0
- package/src/registry-worker/billing/paypal/api.ts +259 -0
- package/src/registry-worker/billing/paypal/client.ts +91 -0
- package/src/registry-worker/billing/paypal/provider.ts +143 -0
- package/src/registry-worker/billing/paypal.test.ts +466 -0
- package/src/registry-worker/billing/provider.ts +114 -0
- package/src/registry-worker/billing/routes.ts +66 -0
- package/src/registry-worker/billing/subscriptions.ts +780 -0
- package/src/registry-worker/billing/thresholds.ts +107 -0
- package/src/registry-worker/core.ts +308 -0
- package/src/registry-worker/devices/devices.test.ts +185 -0
- package/src/registry-worker/devices/policy.ts +71 -0
- package/src/registry-worker/devices/routes.ts +453 -0
- package/src/registry-worker/domains/domains.test.ts +210 -0
- package/src/registry-worker/domains/routes.ts +139 -0
- package/src/registry-worker/domains.ts +88 -0
- package/src/registry-worker/email/sender.ts +75 -0
- package/src/registry-worker/env.d.ts +14716 -0
- package/src/registry-worker/features.ts +20 -0
- package/src/registry-worker/feedback/feedback.test.ts +230 -0
- package/src/registry-worker/feedback/format.ts +148 -0
- package/src/registry-worker/feedback/routes.ts +386 -0
- package/src/registry-worker/flags.ts +39 -0
- package/src/registry-worker/github/checks.test.ts +177 -0
- package/src/registry-worker/github/checks.ts +374 -0
- package/src/registry-worker/gitlab/checks.test.ts +141 -0
- package/src/registry-worker/gitlab/checks.ts +349 -0
- package/src/registry-worker/hooks.ts +54 -0
- package/src/registry-worker/index.ts +2077 -0
- package/src/registry-worker/jobs/index.ts +29 -0
- package/src/registry-worker/jobs/retention.ts +68 -0
- package/src/registry-worker/mcp/mcp.test.ts +355 -0
- package/src/registry-worker/mcp/routes.ts +215 -0
- package/src/registry-worker/mcp/token.ts +126 -0
- package/src/registry-worker/mcp/tools.ts +563 -0
- package/src/registry-worker/notifications/alerts.ts +212 -0
- package/src/registry-worker/notifications/notifications.test.ts +298 -0
- package/src/registry-worker/notifications/outbox.ts +83 -0
- package/src/registry-worker/notifications/routes.ts +280 -0
- package/src/registry-worker/notifications/secrets.ts +49 -0
- package/src/registry-worker/notifications/slack.ts +96 -0
- package/src/registry-worker/notifications/teams.ts +46 -0
- package/src/registry-worker/org/audit.ts +116 -0
- package/src/registry-worker/org/routes.test.ts +163 -0
- package/src/registry-worker/org/routes.ts +302 -0
- package/src/registry-worker/personas/personas.test.ts +78 -0
- package/src/registry-worker/personas/routes.ts +100 -0
- package/src/registry-worker/repo-triggers.ts +20 -0
- package/src/registry-worker/routes/index.ts +74 -0
- package/src/registry-worker/run-events.ts +24 -0
- package/src/registry-worker/runner/dispatch.ts +219 -0
- package/src/registry-worker/runner/jobs.ts +43 -0
- package/src/registry-worker/runner/metering.ts +82 -0
- package/src/registry-worker/runner/policy.ts +59 -0
- package/src/registry-worker/runner/routes.ts +171 -0
- package/src/registry-worker/runner/runner.test.ts +358 -0
- package/src/registry-worker/runner/tokens.ts +93 -0
- package/src/registry-worker/runs.test.ts +60 -0
- package/src/registry-worker/signup/policy.ts +57 -0
- package/src/registry-worker/signup/routes.ts +106 -0
- package/src/registry-worker/signup/signup.test.ts +81 -0
- package/src/registry-worker/sso/aegis.ts +141 -0
- package/src/registry-worker/sso/membership.ts +157 -0
- package/src/registry-worker/sso/routes.ts +458 -0
- package/src/registry-worker/sso/sso.test.ts +344 -0
- package/src/registry-worker/testing/d1-shim.ts +180 -0
- package/src/registry-worker/testing/harness.ts +137 -0
- package/src/runner-worker/index.ts +108 -0
- package/src/runtime/ai-analysis.ts +97 -0
- package/src/runtime/ai-exploration.test.ts +32 -0
- package/src/runtime/ai-exploration.ts +69 -0
- package/src/runtime/ai-repair.ts +74 -0
- package/src/runtime/ai-work.test.ts +99 -0
- package/src/runtime/android-screen-record.test.ts +75 -0
- package/src/runtime/android-screen-record.ts +192 -0
- package/src/runtime/app-understanding.test.ts +123 -0
- package/src/runtime/app-understanding.ts +201 -0
- package/src/runtime/appium-driver.test.ts +179 -0
- package/src/runtime/appium-driver.ts +295 -0
- package/src/runtime/blocker-resolution.test.ts +113 -0
- package/src/runtime/blocker-resolution.ts +111 -0
- package/src/runtime/browser-matrix.integration.test.ts +212 -0
- package/src/runtime/browser-matrix.test.ts +143 -0
- package/src/runtime/browser-matrix.ts +200 -0
- package/src/runtime/canonical-flow.test.ts +52 -0
- package/src/runtime/config-validate.ts +185 -0
- package/src/runtime/continuous-execution.ts +291 -0
- package/src/runtime/cursor-applescript.ts +573 -0
- package/src/runtime/cursor-cli-driver.test.ts +78 -0
- package/src/runtime/cursor-cli-driver.ts +156 -0
- package/src/runtime/cursor-driver-example.ts +117 -0
- package/src/runtime/cursor-driver-index.ts +65 -0
- package/src/runtime/cursor-driver-init.ts +277 -0
- package/src/runtime/cursor-driver-run.test.ts +15 -0
- package/src/runtime/cursor-driver-run.ts +323 -0
- package/src/runtime/cursor-driver.ts +332 -0
- package/src/runtime/cursor-llm-example.ts +90 -0
- package/src/runtime/cursor-llm.ts +206 -0
- package/src/runtime/cursor-mcp-monitor.ts +386 -0
- package/src/runtime/device-clouds/browserstack.ts +73 -0
- package/src/runtime/device-clouds/device-farm.ts +52 -0
- package/src/runtime/device-clouds/index.ts +92 -0
- package/src/runtime/device-clouds/kobiton.ts +70 -0
- package/src/runtime/device-clouds/targets.ts +44 -0
- package/src/runtime/device-clouds/types.ts +62 -0
- package/src/runtime/diff-proposal.ts +84 -0
- package/src/runtime/discovery-task.test.ts +29 -0
- package/src/runtime/discovery-task.ts +284 -0
- package/src/runtime/driver-recovery.ts +69 -0
- package/src/runtime/driver.ts +79 -0
- package/src/runtime/environment.test.ts +104 -0
- package/src/runtime/environment.ts +137 -0
- package/src/runtime/executor.test.ts +509 -0
- package/src/runtime/executor.ts +921 -0
- package/src/runtime/explorer.test.ts +101 -0
- package/src/runtime/explorer.ts +1013 -0
- package/src/runtime/failure-analysis.test.ts +111 -0
- package/src/runtime/failure-analysis.ts +272 -0
- package/src/runtime/fixtures/fake-maestro.sh +36 -0
- package/src/runtime/flow-language.test.ts +268 -0
- package/src/runtime/flow-language.ts +414 -0
- package/src/runtime/init-wizard.ts +354 -0
- package/src/runtime/ios-screen-record.test.ts +68 -0
- package/src/runtime/ios-screen-record.ts +155 -0
- package/src/runtime/journey-editor.ts +452 -0
- package/src/runtime/journey-evidence.test.ts +161 -0
- package/src/runtime/journey-evidence.ts +180 -0
- package/src/runtime/journey-graph.test.ts +257 -0
- package/src/runtime/journey-graph.ts +170 -0
- package/src/runtime/legacy-names.ts +32 -0
- package/src/runtime/llm-example.ts +105 -0
- package/src/runtime/llm.ts +527 -0
- package/src/runtime/local-browser.test.ts +45 -0
- package/src/runtime/local-browser.ts +48 -0
- package/src/runtime/local-registry-stub.test.ts +325 -0
- package/src/runtime/local-registry-stub.ts +803 -0
- package/src/runtime/maestro-driver.test.ts +84 -0
- package/src/runtime/maestro-driver.ts +209 -0
- package/src/runtime/mole-voice.ts +21 -0
- package/src/runtime/nav-crawl.test.ts +100 -0
- package/src/runtime/nav-crawl.ts +153 -0
- package/src/runtime/pipeline.test.ts +405 -0
- package/src/runtime/pipeline.ts +833 -0
- package/src/runtime/planner.test.ts +37 -0
- package/src/runtime/planner.ts +274 -0
- package/src/runtime/platform-ai.ts +76 -0
- package/src/runtime/playwright-driver.test.ts +93 -0
- package/src/runtime/playwright-driver.ts +620 -0
- package/src/runtime/project-spec.ts +140 -0
- package/src/runtime/record-run-verdicts.ts +68 -0
- package/src/runtime/reporter.test.ts +56 -0
- package/src/runtime/reporter.ts +158 -0
- package/src/runtime/reset.test.ts +44 -0
- package/src/runtime/reset.ts +61 -0
- package/src/runtime/reviewer.test.ts +73 -0
- package/src/runtime/reviewer.ts +158 -0
- package/src/runtime/run-job.ts +136 -0
- package/src/runtime/run-once.test.ts +207 -0
- package/src/runtime/run-once.ts +168 -0
- package/src/runtime/run.ts +132 -0
- package/src/runtime/screen-recording.ts +34 -0
- package/src/runtime/serve-gateway.test.ts +74 -0
- package/src/runtime/serve-gateway.ts +164 -0
- package/src/runtime/serve-worker.test.ts +23 -0
- package/src/runtime/serve-worker.ts +278 -0
- package/src/runtime/site-discovery.test.ts +168 -0
- package/src/runtime/site-discovery.ts +308 -0
- package/src/runtime/target-runner.ts +144 -0
- package/src/runtime/test-case-verdicts.test.ts +94 -0
- package/src/runtime/test-case-verdicts.ts +120 -0
- package/src/runtime/ui-reverse-engineering/coordinator.test.ts +59 -0
- package/src/runtime/ui-reverse-engineering/coordinator.ts +391 -0
- package/src/runtime/ui-reverse-engineering/types.ts +197 -0
- package/src/runtime/web-suite.test.ts +97 -0
- package/src/runtime/web-suite.ts +176 -0
- package/src/storage/create-object-store.ts +144 -0
- package/src/storage/keys.ts +34 -0
- package/src/storage/local-artifact-server.test.ts +314 -0
- package/src/storage/local-artifact-server.ts +357 -0
- package/src/storage/object-store.test.ts +28 -0
- package/src/storage/object-store.ts +101 -0
- package/src/storage/registry-object-store.ts +88 -0
- package/src/storage/remote-object-store.ts +104 -0
- package/src/storage/storage-directory.test.ts +43 -0
- package/src/storage/storage-directory.ts +24 -0
- 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
|
+
});
|