@hasna/computer 0.1.3 → 0.1.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.
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Headless mode support for macOS computer use.
3
+ *
4
+ * Three strategies (tried in order):
5
+ * 1. Virtual display via macOS screen sharing (built-in VNC)
6
+ * 2. Lume VM integration (if installed — trycua/cua)
7
+ * 3. Fallback: error with instructions
8
+ *
9
+ * For most users, the practical headless approach is:
10
+ * - Enable macOS Screen Sharing (System Settings > General > Sharing)
11
+ * - The Mac creates a virtual display accessible via VNC
12
+ * - `screencapture` works against this display
13
+ * - Or use a headless Mac mini / Mac Studio with no monitor
14
+ */
15
+ export interface HeadlessConfig {
16
+ /** Strategy to use */
17
+ strategy: "vnc" | "lume" | "auto";
18
+ /** VNC host:port (default: localhost:5900) */
19
+ vncAddress?: string;
20
+ /** Lume VM name */
21
+ lumeVmName?: string;
22
+ }
23
+ /**
24
+ * Check if a display is available for screencapture.
25
+ * Returns false if no display is attached (headless server).
26
+ */
27
+ export declare function hasDisplay(): Promise<boolean>;
28
+ /**
29
+ * Check if macOS Screen Sharing (VNC) is enabled.
30
+ */
31
+ export declare function isScreenSharingEnabled(): Promise<boolean>;
32
+ /**
33
+ * Check if Lume CLI is installed (trycua/cua).
34
+ */
35
+ export declare function isLumeInstalled(): Promise<boolean>;
36
+ /**
37
+ * Start a Lume VM for headless computer use.
38
+ * Returns the VNC address to connect to.
39
+ */
40
+ export declare function startLumeVm(vmName?: string): Promise<string>;
41
+ /**
42
+ * Get headless mode status and instructions.
43
+ */
44
+ export declare function getHeadlessStatus(): Promise<{
45
+ display: boolean;
46
+ screenSharing: boolean;
47
+ lume: boolean;
48
+ recommendation: string;
49
+ }>;
50
+ //# sourceMappingURL=headless.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"headless.d.ts","sourceRoot":"","sources":["../../../src/drivers/mac/headless.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAIH,MAAM,WAAW,cAAc;IAC7B,sBAAsB;IACtB,QAAQ,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;IAClC,8CAA8C;IAC9C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mBAAmB;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC,CASnD;AAED;;GAEG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,OAAO,CAAC,CAO/D;AAED;;GAEG;AACH,wBAAsB,eAAe,IAAI,OAAO,CAAC,OAAO,CAAC,CAIxD;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,MAAM,GAAE,MAA4B,GAAG,OAAO,CAAC,MAAM,CAAC,CA8BvF;AAED;;GAEG;AACH,wBAAsB,iBAAiB,IAAI,OAAO,CAAC;IACjD,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,OAAO,CAAC;IACvB,IAAI,EAAE,OAAO,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC,CAuBD"}
package/dist/index.d.ts CHANGED
@@ -10,6 +10,8 @@ export { createMacDriver, MacDriver } from "./drivers/mac/index.js";
10
10
  export { captureScreenshot, getScreenSize, saveScreenshotToFile } from "./drivers/mac/screenshot.js";
11
11
  export { executeAction } from "./drivers/mac/input.js";
12
12
  export { createProvider, createAnthropicProvider, createOpenAIProvider } from "./providers/index.js";
13
+ export { hasDisplay, isScreenSharingEnabled, isLumeInstalled, getHeadlessStatus } from "./drivers/mac/headless.js";
14
+ export type { HeadlessConfig } from "./drivers/mac/headless.js";
13
15
  export { queryAccessibilityTree, summarizeAccessibilityTree } from "./drivers/mac/accessibility.js";
14
16
  export type { AXElement } from "./drivers/mac/accessibility.js";
15
17
  export { runPostSessionIntegrations, saveToRecordings, registerWithSessions, pushToLogs } from "./lib/integrations.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,YAAY,EACV,QAAQ,EACR,WAAW,EACX,KAAK,EACL,UAAU,EACV,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,SAAS,EACT,OAAO,EACP,UAAU,EACV,YAAY,GACb,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAG1C,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACrG,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAGvD,OAAO,EAAE,cAAc,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAGrG,OAAO,EAAE,sBAAsB,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AACpG,YAAY,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAGhE,OAAO,EAAE,0BAA0B,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAGvH,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC1F,YAAY,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAG5C,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAClE,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAG3D,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACxH,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGtD,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGpF,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpF,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnF,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAClG,YAAY,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAGhE,OAAO,EACL,KAAK,EACL,aAAa,EACb,aAAa,EACb,SAAS,EACT,UAAU,EACV,YAAY,EACZ,aAAa,EACb,aAAa,EACb,QAAQ,EACR,cAAc,EACd,gBAAgB,GACjB,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,YAAY,EACV,QAAQ,EACR,WAAW,EACX,KAAK,EACL,UAAU,EACV,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,aAAa,EACb,gBAAgB,EAChB,aAAa,EACb,SAAS,EACT,OAAO,EACP,UAAU,EACV,YAAY,GACb,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAG1C,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACrG,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAGvD,OAAO,EAAE,cAAc,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAGrG,OAAO,EAAE,UAAU,EAAE,sBAAsB,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACnH,YAAY,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAGhE,OAAO,EAAE,sBAAsB,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AACpG,YAAY,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAGhE,OAAO,EAAE,0BAA0B,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAGvH,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAC1F,YAAY,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAG5C,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAClE,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAG3D,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,cAAc,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACxH,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGtD,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGpF,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACpF,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnF,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAClG,YAAY,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAGhE,OAAO,EACL,KAAK,EACL,aAAa,EACb,aAAa,EACb,SAAS,EACT,UAAU,EACV,YAAY,EACZ,aAAa,EACb,aAAa,EACb,QAAQ,EACR,cAAc,EACd,gBAAgB,GACjB,MAAM,eAAe,CAAC"}
package/dist/index.js CHANGED
@@ -19074,6 +19074,44 @@ function remapCoordinates(action, from, to) {
19074
19074
  break;
19075
19075
  }
19076
19076
  }
19077
+ // src/drivers/mac/headless.ts
19078
+ async function hasDisplay() {
19079
+ const proc = Bun.spawn(["system_profiler", "SPDisplaysDataType", "-detailLevel", "mini"], { stdout: "pipe", stderr: "pipe" });
19080
+ await proc.exited;
19081
+ const stdout = await new Response(proc.stdout).text();
19082
+ return stdout.includes("Resolution:");
19083
+ }
19084
+ async function isScreenSharingEnabled() {
19085
+ const proc = Bun.spawn(["launchctl", "print", "system/com.apple.screensharing"], { stdout: "pipe", stderr: "pipe" });
19086
+ await proc.exited;
19087
+ return proc.exitCode === 0;
19088
+ }
19089
+ async function isLumeInstalled() {
19090
+ const proc = Bun.spawn(["which", "lume"], { stdout: "pipe", stderr: "pipe" });
19091
+ await proc.exited;
19092
+ return proc.exitCode === 0;
19093
+ }
19094
+ async function getHeadlessStatus() {
19095
+ const [display, screenSharing, lume] = await Promise.all([
19096
+ hasDisplay(),
19097
+ isScreenSharingEnabled(),
19098
+ isLumeInstalled()
19099
+ ]);
19100
+ let recommendation;
19101
+ if (display) {
19102
+ recommendation = "Display detected. Headless mode not needed \u2014 use normal mode.";
19103
+ } else if (screenSharing) {
19104
+ recommendation = "No display but Screen Sharing enabled. Connect via VNC to use computer use.";
19105
+ } else if (lume) {
19106
+ recommendation = "No display. Lume is installed \u2014 use --headless to spin up a macOS VM.";
19107
+ } else {
19108
+ recommendation = `No display detected. To use headless mode:
19109
+ ` + ` 1. Enable Screen Sharing: System Settings > General > Sharing > Screen Sharing
19110
+ ` + ` 2. Or install Lume: /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/lume/scripts/install.sh)"
19111
+ ` + " 3. Or connect a display/dummy HDMI adapter";
19112
+ }
19113
+ return { display, screenSharing, lume, recommendation };
19114
+ }
19077
19115
  // src/drivers/mac/accessibility.ts
19078
19116
  import { join as join6, dirname as dirname2 } from "path";
19079
19117
  import { existsSync as existsSync4 } from "fs";
@@ -19345,11 +19383,15 @@ export {
19345
19383
  listSessions,
19346
19384
  listPricing,
19347
19385
  listAgents,
19386
+ isScreenSharingEnabled,
19387
+ isLumeInstalled,
19348
19388
  heartbeat,
19389
+ hasDisplay,
19349
19390
  getStats,
19350
19391
  getSession,
19351
19392
  getScreenSize,
19352
19393
  getScaledSize,
19394
+ getHeadlessStatus,
19353
19395
  getDb,
19354
19396
  getConfigValue,
19355
19397
  getConfigPath,
package/helpers/record ADDED
Binary file
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env swift
2
+ // record.swift — Record mouse/keyboard events via CGEvent tap
3
+ // Usage: record [--duration <seconds>]
4
+ // Output: JSON array of events to stdout
5
+ // Stop: Ctrl+C or duration expires
6
+ //
7
+ // Requires: Accessibility permissions in System Settings
8
+
9
+ import CoreGraphics
10
+ import Foundation
11
+
12
+ struct RecordedEvent: Codable {
13
+ let type: String
14
+ let x: Double?
15
+ let y: Double?
16
+ let button: String?
17
+ let keyCode: Int?
18
+ let characters: String?
19
+ let timestamp: Double
20
+ }
21
+
22
+ var events: [RecordedEvent] = []
23
+ var startTime: Double = 0
24
+ var maxDuration: Double = 60 // default 60 seconds
25
+
26
+ // Parse args
27
+ var args = Array(CommandLine.arguments.dropFirst())
28
+ while !args.isEmpty {
29
+ let arg = args.removeFirst()
30
+ if arg == "--duration" && !args.isEmpty {
31
+ maxDuration = Double(args.removeFirst()) ?? 60
32
+ }
33
+ }
34
+
35
+ func eventType(_ type: CGEventType) -> String? {
36
+ switch type {
37
+ case .leftMouseDown: return "left_click"
38
+ case .rightMouseDown: return "right_click"
39
+ case .leftMouseUp: return "left_mouse_up"
40
+ case .rightMouseUp: return "right_mouse_up"
41
+ case .mouseMoved: return "mouse_move"
42
+ case .leftMouseDragged: return "left_drag"
43
+ case .scrollWheel: return "scroll"
44
+ case .keyDown: return "key_down"
45
+ case .keyUp: return "key_up"
46
+ default: return nil
47
+ }
48
+ }
49
+
50
+ func buttonName(_ type: CGEventType) -> String? {
51
+ switch type {
52
+ case .leftMouseDown, .leftMouseUp, .leftMouseDragged: return "left"
53
+ case .rightMouseDown, .rightMouseUp: return "right"
54
+ default: return nil
55
+ }
56
+ }
57
+
58
+ // Callback for CGEvent tap
59
+ let callback: CGEventTapCallBack = { proxy, type, event, refcon in
60
+ guard let typeName = eventType(type) else { return Unmanaged.passRetained(event) }
61
+
62
+ let now = CFAbsoluteTimeGetCurrent()
63
+ if startTime == 0 { startTime = now }
64
+ let elapsed = now - startTime
65
+
66
+ // Check duration limit
67
+ if elapsed > maxDuration {
68
+ CFRunLoopStop(CFRunLoopGetCurrent())
69
+ return Unmanaged.passRetained(event)
70
+ }
71
+
72
+ let location = event.location
73
+
74
+ let recorded = RecordedEvent(
75
+ type: typeName,
76
+ x: typeName.contains("mouse") || typeName.contains("click") || typeName.contains("drag") || typeName == "scroll" ? Double(location.x) : nil,
77
+ y: typeName.contains("mouse") || typeName.contains("click") || typeName.contains("drag") || typeName == "scroll" ? Double(location.y) : nil,
78
+ button: buttonName(type),
79
+ keyCode: typeName.contains("key") ? Int(event.getIntegerValueField(.keyboardEventKeycode)) : nil,
80
+ characters: nil,
81
+ timestamp: elapsed
82
+ )
83
+
84
+ // For mouse_move, only record every ~50ms to avoid flooding
85
+ if typeName == "mouse_move" {
86
+ if let last = events.last, last.type == "mouse_move" && (elapsed - last.timestamp) < 0.05 {
87
+ return Unmanaged.passRetained(event)
88
+ }
89
+ }
90
+
91
+ events.append(recorded)
92
+
93
+ // Print progress to stderr
94
+ fputs("\r\u{1B}[K Recording... \(events.count) events (\(String(format: "%.1f", elapsed))s / \(String(format: "%.0f", maxDuration))s)", stderr)
95
+
96
+ return Unmanaged.passRetained(event)
97
+ }
98
+
99
+ // Create event tap
100
+ var eventMask: CGEventMask = 0
101
+ eventMask |= (1 << CGEventType.leftMouseDown.rawValue)
102
+ eventMask |= (1 << CGEventType.leftMouseUp.rawValue)
103
+ eventMask |= (1 << CGEventType.rightMouseDown.rawValue)
104
+ eventMask |= (1 << CGEventType.rightMouseUp.rawValue)
105
+ eventMask |= (1 << CGEventType.mouseMoved.rawValue)
106
+ eventMask |= (1 << CGEventType.leftMouseDragged.rawValue)
107
+ eventMask |= (1 << CGEventType.scrollWheel.rawValue)
108
+ eventMask |= (1 << CGEventType.keyDown.rawValue)
109
+ eventMask |= (1 << CGEventType.keyUp.rawValue)
110
+
111
+ guard let tap = CGEvent.tapCreate(
112
+ tap: .cgSessionEventTap,
113
+ place: .headInsertEventTap,
114
+ options: .listenOnly, // Listen only, don't modify events
115
+ eventsOfInterest: eventMask,
116
+ callback: callback,
117
+ userInfo: nil
118
+ ) else {
119
+ fputs("Error: Failed to create event tap. Check Accessibility permissions.\n", stderr)
120
+ exit(1)
121
+ }
122
+
123
+ let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
124
+ CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
125
+ CGEvent.tapEnable(tap: tap, enable: true)
126
+
127
+ fputs("Recording mouse/keyboard events (max \(String(format: "%.0f", maxDuration))s, Ctrl+C to stop)...\n", stderr)
128
+
129
+ // Handle Ctrl+C
130
+ signal(SIGINT) { _ in
131
+ CFRunLoopStop(CFRunLoopGetCurrent())
132
+ }
133
+
134
+ // Run
135
+ CFRunLoopRun()
136
+
137
+ // Output JSON
138
+ fputs("\n", stderr)
139
+ let encoder = JSONEncoder()
140
+ encoder.outputFormatting = [.prettyPrinted]
141
+ if let data = try? encoder.encode(events) {
142
+ print(String(data: data, encoding: .utf8)!)
143
+ }
144
+ fputs("Recorded \(events.count) events\n", stderr)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/computer",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Open-source computer use for AI agents — control your Mac with Anthropic or OpenAI. CLI + MCP server + REST API + Dashboard.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,6 +22,8 @@
22
22
  "helpers/scroll.swift",
23
23
  "helpers/accessibility",
24
24
  "helpers/accessibility.swift",
25
+ "helpers/record",
26
+ "helpers/record.swift",
25
27
  "src/db/migrations",
26
28
  "dashboard/dist",
27
29
  "LICENSE",