@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,132 @@
|
|
|
1
|
+
export async function runBugmole(cfg: any, opts: { mode: string; args?: any }): Promise<any> {
|
|
2
|
+
const { mode, args = {} } = opts;
|
|
3
|
+
|
|
4
|
+
console.log(`[Bugmole MCP] mode=${mode} env=${cfg.runtime.env}`);
|
|
5
|
+
console.log(`[Bugmole MCP] spec_dir=${cfg.runtime.spec_dir}`);
|
|
6
|
+
console.log(`[Bugmole MCP] artifacts_dir=${cfg.runtime.artifacts_dir}`);
|
|
7
|
+
|
|
8
|
+
switch (mode) {
|
|
9
|
+
case "explore": {
|
|
10
|
+
const { explore } = await import("./explorer.js");
|
|
11
|
+
const result = await explore(cfg, {
|
|
12
|
+
baseUrl: args.baseUrl || args.baseurl || cfg.execution?.playwright?.base_url || "http://localhost:3000",
|
|
13
|
+
maxDepth: args.maxDepth || args.maxdepth || 5,
|
|
14
|
+
});
|
|
15
|
+
console.log(`[Bugmole MCP] Exploration result:`, result);
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
case "plan": {
|
|
20
|
+
const { plan } = await import("./planner.js");
|
|
21
|
+
const journeyId = args.journeyId || args.journeyid;
|
|
22
|
+
if (!journeyId || !args.actor) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
"Planning requires guided exploration first. Read bugmole://guidance/agent-testing, run --mode explore --baseUrl <url>, choose a discovered journey and defined actor, then retry with --journeyId <id> --actor <role>.",
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const result = await plan(cfg, {
|
|
28
|
+
journeyId,
|
|
29
|
+
actor: args.actor,
|
|
30
|
+
});
|
|
31
|
+
console.log(`[Bugmole MCP] Planning result:`, result);
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
case "run": {
|
|
36
|
+
const { execute } = await import("./executor.js");
|
|
37
|
+
// parseArgs lowercases flags (`--plan-id` → `planid`); MCP callers pass camelCase.
|
|
38
|
+
const planId = args.planId || args.planid;
|
|
39
|
+
if (!planId) {
|
|
40
|
+
throw new Error("run mode requires --plan-id argument");
|
|
41
|
+
}
|
|
42
|
+
const { targetsFromFlags } = await import("./browser-matrix.js");
|
|
43
|
+
const targets = targetsFromFlags(args.browsers, args.devices);
|
|
44
|
+
const baseUrl = args.baseUrl || args.baseurl;
|
|
45
|
+
const result = await execute(cfg, {
|
|
46
|
+
planId,
|
|
47
|
+
journeyId: args.journeyId || args.journeyid,
|
|
48
|
+
...(typeof baseUrl === "string" && baseUrl ? { baseUrl } : {}),
|
|
49
|
+
...(targets.length ? { targets } : {}),
|
|
50
|
+
});
|
|
51
|
+
console.log(`[Bugmole MCP] Execution result:`, result);
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
case "propose-diff": {
|
|
56
|
+
const { proposeDiff } = await import("./diff-proposal.js");
|
|
57
|
+
if (!args.filePath || !args.evidence) {
|
|
58
|
+
throw new Error("propose-diff mode requires --file-path and --evidence arguments");
|
|
59
|
+
}
|
|
60
|
+
const result = await proposeDiff(cfg, {
|
|
61
|
+
filePath: args.filePath,
|
|
62
|
+
evidence: args.evidence,
|
|
63
|
+
failures: args.failures,
|
|
64
|
+
});
|
|
65
|
+
console.log(`[Bugmole MCP] Diff proposal result:`, result);
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
case "review": {
|
|
70
|
+
const { review } = await import("./reviewer.js");
|
|
71
|
+
if (!args.filePath || !args.changeSummary || !args.diff) {
|
|
72
|
+
throw new Error("review mode requires --file-path, --change-summary, and --diff arguments");
|
|
73
|
+
}
|
|
74
|
+
const result = await review(cfg, {
|
|
75
|
+
filePath: args.filePath,
|
|
76
|
+
changeSummary: args.changeSummary,
|
|
77
|
+
diff: args.diff,
|
|
78
|
+
evidence: args.evidence || {},
|
|
79
|
+
});
|
|
80
|
+
console.log(`[Bugmole MCP] Review result:`, result);
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
case "report": {
|
|
85
|
+
const { generateReport } = await import("./reporter.js");
|
|
86
|
+
const result = await generateReport(cfg, {
|
|
87
|
+
runId: args.runId,
|
|
88
|
+
format: args.format || "markdown",
|
|
89
|
+
});
|
|
90
|
+
console.log(`[Bugmole MCP] Report generated:`, result.reportPath);
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
case "cursor-driver": {
|
|
95
|
+
const { runCursorDriver } = await import("./cursor-driver-run.js");
|
|
96
|
+
if (!args.task) {
|
|
97
|
+
throw new Error("cursor-driver mode requires --task argument");
|
|
98
|
+
}
|
|
99
|
+
const projectDirectory =
|
|
100
|
+
args.projectDirectory || args.projectdirectory || cfg.cursor_driver?.project_directory;
|
|
101
|
+
if (projectDirectory) {
|
|
102
|
+
console.log(`[Bugmole MCP] Using project directory: ${projectDirectory}`);
|
|
103
|
+
}
|
|
104
|
+
const result = await runCursorDriver(args.task, {
|
|
105
|
+
mcpHost: cfg.mcp?.host || "127.0.0.1",
|
|
106
|
+
mcpPort: cfg.mcp?.port || 3187,
|
|
107
|
+
artifactsDir: cfg.runtime.artifacts_dir || "./artifacts",
|
|
108
|
+
timeout: args.timeout || 300000,
|
|
109
|
+
pollInterval: args.pollInterval || 1000,
|
|
110
|
+
requestId: args.requestId,
|
|
111
|
+
customInstructions: args.customInstructions,
|
|
112
|
+
projectDirectory,
|
|
113
|
+
nudgeOptions:
|
|
114
|
+
args.nudgeEnabled === false
|
|
115
|
+
? { enabled: false }
|
|
116
|
+
: {
|
|
117
|
+
enabled: args.nudgeEnabled !== false,
|
|
118
|
+
progressReportTimeout:
|
|
119
|
+
args.nudgeProgressReportTimeout || args.nudgeInactivityThreshold,
|
|
120
|
+
nudgeInterval: args.nudgeInterval,
|
|
121
|
+
maxNudges: args.nudgeMaxNudges,
|
|
122
|
+
customMessage: args.nudgeMessage,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
console.log(`[Bugmole MCP] CURSOR_DRIVER result:`, result);
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
default:
|
|
130
|
+
throw new Error(`Unknown mode: ${mode}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Drop near-identical frames, then restamp what is left as evenly spaced.
|
|
9
|
+
*
|
|
10
|
+
* Without the restamp the dropped frames leave their duration behind and the
|
|
11
|
+
* video is the same length with the motion stuttering through it, which is the
|
|
12
|
+
* opposite of what skipping idle is for.
|
|
13
|
+
*/
|
|
14
|
+
export function mpdecimateArgs(input: string, output: string): string[] {
|
|
15
|
+
return ["-y", "-i", input, "-vf", "mpdecimate,setpts=N/FRAME_RATE/TB", "-an", output];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Returns the trimmed file, or the original when ffmpeg cannot do it. */
|
|
19
|
+
export async function dropIdleFrames(localPath: string, ffmpegCommand: string): Promise<string> {
|
|
20
|
+
const trimmed = localPath.replace(/\.mp4$/, "") + ".trimmed.mp4";
|
|
21
|
+
try {
|
|
22
|
+
await execFileAsync(ffmpegCommand, mpdecimateArgs(localPath, trimmed), { timeout: 300_000 });
|
|
23
|
+
const stats = await fs.stat(trimmed).catch(() => undefined);
|
|
24
|
+
if (!stats || stats.size === 0) return localPath;
|
|
25
|
+
await fs.rm(localPath, { force: true }).catch(() => undefined);
|
|
26
|
+
await fs.rename(trimmed, localPath).catch(() => undefined);
|
|
27
|
+
return localPath;
|
|
28
|
+
} catch {
|
|
29
|
+
// No ffmpeg, or it refused the file. Keeping the untrimmed recording is
|
|
30
|
+
// the point: skipping idle is a convenience, not a requirement.
|
|
31
|
+
await fs.rm(trimmed, { force: true }).catch(() => undefined);
|
|
32
|
+
return localPath;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import WebSocket, { WebSocketServer } from "ws";
|
|
5
|
+
import { connectServeGateway, startServeGateway } from "./serve-gateway.js";
|
|
6
|
+
|
|
7
|
+
async function listen(server: http.Server): Promise<number> {
|
|
8
|
+
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
9
|
+
return (server.address() as { port: number }).port;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function fakeRegistry(projectId: string) {
|
|
13
|
+
const server = http.createServer();
|
|
14
|
+
const sockets = new WebSocketServer({ noServer: true });
|
|
15
|
+
server.on("upgrade", (request, socket, head) => {
|
|
16
|
+
sockets.handleUpgrade(request, socket, head, (client) => sockets.emit("connection", client));
|
|
17
|
+
});
|
|
18
|
+
sockets.on("connection", (socket) => {
|
|
19
|
+
socket.on("message", (raw) => {
|
|
20
|
+
const message = JSON.parse(raw.toString()) as { projectId?: string };
|
|
21
|
+
if (message.projectId === projectId) {
|
|
22
|
+
socket.send(JSON.stringify({ type: "job_queued", projectId, runId: `${projectId}-run` }));
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
return server;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test("serve gateway authenticates workers and routes by project", async () => {
|
|
30
|
+
const registryA = fakeRegistry("project-a");
|
|
31
|
+
const registryB = fakeRegistry("project-b");
|
|
32
|
+
const registryAPort = await listen(registryA);
|
|
33
|
+
const registryBPort = await listen(registryB);
|
|
34
|
+
const gateway = await startServeGateway({ port: 0, secret: "gateway-secret" });
|
|
35
|
+
const messages = new Map<string, Record<string, unknown>>();
|
|
36
|
+
const connect = (projectId: string, registryPort: number) => new Promise<WebSocket>((resolve, reject) => {
|
|
37
|
+
const socket = connectServeGateway(
|
|
38
|
+
gateway.origin,
|
|
39
|
+
gateway.secret,
|
|
40
|
+
{
|
|
41
|
+
workerId: `${projectId}-worker`,
|
|
42
|
+
projectId,
|
|
43
|
+
registryUrl: `http://127.0.0.1:${registryPort}`,
|
|
44
|
+
apiKey: "local-test-key",
|
|
45
|
+
},
|
|
46
|
+
(message) => {
|
|
47
|
+
if (message.type === "job_queued") messages.set(projectId, message);
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
socket.once("open", () => resolve(socket));
|
|
51
|
+
socket.once("error", reject);
|
|
52
|
+
});
|
|
53
|
+
const workerA = await connect("project-a", registryAPort);
|
|
54
|
+
const workerB = await connect("project-b", registryBPort);
|
|
55
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
56
|
+
assert.equal(messages.get("project-a")?.runId, "project-a-run");
|
|
57
|
+
assert.equal(messages.get("project-b")?.runId, "project-b-run");
|
|
58
|
+
workerA.close();
|
|
59
|
+
workerB.close();
|
|
60
|
+
await gateway.close();
|
|
61
|
+
await new Promise<void>((resolve) => registryA.close(() => resolve()));
|
|
62
|
+
await new Promise<void>((resolve) => registryB.close(() => resolve()));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("serve gateway rejects unauthorized worker connections", async () => {
|
|
66
|
+
const gateway = await startServeGateway({ port: 0, secret: "gateway-secret" });
|
|
67
|
+
const rejected = await new Promise<boolean>((resolve) => {
|
|
68
|
+
const socket = new WebSocket(`${gateway.origin.replace(/^http/, "ws")}/worker?secret=wrong`);
|
|
69
|
+
socket.once("open", () => resolve(false));
|
|
70
|
+
socket.once("error", () => resolve(true));
|
|
71
|
+
});
|
|
72
|
+
assert.equal(rejected, true);
|
|
73
|
+
await gateway.close();
|
|
74
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
|
+
import WebSocket, { WebSocketServer } from "ws";
|
|
4
|
+
import { ControlPlaneClient } from "../registry/control-plane-client.js";
|
|
5
|
+
|
|
6
|
+
export type GatewayRegistration = {
|
|
7
|
+
type: "worker_hello";
|
|
8
|
+
workerId: string;
|
|
9
|
+
projectId: string;
|
|
10
|
+
workspaceId?: string;
|
|
11
|
+
registryUrl: string;
|
|
12
|
+
apiKey: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type WorkerMessage = GatewayRegistration | { type: "worker_goodbye"; workerId: string };
|
|
16
|
+
|
|
17
|
+
function sameSecret(actual: string | undefined, expected: string): boolean {
|
|
18
|
+
if (!actual) return false;
|
|
19
|
+
const left = Buffer.from(actual);
|
|
20
|
+
const right = Buffer.from(expected);
|
|
21
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function json(response: http.ServerResponse, status: number, body: unknown): void {
|
|
25
|
+
response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
|
|
26
|
+
response.end(JSON.stringify(body));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type ServeGateway = {
|
|
30
|
+
origin: string;
|
|
31
|
+
secret: string;
|
|
32
|
+
close(): Promise<void>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export type ServeGatewayOptions = {
|
|
36
|
+
host?: string;
|
|
37
|
+
port?: number;
|
|
38
|
+
secret?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export async function startServeGateway(options: ServeGatewayOptions = {}): Promise<ServeGateway> {
|
|
42
|
+
const host = options.host ?? "127.0.0.1";
|
|
43
|
+
const secret = options.secret ?? randomBytes(32).toString("base64url");
|
|
44
|
+
const workers = new Map<string, { socket: WebSocket; projectId: string; registrySocket: WebSocket }>();
|
|
45
|
+
const projects = new Map<string, string>();
|
|
46
|
+
const webSockets = new WebSocketServer({ noServer: true });
|
|
47
|
+
const server = http.createServer((request, response) => {
|
|
48
|
+
const url = new URL(request.url ?? "/", `http://${host}`);
|
|
49
|
+
if (url.pathname === "/health") {
|
|
50
|
+
json(response, 200, { ok: true, workers: workers.size, projects: [...projects.keys()] });
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
json(response, 404, { error: "Not found" });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const removeWorker = (workerId: string) => {
|
|
57
|
+
const worker = workers.get(workerId);
|
|
58
|
+
if (!worker) return;
|
|
59
|
+
workers.delete(workerId);
|
|
60
|
+
if (projects.get(worker.projectId) === workerId) projects.delete(worker.projectId);
|
|
61
|
+
worker.registrySocket.close();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
webSockets.on("connection", (socket) => {
|
|
65
|
+
let workerId: string | undefined;
|
|
66
|
+
socket.on("message", (raw) => {
|
|
67
|
+
let message: WorkerMessage;
|
|
68
|
+
try {
|
|
69
|
+
message = JSON.parse(raw.toString()) as WorkerMessage;
|
|
70
|
+
} catch {
|
|
71
|
+
socket.close(1003, "Invalid JSON");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (message.type === "worker_goodbye") {
|
|
75
|
+
if (message.workerId === workerId) removeWorker(message.workerId);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (message.type !== "worker_hello") return;
|
|
79
|
+
if (!message.workerId || !message.projectId || !message.registryUrl || !message.apiKey) {
|
|
80
|
+
socket.close(1008, "Incomplete worker registration");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const existing = projects.get(message.projectId);
|
|
84
|
+
if (existing && existing !== message.workerId) {
|
|
85
|
+
socket.close(1008, "Project already has an active worker");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
removeWorker(message.workerId);
|
|
89
|
+
const client = new ControlPlaneClient({
|
|
90
|
+
registryUrl: message.registryUrl,
|
|
91
|
+
apiKey: message.apiKey,
|
|
92
|
+
projectId: message.projectId,
|
|
93
|
+
workspaceId: message.workspaceId,
|
|
94
|
+
});
|
|
95
|
+
const registrySocket = client.connectWorker(message.workerId, (registryMessage) => {
|
|
96
|
+
if (socket.readyState === WebSocket.OPEN) socket.send(JSON.stringify(registryMessage));
|
|
97
|
+
});
|
|
98
|
+
workerId = message.workerId;
|
|
99
|
+
workers.set(workerId, { socket, projectId: message.projectId, registrySocket });
|
|
100
|
+
projects.set(message.projectId, workerId);
|
|
101
|
+
socket.send(JSON.stringify({ type: "gateway_registered", workerId, projectId: message.projectId }));
|
|
102
|
+
socket.on("close", () => removeWorker(workerId!));
|
|
103
|
+
socket.on("error", () => removeWorker(workerId!));
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
server.on("upgrade", (request, socket, head) => {
|
|
108
|
+
const url = new URL(request.url ?? "/", `http://${host}`);
|
|
109
|
+
if (url.pathname !== "/worker" || !sameSecret(url.searchParams.get("secret") ?? undefined, secret)) {
|
|
110
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
|
|
111
|
+
socket.destroy();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
webSockets.handleUpgrade(request, socket, head, (client) => {
|
|
115
|
+
webSockets.emit("connection", client, request);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
await new Promise<void>((resolve, reject) => {
|
|
120
|
+
const onError = (error: NodeJS.ErrnoException) => {
|
|
121
|
+
server.removeListener("error", onError);
|
|
122
|
+
reject(error);
|
|
123
|
+
};
|
|
124
|
+
server.once("error", onError);
|
|
125
|
+
server.listen(options.port ?? 3191, host, () => {
|
|
126
|
+
server.removeListener("error", onError);
|
|
127
|
+
resolve();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
const address = server.address();
|
|
131
|
+
if (!address || typeof address === "string") throw new Error("Gateway did not bind");
|
|
132
|
+
return {
|
|
133
|
+
origin: `http://${host}:${address.port}`,
|
|
134
|
+
secret,
|
|
135
|
+
close: async () => {
|
|
136
|
+
for (const workerId of workers.keys()) removeWorker(workerId);
|
|
137
|
+
await new Promise<void>((resolve, reject) => webSockets.close((error) => error ? reject(error) : resolve()));
|
|
138
|
+
await new Promise<void>((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function connectServeGateway(
|
|
144
|
+
origin: string,
|
|
145
|
+
secret: string,
|
|
146
|
+
registration: Omit<GatewayRegistration, "type">,
|
|
147
|
+
onMessage: (message: Record<string, unknown>) => void,
|
|
148
|
+
): WebSocket {
|
|
149
|
+
const url = new URL("/worker", origin.replace(/^http/, "ws"));
|
|
150
|
+
url.searchParams.set("secret", secret);
|
|
151
|
+
// .toString() rather than the URL itself: @types/ws still types its
|
|
152
|
+
// address as node:url's URL, while @types/node 26 makes the global URL the
|
|
153
|
+
// web-standard one, and the two no longer line up.
|
|
154
|
+
const socket = new WebSocket(url.toString());
|
|
155
|
+
socket.on("open", () => socket.send(JSON.stringify({ type: "worker_hello", ...registration })));
|
|
156
|
+
socket.on("message", (raw) => {
|
|
157
|
+
try {
|
|
158
|
+
onMessage(JSON.parse(raw.toString()) as Record<string, unknown>);
|
|
159
|
+
} catch {
|
|
160
|
+
// Ignore malformed gateway messages.
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
return socket;
|
|
164
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { RegistryRequestError } from "../registry/control-plane-client.js";
|
|
4
|
+
import { isClaimConflict, resolveMaxConcurrentRuns, workerTasksEnabled } from "./serve-worker.js";
|
|
5
|
+
|
|
6
|
+
test("runs one run at a time unless a positive limit is configured", () => {
|
|
7
|
+
assert.equal(resolveMaxConcurrentRuns({}), 1);
|
|
8
|
+
assert.equal(resolveMaxConcurrentRuns({ BUGMOLE_MAX_CONCURRENT_RUNS: "3" }), 3);
|
|
9
|
+
assert.equal(resolveMaxConcurrentRuns({ BUGMOLE_MAX_CONCURRENT_RUNS: "0" }), 1);
|
|
10
|
+
assert.equal(resolveMaxConcurrentRuns({ BUGMOLE_MAX_CONCURRENT_RUNS: "lots" }), 1);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("agent tasks stay on unless the worker opts out", () => {
|
|
14
|
+
assert.equal(workerTasksEnabled({}), true);
|
|
15
|
+
assert.equal(workerTasksEnabled({ BUGMOLE_WORKER_TASKS: "false" }), false);
|
|
16
|
+
assert.equal(workerTasksEnabled({ BUGMOLE_WORKER_TASKS: " FALSE " }), false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("treats only a 409 from the registry as a lost claim", () => {
|
|
20
|
+
assert.equal(isClaimConflict(new RegistryRequestError(409)), true);
|
|
21
|
+
assert.equal(isClaimConflict(new RegistryRequestError(500)), false);
|
|
22
|
+
assert.equal(isClaimConflict(new Error("Registry request failed (409)")), false);
|
|
23
|
+
});
|