@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,1556 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* 🐾 Clawd Cursor — AI Desktop Agent
|
|
5
|
+
*
|
|
6
|
+
* Your AI controls your desktop natively.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
42
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
43
|
+
};
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
// Node.js v25+ on macOS: undici's fetch() can crash with EINVAL on setTypeOfService.
|
|
46
|
+
// The throw happens asynchronously inside libuv's socket machinery, so it CANNOT
|
|
47
|
+
// be try/caught at a call site — a process-level handler is the only intercept
|
|
48
|
+
// point. #114 scopes it as tightly as a global handler allows:
|
|
49
|
+
// - the swallow matches ONE exact signature (code+syscall) AND only on darwin,
|
|
50
|
+
// the only platform where the kernel no-op occurs;
|
|
51
|
+
// - every other uncaught exception crashes with FULL provenance (name,
|
|
52
|
+
// message, stack) and a non-zero exit — same observable semantics as
|
|
53
|
+
// Node's default handler, plus our prefix for log correlation.
|
|
54
|
+
process.on('uncaughtException', (err) => {
|
|
55
|
+
if (process.platform === 'darwin' && err?.code === 'EINVAL' && err?.syscall === 'setTypeOfService') {
|
|
56
|
+
// Known-benign: Node v25+ tries to set the IP QoS/TOS socket option via
|
|
57
|
+
// undici's fetch(); macOS doesn't support it. The request continues — this
|
|
58
|
+
// is purely a kernel-level no-op. Debug-level so it stays observable.
|
|
59
|
+
console.debug('[clawdcursor] uncaughtException swallowed (known-benign): setTypeOfService EINVAL on macOS/Node v25+');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
console.error(`Uncaught exception: ${err?.stack ?? err}`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
});
|
|
65
|
+
// v0.8.1: unhandledRejection handler.
|
|
66
|
+
// Prior behavior: rejected promises inside the agent loop killed the Node
|
|
67
|
+
// process with only Node's default warning — HTTP clients would see connection
|
|
68
|
+
// drops with no trace. Log through the new leveled logger so correlation IDs
|
|
69
|
+
// come along, and keep the server running (server stability > loud death).
|
|
70
|
+
// In CLI mode (no active server) we still exit 1 to surface the bug.
|
|
71
|
+
process.on('unhandledRejection', (reason) => {
|
|
72
|
+
try {
|
|
73
|
+
// Lazy-require to avoid pulling the pipeline module at cold CLI startup.
|
|
74
|
+
const { logger } = require('../core/observability/logger');
|
|
75
|
+
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
76
|
+
const stack = reason instanceof Error ? reason.stack : undefined;
|
|
77
|
+
logger.error('unhandledRejection', { msg, stack });
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Logger itself failed — fall back to stderr.
|
|
81
|
+
console.error('unhandledRejection (logger unavailable):', reason);
|
|
82
|
+
}
|
|
83
|
+
// In server mode, (process.env.CLAWD_SERVER_MODE === '1') keep running.
|
|
84
|
+
// In CLI / one-shot mode, exit to surface the bug.
|
|
85
|
+
if (process.env.CLAWD_SERVER_MODE !== '1') {
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
const commander_1 = require("commander");
|
|
90
|
+
const agent_1 = require("../core/agent");
|
|
91
|
+
const http_utility_1 = require("./http-utility");
|
|
92
|
+
const types_1 = require("../types");
|
|
93
|
+
const version_1 = require("./version");
|
|
94
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
95
|
+
const credentials_1 = require("../llm/credentials");
|
|
96
|
+
const config_1 = require("../llm/config");
|
|
97
|
+
const fs = __importStar(require("fs"));
|
|
98
|
+
const path = __importStar(require("path"));
|
|
99
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
100
|
+
const paths_1 = require("../paths");
|
|
101
|
+
const native_helper_1 = require("../platform/native-helper");
|
|
102
|
+
const ui_map_holder_1 = require("../core/sense/ui-map-holder");
|
|
103
|
+
dotenv_1.default.config({ quiet: true });
|
|
104
|
+
// Migrate data from legacy ~/.openclaw/clawdcursor/ to ~/.clawdcursor/
|
|
105
|
+
(0, paths_1.migrateFromLegacyDir)();
|
|
106
|
+
// ── Auth helper ──────────────────────────────────────────────────────────────
|
|
107
|
+
// Reads the saved Bearer token from ~/.clawdcursor/token (written by start/serve).
|
|
108
|
+
function loadAuthToken() {
|
|
109
|
+
try {
|
|
110
|
+
const tokenPath = path.join(require('os').homedir(), '.clawdcursor', 'token');
|
|
111
|
+
return fs.readFileSync(tokenPath, 'utf-8').trim();
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return '';
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function authHeaders() {
|
|
118
|
+
const token = loadAuthToken();
|
|
119
|
+
return token ? { 'Authorization': `Bearer ${token}` } : {};
|
|
120
|
+
}
|
|
121
|
+
// ── Emoji gate (shared utility) ──────────────────────────────────────────────
|
|
122
|
+
const format_1 = require("./format");
|
|
123
|
+
// ── Single-instance pidfile lock ─────────────────────────────────────────────
|
|
124
|
+
// Implementation lives in ./pidfile so it can be unit-tested independently.
|
|
125
|
+
// The richer JSON lockfile records process start time, which lets the
|
|
126
|
+
// liveness check distinguish a real live duplicate from a recycled PID
|
|
127
|
+
// (the bug behind "Failed to reconnect to clawdcursor: -32000" on Windows).
|
|
128
|
+
const pidfile_1 = require("./pidfile");
|
|
129
|
+
/**
|
|
130
|
+
* Graceful exit on a startup-time init failure (bad API key, no providers,
|
|
131
|
+
* etc.). Synchronous `process.exit(N)` while async handles are mid-close
|
|
132
|
+
* triggers libuv asserts on Windows ("Assertion failed: !(handle->flags &
|
|
133
|
+
* UV_HANDLE_CLOSING), src\\win\\async.c:76") — so set the exit code, kick
|
|
134
|
+
* off cleanup, and let the event loop drain. A 2-second hard-kill safety
|
|
135
|
+
* net guarantees the process always exits even if a handle gets stuck.
|
|
136
|
+
*/
|
|
137
|
+
function gracefulExitOnInitFailure(code, agent) {
|
|
138
|
+
process.exitCode = code;
|
|
139
|
+
(0, pidfile_1.releasePidFile)('start');
|
|
140
|
+
try {
|
|
141
|
+
agent.disconnect();
|
|
142
|
+
}
|
|
143
|
+
catch { /* non-fatal */ }
|
|
144
|
+
// Hard-kill safety net: if the loop hangs, force-exit after 2s.
|
|
145
|
+
// .unref() so the timer itself doesn't keep the loop alive.
|
|
146
|
+
setTimeout(() => process.exit(code), 2000).unref();
|
|
147
|
+
}
|
|
148
|
+
const program = new commander_1.Command();
|
|
149
|
+
async function isClawdInstance(port) {
|
|
150
|
+
try {
|
|
151
|
+
const res = await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(2000) });
|
|
152
|
+
const data = await res.json();
|
|
153
|
+
return data.status === 'ok' && typeof data.version === 'string';
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function forceKillPort(port) {
|
|
160
|
+
const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
161
|
+
const os = await Promise.resolve().then(() => __importStar(require('os')));
|
|
162
|
+
// #114: only ever kill processes that are plausibly OURS. The port is
|
|
163
|
+
// configurable and ports get reused — blindly SIGKILLing whatever listens on
|
|
164
|
+
// it can take down an unrelated app (dev server, another tool). clawdcursor
|
|
165
|
+
// always runs under node, so a listener with any other image name is not
|
|
166
|
+
// ours: skip it, tell the user, and let them resolve the conflict.
|
|
167
|
+
const OURS = /node|clawdcursor/i;
|
|
168
|
+
if (os.platform() === 'win32') {
|
|
169
|
+
try {
|
|
170
|
+
const output = execSync(`netstat -ano | findstr :${port} | findstr LISTENING`, { encoding: 'utf-8' });
|
|
171
|
+
const pids = new Set(output.trim().split('\n')
|
|
172
|
+
.map(line => line.trim().split(/\s+/).pop())
|
|
173
|
+
.filter((pid) => !!pid && /^\d+$/.test(pid)));
|
|
174
|
+
if (pids.size === 0)
|
|
175
|
+
return false;
|
|
176
|
+
let killedAny = false;
|
|
177
|
+
for (const pid of pids) {
|
|
178
|
+
let image = '';
|
|
179
|
+
try {
|
|
180
|
+
const row = execSync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`, { encoding: 'utf-8' });
|
|
181
|
+
image = (row.split(',')[0] ?? '').replace(/"/g, '').trim();
|
|
182
|
+
}
|
|
183
|
+
catch { /* tasklist unavailable — treat as unknown */ }
|
|
184
|
+
if (image && !OURS.test(image)) {
|
|
185
|
+
console.warn(`${(0, format_1.e)('⚠️', '[WARN]')} Port ${port} is held by "${image}" (pid ${pid}) — not a clawdcursor process; refusing to kill it. Free the port or change server.port.`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
execSync(`taskkill /F /PID ${pid}`);
|
|
189
|
+
console.log(`${(0, format_1.e)('🐾', '>')} Killed process ${pid}${image ? ` (${image})` : ''}`);
|
|
190
|
+
killedAny = true;
|
|
191
|
+
}
|
|
192
|
+
return killedAny;
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Non-Windows: enumerate PIDs explicitly before killing, mirroring the
|
|
199
|
+
// Windows branch above. Running `kill -9 $(lsof -ti tcp:N)` with an empty
|
|
200
|
+
// substitution sends SIGKILL with no target argument — behavior is
|
|
201
|
+
// distro-dependent and can erroneously kill the current process on some
|
|
202
|
+
// systems. Instead: capture lsof output, parse PIDs in JS, and only kill
|
|
203
|
+
// when we have at least one confirmed PID.
|
|
204
|
+
try {
|
|
205
|
+
const lsofOut = execSync(`lsof -ti tcp:${port}`, { encoding: 'utf-8', shell: '/bin/sh' });
|
|
206
|
+
const pids = lsofOut.trim().split(/\s+/)
|
|
207
|
+
.map(s => parseInt(s, 10))
|
|
208
|
+
.filter(n => Number.isInteger(n) && n > 0);
|
|
209
|
+
if (pids.length === 0)
|
|
210
|
+
return false;
|
|
211
|
+
let killedAny = false;
|
|
212
|
+
for (const pid of pids) {
|
|
213
|
+
let image = '';
|
|
214
|
+
try {
|
|
215
|
+
image = execSync(`ps -p ${pid} -o comm=`, { encoding: 'utf-8', shell: '/bin/sh' }).trim();
|
|
216
|
+
}
|
|
217
|
+
catch { /* ps unavailable / pid gone — treat as unknown */ }
|
|
218
|
+
if (image && !OURS.test(image)) {
|
|
219
|
+
console.warn(`${(0, format_1.e)('⚠️', '[WARN]')} Port ${port} is held by "${image}" (pid ${pid}) — not a clawdcursor process; refusing to kill it. Free the port or change server.port.`);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
process.kill(pid, 'SIGKILL');
|
|
224
|
+
console.log(`${(0, format_1.e)('🐾', '>')} Killed process ${pid}${image ? ` (${image})` : ''}`);
|
|
225
|
+
killedAny = true;
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
// ESRCH = the process already exited between lsof and now — that's
|
|
229
|
+
// success for our purpose (the port is free). Re-throw anything else
|
|
230
|
+
// (e.g. EPERM) so the outer catch returns false.
|
|
231
|
+
if (err?.code !== 'ESRCH')
|
|
232
|
+
throw err;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return killedAny;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
program
|
|
242
|
+
.name('clawdcursor')
|
|
243
|
+
.description('🐾 AI Desktop Agent — native screen control')
|
|
244
|
+
.version(version_1.VERSION);
|
|
245
|
+
async function runAgentMode(opts) {
|
|
246
|
+
// commander stores negated flags as `{ llm: false }` / `{ vision: false }`.
|
|
247
|
+
// Keep the internal explicit names so callers/tests can also pass noLlm /
|
|
248
|
+
// noVision directly.
|
|
249
|
+
const forceNoLlm = Boolean(opts.noLlm || opts.llm === false);
|
|
250
|
+
const forceNoVision = Boolean(opts.noVision || opts.vision === false);
|
|
251
|
+
// Single-instance guard — uses the legacy `start` lockfile name so
|
|
252
|
+
// existing `clawdcursor stop` sweeps still find it.
|
|
253
|
+
const existingPid = (0, pidfile_1.claimPidFile)('start');
|
|
254
|
+
if (existingPid !== null) {
|
|
255
|
+
console.error(`${(0, format_1.e)('❌', '[ERR]')} clawdcursor agent is already running (pid ${existingPid}). Run \`clawdcursor stop\` first.`);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
// ── Consent ──
|
|
259
|
+
const { hasConsent, writeConsentFile, runOnboarding } = await Promise.resolve().then(() => __importStar(require('./onboarding')));
|
|
260
|
+
const canSkipDev = opts.skipConsent && process.env.NODE_ENV === 'development';
|
|
261
|
+
if (opts.accept) {
|
|
262
|
+
writeConsentFile();
|
|
263
|
+
console.log(' Consent recorded.\n');
|
|
264
|
+
}
|
|
265
|
+
else if (!canSkipDev && !hasConsent()) {
|
|
266
|
+
const accepted = await runOnboarding('start', parseInt(opts.port ?? '3847', 10) || 3847);
|
|
267
|
+
if (!accepted)
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
if (process.platform === 'darwin') {
|
|
271
|
+
await (0, native_helper_1.ensureHostAppRunning)();
|
|
272
|
+
}
|
|
273
|
+
// ── Port pre-check ──
|
|
274
|
+
const requestedPort = parseInt(opts.port ?? '3847', 10) || 3847;
|
|
275
|
+
const requestedHost = '127.0.0.1';
|
|
276
|
+
const net = await Promise.resolve().then(() => __importStar(require('net')));
|
|
277
|
+
const portFree = await new Promise((resolve) => {
|
|
278
|
+
const tester = net.createServer()
|
|
279
|
+
.once('error', () => resolve(false))
|
|
280
|
+
.once('listening', () => { tester.close(); resolve(true); });
|
|
281
|
+
tester.listen(requestedPort, requestedHost);
|
|
282
|
+
});
|
|
283
|
+
if (!portFree) {
|
|
284
|
+
console.error(`\n${(0, format_1.e)('❌', '[ERR]')} Port ${requestedPort} is already in use.`);
|
|
285
|
+
console.error(`Another clawdcursor instance may be running.`);
|
|
286
|
+
console.error(`Run 'clawdcursor stop' first, or use --port <other_port>`);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
// ── First-run auto-setup — best-effort, never fatal. ──
|
|
290
|
+
// If no AI providers are found we still boot: the MCP tool surface
|
|
291
|
+
// works fine without an LLM (the host's brain drives it).
|
|
292
|
+
//
|
|
293
|
+
// Skip auto-detect entirely when CLI flags already supply a usable
|
|
294
|
+
// model wiring — otherwise we'd print the misleading "No AI providers
|
|
295
|
+
// found — booting in tools-only mode" line moments before "Using
|
|
296
|
+
// externally configured models: text=X" (BUG-A: contradictory boot
|
|
297
|
+
// banners). The CLI-flag path knows its own answer.
|
|
298
|
+
const configPath = path.join((0, paths_1.getPackageRoot)(), '.clawdcursor-config.json');
|
|
299
|
+
const cliSuppliesLlm = Boolean(opts.apiKey
|
|
300
|
+
|| (opts.baseUrl && (opts.textModel || opts.visionModel || opts.model))
|
|
301
|
+
|| ((opts.textModel || opts.visionModel || opts.model) && opts.provider));
|
|
302
|
+
if (!forceNoLlm && !cliSuppliesLlm && !fs.existsSync(configPath)) {
|
|
303
|
+
console.log(`${(0, format_1.e)('🔍', '*')} First run — auto-detecting AI providers...\n`);
|
|
304
|
+
const { quickSetup } = await Promise.resolve().then(() => __importStar(require('./doctor')));
|
|
305
|
+
const pipeline = await quickSetup();
|
|
306
|
+
if (pipeline) {
|
|
307
|
+
console.log(`${(0, format_1.e)('✅', '[OK]')} Auto-configured! Run \`clawdcursor doctor\` to customize.\n`);
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
console.log(`${(0, format_1.e)('ℹ️', 'i')} No AI providers found — booting in tools-only mode.`);
|
|
311
|
+
console.log(' Your editor host (Claude Code, Cursor, Windsurf, OpenClaw) drives the tools.');
|
|
312
|
+
console.log(' Run `clawdcursor doctor` later if you want the built-in autonomous agent.\n');
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const resolved = (0, config_1.resolveConfig)({
|
|
316
|
+
cliFlags: {
|
|
317
|
+
apiKey: opts.apiKey,
|
|
318
|
+
baseUrl: opts.baseUrl,
|
|
319
|
+
textModel: opts.textModel,
|
|
320
|
+
visionModel: opts.visionModel,
|
|
321
|
+
model: opts.model,
|
|
322
|
+
provider: opts.provider,
|
|
323
|
+
port: opts.port,
|
|
324
|
+
debug: opts.debug,
|
|
325
|
+
noVision: forceNoVision,
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
const config = {
|
|
329
|
+
...types_1.DEFAULT_CONFIG,
|
|
330
|
+
server: {
|
|
331
|
+
...types_1.DEFAULT_CONFIG.server,
|
|
332
|
+
port: resolved.port,
|
|
333
|
+
},
|
|
334
|
+
ai: {
|
|
335
|
+
provider: resolved.provider || types_1.DEFAULT_CONFIG.ai.provider,
|
|
336
|
+
apiKey: resolved.apiKey,
|
|
337
|
+
baseUrl: resolved.baseUrl,
|
|
338
|
+
textBaseUrl: resolved.textBaseUrl,
|
|
339
|
+
textApiKey: resolved.textApiKey,
|
|
340
|
+
visionBaseUrl: resolved.visionBaseUrl,
|
|
341
|
+
visionApiKey: resolved.visionApiKey,
|
|
342
|
+
model: resolved.model,
|
|
343
|
+
visionModel: resolved.visionModel,
|
|
344
|
+
},
|
|
345
|
+
debug: resolved.debug,
|
|
346
|
+
};
|
|
347
|
+
// Auto-detect LLM availability: if neither a text nor a vision model is
|
|
348
|
+
// resolvable, the daemon still boots, but in tools-only mode. The MCP
|
|
349
|
+
// surface is fully available; the autonomous-agent path is disabled.
|
|
350
|
+
const llmAvailable = !forceNoLlm && Boolean(resolved.apiKey || resolved.textApiKey || resolved.visionApiKey
|
|
351
|
+
|| (resolved.baseUrl && (resolved.model || resolved.visionModel))
|
|
352
|
+
|| (resolved.textBaseUrl && resolved.model)
|
|
353
|
+
|| (resolved.visionBaseUrl && resolved.visionModel));
|
|
354
|
+
// SECURITY (#113): this surface is full desktop control. Loopback-only by
|
|
355
|
+
// default — a non-loopback bind (0.0.0.0 / LAN IP in config.server.host)
|
|
356
|
+
// requires an explicit `--allow-remote`, so a config typo can't silently
|
|
357
|
+
// expose the machine with only the bearer token in the way.
|
|
358
|
+
if (!(0, http_utility_1.isLoopbackHost)(config.server.host)) {
|
|
359
|
+
if (!opts.allowRemote) {
|
|
360
|
+
console.error(`${(0, format_1.e)('🛑', '[BLOCKED]')} Refusing to bind to non-loopback host "${config.server.host}".\n` +
|
|
361
|
+
` This endpoint grants FULL desktop control; exposing it beyond 127.0.0.1 means\n` +
|
|
362
|
+
` anyone on the network with the bearer token can drive this machine.\n` +
|
|
363
|
+
` If that is really what you want, restart with: clawdcursor agent --allow-remote\n` +
|
|
364
|
+
` Otherwise set server.host back to 127.0.0.1 in your config.`);
|
|
365
|
+
process.exit(1);
|
|
366
|
+
}
|
|
367
|
+
console.warn(`${(0, format_1.e)('⚠️', '[WARN]')} --allow-remote: binding to "${config.server.host}" — desktop control is\n` +
|
|
368
|
+
` reachable from the network. The Bearer token is the ONLY protection. Prefer an\n` +
|
|
369
|
+
` SSH tunnel or VPN over exposing this directly.`);
|
|
370
|
+
}
|
|
371
|
+
const modeLabel = llmAvailable ? '' : ' (tools-only)';
|
|
372
|
+
console.log(`${picocolors_1.default.green('✓')} ${picocolors_1.default.bold('clawdcursor')} ${picocolors_1.default.gray(`v${version_1.VERSION}`)} ${picocolors_1.default.gray(`— desktop control active on ${config.server.host}:${config.server.port}${modeLabel}`)}`);
|
|
373
|
+
// ── Agent (only when an LLM is configured) ──
|
|
374
|
+
let agent;
|
|
375
|
+
if (llmAvailable) {
|
|
376
|
+
agent = new agent_1.Agent(config, resolved);
|
|
377
|
+
try {
|
|
378
|
+
await agent.connect();
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
console.error(`\n${(0, format_1.e)('❌', '[ERR]')} Failed to initialize native desktop control: ${err}`);
|
|
382
|
+
console.error(`\nThis usually means @nut-tree-fork/nut-js couldn't access the screen.`);
|
|
383
|
+
console.error(`Make sure you're running this on a desktop with a display.`);
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
// ── Scheduler (recurring tasks via cron) ──
|
|
388
|
+
// Loads persisted ScheduledTask[] from ~/.clawdcursor/scheduled-tasks.json
|
|
389
|
+
// and registers every enabled cron job. Idempotent. Only active when an
|
|
390
|
+
// agent is wired — the scheduler dispatches through agent.executeTask().
|
|
391
|
+
// Stdio MCP and `agent --no-llm` skip this; the scheduler tools still load
|
|
392
|
+
// but return an error explaining that no agent context is bound.
|
|
393
|
+
if (agent) {
|
|
394
|
+
try {
|
|
395
|
+
const { initScheduler } = await Promise.resolve().then(() => __importStar(require('../tools/scheduler')));
|
|
396
|
+
const minimalLog = {
|
|
397
|
+
info: (event, data) => console.log(`[scheduler] ${event}`, data ?? ''),
|
|
398
|
+
warn: (event, data) => console.warn(`[scheduler] ${event}`, data ?? ''),
|
|
399
|
+
error: (event, data) => console.error(`[scheduler] ${event}`, data ?? ''),
|
|
400
|
+
};
|
|
401
|
+
const result = initScheduler(agent, minimalLog);
|
|
402
|
+
if (result.registered > 0 || result.failed > 0) {
|
|
403
|
+
console.log(` ${(0, format_1.e)('⏰', '[CRN]')} Scheduler: ${result.registered} active job(s)${result.failed ? `, ${result.failed} failed` : ''}`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
console.warn(` ${(0, format_1.e)('⚠️', '[WARN]')} Scheduler init failed (non-fatal): ${err.message}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// ── HTTP utility surface (/, /health, /stop) + MCP transport at /mcp ──
|
|
411
|
+
const serverShutdown = {
|
|
412
|
+
onAbort: () => {
|
|
413
|
+
agent?.abort();
|
|
414
|
+
},
|
|
415
|
+
onStop: async () => {
|
|
416
|
+
try {
|
|
417
|
+
// Lazy require — only attempt when agent existed (scheduler bound).
|
|
418
|
+
if (agent) {
|
|
419
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
420
|
+
const { stopScheduler } = require('../tools/scheduler');
|
|
421
|
+
stopScheduler();
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
catch { /* non-fatal */ }
|
|
425
|
+
// Abort the in-flight task and let the loop settle so the user sees
|
|
426
|
+
// the "aborted by user" acknowledgment instead of a silent hard kill.
|
|
427
|
+
try {
|
|
428
|
+
agent?.abort();
|
|
429
|
+
await agent?.waitForIdle(2000);
|
|
430
|
+
}
|
|
431
|
+
catch { /* non-fatal */ }
|
|
432
|
+
agent?.disconnect();
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
const app = (0, http_utility_1.createUtilityServer)({
|
|
436
|
+
host: config.server.host,
|
|
437
|
+
...serverShutdown,
|
|
438
|
+
});
|
|
439
|
+
// ── On-screen control banner: double-click = the `clawdcursor stop` flow ──
|
|
440
|
+
// (same abort → graceful-stop → exit sequence as POST /stop, same grace
|
|
441
|
+
// window and hard-kill net). Disable with --no-banner / CLAWD_NO_BANNER=1.
|
|
442
|
+
const { controlBanner } = await Promise.resolve().then(() => __importStar(require('../core/banner')));
|
|
443
|
+
if (opts.banner === false)
|
|
444
|
+
controlBanner.setEnabled(false);
|
|
445
|
+
controlBanner.configure({
|
|
446
|
+
onStopRequested: () => {
|
|
447
|
+
console.log(`\n${(0, format_1.e)('🛑', '[STOP]')} Control banner double-clicked — running the stop flow...`);
|
|
448
|
+
try {
|
|
449
|
+
serverShutdown.onAbort();
|
|
450
|
+
}
|
|
451
|
+
catch { /* non-fatal */ }
|
|
452
|
+
const grace = Promise.resolve().then(() => serverShutdown.onStop()).catch(() => { });
|
|
453
|
+
const cap = new Promise(resolve => setTimeout(resolve, 2500));
|
|
454
|
+
void Promise.race([grace, cap]).then(() => process.exit(0));
|
|
455
|
+
setTimeout(() => process.exit(1), 6000); // hard-kill safety net
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
// Build the ToolContext shared by every MCP handler. In agent mode it
|
|
459
|
+
// reuses the agent's already-connected NativeDesktop and AccessibilityBridge;
|
|
460
|
+
// in --no-llm mode it boots a fresh ToolContext like the legacy serve cmd.
|
|
461
|
+
const { getPlatform } = await Promise.resolve().then(() => __importStar(require('../platform')));
|
|
462
|
+
// One UIMapHolder per daemon session — shared by the agent loop (via runAgent
|
|
463
|
+
// deps) and the MCP surface (via toolCtx → toolContextToAgent bridge). Mirror
|
|
464
|
+
// how cdpDriver is wired: create once here, attach to the agent, pass via ctx.
|
|
465
|
+
const uiMapHolder = new ui_map_holder_1.UIMapHolder();
|
|
466
|
+
let toolCtx;
|
|
467
|
+
if (agent) {
|
|
468
|
+
let platform;
|
|
469
|
+
try {
|
|
470
|
+
platform = await getPlatform();
|
|
471
|
+
}
|
|
472
|
+
catch { /* non-fatal */ }
|
|
473
|
+
// The Agent class doesn't currently own a CDPDriver — that bridge lives
|
|
474
|
+
// on the toolCtx for both `agent` and `agent --no-llm`. Without this,
|
|
475
|
+
// navigate_browser / cdp_* tools hit "Cannot read properties of
|
|
476
|
+
// undefined" on first call. Instantiate one here and wire it into the
|
|
477
|
+
// context so the MCP catalog has the same surface in both modes.
|
|
478
|
+
const { CDPDriver } = await Promise.resolve().then(() => __importStar(require('../platform/cdp-driver')));
|
|
479
|
+
const { DEFAULT_CDP_PORT } = await Promise.resolve().then(() => __importStar(require('../llm/browser-config')));
|
|
480
|
+
const cdp = agent.cdpDriver ?? new CDPDriver(DEFAULT_CDP_PORT);
|
|
481
|
+
if (!agent.cdpDriver)
|
|
482
|
+
agent.cdpDriver = cdp;
|
|
483
|
+
if (!agent.uiMapHolder)
|
|
484
|
+
agent.uiMapHolder = uiMapHolder;
|
|
485
|
+
// Mouse-scale: agent mode used to hardcode 1, which broke every
|
|
486
|
+
// vision-driven click on HiDPI. The contract for mouse_* tools is
|
|
487
|
+
// "input is image-space coords, scale internally to whatever the
|
|
488
|
+
// input driver expects." On Windows + recent nut-js the driver
|
|
489
|
+
// operates in physical-pixel space, so the right factor is
|
|
490
|
+
// physical / image = getScaleFactor(). On 2× DPI: image (418, 453)
|
|
491
|
+
// × 2 → click at physical (836, 906) — which IS image (418, 453)
|
|
492
|
+
// visually. On 1× DPI: factor = 1, no change. Fixes the "agent
|
|
493
|
+
// sees the orange circle, clicks the sidebar 2× to the left" bug.
|
|
494
|
+
toolCtx = {
|
|
495
|
+
desktop: agent.getDesktop(),
|
|
496
|
+
a11y: agent.a11y,
|
|
497
|
+
cdp,
|
|
498
|
+
uiMaps: uiMapHolder,
|
|
499
|
+
platform,
|
|
500
|
+
agent,
|
|
501
|
+
getLogBuffer: http_utility_1.getServerLogBuffer,
|
|
502
|
+
getMouseScaleFactor: () => agent.getDesktop().getScaleFactor(),
|
|
503
|
+
getScreenshotScaleFactor: () => agent.getDesktop().getScaleFactor(),
|
|
504
|
+
ensureInitialized: async () => { }, // agent already initialized
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
toolCtx = await createToolContext();
|
|
509
|
+
toolCtx.ensureInitialized().catch((err) => {
|
|
510
|
+
console.error('Subsystem init failed:', err?.message);
|
|
511
|
+
});
|
|
512
|
+
if (toolCtx.cdp) {
|
|
513
|
+
toolCtx.cdp.connect().then(() => {
|
|
514
|
+
console.log(` ${(0, format_1.e)('🌐', '[NET]')} CDP connected to browser`);
|
|
515
|
+
}).catch(() => {
|
|
516
|
+
console.log(` ${(0, format_1.e)('ℹ️', 'i')} CDP: no browser detected (will retry when web tools are called)`);
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
toolCtx.getLogBuffer = http_utility_1.getServerLogBuffer;
|
|
520
|
+
toolCtx.uiMaps = uiMapHolder;
|
|
521
|
+
}
|
|
522
|
+
// Mount /mcp behind the same Bearer-auth gate the legacy REST routes used.
|
|
523
|
+
// Compact-over-HTTP: default granular for backward compatibility (the
|
|
524
|
+
// dashboard at / calls 9 granular tool names — see dashboard.ts). External
|
|
525
|
+
// agent hosts can opt into the 6-compound public surface via
|
|
526
|
+
// `clawdcursor agent --compact` or `CLAWD_MCP_COMPACT=1`.
|
|
527
|
+
const compactSurface = opts.compact === true || process.env.CLAWD_MCP_COMPACT === '1';
|
|
528
|
+
let mcpToolCount = 0;
|
|
529
|
+
try {
|
|
530
|
+
const { createMcpServer, startMcpHttp } = await Promise.resolve().then(() => __importStar(require('./mcp-server')));
|
|
531
|
+
const { server: mcpServer, toolCount } = await createMcpServer({ compact: compactSurface, ctx: toolCtx });
|
|
532
|
+
mcpToolCount = toolCount;
|
|
533
|
+
app.use('/mcp', http_utility_1.requireAuth);
|
|
534
|
+
await startMcpHttp(mcpServer, app, '/mcp');
|
|
535
|
+
}
|
|
536
|
+
catch (err) {
|
|
537
|
+
console.warn('MCP HTTP transport not loaded:', err.message);
|
|
538
|
+
}
|
|
539
|
+
// LAST mount — JSON 404 for unmatched routes (must come after /mcp).
|
|
540
|
+
(0, http_utility_1.mountJson404)(app);
|
|
541
|
+
app.listen(config.server.port, config.server.host, async () => {
|
|
542
|
+
const serverToken = (0, http_utility_1.initServerToken)();
|
|
543
|
+
const tokenPath = path.join(require('os').homedir(), '.clawdcursor', 'token');
|
|
544
|
+
console.log(`\n${picocolors_1.default.green(`${(0, format_1.e)('🌐', '[NET]')} API server:`)} http://${config.server.host}:${config.server.port}`);
|
|
545
|
+
console.log(`${picocolors_1.default.yellow(`${(0, format_1.e)('🔑', '[KEY]')} Auth token:`)} ${serverToken.slice(0, 8)}...`);
|
|
546
|
+
console.log(picocolors_1.default.gray(` (full token saved to ${tokenPath})`));
|
|
547
|
+
console.log(`\nSurviving HTTP routes:`);
|
|
548
|
+
console.log(` GET / — Dashboard (calls /mcp via JSON-RPC)`);
|
|
549
|
+
console.log(` GET /health — Readiness probe (no auth)`);
|
|
550
|
+
console.log(` POST /abort — Abort the in-flight task (auth, localhost only)`);
|
|
551
|
+
console.log(` POST /stop — Graceful shutdown (auth, localhost only)`);
|
|
552
|
+
console.log(`\nMCP endpoint (the only protocol):`);
|
|
553
|
+
console.log(` POST /mcp — JSON-RPC tools/call & tools/list (auth) — ${compactSurface ? 'compact' : 'granular'} surface, ${mcpToolCount} tools`);
|
|
554
|
+
console.log(` GET /mcp — SSE notifications (auth)`);
|
|
555
|
+
if (!compactSurface) {
|
|
556
|
+
console.log(picocolors_1.default.gray(` (tip: pass --compact or set CLAWD_MCP_COMPACT=1 to expose the 6-compound public surface)`));
|
|
557
|
+
}
|
|
558
|
+
console.log(`\nAll mutating endpoints require: ${picocolors_1.default.cyan('Authorization: Bearer <token>')}`);
|
|
559
|
+
if (llmAvailable) {
|
|
560
|
+
const { loadPipelineConfig } = await Promise.resolve().then(() => __importStar(require('./doctor')));
|
|
561
|
+
// Pass the resolved CLI overlay so the validation/print path sees the
|
|
562
|
+
// same pipeline config the agent runtime will use.
|
|
563
|
+
const pipelineConfig = loadPipelineConfig(resolved);
|
|
564
|
+
if (pipelineConfig && pipelineConfig.layer2.enabled) {
|
|
565
|
+
try {
|
|
566
|
+
const { callTextLLMDirect } = await Promise.resolve().then(() => __importStar(require('../llm/client')));
|
|
567
|
+
const { PROVIDERS, PROVIDER_ENV_VARS } = await Promise.resolve().then(() => __importStar(require('../llm/providers')));
|
|
568
|
+
const { inferProviderFromBaseUrl } = await Promise.resolve().then(() => __importStar(require('../llm/credentials')));
|
|
569
|
+
const layer2ProviderKey = inferProviderFromBaseUrl(pipelineConfig.layer2.baseUrl) || pipelineConfig.providerKey;
|
|
570
|
+
const layer2Provider = PROVIDERS[layer2ProviderKey] || pipelineConfig.provider;
|
|
571
|
+
const layer2ApiKey = (PROVIDER_ENV_VARS[layer2ProviderKey] || [])
|
|
572
|
+
.map((k) => process.env[k]).find((v) => v && v.length > 0)
|
|
573
|
+
|| pipelineConfig.apiKey;
|
|
574
|
+
await callTextLLMDirect({
|
|
575
|
+
baseUrl: pipelineConfig.layer2.baseUrl,
|
|
576
|
+
model: pipelineConfig.layer2.model,
|
|
577
|
+
apiKey: layer2ApiKey,
|
|
578
|
+
isAnthropic: !layer2Provider.openaiCompat,
|
|
579
|
+
messages: [{ role: 'user', content: 'Reply with just the word "ok"' }],
|
|
580
|
+
maxTokens: 5,
|
|
581
|
+
timeoutMs: 10000,
|
|
582
|
+
retries: 0,
|
|
583
|
+
});
|
|
584
|
+
console.log(`${(0, format_1.e)('✅', '[OK]')} API key validated for ${layer2Provider.name}`);
|
|
585
|
+
// Print the resolved model wiring so the user can see which model
|
|
586
|
+
// drives reasoning (text) vs perception (vision) BEFORE any task
|
|
587
|
+
// runs. Without this you only see model names on the task header,
|
|
588
|
+
// which is too late if the wiring is wrong (mismatched provider,
|
|
589
|
+
// unexpected default, stale .clawdcursor-config.json).
|
|
590
|
+
try {
|
|
591
|
+
const textModel = pipelineConfig.layer2?.model || '(default)';
|
|
592
|
+
const visionModel = pipelineConfig.layer3?.model || '(default)';
|
|
593
|
+
const textBase = pipelineConfig.layer2?.baseUrl || '(provider default)';
|
|
594
|
+
const visionBase = pipelineConfig.layer3?.baseUrl || textBase;
|
|
595
|
+
const visionState = pipelineConfig.layer3?.enabled === false ? ' [disabled]' : '';
|
|
596
|
+
console.log(picocolors_1.default.gray(` text : ${textModel} ← ${textBase}`));
|
|
597
|
+
console.log(picocolors_1.default.gray(` vision: ${visionModel}${visionState} ← ${visionBase}`));
|
|
598
|
+
}
|
|
599
|
+
catch { /* non-fatal — boot continues either way */ }
|
|
600
|
+
}
|
|
601
|
+
catch (err) {
|
|
602
|
+
if (err.name === 'LLMAuthError') {
|
|
603
|
+
console.error(`\n${(0, format_1.e)('❌', '[ERR]')} API key INVALID for ${pipelineConfig.provider.name} (${pipelineConfig.layer2.model})`);
|
|
604
|
+
console.error(` The saved config has an expired or revoked key. Tools-only mode still works.\n`);
|
|
605
|
+
const staleConfig = path.join((0, paths_1.getPackageRoot)(), '.clawdcursor-config.json');
|
|
606
|
+
try {
|
|
607
|
+
fs.unlinkSync(staleConfig);
|
|
608
|
+
}
|
|
609
|
+
catch { /* ok */ }
|
|
610
|
+
console.error(` ${(0, format_1.e)('🗑️', '[DEL]')} Removed stale config. Fix your key and restart:`);
|
|
611
|
+
console.error(` 1. Update your API key in .env or environment variables`);
|
|
612
|
+
console.error(` 2. Run: clawdcursor agent (will re-detect providers)`);
|
|
613
|
+
console.error(` Or run: clawdcursor doctor to reconfigure manually\n`);
|
|
614
|
+
if (agent)
|
|
615
|
+
gracefulExitOnInitFailure(1, agent);
|
|
616
|
+
else
|
|
617
|
+
process.exit(1);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
else if (err.name === 'LLMBillingError') {
|
|
621
|
+
console.error(`\n${(0, format_1.e)('❌', '[ERR]')} API credits exhausted for ${pipelineConfig.provider.name}`);
|
|
622
|
+
console.error(` Add credits or switch providers, then restart.`);
|
|
623
|
+
console.error(` Run: clawdcursor doctor to reconfigure\n`);
|
|
624
|
+
if (agent)
|
|
625
|
+
gracefulExitOnInitFailure(1, agent);
|
|
626
|
+
else
|
|
627
|
+
process.exit(1);
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
console.warn(`${(0, format_1.e)('⚠️', '[WARN]')} Could not validate API key: ${err.message?.substring(0, 100)}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
else if (config.ai.model || config.ai.visionModel) {
|
|
636
|
+
console.log(`${(0, format_1.e)('✅', '[OK]')} Using externally configured models: text=${config.ai.model} | vision=${config.ai.visionModel}`);
|
|
637
|
+
}
|
|
638
|
+
const { MIN_RECOMMENDED_CONTEXT } = await Promise.resolve().then(() => __importStar(require('../llm/providers')));
|
|
639
|
+
const ctxWindow = pipelineConfig?.provider?.textContextWindow;
|
|
640
|
+
if (ctxWindow && ctxWindow < MIN_RECOMMENDED_CONTEXT) {
|
|
641
|
+
console.warn(`${(0, format_1.e)('⚠️', '[WARN]')} Text model context window (${Math.round(ctxWindow / 1000)}K) is below the recommended minimum (${Math.round(MIN_RECOMMENDED_CONTEXT / 1000)}K).`);
|
|
642
|
+
console.warn(` Web pages with many elements may overflow. Consider using a larger model.`);
|
|
643
|
+
console.warn(` Run: clawdcursor doctor to switch models\n`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
// Tools-only mode — the MCP catalog is fully available, the autonomous
|
|
648
|
+
// agent is disabled (no LLM was configured). External hosts (Claude
|
|
649
|
+
// Code, Cursor, Windsurf, OpenClaw) drive the verbs directly.
|
|
650
|
+
console.log(`${(0, format_1.e)('🐾', '>')} Tools-only mode. Connect any MCP-capable host — your model drives the verbs.`);
|
|
651
|
+
console.log(` Run \`clawdcursor doctor\` if you want to enable the built-in autonomous agent later.`);
|
|
652
|
+
}
|
|
653
|
+
console.log(`\nReady. ${(0, format_1.e)('🐾', '')}`);
|
|
654
|
+
});
|
|
655
|
+
process.on('SIGINT', () => {
|
|
656
|
+
console.log(`\n${(0, format_1.e)('👋', '--')} Shutting down...`);
|
|
657
|
+
(0, pidfile_1.releasePidFile)('start');
|
|
658
|
+
agent?.disconnect();
|
|
659
|
+
process.exit(0);
|
|
660
|
+
});
|
|
661
|
+
process.on('SIGTERM', () => {
|
|
662
|
+
(0, pidfile_1.releasePidFile)('start');
|
|
663
|
+
agent?.disconnect();
|
|
664
|
+
process.exit(0);
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
program
|
|
668
|
+
.command('agent')
|
|
669
|
+
.description('Start the clawdcursor daemon (autonomous agent + MCP-over-HTTP)')
|
|
670
|
+
.option('--port <port>', 'API server port', '3847')
|
|
671
|
+
.option('--provider <provider>', 'AI provider (auto-detected, or specify: anthropic|openai|ollama|kimi|groq|...)')
|
|
672
|
+
.option('--model <model>', 'Vision model to use')
|
|
673
|
+
.option('--text-model <model>', 'Text/reasoning model for Layer 2')
|
|
674
|
+
.option('--vision-model <model>', 'Vision model for Layer 3')
|
|
675
|
+
.option('--base-url <url>', 'Custom API base URL (OpenAI-compatible)')
|
|
676
|
+
.option('--api-key <key>', 'AI provider API key')
|
|
677
|
+
.option('--debug', 'Save screenshots to debug/ folder (off by default)')
|
|
678
|
+
.option('--accept', 'Accept desktop control consent non-interactively and start')
|
|
679
|
+
.option('--no-vision', 'Refuse vision fallback — blind-first only (high-security mode)')
|
|
680
|
+
.option('--no-llm', 'Force tools-only HTTP MCP mode; skip AI setup, scheduler, and credential validation')
|
|
681
|
+
.option('--skip-consent', 'Skip consent prompt (requires NODE_ENV=development)')
|
|
682
|
+
.option('--compact', 'Expose the 6-compound MCP surface (computer/accessibility/window/system/browser/task) over HTTP /mcp instead of the 97 granular tools (also CLAWD_MCP_COMPACT=1)')
|
|
683
|
+
.option('--allow-remote', 'Permit binding to a non-loopback server.host. DANGER: exposes full desktop control to the network; the Bearer token is the only protection')
|
|
684
|
+
.option('--no-banner', 'Disable the on-screen "desktop control in progress" banner (also CLAWD_NO_BANNER=1)')
|
|
685
|
+
.action(async (opts) => {
|
|
686
|
+
await runAgentMode(opts);
|
|
687
|
+
});
|
|
688
|
+
program
|
|
689
|
+
.command('start')
|
|
690
|
+
.description('[deprecated — use `clawdcursor agent`] Start the Clawd Cursor agent')
|
|
691
|
+
.option('--port <port>', 'API server port', '3847')
|
|
692
|
+
.option('--provider <provider>', 'AI provider (auto-detected, or specify: anthropic|openai|ollama|kimi|groq|...)')
|
|
693
|
+
.option('--model <model>', 'Vision model to use')
|
|
694
|
+
.option('--text-model <model>', 'Text/reasoning model for Layer 2')
|
|
695
|
+
.option('--vision-model <model>', 'Vision model for Layer 3')
|
|
696
|
+
.option('--base-url <url>', 'Custom API base URL (OpenAI-compatible)')
|
|
697
|
+
.option('--api-key <key>', 'AI provider API key')
|
|
698
|
+
.option('--debug', 'Save screenshots to debug/ folder (off by default)')
|
|
699
|
+
.option('--accept', 'Accept desktop control consent non-interactively and start')
|
|
700
|
+
.option('--no-vision', 'Refuse vision fallback — blind-first only (high-security mode)')
|
|
701
|
+
.option('--no-llm', 'Force tools-only HTTP MCP mode; skip AI setup, scheduler, and credential validation')
|
|
702
|
+
.option('--compact', 'Expose the 6-compound MCP surface over HTTP /mcp (also CLAWD_MCP_COMPACT=1)')
|
|
703
|
+
.option('--allow-remote', 'Permit binding to a non-loopback server.host. DANGER: exposes full desktop control to the network; the Bearer token is the only protection')
|
|
704
|
+
.option('--no-banner', 'Disable the on-screen "desktop control in progress" banner (also CLAWD_NO_BANNER=1)')
|
|
705
|
+
.action(async (opts) => {
|
|
706
|
+
// v0.9 PR7.4 — `start` is now a thin deprecation alias for `agent`.
|
|
707
|
+
// The legacy /task /favorites /execute REST surface was deleted; callers
|
|
708
|
+
// that still ran `clawdcursor start` keep working through this proxy
|
|
709
|
+
// until v0.10. Removed in v0.10.
|
|
710
|
+
console.warn(`${(0, format_1.e)('⚠', '[WARN]')} \`clawdcursor start\` is deprecated; use \`clawdcursor agent\`. Removed in v0.10.`);
|
|
711
|
+
await runAgentMode(opts);
|
|
712
|
+
});
|
|
713
|
+
// ── Legacy start command body deleted in PR7.4 ──
|
|
714
|
+
// The runAgentMode() function above is the canonical implementation.
|
|
715
|
+
// `start` and `serve` are now thin deprecation aliases.
|
|
716
|
+
program
|
|
717
|
+
.command('doctor')
|
|
718
|
+
.description('🩺 Diagnose setup and configure the AI provider — only needed for `clawdcursor agent` (the autonomous daemon with its own LLM). Driving clawdcursor from your own agent over MCP needs no doctor: consent + (macOS) `grant` is the whole setup.')
|
|
719
|
+
.option('--provider <provider>', 'AI provider (auto-detected, or specify: anthropic|openai|ollama|kimi|groq|...)')
|
|
720
|
+
.option('--api-key <key>', 'AI provider API key')
|
|
721
|
+
.option('--no-save', 'Don\'t save config to disk')
|
|
722
|
+
.option('--reset', 'Delete saved config and re-detect everything from scratch')
|
|
723
|
+
.action(async (opts) => {
|
|
724
|
+
const { runDoctor } = await Promise.resolve().then(() => __importStar(require('./doctor')));
|
|
725
|
+
const resolvedApi = (0, credentials_1.resolveApiConfig)({
|
|
726
|
+
apiKey: opts.apiKey,
|
|
727
|
+
provider: opts.provider,
|
|
728
|
+
});
|
|
729
|
+
if (opts.reset) {
|
|
730
|
+
const configPath = path.join((0, paths_1.getPackageRoot)(), '.clawdcursor-config.json');
|
|
731
|
+
if (fs.existsSync(configPath)) {
|
|
732
|
+
fs.unlinkSync(configPath);
|
|
733
|
+
console.log(`${(0, format_1.e)('🗑️', '[DEL]')} Cleared saved config — re-detecting from scratch\n`);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
// Only use explicit CLI flags for single-provider override.
|
|
737
|
+
// Auto-detected external credentials should go through multi-provider scan.
|
|
738
|
+
const isExplicit = !!(opts.apiKey || opts.provider);
|
|
739
|
+
await runDoctor({
|
|
740
|
+
apiKey: isExplicit ? resolvedApi.apiKey : undefined,
|
|
741
|
+
provider: isExplicit ? (resolvedApi.provider || opts.provider) : undefined,
|
|
742
|
+
baseUrl: isExplicit ? resolvedApi.baseUrl : undefined,
|
|
743
|
+
textModel: isExplicit ? resolvedApi.textModel : undefined,
|
|
744
|
+
visionModel: isExplicit ? resolvedApi.visionModel : undefined,
|
|
745
|
+
save: opts.save !== false,
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
program
|
|
749
|
+
.command('status')
|
|
750
|
+
.description('📊 Check readiness status (consent, permissions, AI config)')
|
|
751
|
+
.action(async () => {
|
|
752
|
+
const { printStatusReport } = await Promise.resolve().then(() => __importStar(require('./readiness')));
|
|
753
|
+
await printStatusReport();
|
|
754
|
+
});
|
|
755
|
+
program
|
|
756
|
+
.command('grant')
|
|
757
|
+
.description('🔐 Request macOS permissions (triggers system permission dialogs)')
|
|
758
|
+
.action(async () => {
|
|
759
|
+
if (process.platform !== 'darwin') {
|
|
760
|
+
console.log('Permission grants are only needed on macOS.');
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
const { requestPermissions } = await Promise.resolve().then(() => __importStar(require('../platform/native-helper')));
|
|
764
|
+
console.log('🔐 Requesting macOS permissions...');
|
|
765
|
+
console.log(' System dialogs may appear — please allow access.\n');
|
|
766
|
+
try {
|
|
767
|
+
const perms = await requestPermissions();
|
|
768
|
+
console.log(` Accessibility: ${perms.accessibility ? '✅ Granted' : '❌ Denied'}`);
|
|
769
|
+
console.log(` Screen Recording: ${perms.screenRecording ? '✅ Granted' : '❌ Denied'}`);
|
|
770
|
+
if (perms.accessibility && perms.screenRecording) {
|
|
771
|
+
console.log('\n🎉 All permissions granted — ready for desktop control!');
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
console.log('\n⚠️ Some permissions still missing. Grant them in System Settings, then run this again.');
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
catch (err) {
|
|
778
|
+
console.error(`❌ Failed to request permissions: ${err}`);
|
|
779
|
+
console.error(' Ensure ClawdCursor.app is built: cd native && ./build.sh');
|
|
780
|
+
}
|
|
781
|
+
});
|
|
782
|
+
program
|
|
783
|
+
.command('stop')
|
|
784
|
+
.description('Stop a running Clawd Cursor instance')
|
|
785
|
+
.option('--port <port>', 'API server port', '3847')
|
|
786
|
+
.action(async (opts) => {
|
|
787
|
+
const port = parseInt(opts.port, 10);
|
|
788
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
789
|
+
console.error('Invalid port number');
|
|
790
|
+
process.exit(1);
|
|
791
|
+
}
|
|
792
|
+
const isClawd = await isClawdInstance(port);
|
|
793
|
+
if (!isClawd) {
|
|
794
|
+
console.log(`${(0, format_1.e)('🐾', '>')} No running instance found on port ` + port);
|
|
795
|
+
if (process.platform === 'darwin') {
|
|
796
|
+
await (0, native_helper_1.stopHostApp)();
|
|
797
|
+
}
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
// Abort first so any active task exits quickly before shutdown.
|
|
801
|
+
try {
|
|
802
|
+
await fetch(`http://127.0.0.1:${port}/abort`, { method: 'POST', headers: authHeaders(), signal: AbortSignal.timeout(2000) });
|
|
803
|
+
}
|
|
804
|
+
catch {
|
|
805
|
+
// Best effort only.
|
|
806
|
+
}
|
|
807
|
+
const url = `http://127.0.0.1:${port}/stop`;
|
|
808
|
+
try {
|
|
809
|
+
const res = await fetch(url, { method: 'POST', headers: authHeaders(), signal: AbortSignal.timeout(5000) });
|
|
810
|
+
const data = await res.json();
|
|
811
|
+
if (data.stopped) {
|
|
812
|
+
console.log(`${(0, format_1.e)('🐾', '>')} Clawd Cursor stopped`);
|
|
813
|
+
}
|
|
814
|
+
else {
|
|
815
|
+
console.error('Unexpected response:', JSON.stringify(data));
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
catch {
|
|
819
|
+
// fetch may fail because server died mid-response — that's actually success
|
|
820
|
+
}
|
|
821
|
+
// Verify it actually stopped (wait up to 3s)
|
|
822
|
+
let serverStopped = false;
|
|
823
|
+
for (let i = 0; i < 6; i++) {
|
|
824
|
+
await new Promise(r => setTimeout(r, 500));
|
|
825
|
+
try {
|
|
826
|
+
await fetch(`http://127.0.0.1:${port}/health`, { signal: AbortSignal.timeout(1000) });
|
|
827
|
+
// Still alive — keep waiting
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
// Connection refused = dead = success
|
|
831
|
+
console.log(`${(0, format_1.e)('✅', '[OK]')} Server confirmed stopped`);
|
|
832
|
+
serverStopped = true;
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (!serverStopped) {
|
|
837
|
+
console.log(`${(0, format_1.e)('⚠️', '[WARN]')} Graceful stop did not complete — force killing...`);
|
|
838
|
+
const killed = await forceKillPort(port);
|
|
839
|
+
if (killed) {
|
|
840
|
+
console.log(`${(0, format_1.e)('🐾', '>')} Clawd Cursor force stopped`);
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
console.error(`${(0, format_1.e)('❌', '[ERR]')} Could not force stop process on port ` + port);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
// v0.8.3 — also sweep every other clawdcursor-owned pidfile (mcp, serve,
|
|
847
|
+
// start) and kill anything still alive. The old `stop` only targeted
|
|
848
|
+
// port 3847 via `/stop`, which missed `mcp` (stdio, no port) and any
|
|
849
|
+
// zombie `serve` / `start` that had crashed-but-not-released its pidfile.
|
|
850
|
+
// User-reported symptom: "Outlook keeps opening" — a stale serve process
|
|
851
|
+
// was still receiving MCP / REST traffic after the user thought they'd
|
|
852
|
+
// stopped. This sweep ensures `clawdcursor stop` means stop EVERYTHING.
|
|
853
|
+
let sweptCount = 0;
|
|
854
|
+
for (const mode of ['start', 'mcp', 'serve']) {
|
|
855
|
+
try {
|
|
856
|
+
const pidPath = (0, pidfile_1.pidFilePath)(mode);
|
|
857
|
+
if (!fs.existsSync(pidPath))
|
|
858
|
+
continue;
|
|
859
|
+
// readPidLoose accepts both legacy bare-int and the new JSON format.
|
|
860
|
+
const pid = (0, pidfile_1.readPidLoose)(mode);
|
|
861
|
+
if (pid === null || pid === process.pid) {
|
|
862
|
+
fs.unlinkSync(pidPath);
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
if ((0, pidfile_1.isProcessAlive)(pid)) {
|
|
866
|
+
try {
|
|
867
|
+
process.kill(pid, 'SIGTERM');
|
|
868
|
+
// Give it a moment to exit gracefully, then SIGKILL if still up.
|
|
869
|
+
await new Promise(r => setTimeout(r, 500));
|
|
870
|
+
if ((0, pidfile_1.isProcessAlive)(pid))
|
|
871
|
+
process.kill(pid, 'SIGKILL');
|
|
872
|
+
sweptCount++;
|
|
873
|
+
console.log(`${(0, format_1.e)('🐾', '>')} Stopped ${mode} instance (pid ${pid})`);
|
|
874
|
+
}
|
|
875
|
+
catch {
|
|
876
|
+
// Could not kill — the process may be owned by a different user.
|
|
877
|
+
console.warn(`${(0, format_1.e)('⚠', '[WARN]')} Could not stop ${mode} pid ${pid}`);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
// Clean up the pidfile regardless.
|
|
881
|
+
try {
|
|
882
|
+
fs.unlinkSync(pidPath);
|
|
883
|
+
}
|
|
884
|
+
catch { }
|
|
885
|
+
}
|
|
886
|
+
catch { /* best-effort */ }
|
|
887
|
+
}
|
|
888
|
+
if (sweptCount > 0) {
|
|
889
|
+
console.log(`${(0, format_1.e)('✅', '[OK]')} Swept ${sweptCount} additional clawdcursor instance${sweptCount === 1 ? '' : 's'}`);
|
|
890
|
+
}
|
|
891
|
+
if (process.platform === 'darwin') {
|
|
892
|
+
await (0, native_helper_1.stopHostApp)();
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
program
|
|
896
|
+
.command('task [text]')
|
|
897
|
+
.description('Send a task to a running Clawd Cursor instance (interactive if no text given)')
|
|
898
|
+
.option('--port <port>', 'API server port', '3847')
|
|
899
|
+
.action(async (text, opts) => {
|
|
900
|
+
const url = `http://127.0.0.1:${opts.port}/mcp`;
|
|
901
|
+
const sendTask = async (taskText) => {
|
|
902
|
+
try {
|
|
903
|
+
console.log(`\n${(0, format_1.e)('🐾', '>')} Sending: ${taskText}`);
|
|
904
|
+
const res = await fetch(url, {
|
|
905
|
+
method: 'POST',
|
|
906
|
+
headers: {
|
|
907
|
+
'Content-Type': 'application/json',
|
|
908
|
+
Accept: 'application/json, text/event-stream',
|
|
909
|
+
...authHeaders(),
|
|
910
|
+
},
|
|
911
|
+
body: JSON.stringify({
|
|
912
|
+
jsonrpc: '2.0',
|
|
913
|
+
id: 1,
|
|
914
|
+
method: 'tools/call',
|
|
915
|
+
params: { name: 'submit_task', arguments: { task: taskText } },
|
|
916
|
+
}),
|
|
917
|
+
});
|
|
918
|
+
if (res.status === 401) {
|
|
919
|
+
console.error('Auth failed (401). Token mismatch — run: clawdcursor stop && clawdcursor agent');
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
if (!res.ok) {
|
|
923
|
+
console.error(`Server error (${res.status}). Check server logs.`);
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
const data = await res.json();
|
|
927
|
+
if (data?.error) {
|
|
928
|
+
console.error(`MCP error: ${data.error.message ?? JSON.stringify(data.error)}`);
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
// Pull the task result text out of the JSON-RPC envelope
|
|
932
|
+
const content = data?.result?.content;
|
|
933
|
+
if (Array.isArray(content)) {
|
|
934
|
+
for (const block of content) {
|
|
935
|
+
if (block?.type === 'text' && typeof block.text === 'string')
|
|
936
|
+
console.log(block.text);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
else {
|
|
940
|
+
console.log(JSON.stringify(data, null, 2));
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
catch {
|
|
944
|
+
console.error(`Failed to connect to Clawd Cursor at ${url}`);
|
|
945
|
+
console.error('Is the agent running? Start it with: clawdcursor agent');
|
|
946
|
+
}
|
|
947
|
+
};
|
|
948
|
+
if (text) {
|
|
949
|
+
// One-shot mode: clawdcursor task "Open Calculator"
|
|
950
|
+
await sendTask(text);
|
|
951
|
+
}
|
|
952
|
+
else {
|
|
953
|
+
// Interactive mode: spawn a new terminal window
|
|
954
|
+
const os = await Promise.resolve().then(() => __importStar(require('os')));
|
|
955
|
+
const { execFile: spawnExec } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
956
|
+
const platform = os.platform();
|
|
957
|
+
const token = loadAuthToken();
|
|
958
|
+
const scriptContent = platform === 'win32'
|
|
959
|
+
? // Windows: PowerShell script
|
|
960
|
+
`
|
|
961
|
+
$host.UI.RawUI.WindowTitle = "Clawd Cursor - Task Console"
|
|
962
|
+
Write-Host "Clawd Cursor - Interactive Task Mode" -ForegroundColor Cyan
|
|
963
|
+
Write-Host " Type a task and press Enter. Type 'quit' to exit." -ForegroundColor Gray
|
|
964
|
+
Write-Host ""
|
|
965
|
+
$headers = @{ "Content-Type" = "application/json"; "Accept" = "application/json, text/event-stream"${token ? `; "Authorization" = "Bearer ${token}"` : ''} }
|
|
966
|
+
$rpcId = 0
|
|
967
|
+
while ($true) {
|
|
968
|
+
$task = Read-Host "Enter task"
|
|
969
|
+
if (-not $task -or $task -eq "quit" -or $task -eq "exit") {
|
|
970
|
+
Write-Host "Bye!"
|
|
971
|
+
break
|
|
972
|
+
}
|
|
973
|
+
# Strip control characters (Ctrl+L, etc.) that break JSON
|
|
974
|
+
$task = $task -replace '[\\x00-\\x1f]', ''
|
|
975
|
+
$task = $task.Trim()
|
|
976
|
+
if (-not $task) { continue }
|
|
977
|
+
Write-Host "> Sending: $task" -ForegroundColor Yellow
|
|
978
|
+
try {
|
|
979
|
+
$rpcId = $rpcId + 1
|
|
980
|
+
$body = @{
|
|
981
|
+
jsonrpc = '2.0'
|
|
982
|
+
id = $rpcId
|
|
983
|
+
method = 'tools/call'
|
|
984
|
+
params = @{ name = 'submit_task'; arguments = @{ task = $task } }
|
|
985
|
+
} | ConvertTo-Json -Depth 6 -Compress
|
|
986
|
+
$response = Invoke-RestMethod -Uri http://127.0.0.1:${opts.port}/mcp -Method POST -Headers $headers -Body $body
|
|
987
|
+
if ($response.error) {
|
|
988
|
+
Write-Host ("MCP error: " + ($response.error.message)) -ForegroundColor Red
|
|
989
|
+
} elseif ($response.result -and $response.result.content) {
|
|
990
|
+
foreach ($block in $response.result.content) {
|
|
991
|
+
if ($block.type -eq 'text') { Write-Host $block.text }
|
|
992
|
+
}
|
|
993
|
+
} else {
|
|
994
|
+
$response | ConvertTo-Json -Depth 5
|
|
995
|
+
}
|
|
996
|
+
} catch {
|
|
997
|
+
if ($_.Exception.Response) {
|
|
998
|
+
$code = [int]$_.Exception.Response.StatusCode
|
|
999
|
+
if ($code -eq 401) {
|
|
1000
|
+
Write-Host 'Auth failed (401). Token mismatch. Run: clawdcursor stop then clawdcursor agent' -ForegroundColor Red
|
|
1001
|
+
} else {
|
|
1002
|
+
Write-Host "Server error ($code). Check server logs." -ForegroundColor Red
|
|
1003
|
+
}
|
|
1004
|
+
} else {
|
|
1005
|
+
Write-Host 'Failed to connect. Is clawdcursor agent running?' -ForegroundColor Red
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
Write-Host ""
|
|
1009
|
+
}
|
|
1010
|
+
`
|
|
1011
|
+
: // macOS/Linux: bash script
|
|
1012
|
+
`
|
|
1013
|
+
echo "Clawd Cursor - Interactive Task Mode"
|
|
1014
|
+
echo " Type a task and press Enter. Type 'quit' to exit."
|
|
1015
|
+
echo ""
|
|
1016
|
+
AUTH_HEADER="${token ? `Authorization: Bearer ${token}` : ''}"
|
|
1017
|
+
RPC_ID=0
|
|
1018
|
+
while true; do
|
|
1019
|
+
printf "Enter task: "
|
|
1020
|
+
read task
|
|
1021
|
+
if [ -z "$task" ] || [ "$task" = "quit" ] || [ "$task" = "exit" ]; then
|
|
1022
|
+
echo "Bye!"
|
|
1023
|
+
break
|
|
1024
|
+
fi
|
|
1025
|
+
echo "> Sending: $task"
|
|
1026
|
+
RPC_ID=$((RPC_ID + 1))
|
|
1027
|
+
# JSON-encode the task by piping through python; falls back to naive escape if python missing
|
|
1028
|
+
BODY=$(python3 -c "import json,sys; print(json.dumps({'jsonrpc':'2.0','id':$RPC_ID,'method':'tools/call','params':{'name':'submit_task','arguments':{'task':sys.argv[1]}}}))" "$task" 2>/dev/null) || BODY="{\\"jsonrpc\\":\\"2.0\\",\\"id\\":$RPC_ID,\\"method\\":\\"tools/call\\",\\"params\\":{\\"name\\":\\"submit_task\\",\\"arguments\\":{\\"task\\":\\"$task\\"}}}"
|
|
1029
|
+
curl -s -X POST http://127.0.0.1:${opts.port}/mcp \\
|
|
1030
|
+
-H "Content-Type: application/json" \\
|
|
1031
|
+
-H "Accept: application/json, text/event-stream"${token ? ' \\\n -H "$AUTH_HEADER"' : ''} \\
|
|
1032
|
+
-d "$BODY" \\
|
|
1033
|
+
| python3 -c "import json,sys; r=json.load(sys.stdin); content=r.get('result',{}).get('content',[]); [print(b.get('text','')) for b in content if b.get('type')=='text']" 2>/dev/null \\
|
|
1034
|
+
|| echo "Failed to connect. Is clawdcursor agent running?"
|
|
1035
|
+
echo ""
|
|
1036
|
+
done
|
|
1037
|
+
`;
|
|
1038
|
+
if (platform === 'win32') {
|
|
1039
|
+
// Write temp PS1 and open in new Windows Terminal / PowerShell window
|
|
1040
|
+
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
1041
|
+
const path = await Promise.resolve().then(() => __importStar(require('path')));
|
|
1042
|
+
// Private 0700 temp dir (mkdtemp, unguessable suffix) — this script is
|
|
1043
|
+
// EXECUTED, so a predictable tmpdir/clawdcursor-task-<time>.ps1 name was a
|
|
1044
|
+
// symlink/race window (CWE-377). The dir persists for the spawned terminal.
|
|
1045
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'clawdcursor-task-'));
|
|
1046
|
+
const tmpScript = path.join(tmpDir, 'task.ps1');
|
|
1047
|
+
fs.writeFileSync(tmpScript, scriptContent);
|
|
1048
|
+
spawnExec('powershell.exe', [
|
|
1049
|
+
'-Command', `Start-Process powershell -ArgumentList '-NoExit','-ExecutionPolicy','Bypass','-File','${tmpScript}'`
|
|
1050
|
+
], { detached: true, stdio: 'ignore' });
|
|
1051
|
+
}
|
|
1052
|
+
else if (platform === 'darwin') {
|
|
1053
|
+
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
1054
|
+
const path = await Promise.resolve().then(() => __importStar(require('path')));
|
|
1055
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'clawdcursor-task-'));
|
|
1056
|
+
const tmpScript = path.join(tmpDir, 'task.sh');
|
|
1057
|
+
fs.writeFileSync(tmpScript, scriptContent, { mode: 0o755 });
|
|
1058
|
+
spawnExec('open', ['-a', 'Terminal', tmpScript], { detached: true, stdio: 'ignore' });
|
|
1059
|
+
}
|
|
1060
|
+
else {
|
|
1061
|
+
// Linux fallback
|
|
1062
|
+
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
1063
|
+
const path = await Promise.resolve().then(() => __importStar(require('path')));
|
|
1064
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'clawdcursor-task-'));
|
|
1065
|
+
const tmpScript = path.join(tmpDir, 'task.sh');
|
|
1066
|
+
fs.writeFileSync(tmpScript, scriptContent, { mode: 0o755 });
|
|
1067
|
+
// $TERMINAL may be set with surrounding quotes on some distros — strip them before use.
|
|
1068
|
+
const termEnv = (process.env.TERMINAL || '').replace(/^["']|["']$/g, '').trim();
|
|
1069
|
+
const termExec = termEnv || 'x-terminal-emulator';
|
|
1070
|
+
spawnExec(termExec, ['-e', tmpScript], { detached: true, stdio: 'ignore' });
|
|
1071
|
+
}
|
|
1072
|
+
console.log(`${(0, format_1.e)('🐾', '>')} Task console opened in a new terminal window.`);
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
program
|
|
1076
|
+
.command('uninstall')
|
|
1077
|
+
.description('Remove all Clawd Cursor config, data, and skill registrations')
|
|
1078
|
+
.action(async () => {
|
|
1079
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1080
|
+
console.error(`\n${(0, format_1.e)('❌', '[ERR]')} clawdcursor uninstall requires an interactive terminal.\n`);
|
|
1081
|
+
process.exit(1);
|
|
1082
|
+
}
|
|
1083
|
+
const fs = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
1084
|
+
const path = await Promise.resolve().then(() => __importStar(require('path')));
|
|
1085
|
+
const os = await Promise.resolve().then(() => __importStar(require('os')));
|
|
1086
|
+
const readline = await Promise.resolve().then(() => __importStar(require('readline')));
|
|
1087
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1088
|
+
const answer = await new Promise((resolve) => {
|
|
1089
|
+
rl.question(`\n${(0, format_1.e)('⚠️', '[WARN]')} This will remove all Clawd Cursor config and data. Continue? (y/N) `, resolve);
|
|
1090
|
+
});
|
|
1091
|
+
rl.close();
|
|
1092
|
+
if (answer.toLowerCase() !== 'y') {
|
|
1093
|
+
console.log('Cancelled.');
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
console.log(`\n${(0, format_1.e)('🗑️', '[DEL]')} Uninstalling Clawd Cursor...\n`);
|
|
1097
|
+
const clawdRoot = (0, paths_1.getPackageRoot)();
|
|
1098
|
+
const homeDir = os.homedir();
|
|
1099
|
+
let removed = 0;
|
|
1100
|
+
const failed = [];
|
|
1101
|
+
// Resilient remove: never let one locked / permission-denied path abort the
|
|
1102
|
+
// whole uninstall. On Windows a file handle (a running daemon, the logger's
|
|
1103
|
+
// current log file, AV/indexer) can briefly hold a path open → EPERM/EBUSY.
|
|
1104
|
+
// rmSync's maxRetries rides out the transient lock; the try/catch downgrades
|
|
1105
|
+
// a hard failure to a warning + manual-cleanup hint and CONTINUES to the next
|
|
1106
|
+
// step (the old code threw here, crashing as an unhandledRejection and
|
|
1107
|
+
// leaving the install half-removed).
|
|
1108
|
+
const safeRemove = (target, label) => {
|
|
1109
|
+
try {
|
|
1110
|
+
if (!fs.existsSync(target))
|
|
1111
|
+
return;
|
|
1112
|
+
const stat = fs.lstatSync(target);
|
|
1113
|
+
if (stat.isSymbolicLink() || stat.isFile())
|
|
1114
|
+
fs.unlinkSync(target);
|
|
1115
|
+
else
|
|
1116
|
+
fs.rmSync(target, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 });
|
|
1117
|
+
console.log(` ${(0, format_1.e)('🗑️', '[DEL]')} Removed ${label}`);
|
|
1118
|
+
removed++;
|
|
1119
|
+
}
|
|
1120
|
+
catch (err) {
|
|
1121
|
+
const code = err.code || err.message;
|
|
1122
|
+
failed.push(`${target} (${code})`);
|
|
1123
|
+
console.log(` ${(0, format_1.e)('⚠️', '[WARN]')} Could not remove ${label} (${code}) — close any running clawdcursor, then delete manually: ${target}`);
|
|
1124
|
+
}
|
|
1125
|
+
};
|
|
1126
|
+
// 0. Stop any running server first (before deleting token)
|
|
1127
|
+
try {
|
|
1128
|
+
const tokenPath = path.join(homeDir, '.clawdcursor', 'token');
|
|
1129
|
+
if (fs.existsSync(tokenPath)) {
|
|
1130
|
+
const token = fs.readFileSync(tokenPath, 'utf-8').trim();
|
|
1131
|
+
const resp = await fetch('http://127.0.0.1:3847/stop', {
|
|
1132
|
+
method: 'POST',
|
|
1133
|
+
headers: { 'Authorization': `Bearer ${token}` },
|
|
1134
|
+
});
|
|
1135
|
+
if (resp.ok) {
|
|
1136
|
+
console.log(` ${(0, format_1.e)('🛑', '[STOP]')} Stopped running server`);
|
|
1137
|
+
// Give it a moment to shut down
|
|
1138
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
catch { /* server not running — that's fine */ }
|
|
1143
|
+
// 0b. Fallback: if /stop didn't work, try killing via pidfile.
|
|
1144
|
+
// readPidLoose() handles both the new JSON format and the legacy
|
|
1145
|
+
// bare-int format from pre-0.9.2 lockfiles, so this works whether
|
|
1146
|
+
// the running process is stale-old or freshly-installed.
|
|
1147
|
+
for (const mode of ['start', 'mcp', 'serve']) {
|
|
1148
|
+
try {
|
|
1149
|
+
if (!fs.existsSync((0, pidfile_1.pidFilePath)(mode)))
|
|
1150
|
+
continue;
|
|
1151
|
+
const pid = (0, pidfile_1.readPidLoose)(mode);
|
|
1152
|
+
if (pid !== null && pid !== process.pid) {
|
|
1153
|
+
try {
|
|
1154
|
+
process.kill(pid, 0); // check if alive
|
|
1155
|
+
process.kill(pid, 'SIGTERM');
|
|
1156
|
+
console.log(` ${(0, format_1.e)('🛑', '[STOP]')} Killed running ${mode} process (pid ${pid})`);
|
|
1157
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1158
|
+
}
|
|
1159
|
+
catch { /* process already dead */ }
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
catch { /* pidfile read failed — that's fine */ }
|
|
1163
|
+
}
|
|
1164
|
+
// 1. Remove config files in project root
|
|
1165
|
+
const configFiles = [
|
|
1166
|
+
path.join(clawdRoot, '.clawdcursor-config.json'),
|
|
1167
|
+
path.join(clawdRoot, '.clawdcursor-favorites.json'),
|
|
1168
|
+
path.join(clawdRoot, '.env'),
|
|
1169
|
+
];
|
|
1170
|
+
for (const f of configFiles)
|
|
1171
|
+
safeRemove(f, path.basename(f));
|
|
1172
|
+
// 2. Remove ~/.clawdcursor data directory (token, consent, task logs, pid)
|
|
1173
|
+
safeRemove(path.join(homeDir, '.clawdcursor'), path.join(homeDir, '.clawdcursor'));
|
|
1174
|
+
// Also remove legacy data directory
|
|
1175
|
+
safeRemove(path.join(homeDir, '.clawd-cursor'), `legacy ${path.join(homeDir, '.clawd-cursor')}`);
|
|
1176
|
+
// 3. Remove debug folder
|
|
1177
|
+
safeRemove(path.join(clawdRoot, 'debug'), 'debug/');
|
|
1178
|
+
// 4. Remove external skill registrations (OpenClaw, Codex, etc.)
|
|
1179
|
+
const skillPaths = [
|
|
1180
|
+
path.join(homeDir, '.openclaw', 'workspace', 'skills', 'clawdcursor'),
|
|
1181
|
+
path.join(homeDir, '.openclaw-dev', 'workspace', 'skills', 'clawdcursor'),
|
|
1182
|
+
path.join(homeDir, '.openclaw', 'skills', 'clawdcursor'),
|
|
1183
|
+
path.join(homeDir, '.codex', 'skills', 'clawdcursor'),
|
|
1184
|
+
];
|
|
1185
|
+
for (const sp of skillPaths)
|
|
1186
|
+
safeRemove(sp, `skill registration: ${sp}`);
|
|
1187
|
+
// 5. Remove MCP server entries from known config files
|
|
1188
|
+
const mcpConfigs = [
|
|
1189
|
+
// Claude Code
|
|
1190
|
+
path.join(homeDir, '.claude', 'settings.json'),
|
|
1191
|
+
path.join(homeDir, '.claude', 'settings.local.json'),
|
|
1192
|
+
// Cursor
|
|
1193
|
+
path.join(homeDir, '.cursor', 'mcp.json'),
|
|
1194
|
+
// Windsurf
|
|
1195
|
+
path.join(homeDir, '.windsurf', 'mcp.json'),
|
|
1196
|
+
path.join(homeDir, '.codeium', 'windsurf', 'mcp_config.json'),
|
|
1197
|
+
// VS Code / Continue
|
|
1198
|
+
path.join(homeDir, '.vscode', 'mcp.json'),
|
|
1199
|
+
];
|
|
1200
|
+
for (const configPath of mcpConfigs) {
|
|
1201
|
+
try {
|
|
1202
|
+
if (!fs.existsSync(configPath))
|
|
1203
|
+
continue;
|
|
1204
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
1205
|
+
const json = JSON.parse(raw);
|
|
1206
|
+
// Look for "clawdcursor" or "clawd-cursor" key in mcpServers
|
|
1207
|
+
const servers = json.mcpServers || json.servers || {};
|
|
1208
|
+
let found = false;
|
|
1209
|
+
for (const key of Object.keys(servers)) {
|
|
1210
|
+
if (key.toLowerCase().includes('clawdcursor') || key.toLowerCase().includes('clawd-cursor')) {
|
|
1211
|
+
delete servers[key];
|
|
1212
|
+
found = true;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
if (found) {
|
|
1216
|
+
fs.writeFileSync(configPath, JSON.stringify(json, null, 2) + '\n');
|
|
1217
|
+
console.log(` ${(0, format_1.e)('🗑️', '[DEL]')} Removed MCP entry from ${configPath}`);
|
|
1218
|
+
removed++;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
catch { /* skip unreadable configs */ }
|
|
1222
|
+
}
|
|
1223
|
+
// 6. Remove dist folder
|
|
1224
|
+
safeRemove(path.join(clawdRoot, 'dist'), 'dist/');
|
|
1225
|
+
// macOS native helper build output — regenerable, not tracked (#155).
|
|
1226
|
+
safeRemove(path.join(clawdRoot, 'native', '.build'), 'native/.build/');
|
|
1227
|
+
safeRemove(path.join(clawdRoot, 'native', 'ClawdCursor.app'), 'native/ClawdCursor.app/');
|
|
1228
|
+
// 7. Unlink global npm command
|
|
1229
|
+
try {
|
|
1230
|
+
const { execSync } = await Promise.resolve().then(() => __importStar(require('child_process')));
|
|
1231
|
+
execSync('npm unlink -g clawdcursor', { stdio: 'pipe', timeout: 15000 });
|
|
1232
|
+
console.log(` ${(0, format_1.e)('🗑️', '[DEL]')} Removed global clawdcursor command`);
|
|
1233
|
+
removed++;
|
|
1234
|
+
}
|
|
1235
|
+
catch { /* may not be linked globally */ }
|
|
1236
|
+
if (removed === 0 && failed.length === 0) {
|
|
1237
|
+
console.log(' Nothing to clean up.');
|
|
1238
|
+
}
|
|
1239
|
+
if (failed.length > 0) {
|
|
1240
|
+
console.log(`\n${(0, format_1.e)('⚠️', '[WARN]')} ${failed.length} item(s) could not be removed (a running process likely held a file open). Close any clawdcursor process, then delete these manually:`);
|
|
1241
|
+
for (const f of failed)
|
|
1242
|
+
console.log(` ${f}`);
|
|
1243
|
+
}
|
|
1244
|
+
console.log(`\n${(0, format_1.e)('🐾', '>')} ${failed.length > 0 ? 'Uninstall finished with warnings' : 'Fully uninstalled'}. To remove the source code, delete:`);
|
|
1245
|
+
console.log(` ${clawdRoot}`);
|
|
1246
|
+
// Closing the reinstall dead-end: this command just removed the global
|
|
1247
|
+
// `clawdcursor` shim, so `clawdcursor install` can't work afterwards — and
|
|
1248
|
+
// the natural next step shouldn't be a guess. Point the user at the package
|
|
1249
|
+
// manager (works everywhere) plus the turnkey one-liner for their OS.
|
|
1250
|
+
const reinstallOneLiner = process.platform === 'win32'
|
|
1251
|
+
? 'irm https://clawdcursor.com/install.ps1 | iex'
|
|
1252
|
+
: 'curl -fsSL https://clawdcursor.com/install.sh | bash';
|
|
1253
|
+
console.log(`\n${(0, format_1.e)('📦', '>')} To reinstall: npm i -g clawdcursor`);
|
|
1254
|
+
console.log(` or: ${reinstallOneLiner}\n`);
|
|
1255
|
+
});
|
|
1256
|
+
// ── Shared subsystem initialization (used by mcp + serve) ──
|
|
1257
|
+
async function createToolContext() {
|
|
1258
|
+
const { NativeDesktop } = await Promise.resolve().then(() => __importStar(require('../platform/native-desktop')));
|
|
1259
|
+
const { AccessibilityBridge } = await Promise.resolve().then(() => __importStar(require('../platform/accessibility')));
|
|
1260
|
+
const { CDPDriver } = await Promise.resolve().then(() => __importStar(require('../platform/cdp-driver')));
|
|
1261
|
+
const { DEFAULT_CONFIG } = await Promise.resolve().then(() => __importStar(require('../types')));
|
|
1262
|
+
const { DEFAULT_CDP_PORT } = await Promise.resolve().then(() => __importStar(require('../llm/browser-config')));
|
|
1263
|
+
const { getPlatform } = await Promise.resolve().then(() => __importStar(require('../platform')));
|
|
1264
|
+
const desktop = new NativeDesktop({ ...DEFAULT_CONFIG });
|
|
1265
|
+
const a11y = new AccessibilityBridge();
|
|
1266
|
+
const cdp = new CDPDriver(DEFAULT_CDP_PORT);
|
|
1267
|
+
// Session-scoped el_NN UIMap cache. Without this, compile_ui / find_* and
|
|
1268
|
+
// the {element_id, snapshot_id} ref path fail with "no UIMap holder on this
|
|
1269
|
+
// context" over stdio MCP — the v1.5.0 substrate was unreachable for editor-
|
|
1270
|
+
// hosted agents (only the `agent` daemon constructed one). Gauntlet F1.
|
|
1271
|
+
const { UIMapHolder } = await Promise.resolve().then(() => __importStar(require('../core/sense/ui-map-holder')));
|
|
1272
|
+
const uiMaps = new UIMapHolder();
|
|
1273
|
+
// Lazy adapter handle — Tranche 1A primitives run through this. Populated
|
|
1274
|
+
// in ensureInitialized so we share the same adapter the unified pipeline uses.
|
|
1275
|
+
let platform;
|
|
1276
|
+
let initialized = false;
|
|
1277
|
+
let initPromise = null;
|
|
1278
|
+
let mouseScaleFactor = 1;
|
|
1279
|
+
let screenshotScaleFactor = 1;
|
|
1280
|
+
const ensureInitialized = async () => {
|
|
1281
|
+
if (initialized)
|
|
1282
|
+
return;
|
|
1283
|
+
if (initPromise)
|
|
1284
|
+
return initPromise;
|
|
1285
|
+
initPromise = (async () => {
|
|
1286
|
+
await desktop.connect();
|
|
1287
|
+
platform = await getPlatform();
|
|
1288
|
+
screenshotScaleFactor = desktop.getScaleFactor();
|
|
1289
|
+
// mouseScaleFactor: image-space → mouse-driver coords.
|
|
1290
|
+
// • Windows / Linux-X11: nut-js drives in PHYSICAL pixels → use the
|
|
1291
|
+
// physical/image ratio (screenshotScaleFactor).
|
|
1292
|
+
// • macOS: nut-js drives in LOGICAL points (Cocoa/CGEvent space). On a
|
|
1293
|
+
// Retina panel physical≠logical, so the physical scale double-counts
|
|
1294
|
+
// the backing scale and every click lands ~2× off (#154). Map
|
|
1295
|
+
// image-space → LOGICAL instead, using NSScreen's logical width.
|
|
1296
|
+
// (Mirrors imageScale() in agent-loop/coord-scale.ts for System B.)
|
|
1297
|
+
if (process.platform === 'darwin') {
|
|
1298
|
+
try {
|
|
1299
|
+
const { LLM_TARGET_WIDTH } = await Promise.resolve().then(() => __importStar(require('../core/agent-loop/coord-scale')));
|
|
1300
|
+
const lsize = await platform.getScreenSize(); // NSScreen logical + physical dims
|
|
1301
|
+
const lw = lsize?.logicalWidth ?? 0;
|
|
1302
|
+
mouseScaleFactor = lw > LLM_TARGET_WIDTH ? lw / LLM_TARGET_WIDTH : 1;
|
|
1303
|
+
}
|
|
1304
|
+
catch {
|
|
1305
|
+
mouseScaleFactor = screenshotScaleFactor; // best-effort fallback
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
else {
|
|
1309
|
+
mouseScaleFactor = screenshotScaleFactor;
|
|
1310
|
+
}
|
|
1311
|
+
await a11y.warmup();
|
|
1312
|
+
initialized = true;
|
|
1313
|
+
console.log(`Subsystems initialized (mouseScale=${mouseScaleFactor}, screenshotScale=${screenshotScaleFactor})`);
|
|
1314
|
+
})();
|
|
1315
|
+
return initPromise;
|
|
1316
|
+
};
|
|
1317
|
+
return {
|
|
1318
|
+
desktop, a11y, cdp, uiMaps,
|
|
1319
|
+
get platform() { return platform; },
|
|
1320
|
+
getMouseScaleFactor: () => mouseScaleFactor,
|
|
1321
|
+
getScreenshotScaleFactor: () => screenshotScaleFactor,
|
|
1322
|
+
ensureInitialized,
|
|
1323
|
+
};
|
|
1324
|
+
}
|
|
1325
|
+
// ── MCP Mode (for Claude Code, Cursor, Windsurf, Zed, etc.) ──
|
|
1326
|
+
program
|
|
1327
|
+
.command('mcp')
|
|
1328
|
+
.description('Run as MCP tool server over stdio (for Claude Code, Cursor, Windsurf, Zed)')
|
|
1329
|
+
.option('--compact', 'Expose 6 compound tools instead of 97 granular ones (Anthropic Computer-Use style — recommended for most agents)')
|
|
1330
|
+
.option('--no-banner', 'Disable the on-screen "desktop control in progress" banner (also CLAWD_NO_BANNER=1)')
|
|
1331
|
+
.action(async (opts) => {
|
|
1332
|
+
// Single-instance guard (MCP servers can accumulate when editors restart them)
|
|
1333
|
+
const existingMcpPid = (0, pidfile_1.claimPidFile)('mcp');
|
|
1334
|
+
if (existingMcpPid !== null) {
|
|
1335
|
+
process.stderr.write(`[ERROR] clawdcursor mcp is already running (pid ${existingMcpPid}). Kill it first.\n`);
|
|
1336
|
+
process.exit(1);
|
|
1337
|
+
}
|
|
1338
|
+
// MCP mode: stdout is protocol, logs go to stderr
|
|
1339
|
+
const stderrWrite = (prefix, args) => process.stderr.write(`${prefix}${args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ')}\n`);
|
|
1340
|
+
console.log = (...args) => stderrWrite('', args);
|
|
1341
|
+
console.warn = (...args) => stderrWrite('[WARN] ', args);
|
|
1342
|
+
console.error = (...args) => stderrWrite('[ERROR] ', args);
|
|
1343
|
+
// Consent gate — must be accepted before MCP tools become active
|
|
1344
|
+
const { hasConsent } = await Promise.resolve().then(() => __importStar(require('./onboarding')));
|
|
1345
|
+
if (!hasConsent()) {
|
|
1346
|
+
process.stderr.write(`\nERROR: clawdcursor requires one-time consent before use.\n` +
|
|
1347
|
+
`This tool gives AI models full control of your desktop.\n\n` +
|
|
1348
|
+
`Run one of the following, then retry:\n` +
|
|
1349
|
+
` clawdcursor consent # interactive consent prompt\n` +
|
|
1350
|
+
` clawdcursor consent --accept # non-interactive (CI/scripts)\n\n`);
|
|
1351
|
+
process.exit(1);
|
|
1352
|
+
}
|
|
1353
|
+
const mode = opts.compact ? 'compact' : 'granular';
|
|
1354
|
+
console.log(`clawdcursor MCP mode starting... (${mode})`);
|
|
1355
|
+
const ctx = await createToolContext();
|
|
1356
|
+
// v0.9 PR7: server construction is shared with the HTTP transport in
|
|
1357
|
+
// src/mcp-server.ts — same registry, same safety gate, same param shape.
|
|
1358
|
+
const { createMcpServer, startMcpStdio } = await Promise.resolve().then(() => __importStar(require('./mcp-server')));
|
|
1359
|
+
const { server, toolCount } = await createMcpServer({ compact: opts.compact, ctx });
|
|
1360
|
+
await startMcpStdio(server);
|
|
1361
|
+
console.log(`clawdcursor MCP ready — ${toolCount} tools registered`);
|
|
1362
|
+
ctx.ensureInitialized().catch((err) => {
|
|
1363
|
+
console.error('Subsystem init failed:', err?.message);
|
|
1364
|
+
});
|
|
1365
|
+
// Release pidfile on exit so a fresh restart can claim it immediately.
|
|
1366
|
+
// Guard against double-fire (both 'end' and 'close' can emit on the
|
|
1367
|
+
// same stdin teardown). Defer process.exit via setImmediate so libuv
|
|
1368
|
+
// finishes its stream-close bookkeeping before the exit syscall —
|
|
1369
|
+
// calling process.exit() synchronously inside a stdin 'end' handler
|
|
1370
|
+
// causes SIGSEGV on Linux where libuv is still unwinding the read
|
|
1371
|
+
// handle.
|
|
1372
|
+
//
|
|
1373
|
+
// releasePidFile MUST stay synchronous (before the setImmediate). On
|
|
1374
|
+
// headless Linux CI the native subsystems (nut-js → libxdo, sharp's
|
|
1375
|
+
// libvips) can still segfault during process.exit's destructor
|
|
1376
|
+
// chain — so the only way to guarantee the lockfile gets cleaned up
|
|
1377
|
+
// is to unlink it BEFORE any deferred work runs. The orphan-teardown
|
|
1378
|
+
// test asserts lockfile-is-gone for exactly this reason.
|
|
1379
|
+
let mcpExiting = false;
|
|
1380
|
+
const releaseMcp = () => {
|
|
1381
|
+
if (mcpExiting)
|
|
1382
|
+
return;
|
|
1383
|
+
mcpExiting = true;
|
|
1384
|
+
(0, pidfile_1.releasePidFile)('mcp'); // sync — must run before any segfault
|
|
1385
|
+
setImmediate(() => process.exit(0)); // deferred — lets libuv unwind cleanly
|
|
1386
|
+
};
|
|
1387
|
+
process.on('SIGINT', releaseMcp);
|
|
1388
|
+
process.on('SIGTERM', releaseMcp);
|
|
1389
|
+
// On-screen control banner for EXTERNAL agents driving over stdio. The
|
|
1390
|
+
// mcp-server tool wrapper touches it on every consequential call; a
|
|
1391
|
+
// double-click is the human kill switch — exiting the server severs the
|
|
1392
|
+
// editor's desktop control (the host respawns a fresh server on demand).
|
|
1393
|
+
const { controlBanner } = await Promise.resolve().then(() => __importStar(require('../core/banner')));
|
|
1394
|
+
if (opts.banner === false)
|
|
1395
|
+
controlBanner.setEnabled(false);
|
|
1396
|
+
controlBanner.configure({
|
|
1397
|
+
onStopRequested: () => {
|
|
1398
|
+
process.stderr.write('\n[STOP] Control banner double-clicked — shutting down the MCP server.\n');
|
|
1399
|
+
releaseMcp();
|
|
1400
|
+
},
|
|
1401
|
+
});
|
|
1402
|
+
// Parent-death detection (orphan teardown).
|
|
1403
|
+
//
|
|
1404
|
+
// MCP stdio servers receive their JSON-RPC traffic over stdin. When the
|
|
1405
|
+
// host editor (Claude Code, Cursor, etc.) exits without first killing
|
|
1406
|
+
// its child, the child's stdin pipe closes immediately. Without an EOF
|
|
1407
|
+
// handler the orphaned server keeps running, holds its lockfile, and
|
|
1408
|
+
// blocks every subsequent reconnect with "already running, kill it
|
|
1409
|
+
// first". Listen for end / close / error and shut down cleanly so the
|
|
1410
|
+
// next host spawn finds a fresh slate.
|
|
1411
|
+
//
|
|
1412
|
+
// The MCP SDK's StdioServerTransport also installs handlers on stdin,
|
|
1413
|
+
// but its close path is asynchronous and host-dependent; treating EOF
|
|
1414
|
+
// as a hard exit signal here makes the orphan-reaping behavior
|
|
1415
|
+
// deterministic and the same on every platform.
|
|
1416
|
+
process.stdin.on('end', releaseMcp);
|
|
1417
|
+
process.stdin.on('close', releaseMcp);
|
|
1418
|
+
process.stdin.on('error', releaseMcp);
|
|
1419
|
+
});
|
|
1420
|
+
// ── `serve` deprecation alias (v0.9 PR7.4) ──
|
|
1421
|
+
//
|
|
1422
|
+
// `clawdcursor serve` was the legacy "tool server only" daemon. v0.9.0
|
|
1423
|
+
// folded it into `clawdcursor agent`, which now auto-detects LLM
|
|
1424
|
+
// availability — if no model is configured, the daemon boots into
|
|
1425
|
+
// tools-only mode automatically. Kept here as a soft-deprecation alias
|
|
1426
|
+
// for one release; removed in v0.10.
|
|
1427
|
+
program
|
|
1428
|
+
.command('serve')
|
|
1429
|
+
.description('[deprecated — use `clawdcursor agent`] Start the tool server only')
|
|
1430
|
+
.option('--port <port>', 'HTTP server port', '3847')
|
|
1431
|
+
.option('--skip-consent', 'Skip consent prompt (requires NODE_ENV=development)')
|
|
1432
|
+
.action(async (opts) => {
|
|
1433
|
+
console.warn(`${(0, format_1.e)('⚠', '[WARN]')} \`clawdcursor serve\` is deprecated; use \`clawdcursor agent\`. Removed in v0.10.`);
|
|
1434
|
+
await runAgentMode({ ...opts, noLlm: true });
|
|
1435
|
+
});
|
|
1436
|
+
program
|
|
1437
|
+
.command('report')
|
|
1438
|
+
.description('Send an error report to help improve clawdcursor. Shows a preview before sending.')
|
|
1439
|
+
.option('--log <path>', 'Path to a specific task log file')
|
|
1440
|
+
.option('--note <text>', 'Add a note describing what went wrong')
|
|
1441
|
+
.option('--save-only', 'Save report locally without sending')
|
|
1442
|
+
.action(async (opts) => {
|
|
1443
|
+
const { interactiveReport, buildReport, saveReportLocally, submitReport } = await Promise.resolve().then(() => __importStar(require('./report')));
|
|
1444
|
+
if (!process.stdin.isTTY) {
|
|
1445
|
+
// Non-interactive: build and submit directly
|
|
1446
|
+
const report = buildReport(opts.log, opts.note);
|
|
1447
|
+
if (opts.saveOnly) {
|
|
1448
|
+
const p = saveReportLocally(report);
|
|
1449
|
+
console.log(`Report saved: ${p}`);
|
|
1450
|
+
}
|
|
1451
|
+
else {
|
|
1452
|
+
const result = await submitReport(report);
|
|
1453
|
+
if (result.success) {
|
|
1454
|
+
console.log(`Report sent. ID: ${result.reportId}`);
|
|
1455
|
+
}
|
|
1456
|
+
else {
|
|
1457
|
+
const p = saveReportLocally(report);
|
|
1458
|
+
console.log(`Send failed: ${result.error}. Saved locally: ${p}`);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
return;
|
|
1462
|
+
}
|
|
1463
|
+
// Interactive mode
|
|
1464
|
+
await interactiveReport();
|
|
1465
|
+
});
|
|
1466
|
+
// ── Consent management ──────────────────────────────────────────────────────
|
|
1467
|
+
program
|
|
1468
|
+
.command('consent')
|
|
1469
|
+
.description('Manage desktop control consent (required before MCP/REST use)')
|
|
1470
|
+
.option('--accept', 'Accept consent non-interactively (CI/scripted environments)')
|
|
1471
|
+
.option('--revoke', 'Remove stored consent')
|
|
1472
|
+
.option('--status', 'Show current consent status')
|
|
1473
|
+
.action(async (opts) => {
|
|
1474
|
+
const { hasConsent, writeConsentFile, revokeConsent, runOnboarding } = await Promise.resolve().then(() => __importStar(require('./onboarding')));
|
|
1475
|
+
if (opts.status) {
|
|
1476
|
+
if (hasConsent()) {
|
|
1477
|
+
console.log(`${(0, format_1.e)('✅', '[OK]')} Consent: accepted — clawdcursor is authorized to control this desktop.`);
|
|
1478
|
+
}
|
|
1479
|
+
else {
|
|
1480
|
+
console.log(`${(0, format_1.e)('❌', '[ERR]')} Consent: not given — run \`clawdcursor consent\` to authorize.`);
|
|
1481
|
+
}
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
if (opts.revoke) {
|
|
1485
|
+
revokeConsent();
|
|
1486
|
+
console.log(' Consent revoked. clawdcursor will require re-authorization before next use.');
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
if (opts.accept) {
|
|
1490
|
+
writeConsentFile();
|
|
1491
|
+
console.log(' Consent accepted. clawdcursor can now control your desktop.');
|
|
1492
|
+
await autoRegisterSkill();
|
|
1493
|
+
printPostConsentNextSteps();
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
// Interactive flow
|
|
1497
|
+
const accepted = await runOnboarding('consent');
|
|
1498
|
+
if (accepted) {
|
|
1499
|
+
await autoRegisterSkill();
|
|
1500
|
+
printPostConsentNextSteps();
|
|
1501
|
+
}
|
|
1502
|
+
else {
|
|
1503
|
+
process.exit(1);
|
|
1504
|
+
}
|
|
1505
|
+
});
|
|
1506
|
+
/** Two-path "what to do next" panel shown after consent and after doctor success. */
|
|
1507
|
+
function printPostConsentNextSteps() {
|
|
1508
|
+
console.log('');
|
|
1509
|
+
console.log(` ${picocolors_1.default.bold('Two ways to use clawdcursor:')}`);
|
|
1510
|
+
console.log('');
|
|
1511
|
+
console.log(` ${picocolors_1.default.cyan('→ As an autonomous AI agent')} ${picocolors_1.default.gray('(clawdcursor brings the brain)')}`);
|
|
1512
|
+
console.log(` 1. ${picocolors_1.default.cyan('clawdcursor doctor')} Configure your AI provider + models`);
|
|
1513
|
+
console.log(` 2. ${picocolors_1.default.cyan('clawdcursor agent')} Start the daemon (HTTP + MCP on :3847)`);
|
|
1514
|
+
console.log('');
|
|
1515
|
+
console.log(` ${picocolors_1.default.cyan('→ As an MCP tool server')} ${picocolors_1.default.gray('(your editor brings the brain)')}`);
|
|
1516
|
+
console.log(` Register ${picocolors_1.default.cyan('clawdcursor mcp')} with Claude Code, Cursor, Windsurf, Zed, etc.`);
|
|
1517
|
+
console.log(` No daemon, no API key — your editor spawns clawdcursor on demand.`);
|
|
1518
|
+
console.log('');
|
|
1519
|
+
}
|
|
1520
|
+
/** Register the skill into every detected agent framework (best-effort). Runs on
|
|
1521
|
+
* consent so MCP-direct users get clawdcursor's skill without needing `doctor`
|
|
1522
|
+
* — the skill carries the "how to use me sustainably" knowledge the bare MCP
|
|
1523
|
+
* tools don't (fallback positioning, the el_NN UI map, the autonomous daemon). */
|
|
1524
|
+
async function autoRegisterSkill() {
|
|
1525
|
+
try {
|
|
1526
|
+
const { registerSkills } = await Promise.resolve().then(() => __importStar(require('./skill-register')));
|
|
1527
|
+
const { registered, results } = registerSkills();
|
|
1528
|
+
if (registered > 0) {
|
|
1529
|
+
const hosts = results
|
|
1530
|
+
.filter(r => r.ok && r.name.endsWith(' skill'))
|
|
1531
|
+
.map(r => r.name.replace(/ skill$/, ''))
|
|
1532
|
+
.join(', ');
|
|
1533
|
+
console.log(` ${(0, format_1.e)('🧩', '[OK]')} Registered clawdcursor as a skill in: ${hosts}`);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
catch { /* best-effort — never block consent */ }
|
|
1537
|
+
}
|
|
1538
|
+
program
|
|
1539
|
+
.command('register-skill')
|
|
1540
|
+
.description('(Re)register clawdcursor as a skill in your AI agents (Claude Code, OpenClaw, Codex, Cursor)')
|
|
1541
|
+
.action(async () => {
|
|
1542
|
+
const { registerSkills } = await Promise.resolve().then(() => __importStar(require('./skill-register')));
|
|
1543
|
+
const { registered, results } = registerSkills();
|
|
1544
|
+
for (const r of results) {
|
|
1545
|
+
console.log(` ${r.ok ? (0, format_1.e)('✅', '[OK]') : (0, format_1.e)('❌', '[ERR]')} ${r.name}: ${r.detail}`);
|
|
1546
|
+
}
|
|
1547
|
+
console.log('');
|
|
1548
|
+
if (registered > 0) {
|
|
1549
|
+
console.log(` ${(0, format_1.e)('🐾', '>')} Registered into ${registered} agent skill registr${registered === 1 ? 'y' : 'ies'}. Restart the agent to pick it up.`);
|
|
1550
|
+
}
|
|
1551
|
+
else {
|
|
1552
|
+
console.log(` ${(0, format_1.e)('ℹ️', '[i]')} No agent framework detected (Claude Code, OpenClaw, Codex, Cursor). clawdcursor still works via MCP.`);
|
|
1553
|
+
}
|
|
1554
|
+
});
|
|
1555
|
+
program.parse();
|
|
1556
|
+
//# sourceMappingURL=cli.js.map
|