@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,105 @@
1
+ /**
2
+ * Example: Using the unified LLM interface
3
+ *
4
+ * This demonstrates how to use the unified LLM interface that works
5
+ * with OpenAI, Anthropic, Cursor LLM, or Ollama - all with the same API.
6
+ */
7
+
8
+ import { llm, llmStructured } from "./llm.js";
9
+
10
+ /**
11
+ * Example 1: Simple text completion (works with any provider)
12
+ *
13
+ * The provider is automatically selected from config (bugmole.config.yaml)
14
+ */
15
+ export async function exampleSimple() {
16
+ // Works with OpenAI, Anthropic, Cursor, or Ollama
17
+ // Just change LLM_PROVIDER in your config!
18
+ const answer = await llm("Analyze the error handling patterns in this codebase");
19
+ console.log(answer);
20
+ return answer;
21
+ }
22
+
23
+ /**
24
+ * Example 2: Override provider
25
+ */
26
+ export async function exampleOverrideProvider() {
27
+ // Force a specific provider
28
+ const answer = await llm(
29
+ "Your prompt here",
30
+ { provider: "openai" }, // or 'anthropic', 'cursor', 'ollama'
31
+ );
32
+ return answer;
33
+ }
34
+
35
+ /**
36
+ * Example 3: Structured response
37
+ */
38
+ export async function exampleStructured() {
39
+ const response = await llmStructured("Review test failures and propose fixes");
40
+
41
+ console.log("Provider:", response.provider);
42
+ console.log("Model:", response.model);
43
+ console.log("Text:", response.text);
44
+ console.log("Metadata:", response.metadata);
45
+
46
+ return response;
47
+ }
48
+
49
+ /**
50
+ * Example 4: Using in a workflow
51
+ */
52
+ export async function exampleWorkflow() {
53
+ // Step 1: Analyze (uses provider from config)
54
+ const analysis = await llm("Analyze the current test coverage");
55
+
56
+ // Step 2: Generate plan (same provider, or override)
57
+ const plan = await llm(
58
+ `Based on this analysis, create a test plan:\n\n${analysis}`,
59
+ { provider: "cursor" }, // Use Cursor for code-aware planning
60
+ );
61
+
62
+ return { analysis, plan };
63
+ }
64
+
65
+ /**
66
+ * Example 5: Error handling
67
+ */
68
+ export async function exampleWithErrorHandling() {
69
+ try {
70
+ const answer = await llm("Your task here");
71
+ return answer;
72
+ } catch (error: any) {
73
+ if (error.message?.includes("API key")) {
74
+ console.error("Missing API key. Set LLM_API_KEY in .env");
75
+ } else if (error.message?.includes("timeout")) {
76
+ console.error("Request timed out. Try increasing timeout.");
77
+ } else {
78
+ console.error("Error:", error.message);
79
+ }
80
+ throw error;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Example 6: Switching providers dynamically
86
+ */
87
+ export async function exampleSwitchProviders() {
88
+ const providers: Array<"openai" | "anthropic" | "cursor" | "ollama"> = [
89
+ "openai",
90
+ "anthropic",
91
+ "cursor",
92
+ ];
93
+
94
+ const results = await Promise.all(
95
+ providers.map((provider) =>
96
+ llm("What is 2+2?", { provider }).then((text) => ({
97
+ provider,
98
+ text,
99
+ })),
100
+ ),
101
+ );
102
+
103
+ console.log("Results from different providers:", results);
104
+ return results;
105
+ }
@@ -0,0 +1,527 @@
1
+ /**
2
+ * Unified LLM Interface
3
+ *
4
+ * Provides a single, consistent interface for calling any LLM provider:
5
+ * - OpenAI
6
+ * - Anthropic
7
+ * - Cursor LLM (via CURSOR_DRIVER)
8
+ * - Ollama
9
+ *
10
+ * Usage:
11
+ * import { llm } from './llm.js';
12
+ * const response = await llm('Your prompt here');
13
+ */
14
+
15
+ import fs from "node:fs";
16
+ import { preferExisting } from "./legacy-names.js";
17
+ import path from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+ import dotenv from "dotenv";
20
+ import YAML from "yaml";
21
+ import { cursorLLM, cursorLLMStructured } from "./cursor-llm.js";
22
+ import type { CursorDriverResponse } from "./cursor-driver.js";
23
+
24
+ /**
25
+ * Type definitions for API responses
26
+ */
27
+ interface OpenAIResponse {
28
+ choices: Array<{
29
+ message: {
30
+ content: string;
31
+ };
32
+ }>;
33
+ model?: string;
34
+ usage?: { prompt_tokens?: number; completion_tokens?: number };
35
+ }
36
+
37
+ interface AnthropicResponse {
38
+ content: Array<{
39
+ text: string;
40
+ }>;
41
+ model?: string;
42
+ usage?: { input_tokens?: number; output_tokens?: number; cache_read_input_tokens?: number };
43
+ }
44
+
45
+ interface OllamaResponse {
46
+ response: string;
47
+ prompt_eval_count?: number;
48
+ eval_count?: number;
49
+ }
50
+
51
+ /** Token counts a provider reported for one call, when it reports them. */
52
+ export type ModelUsage = { provider: string; model: string; inputTokens?: number; outputTokens?: number };
53
+ export type ModelReply = { text: string; usage: ModelUsage };
54
+
55
+ // Cache config to avoid reloading
56
+ let cachedConfig: any = null;
57
+
58
+ /**
59
+ * Loads and caches the Bugmole MCP configuration
60
+ */
61
+ function loadConfig(configPath: string = preferExisting("bugmole.config.yaml", "valkyrie.config.yaml")): any {
62
+ if (cachedConfig) {
63
+ return cachedConfig;
64
+ }
65
+
66
+ const scriptPath = fileURLToPath(import.meta.url);
67
+ const scriptDir = path.dirname(scriptPath);
68
+ const projectRoot = path.resolve(scriptDir, "../..");
69
+
70
+ const originalCwd = process.cwd();
71
+ process.chdir(projectRoot);
72
+
73
+ try {
74
+ dotenv.config({ quiet: true });
75
+
76
+ const fullConfigPath = path.join(projectRoot, configPath);
77
+ if (!fs.existsSync(fullConfigPath)) {
78
+ throw new Error(`Config file not found: ${fullConfigPath}`);
79
+ }
80
+
81
+ const raw = fs.readFileSync(fullConfigPath, "utf-8");
82
+ const parsed = YAML.parse(raw);
83
+
84
+ cachedConfig = interpolateEnv(parsed);
85
+ return cachedConfig;
86
+ } finally {
87
+ process.chdir(originalCwd);
88
+ }
89
+ }
90
+
91
+ // tiny ${ENV} interpolator for yaml configs
92
+ function interpolateEnv(obj: any): any {
93
+ if (typeof obj === "string") {
94
+ return obj.replace(/\$\{([A-Z0-9_]+)(?::-([^}]+))?\}/g, (_, k, defaultValue) => {
95
+ const envValue = process.env[k];
96
+ if (envValue !== undefined && envValue !== "") {
97
+ return envValue;
98
+ }
99
+ return defaultValue !== undefined ? defaultValue : "";
100
+ });
101
+ }
102
+ if (Array.isArray(obj)) return obj.map(interpolateEnv);
103
+ if (obj && typeof obj === "object") {
104
+ const out: any = {};
105
+ for (const [k, v] of Object.entries(obj)) out[k] = interpolateEnv(v);
106
+ return out;
107
+ }
108
+ return obj;
109
+ }
110
+
111
+ /**
112
+ * LLM Provider types
113
+ */
114
+ export type LLMProvider = "openai" | "anthropic" | "cursor" | "ollama";
115
+
116
+ /**
117
+ * Unified LLM options
118
+ */
119
+ export interface LLMOptions {
120
+ /**
121
+ * Override provider (default: from config)
122
+ */
123
+ provider?: LLMProvider;
124
+
125
+ /**
126
+ * Override model (default: from config)
127
+ */
128
+ model?: string;
129
+
130
+ /**
131
+ * Custom config file path (default: bugmole.config.yaml)
132
+ */
133
+ configPath?: string;
134
+
135
+ /**
136
+ * Timeout in milliseconds (default: 300000 = 5 minutes)
137
+ */
138
+ timeout?: number;
139
+
140
+ /**
141
+ * Request ID for tracking
142
+ */
143
+ requestId?: string;
144
+
145
+ /**
146
+ * Custom instructions (provider-specific)
147
+ */
148
+ customInstructions?: string;
149
+
150
+ /**
151
+ * Project directory (for Cursor provider)
152
+ */
153
+ projectDirectory?: string;
154
+ }
155
+
156
+ /**
157
+ * Unified LLM response
158
+ */
159
+ export interface LLMResponse {
160
+ /**
161
+ * The text response from the LLM
162
+ */
163
+ text: string;
164
+
165
+ /**
166
+ * Provider used
167
+ */
168
+ provider: LLMProvider;
169
+
170
+ /**
171
+ * Model used
172
+ */
173
+ model: string;
174
+
175
+ /**
176
+ * Additional metadata (provider-specific)
177
+ */
178
+ metadata?: Record<string, any>;
179
+ }
180
+
181
+ /**
182
+ * Unified LLM interface - works with any provider
183
+ *
184
+ * Automatically selects the provider based on config:
185
+ * - If provider is 'cursor', uses CURSOR_DRIVER
186
+ * - Otherwise, uses standard HTTP API calls
187
+ *
188
+ * @param prompt - The prompt to send
189
+ * @param options - Optional overrides
190
+ * @returns The text response
191
+ *
192
+ * @example
193
+ * ```typescript
194
+ * import { llm } from './llm.js';
195
+ *
196
+ * // Works with any provider configured in bugmole.config.yaml
197
+ * const answer = await llm('Analyze the error handling in this codebase');
198
+ * console.log(answer);
199
+ * ```
200
+ */
201
+ export async function llm(prompt: string, options: LLMOptions = {}): Promise<string> {
202
+ const cfg = loadConfig(options.configPath);
203
+ const provider = (options.provider || cfg.llm?.provider || "cursor").toLowerCase() as LLMProvider;
204
+ const model = options.model || cfg.llm?.model || "gpt-4o-mini";
205
+
206
+ // Cursor provider uses CURSOR_DRIVER
207
+ if (provider === "cursor") {
208
+ return cursorLLM(prompt, {
209
+ configPath: options.configPath,
210
+ timeout: options.timeout,
211
+ requestId: options.requestId,
212
+ customInstructions: options.customInstructions,
213
+ projectDirectory: options.projectDirectory,
214
+ });
215
+ }
216
+
217
+ // Other providers use HTTP APIs
218
+ return callLLMAPI(prompt, {
219
+ provider,
220
+ model,
221
+ apiKey: cfg.llm?.api_key,
222
+ baseUrl: cfg.llm?.base_url,
223
+ timeout: options.timeout,
224
+ });
225
+ }
226
+
227
+ /**
228
+ * Unified LLM interface with structured response
229
+ *
230
+ * @param prompt - The prompt to send
231
+ * @param options - Optional overrides
232
+ * @returns Structured response
233
+ */
234
+ export async function llmStructured(
235
+ prompt: string,
236
+ options: LLMOptions = {},
237
+ ): Promise<LLMResponse> {
238
+ const cfg = loadConfig(options.configPath);
239
+ const provider = (options.provider || cfg.llm?.provider || "cursor").toLowerCase() as LLMProvider;
240
+ const model = options.model || cfg.llm?.model || "gpt-4o-mini";
241
+
242
+ // Cursor provider uses CURSOR_DRIVER
243
+ if (provider === "cursor") {
244
+ const response = await cursorLLMStructured(prompt, {
245
+ configPath: options.configPath,
246
+ timeout: options.timeout,
247
+ requestId: options.requestId,
248
+ customInstructions: options.customInstructions,
249
+ projectDirectory: options.projectDirectory,
250
+ });
251
+
252
+ if (!response.success || !response.payload) {
253
+ throw new Error(response.error || "LLM call failed");
254
+ }
255
+
256
+ return {
257
+ text: response.payload.answer_markdown || response.payload.summary,
258
+ provider: "cursor",
259
+ model: "cursor",
260
+ metadata: {
261
+ commitId: response.commitId,
262
+ actions: response.payload.actions,
263
+ artifacts: response.payload.artifacts,
264
+ },
265
+ };
266
+ }
267
+
268
+ // Other providers use HTTP APIs
269
+ const text = await callLLMAPI(prompt, {
270
+ provider,
271
+ model,
272
+ apiKey: cfg.llm?.api_key,
273
+ baseUrl: cfg.llm?.base_url,
274
+ timeout: options.timeout,
275
+ });
276
+
277
+ return {
278
+ text,
279
+ provider,
280
+ model,
281
+ };
282
+ }
283
+
284
+ /**
285
+ * Calls a model with explicit settings (e.g. a project's own `llm` config)
286
+ * instead of the package-level config the helpers above read.
287
+ */
288
+ export async function callModel(
289
+ prompt: string,
290
+ options: { provider: string; model: string; apiKey?: string; baseUrl?: string; timeout?: number },
291
+ ): Promise<string> {
292
+ return (await callModelWithUsage(prompt, options)).text;
293
+ }
294
+
295
+ /** callModel, plus the token counts the provider reported. */
296
+ export function callModelWithUsage(
297
+ prompt: string,
298
+ options: { provider: string; model: string; apiKey?: string; baseUrl?: string; timeout?: number; maxTokens?: number },
299
+ ): Promise<ModelReply> {
300
+ return callLLMAPIWithUsage(prompt, { ...options, provider: options.provider.toLowerCase() as LLMProvider });
301
+ }
302
+
303
+ /**
304
+ * Calls standard LLM APIs (OpenAI, Anthropic, Ollama)
305
+ */
306
+ async function callLLMAPI(
307
+ prompt: string,
308
+ options: {
309
+ provider: LLMProvider;
310
+ model: string;
311
+ apiKey?: string;
312
+ baseUrl?: string;
313
+ timeout?: number;
314
+ },
315
+ ): Promise<string> {
316
+ return (await callLLMAPIWithUsage(prompt, options)).text;
317
+ }
318
+
319
+ async function callLLMAPIWithUsage(
320
+ prompt: string,
321
+ options: {
322
+ provider: LLMProvider;
323
+ model: string;
324
+ apiKey?: string;
325
+ baseUrl?: string;
326
+ timeout?: number;
327
+ maxTokens?: number;
328
+ },
329
+ ): Promise<ModelReply> {
330
+ const { provider, model, apiKey, baseUrl, timeout = 300000, maxTokens } = options;
331
+
332
+ if (!apiKey && provider !== "ollama") {
333
+ throw new Error(`API key required for provider: ${provider}`);
334
+ }
335
+
336
+ // OpenAI
337
+ if (provider === "openai") {
338
+ return callOpenAI(prompt, { model, apiKey: apiKey!, baseUrl, timeout, maxTokens });
339
+ }
340
+
341
+ // Anthropic
342
+ if (provider === "anthropic") {
343
+ return callAnthropic(prompt, { model, apiKey: apiKey!, baseUrl, timeout, maxTokens });
344
+ }
345
+
346
+ // Ollama
347
+ if (provider === "ollama") {
348
+ return callOllama(prompt, {
349
+ model,
350
+ baseUrl: baseUrl || "http://localhost:11434",
351
+ timeout,
352
+ });
353
+ }
354
+
355
+ throw new Error(`Unsupported provider: ${provider}`);
356
+ }
357
+
358
+ /**
359
+ * Calls OpenAI API
360
+ */
361
+ async function callOpenAI(
362
+ prompt: string,
363
+ options: { model: string; apiKey: string; baseUrl?: string; timeout: number; maxTokens?: number },
364
+ ): Promise<ModelReply> {
365
+ const { model, apiKey, baseUrl, timeout, maxTokens } = options;
366
+ const url = baseUrl || "https://api.openai.com/v1/chat/completions";
367
+
368
+ const controller = new AbortController();
369
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
370
+
371
+ try {
372
+ const response = await fetch(url, {
373
+ method: "POST",
374
+ headers: {
375
+ "Content-Type": "application/json",
376
+ Authorization: `Bearer ${apiKey}`,
377
+ },
378
+ body: JSON.stringify({
379
+ model,
380
+ messages: [{ role: "user", content: prompt }],
381
+ temperature: 0.7,
382
+ ...(maxTokens ? { max_tokens: maxTokens } : {}),
383
+ }),
384
+ signal: controller.signal,
385
+ });
386
+
387
+ clearTimeout(timeoutId);
388
+
389
+ if (!response.ok) {
390
+ const error = await response.text();
391
+ throw new Error(`OpenAI API error: ${response.status} ${error}`);
392
+ }
393
+
394
+ const data = (await response.json()) as OpenAIResponse;
395
+ return {
396
+ text: data.choices[0]?.message?.content || "",
397
+ usage: {
398
+ provider: "openai",
399
+ model: data.model ?? model,
400
+ inputTokens: data.usage?.prompt_tokens,
401
+ outputTokens: data.usage?.completion_tokens,
402
+ },
403
+ };
404
+ } catch (error: any) {
405
+ clearTimeout(timeoutId);
406
+ if (error.name === "AbortError") {
407
+ throw new Error(`Request timeout after ${timeout}ms`);
408
+ }
409
+ throw error;
410
+ }
411
+ }
412
+
413
+ /**
414
+ * Calls Anthropic API
415
+ */
416
+ /** Accepts a gateway root ("https://gw.example.com") or a full messages URL. */
417
+ export function anthropicMessagesUrl(baseUrl?: string): string {
418
+ if (!baseUrl) return "https://api.anthropic.com/v1/messages";
419
+ const trimmed = baseUrl.replace(/\/+$/, "");
420
+ if (trimmed.endsWith("/messages")) return trimmed;
421
+ return trimmed.endsWith("/v1") ? `${trimmed}/messages` : `${trimmed}/v1/messages`;
422
+ }
423
+
424
+ async function callAnthropic(
425
+ prompt: string,
426
+ options: { model: string; apiKey: string; baseUrl?: string; timeout: number; maxTokens?: number },
427
+ ): Promise<ModelReply> {
428
+ const { model, apiKey, baseUrl, timeout, maxTokens } = options;
429
+
430
+ const controller = new AbortController();
431
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
432
+
433
+ try {
434
+ const response = await fetch(anthropicMessagesUrl(baseUrl), {
435
+ method: "POST",
436
+ headers: {
437
+ "Content-Type": "application/json",
438
+ "x-api-key": apiKey,
439
+ "anthropic-version": "2023-06-01",
440
+ },
441
+ body: JSON.stringify({
442
+ model,
443
+ max_tokens: maxTokens ?? 4096,
444
+ messages: [{ role: "user", content: prompt }],
445
+ }),
446
+ signal: controller.signal,
447
+ });
448
+
449
+ clearTimeout(timeoutId);
450
+
451
+ if (!response.ok) {
452
+ const error = await response.text();
453
+ throw new Error(`Anthropic API error: ${response.status} ${error}`);
454
+ }
455
+
456
+ const data = (await response.json()) as AnthropicResponse;
457
+ return {
458
+ text: data.content[0]?.text || "",
459
+ usage: {
460
+ provider: "anthropic",
461
+ model: data.model ?? model,
462
+ inputTokens: (data.usage?.input_tokens ?? 0) + (data.usage?.cache_read_input_tokens ?? 0) || undefined,
463
+ outputTokens: data.usage?.output_tokens,
464
+ },
465
+ };
466
+ } catch (error: any) {
467
+ clearTimeout(timeoutId);
468
+ if (error.name === "AbortError") {
469
+ throw new Error(`Request timeout after ${timeout}ms`);
470
+ }
471
+ throw error;
472
+ }
473
+ }
474
+
475
+ /**
476
+ * Calls Ollama API
477
+ */
478
+ async function callOllama(
479
+ prompt: string,
480
+ options: { model: string; baseUrl: string; timeout: number },
481
+ ): Promise<ModelReply> {
482
+ const { model, baseUrl, timeout } = options;
483
+
484
+ const controller = new AbortController();
485
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
486
+
487
+ try {
488
+ const response = await fetch(`${baseUrl}/api/generate`, {
489
+ method: "POST",
490
+ headers: {
491
+ "Content-Type": "application/json",
492
+ },
493
+ body: JSON.stringify({
494
+ model,
495
+ prompt,
496
+ stream: false,
497
+ }),
498
+ signal: controller.signal,
499
+ });
500
+
501
+ clearTimeout(timeoutId);
502
+
503
+ if (!response.ok) {
504
+ const error = await response.text();
505
+ throw new Error(`Ollama API error: ${response.status} ${error}`);
506
+ }
507
+
508
+ const data = (await response.json()) as OllamaResponse;
509
+ return {
510
+ text: data.response || "",
511
+ usage: { provider: "ollama", model, inputTokens: data.prompt_eval_count, outputTokens: data.eval_count },
512
+ };
513
+ } catch (error: any) {
514
+ clearTimeout(timeoutId);
515
+ if (error.name === "AbortError") {
516
+ throw new Error(`Request timeout after ${timeout}ms`);
517
+ }
518
+ throw error;
519
+ }
520
+ }
521
+
522
+ /**
523
+ * Reset the config cache (useful for testing or when config changes)
524
+ */
525
+ export function resetLLMConfigCache(): void {
526
+ cachedConfig = null;
527
+ }
@@ -0,0 +1,45 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { MAC_CHROME_PATH, headlessBrowserArgs, resolveHeadlessBrowserPath } from "./local-browser.js";
4
+
5
+ const bundled = "/ms-playwright/chromium/chrome";
6
+
7
+ test("an explicit BUGMOLE_BROWSER_PATH wins over every installed browser", async () => {
8
+ const browser = await resolveHeadlessBrowserPath({
9
+ env: { BUGMOLE_BROWSER_PATH: " /opt/chrome " },
10
+ exists: () => true,
11
+ playwrightChromiumPath: async () => bundled,
12
+ });
13
+ assert.equal(browser, "/opt/chrome");
14
+ });
15
+
16
+ test("keeps using macOS Google Chrome when it is installed", async () => {
17
+ const browser = await resolveHeadlessBrowserPath({
18
+ env: {},
19
+ exists: (candidate) => candidate === MAC_CHROME_PATH || candidate === bundled,
20
+ playwrightChromiumPath: async () => bundled,
21
+ });
22
+ assert.equal(browser, MAC_CHROME_PATH);
23
+ });
24
+
25
+ test("falls back to Playwright's bundled Chromium, as on a Linux worker", async () => {
26
+ const browser = await resolveHeadlessBrowserPath({
27
+ env: {},
28
+ exists: (candidate) => candidate === bundled,
29
+ playwrightChromiumPath: async () => bundled,
30
+ });
31
+ assert.equal(browser, bundled);
32
+ });
33
+
34
+ test("explains how to fix it when no browser is available", async () => {
35
+ await assert.rejects(
36
+ resolveHeadlessBrowserPath({ env: {}, exists: () => false, playwrightChromiumPath: async () => undefined }),
37
+ /BUGMOLE_BROWSER_PATH.*playwright install chromium/,
38
+ );
39
+ });
40
+
41
+ test("adds --no-sandbox only for root on Linux", () => {
42
+ assert.deepEqual(headlessBrowserArgs("linux", 0), ["--no-sandbox"]);
43
+ assert.deepEqual(headlessBrowserArgs("linux", 1000), []);
44
+ assert.deepEqual(headlessBrowserArgs("darwin", 0), []);
45
+ });