@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,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
|
+
});
|