@mseep/clawdcursor 1.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2264 -0
- package/LICENSE +21 -0
- package/README.md +385 -0
- package/SECURITY.md +44 -0
- package/SKILL.md +503 -0
- package/dist/core/agent-loop/agent.d.ts +42 -0
- package/dist/core/agent-loop/agent.js +1023 -0
- package/dist/core/agent-loop/agent.js.map +1 -0
- package/dist/core/agent-loop/batch-tool.d.ts +25 -0
- package/dist/core/agent-loop/batch-tool.js +218 -0
- package/dist/core/agent-loop/batch-tool.js.map +1 -0
- package/dist/core/agent-loop/coord-scale.d.ts +72 -0
- package/dist/core/agent-loop/coord-scale.js +89 -0
- package/dist/core/agent-loop/coord-scale.js.map +1 -0
- package/dist/core/agent-loop/focus-guard.d.ts +24 -0
- package/dist/core/agent-loop/focus-guard.js +29 -0
- package/dist/core/agent-loop/focus-guard.js.map +1 -0
- package/dist/core/agent-loop/project-mcp.d.ts +97 -0
- package/dist/core/agent-loop/project-mcp.js +253 -0
- package/dist/core/agent-loop/project-mcp.js.map +1 -0
- package/dist/core/agent-loop/prompt.d.ts +45 -0
- package/dist/core/agent-loop/prompt.js +426 -0
- package/dist/core/agent-loop/prompt.js.map +1 -0
- package/dist/core/agent-loop/tool-meta.d.ts +93 -0
- package/dist/core/agent-loop/tool-meta.js +651 -0
- package/dist/core/agent-loop/tool-meta.js.map +1 -0
- package/dist/core/agent-loop/tools.d.ts +38 -0
- package/dist/core/agent-loop/tools.js +2134 -0
- package/dist/core/agent-loop/tools.js.map +1 -0
- package/dist/core/agent-loop/types.d.ts +170 -0
- package/dist/core/agent-loop/types.js +12 -0
- package/dist/core/agent-loop/types.js.map +1 -0
- package/dist/core/agent.d.ts +51 -0
- package/dist/core/agent.js +245 -0
- package/dist/core/agent.js.map +1 -0
- package/dist/core/app-categories.d.ts +67 -0
- package/dist/core/app-categories.js +108 -0
- package/dist/core/app-categories.js.map +1 -0
- package/dist/core/banner.d.ts +70 -0
- package/dist/core/banner.js +245 -0
- package/dist/core/banner.js.map +1 -0
- package/dist/core/classify/capability.d.ts +45 -0
- package/dist/core/classify/capability.js +78 -0
- package/dist/core/classify/capability.js.map +1 -0
- package/dist/core/decompose/llm-decomposer.d.ts +35 -0
- package/dist/core/decompose/llm-decomposer.js +156 -0
- package/dist/core/decompose/llm-decomposer.js.map +1 -0
- package/dist/core/decompose/parser.d.ts +27 -0
- package/dist/core/decompose/parser.js +101 -0
- package/dist/core/decompose/parser.js.map +1 -0
- package/dist/core/observability/correlation.d.ts +19 -0
- package/dist/core/observability/correlation.js +36 -0
- package/dist/core/observability/correlation.js.map +1 -0
- package/dist/core/observability/cost-meter.d.ts +51 -0
- package/dist/core/observability/cost-meter.js +134 -0
- package/dist/core/observability/cost-meter.js.map +1 -0
- package/dist/core/observability/logger.d.ts +61 -0
- package/dist/core/observability/logger.js +550 -0
- package/dist/core/observability/logger.js.map +1 -0
- package/dist/core/router/aliases.d.ts +50 -0
- package/dist/core/router/aliases.js +104 -0
- package/dist/core/router/aliases.js.map +1 -0
- package/dist/core/router/normalize.d.ts +41 -0
- package/dist/core/router/normalize.js +80 -0
- package/dist/core/router/normalize.js.map +1 -0
- package/dist/core/safety.d.ts +126 -0
- package/dist/core/safety.js +568 -0
- package/dist/core/safety.js.map +1 -0
- package/dist/core/sense/a11y-resolver.d.ts +73 -0
- package/dist/core/sense/a11y-resolver.js +76 -0
- package/dist/core/sense/a11y-resolver.js.map +1 -0
- package/dist/core/sense/fingerprint.d.ts +41 -0
- package/dist/core/sense/fingerprint.js +123 -0
- package/dist/core/sense/fingerprint.js.map +1 -0
- package/dist/core/sense/rank.d.ts +70 -0
- package/dist/core/sense/rank.js +192 -0
- package/dist/core/sense/rank.js.map +1 -0
- package/dist/core/sense/reactive-check.d.ts +40 -0
- package/dist/core/sense/reactive-check.js +48 -0
- package/dist/core/sense/reactive-check.js.map +1 -0
- package/dist/core/sense/snapshot.d.ts +19 -0
- package/dist/core/sense/snapshot.js +100 -0
- package/dist/core/sense/snapshot.js.map +1 -0
- package/dist/core/sense/types.d.ts +66 -0
- package/dist/core/sense/types.js +9 -0
- package/dist/core/sense/types.js.map +1 -0
- package/dist/core/sense/ui-map-anchors.d.ts +7 -0
- package/dist/core/sense/ui-map-anchors.js +24 -0
- package/dist/core/sense/ui-map-anchors.js.map +1 -0
- package/dist/core/sense/ui-map-elements.d.ts +5 -0
- package/dist/core/sense/ui-map-elements.js +33 -0
- package/dist/core/sense/ui-map-elements.js.map +1 -0
- package/dist/core/sense/ui-map-find.d.ts +56 -0
- package/dist/core/sense/ui-map-find.js +153 -0
- package/dist/core/sense/ui-map-find.js.map +1 -0
- package/dist/core/sense/ui-map-fuse.d.ts +4 -0
- package/dist/core/sense/ui-map-fuse.js +44 -0
- package/dist/core/sense/ui-map-fuse.js.map +1 -0
- package/dist/core/sense/ui-map-geom.d.ts +3 -0
- package/dist/core/sense/ui-map-geom.js +16 -0
- package/dist/core/sense/ui-map-geom.js.map +1 -0
- package/dist/core/sense/ui-map-holder.d.ts +58 -0
- package/dist/core/sense/ui-map-holder.js +87 -0
- package/dist/core/sense/ui-map-holder.js.map +1 -0
- package/dist/core/sense/ui-map-normalize.d.ts +19 -0
- package/dist/core/sense/ui-map-normalize.js +65 -0
- package/dist/core/sense/ui-map-normalize.js.map +1 -0
- package/dist/core/sense/ui-map-render.d.ts +4 -0
- package/dist/core/sense/ui-map-render.js +34 -0
- package/dist/core/sense/ui-map-render.js.map +1 -0
- package/dist/core/sense/ui-map-resolve.d.ts +41 -0
- package/dist/core/sense/ui-map-resolve.js +59 -0
- package/dist/core/sense/ui-map-resolve.js.map +1 -0
- package/dist/core/sense/ui-map-types.d.ts +66 -0
- package/dist/core/sense/ui-map-types.js +11 -0
- package/dist/core/sense/ui-map-types.js.map +1 -0
- package/dist/core/sense/ui-map.d.ts +29 -0
- package/dist/core/sense/ui-map.js +113 -0
- package/dist/core/sense/ui-map.js.map +1 -0
- package/dist/core/verify/assertions.d.ts +132 -0
- package/dist/core/verify/assertions.js +284 -0
- package/dist/core/verify/assertions.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/browser-config.d.ts +36 -0
- package/dist/llm/browser-config.js +83 -0
- package/dist/llm/browser-config.js.map +1 -0
- package/dist/llm/client.d.ts +268 -0
- package/dist/llm/client.js +1094 -0
- package/dist/llm/client.js.map +1 -0
- package/dist/llm/config.d.ts +79 -0
- package/dist/llm/config.js +375 -0
- package/dist/llm/config.js.map +1 -0
- package/dist/llm/credentials.d.ts +35 -0
- package/dist/llm/credentials.js +491 -0
- package/dist/llm/credentials.js.map +1 -0
- package/dist/llm/external-creds.d.ts +42 -0
- package/dist/llm/external-creds.js +169 -0
- package/dist/llm/external-creds.js.map +1 -0
- package/dist/llm/providers.d.ts +123 -0
- package/dist/llm/providers.js +717 -0
- package/dist/llm/providers.js.map +1 -0
- package/dist/paths.d.ts +31 -0
- package/dist/paths.js +147 -0
- package/dist/paths.js.map +1 -0
- package/dist/platform/accessibility.d.ts +139 -0
- package/dist/platform/accessibility.js +670 -0
- package/dist/platform/accessibility.js.map +1 -0
- package/dist/platform/cdp-driver.d.ts +318 -0
- package/dist/platform/cdp-driver.js +1179 -0
- package/dist/platform/cdp-driver.js.map +1 -0
- package/dist/platform/index.d.ts +11 -0
- package/dist/platform/index.js +69 -0
- package/dist/platform/index.js.map +1 -0
- package/dist/platform/keys.d.ts +17 -0
- package/dist/platform/keys.js +129 -0
- package/dist/platform/keys.js.map +1 -0
- package/dist/platform/launch-poll.d.ts +101 -0
- package/dist/platform/launch-poll.js +177 -0
- package/dist/platform/launch-poll.js.map +1 -0
- package/dist/platform/linux.d.ts +173 -0
- package/dist/platform/linux.js +1253 -0
- package/dist/platform/linux.js.map +1 -0
- package/dist/platform/macos.d.ts +136 -0
- package/dist/platform/macos.js +976 -0
- package/dist/platform/macos.js.map +1 -0
- package/dist/platform/native-desktop.d.ts +145 -0
- package/dist/platform/native-desktop.js +936 -0
- package/dist/platform/native-desktop.js.map +1 -0
- package/dist/platform/native-helper.d.ts +130 -0
- package/dist/platform/native-helper.js +592 -0
- package/dist/platform/native-helper.js.map +1 -0
- package/dist/platform/ocr-engine.d.ts +78 -0
- package/dist/platform/ocr-engine.js +363 -0
- package/dist/platform/ocr-engine.js.map +1 -0
- package/dist/platform/ps-runner.d.ts +28 -0
- package/dist/platform/ps-runner.js +228 -0
- package/dist/platform/ps-runner.js.map +1 -0
- package/dist/platform/types.d.ts +397 -0
- package/dist/platform/types.js +15 -0
- package/dist/platform/types.js.map +1 -0
- package/dist/platform/uri-handler.d.ts +75 -0
- package/dist/platform/uri-handler.js +273 -0
- package/dist/platform/uri-handler.js.map +1 -0
- package/dist/platform/wayland-backend.d.ts +53 -0
- package/dist/platform/wayland-backend.js +348 -0
- package/dist/platform/wayland-backend.js.map +1 -0
- package/dist/platform/windows.d.ts +232 -0
- package/dist/platform/windows.js +1210 -0
- package/dist/platform/windows.js.map +1 -0
- package/dist/postbuild.d.ts +10 -0
- package/dist/postbuild.js +98 -0
- package/dist/postbuild.js.map +1 -0
- package/dist/schema/snapshot.d.ts +33 -0
- package/dist/schema/snapshot.js +90 -0
- package/dist/schema/snapshot.js.map +1 -0
- package/dist/shortcuts.d.ts +30 -0
- package/dist/shortcuts.js +261 -0
- package/dist/shortcuts.js.map +1 -0
- package/dist/surface/cli.d.ts +7 -0
- package/dist/surface/cli.js +1556 -0
- package/dist/surface/cli.js.map +1 -0
- package/dist/surface/dashboard.d.ts +8 -0
- package/dist/surface/dashboard.js +1193 -0
- package/dist/surface/dashboard.js.map +1 -0
- package/dist/surface/doctor.d.ts +29 -0
- package/dist/surface/doctor.js +1514 -0
- package/dist/surface/doctor.js.map +1 -0
- package/dist/surface/format.d.ts +10 -0
- package/dist/surface/format.js +37 -0
- package/dist/surface/format.js.map +1 -0
- package/dist/surface/http-utility.d.ts +65 -0
- package/dist/surface/http-utility.js +336 -0
- package/dist/surface/http-utility.js.map +1 -0
- package/dist/surface/mcp-server.d.ts +91 -0
- package/dist/surface/mcp-server.js +280 -0
- package/dist/surface/mcp-server.js.map +1 -0
- package/dist/surface/onboarding.d.ts +15 -0
- package/dist/surface/onboarding.js +184 -0
- package/dist/surface/onboarding.js.map +1 -0
- package/dist/surface/pidfile.d.ts +79 -0
- package/dist/surface/pidfile.js +263 -0
- package/dist/surface/pidfile.js.map +1 -0
- package/dist/surface/readiness.d.ts +45 -0
- package/dist/surface/readiness.js +230 -0
- package/dist/surface/readiness.js.map +1 -0
- package/dist/surface/report.d.ts +68 -0
- package/dist/surface/report.js +341 -0
- package/dist/surface/report.js.map +1 -0
- package/dist/surface/skill-register.d.ts +14 -0
- package/dist/surface/skill-register.js +150 -0
- package/dist/surface/skill-register.js.map +1 -0
- package/dist/surface/version.d.ts +6 -0
- package/dist/surface/version.js +27 -0
- package/dist/surface/version.js.map +1 -0
- package/dist/tools/a11y.d.ts +8 -0
- package/dist/tools/a11y.js +545 -0
- package/dist/tools/a11y.js.map +1 -0
- package/dist/tools/a11y_depth.d.ts +19 -0
- package/dist/tools/a11y_depth.js +455 -0
- package/dist/tools/a11y_depth.js.map +1 -0
- package/dist/tools/agent.d.ts +15 -0
- package/dist/tools/agent.js +248 -0
- package/dist/tools/agent.js.map +1 -0
- package/dist/tools/batch.d.ts +46 -0
- package/dist/tools/batch.js +230 -0
- package/dist/tools/batch.js.map +1 -0
- package/dist/tools/cdp.d.ts +8 -0
- package/dist/tools/cdp.js +233 -0
- package/dist/tools/cdp.js.map +1 -0
- package/dist/tools/compact.d.ts +63 -0
- package/dist/tools/compact.js +418 -0
- package/dist/tools/compact.js.map +1 -0
- package/dist/tools/cost-class.d.ts +38 -0
- package/dist/tools/cost-class.js +117 -0
- package/dist/tools/cost-class.js.map +1 -0
- package/dist/tools/desktop.d.ts +9 -0
- package/dist/tools/desktop.js +346 -0
- package/dist/tools/desktop.js.map +1 -0
- package/dist/tools/electron_bridge.d.ts +41 -0
- package/dist/tools/electron_bridge.js +261 -0
- package/dist/tools/electron_bridge.js.map +1 -0
- package/dist/tools/extras.d.ts +22 -0
- package/dist/tools/extras.js +942 -0
- package/dist/tools/extras.js.map +1 -0
- package/dist/tools/favorites.d.ts +13 -0
- package/dist/tools/favorites.js +137 -0
- package/dist/tools/favorites.js.map +1 -0
- package/dist/tools/introspection.d.ts +13 -0
- package/dist/tools/introspection.js +55 -0
- package/dist/tools/introspection.js.map +1 -0
- package/dist/tools/ocr.d.ts +8 -0
- package/dist/tools/ocr.js +66 -0
- package/dist/tools/ocr.js.map +1 -0
- package/dist/tools/orchestration.d.ts +7 -0
- package/dist/tools/orchestration.js +377 -0
- package/dist/tools/orchestration.js.map +1 -0
- package/dist/tools/playbooks/extract-compose.d.ts +22 -0
- package/dist/tools/playbooks/extract-compose.js +85 -0
- package/dist/tools/playbooks/extract-compose.js.map +1 -0
- package/dist/tools/playbooks/find-replace.d.ts +11 -0
- package/dist/tools/playbooks/find-replace.js +56 -0
- package/dist/tools/playbooks/find-replace.js.map +1 -0
- package/dist/tools/playbooks/index.d.ts +63 -0
- package/dist/tools/playbooks/index.js +70 -0
- package/dist/tools/playbooks/index.js.map +1 -0
- package/dist/tools/playbooks/keys-blocklist.d.ts +24 -0
- package/dist/tools/playbooks/keys-blocklist.js +89 -0
- package/dist/tools/playbooks/keys-blocklist.js.map +1 -0
- package/dist/tools/registry.d.ts +40 -0
- package/dist/tools/registry.js +560 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/safety-gate.d.ts +16 -0
- package/dist/tools/safety-gate.js +70 -0
- package/dist/tools/safety-gate.js.map +1 -0
- package/dist/tools/scheduler.d.ts +76 -0
- package/dist/tools/scheduler.js +413 -0
- package/dist/tools/scheduler.js.map +1 -0
- package/dist/tools/shortcuts.d.ts +13 -0
- package/dist/tools/shortcuts.js +205 -0
- package/dist/tools/shortcuts.js.map +1 -0
- package/dist/tools/smart.d.ts +15 -0
- package/dist/tools/smart.js +785 -0
- package/dist/tools/smart.js.map +1 -0
- package/dist/tools/types.d.ts +174 -0
- package/dist/tools/types.js +67 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tools/window-text.d.ts +15 -0
- package/dist/tools/window-text.js +39 -0
- package/dist/tools/window-text.js.map +1 -0
- package/dist/types.d.ts +122 -0
- package/dist/types.js +41 -0
- package/dist/types.js.map +1 -0
- package/native/Package.swift +38 -0
- package/native/README.md +113 -0
- package/native/Sources/ClawdCursorHelper/main.swift +602 -0
- package/native/Sources/ClawdCursorHost/main.swift +182 -0
- package/native/Sources/PermissionCheck/main.swift +53 -0
- package/native/Sources/ScreenshotHelper/main.swift +219 -0
- package/native/build.sh +139 -0
- package/native/entitlements.plist +12 -0
- package/package.json +115 -0
- package/scripts/banner.ps1 +112 -0
- package/scripts/coord-accuracy.ps1 +140 -0
- package/scripts/coord-uwp.ps1 +80 -0
- package/scripts/edge-glow.ps1 +180 -0
- package/scripts/find-element.ps1 +198 -0
- package/scripts/get-foreground-window.ps1 +71 -0
- package/scripts/get-screen-context.ps1 +183 -0
- package/scripts/get-windows.ps1 +66 -0
- package/scripts/install-panic-hotkey.ps1 +46 -0
- package/scripts/interact-element.ps1 +431 -0
- package/scripts/invoke-element.ps1 +314 -0
- package/scripts/linux/atspi-bridge.py +356 -0
- package/scripts/linux/ocr-recognize.py +154 -0
- package/scripts/mac/_window-picker.jxa +163 -0
- package/scripts/mac/find-element.jxa +0 -0
- package/scripts/mac/find-element.sh +161 -0
- package/scripts/mac/focus-window.jxa +284 -0
- package/scripts/mac/get-focused-element.jxa +102 -0
- package/scripts/mac/get-foreground-window.jxa +173 -0
- package/scripts/mac/get-screen-context.jxa +197 -0
- package/scripts/mac/get-ui-tree.sh +141 -0
- package/scripts/mac/get-windows.jxa +117 -0
- package/scripts/mac/interact-element.sh +235 -0
- package/scripts/mac/invoke-element.jxa +408 -0
- package/scripts/mac/ocr-recognize.swift +124 -0
- package/scripts/ocr-recognize.ps1 +102 -0
- package/scripts/postinstall-native.js +48 -0
- package/scripts/ps-bridge.ps1 +830 -0
- package/scripts/smoke-mcp.ps1 +119 -0
- package/scripts/sync-version.ts +178 -0
- package/scripts/verify-install.js +81 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import AppKit
|
|
3
|
+
import ApplicationServices
|
|
4
|
+
import CoreGraphics
|
|
5
|
+
import Network
|
|
6
|
+
|
|
7
|
+
private let hostPort: UInt16 = {
|
|
8
|
+
if let env = ProcessInfo.processInfo.environment["CLAWDCURSOR_HOST_PORT"], let parsed = UInt16(env) {
|
|
9
|
+
return parsed
|
|
10
|
+
}
|
|
11
|
+
return 3848
|
|
12
|
+
}()
|
|
13
|
+
|
|
14
|
+
private func expectedToken() -> String? {
|
|
15
|
+
let home = FileManager.default.homeDirectoryForCurrentUser
|
|
16
|
+
let tokenPath = home.appendingPathComponent(".clawdcursor/host-token").path
|
|
17
|
+
return try? String(contentsOfFile: tokenPath, encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private func jsonResponse(status: Int, payload: Data) -> Data {
|
|
21
|
+
var response = "HTTP/1.1 \(status) \(status == 200 ? "OK" : "ERROR")\r\n"
|
|
22
|
+
response += "Content-Type: application/json\r\n"
|
|
23
|
+
response += "Content-Length: \(payload.count)\r\n"
|
|
24
|
+
response += "Connection: close\r\n\r\n"
|
|
25
|
+
var data = Data(response.utf8)
|
|
26
|
+
data.append(payload)
|
|
27
|
+
return data
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private func textResponse(status: Int, text: String) -> Data {
|
|
31
|
+
jsonResponse(status: status, payload: Data(text.utf8))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private func runBinary(_ binary: String, args: [String] = [], stdin: Data? = nil) -> (exitCode: Int32, stdout: Data, stderr: Data) {
|
|
35
|
+
let bundlePath = Bundle.main.bundlePath
|
|
36
|
+
let macOSDir = URL(fileURLWithPath: bundlePath).appendingPathComponent("Contents/MacOS")
|
|
37
|
+
let binaryPath = macOSDir.appendingPathComponent(binary).path
|
|
38
|
+
|
|
39
|
+
let process = Process()
|
|
40
|
+
let out = Pipe()
|
|
41
|
+
let err = Pipe()
|
|
42
|
+
let input = Pipe()
|
|
43
|
+
|
|
44
|
+
process.executableURL = URL(fileURLWithPath: binaryPath)
|
|
45
|
+
process.arguments = args
|
|
46
|
+
process.standardOutput = out
|
|
47
|
+
process.standardError = err
|
|
48
|
+
process.standardInput = input
|
|
49
|
+
|
|
50
|
+
do {
|
|
51
|
+
try process.run()
|
|
52
|
+
} catch {
|
|
53
|
+
return (1, Data(), Data("{\"error\":\"failed_to_launch_binary\"}".utf8))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if let stdin {
|
|
57
|
+
input.fileHandleForWriting.write(stdin)
|
|
58
|
+
}
|
|
59
|
+
try? input.fileHandleForWriting.close()
|
|
60
|
+
|
|
61
|
+
process.waitUntilExit()
|
|
62
|
+
let stdout = out.fileHandleForReading.readDataToEndOfFile()
|
|
63
|
+
let stderr = err.fileHandleForReading.readDataToEndOfFile()
|
|
64
|
+
return (process.terminationStatus, stdout, stderr)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private func handleRequest(raw: Data) -> Data {
|
|
68
|
+
guard let request = String(data: raw, encoding: .utf8) else {
|
|
69
|
+
return textResponse(status: 400, text: "{\"error\":\"invalid_utf8\"}")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let parts = request.components(separatedBy: "\r\n\r\n")
|
|
73
|
+
guard let head = parts.first else {
|
|
74
|
+
return textResponse(status: 400, text: "{\"error\":\"invalid_request\"}")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let lines = head.components(separatedBy: "\r\n")
|
|
78
|
+
guard let reqLine = lines.first else {
|
|
79
|
+
return textResponse(status: 400, text: "{\"error\":\"missing_request_line\"}")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let reqParts = reqLine.split(separator: " ")
|
|
83
|
+
guard reqParts.count >= 2 else {
|
|
84
|
+
return textResponse(status: 400, text: "{\"error\":\"bad_request_line\"}")
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let method = String(reqParts[0])
|
|
88
|
+
let path = String(reqParts[1])
|
|
89
|
+
let body = parts.dropFirst().joined(separator: "\r\n\r\n")
|
|
90
|
+
var headers: [String: String] = [:]
|
|
91
|
+
for line in lines.dropFirst() {
|
|
92
|
+
if let idx = line.firstIndex(of: ":") {
|
|
93
|
+
let name = String(line[..<idx]).trimmingCharacters(in: .whitespaces).lowercased()
|
|
94
|
+
let value = String(line[line.index(after: idx)...]).trimmingCharacters(in: .whitespaces)
|
|
95
|
+
headers[name] = value
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if method == "GET" && path == "/health" {
|
|
100
|
+
let payload = "{\"status\":\"ok\",\"service\":\"clawdcursor-host\",\"port\":\(hostPort)}"
|
|
101
|
+
return textResponse(status: 200, text: payload)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if method == "GET" && path == "/status" {
|
|
105
|
+
// Always delegate to the permission-check binary — it runs in the SAME
|
|
106
|
+
// app-bundle context (same TCC identity) and returns the canonical format
|
|
107
|
+
// including processPath + bundleId. This keeps doctor, CLI status, and
|
|
108
|
+
// readiness.ts consistent.
|
|
109
|
+
let result = runBinary("permission-check", args: [])
|
|
110
|
+
if result.exitCode == 0, !result.stdout.isEmpty,
|
|
111
|
+
let text = String(data: result.stdout, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
112
|
+
!text.isEmpty {
|
|
113
|
+
return textResponse(status: 200, text: text)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Fallback: permission-check binary missing or crashed.
|
|
117
|
+
// Check directly but match the SAME JSON schema so callers don't break.
|
|
118
|
+
let axOptions = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: kCFBooleanFalse] as CFDictionary
|
|
119
|
+
let axGranted = AXIsProcessTrustedWithOptions(axOptions)
|
|
120
|
+
let screenGranted = CGPreflightScreenCaptureAccess()
|
|
121
|
+
let bundleId = Bundle.main.bundleIdentifier ?? "unknown"
|
|
122
|
+
let processPath = ProcessInfo.processInfo.arguments.first ?? "unknown"
|
|
123
|
+
let payload = "{\"accessibility\":\(axGranted),\"screenRecording\":\(screenGranted),\"processPath\":\"\(processPath)\",\"bundleId\":\"\(bundleId)\"}"
|
|
124
|
+
return textResponse(status: 200, text: payload)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if method == "POST" && path == "/rpc" {
|
|
128
|
+
guard let token = expectedToken(), !token.isEmpty else {
|
|
129
|
+
return textResponse(status: 503, text: "{\"error\":\"host_token_missing\"}")
|
|
130
|
+
}
|
|
131
|
+
guard headers["x-clawdcursor-token"] == token else {
|
|
132
|
+
return textResponse(status: 401, text: "{\"error\":\"unauthorized\"}")
|
|
133
|
+
}
|
|
134
|
+
let result = runBinary("clawdcursor-helper", stdin: Data((body + "\n").utf8))
|
|
135
|
+
if result.exitCode == 0, !result.stdout.isEmpty {
|
|
136
|
+
let lines = String(data: result.stdout, encoding: .utf8)?.split(separator: "\n") ?? []
|
|
137
|
+
if let first = lines.first {
|
|
138
|
+
return textResponse(status: 200, text: String(first))
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
let stderr = String(data: result.stderr, encoding: .utf8) ?? "unknown error"
|
|
142
|
+
return textResponse(status: 500, text: "{\"error\":\"helper_failed\",\"message\":\"\(stderr.replacingOccurrences(of: "\"", with: "'"))\"}")
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return textResponse(status: 404, text: "{\"error\":\"not_found\"}")
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private var listenerRef: NWListener?
|
|
149
|
+
|
|
150
|
+
private func startServer() throws {
|
|
151
|
+
// SECURITY: Bind to localhost only — reject connections from other machines
|
|
152
|
+
let params = NWParameters.tcp
|
|
153
|
+
params.requiredLocalEndpoint = NWEndpoint.hostPort(host: .ipv4(.loopback), port: NWEndpoint.Port(rawValue: hostPort)!)
|
|
154
|
+
let listener = try NWListener(using: params)
|
|
155
|
+
listener.newConnectionHandler = { conn in
|
|
156
|
+
conn.start(queue: .global())
|
|
157
|
+
conn.receive(minimumIncompleteLength: 1, maximumLength: 1024 * 1024) { data, _, _, _ in
|
|
158
|
+
let response = handleRequest(raw: data ?? Data())
|
|
159
|
+
conn.send(content: response, completion: .contentProcessed { _ in
|
|
160
|
+
conn.cancel()
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
listener.start(queue: .global())
|
|
165
|
+
listenerRef = listener
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let app = NSApplication.shared
|
|
169
|
+
app.setActivationPolicy(.accessory)
|
|
170
|
+
|
|
171
|
+
if let bundleId = Bundle.main.bundleIdentifier {
|
|
172
|
+
NSLog("ClawdCursorHost starting (bundle: \(bundleId), port: \(hostPort))")
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
try startServer()
|
|
176
|
+
DispatchQueue.main.async {
|
|
177
|
+
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
|
178
|
+
statusItem.button?.title = "🐾"
|
|
179
|
+
statusItem.button?.toolTip = "ClawdCursor Host"
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
app.run()
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/// permission-check - Quick permission status checker for Node.js integration
|
|
2
|
+
/// Returns JSON: {"accessibility": bool, "screenRecording": bool}
|
|
3
|
+
/// Use AXTrustedCheckOptionPrompt: true to trigger the system prompt dialog
|
|
4
|
+
|
|
5
|
+
import Foundation
|
|
6
|
+
import ApplicationServices
|
|
7
|
+
import CoreGraphics
|
|
8
|
+
|
|
9
|
+
struct PermissionStatus: Codable {
|
|
10
|
+
let accessibility: Bool
|
|
11
|
+
let screenRecording: Bool
|
|
12
|
+
let processPath: String
|
|
13
|
+
let bundleId: String?
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Check Accessibility permission (does NOT prompt - use --prompt flag for that)
|
|
17
|
+
// Pattern from mediar-ai/MacosUseSDK - use kCFBooleanTrue/False explicitly
|
|
18
|
+
let shouldPrompt = CommandLine.arguments.contains("--prompt")
|
|
19
|
+
let promptValue: CFBoolean = shouldPrompt ? kCFBooleanTrue : kCFBooleanFalse
|
|
20
|
+
let axOptions = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: promptValue] as CFDictionary
|
|
21
|
+
let axGranted = AXIsProcessTrustedWithOptions(axOptions)
|
|
22
|
+
|
|
23
|
+
// Check Screen Recording permission
|
|
24
|
+
// CGPreflightScreenCaptureAccess() returns current state without prompting
|
|
25
|
+
// CGRequestScreenCaptureAccess() triggers the system prompt dialog
|
|
26
|
+
let shouldRequestScreen = CommandLine.arguments.contains("--request-screen-recording")
|
|
27
|
+
let screenGranted: Bool
|
|
28
|
+
if shouldRequestScreen {
|
|
29
|
+
screenGranted = CGRequestScreenCaptureAccess()
|
|
30
|
+
} else {
|
|
31
|
+
screenGranted = CGPreflightScreenCaptureAccess()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Get process info for debugging
|
|
35
|
+
let processPath = ProcessInfo.processInfo.arguments[0]
|
|
36
|
+
let bundleId = Bundle.main.bundleIdentifier
|
|
37
|
+
|
|
38
|
+
let status = PermissionStatus(
|
|
39
|
+
accessibility: axGranted,
|
|
40
|
+
screenRecording: screenGranted,
|
|
41
|
+
processPath: processPath,
|
|
42
|
+
bundleId: bundleId
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
let encoder = JSONEncoder()
|
|
46
|
+
encoder.outputFormatting = .sortedKeys
|
|
47
|
+
if let jsonData = try? encoder.encode(status),
|
|
48
|
+
let jsonString = String(data: jsonData, encoding: .utf8) {
|
|
49
|
+
print(jsonString)
|
|
50
|
+
} else {
|
|
51
|
+
fputs("{\"error\": \"Failed to encode status\"}\n", stderr)
|
|
52
|
+
exit(1)
|
|
53
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/// screenshot-helper - Isolated screen capture subprocess
|
|
2
|
+
/// Runs in separate process to:
|
|
3
|
+
/// 1. Isolate Screen Recording TCC permission
|
|
4
|
+
/// 2. Prevent ReplayKit CPU spin bug (19% idle CPU after capture)
|
|
5
|
+
///
|
|
6
|
+
/// Usage: screenshot-helper <windowId> <outputPath>
|
|
7
|
+
/// screenshot-helper --fullscreen <outputPath>
|
|
8
|
+
///
|
|
9
|
+
/// macOS Tahoe (26.0+) flash-free notes
|
|
10
|
+
/// ────────────────────────────────────
|
|
11
|
+
/// In macOS 26 Tahoe, the system added a "screen captured" white-flash
|
|
12
|
+
/// animation that fires whenever any process calls into the
|
|
13
|
+
/// `screencapture` coordinator daemon — including the deprecated
|
|
14
|
+
/// `CGWindowListCreateImage` path. The flash is a privacy/awareness
|
|
15
|
+
/// feature, not a bug, but it makes silent background captures
|
|
16
|
+
/// (the kind agent tools do dozens of times a session) visually
|
|
17
|
+
/// disruptive.
|
|
18
|
+
///
|
|
19
|
+
/// `ScreenCaptureKit.SCScreenshotManager.captureImage` uses a different
|
|
20
|
+
/// pipeline that Tahoe's flash hook does NOT intercept. On macOS 14+
|
|
21
|
+
/// (Sonoma and later) we prefer that path; on 12-13 (Monterey/Ventura)
|
|
22
|
+
/// we fall back to the legacy CG path which is silent on those OS
|
|
23
|
+
/// versions anyway.
|
|
24
|
+
|
|
25
|
+
import Foundation
|
|
26
|
+
import CoreGraphics
|
|
27
|
+
import ImageIO
|
|
28
|
+
import UniformTypeIdentifiers
|
|
29
|
+
import AppKit
|
|
30
|
+
#if canImport(ScreenCaptureKit)
|
|
31
|
+
import ScreenCaptureKit
|
|
32
|
+
#endif
|
|
33
|
+
|
|
34
|
+
// MARK: - macOS 14+ path (ScreenCaptureKit, flash-free on Tahoe)
|
|
35
|
+
|
|
36
|
+
#if canImport(ScreenCaptureKit)
|
|
37
|
+
@available(macOS 14.0, *)
|
|
38
|
+
func captureFullScreenSCK(outputPath: String) -> Bool {
|
|
39
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
40
|
+
var captured: CGImage? = nil
|
|
41
|
+
var captureError: Error? = nil
|
|
42
|
+
|
|
43
|
+
Task {
|
|
44
|
+
do {
|
|
45
|
+
let content = try await SCShareableContent.current
|
|
46
|
+
guard let display = content.displays.first else {
|
|
47
|
+
captureError = NSError(domain: "ScreenshotHelper", code: 10, userInfo: [NSLocalizedDescriptionKey: "no displays"])
|
|
48
|
+
semaphore.signal()
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
let filter = SCContentFilter(display: display, excludingApplications: [], exceptingWindows: [])
|
|
52
|
+
let config = SCStreamConfiguration()
|
|
53
|
+
// Capture at native resolution. SCK reports display size in
|
|
54
|
+
// points; multiply by the display's backingScaleFactor so
|
|
55
|
+
// the resulting PNG matches the pixel dimensions the legacy
|
|
56
|
+
// CG path produced. Without this, downstream code that
|
|
57
|
+
// reads .width/.height on a retina capture would see
|
|
58
|
+
// half-resolution images.
|
|
59
|
+
let scale = NSScreen.main?.backingScaleFactor ?? 2.0
|
|
60
|
+
config.width = Int(CGFloat(display.width) * scale)
|
|
61
|
+
config.height = Int(CGFloat(display.height) * scale)
|
|
62
|
+
config.scalesToFit = false
|
|
63
|
+
config.showsCursor = false
|
|
64
|
+
let image = try await SCScreenshotManager.captureImage(contentFilter: filter, configuration: config)
|
|
65
|
+
captured = image
|
|
66
|
+
} catch {
|
|
67
|
+
captureError = error
|
|
68
|
+
}
|
|
69
|
+
semaphore.signal()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_ = semaphore.wait(timeout: .now() + 15)
|
|
73
|
+
if let err = captureError {
|
|
74
|
+
fputs("error: ScreenCaptureKit failed: \(err.localizedDescription)\n", stderr)
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
guard let image = captured else {
|
|
78
|
+
fputs("error: ScreenCaptureKit returned no image\n", stderr)
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
return saveImage(image, to: outputPath)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@available(macOS 14.0, *)
|
|
85
|
+
func captureWindowSCK(windowId: CGWindowID, outputPath: String) -> Bool {
|
|
86
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
87
|
+
var captured: CGImage? = nil
|
|
88
|
+
var captureError: Error? = nil
|
|
89
|
+
|
|
90
|
+
Task {
|
|
91
|
+
do {
|
|
92
|
+
let content = try await SCShareableContent.current
|
|
93
|
+
// Find the SCWindow whose windowID matches the caller's CGWindowID.
|
|
94
|
+
// SCWindow.windowID is the same CGWindowID under the hood, so
|
|
95
|
+
// direct equality holds.
|
|
96
|
+
guard let scwindow = content.windows.first(where: { $0.windowID == windowId }) else {
|
|
97
|
+
captureError = NSError(domain: "ScreenshotHelper", code: 11, userInfo: [NSLocalizedDescriptionKey: "window \(windowId) not in shareable content"])
|
|
98
|
+
semaphore.signal()
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
let filter = SCContentFilter(desktopIndependentWindow: scwindow)
|
|
102
|
+
let config = SCStreamConfiguration()
|
|
103
|
+
let scale = NSScreen.main?.backingScaleFactor ?? 2.0
|
|
104
|
+
// Use the window's frame for sizing — SCK requires explicit
|
|
105
|
+
// pixel dimensions on the SCStreamConfiguration.
|
|
106
|
+
config.width = Int(scwindow.frame.width * scale)
|
|
107
|
+
config.height = Int(scwindow.frame.height * scale)
|
|
108
|
+
config.scalesToFit = false
|
|
109
|
+
config.showsCursor = false
|
|
110
|
+
let image = try await SCScreenshotManager.captureImage(contentFilter: filter, configuration: config)
|
|
111
|
+
captured = image
|
|
112
|
+
} catch {
|
|
113
|
+
captureError = error
|
|
114
|
+
}
|
|
115
|
+
semaphore.signal()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
_ = semaphore.wait(timeout: .now() + 15)
|
|
119
|
+
if let err = captureError {
|
|
120
|
+
fputs("error: ScreenCaptureKit window capture failed: \(err.localizedDescription)\n", stderr)
|
|
121
|
+
return false
|
|
122
|
+
}
|
|
123
|
+
guard let image = captured else {
|
|
124
|
+
fputs("error: ScreenCaptureKit returned no image\n", stderr)
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
return saveImage(image, to: outputPath)
|
|
128
|
+
}
|
|
129
|
+
#endif
|
|
130
|
+
|
|
131
|
+
// MARK: - macOS 12-13 path (CGWindowListCreateImage, silent on those versions)
|
|
132
|
+
|
|
133
|
+
func captureWindow(windowId: CGWindowID, outputPath: String) -> Bool {
|
|
134
|
+
#if canImport(ScreenCaptureKit)
|
|
135
|
+
if #available(macOS 14.0, *) {
|
|
136
|
+
return captureWindowSCK(windowId: windowId, outputPath: outputPath)
|
|
137
|
+
}
|
|
138
|
+
#endif
|
|
139
|
+
guard let image = CGWindowListCreateImage(
|
|
140
|
+
.null,
|
|
141
|
+
.optionIncludingWindow,
|
|
142
|
+
windowId,
|
|
143
|
+
[.boundsIgnoreFraming, .nominalResolution]
|
|
144
|
+
) else {
|
|
145
|
+
fputs("error: failed to capture window \(windowId)\n", stderr)
|
|
146
|
+
return false
|
|
147
|
+
}
|
|
148
|
+
return saveImage(image, to: outputPath)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
func captureFullScreen(outputPath: String) -> Bool {
|
|
152
|
+
#if canImport(ScreenCaptureKit)
|
|
153
|
+
if #available(macOS 14.0, *) {
|
|
154
|
+
return captureFullScreenSCK(outputPath: outputPath)
|
|
155
|
+
}
|
|
156
|
+
#endif
|
|
157
|
+
guard let image = CGWindowListCreateImage(
|
|
158
|
+
CGRect.infinite,
|
|
159
|
+
.optionOnScreenOnly,
|
|
160
|
+
kCGNullWindowID,
|
|
161
|
+
[.nominalResolution]
|
|
162
|
+
) else {
|
|
163
|
+
fputs("error: failed to capture screen\n", stderr)
|
|
164
|
+
return false
|
|
165
|
+
}
|
|
166
|
+
return saveImage(image, to: outputPath)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// MARK: - Shared encoding path
|
|
170
|
+
|
|
171
|
+
func saveImage(_ image: CGImage, to path: String) -> Bool {
|
|
172
|
+
let url = URL(fileURLWithPath: path)
|
|
173
|
+
guard let destination = CGImageDestinationCreateWithURL(
|
|
174
|
+
url as CFURL,
|
|
175
|
+
UTType.png.identifier as CFString,
|
|
176
|
+
1,
|
|
177
|
+
nil
|
|
178
|
+
) else {
|
|
179
|
+
fputs("error: failed to create image destination\n", stderr)
|
|
180
|
+
return false
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
CGImageDestinationAddImage(destination, image, nil)
|
|
184
|
+
|
|
185
|
+
if CGImageDestinationFinalize(destination) {
|
|
186
|
+
print("{\"success\": true, \"path\": \"\(path)\", \"width\": \(image.width), \"height\": \(image.height)}")
|
|
187
|
+
return true
|
|
188
|
+
} else {
|
|
189
|
+
fputs("error: failed to write image\n", stderr)
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// MARK: - Entry
|
|
195
|
+
|
|
196
|
+
// Check Screen Recording permission first
|
|
197
|
+
if !CGPreflightScreenCaptureAccess() {
|
|
198
|
+
fputs("{\"error\": \"screen_recording_denied\", \"message\": \"Grant Screen Recording permission in System Settings > Privacy & Security > Screen & System Audio Recording\"}\n", stderr)
|
|
199
|
+
exit(2)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Parse arguments
|
|
203
|
+
let args = CommandLine.arguments
|
|
204
|
+
guard args.count >= 3 else {
|
|
205
|
+
fputs("usage: screenshot-helper <windowId|--fullscreen> <outputPath>\n", stderr)
|
|
206
|
+
exit(1)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let success: Bool
|
|
210
|
+
if args[1] == "--fullscreen" {
|
|
211
|
+
success = captureFullScreen(outputPath: args[2])
|
|
212
|
+
} else if let windowId = UInt32(args[1]) {
|
|
213
|
+
success = captureWindow(windowId: CGWindowID(windowId), outputPath: args[2])
|
|
214
|
+
} else {
|
|
215
|
+
fputs("error: invalid window ID\n", stderr)
|
|
216
|
+
exit(1)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
exit(success ? 0 : 1)
|
package/native/build.sh
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Build script for ClawdCursor native helper (macOS only)
|
|
3
|
+
# Usage: ./build.sh [--adhoc]
|
|
4
|
+
# --adhoc is now the DEFAULT behavior (required for TCC on macOS 26+)
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
8
|
+
cd "$SCRIPT_DIR"
|
|
9
|
+
|
|
10
|
+
# Ensure this script is executable (no dependency on caller having set +x)
|
|
11
|
+
if [ ! -x "$0" ]; then
|
|
12
|
+
chmod +x "$0"
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
echo "🔨 Building ClawdCursor native helper..."
|
|
16
|
+
|
|
17
|
+
# Build all targets in release mode
|
|
18
|
+
if ! swift build -c release; then
|
|
19
|
+
echo "❌ Swift build failed. Ensure Xcode Command Line Tools are installed:"
|
|
20
|
+
echo " xcode-select --install"
|
|
21
|
+
exit 1
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# Get the build directory
|
|
25
|
+
BUILD_DIR=".build/release"
|
|
26
|
+
|
|
27
|
+
# Create the .app bundle structure
|
|
28
|
+
APP_DIR="ClawdCursor.app/Contents/MacOS"
|
|
29
|
+
mkdir -p "$APP_DIR"
|
|
30
|
+
|
|
31
|
+
# These four binaries are ALL required for correct operation
|
|
32
|
+
REQUIRED_BINARIES="ClawdCursorHost clawdcursor-helper screenshot-helper permission-check"
|
|
33
|
+
MISSING=0
|
|
34
|
+
|
|
35
|
+
for binary in $REQUIRED_BINARIES; do
|
|
36
|
+
if [ -f "$BUILD_DIR/$binary" ]; then
|
|
37
|
+
cp "$BUILD_DIR/$binary" "$APP_DIR/"
|
|
38
|
+
echo " ✓ Copied $binary"
|
|
39
|
+
else
|
|
40
|
+
echo " ❌ MISSING required binary: $binary"
|
|
41
|
+
MISSING=1
|
|
42
|
+
fi
|
|
43
|
+
done
|
|
44
|
+
|
|
45
|
+
if [ "$MISSING" -eq 1 ]; then
|
|
46
|
+
echo ""
|
|
47
|
+
echo "❌ Build incomplete — one or more required binaries are missing."
|
|
48
|
+
echo " Check the swift build output above for compilation errors."
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
echo "✅ Built ClawdCursor.app"
|
|
53
|
+
|
|
54
|
+
# Generate Contents/Info.plist — REQUIRED for a valid .app bundle. Without it
|
|
55
|
+
# codesign fails with "bundle format unrecognized, invalid, or unsuitable"
|
|
56
|
+
# (issue #150). CFBundleExecutable names the main binary inside Contents/MacOS/.
|
|
57
|
+
cat > "ClawdCursor.app/Contents/Info.plist" <<'PLIST'
|
|
58
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
59
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
60
|
+
<plist version="1.0">
|
|
61
|
+
<dict>
|
|
62
|
+
<key>CFBundleIdentifier</key>
|
|
63
|
+
<string>com.clawdcursor.helper</string>
|
|
64
|
+
<key>CFBundleName</key>
|
|
65
|
+
<string>ClawdCursor</string>
|
|
66
|
+
<key>CFBundleDisplayName</key>
|
|
67
|
+
<string>ClawdCursor</string>
|
|
68
|
+
<key>CFBundleExecutable</key>
|
|
69
|
+
<string>ClawdCursorHost</string>
|
|
70
|
+
<key>CFBundlePackageType</key>
|
|
71
|
+
<string>APPL</string>
|
|
72
|
+
<key>CFBundleInfoDictionaryVersion</key>
|
|
73
|
+
<string>6.0</string>
|
|
74
|
+
<key>CFBundleVersion</key>
|
|
75
|
+
<string>1.0.0</string>
|
|
76
|
+
<key>CFBundleShortVersionString</key>
|
|
77
|
+
<string>1.0.0</string>
|
|
78
|
+
<key>LSMinimumSystemVersion</key>
|
|
79
|
+
<string>13.0</string>
|
|
80
|
+
<key>LSUIElement</key>
|
|
81
|
+
<true/>
|
|
82
|
+
<key>NSAppleEventsUsageDescription</key>
|
|
83
|
+
<string>ClawdCursor controls other apps to automate desktop tasks you request.</string>
|
|
84
|
+
</dict>
|
|
85
|
+
</plist>
|
|
86
|
+
PLIST
|
|
87
|
+
echo " ✓ Wrote Contents/Info.plist"
|
|
88
|
+
|
|
89
|
+
# Code signing (REQUIRED for TCC on macOS 26+ / Tahoe)
|
|
90
|
+
# Without signing, the app won't appear in System Settings privacy panels
|
|
91
|
+
if [[ -n "$CLAWDCURSOR_SIGN_IDENTITY" ]]; then
|
|
92
|
+
echo "🔐 Signing with Developer ID: $CLAWDCURSOR_SIGN_IDENTITY"
|
|
93
|
+
codesign --sign "$CLAWDCURSOR_SIGN_IDENTITY" \
|
|
94
|
+
--options runtime \
|
|
95
|
+
--entitlements entitlements.plist \
|
|
96
|
+
--force \
|
|
97
|
+
--deep \
|
|
98
|
+
"ClawdCursor.app"
|
|
99
|
+
echo "✅ Signed with Developer ID"
|
|
100
|
+
else
|
|
101
|
+
# Ad-hoc sign by default — CRITICAL for TCC to recognize the app.
|
|
102
|
+
# NOTE: do NOT use --options runtime (hardened runtime) on the ad-hoc build.
|
|
103
|
+
# It makes macOS evaluate the helper's Screen-Recording TCC by its OWN
|
|
104
|
+
# identity (com.clawdcursor.helper) instead of letting it inherit the
|
|
105
|
+
# daemon host's grant — so a daemon-spawned screenshot-helper is denied and
|
|
106
|
+
# silently falls back to nut-js (#149). Hardened runtime is only needed on
|
|
107
|
+
# the Developer-ID/notarized branch above.
|
|
108
|
+
echo "🔐 Ad-hoc signing (required for TCC permissions)..."
|
|
109
|
+
if [ -f "entitlements.plist" ]; then
|
|
110
|
+
codesign --sign - \
|
|
111
|
+
--entitlements entitlements.plist \
|
|
112
|
+
--force \
|
|
113
|
+
--deep \
|
|
114
|
+
"ClawdCursor.app"
|
|
115
|
+
else
|
|
116
|
+
codesign --sign - \
|
|
117
|
+
--force \
|
|
118
|
+
--deep \
|
|
119
|
+
"ClawdCursor.app"
|
|
120
|
+
fi
|
|
121
|
+
echo "✅ Ad-hoc signed"
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# Verify signature
|
|
125
|
+
if codesign -v "ClawdCursor.app" 2>/dev/null; then
|
|
126
|
+
echo "✅ Signature verified"
|
|
127
|
+
else
|
|
128
|
+
echo "⚠️ Signature verification failed — TCC permissions may not work"
|
|
129
|
+
echo " On macOS 26+ (Tahoe), unsigned binaries don't appear in privacy settings"
|
|
130
|
+
fi
|
|
131
|
+
|
|
132
|
+
echo ""
|
|
133
|
+
echo "📦 Output: $SCRIPT_DIR/ClawdCursor.app"
|
|
134
|
+
echo ""
|
|
135
|
+
echo "To test permissions:"
|
|
136
|
+
echo " ./ClawdCursor.app/Contents/MacOS/permission-check"
|
|
137
|
+
echo ""
|
|
138
|
+
echo "To launch:"
|
|
139
|
+
echo " open ClawdCursor.app"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>com.apple.security.automation.apple-events</key>
|
|
6
|
+
<true/>
|
|
7
|
+
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
|
8
|
+
<false/>
|
|
9
|
+
<key>com.apple.security.cs.disable-library-validation</key>
|
|
10
|
+
<false/>
|
|
11
|
+
</dict>
|
|
12
|
+
</plist>
|