@gajae-code/coding-agent 0.5.4 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/dist/types/cli/web-search-cli.d.ts +12 -0
- package/dist/types/commands/rlm.d.ts +10 -0
- package/dist/types/commands/web-search.d.ts +54 -0
- package/dist/types/config/keybindings.d.ts +10 -0
- package/dist/types/config/model-profiles.d.ts +2 -1
- package/dist/types/config/model-registry.d.ts +3 -0
- package/dist/types/config/models-config-schema.d.ts +3 -0
- package/dist/types/config/settings-schema.d.ts +61 -3
- package/dist/types/edit/notebook.d.ts +3 -0
- package/dist/types/eval/py/executor.d.ts +3 -0
- package/dist/types/eval/py/kernel.d.ts +3 -1
- package/dist/types/eval/py/runtime.d.ts +9 -1
- package/dist/types/exec/bash-executor.d.ts +4 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
- package/dist/types/extensibility/custom-tools/wrapper.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +1 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +6 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +14 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +6 -0
- package/dist/types/gjc-runtime/tmux-gc.d.ts +3 -3
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +4 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +18 -0
- package/dist/types/goals/state.d.ts +1 -1
- package/dist/types/goals/tools/goal-tool.d.ts +2 -0
- package/dist/types/main.d.ts +11 -0
- package/dist/types/modes/components/custom-editor.d.ts +4 -2
- package/dist/types/modes/components/custom-model-preset-wizard.d.ts +12 -0
- package/dist/types/modes/components/model-selector.d.ts +5 -2
- package/dist/types/modes/components/status-line.d.ts +4 -1
- package/dist/types/modes/controllers/input-controller.d.ts +3 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/print-mode.d.ts +6 -0
- package/dist/types/modes/rpc/rpc-client.d.ts +21 -0
- package/dist/types/modes/rpc/rpc-socket-security.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +13 -0
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +2 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +1 -0
- package/dist/types/rlm/artifacts.d.ts +9 -0
- package/dist/types/rlm/complete-research-tool.d.ts +35 -0
- package/dist/types/rlm/data-context.d.ts +6 -0
- package/dist/types/rlm/index.d.ts +35 -0
- package/dist/types/rlm/notebook.d.ts +12 -0
- package/dist/types/rlm/preset.d.ts +23 -0
- package/dist/types/rlm/python-tool.d.ts +16 -0
- package/dist/types/rlm/report.d.ts +14 -0
- package/dist/types/rlm/types.d.ts +37 -0
- package/dist/types/sdk.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +21 -0
- package/dist/types/tools/bash-allowed-prefixes.d.ts +6 -1
- package/dist/types/tools/browser/attach.d.ts +19 -3
- package/dist/types/tools/browser/registry.d.ts +15 -0
- package/dist/types/tools/browser/render.d.ts +3 -0
- package/dist/types/tools/browser.d.ts +18 -1
- package/dist/types/tools/computer/render.d.ts +17 -0
- package/dist/types/tools/computer.d.ts +465 -0
- package/dist/types/tools/index.d.ts +24 -1
- package/dist/types/tools/job.d.ts +13 -0
- package/dist/types/tools/tool-timeouts.d.ts +5 -0
- package/dist/types/web/search/index.d.ts +32 -2
- package/dist/types/web/search/providers/base.d.ts +22 -0
- package/dist/types/web/search/providers/xai.d.ts +64 -0
- package/dist/types/web/search/types.d.ts +11 -3
- package/package.json +7 -7
- package/src/cli/web-search-cli.ts +123 -8
- package/src/cli.ts +2 -0
- package/src/commands/rlm.ts +19 -0
- package/src/commands/web-search.ts +66 -0
- package/src/config/keybindings.ts +11 -0
- package/src/config/model-profiles.ts +11 -3
- package/src/config/model-registry.ts +55 -1
- package/src/config/models-config-schema.ts +1 -0
- package/src/config/settings-schema.ts +67 -1
- package/src/edit/notebook.ts +6 -2
- package/src/eval/py/executor.ts +8 -1
- package/src/eval/py/kernel.ts +9 -4
- package/src/eval/py/runtime.ts +153 -32
- package/src/exec/bash-executor.ts +10 -4
- package/src/extensibility/custom-tools/types.ts +2 -0
- package/src/extensibility/custom-tools/wrapper.ts +2 -0
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/extensibility/extensions/wrapper.ts +1 -0
- package/src/gjc-runtime/launch-tmux.ts +129 -1
- package/src/gjc-runtime/session-state-sidecar.ts +61 -1
- package/src/gjc-runtime/tmux-common.ts +26 -2
- package/src/gjc-runtime/tmux-gc.ts +40 -27
- package/src/gjc-runtime/tmux-sessions.ts +13 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +340 -18
- package/src/goals/runtime.ts +4 -3
- package/src/goals/state.ts +1 -1
- package/src/goals/tools/goal-tool.ts +16 -3
- package/src/internal-urls/docs-index.generated.ts +13 -9
- package/src/main.ts +28 -3
- package/src/modes/components/custom-editor.ts +13 -4
- package/src/modes/components/custom-model-preset-wizard.ts +293 -0
- package/src/modes/components/hook-selector.ts +1 -1
- package/src/modes/components/model-selector.ts +72 -29
- package/src/modes/components/skill-message.ts +62 -8
- package/src/modes/components/status-line.ts +13 -1
- package/src/modes/controllers/input-controller.ts +60 -11
- package/src/modes/controllers/selector-controller.ts +39 -0
- package/src/modes/interactive-mode.ts +1 -1
- package/src/modes/print-mode.ts +14 -4
- package/src/modes/rpc/rpc-client.ts +250 -80
- package/src/modes/rpc/rpc-mode.ts +6 -12
- package/src/modes/rpc/rpc-socket-security.ts +103 -0
- package/src/modes/rpc/rpc-types.ts +10 -0
- package/src/modes/shared/agent-wire/command-dispatch.ts +7 -0
- package/src/modes/shared/agent-wire/command-validation.ts +1 -0
- package/src/modes/shared/agent-wire/scopes.ts +1 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +9 -0
- package/src/modes/utils/hotkeys-markdown.ts +4 -2
- package/src/modes/utils/ui-helpers.ts +2 -2
- package/src/prompts/goals/goal-continuation.md +1 -0
- package/src/prompts/goals/goal-mode-active.md +1 -0
- package/src/prompts/system/rlm-report-command.md +1 -0
- package/src/prompts/system/rlm-research.md +23 -0
- package/src/prompts/tools/bash.md +23 -2
- package/src/prompts/tools/browser.md +7 -3
- package/src/prompts/tools/computer.md +74 -0
- package/src/prompts/tools/goal.md +3 -0
- package/src/prompts/tools/job.md +9 -1
- package/src/prompts/tools/web-search.md +7 -0
- package/src/rlm/artifacts.ts +60 -0
- package/src/rlm/complete-research-tool.ts +163 -0
- package/src/rlm/data-context.ts +26 -0
- package/src/rlm/index.ts +339 -0
- package/src/rlm/notebook.ts +108 -0
- package/src/rlm/preset.ts +76 -0
- package/src/rlm/python-tool.ts +68 -0
- package/src/rlm/report.ts +70 -0
- package/src/rlm/types.ts +40 -0
- package/src/sdk.ts +12 -0
- package/src/session/agent-session.ts +48 -3
- package/src/slash-commands/builtin-registry.ts +17 -0
- package/src/tools/bash-allowed-prefixes.ts +84 -1
- package/src/tools/bash.ts +80 -13
- package/src/tools/browser/attach.ts +103 -3
- package/src/tools/browser/registry.ts +176 -2
- package/src/tools/browser/render.ts +9 -1
- package/src/tools/browser.ts +33 -0
- package/src/tools/computer/render.ts +78 -0
- package/src/tools/computer.ts +640 -0
- package/src/tools/index.ts +41 -1
- package/src/tools/job.ts +88 -5
- package/src/tools/json-tree.ts +42 -29
- package/src/tools/renderers.ts +2 -0
- package/src/tools/tool-timeouts.ts +1 -0
- package/src/web/search/index.ts +27 -2
- package/src/web/search/provider.ts +16 -1
- package/src/web/search/providers/base.ts +22 -0
- package/src/web/search/providers/xai.ts +511 -0
- package/src/web/search/render.ts +7 -0
- package/src/web/search/types.ts +11 -1
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@gajae-code/agent-core";
|
|
5
|
+
import type { ImageContent } from "@gajae-code/ai";
|
|
6
|
+
import { prompt } from "@gajae-code/utils";
|
|
7
|
+
import * as z from "zod/v4";
|
|
8
|
+
import computerDescription from "../prompts/tools/computer.md" with { type: "text" };
|
|
9
|
+
import type { ToolSession } from "./index";
|
|
10
|
+
import type { OutputMeta } from "./output-meta";
|
|
11
|
+
import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
|
|
12
|
+
import { toolResult } from "./tool-result";
|
|
13
|
+
import { clampTimeout } from "./tool-timeouts";
|
|
14
|
+
|
|
15
|
+
const buttonSchema = z.enum(["left", "right", "middle"]);
|
|
16
|
+
const shared = {
|
|
17
|
+
timeout: z.number().positive().optional().describe("Maximum time in seconds for this action."),
|
|
18
|
+
include_screenshot: z.boolean().optional().describe("Capture a bounded post-action screenshot when supported."),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const screenshotSchema = z.object({ action: z.literal("screenshot"), ...shared }).strict();
|
|
22
|
+
const clickSchema = z
|
|
23
|
+
.object({ action: z.literal("click"), x: z.number(), y: z.number(), button: buttonSchema.optional(), ...shared })
|
|
24
|
+
.strict();
|
|
25
|
+
const doubleClickSchema = z
|
|
26
|
+
.object({
|
|
27
|
+
action: z.literal("double_click"),
|
|
28
|
+
x: z.number(),
|
|
29
|
+
y: z.number(),
|
|
30
|
+
button: buttonSchema.optional(),
|
|
31
|
+
...shared,
|
|
32
|
+
})
|
|
33
|
+
.strict();
|
|
34
|
+
const moveSchema = z
|
|
35
|
+
.object({ action: z.literal("move"), x: z.number(), y: z.number(), button: buttonSchema.optional(), ...shared })
|
|
36
|
+
.strict();
|
|
37
|
+
const dragSchema = z
|
|
38
|
+
.object({
|
|
39
|
+
action: z.literal("drag"),
|
|
40
|
+
x: z.number(),
|
|
41
|
+
y: z.number(),
|
|
42
|
+
to_x: z.number(),
|
|
43
|
+
to_y: z.number(),
|
|
44
|
+
button: buttonSchema.optional(),
|
|
45
|
+
...shared,
|
|
46
|
+
})
|
|
47
|
+
.strict();
|
|
48
|
+
const scrollSchema = z
|
|
49
|
+
.object({
|
|
50
|
+
action: z.literal("scroll"),
|
|
51
|
+
x: z.number(),
|
|
52
|
+
y: z.number(),
|
|
53
|
+
scroll_x: z.number(),
|
|
54
|
+
scroll_y: z.number(),
|
|
55
|
+
...shared,
|
|
56
|
+
})
|
|
57
|
+
.strict();
|
|
58
|
+
const typeSchema = z.object({ action: z.literal("type"), text: z.string(), ...shared }).strict();
|
|
59
|
+
const keypressSchema = z
|
|
60
|
+
.object({ action: z.literal("keypress"), keys: z.array(z.string()).min(1), ...shared })
|
|
61
|
+
.strict();
|
|
62
|
+
const waitSchema = z.object({ action: z.literal("wait"), ms: z.number().int().nonnegative(), ...shared }).strict();
|
|
63
|
+
|
|
64
|
+
const singleActionSchemas = [
|
|
65
|
+
screenshotSchema,
|
|
66
|
+
clickSchema,
|
|
67
|
+
doubleClickSchema,
|
|
68
|
+
moveSchema,
|
|
69
|
+
dragSchema,
|
|
70
|
+
scrollSchema,
|
|
71
|
+
typeSchema,
|
|
72
|
+
keypressSchema,
|
|
73
|
+
waitSchema,
|
|
74
|
+
] as const;
|
|
75
|
+
|
|
76
|
+
export const singleComputerSchema = z.discriminatedUnion("action", singleActionSchemas);
|
|
77
|
+
|
|
78
|
+
const batchSchema = z
|
|
79
|
+
.object({
|
|
80
|
+
action: z.literal("batch"),
|
|
81
|
+
actions: z.array(singleComputerSchema).min(1).describe("Sequence of computer actions to execute in order."),
|
|
82
|
+
...shared,
|
|
83
|
+
})
|
|
84
|
+
.strict();
|
|
85
|
+
|
|
86
|
+
export const computerSchema = z.union([singleComputerSchema, batchSchema]);
|
|
87
|
+
|
|
88
|
+
export type SingleComputerParams = z.infer<typeof singleComputerSchema>;
|
|
89
|
+
export type ComputerParams = z.infer<typeof computerSchema>;
|
|
90
|
+
export type ComputerActionName = ComputerParams["action"];
|
|
91
|
+
|
|
92
|
+
export interface ComputerScreenshotDetails {
|
|
93
|
+
widthPx: number;
|
|
94
|
+
heightPx: number;
|
|
95
|
+
scaleX?: number;
|
|
96
|
+
scaleY?: number;
|
|
97
|
+
originX?: number;
|
|
98
|
+
originY?: number;
|
|
99
|
+
displayEpoch?: string;
|
|
100
|
+
captureId?: string;
|
|
101
|
+
pngBytes?: number;
|
|
102
|
+
path?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ComputerToolDetails {
|
|
106
|
+
action: ComputerActionName;
|
|
107
|
+
status: "success" | "disabled" | "error";
|
|
108
|
+
code?: string;
|
|
109
|
+
message?: string;
|
|
110
|
+
x?: number;
|
|
111
|
+
y?: number;
|
|
112
|
+
toX?: number;
|
|
113
|
+
toY?: number;
|
|
114
|
+
scrollX?: number;
|
|
115
|
+
scrollY?: number;
|
|
116
|
+
button?: string;
|
|
117
|
+
keys?: string[];
|
|
118
|
+
ms?: number;
|
|
119
|
+
screenshot?: ComputerScreenshotDetails;
|
|
120
|
+
supervisor?: string;
|
|
121
|
+
steps?: ComputerToolDetails[];
|
|
122
|
+
meta?: OutputMeta;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
type NativeController = {
|
|
126
|
+
screenshot?: () => Promise<NativeScreenshot> | NativeScreenshot;
|
|
127
|
+
click?: (expectedEpoch: number | undefined, x: number, y: number, button?: string) => void;
|
|
128
|
+
doubleClick?: (expectedEpoch: number | undefined, x: number, y: number, button?: string) => void;
|
|
129
|
+
move?: (expectedEpoch: number | undefined, x: number, y: number) => void;
|
|
130
|
+
drag?: (expectedEpoch: number | undefined, x: number, y: number, toX: number, toY: number, button?: string) => void;
|
|
131
|
+
scroll?: (expectedEpoch: number | undefined, x: number, y: number, scrollX: number, scrollY: number) => void;
|
|
132
|
+
type?: (expectedEpoch: number | undefined, text: string) => void;
|
|
133
|
+
keypress?: (expectedEpoch: number | undefined, keys: string[]) => void;
|
|
134
|
+
wait?: (expectedEpoch: number | undefined, ms: number) => void;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
type NativeScreenshot = {
|
|
138
|
+
png?: Uint8Array | Buffer | ArrayBuffer | string;
|
|
139
|
+
widthPx?: number;
|
|
140
|
+
heightPx?: number;
|
|
141
|
+
scaleX?: number;
|
|
142
|
+
scaleY?: number;
|
|
143
|
+
originX?: number;
|
|
144
|
+
originY?: number;
|
|
145
|
+
displayEpoch?: string;
|
|
146
|
+
captureId?: string;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
export type ComputerControllerFactory = () => NativeController;
|
|
150
|
+
|
|
151
|
+
export const COMPUTER_DISABLED_CODE = "COMPUTER_DISABLED";
|
|
152
|
+
|
|
153
|
+
const NATIVE_ERROR_CODES = new Set([
|
|
154
|
+
"COMPUTER_SUSPENDED",
|
|
155
|
+
"COMPUTER_SUPERVISOR_NOT_LIVE",
|
|
156
|
+
"COMPUTER_PERMISSION_REQUIRED",
|
|
157
|
+
"COMPUTER_DISPLAY_STALE",
|
|
158
|
+
"COMPUTER_COORD_INVALID",
|
|
159
|
+
"COMPUTER_CANCELLED",
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
function createNativeComputerController(): NativeController {
|
|
163
|
+
const natives = require("@gajae-code/natives") as { ComputerController?: new () => NativeController };
|
|
164
|
+
if (!natives.ComputerController) {
|
|
165
|
+
throw new ToolError("ComputerController is unavailable in @gajae-code/natives.", {
|
|
166
|
+
code: "COMPUTER_UNAVAILABLE",
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return new natives.ComputerController();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let controllerFactory: ComputerControllerFactory = createNativeComputerController;
|
|
173
|
+
let platformOverrideForTests: NodeJS.Platform | undefined;
|
|
174
|
+
let archOverrideForTests: NodeJS.Architecture | undefined;
|
|
175
|
+
const screenshotFallbackDirs = new WeakMap<ToolSession, Promise<string>>();
|
|
176
|
+
|
|
177
|
+
export function setComputerControllerFactoryForTests(factory: ComputerControllerFactory | undefined): void {
|
|
178
|
+
controllerFactory = factory ?? createNativeComputerController;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function setComputerPlatformForTests(platform: NodeJS.Platform | undefined): void {
|
|
182
|
+
platformOverrideForTests = platform;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function setComputerArchForTests(arch: NodeJS.Architecture | undefined): void {
|
|
186
|
+
archOverrideForTests = arch;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function currentComputerPlatform(): NodeJS.Platform {
|
|
190
|
+
return platformOverrideForTests ?? process.platform;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function currentComputerArch(): NodeJS.Architecture {
|
|
194
|
+
return archOverrideForTests ?? process.arch;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function isComputerSupportedPlatform(
|
|
198
|
+
platform: NodeJS.Platform = currentComputerPlatform(),
|
|
199
|
+
arch: NodeJS.Architecture = currentComputerArch(),
|
|
200
|
+
): boolean {
|
|
201
|
+
return platform === "darwin" && arch === "arm64";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Whether the computer capability is loaded/advertised at all on this platform.
|
|
206
|
+
* macOS is callable; Linux is listable (support planned); Windows is fully absent.
|
|
207
|
+
*/
|
|
208
|
+
export function isComputerLoadablePlatform(platform: NodeJS.Platform = process.platform): boolean {
|
|
209
|
+
return platform !== "win32";
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function isComputerEnabled(session: Pick<ToolSession, "settings">): boolean {
|
|
213
|
+
if (session.settings.get("computer.enabled")) return true;
|
|
214
|
+
if (session.settings.has("computer.enabled")) return false;
|
|
215
|
+
if (session.settings.has("computer.alwaysOn")) return Boolean(session.settings.get("computer.alwaysOn"));
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function isComputerCallable(
|
|
220
|
+
session: Pick<ToolSession, "settings">,
|
|
221
|
+
platform: NodeJS.Platform = currentComputerPlatform(),
|
|
222
|
+
arch: NodeJS.Architecture = currentComputerArch(),
|
|
223
|
+
): boolean {
|
|
224
|
+
return isComputerSupportedPlatform(platform, arch) && isComputerEnabled(session);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export class ComputerTool implements AgentTool<typeof computerSchema, ComputerToolDetails> {
|
|
228
|
+
readonly name = "computer";
|
|
229
|
+
readonly label = "Computer";
|
|
230
|
+
readonly loadMode = "discoverable";
|
|
231
|
+
readonly summary =
|
|
232
|
+
"Control the macOS desktop (Apple Silicon) with screenshot, pointer, keyboard, scroll, and wait actions; available by default on supported hosts and supervisor-gated";
|
|
233
|
+
readonly parameters = computerSchema;
|
|
234
|
+
readonly strict = true;
|
|
235
|
+
#description?: string;
|
|
236
|
+
|
|
237
|
+
constructor(private readonly session: ToolSession) {}
|
|
238
|
+
|
|
239
|
+
static createIf(session: ToolSession): ComputerTool | null {
|
|
240
|
+
return isComputerCallable(session) ? new ComputerTool(session) : null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
get description(): string {
|
|
244
|
+
this.#description ??= prompt.render(computerDescription, {});
|
|
245
|
+
return this.#description;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async execute(
|
|
249
|
+
_toolCallId: string,
|
|
250
|
+
params: ComputerParams,
|
|
251
|
+
signal?: AbortSignal,
|
|
252
|
+
_onUpdate?: AgentToolUpdateCallback<ComputerToolDetails>,
|
|
253
|
+
_ctx?: AgentToolContext,
|
|
254
|
+
): Promise<AgentToolResult<ComputerToolDetails>> {
|
|
255
|
+
const details = detailsFromParams(params);
|
|
256
|
+
const hotkey = this.session.settings.get("computer.killSwitchHotkey") as string | undefined;
|
|
257
|
+
if (!isComputerCallable(this.session)) {
|
|
258
|
+
details.status = "disabled";
|
|
259
|
+
details.code = COMPUTER_DISABLED_CODE;
|
|
260
|
+
details.message =
|
|
261
|
+
"The computer tool is disabled or unsupported. It requires Apple Silicon macOS; set computer.alwaysOn=false to disable, or computer.enabled=true to manually enable on a supported host.";
|
|
262
|
+
await writeComputerAuditLog(this.session, details);
|
|
263
|
+
return { ...toolResult(details).text(`${COMPUTER_DISABLED_CODE}: ${details.message}`).done(), isError: true };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
throwIfAborted(signal);
|
|
268
|
+
const timeoutSeconds = clampTimeout("computer", params.timeout);
|
|
269
|
+
const timeoutMs = timeoutSeconds > 0 ? timeoutSeconds * 1000 : undefined;
|
|
270
|
+
const controller = controllerFactory();
|
|
271
|
+
// Native ComputerController methods are synchronous and accept no AbortSignal,
|
|
272
|
+
// so cancellation is honored before dispatch and wait() is bounded by timeoutMs.
|
|
273
|
+
if (params.action === "batch") {
|
|
274
|
+
const batchResult = await dispatchBatchComputerActions(controller, params.actions, timeoutMs, hotkey);
|
|
275
|
+
details.steps = batchResult.steps;
|
|
276
|
+
if (batchResult.screenshot) details.screenshot = batchResult.screenshot;
|
|
277
|
+
details.status = batchResult.failedStep ? "error" : "success";
|
|
278
|
+
if (batchResult.failedStep) {
|
|
279
|
+
details.code = batchResult.failedStep.code;
|
|
280
|
+
details.message = batchResult.failedStep.message;
|
|
281
|
+
await writeComputerAuditLog(this.session, details);
|
|
282
|
+
return {
|
|
283
|
+
...toolResult(details).text(`${details.code}: ${details.message}`).done(),
|
|
284
|
+
isError: true,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
details.message = describeComputerSuccess(details);
|
|
288
|
+
const image = imageContentFromNativeResult(batchResult.screenshotSource);
|
|
289
|
+
if (batchResult.screenshotSource !== undefined) {
|
|
290
|
+
await persistScreenshotFallback(batchResult.screenshotSource, details.screenshot, this.session);
|
|
291
|
+
details.message = describeComputerSuccess(details);
|
|
292
|
+
}
|
|
293
|
+
await writeComputerAuditLog(this.session, details);
|
|
294
|
+
return image
|
|
295
|
+
? toolResult(details)
|
|
296
|
+
.content([{ type: "text", text: details.message }, image])
|
|
297
|
+
.done()
|
|
298
|
+
: toolResult(details).text(details.message).done();
|
|
299
|
+
}
|
|
300
|
+
const result = await dispatchComputerAction(controller, params, timeoutMs);
|
|
301
|
+
const screenshot = normalizeScreenshot(result);
|
|
302
|
+
if (screenshot) details.screenshot = screenshot;
|
|
303
|
+
details.status = "success";
|
|
304
|
+
details.message = describeComputerSuccess(details);
|
|
305
|
+
const image = imageContentFromNativeResult(result);
|
|
306
|
+
if (screenshot) {
|
|
307
|
+
await persistScreenshotFallback(result, details.screenshot, this.session);
|
|
308
|
+
details.message = describeComputerSuccess(details);
|
|
309
|
+
}
|
|
310
|
+
await writeComputerAuditLog(this.session, details);
|
|
311
|
+
return image
|
|
312
|
+
? toolResult(details)
|
|
313
|
+
.content([{ type: "text", text: details.message }, image])
|
|
314
|
+
.done()
|
|
315
|
+
: toolResult(details).text(details.message).done();
|
|
316
|
+
} catch (error) {
|
|
317
|
+
if (error instanceof ToolAbortError) throw error;
|
|
318
|
+
const mapped = mapComputerError(error, hotkey);
|
|
319
|
+
details.status = mapped.code === COMPUTER_DISABLED_CODE ? "disabled" : "error";
|
|
320
|
+
details.code = mapped.code;
|
|
321
|
+
details.message = mapped.message;
|
|
322
|
+
await writeComputerAuditLog(this.session, details);
|
|
323
|
+
return { ...toolResult(details).text(`${mapped.code}: ${mapped.message}`).done(), isError: true };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
interface CoordinateBounds {
|
|
329
|
+
widthPx: number;
|
|
330
|
+
heightPx: number;
|
|
331
|
+
originX?: number;
|
|
332
|
+
originY?: number;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function validatePointerCoordinates(action: string, x: number, y: number, bounds: CoordinateBounds | undefined): void {
|
|
336
|
+
if (!bounds) return;
|
|
337
|
+
const minX = bounds.originX ?? 0;
|
|
338
|
+
const minY = bounds.originY ?? 0;
|
|
339
|
+
const maxX = minX + bounds.widthPx;
|
|
340
|
+
const maxY = minY + bounds.heightPx;
|
|
341
|
+
if (x < minX || x >= maxX || y < minY || y >= maxY) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
`COMPUTER_COORD_INVALID: ${action} coordinates (${x},${y}) are outside the latest screenshot bounds [${minX},${minY})..[${maxX},${maxY}). Capture a fresh screenshot and use coordinates within its frame.`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function dispatchComputerAction(
|
|
349
|
+
controller: NativeController,
|
|
350
|
+
params: SingleComputerParams,
|
|
351
|
+
timeoutMs: number | undefined,
|
|
352
|
+
bounds?: CoordinateBounds,
|
|
353
|
+
): Promise<unknown> | unknown {
|
|
354
|
+
// expectedEpoch is undefined until lossless epoch transport lands (follow-up):
|
|
355
|
+
// the native gate skips the stale-display check when the epoch is absent.
|
|
356
|
+
switch (params.action) {
|
|
357
|
+
case "screenshot":
|
|
358
|
+
return controller.screenshot?.();
|
|
359
|
+
case "click":
|
|
360
|
+
validatePointerCoordinates("click", params.x, params.y, bounds);
|
|
361
|
+
return controller.click?.(undefined, params.x, params.y, params.button ?? "left");
|
|
362
|
+
case "double_click":
|
|
363
|
+
validatePointerCoordinates("double_click", params.x, params.y, bounds);
|
|
364
|
+
return controller.doubleClick?.(undefined, params.x, params.y, params.button ?? "left");
|
|
365
|
+
case "move":
|
|
366
|
+
validatePointerCoordinates("move", params.x, params.y, bounds);
|
|
367
|
+
return controller.move?.(undefined, params.x, params.y);
|
|
368
|
+
case "drag":
|
|
369
|
+
validatePointerCoordinates("drag start", params.x, params.y, bounds);
|
|
370
|
+
validatePointerCoordinates("drag end", params.to_x, params.to_y, bounds);
|
|
371
|
+
return controller.drag?.(undefined, params.x, params.y, params.to_x, params.to_y, params.button ?? "left");
|
|
372
|
+
case "scroll":
|
|
373
|
+
validatePointerCoordinates("scroll", params.x, params.y, bounds);
|
|
374
|
+
return controller.scroll?.(undefined, params.x, params.y, params.scroll_x, params.scroll_y);
|
|
375
|
+
case "type":
|
|
376
|
+
return controller.type?.(undefined, params.text);
|
|
377
|
+
case "keypress":
|
|
378
|
+
return controller.keypress?.(undefined, params.keys);
|
|
379
|
+
case "wait":
|
|
380
|
+
return controller.wait?.(undefined, capWaitMs(params.ms, timeoutMs));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
interface BatchDispatchResult {
|
|
385
|
+
steps: ComputerToolDetails[];
|
|
386
|
+
screenshot?: ComputerScreenshotDetails;
|
|
387
|
+
screenshotSource?: unknown;
|
|
388
|
+
failedStep?: { code: string; message: string };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function dispatchBatchComputerActions(
|
|
392
|
+
controller: NativeController,
|
|
393
|
+
actions: readonly SingleComputerParams[],
|
|
394
|
+
timeoutMs: number | undefined,
|
|
395
|
+
hotkey?: string,
|
|
396
|
+
): Promise<BatchDispatchResult> {
|
|
397
|
+
const steps: ComputerToolDetails[] = [];
|
|
398
|
+
let lastScreenshot: ComputerScreenshotDetails | undefined;
|
|
399
|
+
let lastScreenshotSource: unknown;
|
|
400
|
+
let bounds: CoordinateBounds | undefined;
|
|
401
|
+
for (const single of actions) {
|
|
402
|
+
const stepDetails = detailsFromParams(single);
|
|
403
|
+
try {
|
|
404
|
+
const result = await dispatchComputerAction(controller, single, timeoutMs, bounds);
|
|
405
|
+
const screenshot = normalizeScreenshot(result);
|
|
406
|
+
if (screenshot) {
|
|
407
|
+
stepDetails.screenshot = screenshot;
|
|
408
|
+
lastScreenshot = screenshot;
|
|
409
|
+
lastScreenshotSource = result;
|
|
410
|
+
bounds = screenshot;
|
|
411
|
+
}
|
|
412
|
+
stepDetails.status = "success";
|
|
413
|
+
stepDetails.message = describeComputerSuccess(stepDetails);
|
|
414
|
+
} catch (error) {
|
|
415
|
+
if (error instanceof ToolAbortError) throw error;
|
|
416
|
+
const mapped = mapComputerError(error, hotkey);
|
|
417
|
+
stepDetails.status = mapped.code === COMPUTER_DISABLED_CODE ? "disabled" : "error";
|
|
418
|
+
stepDetails.code = mapped.code;
|
|
419
|
+
stepDetails.message = mapped.message;
|
|
420
|
+
steps.push(stepDetails);
|
|
421
|
+
return {
|
|
422
|
+
steps,
|
|
423
|
+
screenshot: lastScreenshot,
|
|
424
|
+
screenshotSource: lastScreenshotSource,
|
|
425
|
+
failedStep: { code: mapped.code, message: mapped.message },
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
steps.push(stepDetails);
|
|
429
|
+
}
|
|
430
|
+
return { steps, screenshot: lastScreenshot, screenshotSource: lastScreenshotSource };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function detailsFromParams(params: ComputerParams): ComputerToolDetails {
|
|
434
|
+
const details: ComputerToolDetails = { action: params.action, status: "success" };
|
|
435
|
+
if ("x" in params) details.x = params.x;
|
|
436
|
+
if ("y" in params) details.y = params.y;
|
|
437
|
+
if ("to_x" in params) details.toX = params.to_x;
|
|
438
|
+
if ("to_y" in params) details.toY = params.to_y;
|
|
439
|
+
if ("scroll_x" in params) details.scrollX = params.scroll_x;
|
|
440
|
+
if ("scroll_y" in params) details.scrollY = params.scroll_y;
|
|
441
|
+
if ("button" in params) details.button = params.button;
|
|
442
|
+
if ("keys" in params) details.keys = params.keys;
|
|
443
|
+
if ("ms" in params) details.ms = params.ms;
|
|
444
|
+
return details;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const MAX_COMPUTER_WAIT_MS = 60_000;
|
|
448
|
+
|
|
449
|
+
function capWaitMs(ms: number, timeoutMs: number | undefined): number {
|
|
450
|
+
const ceiling = timeoutMs && timeoutMs > 0 ? timeoutMs : MAX_COMPUTER_WAIT_MS;
|
|
451
|
+
return Math.min(Math.max(0, ms), ceiling);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function normalizeScreenshot(value: unknown): ComputerScreenshotDetails | undefined {
|
|
455
|
+
const candidate =
|
|
456
|
+
value && typeof value === "object" && "screenshot" in value
|
|
457
|
+
? (value as { screenshot?: unknown }).screenshot
|
|
458
|
+
: value;
|
|
459
|
+
if (!candidate || typeof candidate !== "object") return undefined;
|
|
460
|
+
const shot = candidate as NativeScreenshot;
|
|
461
|
+
if (typeof shot.widthPx !== "number" || typeof shot.heightPx !== "number") return undefined;
|
|
462
|
+
return {
|
|
463
|
+
widthPx: shot.widthPx,
|
|
464
|
+
heightPx: shot.heightPx,
|
|
465
|
+
scaleX: shot.scaleX,
|
|
466
|
+
scaleY: shot.scaleY,
|
|
467
|
+
originX: shot.originX,
|
|
468
|
+
originY: shot.originY,
|
|
469
|
+
displayEpoch: shot.displayEpoch,
|
|
470
|
+
captureId: shot.captureId,
|
|
471
|
+
pngBytes: getPngByteLength(shot.png),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function imageContentFromNativeResult(value: unknown): ImageContent | undefined {
|
|
476
|
+
const candidate =
|
|
477
|
+
value && typeof value === "object" && "screenshot" in value
|
|
478
|
+
? (value as { screenshot?: unknown }).screenshot
|
|
479
|
+
: value;
|
|
480
|
+
if (!candidate || typeof candidate !== "object") return undefined;
|
|
481
|
+
const png = (candidate as NativeScreenshot).png;
|
|
482
|
+
const data = pngToBase64(png);
|
|
483
|
+
return data ? { type: "image", data, mimeType: "image/png" } : undefined;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
async function persistScreenshotFallback(
|
|
487
|
+
value: unknown,
|
|
488
|
+
screenshot: ComputerScreenshotDetails | undefined,
|
|
489
|
+
session: ToolSession,
|
|
490
|
+
): Promise<void> {
|
|
491
|
+
if (!screenshot || screenshot.path) return;
|
|
492
|
+
const image = imageContentFromNativeResult(value);
|
|
493
|
+
if (!image) return;
|
|
494
|
+
const dir = await getScreenshotFallbackDir(session);
|
|
495
|
+
const filePath = path.join(dir, `computer-${Date.now()}-${Math.random().toString(36).slice(2)}.png`);
|
|
496
|
+
await fs.writeFile(filePath, Buffer.from(image.data, "base64"), { mode: 0o600 });
|
|
497
|
+
screenshot.path = filePath;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function getScreenshotFallbackDir(session: ToolSession): Promise<string> {
|
|
501
|
+
let dir = screenshotFallbackDirs.get(session);
|
|
502
|
+
if (!dir) {
|
|
503
|
+
dir = createScreenshotFallbackDir();
|
|
504
|
+
screenshotFallbackDirs.set(session, dir);
|
|
505
|
+
}
|
|
506
|
+
return dir;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function createScreenshotFallbackDir(): Promise<string> {
|
|
510
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "gjc-computer-screenshots-"));
|
|
511
|
+
await fs.chmod(dir, 0o700);
|
|
512
|
+
return dir;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function pngToBase64(png: NativeScreenshot["png"]): string | undefined {
|
|
516
|
+
if (png === undefined) return undefined;
|
|
517
|
+
if (typeof png === "string") return png;
|
|
518
|
+
if (png instanceof ArrayBuffer) return Buffer.from(png).toString("base64");
|
|
519
|
+
return Buffer.from(png).toString("base64");
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function getPngByteLength(png: NativeScreenshot["png"]): number | undefined {
|
|
523
|
+
if (png === undefined) return undefined;
|
|
524
|
+
if (typeof png === "string") return Buffer.byteLength(png, "base64");
|
|
525
|
+
if (png instanceof ArrayBuffer) return png.byteLength;
|
|
526
|
+
return png.byteLength;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function mapComputerError(error: unknown, hotkey?: string): { code: string; message: string } {
|
|
530
|
+
if (error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError")) {
|
|
531
|
+
return {
|
|
532
|
+
code: "COMPUTER_CANCELLED",
|
|
533
|
+
message: `Computer action was cancelled. Stop and wait for the user${hotkey ? ` (kill-switch hotkey: ${hotkey})` : ""}.`,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
const maybe = error as { code?: unknown; message?: unknown };
|
|
537
|
+
const rawMessage =
|
|
538
|
+
typeof maybe?.message === "string" && maybe.message.length > 0 ? maybe.message : "Computer action failed.";
|
|
539
|
+
const rawCode = typeof maybe?.code === "string" ? maybe.code : undefined;
|
|
540
|
+
const isComputerCode = (value: string | undefined): value is string =>
|
|
541
|
+
value !== undefined && (NATIVE_ERROR_CODES.has(value) || value.startsWith("COMPUTER_"));
|
|
542
|
+
// Native NAPI errors carry the stable code in the message ("CODE: reason") with
|
|
543
|
+
// error.code set to the NAPI status, so fall back to the message prefix.
|
|
544
|
+
const messageCode = /^(COMPUTER_[A-Z_]+):/.exec(rawMessage)?.[1];
|
|
545
|
+
const code = isComputerCode(rawCode) ? rawCode : (messageCode ?? "COMPUTER_ERROR");
|
|
546
|
+
const reason = messageCode ? rawMessage.slice(messageCode.length + 1).trim() : rawMessage;
|
|
547
|
+
const recoveryHints: Record<string, string> = {
|
|
548
|
+
COMPUTER_COORD_INVALID: "Capture a fresh screenshot and use coordinates within its frame.",
|
|
549
|
+
COMPUTER_DISPLAY_STALE:
|
|
550
|
+
"Capture a fresh screenshot before acting; the display changed since the last screenshot.",
|
|
551
|
+
COMPUTER_SUPERVISOR_NOT_LIVE: `Stop and wait for the user${hotkey ? ` (kill-switch hotkey: ${hotkey})` : ""}.`,
|
|
552
|
+
COMPUTER_SUSPENDED: `Stop and wait for the user${hotkey ? ` (kill-switch hotkey: ${hotkey})` : ""}.`,
|
|
553
|
+
COMPUTER_CANCELLED: `Stop and wait for the user${hotkey ? ` (kill-switch hotkey: ${hotkey})` : ""}.`,
|
|
554
|
+
COMPUTER_PERMISSION_REQUIRED:
|
|
555
|
+
"The host needs screen-recording or accessibility permission. Ask the user to grant it.",
|
|
556
|
+
COMPUTER_DISABLED:
|
|
557
|
+
"The computer tool is disabled or unsupported. Do not retry without enabling it on Apple Silicon macOS.",
|
|
558
|
+
};
|
|
559
|
+
const hint = recoveryHints[code];
|
|
560
|
+
const message = hint ? `${code}: ${reason} ${hint}` : `${code}: ${reason}`;
|
|
561
|
+
return { code, message };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
interface ComputerAuditRecord {
|
|
565
|
+
timestamp: string;
|
|
566
|
+
action: ComputerActionName;
|
|
567
|
+
status: "success" | "error" | "disabled";
|
|
568
|
+
code?: string;
|
|
569
|
+
x?: number;
|
|
570
|
+
y?: number;
|
|
571
|
+
toX?: number;
|
|
572
|
+
toY?: number;
|
|
573
|
+
scrollX?: number;
|
|
574
|
+
scrollY?: number;
|
|
575
|
+
button?: string;
|
|
576
|
+
keys?: string[];
|
|
577
|
+
ms?: number;
|
|
578
|
+
screenshotWidthPx?: number;
|
|
579
|
+
screenshotHeightPx?: number;
|
|
580
|
+
message?: string;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function auditRecordFromDetails(details: ComputerToolDetails): ComputerAuditRecord {
|
|
584
|
+
const record: ComputerAuditRecord = {
|
|
585
|
+
timestamp: new Date().toISOString(),
|
|
586
|
+
action: details.action,
|
|
587
|
+
status: details.status,
|
|
588
|
+
};
|
|
589
|
+
if (details.code) record.code = details.code;
|
|
590
|
+
if (details.x !== undefined) record.x = details.x;
|
|
591
|
+
if (details.y !== undefined) record.y = details.y;
|
|
592
|
+
if (details.toX !== undefined) record.toX = details.toX;
|
|
593
|
+
if (details.toY !== undefined) record.toY = details.toY;
|
|
594
|
+
if (details.scrollX !== undefined) record.scrollX = details.scrollX;
|
|
595
|
+
if (details.scrollY !== undefined) record.scrollY = details.scrollY;
|
|
596
|
+
if (details.button) record.button = details.button;
|
|
597
|
+
if (details.keys) record.keys = details.keys;
|
|
598
|
+
if (details.ms !== undefined) record.ms = details.ms;
|
|
599
|
+
if (details.screenshot) {
|
|
600
|
+
record.screenshotWidthPx = details.screenshot.widthPx;
|
|
601
|
+
record.screenshotHeightPx = details.screenshot.heightPx;
|
|
602
|
+
}
|
|
603
|
+
if (details.message) record.message = details.message;
|
|
604
|
+
return record;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async function writeComputerAuditLog(session: ToolSession, details: ComputerToolDetails): Promise<void> {
|
|
608
|
+
if (!session.settings.get("computer.auditLog.enabled")) return;
|
|
609
|
+
const sessionFile = session.getSessionFile();
|
|
610
|
+
if (!sessionFile) return;
|
|
611
|
+
const auditPath = path.join(path.dirname(sessionFile), ".computer-audit.jsonl");
|
|
612
|
+
const record = auditRecordFromDetails(details);
|
|
613
|
+
if (details.steps) {
|
|
614
|
+
for (const step of details.steps) {
|
|
615
|
+
await writeComputerAuditLog(session, step);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
try {
|
|
619
|
+
await fs.appendFile(auditPath, `${JSON.stringify(record)}\n`, "utf8");
|
|
620
|
+
} catch {
|
|
621
|
+
// Audit logging is best-effort; do not let it fail the action.
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function describeComputerSuccess(details: ComputerToolDetails): string {
|
|
626
|
+
if (details.action === "batch" && details.steps) {
|
|
627
|
+
const successCount = details.steps.filter(s => s.status === "success").length;
|
|
628
|
+
const summary = `${successCount}/${details.steps.length} batch steps completed`;
|
|
629
|
+
if (details.screenshot) {
|
|
630
|
+
const location = details.screenshot.path ? `; saved ${details.screenshot.path}` : "";
|
|
631
|
+
return `Computer batch completed (${summary}; final screenshot ${details.screenshot.widthPx}x${details.screenshot.heightPx}${location}).`;
|
|
632
|
+
}
|
|
633
|
+
return `Computer batch completed (${summary}).`;
|
|
634
|
+
}
|
|
635
|
+
if (details.screenshot) {
|
|
636
|
+
const location = details.screenshot.path ? `; saved ${details.screenshot.path}` : "";
|
|
637
|
+
return `Computer ${details.action} completed (${details.screenshot.widthPx}x${details.screenshot.heightPx}${location}).`;
|
|
638
|
+
}
|
|
639
|
+
return `Computer ${details.action} completed.`;
|
|
640
|
+
}
|