@hasna/computer 0.1.2 → 0.1.4
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/dashboard/dist/assets/index-6UXnbsOB.js +40 -0
- package/dashboard/dist/assets/index-CwAxlYtY.css +1 -0
- package/dashboard/dist/index.html +13 -0
- package/dist/cli/index.js +18367 -17672
- package/dist/drivers/mac/headless.d.ts +50 -0
- package/dist/drivers/mac/headless.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +42 -0
- package/dist/server/index.js +26 -0
- package/helpers/record +0 -0
- package/helpers/record.swift +144 -0
- package/package.json +3 -1
|
@@ -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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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/dist/server/index.js
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
var __require = import.meta.require;
|
|
4
4
|
|
|
5
|
+
// src/server/index.ts
|
|
6
|
+
import { join as join6, dirname as dirname2 } from "path";
|
|
7
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
8
|
+
import { existsSync as existsSync4 } from "fs";
|
|
9
|
+
|
|
5
10
|
// src/agent/loop.ts
|
|
6
11
|
import { randomUUID } from "crypto";
|
|
7
12
|
import { mkdir } from "fs/promises";
|
|
@@ -18992,6 +18997,12 @@ function remapCoordinates(action, from, to) {
|
|
|
18992
18997
|
|
|
18993
18998
|
// src/server/index.ts
|
|
18994
18999
|
var PORT = parseInt(process.env.COMPUTER_PORT ?? "19450");
|
|
19000
|
+
var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
|
|
19001
|
+
var DASHBOARD_DIRS = [
|
|
19002
|
+
join6(__dirname2, "..", "..", "dashboard", "dist"),
|
|
19003
|
+
join6(__dirname2, "..", "dashboard", "dist")
|
|
19004
|
+
];
|
|
19005
|
+
var DASHBOARD_DIR = DASHBOARD_DIRS.find((d) => existsSync4(d));
|
|
18995
19006
|
var server = Bun.serve({
|
|
18996
19007
|
port: PORT,
|
|
18997
19008
|
async fetch(req) {
|
|
@@ -19059,6 +19070,21 @@ var server = Bun.serve({
|
|
|
19059
19070
|
if (method === "GET" && (path === "/health" || path === "/")) {
|
|
19060
19071
|
return Response.json({ status: "ok", name: "computer", version: "0.1.0", port: PORT }, { headers: corsHeaders });
|
|
19061
19072
|
}
|
|
19073
|
+
if (DASHBOARD_DIR && method === "GET" && (path.startsWith("/dashboard") || path === "/")) {
|
|
19074
|
+
if (path === "/" || path === "/dashboard" || path === "/dashboard/") {
|
|
19075
|
+
return new Response(Bun.file(join6(DASHBOARD_DIR, "index.html")), {
|
|
19076
|
+
headers: { "Content-Type": "text/html", "Access-Control-Allow-Origin": "*" }
|
|
19077
|
+
});
|
|
19078
|
+
}
|
|
19079
|
+
const filePath = path.replace("/dashboard", "");
|
|
19080
|
+
const fullPath = join6(DASHBOARD_DIR, filePath);
|
|
19081
|
+
if (existsSync4(fullPath)) {
|
|
19082
|
+
return new Response(Bun.file(fullPath), { headers: { "Access-Control-Allow-Origin": "*" } });
|
|
19083
|
+
}
|
|
19084
|
+
return new Response(Bun.file(join6(DASHBOARD_DIR, "index.html")), {
|
|
19085
|
+
headers: { "Content-Type": "text/html", "Access-Control-Allow-Origin": "*" }
|
|
19086
|
+
});
|
|
19087
|
+
}
|
|
19062
19088
|
return Response.json({ error: "Not found" }, { status: 404, headers: corsHeaders });
|
|
19063
19089
|
} catch (err) {
|
|
19064
19090
|
const message = err instanceof Error ? err.message : String(err);
|
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
|
+
"version": "0.1.4",
|
|
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",
|