@oharness/engine 0.9.3 → 0.11.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 (170) hide show
  1. package/dist/acp/claude.d.ts +34 -0
  2. package/dist/acp/claude.js +122 -0
  3. package/dist/acp/claude.js.map +1 -0
  4. package/dist/acp/client.d.ts +193 -0
  5. package/dist/acp/client.js +279 -0
  6. package/dist/acp/client.js.map +1 -0
  7. package/dist/acp/runner.d.ts +80 -0
  8. package/dist/acp/runner.js +159 -0
  9. package/dist/acp/runner.js.map +1 -0
  10. package/dist/agent/agent.d.ts +258 -2
  11. package/dist/agent/agent.js +1010 -53
  12. package/dist/agent/agent.js.map +1 -1
  13. package/dist/agent/background.d.ts +135 -0
  14. package/dist/agent/background.js +307 -0
  15. package/dist/agent/background.js.map +1 -0
  16. package/dist/agent/compaction.d.ts +7 -0
  17. package/dist/agent/compaction.js +11 -3
  18. package/dist/agent/compaction.js.map +1 -1
  19. package/dist/agent/goal.d.ts +36 -0
  20. package/dist/agent/goal.js +71 -0
  21. package/dist/agent/goal.js.map +1 -0
  22. package/dist/agent/hooks.js +6 -6
  23. package/dist/agent/hooks.js.map +1 -1
  24. package/dist/agent/permissions.d.ts +6 -0
  25. package/dist/agent/permissions.js +2 -1
  26. package/dist/agent/permissions.js.map +1 -1
  27. package/dist/agent/session.d.ts +38 -0
  28. package/dist/agent/session.js +91 -2
  29. package/dist/agent/session.js.map +1 -1
  30. package/dist/agent/store.d.ts +25 -0
  31. package/dist/agent/store.js +21 -0
  32. package/dist/agent/store.js.map +1 -1
  33. package/dist/agent/subagent.d.ts +21 -0
  34. package/dist/agent/subagent.js +17 -1
  35. package/dist/agent/subagent.js.map +1 -1
  36. package/dist/agent/workspace.d.ts +9 -0
  37. package/dist/agent/workspace.js +59 -5
  38. package/dist/agent/workspace.js.map +1 -1
  39. package/dist/auth/index.d.ts +3 -1
  40. package/dist/auth/index.js +3 -1
  41. package/dist/auth/index.js.map +1 -1
  42. package/dist/auth/manager.d.ts +9 -0
  43. package/dist/auth/manager.js +71 -6
  44. package/dist/auth/manager.js.map +1 -1
  45. package/dist/auth/oauth.d.ts +33 -0
  46. package/dist/auth/oauth.js +59 -8
  47. package/dist/auth/oauth.js.map +1 -1
  48. package/dist/auth/providers/account.js +5 -4
  49. package/dist/auth/providers/account.js.map +1 -1
  50. package/dist/auth/providers/anthropic.d.ts +25 -5
  51. package/dist/auth/providers/anthropic.js +47 -6
  52. package/dist/auth/providers/anthropic.js.map +1 -1
  53. package/dist/auth/providers/chatgpt.d.ts +8 -0
  54. package/dist/auth/providers/chatgpt.js +180 -0
  55. package/dist/auth/providers/chatgpt.js.map +1 -0
  56. package/dist/auth/providers/openrouter.d.ts +2 -0
  57. package/dist/auth/providers/openrouter.js +101 -0
  58. package/dist/auth/providers/openrouter.js.map +1 -0
  59. package/dist/auth/types.d.ts +15 -0
  60. package/dist/auth/types.js.map +1 -1
  61. package/dist/browser/index.d.ts +1 -1
  62. package/dist/browser/plugin.d.ts +6 -1
  63. package/dist/browser/plugin.js.map +1 -1
  64. package/dist/browser/session.d.ts +47 -0
  65. package/dist/browser/session.js +177 -2
  66. package/dist/browser/session.js.map +1 -1
  67. package/dist/browser/tools.js +142 -18
  68. package/dist/browser/tools.js.map +1 -1
  69. package/dist/computer/arbiter.d.ts +69 -0
  70. package/dist/computer/arbiter.js +128 -0
  71. package/dist/computer/arbiter.js.map +1 -0
  72. package/dist/computer/cua-driver.d.ts +174 -0
  73. package/dist/computer/cua-driver.js +597 -0
  74. package/dist/computer/cua-driver.js.map +1 -0
  75. package/dist/computer/helper-client.d.ts +80 -0
  76. package/dist/computer/helper-client.js +166 -0
  77. package/dist/computer/helper-client.js.map +1 -0
  78. package/dist/computer/index.d.ts +16 -0
  79. package/dist/computer/index.js +9 -0
  80. package/dist/computer/index.js.map +1 -0
  81. package/dist/computer/mode.d.ts +34 -0
  82. package/dist/computer/mode.js +52 -0
  83. package/dist/computer/mode.js.map +1 -0
  84. package/dist/computer/plugin.d.ts +24 -0
  85. package/dist/computer/plugin.js +17 -0
  86. package/dist/computer/plugin.js.map +1 -0
  87. package/dist/computer/policy.d.ts +29 -0
  88. package/dist/computer/policy.js +127 -0
  89. package/dist/computer/policy.js.map +1 -0
  90. package/dist/computer/session.d.ts +228 -0
  91. package/dist/computer/session.js +493 -0
  92. package/dist/computer/session.js.map +1 -0
  93. package/dist/computer/target.d.ts +206 -0
  94. package/dist/computer/target.js +31 -0
  95. package/dist/computer/target.js.map +1 -0
  96. package/dist/computer/tools.d.ts +8 -0
  97. package/dist/computer/tools.js +664 -0
  98. package/dist/computer/tools.js.map +1 -0
  99. package/dist/config.d.ts +7 -0
  100. package/dist/config.js.map +1 -1
  101. package/dist/core/tokens.js +4 -0
  102. package/dist/core/tokens.js.map +1 -1
  103. package/dist/core/types.d.ts +16 -1
  104. package/dist/core/types.js.map +1 -1
  105. package/dist/harness.d.ts +128 -1
  106. package/dist/harness.js +287 -30
  107. package/dist/harness.js.map +1 -1
  108. package/dist/index.d.ts +3 -0
  109. package/dist/index.js +2 -0
  110. package/dist/index.js.map +1 -1
  111. package/dist/mcp/manager.d.ts +15 -0
  112. package/dist/mcp/manager.js +28 -1
  113. package/dist/mcp/manager.js.map +1 -1
  114. package/dist/mcp/transport.js +3 -0
  115. package/dist/mcp/transport.js.map +1 -1
  116. package/dist/providers/catalog.js +6 -0
  117. package/dist/providers/catalog.js.map +1 -1
  118. package/dist/providers/catalog.json +14 -3
  119. package/dist/providers/discovery.js +10 -2
  120. package/dist/providers/discovery.js.map +1 -1
  121. package/dist/providers/models-dev.d.ts +15 -0
  122. package/dist/providers/models-dev.js +15 -0
  123. package/dist/providers/models-dev.js.map +1 -1
  124. package/dist/providers/models-dev.json +114 -102
  125. package/dist/providers/piai.d.ts +54 -2
  126. package/dist/providers/piai.js +154 -9
  127. package/dist/providers/piai.js.map +1 -1
  128. package/dist/providers/registry.d.ts +16 -1
  129. package/dist/providers/registry.js +42 -11
  130. package/dist/providers/registry.js.map +1 -1
  131. package/dist/providers/types.d.ts +6 -0
  132. package/dist/providers/types.js.map +1 -1
  133. package/dist/runtime-plugins/builtin/studio.d.ts +7 -0
  134. package/dist/runtime-plugins/builtin/studio.js +69 -4
  135. package/dist/runtime-plugins/builtin/studio.js.map +1 -1
  136. package/dist/runtime-plugins/cloud-connector.js +5 -0
  137. package/dist/runtime-plugins/cloud-connector.js.map +1 -1
  138. package/dist/schedule/scheduler.d.ts +38 -0
  139. package/dist/schedule/scheduler.js +101 -1
  140. package/dist/schedule/scheduler.js.map +1 -1
  141. package/dist/tools/builtin/bash.d.ts +6 -0
  142. package/dist/tools/builtin/bash.js +8 -11
  143. package/dist/tools/builtin/bash.js.map +1 -1
  144. package/dist/tools/builtin/recall.js +4 -0
  145. package/dist/tools/builtin/recall.js.map +1 -1
  146. package/dist/tools/builtin/routine.d.ts +9 -0
  147. package/dist/tools/builtin/routine.js +23 -2
  148. package/dist/tools/builtin/routine.js.map +1 -1
  149. package/dist/tools/builtin/task-control.d.ts +21 -0
  150. package/dist/tools/builtin/task-control.js +120 -0
  151. package/dist/tools/builtin/task-control.js.map +1 -0
  152. package/dist/tools/builtin/task.js +50 -9
  153. package/dist/tools/builtin/task.js.map +1 -1
  154. package/dist/tools/builtin/todo.d.ts +1 -1
  155. package/dist/tools/builtin/worktree.d.ts +10 -0
  156. package/dist/tools/builtin/worktree.js +52 -0
  157. package/dist/tools/builtin/worktree.js.map +1 -0
  158. package/dist/tools/define.d.ts +4 -1
  159. package/dist/tools/define.js +2 -0
  160. package/dist/tools/define.js.map +1 -1
  161. package/dist/tools/types.d.ts +112 -0
  162. package/dist/tools/types.js.map +1 -1
  163. package/dist/util/shell.d.ts +30 -0
  164. package/dist/util/shell.js +70 -0
  165. package/dist/util/shell.js.map +1 -1
  166. package/dist/worktree/index.d.ts +109 -0
  167. package/dist/worktree/index.js +463 -0
  168. package/dist/worktree/index.js.map +1 -0
  169. package/package.json +3 -3
  170. package/skills/media-generation/SKILL.md +22 -1
@@ -0,0 +1,664 @@
1
+ import { z } from "zod";
2
+ import { defineTool } from "../tools/define.js";
3
+ import { toolError, toolImage, toolText } from "../tools/types.js";
4
+ import { errorMessage } from "../core/errors.js";
5
+ import { ComputerError } from "./target.js";
6
+ import { COMPUTER_ENABLE_TOOL, computerOn, setComputerOn } from "./mode.js";
7
+ /**
8
+ * The computer tools.
9
+ *
10
+ * Built against one conversation's session, the way the browser tools are: a
11
+ * ref means nothing without the tree it came from, and the permission prompt
12
+ * has to name the app a call reaches, which is session state rather than an
13
+ * argument.
14
+ *
15
+ * Shaped like the browser tools the model already knows: read a tree of refs,
16
+ * act on a ref, fall back to pixels from a screenshot, chain with a batch.
17
+ */
18
+ /** Screenshots are sent at most this wide; the frame maps coordinates back. */
19
+ const SHOT_MAX_WIDTH = 1280;
20
+ /**
21
+ * A zoom is sent at most this wide: past it a model downscales the image
22
+ * anyway, and the point of a zoom is that nothing gets downscaled.
23
+ */
24
+ const ZOOM_MAX_WIDTH = 1568;
25
+ /** Margin around a zoomed element, in screen units, so its edges are in the picture. */
26
+ const ZOOM_REF_MARGIN = 12;
27
+ /** Nodes in one tree read, so a huge window cannot flood the context. */
28
+ const TREE_MAX_NODES = 400;
29
+ const target = {
30
+ app: z.string().optional().describe("App name or id (bundle id on macOS, exe name on Windows). Uses its front window."),
31
+ window: z.string().optional().describe("Window id from computer_list_apps. Omit to use the app's front window, or the last window used."),
32
+ };
33
+ const modifiersParam = z
34
+ .array(z.enum(["cmd", "ctrl", "alt", "option", "shift", "mod", "win"]))
35
+ .optional()
36
+ .describe("Held while acting. `mod` is cmd on macOS and ctrl on Windows.");
37
+ export function createComputerTools(session) {
38
+ /** Every call is approved per app: `Computer(com.apple.Notes)`, `Computer(excel.exe)`. */
39
+ const principals = (input) => {
40
+ const id = session.appIdFor(input);
41
+ return id ? [`Computer(${id})`] : [];
42
+ };
43
+ const listApps = defineTool({
44
+ name: "computer_list_apps",
45
+ description: "List the apps running on the user's computer and their open windows, with window ids and what each app allows (full, click-only, look-only). Start here.",
46
+ parameters: z.object({}),
47
+ // Names and titles only; reading a window's contents is what asks.
48
+ readOnly: true,
49
+ renderTitle: () => "computer_list_apps",
50
+ async execute(_input, ctx) {
51
+ return run(session, ctx, async () => {
52
+ const [apps, windows, displays] = await Promise.all([session.listApps(), session.listWindows(), session.listDisplays()]);
53
+ const lines = [];
54
+ // One monitor needs no saying; with more, each window says where it is.
55
+ const several = displays.length > 1;
56
+ if (several) {
57
+ lines.push("Displays:");
58
+ for (const d of displays)
59
+ lines.push(` display ${d.id}: ${d.name} — ${Math.round(d.frame.w)}x${Math.round(d.frame.h)} at (${Math.round(d.frame.x)}, ${Math.round(d.frame.y)})${d.primary ? " [primary]" : ""}`);
60
+ lines.push("");
61
+ }
62
+ for (const app of apps) {
63
+ const own = windows.filter((w) => w.pid === app.pid);
64
+ if (!own.length && !app.active)
65
+ continue;
66
+ const tier = session.tierOf({ appId: app.id, app: app.name });
67
+ lines.push(`${app.name} (${app.id})${app.active ? " — frontmost" : ""}${tier === "full" ? "" : ` — ${tier === "read" ? "look only" : tier === "click" ? "click only" : "blocked"}`}`);
68
+ for (const w of own) {
69
+ const on = several && !w.minimized && w.onScreen !== false ? displayOf(w.frame, displays) : undefined;
70
+ lines.push(` window ${w.id}: ${w.title || "(untitled)"}${w.minimized ? " [minimized]" : w.onScreen === false ? " [not on screen]" : ""}${w.focused ? " [focused]" : ""}${on ? ` [display ${on.id}]` : ""}`);
71
+ }
72
+ }
73
+ return toolText(lines.join("\n") || "No apps with windows.");
74
+ });
75
+ },
76
+ });
77
+ const readApp = defineTool({
78
+ name: "computer_read_app",
79
+ description: "Read a window's accessibility tree: its buttons, fields, menus and text, each with a `ref` to act on. Prefer this to a screenshot; it is exact and costs far fewer tokens.",
80
+ parameters: z.object({
81
+ ...target,
82
+ depth: z.number().int().min(1).max(40).optional().describe("How deep to walk. Default 25."),
83
+ }),
84
+ // Looking changes nothing, so plan mode allows it; the first look at an
85
+ // app still asks, because a screenshot shows whatever is in it.
86
+ observes: true,
87
+ principals,
88
+ renderTitle: (input) => `computer_read_app(${input.app ?? input.window ?? ""})`,
89
+ async execute(input, ctx) {
90
+ return run(session, ctx, async () => {
91
+ const t = await session.runtime.target();
92
+ const window = await session.resolveWindow(input);
93
+ session.guard(window, "look");
94
+ const tree = await t.axTree({ window: window.id, depth: input.depth ?? 25, maxNodes: TREE_MAX_NODES });
95
+ session.rememberTree(tree.nodes);
96
+ await session.announce({ kind: "look", window: window.id, app: window.app }, t);
97
+ const header = `${window.app} — window ${window.id}: ${window.title || "(untitled)"}`;
98
+ const body = tree.nodes.map(formatNode).join("\n");
99
+ const tail = tree.truncated ? `\n… truncated at ${TREE_MAX_NODES} nodes; read a smaller depth or take a screenshot.` : "";
100
+ return toolText(`${header}\n${body || "(the accessibility tree is empty — take a screenshot instead)"}${tail}`);
101
+ });
102
+ },
103
+ });
104
+ const screenshot = defineTool({
105
+ name: "computer_screenshot",
106
+ description: "Take a screenshot of one window (default) or a whole display. Returns the image; x/y in other tools are pixels in the latest screenshot of that window.",
107
+ parameters: z.object({
108
+ ...target,
109
+ display: z.string().optional().describe('A display id or name from computer_list_apps ("primary" works too), to capture a whole screen instead of a window.'),
110
+ }),
111
+ // Looking changes nothing, so plan mode allows it; the first look at an
112
+ // app still asks, because a screenshot shows whatever is in it.
113
+ observes: true,
114
+ principals,
115
+ renderTitle: (input) => `computer_screenshot(${input.app ?? input.window ?? input.display ?? ""})`,
116
+ async execute(input, ctx) {
117
+ return run(session, ctx, async () => {
118
+ const t = await session.runtime.target();
119
+ if (input.display) {
120
+ const display = await session.resolveDisplay(input.display);
121
+ const id = display?.id ?? input.display;
122
+ const shot = await t.screenshot({ display: id, maxWidth: SHOT_MAX_WIDTH });
123
+ session.rememberShot(`display:${id}`, shot);
124
+ return toolImage(shot.mimeType ?? "image/jpeg", shot.data, `Display ${id}${display ? ` (${display.name})` : ""}, ${shot.width}x${shot.height} px.`);
125
+ }
126
+ const window = await session.resolveWindow(input);
127
+ session.guard(window, "look");
128
+ const shot = await t.screenshot({ window: window.id, maxWidth: SHOT_MAX_WIDTH });
129
+ session.rememberShot(window.id, shot);
130
+ await session.announce({ kind: "look", window: window.id, app: window.app }, t);
131
+ session.sawWindow(window.id, { mimeType: shot.mimeType ?? "image/jpeg", data: shot.data });
132
+ return toolImage(shot.mimeType ?? "image/jpeg", shot.data, `${window.app} — window ${window.id}, ${shot.width}x${shot.height} px.`);
133
+ });
134
+ },
135
+ });
136
+ const clickParams = z.object({
137
+ ...target,
138
+ ref: z.string().optional().describe("Element ref from computer_read_app. Preferred: works without moving the cursor."),
139
+ x: z.number().optional().describe("Pixel x in the latest screenshot of the window."),
140
+ y: z.number().optional(),
141
+ button: z.enum(["left", "right", "middle"]).optional(),
142
+ double: z.boolean().optional(),
143
+ modifiers: modifiersParam,
144
+ });
145
+ const zoom = defineTool({
146
+ name: "computer_zoom",
147
+ description: "Look closely at part of a window (or display) you already have a screenshot of: returns just that region at the screen's full pixel density, so small text and fine detail are legible. Pass `region` as [x0, y0, x1, y1] in pixels of the latest computer_screenshot, or `ref` to zoom on an element from computer_read_app. A reading aid only: x/y for the other tools still refer to the screenshot, never to the zoomed image.",
148
+ parameters: z.object({
149
+ ...target,
150
+ display: z.string().optional().describe("Zoom into the last screenshot of this display instead of a window."),
151
+ region: z.array(z.number()).length(4).optional().describe("[x0, y0, x1, y1]: left, top, right, bottom, in pixels of the latest screenshot."),
152
+ ref: z.string().optional().describe("Element ref from computer_read_app; zooms on that element with a small margin."),
153
+ }),
154
+ observes: true,
155
+ principals,
156
+ renderTitle: (input) => `computer_zoom(${input.ref ?? input.region?.join(",") ?? ""})`,
157
+ async execute(input, ctx) {
158
+ return run(session, ctx, async () => {
159
+ if (!input.region === !input.ref)
160
+ throw new ComputerError("refused", "Pass either region (pixels of the latest screenshot) or ref, not both.");
161
+ const t = await session.runtime.target();
162
+ if (input.display) {
163
+ if (!input.region)
164
+ throw new ComputerError("refused", "On a display, zoom by region: refs belong to windows.");
165
+ const display = await session.resolveDisplay(input.display);
166
+ const id = display?.id ?? input.display;
167
+ const key = `display:${id}`;
168
+ const rect = session.regionToScreen(key, `display ${id}`, input.region);
169
+ const shot = await t.screenshot({ display: id, region: rect, maxWidth: ZOOM_MAX_WIDTH });
170
+ return toolImage(shot.mimeType ?? "image/jpeg", shot.data, zoomCaption(`Display ${id}`, session.screenToRegion(key, shot.frame), shot));
171
+ }
172
+ const window = await session.resolveWindow(input);
173
+ session.guard(window, "look");
174
+ let rect;
175
+ if (input.ref) {
176
+ const frame = session.frameOf(input.ref);
177
+ if (!frame)
178
+ throw new ComputerError("stale_ref", `Unknown ref ${input.ref}, or it has no place on screen. Read the window again with computer_read_app.`);
179
+ rect = within({ x: frame.x - ZOOM_REF_MARGIN, y: frame.y - ZOOM_REF_MARGIN, w: frame.w + 2 * ZOOM_REF_MARGIN, h: frame.h + 2 * ZOOM_REF_MARGIN }, window.frame);
180
+ }
181
+ else {
182
+ rect = session.regionToScreen(window.id, window.app, input.region);
183
+ }
184
+ const shot = await t.screenshot({ window: window.id, region: rect, maxWidth: ZOOM_MAX_WIDTH });
185
+ // Not remembered as the window's screenshot: coordinates keep meaning
186
+ // pixels of the full one.
187
+ await session.announce({ kind: "look", window: window.id, app: window.app }, t);
188
+ return toolImage(shot.mimeType ?? "image/jpeg", shot.data, zoomCaption(window.app, session.screenToRegion(window.id, shot.frame), shot));
189
+ });
190
+ },
191
+ });
192
+ const click = defineTool({
193
+ name: "computer_click",
194
+ description: "Click an element by ref, or a point by x/y from a screenshot. Right-click opens a context menu.",
195
+ parameters: clickParams,
196
+ principals,
197
+ renderTitle: (input) => `computer_click(${input.ref ?? `${input.x},${input.y}`})`,
198
+ async execute(input, ctx) {
199
+ return run(session, ctx, () => doClick(session, input, ctx));
200
+ },
201
+ });
202
+ const typeText = defineTool({
203
+ name: "computer_type",
204
+ description: "Type text. With a ref, into that field (set `replace` to overwrite its value, which works in the background); without, into whatever has focus in the window.",
205
+ parameters: z.object({
206
+ ...target,
207
+ text: z.string(),
208
+ ref: z.string().optional(),
209
+ replace: z.boolean().optional().describe("Replace the field's value instead of typing at the caret."),
210
+ }),
211
+ principals,
212
+ renderTitle: (input) => `computer_type(${input.ref ?? input.app ?? ""})`,
213
+ async execute(input, ctx) {
214
+ return run(session, ctx, () => doType(session, input, ctx));
215
+ },
216
+ });
217
+ const key = defineTool({
218
+ name: "computer_key",
219
+ description: 'Press a key or chord, e.g. "Enter", "Escape", "Tab", "mod+s", "cmd+shift+t", "ctrl+c".',
220
+ parameters: z.object({ ...target, key: z.string() }),
221
+ principals,
222
+ renderTitle: (input) => `computer_key(${input.key})`,
223
+ async execute(input, ctx) {
224
+ return run(session, ctx, () => doKey(session, input, ctx));
225
+ },
226
+ });
227
+ const scroll = defineTool({
228
+ name: "computer_scroll",
229
+ description: "Scroll inside a window, at a ref or a screenshot point. dy > 0 scrolls down.",
230
+ parameters: z.object({
231
+ ...target,
232
+ ref: z.string().optional(),
233
+ x: z.number().optional(),
234
+ y: z.number().optional(),
235
+ dx: z.number().optional(),
236
+ dy: z.number().optional(),
237
+ }),
238
+ principals,
239
+ renderTitle: (input) => `computer_scroll(${input.dy ?? 0})`,
240
+ async execute(input, ctx) {
241
+ return run(session, ctx, () => doScroll(session, input, ctx));
242
+ },
243
+ });
244
+ const drag = defineTool({
245
+ name: "computer_drag",
246
+ description: "Drag from one screenshot point to another. Usually needs computer_take_control first.",
247
+ parameters: z.object({
248
+ ...target,
249
+ from: z.object({ x: z.number(), y: z.number() }),
250
+ to: z.object({ x: z.number(), y: z.number() }),
251
+ modifiers: modifiersParam,
252
+ }),
253
+ principals,
254
+ renderTitle: () => "computer_drag",
255
+ async execute(input, ctx) {
256
+ return run(session, ctx, () => doDrag(session, input, ctx));
257
+ },
258
+ });
259
+ const menu = defineTool({
260
+ name: "computer_menu",
261
+ description: 'Choose an item from the app\'s menu bar by its path, e.g. ["File", "New Note"] or ["Format", "Font", "Bold"]. Runs in the background through accessibility — prefer it to the item\'s keyboard shortcut, which needs the real keyboard when the app has more than one window.',
262
+ parameters: z.object({
263
+ ...target,
264
+ path: z.array(z.string().min(1)).min(1).max(16).describe('Menu titles from the menu bar down, as the app shows them, e.g. ["File", "New Note"].'),
265
+ }),
266
+ principals,
267
+ renderTitle: (input) => `computer_menu(${input.path.join(" › ")})`,
268
+ async execute(input, ctx) {
269
+ return run(session, ctx, async () => {
270
+ const t = await session.runtime.target();
271
+ if (!t.invokeMenu) {
272
+ return toolError("Menus cannot be chosen directly on this machine; use the item's keyboard shortcut with computer_key.");
273
+ }
274
+ const window = await session.resolveWindow(input);
275
+ session.guard(window, "click");
276
+ await session.respectUser(window, t);
277
+ await session.announce({ kind: "click", window: window.id, app: window.app }, t);
278
+ await t.invokeMenu(window.id, input.path);
279
+ return toolText(`Chose ${input.path.join(" › ")} in ${window.app}.`);
280
+ });
281
+ },
282
+ });
283
+ const openApp = defineTool({
284
+ name: "computer_open_app",
285
+ description: "Launch an app, or bring it up if it is running, by name or id.",
286
+ parameters: z.object({ app: z.string() }),
287
+ principals,
288
+ renderTitle: (input) => `computer_open_app(${input.app})`,
289
+ async execute(input, ctx) {
290
+ return run(session, ctx, async () => {
291
+ const t = await session.runtime.target();
292
+ await t.open(input.app);
293
+ // Give it a moment to put a window up, then report what came up.
294
+ for (let i = 0; i < 20; i++) {
295
+ try {
296
+ const window = await session.resolveWindow({ app: input.app });
297
+ session.guard(window, "look");
298
+ await session.announce({ kind: "look", window: window.id, app: window.app }, t);
299
+ return toolText(`${window.app} is open — window ${window.id}: ${window.title || "(untitled)"}.`);
300
+ }
301
+ catch (err) {
302
+ if (err instanceof ComputerError && err.code === "refused")
303
+ throw err;
304
+ await sleep(250, ctx.signal);
305
+ }
306
+ }
307
+ return toolText(`Asked ${input.app} to open; no window has appeared yet. Call computer_list_apps shortly.`);
308
+ });
309
+ },
310
+ });
311
+ const takeControl = defineTool({
312
+ name: "computer_take_control",
313
+ description: "Take the real mouse and keyboard, for actions that cannot run in the background. The user is shown that you are in control and can take it back at any time. Keep it short: do the whole sequence in one computer_batch, then call computer_release_control.",
314
+ parameters: z.object({ reason: z.string().describe("One line the user sees, e.g. 'Drag the file into the upload area'.") }),
315
+ principals: () => ["ComputerTakeover"],
316
+ renderTitle: (input) => `computer_take_control(${input.reason})`,
317
+ async execute(input, ctx) {
318
+ return run(session, ctx, async () => {
319
+ if (session.runtime.takeover() === "never") {
320
+ return toolError("Taking over the mouse and keyboard is turned off in Settings › Computer use. Stay with background actions, or ask the user to do this step.");
321
+ }
322
+ const t = await session.runtime.target();
323
+ await session.runtime.arbiter.takeControl(session.holder(), ctx.signal, (holder) => ctx.onProgress({ toolCallId: ctx.toolCallId, toolName: "computer_take_control", message: `Waiting for "${holder.label}" to release the computer…` }));
324
+ const front = await t.frontmost().catch(() => undefined);
325
+ if (front)
326
+ ctx.host.state.set(PREVIOUS_FRONT, front.pid);
327
+ await session.announce({ kind: "control", app: input.reason });
328
+ return toolText("You have control of the mouse and keyboard. Act now, then call computer_release_control. If the user moves the mouse or presses a key, control returns to them and your action stops.");
329
+ });
330
+ },
331
+ });
332
+ const releaseControl = defineTool({
333
+ name: "computer_release_control",
334
+ description: "Give the mouse and keyboard back to the user and return focus to where they were.",
335
+ parameters: z.object({}),
336
+ sessionLocal: true,
337
+ renderTitle: () => "computer_release_control",
338
+ async execute(_input, ctx) {
339
+ return run(session, ctx, async () => {
340
+ const released = session.runtime.arbiter.releaseControl(session.id);
341
+ const previous = ctx.host.state.get(PREVIOUS_FRONT);
342
+ ctx.host.state.delete(PREVIOUS_FRONT);
343
+ if (released && typeof previous === "number") {
344
+ await (await session.runtime.target()).activate(previous).catch(() => { });
345
+ }
346
+ await session.announce({ kind: "release" });
347
+ return toolText(released ? "Control returned to the user." : "You did not have control.");
348
+ });
349
+ },
350
+ });
351
+ const single = {
352
+ click: (input, ctx) => doClick(session, input, ctx).then(textOf),
353
+ type: (input, ctx) => doType(session, input, ctx).then(textOf),
354
+ key: (input, ctx) => doKey(session, input, ctx).then(textOf),
355
+ scroll: (input, ctx) => doScroll(session, input, ctx).then(textOf),
356
+ drag: (input, ctx) => doDrag(session, input, ctx).then(textOf),
357
+ wait: async (input, ctx) => {
358
+ await sleep(Math.min(5_000, input.ms ?? 500), ctx.signal);
359
+ return "waited";
360
+ },
361
+ };
362
+ /**
363
+ * Each action in a batch, with the fields its single tool takes — spelled
364
+ * out, so the model is shown that a key action needs its `key`. It used to
365
+ * be {action, ...anything}: `{"action":"key"}` went through, and failed
366
+ * deep inside as "Cannot read properties of undefined (reading 'split')",
367
+ * an error nobody could act on.
368
+ */
369
+ const point = { x: z.number(), y: z.number() };
370
+ const batchAction = z.discriminatedUnion("action", [
371
+ z.object({
372
+ action: z.literal("click"),
373
+ ref: z.string().optional().describe("Element ref from computer_read_app."),
374
+ x: z.number().optional().describe("Pixel x in the latest screenshot of the window."),
375
+ y: z.number().optional(),
376
+ button: z.enum(["left", "right", "middle"]).optional(),
377
+ double: z.boolean().optional(),
378
+ modifiers: modifiersParam,
379
+ }),
380
+ z.object({
381
+ action: z.literal("type"),
382
+ text: z.string(),
383
+ ref: z.string().optional(),
384
+ replace: z.boolean().optional(),
385
+ }),
386
+ z.object({
387
+ action: z.literal("key"),
388
+ key: z.string().min(1).describe('The key or chord, e.g. "Enter", "Tab", "mod+n", "cmd+shift+t".'),
389
+ }),
390
+ z.object({
391
+ action: z.literal("scroll"),
392
+ ref: z.string().optional(),
393
+ x: z.number().optional(),
394
+ y: z.number().optional(),
395
+ dx: z.number().optional(),
396
+ dy: z.number().optional(),
397
+ }),
398
+ z.object({ action: z.literal("drag"), from: z.object(point), to: z.object(point), modifiers: modifiersParam }),
399
+ z.object({ action: z.literal("wait"), ms: z.number().int().min(0).max(5_000).optional() }),
400
+ ]);
401
+ const batch = defineTool({
402
+ name: "computer_batch",
403
+ description: 'Run several actions in one window, in order, stopping at the first failure. Each action is one of: {action:"click", ref | x,y, button?, double?, modifiers?}, {action:"type", text, ref?, replace?}, {action:"key", key}, {action:"scroll", ref | x,y, dx?, dy?}, {action:"drag", from:{x,y}, to:{x,y}}, {action:"wait", ms?}. All actions use the batch\'s app/window.',
404
+ parameters: z.object({
405
+ ...target,
406
+ actions: z.array(batchAction).min(1).max(30),
407
+ }),
408
+ // One app per batch: a batch naming one approved app must not smuggle in
409
+ // an action on another, since any matching principal allows the call.
410
+ principals: (input) => principals(input),
411
+ renderTitle: (input) => `computer_batch(${input.actions.length} actions)`,
412
+ async execute(input, ctx) {
413
+ return run(session, ctx, async () => {
414
+ const window = await session.resolveWindow(input);
415
+ const out = [];
416
+ for (const [i, raw] of input.actions.entries()) {
417
+ const { action, ...rest } = raw;
418
+ if (typeof rest["ref"] === "string" && rest["ref"] !== "" && !String(rest["ref"]).startsWith(`w${window.id}.`)) {
419
+ out.push(`${i + 1}. ${action}: refused — ref ${String(rest["ref"])} is not in this batch's window`);
420
+ break;
421
+ }
422
+ try {
423
+ out.push(`${i + 1}. ${action}: ${await single[action]({ ...rest, window: window.id }, ctx)}`);
424
+ }
425
+ catch (err) {
426
+ out.push(`${i + 1}. ${action}: failed — ${errorMessage(err)}`);
427
+ return toolError(out.join("\n"));
428
+ }
429
+ }
430
+ return toolText(out.join("\n"));
431
+ });
432
+ },
433
+ });
434
+ return [enableTool(session), listApps, readApp, screenshot, zoom, click, typeText, key, menu, scroll, drag, openApp, takeControl, releaseControl, batch];
435
+ }
436
+ const PREVIOUS_FRONT = "computer:previousFront";
437
+ async function doClick(session, input, _ctx) {
438
+ const t = await session.runtime.target();
439
+ const window = await session.resolveWindow(input);
440
+ session.guard(window, "click");
441
+ await session.respectUser(window, t);
442
+ const foreground = session.runtime.arbiter.holdsControl(session.id);
443
+ if (input.ref && !input.modifiers?.length && !input.double && (input.button ?? "left") !== "middle") {
444
+ const frame = session.frameOf(input.ref);
445
+ await session.announce({ kind: "click", window: window.id, app: window.app, ...(frame ? { frame, point: centre(frame) } : {}) }, t);
446
+ try {
447
+ await t.axAction(input.ref, input.button === "right" ? "showMenu" : "press");
448
+ return toolText(`Clicked ${input.ref} in ${window.app}.`);
449
+ }
450
+ catch (err) {
451
+ // An element with no press action still has a place on screen.
452
+ if (!(err instanceof ComputerError) || err.code !== "unsupported" || !frame)
453
+ throw err;
454
+ const point = centre(frame);
455
+ await t.input({ type: "click", ...point, button: input.button ?? "left" }, { window: window.id, foreground });
456
+ return toolText(`Clicked ${input.ref} in ${window.app} (by position).`);
457
+ }
458
+ }
459
+ const point = input.ref ? refPoint(session, input.ref) : pointFrom(session, window, input);
460
+ await session.announce({ kind: "click", window: window.id, app: window.app, point }, t);
461
+ await t.input({ type: "click", ...point, button: input.button ?? "left", count: input.double ? 2 : 1, ...(input.modifiers ? { modifiers: input.modifiers } : {}) }, { window: window.id, foreground });
462
+ return toolText(`Clicked in ${window.app}.`);
463
+ }
464
+ async function doType(session, input, ctx) {
465
+ const t = await session.runtime.target();
466
+ const window = await session.resolveWindow(input);
467
+ session.guard(window, "type");
468
+ if (input.ref && session.isSecure(input.ref)) {
469
+ throw new ComputerError("refused", "That is a password field. Ask the user to type it themselves.");
470
+ }
471
+ await session.respectUser(window, t);
472
+ const frame = input.ref ? session.frameOf(input.ref) : undefined;
473
+ await session.announce({ kind: "type", window: window.id, app: window.app, ...(frame ? { frame, point: centre(frame) } : {}) }, t);
474
+ if (input.ref && input.replace) {
475
+ await t.axAction(input.ref, "setValue", input.text);
476
+ return toolText(`Set the value of ${input.ref}.`);
477
+ }
478
+ if (input.ref)
479
+ await t.axAction(input.ref, "focus").catch(() => { });
480
+ await session.waitForQuiet(t, ctx.signal);
481
+ await t.input({ type: "text", text: input.text }, { window: window.id, foreground: session.runtime.arbiter.holdsControl(session.id) });
482
+ return toolText(`Typed ${input.text.length} characters into ${window.app}.`);
483
+ }
484
+ async function doKey(session, input, ctx) {
485
+ const t = await session.runtime.target();
486
+ const window = await session.resolveWindow(input);
487
+ session.guard(window, "type");
488
+ await session.respectUser(window, t);
489
+ if (typeof input.key !== "string" || !input.key.trim()) {
490
+ throw new ComputerError("refused", 'Say which key: {key: "Enter"}, {key: "mod+n"}.');
491
+ }
492
+ const { key, modifiers } = parseChord(input.key, (await session.runtime.info()).platform);
493
+ await session.announce({ kind: "key", window: window.id, app: window.app }, t);
494
+ await session.waitForQuiet(t, ctx.signal);
495
+ await t.input({ type: "key", key, modifiers }, { window: window.id, foreground: session.runtime.arbiter.holdsControl(session.id) });
496
+ return toolText(`Pressed ${input.key} in ${window.app}.`);
497
+ }
498
+ async function doScroll(session, input, _ctx) {
499
+ const t = await session.runtime.target();
500
+ const window = await session.resolveWindow(input);
501
+ session.guard(window, "click");
502
+ await session.respectUser(window, t);
503
+ const point = input.ref
504
+ ? refPoint(session, input.ref)
505
+ : input.x !== undefined && input.y !== undefined
506
+ ? session.toScreen(window, input.x, input.y)
507
+ : centre(window.frame);
508
+ await session.announce({ kind: "scroll", window: window.id, app: window.app, point }, t);
509
+ await t.input({ type: "scroll", ...point, dx: input.dx ?? 0, dy: input.dy ?? 0 }, { window: window.id, foreground: session.runtime.arbiter.holdsControl(session.id) });
510
+ return toolText(`Scrolled in ${window.app}.`);
511
+ }
512
+ async function doDrag(session, input, _ctx) {
513
+ const t = await session.runtime.target();
514
+ const window = await session.resolveWindow(input);
515
+ session.guard(window, "click");
516
+ await session.respectUser(window, t);
517
+ const from = session.toScreen(window, input.from.x, input.from.y);
518
+ const to = session.toScreen(window, input.to.x, input.to.y);
519
+ await session.announce({ kind: "drag", window: window.id, app: window.app, point: to }, t);
520
+ await t.input({ type: "drag", from, to, ...(input.modifiers ? { modifiers: input.modifiers } : {}) }, { window: window.id, foreground: session.runtime.arbiter.holdsControl(session.id) });
521
+ return toolText(`Dragged in ${window.app}.`);
522
+ }
523
+ // ---- helpers ---------------------------------------------------------------
524
+ function pointFrom(session, window, input) {
525
+ if (input.x === undefined || input.y === undefined) {
526
+ throw new ComputerError("refused", "Pass a ref from computer_read_app, or x and y from a screenshot.");
527
+ }
528
+ return session.toScreen(window, input.x, input.y);
529
+ }
530
+ function refPoint(session, ref) {
531
+ const frame = session.frameOf(ref);
532
+ if (!frame)
533
+ throw new ComputerError("stale_ref", `Unknown ref ${ref}. Read the window again with computer_read_app.`);
534
+ return centre(frame);
535
+ }
536
+ /** The display a window is mostly on: the one holding its centre, else the first it touches. */
537
+ function displayOf(frame, displays) {
538
+ const c = centre(frame);
539
+ const holds = (d, x, y) => x >= d.frame.x && x < d.frame.x + d.frame.w && y >= d.frame.y && y < d.frame.y + d.frame.h;
540
+ return displays.find((d) => holds(d, c.x, c.y)) ?? displays.find((d) => intersects(d.frame, frame));
541
+ }
542
+ function intersects(a, b) {
543
+ return a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
544
+ }
545
+ /** `rect` clipped to `bounds`. */
546
+ function within(rect, bounds) {
547
+ const x = Math.max(rect.x, bounds.x);
548
+ const y = Math.max(rect.y, bounds.y);
549
+ const right = Math.min(rect.x + rect.w, bounds.x + bounds.w);
550
+ const bottom = Math.min(rect.y + rect.h, bounds.y + bounds.h);
551
+ if (right <= x || bottom <= y)
552
+ throw new ComputerError("refused", "That element is outside its window; scroll it into view first.");
553
+ return { x, y, w: right - x, h: bottom - y };
554
+ }
555
+ function zoomCaption(what, region, shot) {
556
+ const where = region ? `region [${region.join(", ")}] of the last screenshot` : "the region";
557
+ return `${what} — ${where}, at ${shot.width}x${shot.height} px. A reading aid: x/y for other tools still refer to the screenshot, not this image.`;
558
+ }
559
+ function centre(frame) {
560
+ return { x: frame.x + frame.w / 2, y: frame.y + frame.h / 2 };
561
+ }
562
+ /** `mod+shift+s` → key "s" with the platform's command key and shift. */
563
+ export function parseChord(chord, platform) {
564
+ const parts = chord.split("+").map((p) => p.trim()).filter(Boolean);
565
+ const key = parts.pop() ?? chord;
566
+ const modifiers = parts.map((m) => {
567
+ const lower = m.toLowerCase();
568
+ if (lower === "mod")
569
+ return platform === "darwin" ? "cmd" : "ctrl";
570
+ if (lower === "option")
571
+ return "alt";
572
+ if (lower === "control")
573
+ return "ctrl";
574
+ if (lower === "command" || lower === "meta")
575
+ return platform === "darwin" ? "cmd" : "win";
576
+ return lower;
577
+ });
578
+ return { key, modifiers };
579
+ }
580
+ function formatNode(node) {
581
+ const indent = " ".repeat(Math.max(0, node.depth));
582
+ const name = node.name ? ` "${node.name.slice(0, 120)}"` : "";
583
+ const value = node.secure ? " = •••" : node.value ? ` = "${node.value.slice(0, 200)}"` : "";
584
+ const flags = node.disabled ? " (disabled)" : "";
585
+ return `${indent}[${node.ref}] ${node.role}${name}${value}${flags}`;
586
+ }
587
+ function textOf(result) {
588
+ return result.content.map((b) => (b.type === "text" ? b.text : "")).join(" ");
589
+ }
590
+ function sleep(ms, signal) {
591
+ return new Promise((resolve) => {
592
+ const timer = setTimeout(resolve, ms);
593
+ signal?.addEventListener("abort", () => {
594
+ clearTimeout(timer);
595
+ resolve();
596
+ }, { once: true });
597
+ });
598
+ }
599
+ /**
600
+ * Turn failures into answers the model can act on.
601
+ *
602
+ * A refusal from the policy, the arbiter or the helper becomes a plain
603
+ * sentence; anything else is a crash, and the helper is restarted on the
604
+ * next call rather than left half-alive.
605
+ */
606
+ async function run(session, ctx, fn) {
607
+ try {
608
+ return await fn();
609
+ }
610
+ catch (err) {
611
+ if (err instanceof ComputerError) {
612
+ if (err.code === "permission") {
613
+ const what = err.grant === "screen" ? "Screen Recording" : "Accessibility";
614
+ return toolError(`${what} permission is not granted to OHarness. Ask the user to allow it; the app shows them how. (${err.message})`);
615
+ }
616
+ if (err.code === "needs_foreground") {
617
+ return toolError(`${err.message} This action needs the real mouse and keyboard: call computer_take_control, act, then computer_release_control.`);
618
+ }
619
+ return toolError(err.message);
620
+ }
621
+ ctx.logger.warn("computer tool failed", { error: errorMessage(err) });
622
+ session.runtime.reset();
623
+ return toolError(`The computer helper failed: ${errorMessage(err)}`);
624
+ }
625
+ }
626
+ /**
627
+ * `computer_enable`: the agent switches Computer use on for its conversation.
628
+ *
629
+ * The setting is read on every question rather than when the tool is built,
630
+ * because a conversation's tools outlive a change in Settings: `off` hides the
631
+ * tool, and `auto` makes it session-local, which the permission engine lets
632
+ * through unasked — what it changes is a flag in this conversation, and each
633
+ * app the agent then reaches still asks on its own. `ask` leaves it an
634
+ * ordinary call, so the prompt goes wherever prompts go, a paired phone too.
635
+ */
636
+ function enableTool(session) {
637
+ const tool = defineTool({
638
+ name: COMPUTER_ENABLE_TOOL,
639
+ description: "Switch on Computer use for this conversation, when the request needs you to see or operate apps on the user's computer (native apps such as Notes, Finder, Excel or Settings). " +
640
+ "Not for web pages (use the browser tools), files or commands (use the file tools and bash). The user may be asked to approve; once on, the computer_* tools appear.",
641
+ parameters: z.object({
642
+ reason: z.string().describe("What you need the computer for, in a few words the user will read, e.g. \"add the meeting to Notes\"."),
643
+ }),
644
+ // Switching it on looks at nothing; plan mode may still turn it on, to look.
645
+ observes: true,
646
+ renderTitle: (input) => `computer_enable(${input.reason})`,
647
+ async execute(_input, ctx) {
648
+ const state = ctx.host.state;
649
+ if (computerOn(state))
650
+ return toolText("Computer use is already on for this conversation.");
651
+ if (session.runtime.enableMode() === "off") {
652
+ return toolError("Computer use can only be switched on by the user here, from the + menu. Ask them to.");
653
+ }
654
+ setComputerOn(state, true);
655
+ session.bind(ctx.sessionId);
656
+ session.runtime.emitEnabled(ctx.sessionId);
657
+ return toolText("Computer use is on for this conversation. The computer_* tools are now available: start with computer_list_apps. Each app still asks the user the first time.");
658
+ },
659
+ });
660
+ Object.defineProperty(tool, "hidden", { get: () => session.runtime.enableMode() === "off", enumerable: true });
661
+ Object.defineProperty(tool, "sessionLocal", { get: () => session.runtime.enableMode() === "auto", enumerable: true });
662
+ return tool;
663
+ }
664
+ //# sourceMappingURL=tools.js.map