@mseep/clawdcursor 1.5.5
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/CHANGELOG.md +2264 -0
- package/LICENSE +21 -0
- package/README.md +385 -0
- package/SECURITY.md +44 -0
- package/SKILL.md +503 -0
- package/dist/core/agent-loop/agent.d.ts +42 -0
- package/dist/core/agent-loop/agent.js +1023 -0
- package/dist/core/agent-loop/agent.js.map +1 -0
- package/dist/core/agent-loop/batch-tool.d.ts +25 -0
- package/dist/core/agent-loop/batch-tool.js +218 -0
- package/dist/core/agent-loop/batch-tool.js.map +1 -0
- package/dist/core/agent-loop/coord-scale.d.ts +72 -0
- package/dist/core/agent-loop/coord-scale.js +89 -0
- package/dist/core/agent-loop/coord-scale.js.map +1 -0
- package/dist/core/agent-loop/focus-guard.d.ts +24 -0
- package/dist/core/agent-loop/focus-guard.js +29 -0
- package/dist/core/agent-loop/focus-guard.js.map +1 -0
- package/dist/core/agent-loop/project-mcp.d.ts +97 -0
- package/dist/core/agent-loop/project-mcp.js +253 -0
- package/dist/core/agent-loop/project-mcp.js.map +1 -0
- package/dist/core/agent-loop/prompt.d.ts +45 -0
- package/dist/core/agent-loop/prompt.js +426 -0
- package/dist/core/agent-loop/prompt.js.map +1 -0
- package/dist/core/agent-loop/tool-meta.d.ts +93 -0
- package/dist/core/agent-loop/tool-meta.js +651 -0
- package/dist/core/agent-loop/tool-meta.js.map +1 -0
- package/dist/core/agent-loop/tools.d.ts +38 -0
- package/dist/core/agent-loop/tools.js +2134 -0
- package/dist/core/agent-loop/tools.js.map +1 -0
- package/dist/core/agent-loop/types.d.ts +170 -0
- package/dist/core/agent-loop/types.js +12 -0
- package/dist/core/agent-loop/types.js.map +1 -0
- package/dist/core/agent.d.ts +51 -0
- package/dist/core/agent.js +245 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/app-categories.d.ts +67 -0
- package/dist/core/app-categories.js +108 -0
- package/dist/core/app-categories.js.map +1 -0
- package/dist/core/banner.d.ts +70 -0
- package/dist/core/banner.js +245 -0
- package/dist/core/banner.js.map +1 -0
- package/dist/core/classify/capability.d.ts +45 -0
- package/dist/core/classify/capability.js +78 -0
- package/dist/core/classify/capability.js.map +1 -0
- package/dist/core/decompose/llm-decomposer.d.ts +35 -0
- package/dist/core/decompose/llm-decomposer.js +156 -0
- package/dist/core/decompose/llm-decomposer.js.map +1 -0
- package/dist/core/decompose/parser.d.ts +27 -0
- package/dist/core/decompose/parser.js +101 -0
- package/dist/core/decompose/parser.js.map +1 -0
- package/dist/core/observability/correlation.d.ts +19 -0
- package/dist/core/observability/correlation.js +36 -0
- package/dist/core/observability/correlation.js.map +1 -0
- package/dist/core/observability/cost-meter.d.ts +51 -0
- package/dist/core/observability/cost-meter.js +134 -0
- package/dist/core/observability/cost-meter.js.map +1 -0
- package/dist/core/observability/logger.d.ts +61 -0
- package/dist/core/observability/logger.js +550 -0
- package/dist/core/observability/logger.js.map +1 -0
- package/dist/core/router/aliases.d.ts +50 -0
- package/dist/core/router/aliases.js +104 -0
- package/dist/core/router/aliases.js.map +1 -0
- package/dist/core/router/normalize.d.ts +41 -0
- package/dist/core/router/normalize.js +80 -0
- package/dist/core/router/normalize.js.map +1 -0
- package/dist/core/safety.d.ts +126 -0
- package/dist/core/safety.js +568 -0
- package/dist/core/safety.js.map +1 -0
- package/dist/core/sense/a11y-resolver.d.ts +73 -0
- package/dist/core/sense/a11y-resolver.js +76 -0
- package/dist/core/sense/a11y-resolver.js.map +1 -0
- package/dist/core/sense/fingerprint.d.ts +41 -0
- package/dist/core/sense/fingerprint.js +123 -0
- package/dist/core/sense/fingerprint.js.map +1 -0
- package/dist/core/sense/rank.d.ts +70 -0
- package/dist/core/sense/rank.js +192 -0
- package/dist/core/sense/rank.js.map +1 -0
- package/dist/core/sense/reactive-check.d.ts +40 -0
- package/dist/core/sense/reactive-check.js +48 -0
- package/dist/core/sense/reactive-check.js.map +1 -0
- package/dist/core/sense/snapshot.d.ts +19 -0
- package/dist/core/sense/snapshot.js +100 -0
- package/dist/core/sense/snapshot.js.map +1 -0
- package/dist/core/sense/types.d.ts +66 -0
- package/dist/core/sense/types.js +9 -0
- package/dist/core/sense/types.js.map +1 -0
- package/dist/core/sense/ui-map-anchors.d.ts +7 -0
- package/dist/core/sense/ui-map-anchors.js +24 -0
- package/dist/core/sense/ui-map-anchors.js.map +1 -0
- package/dist/core/sense/ui-map-elements.d.ts +5 -0
- package/dist/core/sense/ui-map-elements.js +33 -0
- package/dist/core/sense/ui-map-elements.js.map +1 -0
- package/dist/core/sense/ui-map-find.d.ts +56 -0
- package/dist/core/sense/ui-map-find.js +153 -0
- package/dist/core/sense/ui-map-find.js.map +1 -0
- package/dist/core/sense/ui-map-fuse.d.ts +4 -0
- package/dist/core/sense/ui-map-fuse.js +44 -0
- package/dist/core/sense/ui-map-fuse.js.map +1 -0
- package/dist/core/sense/ui-map-geom.d.ts +3 -0
- package/dist/core/sense/ui-map-geom.js +16 -0
- package/dist/core/sense/ui-map-geom.js.map +1 -0
- package/dist/core/sense/ui-map-holder.d.ts +58 -0
- package/dist/core/sense/ui-map-holder.js +87 -0
- package/dist/core/sense/ui-map-holder.js.map +1 -0
- package/dist/core/sense/ui-map-normalize.d.ts +19 -0
- package/dist/core/sense/ui-map-normalize.js +65 -0
- package/dist/core/sense/ui-map-normalize.js.map +1 -0
- package/dist/core/sense/ui-map-render.d.ts +4 -0
- package/dist/core/sense/ui-map-render.js +34 -0
- package/dist/core/sense/ui-map-render.js.map +1 -0
- package/dist/core/sense/ui-map-resolve.d.ts +41 -0
- package/dist/core/sense/ui-map-resolve.js +59 -0
- package/dist/core/sense/ui-map-resolve.js.map +1 -0
- package/dist/core/sense/ui-map-types.d.ts +66 -0
- package/dist/core/sense/ui-map-types.js +11 -0
- package/dist/core/sense/ui-map-types.js.map +1 -0
- package/dist/core/sense/ui-map.d.ts +29 -0
- package/dist/core/sense/ui-map.js +113 -0
- package/dist/core/sense/ui-map.js.map +1 -0
- package/dist/core/verify/assertions.d.ts +132 -0
- package/dist/core/verify/assertions.js +284 -0
- package/dist/core/verify/assertions.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/browser-config.d.ts +36 -0
- package/dist/llm/browser-config.js +83 -0
- package/dist/llm/browser-config.js.map +1 -0
- package/dist/llm/client.d.ts +268 -0
- package/dist/llm/client.js +1094 -0
- package/dist/llm/client.js.map +1 -0
- package/dist/llm/config.d.ts +79 -0
- package/dist/llm/config.js +375 -0
- package/dist/llm/config.js.map +1 -0
- package/dist/llm/credentials.d.ts +35 -0
- package/dist/llm/credentials.js +491 -0
- package/dist/llm/credentials.js.map +1 -0
- package/dist/llm/external-creds.d.ts +42 -0
- package/dist/llm/external-creds.js +169 -0
- package/dist/llm/external-creds.js.map +1 -0
- package/dist/llm/providers.d.ts +123 -0
- package/dist/llm/providers.js +717 -0
- package/dist/llm/providers.js.map +1 -0
- package/dist/paths.d.ts +31 -0
- package/dist/paths.js +147 -0
- package/dist/paths.js.map +1 -0
- package/dist/platform/accessibility.d.ts +139 -0
- package/dist/platform/accessibility.js +670 -0
- package/dist/platform/accessibility.js.map +1 -0
- package/dist/platform/cdp-driver.d.ts +318 -0
- package/dist/platform/cdp-driver.js +1179 -0
- package/dist/platform/cdp-driver.js.map +1 -0
- package/dist/platform/index.d.ts +11 -0
- package/dist/platform/index.js +69 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/keys.d.ts +17 -0
- package/dist/platform/keys.js +129 -0
- package/dist/platform/keys.js.map +1 -0
- package/dist/platform/launch-poll.d.ts +101 -0
- package/dist/platform/launch-poll.js +177 -0
- package/dist/platform/launch-poll.js.map +1 -0
- package/dist/platform/linux.d.ts +173 -0
- package/dist/platform/linux.js +1253 -0
- package/dist/platform/linux.js.map +1 -0
- package/dist/platform/macos.d.ts +136 -0
- package/dist/platform/macos.js +976 -0
- package/dist/platform/macos.js.map +1 -0
- package/dist/platform/native-desktop.d.ts +145 -0
- package/dist/platform/native-desktop.js +936 -0
- package/dist/platform/native-desktop.js.map +1 -0
- package/dist/platform/native-helper.d.ts +130 -0
- package/dist/platform/native-helper.js +592 -0
- package/dist/platform/native-helper.js.map +1 -0
- package/dist/platform/ocr-engine.d.ts +78 -0
- package/dist/platform/ocr-engine.js +363 -0
- package/dist/platform/ocr-engine.js.map +1 -0
- package/dist/platform/ps-runner.d.ts +28 -0
- package/dist/platform/ps-runner.js +228 -0
- package/dist/platform/ps-runner.js.map +1 -0
- package/dist/platform/types.d.ts +397 -0
- package/dist/platform/types.js +15 -0
- package/dist/platform/types.js.map +1 -0
- package/dist/platform/uri-handler.d.ts +75 -0
- package/dist/platform/uri-handler.js +273 -0
- package/dist/platform/uri-handler.js.map +1 -0
- package/dist/platform/wayland-backend.d.ts +53 -0
- package/dist/platform/wayland-backend.js +348 -0
- package/dist/platform/wayland-backend.js.map +1 -0
- package/dist/platform/windows.d.ts +232 -0
- package/dist/platform/windows.js +1210 -0
- package/dist/platform/windows.js.map +1 -0
- package/dist/postbuild.d.ts +10 -0
- package/dist/postbuild.js +98 -0
- package/dist/postbuild.js.map +1 -0
- package/dist/schema/snapshot.d.ts +33 -0
- package/dist/schema/snapshot.js +90 -0
- package/dist/schema/snapshot.js.map +1 -0
- package/dist/shortcuts.d.ts +30 -0
- package/dist/shortcuts.js +261 -0
- package/dist/shortcuts.js.map +1 -0
- package/dist/surface/cli.d.ts +7 -0
- package/dist/surface/cli.js +1556 -0
- package/dist/surface/cli.js.map +1 -0
- package/dist/surface/dashboard.d.ts +8 -0
- package/dist/surface/dashboard.js +1193 -0
- package/dist/surface/dashboard.js.map +1 -0
- package/dist/surface/doctor.d.ts +29 -0
- package/dist/surface/doctor.js +1514 -0
- package/dist/surface/doctor.js.map +1 -0
- package/dist/surface/format.d.ts +10 -0
- package/dist/surface/format.js +37 -0
- package/dist/surface/format.js.map +1 -0
- package/dist/surface/http-utility.d.ts +65 -0
- package/dist/surface/http-utility.js +336 -0
- package/dist/surface/http-utility.js.map +1 -0
- package/dist/surface/mcp-server.d.ts +91 -0
- package/dist/surface/mcp-server.js +280 -0
- package/dist/surface/mcp-server.js.map +1 -0
- package/dist/surface/onboarding.d.ts +15 -0
- package/dist/surface/onboarding.js +184 -0
- package/dist/surface/onboarding.js.map +1 -0
- package/dist/surface/pidfile.d.ts +79 -0
- package/dist/surface/pidfile.js +263 -0
- package/dist/surface/pidfile.js.map +1 -0
- package/dist/surface/readiness.d.ts +45 -0
- package/dist/surface/readiness.js +230 -0
- package/dist/surface/readiness.js.map +1 -0
- package/dist/surface/report.d.ts +68 -0
- package/dist/surface/report.js +341 -0
- package/dist/surface/report.js.map +1 -0
- package/dist/surface/skill-register.d.ts +14 -0
- package/dist/surface/skill-register.js +150 -0
- package/dist/surface/skill-register.js.map +1 -0
- package/dist/surface/version.d.ts +6 -0
- package/dist/surface/version.js +27 -0
- package/dist/surface/version.js.map +1 -0
- package/dist/tools/a11y.d.ts +8 -0
- package/dist/tools/a11y.js +545 -0
- package/dist/tools/a11y.js.map +1 -0
- package/dist/tools/a11y_depth.d.ts +19 -0
- package/dist/tools/a11y_depth.js +455 -0
- package/dist/tools/a11y_depth.js.map +1 -0
- package/dist/tools/agent.d.ts +15 -0
- package/dist/tools/agent.js +248 -0
- package/dist/tools/agent.js.map +1 -0
- package/dist/tools/batch.d.ts +46 -0
- package/dist/tools/batch.js +230 -0
- package/dist/tools/batch.js.map +1 -0
- package/dist/tools/cdp.d.ts +8 -0
- package/dist/tools/cdp.js +233 -0
- package/dist/tools/cdp.js.map +1 -0
- package/dist/tools/compact.d.ts +63 -0
- package/dist/tools/compact.js +418 -0
- package/dist/tools/compact.js.map +1 -0
- package/dist/tools/cost-class.d.ts +38 -0
- package/dist/tools/cost-class.js +117 -0
- package/dist/tools/cost-class.js.map +1 -0
- package/dist/tools/desktop.d.ts +9 -0
- package/dist/tools/desktop.js +346 -0
- package/dist/tools/desktop.js.map +1 -0
- package/dist/tools/electron_bridge.d.ts +41 -0
- package/dist/tools/electron_bridge.js +261 -0
- package/dist/tools/electron_bridge.js.map +1 -0
- package/dist/tools/extras.d.ts +22 -0
- package/dist/tools/extras.js +942 -0
- package/dist/tools/extras.js.map +1 -0
- package/dist/tools/favorites.d.ts +13 -0
- package/dist/tools/favorites.js +137 -0
- package/dist/tools/favorites.js.map +1 -0
- package/dist/tools/introspection.d.ts +13 -0
- package/dist/tools/introspection.js +55 -0
- package/dist/tools/introspection.js.map +1 -0
- package/dist/tools/ocr.d.ts +8 -0
- package/dist/tools/ocr.js +66 -0
- package/dist/tools/ocr.js.map +1 -0
- package/dist/tools/orchestration.d.ts +7 -0
- package/dist/tools/orchestration.js +377 -0
- package/dist/tools/orchestration.js.map +1 -0
- package/dist/tools/playbooks/extract-compose.d.ts +22 -0
- package/dist/tools/playbooks/extract-compose.js +85 -0
- package/dist/tools/playbooks/extract-compose.js.map +1 -0
- package/dist/tools/playbooks/find-replace.d.ts +11 -0
- package/dist/tools/playbooks/find-replace.js +56 -0
- package/dist/tools/playbooks/find-replace.js.map +1 -0
- package/dist/tools/playbooks/index.d.ts +63 -0
- package/dist/tools/playbooks/index.js +70 -0
- package/dist/tools/playbooks/index.js.map +1 -0
- package/dist/tools/playbooks/keys-blocklist.d.ts +24 -0
- package/dist/tools/playbooks/keys-blocklist.js +89 -0
- package/dist/tools/playbooks/keys-blocklist.js.map +1 -0
- package/dist/tools/registry.d.ts +40 -0
- package/dist/tools/registry.js +560 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/safety-gate.d.ts +16 -0
- package/dist/tools/safety-gate.js +70 -0
- package/dist/tools/safety-gate.js.map +1 -0
- package/dist/tools/scheduler.d.ts +76 -0
- package/dist/tools/scheduler.js +413 -0
- package/dist/tools/scheduler.js.map +1 -0
- package/dist/tools/shortcuts.d.ts +13 -0
- package/dist/tools/shortcuts.js +205 -0
- package/dist/tools/shortcuts.js.map +1 -0
- package/dist/tools/smart.d.ts +15 -0
- package/dist/tools/smart.js +785 -0
- package/dist/tools/smart.js.map +1 -0
- package/dist/tools/types.d.ts +174 -0
- package/dist/tools/types.js +67 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/window-text.d.ts +15 -0
- package/dist/tools/window-text.js +39 -0
- package/dist/tools/window-text.js.map +1 -0
- package/dist/types.d.ts +122 -0
- package/dist/types.js +41 -0
- package/dist/types.js.map +1 -0
- package/native/Package.swift +38 -0
- package/native/README.md +113 -0
- package/native/Sources/ClawdCursorHelper/main.swift +602 -0
- package/native/Sources/ClawdCursorHost/main.swift +182 -0
- package/native/Sources/PermissionCheck/main.swift +53 -0
- package/native/Sources/ScreenshotHelper/main.swift +219 -0
- package/native/build.sh +139 -0
- package/native/entitlements.plist +12 -0
- package/package.json +115 -0
- package/scripts/banner.ps1 +112 -0
- package/scripts/coord-accuracy.ps1 +140 -0
- package/scripts/coord-uwp.ps1 +80 -0
- package/scripts/edge-glow.ps1 +180 -0
- package/scripts/find-element.ps1 +198 -0
- package/scripts/get-foreground-window.ps1 +71 -0
- package/scripts/get-screen-context.ps1 +183 -0
- package/scripts/get-windows.ps1 +66 -0
- package/scripts/install-panic-hotkey.ps1 +46 -0
- package/scripts/interact-element.ps1 +431 -0
- package/scripts/invoke-element.ps1 +314 -0
- package/scripts/linux/atspi-bridge.py +356 -0
- package/scripts/linux/ocr-recognize.py +154 -0
- package/scripts/mac/_window-picker.jxa +163 -0
- package/scripts/mac/find-element.jxa +0 -0
- package/scripts/mac/find-element.sh +161 -0
- package/scripts/mac/focus-window.jxa +284 -0
- package/scripts/mac/get-focused-element.jxa +102 -0
- package/scripts/mac/get-foreground-window.jxa +173 -0
- package/scripts/mac/get-screen-context.jxa +197 -0
- package/scripts/mac/get-ui-tree.sh +141 -0
- package/scripts/mac/get-windows.jxa +117 -0
- package/scripts/mac/interact-element.sh +235 -0
- package/scripts/mac/invoke-element.jxa +408 -0
- package/scripts/mac/ocr-recognize.swift +124 -0
- package/scripts/ocr-recognize.ps1 +102 -0
- package/scripts/postinstall-native.js +48 -0
- package/scripts/ps-bridge.ps1 +830 -0
- package/scripts/smoke-mcp.ps1 +119 -0
- package/scripts/sync-version.ts +178 -0
- package/scripts/verify-install.js +81 -0
|
@@ -0,0 +1,1514 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 🩺 Clawd Cursor Doctor - diagnoses setup and auto-configures the pipeline.
|
|
4
|
+
*
|
|
5
|
+
* Phases:
|
|
6
|
+
* 1. Screen capture test (nut-js)
|
|
7
|
+
* 2. Accessibility bridge test (PowerShell / osascript)
|
|
8
|
+
* 3. AI provider scan — all providers in parallel
|
|
9
|
+
* 4. Model verification — text: instruction-following, vision: real image input
|
|
10
|
+
* 5. Smoke test — a11y→LLM round-trip (reads active window, confirms via model)
|
|
11
|
+
* 6. Interactive pipeline selection
|
|
12
|
+
* 7. Save config
|
|
13
|
+
*/
|
|
14
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
17
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
18
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
19
|
+
}
|
|
20
|
+
Object.defineProperty(o, k2, desc);
|
|
21
|
+
}) : (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
o[k2] = m[k];
|
|
24
|
+
}));
|
|
25
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
26
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
27
|
+
}) : function(o, v) {
|
|
28
|
+
o["default"] = v;
|
|
29
|
+
});
|
|
30
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
31
|
+
var ownKeys = function(o) {
|
|
32
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
33
|
+
var ar = [];
|
|
34
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
35
|
+
return ar;
|
|
36
|
+
};
|
|
37
|
+
return ownKeys(o);
|
|
38
|
+
};
|
|
39
|
+
return function (mod) {
|
|
40
|
+
if (mod && mod.__esModule) return mod;
|
|
41
|
+
var result = {};
|
|
42
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
43
|
+
__setModuleDefault(result, mod);
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
})();
|
|
47
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
48
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
49
|
+
};
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
exports.quickSetup = quickSetup;
|
|
52
|
+
exports.runDoctor = runDoctor;
|
|
53
|
+
exports.loadPipelineConfig = loadPipelineConfig;
|
|
54
|
+
const fs = __importStar(require("fs"));
|
|
55
|
+
const path = __importStar(require("path"));
|
|
56
|
+
const readline = __importStar(require("readline"));
|
|
57
|
+
const child_process_1 = require("child_process");
|
|
58
|
+
const util_1 = require("util");
|
|
59
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
60
|
+
const native_desktop_1 = require("../platform/native-desktop");
|
|
61
|
+
const accessibility_1 = require("../platform/accessibility");
|
|
62
|
+
const providers_1 = require("../llm/providers");
|
|
63
|
+
const types_1 = require("../types");
|
|
64
|
+
const paths_1 = require("../paths");
|
|
65
|
+
const credentials_1 = require("../llm/credentials");
|
|
66
|
+
const client_1 = require("../llm/client");
|
|
67
|
+
const onboarding_1 = require("./onboarding");
|
|
68
|
+
const native_helper_1 = require("../platform/native-helper");
|
|
69
|
+
const CONFIG_FILE = '.clawdcursor-config.json';
|
|
70
|
+
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
71
|
+
/**
|
|
72
|
+
* Quick, non-interactive setup for first run auto-configuration.
|
|
73
|
+
* Tests discovered providers with short timeouts and builds the best pipeline.
|
|
74
|
+
* Returns null if no providers work.
|
|
75
|
+
*/
|
|
76
|
+
async function quickSetup() {
|
|
77
|
+
console.log('🔍 Scanning available AI providers...');
|
|
78
|
+
// 1. Scan providers (reuse existing logic)
|
|
79
|
+
const scanResults = await (0, providers_1.scanProviders)();
|
|
80
|
+
const anyAvailable = scanResults.some(s => s.available);
|
|
81
|
+
if (!anyAvailable) {
|
|
82
|
+
console.log('⚠️ No AI providers detected. Layer 1 (Action Router) will still work.');
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
// 2. Quick test available providers (with shorter timeout for first run)
|
|
86
|
+
console.log('⚡ Quick-testing discovered models...');
|
|
87
|
+
const modelTests = await quickTestAllProviders(scanResults);
|
|
88
|
+
const workingText = modelTests.filter(t => t.role === 'text' && t.ok);
|
|
89
|
+
const workingVision = modelTests.filter(t => t.role === 'vision' && t.ok);
|
|
90
|
+
if (workingText.length === 0 && workingVision.length === 0) {
|
|
91
|
+
console.log('⚠️ No working models found. Layer 1 (Action Router) will still work.');
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
// 3. Build best pipeline automatically
|
|
95
|
+
const pipeline = (0, providers_1.buildMixedPipeline)(scanResults, modelTests);
|
|
96
|
+
// 4. Save to .clawdcursor-config.json
|
|
97
|
+
savePipelineConfig(pipeline, scanResults);
|
|
98
|
+
// 5. Return pipeline
|
|
99
|
+
return pipeline;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Quick version of testAllProviders with 5s timeout per provider for auto-setup.
|
|
103
|
+
*/
|
|
104
|
+
async function quickTestAllProviders(scanResults) {
|
|
105
|
+
const promises = [];
|
|
106
|
+
for (const scan of scanResults) {
|
|
107
|
+
if (!scan.available)
|
|
108
|
+
continue;
|
|
109
|
+
const provider = providers_1.PROVIDERS[scan.key];
|
|
110
|
+
if (!provider)
|
|
111
|
+
continue;
|
|
112
|
+
// ── Text model test ──────────────────────────────────────────
|
|
113
|
+
if (scan.key === 'ollama') {
|
|
114
|
+
const ollamaTextModel = pickOllamaTextModel(scan.ollamaModels || []);
|
|
115
|
+
if (ollamaTextModel) {
|
|
116
|
+
promises.push(quickTestModelAsync(provider, scan.apiKey, ollamaTextModel, 'text', scan.key));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
promises.push(quickTestModelAsync(provider, scan.apiKey, provider.textModel, 'text', scan.key));
|
|
121
|
+
}
|
|
122
|
+
// ── Vision model test ────────────────────────────────────────
|
|
123
|
+
if (scan.key === 'ollama') {
|
|
124
|
+
const ollamaVisionModels = scan.ollamaVisionModels || [];
|
|
125
|
+
if (ollamaVisionModels.length > 0) {
|
|
126
|
+
promises.push(quickTestModelAsync(provider, scan.apiKey, ollamaVisionModels[0], 'vision', scan.key));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
promises.push(quickTestModelAsync(provider, scan.apiKey, provider.visionModel, 'vision', scan.key));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const settled = await Promise.allSettled(promises);
|
|
134
|
+
const testResults = [];
|
|
135
|
+
for (const result of settled) {
|
|
136
|
+
if (result.status === 'fulfilled') {
|
|
137
|
+
testResults.push(result.value);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return testResults;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Quick model test with 5s timeout for auto-setup.
|
|
144
|
+
*/
|
|
145
|
+
async function quickTestModelAsync(provider, apiKey, model, role, providerKey) {
|
|
146
|
+
const result = await quickTestModel(provider, apiKey, model, role === 'vision');
|
|
147
|
+
return {
|
|
148
|
+
providerKey,
|
|
149
|
+
model,
|
|
150
|
+
role,
|
|
151
|
+
ok: result.ok,
|
|
152
|
+
latencyMs: result.latencyMs,
|
|
153
|
+
error: result.error,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Quick model test with 5s timeout — uses real tests for both text and vision.
|
|
158
|
+
*/
|
|
159
|
+
async function quickTestModel(provider, apiKey, model, isVision) {
|
|
160
|
+
if (isVision) {
|
|
161
|
+
return testVisionModel(provider, apiKey, model);
|
|
162
|
+
}
|
|
163
|
+
return testTextModel(provider, apiKey, model);
|
|
164
|
+
}
|
|
165
|
+
async function runDoctor(opts) {
|
|
166
|
+
// Doctor is interactive-only. If stdin is not a TTY (e.g. run in background,
|
|
167
|
+
// piped, or via a script), exit immediately instead of hanging forever waiting
|
|
168
|
+
// for user input that will never come.
|
|
169
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
170
|
+
console.error('\n❌ clawdcursor doctor requires an interactive terminal.\n' +
|
|
171
|
+
' Open a terminal window and run: clawdcursor doctor\n' +
|
|
172
|
+
' Do NOT run it in the background, piped, or from a script.\n');
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
const results = [];
|
|
176
|
+
console.log(`\n🩺 Clawd Cursor Doctor - diagnosing your setup...\n`);
|
|
177
|
+
// ─── 0. Version Check ───────────────────────────────────────────
|
|
178
|
+
console.log('📦 Version check...');
|
|
179
|
+
await checkForUpdates(results);
|
|
180
|
+
// ─── 0b. Consent Check ──────────────────────────────────────────
|
|
181
|
+
console.log('📝 Consent check...');
|
|
182
|
+
const consentGranted = (0, onboarding_1.hasConsent)();
|
|
183
|
+
if (consentGranted) {
|
|
184
|
+
results.push({ name: 'Desktop control consent', ok: true, detail: 'Granted' });
|
|
185
|
+
console.log(' ✅ Consent granted');
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
results.push({ name: 'Desktop control consent', ok: false, detail: 'Run: clawdcursor consent' });
|
|
189
|
+
console.log(' ❌ Consent not granted — run: clawdcursor consent');
|
|
190
|
+
}
|
|
191
|
+
// ─── 1. Screen Capture ───────────────────────────────────────────
|
|
192
|
+
console.log('📸 Screen capture...');
|
|
193
|
+
const config = { ...types_1.DEFAULT_CONFIG };
|
|
194
|
+
const desktop = new native_desktop_1.NativeDesktop(config);
|
|
195
|
+
try {
|
|
196
|
+
const start = performance.now();
|
|
197
|
+
await desktop.connect();
|
|
198
|
+
const frame = await desktop.captureForLLM();
|
|
199
|
+
const ms = Math.round(performance.now() - start);
|
|
200
|
+
const size = desktop.getScreenSize();
|
|
201
|
+
results.push({
|
|
202
|
+
name: 'Screen capture',
|
|
203
|
+
ok: true,
|
|
204
|
+
detail: `${size.width}x${size.height}, ${(frame.buffer.length / 1024).toFixed(0)}KB, ${ms}ms`,
|
|
205
|
+
latencyMs: ms,
|
|
206
|
+
});
|
|
207
|
+
console.log(` ✅ ${size.width}x${size.height}, ${ms}ms`);
|
|
208
|
+
desktop.disconnect();
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
results.push({ name: 'Screen capture', ok: false, detail: String(err) });
|
|
212
|
+
console.log(` ❌ ${err}`);
|
|
213
|
+
desktop.disconnect();
|
|
214
|
+
}
|
|
215
|
+
// ─── 1b. macOS Permissions (Screen Recording + Accessibility) ───
|
|
216
|
+
// Uses the SAME canonical path as readiness.ts and CLI status:
|
|
217
|
+
// Host /status → permission-check binary → direct AXIsProcessTrusted fallback
|
|
218
|
+
// This ensures doctor, status, and readiness always agree.
|
|
219
|
+
if ((0, native_helper_1.isMacOS)()) {
|
|
220
|
+
console.log('🍎 macOS permissions (via native permission-check)...');
|
|
221
|
+
try {
|
|
222
|
+
let perms = await (0, native_helper_1.checkPermissionsQuick)();
|
|
223
|
+
// If any permission is missing, trigger system popups to request them
|
|
224
|
+
if (!perms.accessibility || !perms.screenRecording) {
|
|
225
|
+
console.log(' 🔐 Requesting macOS permissions (system dialogs may appear)...');
|
|
226
|
+
try {
|
|
227
|
+
perms = await (0, native_helper_1.requestPermissions)();
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
// If requesting fails, continue with the original check results
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
results.push({
|
|
234
|
+
name: 'macOS Accessibility permission',
|
|
235
|
+
ok: perms.accessibility,
|
|
236
|
+
detail: perms.accessibility
|
|
237
|
+
? 'Granted — clawdcursor can read UI elements'
|
|
238
|
+
: 'DENIED — open System Settings → Privacy & Security → Accessibility → enable ClawdCursor',
|
|
239
|
+
});
|
|
240
|
+
if (perms.accessibility) {
|
|
241
|
+
console.log(' ✅ Accessibility permission granted');
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
console.log(' ❌ Accessibility permission DENIED');
|
|
245
|
+
console.log(' → System Settings → Privacy & Security → Accessibility → enable ClawdCursor');
|
|
246
|
+
}
|
|
247
|
+
results.push({
|
|
248
|
+
name: 'macOS Screen Recording permission',
|
|
249
|
+
ok: perms.screenRecording,
|
|
250
|
+
detail: perms.screenRecording
|
|
251
|
+
? 'Granted — clawdcursor can capture the screen'
|
|
252
|
+
: 'DENIED — open System Settings → Privacy & Security → Screen & System Audio Recording → enable ClawdCursor',
|
|
253
|
+
});
|
|
254
|
+
if (perms.screenRecording) {
|
|
255
|
+
console.log(' ✅ Screen Recording permission granted');
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
console.log(' ❌ Screen Recording permission DENIED');
|
|
259
|
+
console.log(' → System Settings → Privacy & Security → Screen & System Audio Recording → enable ClawdCursor');
|
|
260
|
+
}
|
|
261
|
+
if (perms.bundleId) {
|
|
262
|
+
console.log(` ℹ Checked process: ${perms.bundleId}`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
results.push({ name: 'macOS Accessibility permission', ok: false, detail: `Could not query: ${err}` });
|
|
267
|
+
results.push({ name: 'macOS Screen Recording permission', ok: false, detail: `Could not query: ${err}` });
|
|
268
|
+
console.log(` ❌ Permission check failed: ${err}`);
|
|
269
|
+
console.log(' → Ensure ClawdCursor.app is built: cd native && ./build.sh');
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
// ─── 2. Accessibility Bridge ─────────────────────────────────────
|
|
273
|
+
console.log('♿ Accessibility bridge...');
|
|
274
|
+
const a11y = new accessibility_1.AccessibilityBridge();
|
|
275
|
+
try {
|
|
276
|
+
const start = performance.now();
|
|
277
|
+
const available = await a11y.isShellAvailable();
|
|
278
|
+
if (available) {
|
|
279
|
+
const windows = await a11y.getWindows(true);
|
|
280
|
+
const ms = Math.round(performance.now() - start);
|
|
281
|
+
results.push({
|
|
282
|
+
name: 'Accessibility bridge',
|
|
283
|
+
ok: true,
|
|
284
|
+
detail: `${windows.length} windows detected, ${ms}ms`,
|
|
285
|
+
latencyMs: ms,
|
|
286
|
+
});
|
|
287
|
+
console.log(` ✅ ${windows.length} windows detected, ${ms}ms`);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
results.push({ name: 'Accessibility bridge', ok: false, detail: 'Shell not available' });
|
|
291
|
+
console.log(` ❌ Shell not available`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
catch (err) {
|
|
295
|
+
results.push({ name: 'Accessibility bridge', ok: false, detail: String(err) });
|
|
296
|
+
console.log(` ❌ ${err}`);
|
|
297
|
+
}
|
|
298
|
+
// ─── 3. AI Providers — Multi-Provider Scan ──────────────────────
|
|
299
|
+
// If --provider and --api-key are explicitly given, use the legacy single-provider path
|
|
300
|
+
if (opts.apiKey && (opts.provider || opts.baseUrl || opts.textModel || opts.visionModel)) {
|
|
301
|
+
return runSingleProviderFlow(opts, results);
|
|
302
|
+
}
|
|
303
|
+
// Otherwise scan ALL providers in parallel
|
|
304
|
+
console.log(`\n🔍 Scanning providers...`);
|
|
305
|
+
const scanResults = await (0, providers_1.scanProviders)();
|
|
306
|
+
// If --api-key is given without --provider, inject it into scan results
|
|
307
|
+
if (opts.apiKey) {
|
|
308
|
+
const detectedKey = (0, providers_1.detectProvider)(opts.apiKey, opts.provider);
|
|
309
|
+
const existing = scanResults.find(s => s.key === detectedKey);
|
|
310
|
+
if (existing) {
|
|
311
|
+
existing.available = true;
|
|
312
|
+
existing.apiKey = opts.apiKey;
|
|
313
|
+
existing.detail = `key provided via CLI (${opts.apiKey.substring(0, 8)}...)`;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
// Print scan results
|
|
317
|
+
for (const scan of scanResults) {
|
|
318
|
+
const icon = scan.available ? '✅' : '❌';
|
|
319
|
+
const padded = (scan.name + ':').padEnd(20);
|
|
320
|
+
console.log(` ${padded} ${icon} ${scan.detail}`);
|
|
321
|
+
}
|
|
322
|
+
// Show unavailable cloud providers with setup instructions
|
|
323
|
+
const unavailableCloud = scanResults.filter(s => !s.available && s.key !== 'ollama');
|
|
324
|
+
if (unavailableCloud.length > 0) {
|
|
325
|
+
console.log(`\n 💡 Cloud providers not configured (add API keys to unlock):`);
|
|
326
|
+
const keyInfo = {
|
|
327
|
+
anthropic: 'ANTHROPIC_API_KEY — https://console.anthropic.com (vision + computer use)',
|
|
328
|
+
openai: 'OPENAI_API_KEY — https://platform.openai.com (GPT-4o vision)',
|
|
329
|
+
kimi: 'MOONSHOT_API_KEY — https://platform.moonshot.cn (256k context)',
|
|
330
|
+
groq: 'GROQ_API_KEY — https://console.groq.com (fast inference)',
|
|
331
|
+
together: 'TOGETHER_API_KEY — https://api.together.xyz (open models)',
|
|
332
|
+
deepseek: 'DEEPSEEK_API_KEY — https://platform.deepseek.com (reasoning)',
|
|
333
|
+
gemini: 'GEMINI_API_KEY — https://aistudio.google.com (Gemini 2.5 Flash — budget pick, 1M ctx, handles text+vision with one model)',
|
|
334
|
+
mistral: 'MISTRAL_API_KEY — https://console.mistral.ai (Pixtral vision)',
|
|
335
|
+
xai: 'XAI_API_KEY — https://console.x.ai (Grok vision)',
|
|
336
|
+
alibaba: 'DASHSCOPE_API_KEY — https://dashscope.console.aliyun.com (Qwen)',
|
|
337
|
+
fireworks: 'FIREWORKS_API_KEY — https://fireworks.ai (fast open models)',
|
|
338
|
+
cohere: 'COHERE_API_KEY — https://dashboard.cohere.com (Command R)',
|
|
339
|
+
perplexity: 'PERPLEXITY_API_KEY — https://www.perplexity.ai (online search)',
|
|
340
|
+
};
|
|
341
|
+
for (const scan of unavailableCloud) {
|
|
342
|
+
if (keyInfo[scan.key]) {
|
|
343
|
+
console.log(` ${scan.name}: set ${keyInfo[scan.key]}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
console.log(` Set in .env file or as environment variable, then re-run: clawdcursor doctor`);
|
|
347
|
+
// Offer to add a provider interactively
|
|
348
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
349
|
+
const rlSetup = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
350
|
+
const ask = (q) => new Promise(resolve => rlSetup.question(q, resolve));
|
|
351
|
+
// Step 1: Pick a provider
|
|
352
|
+
const providerList = [
|
|
353
|
+
{ key: 'anthropic', label: 'Anthropic (Claude)', envVar: 'ANTHROPIC_API_KEY' },
|
|
354
|
+
{ key: 'openai', label: 'OpenAI (GPT-4o)', envVar: 'OPENAI_API_KEY' },
|
|
355
|
+
{ key: 'kimi', label: 'Kimi / Moonshot', envVar: 'MOONSHOT_API_KEY' },
|
|
356
|
+
{ key: 'gemini', label: 'Google Gemini', envVar: 'GEMINI_API_KEY' },
|
|
357
|
+
{ key: 'groq', label: 'Groq', envVar: 'GROQ_API_KEY' },
|
|
358
|
+
{ key: 'deepseek', label: 'DeepSeek', envVar: 'DEEPSEEK_API_KEY' },
|
|
359
|
+
{ key: 'together', label: 'Together AI', envVar: 'TOGETHER_API_KEY' },
|
|
360
|
+
{ key: 'mistral', label: 'Mistral AI', envVar: 'MISTRAL_API_KEY' },
|
|
361
|
+
{ key: 'xai', label: 'xAI (Grok)', envVar: 'XAI_API_KEY' },
|
|
362
|
+
{ key: 'alibaba', label: 'Alibaba (Qwen)', envVar: 'DASHSCOPE_API_KEY' },
|
|
363
|
+
{ key: 'fireworks', label: 'Fireworks AI', envVar: 'FIREWORKS_API_KEY' },
|
|
364
|
+
{ key: 'cohere', label: 'Cohere', envVar: 'COHERE_API_KEY' },
|
|
365
|
+
{ key: 'perplexity', label: 'Perplexity', envVar: 'PERPLEXITY_API_KEY' },
|
|
366
|
+
];
|
|
367
|
+
console.log('\n Select a provider to configure (or Enter to skip):\n');
|
|
368
|
+
for (let i = 0; i < providerList.length; i++) {
|
|
369
|
+
const p = providerList[i];
|
|
370
|
+
const existing = scanResults.find(s => s.key === p.key);
|
|
371
|
+
const status = existing?.available ? ' ✅ (key found)' : '';
|
|
372
|
+
console.log(` ${String(i + 1).padStart(2)}. ${p.label}${status}`);
|
|
373
|
+
}
|
|
374
|
+
const choice = await ask('\n Enter number (1-13) or press Enter to skip: ');
|
|
375
|
+
const choiceNum = parseInt(choice.trim());
|
|
376
|
+
if (choiceNum >= 1 && choiceNum <= providerList.length) {
|
|
377
|
+
const selected = providerList[choiceNum - 1];
|
|
378
|
+
console.log(`\n Selected: ${selected.label}`);
|
|
379
|
+
// Step 2: Paste the key
|
|
380
|
+
const keyInput = await ask(` 🔑 Paste your ${selected.label} API key: `);
|
|
381
|
+
const trimmedKey = keyInput.trim();
|
|
382
|
+
if (trimmedKey) {
|
|
383
|
+
const matchingScan = scanResults.find(s => s.key === selected.key);
|
|
384
|
+
if (matchingScan) {
|
|
385
|
+
matchingScan.available = true;
|
|
386
|
+
matchingScan.apiKey = trimmedKey;
|
|
387
|
+
matchingScan.detail = `key added (${trimmedKey.substring(0, 8)}...)`;
|
|
388
|
+
}
|
|
389
|
+
// Save to .env
|
|
390
|
+
const envPath = path.join(process.cwd(), '.env');
|
|
391
|
+
const envLine = `${selected.envVar}=${trimmedKey}\n`;
|
|
392
|
+
try {
|
|
393
|
+
fs.appendFileSync(envPath, envLine);
|
|
394
|
+
console.log(` 💾 Saved to .env as ${selected.envVar}`);
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
console.log(` ⚠️ Could not save to .env — set ${selected.envVar}=${trimmedKey} manually`);
|
|
398
|
+
}
|
|
399
|
+
console.log(` ✅ ${selected.label} configured! Testing...`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
rlSetup.close();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
const anyAvailable = scanResults.some(s => s.available);
|
|
406
|
+
if (!anyAvailable) {
|
|
407
|
+
// Nothing available at all — show setup instructions
|
|
408
|
+
printNoProvidersHelp(results);
|
|
409
|
+
return (0, providers_1.buildMixedPipeline)(scanResults, []);
|
|
410
|
+
}
|
|
411
|
+
// ─── 4. Test discovered providers ───────────────────────────────
|
|
412
|
+
console.log(`\n Testing models...`);
|
|
413
|
+
const modelTests = await testAllProviders(scanResults);
|
|
414
|
+
// Print test results
|
|
415
|
+
for (const test of modelTests) {
|
|
416
|
+
const icon = test.ok ? '✅' : '❌';
|
|
417
|
+
const providerName = providers_1.PROVIDERS[test.providerKey]?.name || test.providerKey;
|
|
418
|
+
const latency = test.latencyMs ? `${test.latencyMs}ms` : test.error || 'failed';
|
|
419
|
+
console.log(` ${test.role === 'text' ? 'Text: ' : 'Vision:'} ${test.model} (${providerName}) ${icon} ${latency}`);
|
|
420
|
+
}
|
|
421
|
+
const workingText = modelTests.filter(t => t.role === 'text' && t.ok);
|
|
422
|
+
const workingVision = modelTests.filter(t => t.role === 'vision' && t.ok);
|
|
423
|
+
if (workingText.length > 0) {
|
|
424
|
+
results.push({
|
|
425
|
+
name: 'Text model',
|
|
426
|
+
ok: true,
|
|
427
|
+
detail: workingText.map(t => `${t.model} via ${t.providerKey}`).join(', '),
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
results.push({ name: 'Text model', ok: false, detail: 'No working text model found' });
|
|
432
|
+
}
|
|
433
|
+
if (workingVision.length > 0) {
|
|
434
|
+
results.push({
|
|
435
|
+
name: 'Vision model',
|
|
436
|
+
ok: true,
|
|
437
|
+
detail: workingVision.map(t => `${t.model} via ${t.providerKey}`).join(', '),
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
results.push({ name: 'Vision model', ok: false, detail: 'No working vision model found' });
|
|
442
|
+
}
|
|
443
|
+
// ─── 5. Smoke Test — end-to-end pipeline sanity ─────────────
|
|
444
|
+
if (workingText.length > 0) {
|
|
445
|
+
console.log(`\n🧪 Smoke test...`);
|
|
446
|
+
const bestText = workingText[0];
|
|
447
|
+
const smokeProvider = providers_1.PROVIDERS[bestText.providerKey];
|
|
448
|
+
const smokeScan = scanResults.find(s => s.key === bestText.providerKey);
|
|
449
|
+
const smokeKey = smokeScan?.apiKey || '';
|
|
450
|
+
// Quick round-trip: read active window title via a11y, ask LLM to echo it
|
|
451
|
+
try {
|
|
452
|
+
const smokeA11y = new accessibility_1.AccessibilityBridge();
|
|
453
|
+
const activeWin = await smokeA11y.getActiveWindow();
|
|
454
|
+
const windowTitle = activeWin?.title || 'Terminal';
|
|
455
|
+
// Self-test prompt — explicit framing so safety-trained models (Anthropic
|
|
456
|
+
// Haiku/Sonnet, GPT-4o-mini) don't decline thinking it's prompt
|
|
457
|
+
// injection. Earlier "Reply with exactly: SMOKE_PASS" without context
|
|
458
|
+
// had Sonnet politely refusing with "I appreciate your message but…".
|
|
459
|
+
const smokeSystem = 'You are running a startup self-test for the clawdcursor desktop-automation CLI. ' +
|
|
460
|
+
'The user already configured your API key and is verifying that round-trip calls work. ' +
|
|
461
|
+
'For this self-test ONLY, follow the literal-reply instruction below — no commentary, no questions, no safety addendum. ' +
|
|
462
|
+
'A conformant response is one short token. Anything else fails the test.';
|
|
463
|
+
const smokeInstruction = `Self-test ping. The active window title on this machine right now is "${windowTitle}". ` +
|
|
464
|
+
`To confirm the round-trip works, respond with exactly this token and nothing else: SMOKE_PASS`;
|
|
465
|
+
let smokeText = '';
|
|
466
|
+
if (smokeProvider.openaiCompat) {
|
|
467
|
+
const res = await fetch(`${smokeProvider.baseUrl}/chat/completions`, {
|
|
468
|
+
method: 'POST',
|
|
469
|
+
headers: { 'Content-Type': 'application/json', ...smokeProvider.authHeader(smokeKey) },
|
|
470
|
+
body: JSON.stringify({
|
|
471
|
+
model: bestText.model, max_tokens: 15, temperature: 0,
|
|
472
|
+
messages: [
|
|
473
|
+
{ role: 'system', content: smokeSystem },
|
|
474
|
+
{ role: 'user', content: smokeInstruction },
|
|
475
|
+
],
|
|
476
|
+
}),
|
|
477
|
+
signal: AbortSignal.timeout(8000),
|
|
478
|
+
});
|
|
479
|
+
const data = await res.json();
|
|
480
|
+
smokeText = data.choices?.[0]?.message?.content || '';
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
const res = await fetch(`${smokeProvider.baseUrl}/messages`, {
|
|
484
|
+
method: 'POST',
|
|
485
|
+
headers: { 'Content-Type': 'application/json', ...smokeProvider.authHeader(smokeKey), ...smokeProvider.extraHeaders },
|
|
486
|
+
body: JSON.stringify({
|
|
487
|
+
model: bestText.model, max_tokens: 15,
|
|
488
|
+
system: smokeSystem,
|
|
489
|
+
messages: [{ role: 'user', content: smokeInstruction }],
|
|
490
|
+
}),
|
|
491
|
+
signal: AbortSignal.timeout(8000),
|
|
492
|
+
});
|
|
493
|
+
const data = await res.json();
|
|
494
|
+
smokeText = data.content?.[0]?.text || '';
|
|
495
|
+
}
|
|
496
|
+
const smokeOk = smokeText.includes('SMOKE_PASS');
|
|
497
|
+
if (smokeOk) {
|
|
498
|
+
console.log(` ✅ A11y → LLM round-trip passed (window: "${windowTitle}")`);
|
|
499
|
+
results.push({ name: 'Smoke test (a11y→LLM)', ok: true, detail: `Window "${windowTitle}" — model confirmed` });
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
console.log(` ⚠️ LLM responded but didn't confirm (got: "${smokeText.substring(0, 40)}")`);
|
|
503
|
+
results.push({ name: 'Smoke test (a11y→LLM)', ok: false, detail: `Model didn't follow instruction: "${smokeText.substring(0, 40)}"` });
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
catch (err) {
|
|
507
|
+
console.log(` ⚠️ Smoke test skipped: ${err}`);
|
|
508
|
+
results.push({ name: 'Smoke test (a11y→LLM)', ok: false, detail: `Error: ${err}` });
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
// ─── 6. Interactive provider/model selection ───────────────────
|
|
512
|
+
const recommendedPipeline = (0, providers_1.buildMixedPipeline)(scanResults, modelTests);
|
|
513
|
+
const gpuInfo = await detectGpuInfo();
|
|
514
|
+
if (gpuInfo) {
|
|
515
|
+
console.log(`\n🎮 GPU detected: ${gpuInfo}`);
|
|
516
|
+
}
|
|
517
|
+
const allVision = modelTests.filter(t => t.role === 'vision');
|
|
518
|
+
const selected = await promptPipelineSelection(workingText, workingVision, allVision, recommendedPipeline);
|
|
519
|
+
const pipeline = buildPipelineFromSelection(scanResults, selected);
|
|
520
|
+
console.log(`\n🧠 Selected pipeline:`);
|
|
521
|
+
console.log(` Layer 1: Action Router (offline) ✅`);
|
|
522
|
+
console.log(` Layer 2: ${pipeline.layer2.enabled ? `${pipeline.layer2.model} via ${providerNameForUrl(pipeline.layer2.baseUrl)}` : 'DISABLED'} ${pipeline.layer2.enabled ? '✅' : '❌'}`);
|
|
523
|
+
console.log(` Layer 3: ${pipeline.layer3.enabled ? `${pipeline.layer3.model} via ${providerNameForUrl(pipeline.layer3.baseUrl)}` : 'DISABLED'} ${pipeline.layer3.enabled ? '✅' : '❌'}`);
|
|
524
|
+
if (pipeline.layer3.computerUse) {
|
|
525
|
+
console.log(` 🖥️ Computer Use API: enabled (Anthropic native)`);
|
|
526
|
+
}
|
|
527
|
+
// ─── 7. Save Config ─────────────────────────────────────────────
|
|
528
|
+
if (opts.save !== false) {
|
|
529
|
+
savePipelineConfig(pipeline, scanResults);
|
|
530
|
+
}
|
|
531
|
+
// ─── 8. External Skill Registration (optional) ─────────────────
|
|
532
|
+
await registerExternalSkills(results);
|
|
533
|
+
// ─── Summary ────────────────────────────────────────────────────
|
|
534
|
+
printSummary(results, pipeline);
|
|
535
|
+
return pipeline;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Legacy single-provider flow — used when both --provider and --api-key are explicitly given.
|
|
539
|
+
* Preserves backward compatibility with CLI flags.
|
|
540
|
+
*/
|
|
541
|
+
async function runSingleProviderFlow(opts, results) {
|
|
542
|
+
const resolvedApi = (0, credentials_1.resolveApiConfig)(opts);
|
|
543
|
+
const apiKey = resolvedApi.apiKey;
|
|
544
|
+
const providerKey = (0, providers_1.detectProvider)(apiKey, opts.provider);
|
|
545
|
+
const baseProvider = providers_1.PROVIDERS[providerKey];
|
|
546
|
+
const provider = opts.baseUrl
|
|
547
|
+
? {
|
|
548
|
+
...baseProvider,
|
|
549
|
+
name: `${baseProvider.name} (OpenAI-compatible endpoint)`,
|
|
550
|
+
baseUrl: opts.baseUrl,
|
|
551
|
+
openaiCompat: true,
|
|
552
|
+
computerUse: false,
|
|
553
|
+
textModel: opts.textModel || baseProvider.textModel,
|
|
554
|
+
visionModel: opts.visionModel || baseProvider.visionModel,
|
|
555
|
+
}
|
|
556
|
+
: baseProvider;
|
|
557
|
+
console.log(`\n🔑 AI Provider: ${provider.name} (explicit override)`);
|
|
558
|
+
let textModelWorks = false;
|
|
559
|
+
let visionModelWorks = false;
|
|
560
|
+
let textModel = opts.textModel || provider.textModel;
|
|
561
|
+
const visionModel = opts.visionModel || provider.visionModel;
|
|
562
|
+
// Test text model (Layer 2)
|
|
563
|
+
console.log(` Testing ${textModel} (text)...`);
|
|
564
|
+
const textResult = await testModel(provider, apiKey, textModel, false);
|
|
565
|
+
if (textResult.ok) {
|
|
566
|
+
textModelWorks = true;
|
|
567
|
+
results.push({
|
|
568
|
+
name: `Text model (${textModel})`,
|
|
569
|
+
ok: true,
|
|
570
|
+
detail: `${textResult.latencyMs}ms`,
|
|
571
|
+
latencyMs: textResult.latencyMs,
|
|
572
|
+
});
|
|
573
|
+
console.log(` ✅ ${textModel}: ${textResult.latencyMs}ms`);
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
results.push({ name: `Text model (${textModel})`, ok: false, detail: textResult.error || 'Failed' });
|
|
577
|
+
console.log(` ❌ ${textModel}: ${textResult.error}`);
|
|
578
|
+
// Try fallback - if explicit provider fails, try Ollama with best available model
|
|
579
|
+
if (providerKey !== 'ollama') {
|
|
580
|
+
console.log(` 🔄 Trying Ollama fallback...`);
|
|
581
|
+
try {
|
|
582
|
+
const ollamaRes = await fetch('http://localhost:11434/api/tags', { signal: AbortSignal.timeout(3000) });
|
|
583
|
+
if (ollamaRes.ok) {
|
|
584
|
+
const ollamaData = await ollamaRes.json();
|
|
585
|
+
const ollamaModels = (ollamaData.models || []).map((m) => m.name);
|
|
586
|
+
const bestModel = pickOllamaTextModel(ollamaModels);
|
|
587
|
+
if (bestModel) {
|
|
588
|
+
const ollamaResult = await testModel(providers_1.PROVIDERS['ollama'], '', bestModel, false);
|
|
589
|
+
if (ollamaResult.ok) {
|
|
590
|
+
textModelWorks = true;
|
|
591
|
+
textModel = bestModel;
|
|
592
|
+
console.log(` ✅ Ollama ${bestModel}: ${ollamaResult.latencyMs}ms (fallback)`);
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
console.log(` ❌ Ollama not available either`);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
console.log(` ❌ Ollama running but no models pulled`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
console.log(` ❌ Ollama not available either`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
console.log(` ❌ Ollama not available either`);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
// Test vision model (Layer 3) — with actual image
|
|
612
|
+
if (apiKey) {
|
|
613
|
+
console.log(` Testing ${visionModel} (vision)...`);
|
|
614
|
+
const visionResult = await testModel(provider, apiKey, visionModel, true);
|
|
615
|
+
if (visionResult.ok) {
|
|
616
|
+
visionModelWorks = true;
|
|
617
|
+
results.push({
|
|
618
|
+
name: `Vision model (${visionModel})`,
|
|
619
|
+
ok: true,
|
|
620
|
+
detail: `${visionResult.latencyMs}ms`,
|
|
621
|
+
latencyMs: visionResult.latencyMs,
|
|
622
|
+
});
|
|
623
|
+
console.log(` ✅ ${visionModel}: ${visionResult.latencyMs}ms`);
|
|
624
|
+
}
|
|
625
|
+
else {
|
|
626
|
+
results.push({ name: `Vision model (${visionModel})`, ok: false, detail: visionResult.error || 'Failed' });
|
|
627
|
+
console.log(` ❌ ${visionModel}: ${visionResult.error}`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
console.log(` ⚠️ No API key — vision model skipped`);
|
|
632
|
+
results.push({ name: 'Vision model', ok: false, detail: 'No API key' });
|
|
633
|
+
}
|
|
634
|
+
// Build pipeline
|
|
635
|
+
const pipeline = (0, providers_1.buildPipeline)(providerKey, apiKey, textModelWorks, visionModelWorks, textModel !== provider.textModel ? textModel : undefined);
|
|
636
|
+
// Handle mixed providers (e.g., Ollama for text, cloud for vision)
|
|
637
|
+
// If the text model was resolved from Ollama but the main provider is cloud, set Layer 2 to Ollama baseUrl
|
|
638
|
+
if (providerKey !== 'ollama' && pipeline.layer2.model && !pipeline.layer2.baseUrl) {
|
|
639
|
+
// Check if the text model is an Ollama model by testing the Ollama endpoint
|
|
640
|
+
try {
|
|
641
|
+
const testRes = await fetch(`http://localhost:11434/api/show`, {
|
|
642
|
+
method: 'POST',
|
|
643
|
+
body: JSON.stringify({ name: pipeline.layer2.model }),
|
|
644
|
+
signal: AbortSignal.timeout(2000),
|
|
645
|
+
});
|
|
646
|
+
if (testRes.ok) {
|
|
647
|
+
pipeline.layer2.baseUrl = providers_1.PROVIDERS['ollama'].baseUrl;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
catch { /* not Ollama model, leave baseUrl as-is */ }
|
|
651
|
+
}
|
|
652
|
+
console.log(`\n🧠 Recommended pipeline:`);
|
|
653
|
+
console.log(` Layer 1: Action Router (offline, instant) ✅`);
|
|
654
|
+
console.log(` Layer 2: Accessibility Reasoner → ${pipeline.layer2.enabled ? pipeline.layer2.model : 'DISABLED'} ${pipeline.layer2.enabled ? '✅' : '❌'}`);
|
|
655
|
+
console.log(` Layer 3: Screenshot → ${pipeline.layer3.enabled ? pipeline.layer3.model : 'DISABLED'} ${pipeline.layer3.enabled ? '✅' : '❌'}`);
|
|
656
|
+
if (pipeline.layer3.computerUse) {
|
|
657
|
+
console.log(` 🖥️ Computer Use API: enabled (Anthropic native)`);
|
|
658
|
+
}
|
|
659
|
+
// Save Config
|
|
660
|
+
if (opts.save !== false) {
|
|
661
|
+
const configPath = path.join((0, paths_1.getPackageRoot)(), CONFIG_FILE);
|
|
662
|
+
// SECURITY: this file stores provider/model names and base URLs only.
|
|
663
|
+
// API keys are NEVER written here; they must live in env vars or .env files.
|
|
664
|
+
const singleTextEntry = {
|
|
665
|
+
enabled: pipeline.layer2.enabled,
|
|
666
|
+
model: pipeline.layer2.model,
|
|
667
|
+
baseUrl: pipeline.layer2.baseUrl,
|
|
668
|
+
provider: providerKey,
|
|
669
|
+
};
|
|
670
|
+
const singleVisionEntry = {
|
|
671
|
+
enabled: pipeline.layer3.enabled,
|
|
672
|
+
model: pipeline.layer3.model,
|
|
673
|
+
computerUse: pipeline.layer3.computerUse,
|
|
674
|
+
provider: providerKey,
|
|
675
|
+
};
|
|
676
|
+
const configData = {
|
|
677
|
+
provider: providerKey,
|
|
678
|
+
pipeline: {
|
|
679
|
+
textModel: singleTextEntry,
|
|
680
|
+
visionModel: singleVisionEntry,
|
|
681
|
+
layer2: singleTextEntry,
|
|
682
|
+
layer3: singleVisionEntry,
|
|
683
|
+
},
|
|
684
|
+
compilation: {
|
|
685
|
+
ocr: true,
|
|
686
|
+
a11y: true,
|
|
687
|
+
cdp: true,
|
|
688
|
+
parallel: true,
|
|
689
|
+
},
|
|
690
|
+
diagnosedAt: new Date().toISOString(),
|
|
691
|
+
};
|
|
692
|
+
fs.writeFileSync(configPath, JSON.stringify(configData, null, 2));
|
|
693
|
+
console.log(`\n💾 Config saved to ${CONFIG_FILE}`);
|
|
694
|
+
}
|
|
695
|
+
// External Skill Registration (optional)
|
|
696
|
+
await registerExternalSkills(results);
|
|
697
|
+
// Summary
|
|
698
|
+
printSummary(results, pipeline);
|
|
699
|
+
return pipeline;
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Test all available providers in parallel. Returns model test results.
|
|
703
|
+
*/
|
|
704
|
+
async function testAllProviders(scanResults) {
|
|
705
|
+
const promises = [];
|
|
706
|
+
for (const scan of scanResults) {
|
|
707
|
+
if (!scan.available)
|
|
708
|
+
continue;
|
|
709
|
+
const provider = providers_1.PROVIDERS[scan.key];
|
|
710
|
+
if (!provider)
|
|
711
|
+
continue;
|
|
712
|
+
// ── Text model test ──────────────────────────────────────────
|
|
713
|
+
if (scan.key === 'ollama') {
|
|
714
|
+
// For Ollama, pick the best available text model
|
|
715
|
+
const ollamaTextModel = pickOllamaTextModel(scan.ollamaModels || []);
|
|
716
|
+
if (ollamaTextModel) {
|
|
717
|
+
promises.push(testModelAsync(provider, scan.apiKey, ollamaTextModel, 'text', scan.key));
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
else {
|
|
721
|
+
promises.push(testModelAsync(provider, scan.apiKey, provider.textModel, 'text', scan.key));
|
|
722
|
+
}
|
|
723
|
+
// ── Vision model test ────────────────────────────────────────
|
|
724
|
+
if (scan.key === 'ollama') {
|
|
725
|
+
// For Ollama, only test vision if a vision-capable model exists
|
|
726
|
+
const ollamaVisionModels = scan.ollamaVisionModels || [];
|
|
727
|
+
if (ollamaVisionModels.length > 0) {
|
|
728
|
+
promises.push(testModelAsync(provider, scan.apiKey, ollamaVisionModels[0], 'vision', scan.key));
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
// Cloud providers: test vision model
|
|
733
|
+
promises.push(testModelAsync(provider, scan.apiKey, provider.visionModel, 'vision', scan.key));
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
const settled = await Promise.allSettled(promises);
|
|
737
|
+
const testResults = [];
|
|
738
|
+
for (const result of settled) {
|
|
739
|
+
if (result.status === 'fulfilled') {
|
|
740
|
+
testResults.push(result.value);
|
|
741
|
+
}
|
|
742
|
+
// rejected promises are silently dropped — the provider just doesn't work
|
|
743
|
+
}
|
|
744
|
+
return testResults;
|
|
745
|
+
}
|
|
746
|
+
function buildPipelineFromSelection(scanResults, selected) {
|
|
747
|
+
const primaryProviderKey = selected.layer3?.providerKey || selected.layer2?.providerKey || 'ollama';
|
|
748
|
+
const primaryProvider = providers_1.PROVIDERS[primaryProviderKey] || providers_1.PROVIDERS['ollama'];
|
|
749
|
+
const primaryScan = scanResults.find(s => s.key === primaryProviderKey);
|
|
750
|
+
const primaryApiKey = primaryScan?.apiKey || '';
|
|
751
|
+
const layer2Provider = selected.layer2 ? (providers_1.PROVIDERS[selected.layer2.providerKey] || providers_1.PROVIDERS['ollama']) : primaryProvider;
|
|
752
|
+
const layer3Provider = selected.layer3 ? (providers_1.PROVIDERS[selected.layer3.providerKey] || providers_1.PROVIDERS['ollama']) : primaryProvider;
|
|
753
|
+
return {
|
|
754
|
+
provider: primaryProvider,
|
|
755
|
+
providerKey: primaryProviderKey,
|
|
756
|
+
apiKey: primaryApiKey,
|
|
757
|
+
layer1: true,
|
|
758
|
+
layer2: {
|
|
759
|
+
enabled: !!selected.layer2,
|
|
760
|
+
model: selected.layer2?.model || layer2Provider.textModel,
|
|
761
|
+
baseUrl: layer2Provider.baseUrl,
|
|
762
|
+
},
|
|
763
|
+
layer3: {
|
|
764
|
+
enabled: !!selected.layer3,
|
|
765
|
+
model: selected.layer3?.model || layer3Provider.visionModel,
|
|
766
|
+
baseUrl: layer3Provider.baseUrl,
|
|
767
|
+
computerUse: !!selected.layer3 && layer3Provider.computerUse,
|
|
768
|
+
},
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
async function detectGpuInfo() {
|
|
772
|
+
if (process.platform === 'win32') {
|
|
773
|
+
try {
|
|
774
|
+
const { stdout } = await execFileAsync('nvidia-smi', [
|
|
775
|
+
'--query-gpu=name,memory.total',
|
|
776
|
+
'--format=csv,noheader,nounits',
|
|
777
|
+
]);
|
|
778
|
+
const lines = stdout
|
|
779
|
+
.split(/\r?\n/)
|
|
780
|
+
.map(l => l.trim())
|
|
781
|
+
.filter(Boolean);
|
|
782
|
+
if (lines.length === 0)
|
|
783
|
+
return null;
|
|
784
|
+
return lines
|
|
785
|
+
.map(line => {
|
|
786
|
+
const parts = line.split(',').map(p => p.trim());
|
|
787
|
+
return parts.length >= 2 ? `${parts[0]} (${parts[1]} MB VRAM)` : line;
|
|
788
|
+
})
|
|
789
|
+
.join(' | ');
|
|
790
|
+
}
|
|
791
|
+
catch {
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
if (process.platform === 'darwin') {
|
|
796
|
+
try {
|
|
797
|
+
// system_profiler -json is the canonical Mac GPU query.
|
|
798
|
+
const { stdout } = await execFileAsync('system_profiler', [
|
|
799
|
+
'SPDisplaysDataType',
|
|
800
|
+
'-json',
|
|
801
|
+
]);
|
|
802
|
+
const data = JSON.parse(stdout);
|
|
803
|
+
const entries = data?.SPDisplaysDataType ?? [];
|
|
804
|
+
const gpus = await Promise.all(entries.map(async (d) => {
|
|
805
|
+
const name = d['sppci_model'] || d['_name'] || 'Unknown GPU';
|
|
806
|
+
// Discrete GPUs (Intel/AMD/NVIDIA on older Macs) expose VRAM directly.
|
|
807
|
+
const vram = d['spdisplays_vram'] || d['spdisplays_vram_shared'];
|
|
808
|
+
if (vram)
|
|
809
|
+
return `${name} (${vram} VRAM)`;
|
|
810
|
+
// Apple Silicon uses unified memory — show GPU cores + total RAM instead.
|
|
811
|
+
const gpuCores = d['sppci_cores'];
|
|
812
|
+
if (gpuCores) {
|
|
813
|
+
let unifiedMem = '';
|
|
814
|
+
try {
|
|
815
|
+
const { stdout: memOut } = await execFileAsync('sysctl', ['-n', 'hw.memsize']);
|
|
816
|
+
const bytes = parseInt(memOut.trim(), 10);
|
|
817
|
+
if (!Number.isNaN(bytes)) {
|
|
818
|
+
unifiedMem = ` / ${Math.round(bytes / 1073741824)} GB unified`;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
catch { /* ignore */ }
|
|
822
|
+
return `${name} (${gpuCores} GPU cores${unifiedMem})`;
|
|
823
|
+
}
|
|
824
|
+
return name;
|
|
825
|
+
}));
|
|
826
|
+
const filtered = gpus.filter(Boolean);
|
|
827
|
+
return filtered.length > 0 ? filtered.join(' | ') : null;
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
return null;
|
|
834
|
+
}
|
|
835
|
+
async function promptPipelineSelection(workingText, workingVision, allVision, recommended) {
|
|
836
|
+
const recommendedText = recommended.layer2.enabled
|
|
837
|
+
? { providerKey: providerKeyForUrl(recommended.layer2.baseUrl) || recommended.providerKey, model: recommended.layer2.model }
|
|
838
|
+
: null;
|
|
839
|
+
const recommendedVision = recommended.layer3.enabled
|
|
840
|
+
? { providerKey: providerKeyForUrl(recommended.layer3.baseUrl) || recommended.providerKey, model: recommended.layer3.model }
|
|
841
|
+
: null;
|
|
842
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
843
|
+
return {
|
|
844
|
+
layer2: recommendedText,
|
|
845
|
+
layer3: recommendedVision,
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
console.log('\n🧩 Choose your pipeline models (press Enter for recommended).');
|
|
849
|
+
const rl = readline.createInterface({
|
|
850
|
+
input: process.stdin,
|
|
851
|
+
output: process.stdout,
|
|
852
|
+
});
|
|
853
|
+
try {
|
|
854
|
+
const layer2 = await promptCategoryChoice(rl, 'TEXT LLM (Layer 2)', workingText, recommendedText);
|
|
855
|
+
// For vision: if no working models, show ALL tested models (including failed)
|
|
856
|
+
// so the user can still pick one — the test image might have been the issue, not the model.
|
|
857
|
+
const visionOptions = workingVision.length > 0 ? workingVision : allVision;
|
|
858
|
+
const layer3 = await promptCategoryChoice(rl, 'VISION LLM (Layer 3)', visionOptions, recommendedVision, workingVision.length === 0);
|
|
859
|
+
return { layer2, layer3 };
|
|
860
|
+
}
|
|
861
|
+
finally {
|
|
862
|
+
rl.close();
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
async function promptCategoryChoice(rl, title, options, recommendedChoice, showFailedWarning = false) {
|
|
866
|
+
console.log(`\n${title}:`);
|
|
867
|
+
if (options.length === 0) {
|
|
868
|
+
console.log(' No models found. This layer will be disabled.');
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
if (showFailedWarning) {
|
|
872
|
+
console.log(' ⚠️ No models passed auto-test (test image may be the issue, not the model).');
|
|
873
|
+
console.log(' Pick one anyway — most vision models work fine:\n');
|
|
874
|
+
}
|
|
875
|
+
options.forEach((opt, idx) => {
|
|
876
|
+
const providerName = providers_1.PROVIDERS[opt.providerKey]?.name || opt.providerKey;
|
|
877
|
+
const recommendedMark = (recommendedChoice && opt.providerKey === recommendedChoice.providerKey && opt.model === recommendedChoice.model) ? ' ★ recommended' : '';
|
|
878
|
+
const latency = opt.latencyMs ? `, ${opt.latencyMs}ms` : '';
|
|
879
|
+
const status = opt.ok ? '✅' : '⚠️';
|
|
880
|
+
console.log(` ${idx + 1}. ${status} ${opt.model} (${providerName}${latency})${recommendedMark}`);
|
|
881
|
+
});
|
|
882
|
+
const recommendedIndex = recommendedChoice
|
|
883
|
+
? options.findIndex(opt => opt.providerKey === recommendedChoice.providerKey && opt.model === recommendedChoice.model)
|
|
884
|
+
: -1;
|
|
885
|
+
const defaultIndex = recommendedIndex >= 0 ? recommendedIndex : 0;
|
|
886
|
+
const input = await askQuestion(rl, ` Pick 1-${options.length} (Enter=${defaultIndex + 1}): `);
|
|
887
|
+
const trimmed = input.trim();
|
|
888
|
+
if (!trimmed) {
|
|
889
|
+
const selected = options[defaultIndex];
|
|
890
|
+
return { providerKey: selected.providerKey, model: selected.model };
|
|
891
|
+
}
|
|
892
|
+
const selectedIdx = Number(trimmed);
|
|
893
|
+
if (!Number.isInteger(selectedIdx) || selectedIdx < 1 || selectedIdx > options.length) {
|
|
894
|
+
console.log(` Invalid choice "${trimmed}". Using default ${defaultIndex + 1}.`);
|
|
895
|
+
const selected = options[defaultIndex];
|
|
896
|
+
return { providerKey: selected.providerKey, model: selected.model };
|
|
897
|
+
}
|
|
898
|
+
const selected = options[selectedIdx - 1];
|
|
899
|
+
return { providerKey: selected.providerKey, model: selected.model };
|
|
900
|
+
}
|
|
901
|
+
function askQuestion(rl, prompt) {
|
|
902
|
+
return new Promise(resolve => rl.question(prompt, resolve));
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Pick the best Ollama text model from available models.
|
|
906
|
+
* Prefers: qwen2.5 variants, then llama variants, then first available.
|
|
907
|
+
*/
|
|
908
|
+
function pickOllamaTextModel(models) {
|
|
909
|
+
if (models.length === 0)
|
|
910
|
+
return null;
|
|
911
|
+
// Prefer qwen2.5 models (good for tool calling)
|
|
912
|
+
const qwen = models.find(m => m.toLowerCase().startsWith('qwen2.5'));
|
|
913
|
+
if (qwen)
|
|
914
|
+
return qwen;
|
|
915
|
+
// Then llama models
|
|
916
|
+
const llama = models.find(m => m.toLowerCase().startsWith('llama'));
|
|
917
|
+
if (llama)
|
|
918
|
+
return llama;
|
|
919
|
+
// Then qwen3 models
|
|
920
|
+
const qwen3 = models.find(m => m.toLowerCase().startsWith('qwen3'));
|
|
921
|
+
if (qwen3)
|
|
922
|
+
return qwen3;
|
|
923
|
+
// Then deepseek models
|
|
924
|
+
const deepseek = models.find(m => m.toLowerCase().startsWith('deepseek'));
|
|
925
|
+
if (deepseek)
|
|
926
|
+
return deepseek;
|
|
927
|
+
// Skip vision-only models
|
|
928
|
+
const nonVision = models.find(m => !isLikelyVisionOnly(m));
|
|
929
|
+
if (nonVision)
|
|
930
|
+
return nonVision;
|
|
931
|
+
// Last resort: first model
|
|
932
|
+
return models[0];
|
|
933
|
+
}
|
|
934
|
+
function isLikelyVisionOnly(modelId) {
|
|
935
|
+
const lower = modelId.toLowerCase();
|
|
936
|
+
return lower.startsWith('llava') || lower.startsWith('bakllava') || lower.startsWith('moondream');
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Test a model asynchronously, returning a ModelTestResult.
|
|
940
|
+
*/
|
|
941
|
+
async function testModelAsync(provider, apiKey, model, role, providerKey) {
|
|
942
|
+
const result = await testModel(provider, apiKey, model, role === 'vision');
|
|
943
|
+
return {
|
|
944
|
+
providerKey,
|
|
945
|
+
model,
|
|
946
|
+
role,
|
|
947
|
+
ok: result.ok,
|
|
948
|
+
latencyMs: result.latencyMs,
|
|
949
|
+
error: result.error,
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Save pipeline config to disk, including multi-provider info.
|
|
954
|
+
*/
|
|
955
|
+
function savePipelineConfig(pipeline, scanResults) {
|
|
956
|
+
// Always save to the package directory so loadPipelineConfig finds it reliably
|
|
957
|
+
const configPath = path.join((0, paths_1.getPackageRoot)(), CONFIG_FILE);
|
|
958
|
+
// Determine which providers are actually used (kept inline to surface
|
|
959
|
+
// them in logs if debugging routing). The matching scanResults rows
|
|
960
|
+
// aren't needed downstream — the pipeline already has its API keys.
|
|
961
|
+
const layer2ProviderKey = providerKeyForUrl(pipeline.layer2.baseUrl) || pipeline.providerKey;
|
|
962
|
+
const layer3ProviderKey = providerKeyForUrl(pipeline.layer3.baseUrl) || pipeline.providerKey;
|
|
963
|
+
void layer2ProviderKey;
|
|
964
|
+
void layer3ProviderKey;
|
|
965
|
+
const textModelEntry = {
|
|
966
|
+
enabled: pipeline.layer2.enabled,
|
|
967
|
+
model: pipeline.layer2.model,
|
|
968
|
+
baseUrl: pipeline.layer2.baseUrl,
|
|
969
|
+
provider: layer2ProviderKey,
|
|
970
|
+
};
|
|
971
|
+
const visionModelEntry = {
|
|
972
|
+
enabled: pipeline.layer3.enabled,
|
|
973
|
+
model: pipeline.layer3.model,
|
|
974
|
+
baseUrl: pipeline.layer3.baseUrl,
|
|
975
|
+
computerUse: pipeline.layer3.computerUse,
|
|
976
|
+
provider: layer3ProviderKey,
|
|
977
|
+
};
|
|
978
|
+
const configData = {
|
|
979
|
+
provider: pipeline.providerKey,
|
|
980
|
+
pipeline: {
|
|
981
|
+
// Primary field names (v0.7.5+)
|
|
982
|
+
textModel: textModelEntry,
|
|
983
|
+
visionModel: visionModelEntry,
|
|
984
|
+
// Legacy field names for backward compatibility
|
|
985
|
+
layer2: textModelEntry,
|
|
986
|
+
layer3: visionModelEntry,
|
|
987
|
+
},
|
|
988
|
+
// Compilation features — which perception channels are enabled
|
|
989
|
+
compilation: {
|
|
990
|
+
ocr: true,
|
|
991
|
+
a11y: true,
|
|
992
|
+
cdp: true,
|
|
993
|
+
parallel: true,
|
|
994
|
+
},
|
|
995
|
+
// Store API keys by provider so we can reconstruct later
|
|
996
|
+
providerKeys: Object.fromEntries(scanResults
|
|
997
|
+
.filter(s => s.available && s.apiKey)
|
|
998
|
+
.map(s => [s.key, '(set via env)'])),
|
|
999
|
+
diagnosedAt: new Date().toISOString(),
|
|
1000
|
+
};
|
|
1001
|
+
fs.writeFileSync(configPath, JSON.stringify(configData, null, 2));
|
|
1002
|
+
console.log(`\n💾 Config saved to ${CONFIG_FILE}`);
|
|
1003
|
+
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Look up provider key from a base URL.
|
|
1006
|
+
*/
|
|
1007
|
+
function providerKeyForUrl(baseUrl) {
|
|
1008
|
+
for (const [key, profile] of Object.entries(providers_1.PROVIDERS)) {
|
|
1009
|
+
if (profile.baseUrl === baseUrl)
|
|
1010
|
+
return key;
|
|
1011
|
+
}
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Get a human-friendly provider name from a base URL.
|
|
1016
|
+
*/
|
|
1017
|
+
function providerNameForUrl(baseUrl) {
|
|
1018
|
+
for (const profile of Object.values(providers_1.PROVIDERS)) {
|
|
1019
|
+
if (profile.baseUrl === baseUrl)
|
|
1020
|
+
return profile.name;
|
|
1021
|
+
}
|
|
1022
|
+
return baseUrl;
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Print "no providers found" help message.
|
|
1026
|
+
*/
|
|
1027
|
+
function printNoProvidersHelp(results) {
|
|
1028
|
+
console.log(`\n ❌ No AI providers found!\n`);
|
|
1029
|
+
console.log(` Option 1 (Free, local):`);
|
|
1030
|
+
console.log(` Install Ollama: https://ollama.ai`);
|
|
1031
|
+
console.log(` Then: ollama pull <model> (e.g. qwen2.5:7b, llama3.2, gemma2)\n`);
|
|
1032
|
+
console.log(` Option 2 (Cloud — Budget pick):`);
|
|
1033
|
+
console.log(` Google Gemini 2.5 Flash — one model handles both text + vision roles`);
|
|
1034
|
+
console.log(` Cost: ~$0.15/1M input tokens, 1M context window`);
|
|
1035
|
+
console.log(` Get key: https://aistudio.google.com (free tier available)`);
|
|
1036
|
+
console.log(` Set: GEMINI_API_KEY=AIza...\n`);
|
|
1037
|
+
console.log(` Option 3 (Cloud — Best quality):`);
|
|
1038
|
+
console.log(` - Anthropic: https://console.anthropic.com (Computer Use, best accuracy)`);
|
|
1039
|
+
console.log(` - OpenAI: https://platform.openai.com (GPT-4o vision)`);
|
|
1040
|
+
console.log(` - Groq: https://console.groq.com (fastest inference)`);
|
|
1041
|
+
console.log(` - DeepSeek: https://platform.deepseek.com (reasoning)`);
|
|
1042
|
+
console.log(` - Any OpenAI-compatible endpoint`);
|
|
1043
|
+
console.log(` Then: clawdcursor install --api-key YOUR_KEY\n`);
|
|
1044
|
+
results.push({ name: 'AI Providers', ok: false, detail: 'No providers available' });
|
|
1045
|
+
results.push({ name: 'Text model', ok: false, detail: 'No providers available' });
|
|
1046
|
+
results.push({ name: 'Vision model', ok: false, detail: 'No providers available' });
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Print the final summary.
|
|
1050
|
+
*/
|
|
1051
|
+
function printSummary(results, pipeline) {
|
|
1052
|
+
const allOk = results.every(r => r.ok);
|
|
1053
|
+
const consentMissing = results.some(r => r.name === 'Desktop control consent' && !r.ok);
|
|
1054
|
+
console.log(`\n${'═'.repeat(50)}`);
|
|
1055
|
+
if (allOk) {
|
|
1056
|
+
console.log(`✅ All systems go!\n`);
|
|
1057
|
+
console.log(` Two ways to use clawdcursor — pick the one that fits your setup:\n`);
|
|
1058
|
+
console.log(` ${picocolors_1.default.bold('1. As an MCP server for your editor')} ${picocolors_1.default.gray('(Claude Code, Cursor, Windsurf, Zed)')}`);
|
|
1059
|
+
console.log(` Register ${picocolors_1.default.cyan('clawdcursor mcp')} in your editor's MCP config.`);
|
|
1060
|
+
console.log(` Your editor's AI gets 97 desktop tools (or 6 compound via ${picocolors_1.default.cyan('--compact')}).`);
|
|
1061
|
+
console.log(` Stdio transport — no daemon, no port, no token.\n`);
|
|
1062
|
+
console.log(` ${picocolors_1.default.bold('2. As a local HTTP daemon')} ${picocolors_1.default.gray('(for any HTTP client, or for the built-in autonomous agent)')}`);
|
|
1063
|
+
console.log(` Run ${picocolors_1.default.cyan('clawdcursor agent')} — exposes the same 97 tools at ${picocolors_1.default.cyan('POST /mcp')} on ${picocolors_1.default.cyan(':3847')}.`);
|
|
1064
|
+
console.log(` With an LLM configured ${pipeline.layer2.enabled ? picocolors_1.default.green('(you have one)') : picocolors_1.default.yellow(`(none yet — re-run ${picocolors_1.default.cyan('clawdcursor doctor')} after adding a key)`)},`);
|
|
1065
|
+
console.log(` you also get ${picocolors_1.default.cyan('clawdcursor task "<plain English>"')} for end-to-end autonomous runs.\n`);
|
|
1066
|
+
console.log(` Run now:`);
|
|
1067
|
+
console.log(` ${picocolors_1.default.cyan('clawdcursor agent')} ${picocolors_1.default.gray('# or skip the daemon and wire `clawdcursor mcp` into your editor')}`);
|
|
1068
|
+
}
|
|
1069
|
+
else {
|
|
1070
|
+
const failures = results.filter(r => !r.ok);
|
|
1071
|
+
console.log(`⚠️ ${failures.length} issue(s) detected:`);
|
|
1072
|
+
for (const f of failures) {
|
|
1073
|
+
console.log(` ❌ ${f.name}: ${f.detail}`);
|
|
1074
|
+
}
|
|
1075
|
+
// Consent is critical — highlight it
|
|
1076
|
+
if (consentMissing) {
|
|
1077
|
+
console.log(`\n🔐 Consent required before desktop control can work:`);
|
|
1078
|
+
console.log(` Run: clawdcursor consent`);
|
|
1079
|
+
console.log('');
|
|
1080
|
+
}
|
|
1081
|
+
const textFailed = !pipeline.layer2.enabled;
|
|
1082
|
+
const visionFailed = !pipeline.layer3.enabled;
|
|
1083
|
+
if (textFailed || visionFailed) {
|
|
1084
|
+
console.log(`\n💡 Quick fixes:\n`);
|
|
1085
|
+
}
|
|
1086
|
+
if (textFailed) {
|
|
1087
|
+
console.log(` Text LLM missing — needed for accessibility reasoning (Layer 2)`);
|
|
1088
|
+
console.log(` Free (local): ollama pull <model> && ollama serve (e.g. qwen2.5:7b, llama3.2)`);
|
|
1089
|
+
console.log(` Cloud: clawdcursor install --provider <provider> --api-key YOUR_KEY`);
|
|
1090
|
+
console.log('');
|
|
1091
|
+
}
|
|
1092
|
+
if (visionFailed) {
|
|
1093
|
+
console.log(` Vision LLM missing — needed for screenshot analysis (Layer 3)`);
|
|
1094
|
+
console.log(` Run: clawdcursor install --provider <provider> --api-key YOUR_KEY`);
|
|
1095
|
+
console.log(` Supported: Any provider with vision models (Anthropic, OpenAI, Groq, etc.)`);
|
|
1096
|
+
console.log('');
|
|
1097
|
+
}
|
|
1098
|
+
if (visionFailed && !textFailed) {
|
|
1099
|
+
console.log(` ℹ️ Running without vision — action router + accessibility reasoner handle most tasks.`);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
console.log('');
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* Register Clawd Cursor as a skill in detected external platforms (Claude Code,
|
|
1106
|
+
* OpenClaw, Codex, Cursor). Delegates to the shared, install-path-agnostic
|
|
1107
|
+
* implementation in ./skill-register so `consent` and `register-skill` register
|
|
1108
|
+
* the skill the same way doctor does (the logic used to live ONLY here, which is
|
|
1109
|
+
* why MCP-direct users — told to skip doctor — never got the skill).
|
|
1110
|
+
*/
|
|
1111
|
+
async function registerExternalSkills(results) {
|
|
1112
|
+
const { registerSkills } = await Promise.resolve().then(() => __importStar(require('./skill-register')));
|
|
1113
|
+
for (const r of registerSkills().results) {
|
|
1114
|
+
results.push({ name: r.name, ok: r.ok, detail: r.detail });
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Check for newer versions on GitHub releases.
|
|
1119
|
+
*/
|
|
1120
|
+
async function checkForUpdates(results) {
|
|
1121
|
+
try {
|
|
1122
|
+
const pkgPath = path.join((0, paths_1.getPackageRoot)(), 'package.json');
|
|
1123
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
1124
|
+
const currentVersion = pkg.version || '0.0.0';
|
|
1125
|
+
console.log(` Current: v${currentVersion}`);
|
|
1126
|
+
const controller = new AbortController();
|
|
1127
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
1128
|
+
const res = await fetch('https://api.github.com/repos/AmrDab/clawdcursor/releases/latest', {
|
|
1129
|
+
headers: { 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'clawdcursor-doctor' },
|
|
1130
|
+
signal: controller.signal,
|
|
1131
|
+
});
|
|
1132
|
+
clearTimeout(timeout);
|
|
1133
|
+
if (res.ok) {
|
|
1134
|
+
const data = await res.json();
|
|
1135
|
+
const latestTag = (data.tag_name || '').replace(/^v/, '');
|
|
1136
|
+
if (latestTag && latestTag !== currentVersion && compareVersions(latestTag, currentVersion) > 0) {
|
|
1137
|
+
console.log(` ⬆️ Update available: v${latestTag} (you have v${currentVersion})`);
|
|
1138
|
+
// Recommend the canonical installer one-liner — same command users
|
|
1139
|
+
// ran the first time, smart-updates an existing ~/clawdcursor clone
|
|
1140
|
+
// in place via git fetch + checkout. The previous recommendation
|
|
1141
|
+
// (a bare `git pull` chain) only worked from inside the install dir
|
|
1142
|
+
// and assumed the user knew where it was.
|
|
1143
|
+
const updateCmd = process.platform === 'win32'
|
|
1144
|
+
? 'irm https://clawdcursor.com/install.ps1 | iex'
|
|
1145
|
+
: 'curl -fsSL https://clawdcursor.com/install.sh | bash';
|
|
1146
|
+
console.log(` Run: ${updateCmd}`);
|
|
1147
|
+
results.push({
|
|
1148
|
+
name: 'Version',
|
|
1149
|
+
ok: false,
|
|
1150
|
+
detail: `Update available: v${latestTag} (current: v${currentVersion})`,
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
else {
|
|
1154
|
+
console.log(` ✅ Up to date (v${currentVersion})`);
|
|
1155
|
+
results.push({ name: 'Version', ok: true, detail: `v${currentVersion} (latest)` });
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
else {
|
|
1159
|
+
// GitHub API rate limit or error — skip gracefully
|
|
1160
|
+
console.log(` ✅ v${currentVersion} (update check skipped — GitHub API returned ${res.status})`);
|
|
1161
|
+
results.push({ name: 'Version', ok: true, detail: `v${currentVersion} (update check skipped)` });
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
catch (err) {
|
|
1165
|
+
if (err.name === 'AbortError') {
|
|
1166
|
+
console.log(` ⚠️ Update check timed out (5s) — skipping`);
|
|
1167
|
+
}
|
|
1168
|
+
else {
|
|
1169
|
+
console.log(` ⚠️ Update check failed — skipping`);
|
|
1170
|
+
}
|
|
1171
|
+
// Don't fail the doctor for a version check issue
|
|
1172
|
+
const pkgPath = path.join((0, paths_1.getPackageRoot)(), 'package.json');
|
|
1173
|
+
try {
|
|
1174
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
1175
|
+
results.push({ name: 'Version', ok: true, detail: `v${pkg.version} (update check unavailable)` });
|
|
1176
|
+
}
|
|
1177
|
+
catch {
|
|
1178
|
+
results.push({ name: 'Version', ok: true, detail: 'unknown (update check unavailable)' });
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Simple semver comparison. Returns >0 if a > b, <0 if a < b, 0 if equal.
|
|
1184
|
+
*/
|
|
1185
|
+
function compareVersions(a, b) {
|
|
1186
|
+
const pa = a.split('.').map(Number);
|
|
1187
|
+
const pb = b.split('.').map(Number);
|
|
1188
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
1189
|
+
const na = pa[i] || 0;
|
|
1190
|
+
const nb = pb[i] || 0;
|
|
1191
|
+
if (na !== nb)
|
|
1192
|
+
return na - nb;
|
|
1193
|
+
}
|
|
1194
|
+
return 0;
|
|
1195
|
+
}
|
|
1196
|
+
/**
|
|
1197
|
+
* Test if a model is responding AND can follow instructions.
|
|
1198
|
+
* Text models: "Reply with exactly: CLAWD_OK" → verify response contains CLAWD_OK.
|
|
1199
|
+
* Vision models: send a 1x1 green pixel → verify non-empty meaningful response.
|
|
1200
|
+
*/
|
|
1201
|
+
async function testModel(provider, apiKey, model, isVision) {
|
|
1202
|
+
if (isVision) {
|
|
1203
|
+
return testVisionModel(provider, apiKey, model);
|
|
1204
|
+
}
|
|
1205
|
+
return testTextModel(provider, apiKey, model);
|
|
1206
|
+
}
|
|
1207
|
+
/** Text model: verify instruction-following, not just connectivity */
|
|
1208
|
+
async function testTextModel(provider, apiKey, model) {
|
|
1209
|
+
const start = performance.now();
|
|
1210
|
+
const TIMEOUT = 8000;
|
|
1211
|
+
const INSTRUCTION = 'Reply with exactly one word: CLAWD_OK — nothing else.';
|
|
1212
|
+
try {
|
|
1213
|
+
let text = '';
|
|
1214
|
+
if (provider.openaiCompat) {
|
|
1215
|
+
const response = await fetch(`${provider.baseUrl}/chat/completions`, {
|
|
1216
|
+
method: 'POST',
|
|
1217
|
+
headers: {
|
|
1218
|
+
'Content-Type': 'application/json',
|
|
1219
|
+
...provider.authHeader(apiKey),
|
|
1220
|
+
},
|
|
1221
|
+
body: JSON.stringify({
|
|
1222
|
+
model,
|
|
1223
|
+
max_tokens: 10,
|
|
1224
|
+
// Omit temperature for reasoning models (kimi-k2.5 etc.) that reject temperature=0
|
|
1225
|
+
...(provider.reasoningVisionModel && model === provider.visionModel ? {} : { temperature: 0 }),
|
|
1226
|
+
messages: [{ role: 'user', content: INSTRUCTION }],
|
|
1227
|
+
}),
|
|
1228
|
+
signal: AbortSignal.timeout(TIMEOUT),
|
|
1229
|
+
});
|
|
1230
|
+
const data = await response.json();
|
|
1231
|
+
if (data.error) {
|
|
1232
|
+
return { ok: false, error: extractErrorMessage(data.error) };
|
|
1233
|
+
}
|
|
1234
|
+
text = data.choices?.[0]?.message?.content || '';
|
|
1235
|
+
}
|
|
1236
|
+
else {
|
|
1237
|
+
// Anthropic API
|
|
1238
|
+
const response = await fetch(`${provider.baseUrl}/messages`, {
|
|
1239
|
+
method: 'POST',
|
|
1240
|
+
headers: {
|
|
1241
|
+
'Content-Type': 'application/json',
|
|
1242
|
+
...provider.authHeader(apiKey),
|
|
1243
|
+
...provider.extraHeaders,
|
|
1244
|
+
},
|
|
1245
|
+
body: JSON.stringify({
|
|
1246
|
+
model,
|
|
1247
|
+
max_tokens: 10,
|
|
1248
|
+
messages: [{ role: 'user', content: INSTRUCTION }],
|
|
1249
|
+
}),
|
|
1250
|
+
signal: AbortSignal.timeout(TIMEOUT),
|
|
1251
|
+
});
|
|
1252
|
+
const data = await response.json();
|
|
1253
|
+
if (data.type === 'error' && data.error) {
|
|
1254
|
+
const hint = (data.error.type === 'not_found_error' || data.error.type === 'invalid_request_error')
|
|
1255
|
+
? ' — check model id matches your provider'
|
|
1256
|
+
: '';
|
|
1257
|
+
return { ok: false, error: extractErrorMessage(data.error) + hint };
|
|
1258
|
+
}
|
|
1259
|
+
if (data.error) {
|
|
1260
|
+
return { ok: false, error: extractErrorMessage(data.error) };
|
|
1261
|
+
}
|
|
1262
|
+
text = data.content?.[0]?.text || '';
|
|
1263
|
+
}
|
|
1264
|
+
if (!text)
|
|
1265
|
+
return { ok: false, error: 'Empty response' };
|
|
1266
|
+
// Verify instruction-following
|
|
1267
|
+
if (!text.includes('CLAWD_OK')) {
|
|
1268
|
+
return { ok: false, error: `Model responded but didn't follow instructions (got: "${text.substring(0, 50)}")` };
|
|
1269
|
+
}
|
|
1270
|
+
return { ok: true, latencyMs: Math.round(performance.now() - start) };
|
|
1271
|
+
}
|
|
1272
|
+
catch (err) {
|
|
1273
|
+
if (err.name === 'TimeoutError' || err.name === 'AbortError') {
|
|
1274
|
+
return { ok: false, error: `Timeout (${TIMEOUT / 1000}s)` };
|
|
1275
|
+
}
|
|
1276
|
+
return { ok: false, error: err.message || String(err) };
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
/** Vision model: send a real image and verify the model can process it */
|
|
1280
|
+
async function testVisionModel(provider, apiKey, model) {
|
|
1281
|
+
const start = performance.now();
|
|
1282
|
+
const TIMEOUT = 10000; // vision needs slightly more time
|
|
1283
|
+
// 64x64 solid green JPEG (292 bytes) — JPEG is universally supported by all vision APIs.
|
|
1284
|
+
// PNG fails on some providers (e.g., Kimi rejects PNG with "failed to decode image").
|
|
1285
|
+
const TEST_IMAGE = '/9j/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCABAAEADASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAT/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAf/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCABNEfAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/2Q==';
|
|
1286
|
+
const TEST_IMAGE_MIME = 'image/jpeg';
|
|
1287
|
+
try {
|
|
1288
|
+
const text = await (0, client_1.callVisionLLMDirect)({
|
|
1289
|
+
baseUrl: provider.baseUrl,
|
|
1290
|
+
model,
|
|
1291
|
+
apiKey,
|
|
1292
|
+
isAnthropic: !provider.openaiCompat,
|
|
1293
|
+
providerProfile: provider, // passes reasoningVisionModel flag for temperature handling
|
|
1294
|
+
messages: [{
|
|
1295
|
+
role: 'user',
|
|
1296
|
+
content: [
|
|
1297
|
+
{ type: 'image', source: { type: 'base64', media_type: TEST_IMAGE_MIME, data: TEST_IMAGE } },
|
|
1298
|
+
{ type: 'text', text: 'What color is this image? Reply with one word.' },
|
|
1299
|
+
],
|
|
1300
|
+
}],
|
|
1301
|
+
maxTokens: 20,
|
|
1302
|
+
timeoutMs: TIMEOUT,
|
|
1303
|
+
retries: 0,
|
|
1304
|
+
});
|
|
1305
|
+
if (!text)
|
|
1306
|
+
return { ok: false, error: 'Empty response — model may not support vision' };
|
|
1307
|
+
// Any non-empty response proves the model accepted the image
|
|
1308
|
+
// Bonus: check if it said "green" (but don't require it — some models describe differently)
|
|
1309
|
+
const lower = text.toLowerCase();
|
|
1310
|
+
const recognizedColor = lower.includes('green') || lower.includes('color');
|
|
1311
|
+
return {
|
|
1312
|
+
ok: true,
|
|
1313
|
+
latencyMs: Math.round(performance.now() - start),
|
|
1314
|
+
...(recognizedColor ? {} : {}), // response is valid either way
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
catch (err) {
|
|
1318
|
+
if (err.name === 'TimeoutError' || err.name === 'AbortError') {
|
|
1319
|
+
return { ok: false, error: `Timeout (${TIMEOUT / 1000}s)` };
|
|
1320
|
+
}
|
|
1321
|
+
// Common error: model doesn't support multimodal input
|
|
1322
|
+
const msg = err.message || String(err);
|
|
1323
|
+
if (msg.includes('image') || msg.includes('multimodal') || msg.includes('vision')) {
|
|
1324
|
+
return { ok: false, error: `Model does not support vision input: ${msg}` };
|
|
1325
|
+
}
|
|
1326
|
+
return { ok: false, error: msg };
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
/** Extract a human-readable error message from an API error response */
|
|
1330
|
+
function extractErrorMessage(error) {
|
|
1331
|
+
if (typeof error === 'string')
|
|
1332
|
+
return error;
|
|
1333
|
+
if (typeof error === 'object' && error !== null) {
|
|
1334
|
+
return error.message || JSON.stringify(error);
|
|
1335
|
+
}
|
|
1336
|
+
return String(error);
|
|
1337
|
+
}
|
|
1338
|
+
/**
|
|
1339
|
+
* Load saved pipeline config from disk.
|
|
1340
|
+
*/
|
|
1341
|
+
function resolveProviderApiKey(providerKey, fallbackApiKey) {
|
|
1342
|
+
const normalizedProvider = (providerKey || '').toLowerCase();
|
|
1343
|
+
if (!normalizedProvider)
|
|
1344
|
+
return fallbackApiKey || '';
|
|
1345
|
+
const resolved = (0, credentials_1.resolveApiConfig)({ provider: normalizedProvider });
|
|
1346
|
+
if (resolved.apiKey)
|
|
1347
|
+
return resolved.apiKey;
|
|
1348
|
+
return fallbackApiKey || '';
|
|
1349
|
+
}
|
|
1350
|
+
function loadPipelineConfig(overlay) {
|
|
1351
|
+
const pkgDir = (0, paths_1.getPackageRoot)();
|
|
1352
|
+
let configPath = path.join(pkgDir, CONFIG_FILE);
|
|
1353
|
+
if (!fs.existsSync(configPath)) {
|
|
1354
|
+
configPath = path.join(process.cwd(), CONFIG_FILE);
|
|
1355
|
+
}
|
|
1356
|
+
// Read on-disk config (may be null if no file exists).
|
|
1357
|
+
let diskConfig = null;
|
|
1358
|
+
try {
|
|
1359
|
+
if (fs.existsSync(configPath)) {
|
|
1360
|
+
const raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
1361
|
+
const providerKey = raw.provider || 'ollama';
|
|
1362
|
+
const provider = providers_1.PROVIDERS[providerKey] || providers_1.PROVIDERS['ollama'];
|
|
1363
|
+
// Resolve API key: check provider-scoped env vars FIRST, then fall back to
|
|
1364
|
+
// generic resolution. This prevents OpenClaw auth-profiles (e.g. a stale
|
|
1365
|
+
// Anthropic key) from overriding the correct provider-specific key.
|
|
1366
|
+
const scopedEnvKey = (providers_1.PROVIDER_ENV_VARS[providerKey] || [])
|
|
1367
|
+
.map(k => process.env[k])
|
|
1368
|
+
.find(v => v && v.length > 0) || '';
|
|
1369
|
+
const resolvedDefault = (0, credentials_1.resolveApiConfig)();
|
|
1370
|
+
const defaultApiKey = scopedEnvKey || resolvedDefault.apiKey;
|
|
1371
|
+
// Support both v0.7.5+ (textModel/visionModel) and legacy (layer2/layer3) field names
|
|
1372
|
+
const layer2Data = raw.pipeline?.textModel ?? raw.pipeline?.layer2;
|
|
1373
|
+
const layer3Data = raw.pipeline?.visionModel ?? raw.pipeline?.layer3;
|
|
1374
|
+
const layer2BaseUrl = layer2Data?.baseUrl ?? provider.baseUrl;
|
|
1375
|
+
const layer3BaseUrl = layer3Data?.baseUrl ?? provider.baseUrl;
|
|
1376
|
+
const layer2ProviderKey = layer2Data?.provider || providerKey;
|
|
1377
|
+
const layer3ProviderKey = layer3Data?.provider || providerKey;
|
|
1378
|
+
const layer3ComputerUse = layer3Data?.computerUse ?? false;
|
|
1379
|
+
// Resolve API keys PER LAYER based on each layer's provider.
|
|
1380
|
+
// Mixed pipelines (e.g., Kimi text + Anthropic vision) need different keys.
|
|
1381
|
+
const layer2ApiKey = resolveProviderApiKey(layer2ProviderKey, defaultApiKey);
|
|
1382
|
+
const layer3ApiKey = resolveProviderApiKey(layer3ProviderKey, defaultApiKey);
|
|
1383
|
+
diskConfig = {
|
|
1384
|
+
provider,
|
|
1385
|
+
providerKey,
|
|
1386
|
+
apiKey: layer2ApiKey, // primary key = text layer key (most LLM calls use text)
|
|
1387
|
+
layer1: true,
|
|
1388
|
+
layer2: {
|
|
1389
|
+
enabled: layer2Data?.enabled ?? false,
|
|
1390
|
+
model: layer2Data?.model ?? provider.textModel,
|
|
1391
|
+
baseUrl: layer2BaseUrl,
|
|
1392
|
+
apiKey: layer2ApiKey, // per-layer key for mixed-provider pipelines
|
|
1393
|
+
},
|
|
1394
|
+
layer3: {
|
|
1395
|
+
enabled: layer3Data?.enabled ?? false,
|
|
1396
|
+
model: layer3Data?.model ?? provider.visionModel,
|
|
1397
|
+
baseUrl: layer3BaseUrl,
|
|
1398
|
+
computerUse: layer3ComputerUse,
|
|
1399
|
+
apiKey: layer3ApiKey, // always resolve per-layer, not just for CU
|
|
1400
|
+
},
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
catch {
|
|
1405
|
+
diskConfig = null;
|
|
1406
|
+
}
|
|
1407
|
+
// No CLI overlay → just return whatever (or null) the disk gave us.
|
|
1408
|
+
if (!overlay)
|
|
1409
|
+
return diskConfig;
|
|
1410
|
+
return applyCliOverlay(diskConfig, overlay);
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Overlay CLI-sourced fields from a ResolvedConfig onto a PipelineConfig.
|
|
1414
|
+
*
|
|
1415
|
+
* If `disk` is null but the overlay supplies enough to construct a pipeline
|
|
1416
|
+
* (a text-model+base-url OR a vision-model+base-url with a CLI source),
|
|
1417
|
+
* a minimal PipelineConfig is synthesized so the agent runtime sees the
|
|
1418
|
+
* flags supplied on the command line.
|
|
1419
|
+
*
|
|
1420
|
+
* Only fields whose `source` tag is 'cli' override the disk values; this
|
|
1421
|
+
* preserves the canonical precedence ladder. Project/user/env-sourced
|
|
1422
|
+
* values from `ResolvedConfig` are NOT applied here — those flow through
|
|
1423
|
+
* the existing disk-config path and provider-scoped env resolution.
|
|
1424
|
+
*/
|
|
1425
|
+
function applyCliOverlay(disk, overlay) {
|
|
1426
|
+
const src = overlay.source;
|
|
1427
|
+
const textBaseFromCli = src.textBaseUrl === 'cli' ? overlay.textBaseUrl
|
|
1428
|
+
: src.baseUrl === 'cli' ? overlay.baseUrl
|
|
1429
|
+
: undefined;
|
|
1430
|
+
const textModelFromCli = src.model === 'cli' ? overlay.model : undefined;
|
|
1431
|
+
const textApiFromCli = src.textApiKey === 'cli' ? overlay.textApiKey
|
|
1432
|
+
: src.apiKey === 'cli' ? overlay.apiKey
|
|
1433
|
+
: undefined;
|
|
1434
|
+
const visionBaseFromCli = src.visionBaseUrl === 'cli' ? overlay.visionBaseUrl
|
|
1435
|
+
: src.baseUrl === 'cli' ? overlay.baseUrl
|
|
1436
|
+
: undefined;
|
|
1437
|
+
const visionModelFromCli = src.visionModel === 'cli' ? overlay.visionModel : undefined;
|
|
1438
|
+
const visionApiFromCli = src.visionApiKey === 'cli' ? overlay.visionApiKey
|
|
1439
|
+
: src.apiKey === 'cli' ? overlay.apiKey
|
|
1440
|
+
: undefined;
|
|
1441
|
+
const providerFromCli = src.provider === 'cli' ? overlay.provider : undefined;
|
|
1442
|
+
const anyCli = textBaseFromCli || textModelFromCli || textApiFromCli
|
|
1443
|
+
|| visionBaseFromCli || visionModelFromCli || visionApiFromCli
|
|
1444
|
+
|| providerFromCli;
|
|
1445
|
+
if (!disk && !anyCli)
|
|
1446
|
+
return null;
|
|
1447
|
+
// Synthesize a minimal pipeline if no disk config exists.
|
|
1448
|
+
if (!disk) {
|
|
1449
|
+
const layer2BaseUrl = textBaseFromCli || '';
|
|
1450
|
+
const layer3BaseUrl = visionBaseFromCli || '';
|
|
1451
|
+
const providerKey = providerFromCli
|
|
1452
|
+
|| (0, credentials_1.inferProviderFromBaseUrl)(layer2BaseUrl || layer3BaseUrl)
|
|
1453
|
+
|| 'ollama';
|
|
1454
|
+
const provider = providers_1.PROVIDERS[providerKey] || providers_1.PROVIDERS['ollama'];
|
|
1455
|
+
const layer2Model = textModelFromCli || '';
|
|
1456
|
+
const layer3Model = visionModelFromCli || '';
|
|
1457
|
+
const layer2ApiKey = textApiFromCli || '';
|
|
1458
|
+
const layer3ApiKey = visionApiFromCli || layer2ApiKey;
|
|
1459
|
+
return {
|
|
1460
|
+
provider,
|
|
1461
|
+
providerKey,
|
|
1462
|
+
apiKey: layer2ApiKey,
|
|
1463
|
+
layer1: true,
|
|
1464
|
+
layer2: {
|
|
1465
|
+
enabled: Boolean(layer2Model && layer2BaseUrl),
|
|
1466
|
+
model: layer2Model,
|
|
1467
|
+
baseUrl: layer2BaseUrl,
|
|
1468
|
+
apiKey: layer2ApiKey,
|
|
1469
|
+
},
|
|
1470
|
+
layer3: {
|
|
1471
|
+
enabled: Boolean(layer3Model && layer3BaseUrl),
|
|
1472
|
+
model: layer3Model,
|
|
1473
|
+
baseUrl: layer3BaseUrl,
|
|
1474
|
+
computerUse: false,
|
|
1475
|
+
apiKey: layer3ApiKey,
|
|
1476
|
+
},
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
// Disk config exists — overlay CLI fields on top of it. Each layer's
|
|
1480
|
+
// `enabled` flag flips to true once the layer has both a model and a
|
|
1481
|
+
// base URL (CLI flags can complete a partial disk config the same way
|
|
1482
|
+
// an explicit `enabled: true` would).
|
|
1483
|
+
const layer2BaseUrl = textBaseFromCli ?? disk.layer2.baseUrl;
|
|
1484
|
+
const layer2Model = textModelFromCli ?? disk.layer2.model;
|
|
1485
|
+
const layer2ApiKey = textApiFromCli ?? disk.layer2.apiKey ?? disk.apiKey;
|
|
1486
|
+
const layer3BaseUrl = visionBaseFromCli ?? disk.layer3.baseUrl;
|
|
1487
|
+
const layer3Model = visionModelFromCli ?? disk.layer3.model;
|
|
1488
|
+
const layer3ApiKey = visionApiFromCli ?? disk.layer3.apiKey ?? disk.apiKey;
|
|
1489
|
+
const layer2Enabled = disk.layer2.enabled || Boolean((textModelFromCli || textBaseFromCli) && layer2Model && layer2BaseUrl);
|
|
1490
|
+
const layer3Enabled = disk.layer3.enabled || Boolean((visionModelFromCli || visionBaseFromCli) && layer3Model && layer3BaseUrl);
|
|
1491
|
+
const providerKey = providerFromCli ?? disk.providerKey;
|
|
1492
|
+
const provider = providerFromCli ? (providers_1.PROVIDERS[providerFromCli] || disk.provider) : disk.provider;
|
|
1493
|
+
return {
|
|
1494
|
+
...disk,
|
|
1495
|
+
provider,
|
|
1496
|
+
providerKey,
|
|
1497
|
+
apiKey: layer2ApiKey,
|
|
1498
|
+
layer2: {
|
|
1499
|
+
...disk.layer2,
|
|
1500
|
+
enabled: layer2Enabled,
|
|
1501
|
+
model: layer2Model,
|
|
1502
|
+
baseUrl: layer2BaseUrl,
|
|
1503
|
+
apiKey: layer2ApiKey,
|
|
1504
|
+
},
|
|
1505
|
+
layer3: {
|
|
1506
|
+
...disk.layer3,
|
|
1507
|
+
enabled: layer3Enabled,
|
|
1508
|
+
model: layer3Model,
|
|
1509
|
+
baseUrl: layer3BaseUrl,
|
|
1510
|
+
apiKey: layer3ApiKey,
|
|
1511
|
+
},
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
//# sourceMappingURL=doctor.js.map
|