@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.
Files changed (354) hide show
  1. package/CHANGELOG.md +2264 -0
  2. package/LICENSE +21 -0
  3. package/README.md +385 -0
  4. package/SECURITY.md +44 -0
  5. package/SKILL.md +503 -0
  6. package/dist/core/agent-loop/agent.d.ts +42 -0
  7. package/dist/core/agent-loop/agent.js +1023 -0
  8. package/dist/core/agent-loop/agent.js.map +1 -0
  9. package/dist/core/agent-loop/batch-tool.d.ts +25 -0
  10. package/dist/core/agent-loop/batch-tool.js +218 -0
  11. package/dist/core/agent-loop/batch-tool.js.map +1 -0
  12. package/dist/core/agent-loop/coord-scale.d.ts +72 -0
  13. package/dist/core/agent-loop/coord-scale.js +89 -0
  14. package/dist/core/agent-loop/coord-scale.js.map +1 -0
  15. package/dist/core/agent-loop/focus-guard.d.ts +24 -0
  16. package/dist/core/agent-loop/focus-guard.js +29 -0
  17. package/dist/core/agent-loop/focus-guard.js.map +1 -0
  18. package/dist/core/agent-loop/project-mcp.d.ts +97 -0
  19. package/dist/core/agent-loop/project-mcp.js +253 -0
  20. package/dist/core/agent-loop/project-mcp.js.map +1 -0
  21. package/dist/core/agent-loop/prompt.d.ts +45 -0
  22. package/dist/core/agent-loop/prompt.js +426 -0
  23. package/dist/core/agent-loop/prompt.js.map +1 -0
  24. package/dist/core/agent-loop/tool-meta.d.ts +93 -0
  25. package/dist/core/agent-loop/tool-meta.js +651 -0
  26. package/dist/core/agent-loop/tool-meta.js.map +1 -0
  27. package/dist/core/agent-loop/tools.d.ts +38 -0
  28. package/dist/core/agent-loop/tools.js +2134 -0
  29. package/dist/core/agent-loop/tools.js.map +1 -0
  30. package/dist/core/agent-loop/types.d.ts +170 -0
  31. package/dist/core/agent-loop/types.js +12 -0
  32. package/dist/core/agent-loop/types.js.map +1 -0
  33. package/dist/core/agent.d.ts +51 -0
  34. package/dist/core/agent.js +245 -0
  35. package/dist/core/agent.js.map +1 -0
  36. package/dist/core/app-categories.d.ts +67 -0
  37. package/dist/core/app-categories.js +108 -0
  38. package/dist/core/app-categories.js.map +1 -0
  39. package/dist/core/banner.d.ts +70 -0
  40. package/dist/core/banner.js +245 -0
  41. package/dist/core/banner.js.map +1 -0
  42. package/dist/core/classify/capability.d.ts +45 -0
  43. package/dist/core/classify/capability.js +78 -0
  44. package/dist/core/classify/capability.js.map +1 -0
  45. package/dist/core/decompose/llm-decomposer.d.ts +35 -0
  46. package/dist/core/decompose/llm-decomposer.js +156 -0
  47. package/dist/core/decompose/llm-decomposer.js.map +1 -0
  48. package/dist/core/decompose/parser.d.ts +27 -0
  49. package/dist/core/decompose/parser.js +101 -0
  50. package/dist/core/decompose/parser.js.map +1 -0
  51. package/dist/core/observability/correlation.d.ts +19 -0
  52. package/dist/core/observability/correlation.js +36 -0
  53. package/dist/core/observability/correlation.js.map +1 -0
  54. package/dist/core/observability/cost-meter.d.ts +51 -0
  55. package/dist/core/observability/cost-meter.js +134 -0
  56. package/dist/core/observability/cost-meter.js.map +1 -0
  57. package/dist/core/observability/logger.d.ts +61 -0
  58. package/dist/core/observability/logger.js +550 -0
  59. package/dist/core/observability/logger.js.map +1 -0
  60. package/dist/core/router/aliases.d.ts +50 -0
  61. package/dist/core/router/aliases.js +104 -0
  62. package/dist/core/router/aliases.js.map +1 -0
  63. package/dist/core/router/normalize.d.ts +41 -0
  64. package/dist/core/router/normalize.js +80 -0
  65. package/dist/core/router/normalize.js.map +1 -0
  66. package/dist/core/safety.d.ts +126 -0
  67. package/dist/core/safety.js +568 -0
  68. package/dist/core/safety.js.map +1 -0
  69. package/dist/core/sense/a11y-resolver.d.ts +73 -0
  70. package/dist/core/sense/a11y-resolver.js +76 -0
  71. package/dist/core/sense/a11y-resolver.js.map +1 -0
  72. package/dist/core/sense/fingerprint.d.ts +41 -0
  73. package/dist/core/sense/fingerprint.js +123 -0
  74. package/dist/core/sense/fingerprint.js.map +1 -0
  75. package/dist/core/sense/rank.d.ts +70 -0
  76. package/dist/core/sense/rank.js +192 -0
  77. package/dist/core/sense/rank.js.map +1 -0
  78. package/dist/core/sense/reactive-check.d.ts +40 -0
  79. package/dist/core/sense/reactive-check.js +48 -0
  80. package/dist/core/sense/reactive-check.js.map +1 -0
  81. package/dist/core/sense/snapshot.d.ts +19 -0
  82. package/dist/core/sense/snapshot.js +100 -0
  83. package/dist/core/sense/snapshot.js.map +1 -0
  84. package/dist/core/sense/types.d.ts +66 -0
  85. package/dist/core/sense/types.js +9 -0
  86. package/dist/core/sense/types.js.map +1 -0
  87. package/dist/core/sense/ui-map-anchors.d.ts +7 -0
  88. package/dist/core/sense/ui-map-anchors.js +24 -0
  89. package/dist/core/sense/ui-map-anchors.js.map +1 -0
  90. package/dist/core/sense/ui-map-elements.d.ts +5 -0
  91. package/dist/core/sense/ui-map-elements.js +33 -0
  92. package/dist/core/sense/ui-map-elements.js.map +1 -0
  93. package/dist/core/sense/ui-map-find.d.ts +56 -0
  94. package/dist/core/sense/ui-map-find.js +153 -0
  95. package/dist/core/sense/ui-map-find.js.map +1 -0
  96. package/dist/core/sense/ui-map-fuse.d.ts +4 -0
  97. package/dist/core/sense/ui-map-fuse.js +44 -0
  98. package/dist/core/sense/ui-map-fuse.js.map +1 -0
  99. package/dist/core/sense/ui-map-geom.d.ts +3 -0
  100. package/dist/core/sense/ui-map-geom.js +16 -0
  101. package/dist/core/sense/ui-map-geom.js.map +1 -0
  102. package/dist/core/sense/ui-map-holder.d.ts +58 -0
  103. package/dist/core/sense/ui-map-holder.js +87 -0
  104. package/dist/core/sense/ui-map-holder.js.map +1 -0
  105. package/dist/core/sense/ui-map-normalize.d.ts +19 -0
  106. package/dist/core/sense/ui-map-normalize.js +65 -0
  107. package/dist/core/sense/ui-map-normalize.js.map +1 -0
  108. package/dist/core/sense/ui-map-render.d.ts +4 -0
  109. package/dist/core/sense/ui-map-render.js +34 -0
  110. package/dist/core/sense/ui-map-render.js.map +1 -0
  111. package/dist/core/sense/ui-map-resolve.d.ts +41 -0
  112. package/dist/core/sense/ui-map-resolve.js +59 -0
  113. package/dist/core/sense/ui-map-resolve.js.map +1 -0
  114. package/dist/core/sense/ui-map-types.d.ts +66 -0
  115. package/dist/core/sense/ui-map-types.js +11 -0
  116. package/dist/core/sense/ui-map-types.js.map +1 -0
  117. package/dist/core/sense/ui-map.d.ts +29 -0
  118. package/dist/core/sense/ui-map.js +113 -0
  119. package/dist/core/sense/ui-map.js.map +1 -0
  120. package/dist/core/verify/assertions.d.ts +132 -0
  121. package/dist/core/verify/assertions.js +284 -0
  122. package/dist/core/verify/assertions.js.map +1 -0
  123. package/dist/index.d.ts +21 -0
  124. package/dist/index.js +24 -0
  125. package/dist/index.js.map +1 -0
  126. package/dist/llm/browser-config.d.ts +36 -0
  127. package/dist/llm/browser-config.js +83 -0
  128. package/dist/llm/browser-config.js.map +1 -0
  129. package/dist/llm/client.d.ts +268 -0
  130. package/dist/llm/client.js +1094 -0
  131. package/dist/llm/client.js.map +1 -0
  132. package/dist/llm/config.d.ts +79 -0
  133. package/dist/llm/config.js +375 -0
  134. package/dist/llm/config.js.map +1 -0
  135. package/dist/llm/credentials.d.ts +35 -0
  136. package/dist/llm/credentials.js +491 -0
  137. package/dist/llm/credentials.js.map +1 -0
  138. package/dist/llm/external-creds.d.ts +42 -0
  139. package/dist/llm/external-creds.js +169 -0
  140. package/dist/llm/external-creds.js.map +1 -0
  141. package/dist/llm/providers.d.ts +123 -0
  142. package/dist/llm/providers.js +717 -0
  143. package/dist/llm/providers.js.map +1 -0
  144. package/dist/paths.d.ts +31 -0
  145. package/dist/paths.js +147 -0
  146. package/dist/paths.js.map +1 -0
  147. package/dist/platform/accessibility.d.ts +139 -0
  148. package/dist/platform/accessibility.js +670 -0
  149. package/dist/platform/accessibility.js.map +1 -0
  150. package/dist/platform/cdp-driver.d.ts +318 -0
  151. package/dist/platform/cdp-driver.js +1179 -0
  152. package/dist/platform/cdp-driver.js.map +1 -0
  153. package/dist/platform/index.d.ts +11 -0
  154. package/dist/platform/index.js +69 -0
  155. package/dist/platform/index.js.map +1 -0
  156. package/dist/platform/keys.d.ts +17 -0
  157. package/dist/platform/keys.js +129 -0
  158. package/dist/platform/keys.js.map +1 -0
  159. package/dist/platform/launch-poll.d.ts +101 -0
  160. package/dist/platform/launch-poll.js +177 -0
  161. package/dist/platform/launch-poll.js.map +1 -0
  162. package/dist/platform/linux.d.ts +173 -0
  163. package/dist/platform/linux.js +1253 -0
  164. package/dist/platform/linux.js.map +1 -0
  165. package/dist/platform/macos.d.ts +136 -0
  166. package/dist/platform/macos.js +976 -0
  167. package/dist/platform/macos.js.map +1 -0
  168. package/dist/platform/native-desktop.d.ts +145 -0
  169. package/dist/platform/native-desktop.js +936 -0
  170. package/dist/platform/native-desktop.js.map +1 -0
  171. package/dist/platform/native-helper.d.ts +130 -0
  172. package/dist/platform/native-helper.js +592 -0
  173. package/dist/platform/native-helper.js.map +1 -0
  174. package/dist/platform/ocr-engine.d.ts +78 -0
  175. package/dist/platform/ocr-engine.js +363 -0
  176. package/dist/platform/ocr-engine.js.map +1 -0
  177. package/dist/platform/ps-runner.d.ts +28 -0
  178. package/dist/platform/ps-runner.js +228 -0
  179. package/dist/platform/ps-runner.js.map +1 -0
  180. package/dist/platform/types.d.ts +397 -0
  181. package/dist/platform/types.js +15 -0
  182. package/dist/platform/types.js.map +1 -0
  183. package/dist/platform/uri-handler.d.ts +75 -0
  184. package/dist/platform/uri-handler.js +273 -0
  185. package/dist/platform/uri-handler.js.map +1 -0
  186. package/dist/platform/wayland-backend.d.ts +53 -0
  187. package/dist/platform/wayland-backend.js +348 -0
  188. package/dist/platform/wayland-backend.js.map +1 -0
  189. package/dist/platform/windows.d.ts +232 -0
  190. package/dist/platform/windows.js +1210 -0
  191. package/dist/platform/windows.js.map +1 -0
  192. package/dist/postbuild.d.ts +10 -0
  193. package/dist/postbuild.js +98 -0
  194. package/dist/postbuild.js.map +1 -0
  195. package/dist/schema/snapshot.d.ts +33 -0
  196. package/dist/schema/snapshot.js +90 -0
  197. package/dist/schema/snapshot.js.map +1 -0
  198. package/dist/shortcuts.d.ts +30 -0
  199. package/dist/shortcuts.js +261 -0
  200. package/dist/shortcuts.js.map +1 -0
  201. package/dist/surface/cli.d.ts +7 -0
  202. package/dist/surface/cli.js +1556 -0
  203. package/dist/surface/cli.js.map +1 -0
  204. package/dist/surface/dashboard.d.ts +8 -0
  205. package/dist/surface/dashboard.js +1193 -0
  206. package/dist/surface/dashboard.js.map +1 -0
  207. package/dist/surface/doctor.d.ts +29 -0
  208. package/dist/surface/doctor.js +1514 -0
  209. package/dist/surface/doctor.js.map +1 -0
  210. package/dist/surface/format.d.ts +10 -0
  211. package/dist/surface/format.js +37 -0
  212. package/dist/surface/format.js.map +1 -0
  213. package/dist/surface/http-utility.d.ts +65 -0
  214. package/dist/surface/http-utility.js +336 -0
  215. package/dist/surface/http-utility.js.map +1 -0
  216. package/dist/surface/mcp-server.d.ts +91 -0
  217. package/dist/surface/mcp-server.js +280 -0
  218. package/dist/surface/mcp-server.js.map +1 -0
  219. package/dist/surface/onboarding.d.ts +15 -0
  220. package/dist/surface/onboarding.js +184 -0
  221. package/dist/surface/onboarding.js.map +1 -0
  222. package/dist/surface/pidfile.d.ts +79 -0
  223. package/dist/surface/pidfile.js +263 -0
  224. package/dist/surface/pidfile.js.map +1 -0
  225. package/dist/surface/readiness.d.ts +45 -0
  226. package/dist/surface/readiness.js +230 -0
  227. package/dist/surface/readiness.js.map +1 -0
  228. package/dist/surface/report.d.ts +68 -0
  229. package/dist/surface/report.js +341 -0
  230. package/dist/surface/report.js.map +1 -0
  231. package/dist/surface/skill-register.d.ts +14 -0
  232. package/dist/surface/skill-register.js +150 -0
  233. package/dist/surface/skill-register.js.map +1 -0
  234. package/dist/surface/version.d.ts +6 -0
  235. package/dist/surface/version.js +27 -0
  236. package/dist/surface/version.js.map +1 -0
  237. package/dist/tools/a11y.d.ts +8 -0
  238. package/dist/tools/a11y.js +545 -0
  239. package/dist/tools/a11y.js.map +1 -0
  240. package/dist/tools/a11y_depth.d.ts +19 -0
  241. package/dist/tools/a11y_depth.js +455 -0
  242. package/dist/tools/a11y_depth.js.map +1 -0
  243. package/dist/tools/agent.d.ts +15 -0
  244. package/dist/tools/agent.js +248 -0
  245. package/dist/tools/agent.js.map +1 -0
  246. package/dist/tools/batch.d.ts +46 -0
  247. package/dist/tools/batch.js +230 -0
  248. package/dist/tools/batch.js.map +1 -0
  249. package/dist/tools/cdp.d.ts +8 -0
  250. package/dist/tools/cdp.js +233 -0
  251. package/dist/tools/cdp.js.map +1 -0
  252. package/dist/tools/compact.d.ts +63 -0
  253. package/dist/tools/compact.js +418 -0
  254. package/dist/tools/compact.js.map +1 -0
  255. package/dist/tools/cost-class.d.ts +38 -0
  256. package/dist/tools/cost-class.js +117 -0
  257. package/dist/tools/cost-class.js.map +1 -0
  258. package/dist/tools/desktop.d.ts +9 -0
  259. package/dist/tools/desktop.js +346 -0
  260. package/dist/tools/desktop.js.map +1 -0
  261. package/dist/tools/electron_bridge.d.ts +41 -0
  262. package/dist/tools/electron_bridge.js +261 -0
  263. package/dist/tools/electron_bridge.js.map +1 -0
  264. package/dist/tools/extras.d.ts +22 -0
  265. package/dist/tools/extras.js +942 -0
  266. package/dist/tools/extras.js.map +1 -0
  267. package/dist/tools/favorites.d.ts +13 -0
  268. package/dist/tools/favorites.js +137 -0
  269. package/dist/tools/favorites.js.map +1 -0
  270. package/dist/tools/introspection.d.ts +13 -0
  271. package/dist/tools/introspection.js +55 -0
  272. package/dist/tools/introspection.js.map +1 -0
  273. package/dist/tools/ocr.d.ts +8 -0
  274. package/dist/tools/ocr.js +66 -0
  275. package/dist/tools/ocr.js.map +1 -0
  276. package/dist/tools/orchestration.d.ts +7 -0
  277. package/dist/tools/orchestration.js +377 -0
  278. package/dist/tools/orchestration.js.map +1 -0
  279. package/dist/tools/playbooks/extract-compose.d.ts +22 -0
  280. package/dist/tools/playbooks/extract-compose.js +85 -0
  281. package/dist/tools/playbooks/extract-compose.js.map +1 -0
  282. package/dist/tools/playbooks/find-replace.d.ts +11 -0
  283. package/dist/tools/playbooks/find-replace.js +56 -0
  284. package/dist/tools/playbooks/find-replace.js.map +1 -0
  285. package/dist/tools/playbooks/index.d.ts +63 -0
  286. package/dist/tools/playbooks/index.js +70 -0
  287. package/dist/tools/playbooks/index.js.map +1 -0
  288. package/dist/tools/playbooks/keys-blocklist.d.ts +24 -0
  289. package/dist/tools/playbooks/keys-blocklist.js +89 -0
  290. package/dist/tools/playbooks/keys-blocklist.js.map +1 -0
  291. package/dist/tools/registry.d.ts +40 -0
  292. package/dist/tools/registry.js +560 -0
  293. package/dist/tools/registry.js.map +1 -0
  294. package/dist/tools/safety-gate.d.ts +16 -0
  295. package/dist/tools/safety-gate.js +70 -0
  296. package/dist/tools/safety-gate.js.map +1 -0
  297. package/dist/tools/scheduler.d.ts +76 -0
  298. package/dist/tools/scheduler.js +413 -0
  299. package/dist/tools/scheduler.js.map +1 -0
  300. package/dist/tools/shortcuts.d.ts +13 -0
  301. package/dist/tools/shortcuts.js +205 -0
  302. package/dist/tools/shortcuts.js.map +1 -0
  303. package/dist/tools/smart.d.ts +15 -0
  304. package/dist/tools/smart.js +785 -0
  305. package/dist/tools/smart.js.map +1 -0
  306. package/dist/tools/types.d.ts +174 -0
  307. package/dist/tools/types.js +67 -0
  308. package/dist/tools/types.js.map +1 -0
  309. package/dist/tools/window-text.d.ts +15 -0
  310. package/dist/tools/window-text.js +39 -0
  311. package/dist/tools/window-text.js.map +1 -0
  312. package/dist/types.d.ts +122 -0
  313. package/dist/types.js +41 -0
  314. package/dist/types.js.map +1 -0
  315. package/native/Package.swift +38 -0
  316. package/native/README.md +113 -0
  317. package/native/Sources/ClawdCursorHelper/main.swift +602 -0
  318. package/native/Sources/ClawdCursorHost/main.swift +182 -0
  319. package/native/Sources/PermissionCheck/main.swift +53 -0
  320. package/native/Sources/ScreenshotHelper/main.swift +219 -0
  321. package/native/build.sh +139 -0
  322. package/native/entitlements.plist +12 -0
  323. package/package.json +115 -0
  324. package/scripts/banner.ps1 +112 -0
  325. package/scripts/coord-accuracy.ps1 +140 -0
  326. package/scripts/coord-uwp.ps1 +80 -0
  327. package/scripts/edge-glow.ps1 +180 -0
  328. package/scripts/find-element.ps1 +198 -0
  329. package/scripts/get-foreground-window.ps1 +71 -0
  330. package/scripts/get-screen-context.ps1 +183 -0
  331. package/scripts/get-windows.ps1 +66 -0
  332. package/scripts/install-panic-hotkey.ps1 +46 -0
  333. package/scripts/interact-element.ps1 +431 -0
  334. package/scripts/invoke-element.ps1 +314 -0
  335. package/scripts/linux/atspi-bridge.py +356 -0
  336. package/scripts/linux/ocr-recognize.py +154 -0
  337. package/scripts/mac/_window-picker.jxa +163 -0
  338. package/scripts/mac/find-element.jxa +0 -0
  339. package/scripts/mac/find-element.sh +161 -0
  340. package/scripts/mac/focus-window.jxa +284 -0
  341. package/scripts/mac/get-focused-element.jxa +102 -0
  342. package/scripts/mac/get-foreground-window.jxa +173 -0
  343. package/scripts/mac/get-screen-context.jxa +197 -0
  344. package/scripts/mac/get-ui-tree.sh +141 -0
  345. package/scripts/mac/get-windows.jxa +117 -0
  346. package/scripts/mac/interact-element.sh +235 -0
  347. package/scripts/mac/invoke-element.jxa +408 -0
  348. package/scripts/mac/ocr-recognize.swift +124 -0
  349. package/scripts/ocr-recognize.ps1 +102 -0
  350. package/scripts/postinstall-native.js +48 -0
  351. package/scripts/ps-bridge.ps1 +830 -0
  352. package/scripts/smoke-mcp.ps1 +119 -0
  353. package/scripts/sync-version.ts +178 -0
  354. 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