harnery 0.46.0 → 0.47.0

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 (187) hide show
  1. package/README.md +24 -0
  2. package/bin/README.md +10 -0
  3. package/bin/agent-coord.mjs +3 -0
  4. package/bin/agent-hook.mjs +3 -0
  5. package/bin/harn.mjs +3 -0
  6. package/bin/run.mjs +43 -0
  7. package/dist/commander.d.ts +18 -6
  8. package/dist/commander.d.ts.map +1 -1
  9. package/dist/commander.js +74 -2
  10. package/dist/commands/agents.d.ts +25 -0
  11. package/dist/commands/agents.d.ts.map +1 -1
  12. package/dist/commands/agents.js +70 -27
  13. package/dist/commands/autoupdate.d.ts +12 -0
  14. package/dist/commands/autoupdate.d.ts.map +1 -0
  15. package/dist/commands/autoupdate.js +190 -0
  16. package/dist/commands/computer.d.ts +45 -0
  17. package/dist/commands/computer.d.ts.map +1 -0
  18. package/dist/commands/computer.js +509 -0
  19. package/dist/commands/docs.js +3 -1
  20. package/dist/commands/fetch.js +11 -1
  21. package/dist/commands/grep.d.ts +72 -0
  22. package/dist/commands/grep.d.ts.map +1 -1
  23. package/dist/commands/grep.js +200 -47
  24. package/dist/commands/init.d.ts.map +1 -1
  25. package/dist/commands/init.js +15 -9
  26. package/dist/commands/rm.js +2 -2
  27. package/dist/commands/web.d.ts.map +1 -1
  28. package/dist/commands/web.js +2 -0
  29. package/dist/core/agents/canonical-emit.d.ts.map +1 -1
  30. package/dist/core/agents/canonical-emit.js +2 -3
  31. package/dist/core/agents/coord-bin.d.ts +3 -0
  32. package/dist/core/agents/coord-bin.d.ts.map +1 -1
  33. package/dist/core/agents/coord-bin.js +6 -0
  34. package/dist/core/agents/coord-client.d.ts.map +1 -1
  35. package/dist/core/agents/coord-client.js +6 -2
  36. package/dist/core/agents/render/prompt-context.d.ts +55 -7
  37. package/dist/core/agents/render/prompt-context.d.ts.map +1 -1
  38. package/dist/core/agents/render/prompt-context.js +122 -48
  39. package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
  40. package/dist/core/agents/rules/claim-conflict.js +36 -16
  41. package/dist/core/agents/rules/stop-hook.d.ts +6 -0
  42. package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
  43. package/dist/core/agents/rules/stop-hook.js +20 -1
  44. package/dist/core/agents/state/proc-start.d.ts +3 -1
  45. package/dist/core/agents/state/proc-start.d.ts.map +1 -1
  46. package/dist/core/agents/state/proc-start.js +26 -4
  47. package/dist/core/autoupdate/engine.d.ts +97 -0
  48. package/dist/core/autoupdate/engine.d.ts.map +1 -0
  49. package/dist/core/autoupdate/engine.js +548 -0
  50. package/dist/core/autoupdate/fingerprint.d.ts +16 -0
  51. package/dist/core/autoupdate/fingerprint.d.ts.map +1 -0
  52. package/dist/core/autoupdate/fingerprint.js +73 -0
  53. package/dist/core/autoupdate/git.d.ts +30 -0
  54. package/dist/core/autoupdate/git.d.ts.map +1 -0
  55. package/dist/core/autoupdate/git.js +82 -0
  56. package/dist/core/autoupdate/index.d.ts +6 -0
  57. package/dist/core/autoupdate/index.d.ts.map +1 -0
  58. package/dist/core/autoupdate/index.js +4 -0
  59. package/dist/core/autoupdate/state.d.ts +59 -0
  60. package/dist/core/autoupdate/state.d.ts.map +1 -0
  61. package/dist/core/autoupdate/state.js +214 -0
  62. package/dist/core/autoupdate/types.d.ts +113 -0
  63. package/dist/core/autoupdate/types.d.ts.map +1 -0
  64. package/dist/core/autoupdate/types.js +35 -0
  65. package/dist/core/config.d.ts +53 -1
  66. package/dist/core/config.d.ts.map +1 -1
  67. package/dist/core/config.js +114 -1
  68. package/dist/core/events/v3/bootstrap.js +1 -0
  69. package/dist/core/events/v3/runtime-build.d.ts.map +1 -1
  70. package/dist/core/events/v3/runtime-build.js +1 -0
  71. package/dist/core/hooks/adapter/wiring.d.ts +12 -2
  72. package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
  73. package/dist/core/hooks/adapter/wiring.js +38 -8
  74. package/dist/core/hooks/autoupdate-cue.d.ts +29 -0
  75. package/dist/core/hooks/autoupdate-cue.d.ts.map +1 -0
  76. package/dist/core/hooks/autoupdate-cue.js +53 -0
  77. package/dist/core/hooks/cli.js +52 -4
  78. package/dist/core/hooks/effects/index.js +4 -4
  79. package/dist/core/hooks/resolve/coord-root.js +1 -0
  80. package/dist/core/hooks/resolve/transcript.d.ts +12 -0
  81. package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
  82. package/dist/core/hooks/resolve/transcript.js +54 -0
  83. package/dist/core/semantic/contract.d.ts +3 -3
  84. package/dist/core/semantic/service.d.ts.map +1 -1
  85. package/dist/core/semantic/service.js +4 -2
  86. package/dist/core/storage/modes.d.ts +1 -1
  87. package/dist/core/storage/modes.d.ts.map +1 -1
  88. package/dist/core/storage/modes.js +5 -1
  89. package/dist/core/workflow/durable-record.d.ts +2 -0
  90. package/dist/core/workflow/durable-record.d.ts.map +1 -1
  91. package/dist/core/workflow/durable-record.js +13 -1
  92. package/dist/core/workflow/engine.js +4 -0
  93. package/dist/core/workflow/spawn-claude.d.ts +7 -0
  94. package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
  95. package/dist/core/workflow/spawn-claude.js +16 -4
  96. package/dist/core/workflow/specialists.d.ts.map +1 -1
  97. package/dist/core/workflow/specialists.js +1 -0
  98. package/dist/core/workflow/types.d.ts +10 -0
  99. package/dist/core/workflow/types.d.ts.map +1 -1
  100. package/dist/core/workflow/workspaces/leases.d.ts.map +1 -1
  101. package/dist/core/workflow/workspaces/leases.js +1 -9
  102. package/dist/lib/computer/cua.d.ts +50 -0
  103. package/dist/lib/computer/cua.d.ts.map +1 -0
  104. package/dist/lib/computer/cua.js +474 -0
  105. package/dist/lib/computer/host.d.ts +19 -0
  106. package/dist/lib/computer/host.d.ts.map +1 -0
  107. package/dist/lib/computer/host.js +72 -0
  108. package/dist/lib/computer/lease.d.ts +13 -0
  109. package/dist/lib/computer/lease.d.ts.map +1 -0
  110. package/dist/lib/computer/lease.js +269 -0
  111. package/dist/lib/computer/types.d.ts +163 -0
  112. package/dist/lib/computer/types.d.ts.map +1 -0
  113. package/dist/lib/computer/types.js +10 -0
  114. package/dist/lib/docs-metadata-v2.d.ts.map +1 -1
  115. package/dist/lib/docs-metadata-v2.js +6 -5
  116. package/dist/lib/exec.d.ts.map +1 -1
  117. package/dist/lib/exec.js +1 -0
  118. package/dist/lib/guarded-remove.d.ts.map +1 -1
  119. package/dist/lib/guarded-remove.js +5 -4
  120. package/dist/lib/headless/index.d.ts +33 -0
  121. package/dist/lib/headless/index.d.ts.map +1 -1
  122. package/dist/lib/headless/index.js +38 -2
  123. package/dist/lib/http/client.d.ts +2 -9
  124. package/dist/lib/http/client.d.ts.map +1 -1
  125. package/dist/lib/http/client.js +30 -9
  126. package/dist/lib/instructions/apply.d.ts +9 -0
  127. package/dist/lib/instructions/apply.d.ts.map +1 -1
  128. package/dist/lib/instructions/apply.js +92 -14
  129. package/dist/lib/instructions/git-hooks.d.ts.map +1 -1
  130. package/dist/lib/instructions/git-hooks.js +8 -4
  131. package/dist/lib/instructions/templates.d.ts +11 -0
  132. package/dist/lib/instructions/templates.d.ts.map +1 -1
  133. package/dist/lib/instructions/templates.js +81 -1
  134. package/package.json +5 -5
  135. package/schemas/config.schema.json +89 -1
  136. package/src/commander.ts +98 -8
  137. package/src/commands/agents.ts +100 -32
  138. package/src/commands/autoupdate.ts +250 -0
  139. package/src/commands/computer.ts +682 -0
  140. package/src/commands/docs.ts +3 -1
  141. package/src/commands/fetch.ts +11 -1
  142. package/src/commands/grep.ts +274 -59
  143. package/src/commands/init.ts +17 -7
  144. package/src/commands/rm.ts +2 -2
  145. package/src/commands/web.ts +2 -0
  146. package/src/core/agents/canonical-emit.ts +2 -3
  147. package/src/core/agents/coord-bin.ts +30 -0
  148. package/src/core/agents/coord-client.ts +6 -2
  149. package/src/core/agents/render/prompt-context.ts +152 -53
  150. package/src/core/agents/rules/claim-conflict.ts +50 -18
  151. package/src/core/agents/rules/stop-hook.ts +28 -1
  152. package/src/core/agents/state/proc-start.ts +29 -5
  153. package/src/core/autoupdate/engine.ts +691 -0
  154. package/src/core/autoupdate/fingerprint.ts +89 -0
  155. package/src/core/autoupdate/git.ts +120 -0
  156. package/src/core/autoupdate/index.ts +29 -0
  157. package/src/core/autoupdate/state.ts +269 -0
  158. package/src/core/autoupdate/types.ts +135 -0
  159. package/src/core/config.ts +142 -1
  160. package/src/core/events/v3/bootstrap.ts +1 -0
  161. package/src/core/events/v3/runtime-build.ts +1 -0
  162. package/src/core/hooks/adapter/wiring.ts +51 -6
  163. package/src/core/hooks/autoupdate-cue.ts +72 -0
  164. package/src/core/hooks/cli.ts +54 -1
  165. package/src/core/hooks/effects/index.ts +4 -4
  166. package/src/core/hooks/resolve/coord-root.ts +1 -0
  167. package/src/core/hooks/resolve/transcript.ts +61 -0
  168. package/src/core/semantic/service.ts +6 -2
  169. package/src/core/storage/modes.ts +7 -1
  170. package/src/core/workflow/durable-record.ts +15 -1
  171. package/src/core/workflow/engine.ts +3 -0
  172. package/src/core/workflow/spawn-claude.ts +16 -4
  173. package/src/core/workflow/specialists.ts +1 -0
  174. package/src/core/workflow/types.ts +10 -0
  175. package/src/core/workflow/workspaces/leases.ts +1 -9
  176. package/src/lib/computer/cua.ts +572 -0
  177. package/src/lib/computer/host.ts +87 -0
  178. package/src/lib/computer/lease.ts +299 -0
  179. package/src/lib/computer/types.ts +177 -0
  180. package/src/lib/docs-metadata-v2.ts +24 -5
  181. package/src/lib/exec.ts +1 -0
  182. package/src/lib/guarded-remove.ts +5 -3
  183. package/src/lib/headless/index.ts +66 -2
  184. package/src/lib/http/client.ts +33 -19
  185. package/src/lib/instructions/apply.ts +100 -12
  186. package/src/lib/instructions/git-hooks.ts +8 -4
  187. package/src/lib/instructions/templates.ts +95 -1
@@ -0,0 +1,474 @@
1
+ /**
2
+ * Cua Driver adapter for `harn computer` (ADR 0228).
3
+ *
4
+ * Everything specific to this driver lives here: the pinned release and its
5
+ * archive hashes, the archive layout, the driver's preference file, and the
6
+ * `call` / `list-tools` / `describe` / `status` / `serve` / `stop` arguments.
7
+ * The command layer and the desktop lease see only `DesktopDriver`.
8
+ *
9
+ * Under WSL the Windows build runs through interop: the `/mnt/<drive>/...`
10
+ * executable is spawned directly, environment variables cross through
11
+ * `WSLENV`, and file paths in tool arguments and results are translated with
12
+ * `wslpath`.
13
+ *
14
+ * Internal module: not exported from package.json.
15
+ */
16
+ import { spawn, spawnSync } from "node:child_process";
17
+ import { createHash, randomBytes } from "node:crypto";
18
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
19
+ import { posix, win32 } from "node:path";
20
+ import { windowsPathToWsl, wslPathToWindows } from "../browser/windows-chrome.js";
21
+ import { extractZip, listZip } from "../zip/index.js";
22
+ export const CUA_DRIVER_ID = "cua";
23
+ export const CUA_DRIVER_VERSION = "0.38.0";
24
+ const RELEASE_BASE = `https://github.com/trycua/cua/releases/download/cua-driver-rs-v${CUA_DRIVER_VERSION}`;
25
+ function pin(arch, sha256) {
26
+ const stem = `cua-driver-rs-${CUA_DRIVER_VERSION}-windows-${arch}`;
27
+ return {
28
+ version: CUA_DRIVER_VERSION,
29
+ fileName: `${stem}.zip`,
30
+ url: `${RELEASE_BASE}/${stem}.zip`,
31
+ sha256,
32
+ executableEntry: `${stem}/cua-driver.exe`,
33
+ };
34
+ }
35
+ /** Pinned archives keyed by `windows-<arch>`. Upgrading is a change here. */
36
+ export const CUA_PINS = Object.freeze({
37
+ "windows-x86_64": pin("x86_64", "997f20114ad3db227a854e1e5a886234f76405b155f4af70605d4b7ca4a436a1"),
38
+ "windows-arm64": pin("arm64", "c442872360cbb819e288b9f30daa7e3d2693d5945495d1400bcd90af5bd16402"),
39
+ });
40
+ /** Preferences merged into the driver's config before its first run. */
41
+ export const CUA_PREFERENCES = Object.freeze({
42
+ telemetry_enabled: false,
43
+ });
44
+ export const CUA_TELEMETRY_ENV = "CUA_DRIVER_RS_TELEMETRY_ENABLED";
45
+ export const INSTALL_MARKER = ".harnery-install.json";
46
+ export const CALL_TIMEOUT_MS = 120_000;
47
+ export const STATUS_TIMEOUT_MS = 15_000;
48
+ export const START_DEADLINE_MS = 10_000;
49
+ const START_POLL_MS = 250;
50
+ /** The release's executables are tens of MB each; allow headroom over the defaults. */
51
+ const ARCHIVE_LIMITS = {
52
+ archiveBytes: 256 * 1024 * 1024,
53
+ entryBytes: 128 * 1024 * 1024,
54
+ totalBytes: 512 * 1024 * 1024,
55
+ };
56
+ /** Arg keys whose string values are file paths in the caller's syntax. */
57
+ const PATH_ARG_KEYS = new Set([
58
+ "screenshot_out_file",
59
+ "output_dir",
60
+ "path",
61
+ "paths",
62
+ "image_path",
63
+ "file_path",
64
+ ]);
65
+ function pathApi(host) {
66
+ return host.kind === "windows" ? win32 : posix;
67
+ }
68
+ function requireSupported(host) {
69
+ if (host.kind === "unsupported" || !host.desktopStateDir)
70
+ throw new Error(`Computer use is not supported on this host: ${host.reason ?? "no shared desktop state directory"}`);
71
+ return host.desktopStateDir;
72
+ }
73
+ /** Map a process architecture onto a pinned Windows build. */
74
+ export function cuaArch(arch) {
75
+ if (arch === "x64" || arch === "x86_64" || arch === "amd64")
76
+ return "x86_64";
77
+ if (arch === "arm64" || arch === "aarch64")
78
+ return "arm64";
79
+ return undefined;
80
+ }
81
+ function normalize(text) {
82
+ return (text ?? "").replace(/\r\n?/g, "\n");
83
+ }
84
+ function parseJson(stdout) {
85
+ const trimmed = stdout.trim();
86
+ if (!trimmed)
87
+ return undefined;
88
+ try {
89
+ return JSON.parse(trimmed);
90
+ }
91
+ catch {
92
+ return undefined;
93
+ }
94
+ }
95
+ const defaultSpawnSync = (command, args, options) => {
96
+ const result = spawnSync(command, args, {
97
+ encoding: "utf8",
98
+ timeout: options.timeout,
99
+ env: options.env,
100
+ windowsHide: true,
101
+ maxBuffer: 64 * 1024 * 1024,
102
+ });
103
+ return {
104
+ status: result.status,
105
+ stdout: result.stdout ?? "",
106
+ stderr: result.stderr ?? "",
107
+ error: result.error,
108
+ };
109
+ };
110
+ const defaultSpawnDetached = (command, args, env) => {
111
+ spawn(command, args, { detached: true, stdio: "ignore", windowsHide: true, env }).unref();
112
+ };
113
+ /** Create a Cua Driver adapter. Every side effect is injectable for tests. */
114
+ export function createCuaDriver(deps = {}) {
115
+ const run = deps.spawnSync ?? defaultSpawnSync;
116
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
117
+ const now = deps.now ?? Date.now;
118
+ const baseEnv = () => deps.env ?? process.env;
119
+ let userProfileCache;
120
+ const windowsUserProfile = deps.windowsUserProfile ??
121
+ ((host) => {
122
+ if (host.kind === "windows")
123
+ return baseEnv().USERPROFILE?.trim() || undefined;
124
+ const result = run("cmd.exe", ["/c", "echo %USERPROFILE%"], {
125
+ timeout: STATUS_TIMEOUT_MS,
126
+ env: baseEnv(),
127
+ });
128
+ const value = normalize(result.stdout).trim();
129
+ return result.status === 0 && /^[A-Za-z]:\\/.test(value) ? value : undefined;
130
+ });
131
+ const wslpath = deps.wslpath ??
132
+ ((flag, path) => {
133
+ const result = run("wslpath", [flag, path], { timeout: STATUS_TIMEOUT_MS, env: baseEnv() });
134
+ const value = normalize(result.stdout).trim();
135
+ return result.status === 0 && value ? value : undefined;
136
+ });
137
+ function artifactFor() {
138
+ const arch = deps.arch ?? process.arch;
139
+ const mapped = cuaArch(arch);
140
+ if (!mapped)
141
+ throw new Error(`No pinned Cua Driver build for architecture ${arch}; supported: x64 (x86_64), arm64.`);
142
+ return (deps.pins ?? CUA_PINS)[`windows-${mapped}`];
143
+ }
144
+ function paths(host) {
145
+ const stateDir = requireSupported(host);
146
+ const p = pathApi(host);
147
+ const installDir = p.join(stateDir, "tools", "cua-driver", CUA_DRIVER_VERSION);
148
+ return { installDir, executable: p.join(installDir, "cua-driver.exe") };
149
+ }
150
+ function preferenceFile(host) {
151
+ requireSupported(host);
152
+ if (!userProfileCache) {
153
+ const profile = windowsUserProfile(host);
154
+ if (!profile)
155
+ throw new Error("Could not resolve the Windows user profile directory (%USERPROFILE%) for the driver's preference file.");
156
+ userProfileCache = profile;
157
+ }
158
+ if (host.kind === "windows")
159
+ return win32.join(userProfileCache, ".cua-driver", "config.json");
160
+ return posix.join(windowsPathToWsl(userProfileCache), ".cua-driver", "config.json");
161
+ }
162
+ function readMarker(installDir, p) {
163
+ try {
164
+ return JSON.parse(readFileSync(p.join(installDir, INSTALL_MARKER), "utf8"));
165
+ }
166
+ catch {
167
+ return undefined;
168
+ }
169
+ }
170
+ function installedExecutable(host) {
171
+ if (host.kind === "unsupported" || !host.desktopStateDir)
172
+ return undefined;
173
+ let artifact;
174
+ try {
175
+ artifact = artifactFor();
176
+ }
177
+ catch {
178
+ return undefined;
179
+ }
180
+ const { installDir, executable } = paths(host);
181
+ const marker = readMarker(installDir, pathApi(host));
182
+ if (!marker || marker.version !== artifact.version || marker.sha256 !== artifact.sha256)
183
+ return undefined;
184
+ return existsSync(executable) ? executable : undefined;
185
+ }
186
+ function driverEnv(host) {
187
+ const env = { ...baseEnv(), [CUA_TELEMETRY_ENV]: "0" };
188
+ if (host.kind === "wsl") {
189
+ const entries = (env.WSLENV ?? "").split(":").filter(Boolean);
190
+ if (!entries.some((entry) => entry.split("/")[0] === CUA_TELEMETRY_ENV))
191
+ entries.push(CUA_TELEMETRY_ENV);
192
+ env.WSLENV = entries.join(":");
193
+ }
194
+ return env;
195
+ }
196
+ function requireExecutable(host) {
197
+ requireSupported(host);
198
+ const exe = installedExecutable(host);
199
+ if (!exe)
200
+ throw new Error(`Cua Driver ${CUA_DRIVER_VERSION} is not installed for this desktop; install it with the computer install command first.`);
201
+ return exe;
202
+ }
203
+ function exec(host, args, timeout) {
204
+ const exe = requireExecutable(host);
205
+ const result = run(exe, args, { timeout, env: driverEnv(host) });
206
+ const stdout = normalize(result.stdout);
207
+ let stderr = normalize(result.stderr);
208
+ if (result.error) {
209
+ const code = result.error.code;
210
+ const note = code === "ETIMEDOUT"
211
+ ? `cua-driver ${args[0]} timed out after ${Math.round(timeout / 1000)}s`
212
+ : `cua-driver ${args[0]} failed to run: ${result.error.message}`;
213
+ stderr = stderr ? `${stderr.replace(/\n$/, "")}\n${note}\n` : `${note}\n`;
214
+ }
215
+ const out = {
216
+ exitCode: result.status ?? (result.error ? -1 : 1),
217
+ stdout,
218
+ stderr,
219
+ };
220
+ const json = parseJson(stdout);
221
+ if (json !== undefined)
222
+ out.json = json;
223
+ return out;
224
+ }
225
+ function daemonStatus(host) {
226
+ const result = exec(host, ["status"], STATUS_TIMEOUT_MS);
227
+ const text = result.stdout.trim() || result.stderr.trim();
228
+ const detail = text.split("\n").slice(0, 3).join("\n");
229
+ const running = result.exitCode === 0 && !/not running/i.test(result.stdout);
230
+ const status = { running, detail };
231
+ const pid = /pid:\s*(\d+)/i.exec(result.stdout);
232
+ if (running && pid)
233
+ status.pid = Number(pid[1]);
234
+ return status;
235
+ }
236
+ function planInstall(host) {
237
+ requireSupported(host);
238
+ const artifact = artifactFor();
239
+ const { installDir, executable } = paths(host);
240
+ return {
241
+ driver: CUA_DRIVER_ID,
242
+ version: CUA_DRIVER_VERSION,
243
+ host: host.kind,
244
+ artifact,
245
+ installDir,
246
+ executable,
247
+ alreadyInstalled: installedExecutable(host) === executable,
248
+ preferenceFile: preferenceFile(host),
249
+ preferences: { ...CUA_PREFERENCES },
250
+ };
251
+ }
252
+ function mergePreferences(file, preferences, p) {
253
+ let existing = {};
254
+ if (existsSync(file)) {
255
+ const raw = readFileSync(file, "utf8").replace(/^/, "");
256
+ if (raw.trim()) {
257
+ let parsed;
258
+ try {
259
+ parsed = JSON.parse(raw);
260
+ }
261
+ catch {
262
+ throw new Error(`Driver preference file ${file} is not valid JSON; fix or remove it, then retry.`);
263
+ }
264
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
265
+ throw new Error(`Driver preference file ${file} is not a JSON object.`);
266
+ existing = parsed;
267
+ }
268
+ }
269
+ const merged = { ...existing, ...preferences };
270
+ mkdirSync(p.dirname(file), { recursive: true });
271
+ const temp = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
272
+ writeFileSync(temp, `${JSON.stringify(merged, null, 2)}\n`);
273
+ renameSync(temp, file);
274
+ }
275
+ async function download(url) {
276
+ const fetchFn = deps.fetch ?? globalThis.fetch;
277
+ let response;
278
+ try {
279
+ response = await fetchFn(url, { redirect: "follow" });
280
+ }
281
+ catch (error) {
282
+ throw new Error(`Downloading ${url} failed: ${error instanceof Error ? error.message : String(error)}`);
283
+ }
284
+ if (!response.ok)
285
+ throw new Error(`Downloading ${url} failed: HTTP ${response.status}`);
286
+ return new Uint8Array(await response.arrayBuffer());
287
+ }
288
+ async function install(plan) {
289
+ if (plan.host === "unsupported")
290
+ throw new Error("Computer use is not supported on this host; nothing to install.");
291
+ const p = plan.host === "windows" ? win32 : posix;
292
+ const artifact = plan.artifact;
293
+ const markerOk = () => {
294
+ const marker = readMarker(plan.installDir, p);
295
+ return (!!marker &&
296
+ marker.version === artifact.version &&
297
+ marker.sha256 === artifact.sha256 &&
298
+ existsSync(plan.executable));
299
+ };
300
+ // Preferences go down before the binary can ever run, installed or not.
301
+ mergePreferences(plan.preferenceFile, plan.preferences, p);
302
+ if (markerOk())
303
+ return { ...plan, alreadyInstalled: true, installed: false };
304
+ const bytes = await download(artifact.url);
305
+ const actual = createHash("sha256").update(bytes).digest("hex");
306
+ if (actual !== artifact.sha256.toLowerCase())
307
+ throw new Error(`Refusing ${artifact.fileName}: SHA-256 ${actual} does not match the pinned ${artifact.sha256}. Nothing was installed.`);
308
+ const parent = p.dirname(plan.installDir);
309
+ mkdirSync(parent, { recursive: true });
310
+ const tag = `${process.pid}-${randomBytes(4).toString("hex")}`;
311
+ const archive = p.join(parent, `.download-${tag}.zip`);
312
+ const stage = p.join(parent, `.stage-${tag}`);
313
+ try {
314
+ writeFileSync(archive, bytes);
315
+ const topDir = artifact.executableEntry.split("/")[0];
316
+ const entries = listZip(archive, { limits: ARCHIVE_LIMITS });
317
+ const stray = entries.find((entry) => !entry.name.startsWith(`${topDir}/`));
318
+ if (stray)
319
+ throw new Error(`Unexpected entry outside ${topDir}/ in archive: ${stray.name}`);
320
+ if (!entries.some((entry) => entry.name === artifact.executableEntry))
321
+ throw new Error(`Archive lacks ${artifact.executableEntry}.`);
322
+ extractZip(archive, stage, { limits: ARCHIVE_LIMITS });
323
+ const extracted = p.join(stage, topDir);
324
+ if (!statSync(extracted).isDirectory())
325
+ throw new Error(`Archive lacks ${topDir}/.`);
326
+ const marker = {
327
+ version: artifact.version,
328
+ sha256: artifact.sha256,
329
+ installed_at: new Date(now()).toISOString(),
330
+ };
331
+ writeFileSync(p.join(extracted, INSTALL_MARKER), `${JSON.stringify(marker, null, 2)}\n`);
332
+ // A versioned dir without a matching marker is a broken earlier attempt.
333
+ if (existsSync(plan.installDir))
334
+ rmSync(plan.installDir, { recursive: true, force: true });
335
+ renameSync(extracted, plan.installDir);
336
+ }
337
+ finally {
338
+ rmSync(archive, { force: true });
339
+ rmSync(stage, { recursive: true, force: true });
340
+ }
341
+ return { ...plan, alreadyInstalled: false, installed: true };
342
+ }
343
+ async function startDaemon(host) {
344
+ const exe = requireExecutable(host);
345
+ const current = daemonStatus(host);
346
+ if (current.running)
347
+ return current;
348
+ const env = driverEnv(host);
349
+ if (host.kind === "wsl") {
350
+ const windowsExe = toWindows(exe).replace(/'/g, "''");
351
+ const command = `Start-Process -FilePath '${windowsExe}' -ArgumentList 'serve' -WindowStyle Hidden`;
352
+ const result = run("powershell.exe", ["-NoProfile", "-Command", command], {
353
+ timeout: 30_000,
354
+ env,
355
+ });
356
+ if (result.status !== 0)
357
+ throw new Error(`Starting the driver daemon failed: ${normalize(result.stderr || result.stdout || result.error?.message || "no output")
358
+ .trim()
359
+ .slice(0, 400)}`);
360
+ }
361
+ else {
362
+ (deps.spawnDetached ?? defaultSpawnDetached)(exe, ["serve"], env);
363
+ }
364
+ const deadline = now() + START_DEADLINE_MS;
365
+ let status = current;
366
+ while (now() < deadline) {
367
+ await sleep(START_POLL_MS);
368
+ status = daemonStatus(host);
369
+ if (status.running)
370
+ return status;
371
+ }
372
+ return status;
373
+ }
374
+ function stopDaemon(host) {
375
+ exec(host, ["stop"], STATUS_TIMEOUT_MS);
376
+ return daemonStatus(host);
377
+ }
378
+ function toWindows(path) {
379
+ const converted = wslpath("-w", path);
380
+ if (converted)
381
+ return converted;
382
+ if (/^\/mnt\/[a-z]\//.test(path))
383
+ return wslPathToWindows(path);
384
+ // wslpath can refuse a path that does not exist yet (an output file).
385
+ const dir = posix.dirname(path);
386
+ const parent = dir !== path ? wslpath("-w", dir) : undefined;
387
+ return parent ? `${parent.replace(/\\$/, "")}\\${posix.basename(path)}` : path;
388
+ }
389
+ function toCaller(path) {
390
+ const converted = wslpath("-u", path);
391
+ if (converted)
392
+ return converted;
393
+ try {
394
+ return windowsPathToWsl(path);
395
+ }
396
+ catch {
397
+ return path;
398
+ }
399
+ }
400
+ function translateArgsPaths(host, _tool, args) {
401
+ if (host.kind !== "wsl")
402
+ return args;
403
+ const convert = (value) => typeof value === "string" && value.startsWith("/") ? toWindows(value) : value;
404
+ const out = { ...args };
405
+ for (const [key, value] of Object.entries(args)) {
406
+ if (!PATH_ARG_KEYS.has(key))
407
+ continue;
408
+ out[key] = Array.isArray(value) ? value.map(convert) : convert(value);
409
+ }
410
+ return out;
411
+ }
412
+ function translateResultPaths(host, stdout) {
413
+ if (host.kind !== "wsl")
414
+ return stdout;
415
+ let parsed;
416
+ try {
417
+ parsed = JSON.parse(stdout);
418
+ }
419
+ catch {
420
+ return stdout;
421
+ }
422
+ let changed = false;
423
+ // Strip the Win32 extended-length prefixes the driver may return:
424
+ // `\\?\UNC\host\share` -> `\\host\share`, `\\?\C:\x` -> `C:\x`.
425
+ const unprefix = (value) => value.replace(/^\\\\\?\\UNC\\/i, "\\\\").replace(/^\\\\\?\\(?=[A-Za-z]:\\)/, "");
426
+ const isWindowsPath = (value) => /^[A-Za-z]:\\/.test(unprefix(value)) || /^\\\\wsl/i.test(unprefix(value));
427
+ const isPathKey = (key) => key === "path" || key === "paths" || key.endsWith("_path") || key.endsWith("_file");
428
+ const convert = (value) => {
429
+ if (typeof value !== "string" || !isWindowsPath(value))
430
+ return value;
431
+ changed = true;
432
+ return toCaller(unprefix(value));
433
+ };
434
+ const walk = (node) => {
435
+ if (Array.isArray(node))
436
+ return node.map(walk);
437
+ if (!node || typeof node !== "object")
438
+ return node;
439
+ const out = {};
440
+ for (const [key, value] of Object.entries(node)) {
441
+ if (isPathKey(key) && typeof value === "string")
442
+ out[key] = convert(value);
443
+ else if (isPathKey(key) && Array.isArray(value))
444
+ out[key] = value.map(convert);
445
+ else
446
+ out[key] = walk(value);
447
+ }
448
+ return out;
449
+ };
450
+ const result = walk(parsed);
451
+ if (!changed)
452
+ return stdout;
453
+ const pretty = stdout.trim().includes("\n");
454
+ const trailing = stdout.endsWith("\n") ? "\n" : "";
455
+ return `${pretty ? JSON.stringify(result, null, 2) : JSON.stringify(result)}${trailing}`;
456
+ }
457
+ return {
458
+ id: CUA_DRIVER_ID,
459
+ version: CUA_DRIVER_VERSION,
460
+ planInstall,
461
+ install,
462
+ installedExecutable,
463
+ daemonStatus,
464
+ startDaemon,
465
+ stopDaemon,
466
+ call: (host, tool, args) => exec(host, ["call", tool, JSON.stringify(args)], CALL_TIMEOUT_MS),
467
+ listTools: (host) => exec(host, ["list-tools"], CALL_TIMEOUT_MS),
468
+ describe: (host, tool) => exec(host, ["describe", tool], CALL_TIMEOUT_MS),
469
+ health: (host) => exec(host, ["call", "health_report", "{}"], CALL_TIMEOUT_MS),
470
+ translateArgsPaths,
471
+ translateResultPaths,
472
+ };
473
+ }
474
+ export const cuaDriver = createCuaDriver();
@@ -0,0 +1,19 @@
1
+ import type { HostInfo } from "./types.js";
2
+ export interface HostDeps {
3
+ /** Defaults to `process.platform`. */
4
+ platform?: NodeJS.Platform;
5
+ /** Defaults to `process.env`. Native Windows reads `LOCALAPPDATA` from it. */
6
+ env?: NodeJS.ProcessEnv;
7
+ /** Defaults to the shared WSL probe. Consulted only on Linux. */
8
+ isWsl?: () => boolean;
9
+ /** `%LOCALAPPDATA%` as a Windows path, read through interop. WSL only. */
10
+ localAppData?: () => string | undefined;
11
+ }
12
+ /**
13
+ * Resolve the host. The default (no deps) result is cached for the life of
14
+ * the process; injected deps always recompute.
15
+ */
16
+ export declare function detectHost(deps?: HostDeps): HostInfo;
17
+ /** Forget the cached default result (tests only). */
18
+ export declare function resetHostCache(): void;
19
+ //# sourceMappingURL=host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../../src/lib/computer/host.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,WAAW,QAAQ;IACvB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,8EAA8E;IAC9E,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC;IACtB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;CACzC;AASD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAKpD;AAED,qDAAqD;AACrD,wBAAgB,cAAc,IAAI,IAAI,CAErC"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Host detection for `harn computer` (ADR 0228).
3
+ *
4
+ * Decides whose desktop this process can drive and where the state shared by
5
+ * every caller of that desktop lives (`%LOCALAPPDATA%\harnery`). Native
6
+ * Windows and WSL2 both drive the Windows desktop; everything else is
7
+ * unsupported for now.
8
+ *
9
+ * Internal module: not exported from package.json.
10
+ */
11
+ import { win32 } from "node:path";
12
+ import { isWSL } from "../browser/launch-args.js";
13
+ import { windowsLocalAppData, windowsPathToWsl } from "../browser/windows-chrome.js";
14
+ const MACOS_REASON = "macOS is not yet supported: its desktop driver needs an app bundle and accessibility and screen-recording grants (ADR 0228).";
15
+ const LINUX_REASON = "Linux desktops are not yet supported; computer use drives a Windows desktop from native Windows or WSL2 (ADR 0228).";
16
+ let cached;
17
+ /**
18
+ * Resolve the host. The default (no deps) result is cached for the life of
19
+ * the process; injected deps always recompute.
20
+ */
21
+ export function detectHost(deps) {
22
+ if (!deps && cached)
23
+ return cached;
24
+ const result = resolveHost(deps ?? {});
25
+ if (!deps)
26
+ cached = result;
27
+ return result;
28
+ }
29
+ /** Forget the cached default result (tests only). */
30
+ export function resetHostCache() {
31
+ cached = undefined;
32
+ }
33
+ function resolveHost(deps) {
34
+ const platform = deps.platform ?? process.platform;
35
+ const env = deps.env ?? process.env;
36
+ if (platform === "win32") {
37
+ const localAppData = env.LOCALAPPDATA?.trim();
38
+ if (!localAppData)
39
+ return {
40
+ kind: "unsupported",
41
+ reason: "LOCALAPPDATA is not set, so the shared desktop state directory is unknown.",
42
+ };
43
+ return { kind: "windows", desktopStateDir: win32.join(localAppData, "harnery") };
44
+ }
45
+ if (platform === "linux" && (deps.isWsl ?? isWSL)()) {
46
+ const localAppData = (deps.localAppData ?? windowsLocalAppData)();
47
+ if (!localAppData)
48
+ return {
49
+ kind: "unsupported",
50
+ reason: "Running under WSL, but %LOCALAPPDATA% could not be read through Windows interop (cmd.exe). Check that interop is enabled.",
51
+ };
52
+ let mounted;
53
+ try {
54
+ mounted = windowsPathToWsl(localAppData);
55
+ }
56
+ catch {
57
+ return {
58
+ kind: "unsupported",
59
+ reason: `Running under WSL, but %LOCALAPPDATA% is not a drive-letter path: ${localAppData}`,
60
+ };
61
+ }
62
+ return { kind: "wsl", desktopStateDir: `${mounted}/harnery` };
63
+ }
64
+ if (platform === "darwin")
65
+ return { kind: "unsupported", reason: MACOS_REASON };
66
+ if (platform === "linux")
67
+ return { kind: "unsupported", reason: LINUX_REASON };
68
+ return {
69
+ kind: "unsupported",
70
+ reason: `Platform ${platform} is not supported for computer use (ADR 0228).`,
71
+ };
72
+ }
@@ -0,0 +1,13 @@
1
+ import type { LeaseApi } from "./types.js";
2
+ /** Idle expiry when `LeaseOptions.idleMs` is omitted. */
3
+ export declare const DEFAULT_LEASE_IDLE_MS = 120000;
4
+ /** Directory holding the lease file for a desktop state dir. */
5
+ export declare function leaseDir(desktopStateDir: string): string;
6
+ export interface DesktopLeaseDeps {
7
+ /** Async sleep used while waiting for another owner's lease. */
8
+ sleep?: (ms: number) => Promise<void>;
9
+ }
10
+ /** Build a lease API; tests inject `sleep` (and `LeaseOptions.now`). */
11
+ export declare function createDesktopLease(deps?: DesktopLeaseDeps): LeaseApi;
12
+ export declare const desktopLease: LeaseApi;
13
+ //# sourceMappingURL=lease.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lease.d.ts","sourceRoot":"","sources":["../../../src/lib/computer/lease.ts"],"names":[],"mappings":"AAkCA,OAAO,KAAK,EAGV,QAAQ,EAGT,MAAM,YAAY,CAAC;AAEpB,yDAAyD;AACzD,eAAO,MAAM,qBAAqB,SAAU,CAAC;AAW7C,gEAAgE;AAChE,wBAAgB,QAAQ,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAExD;AAsLD,MAAM,WAAW,gBAAgB;IAC/B,gEAAgE;IAChE,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAED,wEAAwE;AACxE,wBAAgB,kBAAkB,CAAC,IAAI,GAAE,gBAAqB,GAAG,QAAQ,CAmDxE;AAED,eAAO,MAAM,YAAY,EAAE,QAA+B,CAAC"}