@ai-agent-forge/plugin-desktop-windows-input 0.85.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.
package/README.md ADDED
@@ -0,0 +1,42 @@
1
+ # @agent-forge/plugin-desktop-windows-input
2
+
3
+ Bounded Windows desktop input capability plugin for [Agent Forge](https://github.com/agent-forge/agent-forge), extracted from the host's
4
+ `src/plugins/desktop` (D-075 S3). It registers `desktop_execute` (one focus/click/type/key/scroll action against an observed UIA target, always
5
+ followed by a fresh observation) and, only when the injected runtime exposes a replay method, `desktop_replay`. The shipped production facade
6
+ intentionally exposes no replay.
7
+
8
+ The plugin refuses to register any tool when the host runs in `readonly` mode. On non-Windows platforms importing the package does not throw;
9
+ tools report an explicit `unsupported` result instead of probing or falling back to a different driver.
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ # from a local checkout
15
+ agent-forge plugin install ./plugins/desktop-windows-input
16
+
17
+ # from npm
18
+ agent-forge plugin install @agent-forge/plugin-desktop-windows-input
19
+ ```
20
+
21
+ The package ships two manifests: `agent-forge.json` (ecosystem manifest for the deterministic plugin resolver, `platform.os: ["windows"]`) and
22
+ `plugin.json` (host-load manifest consumed by `loadCapabilityPluginFromManifest`, entry `./dist/entry.js`).
23
+
24
+ ## Library surface
25
+
26
+ `import { DesktopWindowsInputAdapter, WindowsUiAutomationDriver } from "@agent-forge/plugin-desktop-windows-input";`
27
+
28
+ - `DesktopWindowsInputAdapter` — production input facade (no replay method).
29
+ - `WindowsUiAutomationDriver(options?)` — PowerShell UIA driver; supports `command`/`timeoutMs`/`spawnSync`/`platform` injection for brokers and tests.
30
+ - `DesktopWindowsAdapter` — full-control adapter with replay, carried for host wiring parity with the observe package.
31
+ - `createDesktopInputCapabilityFactoryV1(runtime)` — registers `desktop_execute` (and `desktop_replay` when the runtime provides it) on a `CapabilityAPI`.
32
+ - Default export of `dist/entry.js` — the precomposed factory used by external plugin loading.
33
+
34
+ The UIA adapter code is a deliberate inlined copy shared with `@agent-forge/plugin-desktop-windows-observe` so both packages stay self-contained.
35
+
36
+ ## Development
37
+
38
+ ```sh
39
+ npm install --ignore-scripts # repo root, creates the workspace symlink
40
+ npm run build # plugins/desktop-windows-input (tsgo)
41
+ npm test # vitest; the entry fidelity test requires `npm run build` first
42
+ ```
@@ -0,0 +1,17 @@
1
+ {
2
+ "packageFormatVersion": 1,
3
+ "id": "agent-forge.plugin.desktop-windows-input",
4
+ "pluginVersion": "0.84.4",
5
+ "apiVersion": "1",
6
+ "minHostVersion": "0.84.4",
7
+ "capabilityRefs": [
8
+ { "id": "desktop-session", "version": 1 },
9
+ { "id": "desktop-uia", "version": 1 },
10
+ { "id": "desktop-input", "version": 1 },
11
+ { "id": "desktop-replay", "version": 1 }
12
+ ],
13
+ "platform": {
14
+ "os": ["windows"],
15
+ "runtime": [{ "name": "node" }]
16
+ }
17
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Bounded desktop input and replay capability factory, migrated from
3
+ * agent-forge `src/profile/runtime-capabilities-desktop-input.ts`
4
+ * (`desktopInputFactory`, D-075 S3 extraction). Differences from the host
5
+ * original:
6
+ * - The factory type is the public `CapabilityPluginSyncFactory` from the
7
+ * plugin SDK instead of the host-internal `BuiltinCapabilityPlugin`.
8
+ * - Projection helpers live in `projection.ts`, input helpers in `shared.ts`.
9
+ * Tool names, parameters, budgets and result shapes are unchanged, including
10
+ * the readonly-mode refusal to register input tools and the optional replay
11
+ * registration when the runtime exposes a replay method.
12
+ */
13
+ import type { CapabilityPluginSyncFactory } from "@agent-forge/plugin-sdk";
14
+ import type { DesktopFullControlAdapterV1 } from "./types.ts";
15
+ export declare function createDesktopInputCapabilityFactoryV1(runtime: DesktopFullControlAdapterV1): CapabilityPluginSyncFactory;
16
+ //# sourceMappingURL=capability.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,yBAAyB,CAAC;AAc3E,OAAO,KAAK,EACX,2BAA2B,EAI3B,MAAM,YAAY,CAAC;AA2SpB,wBAAgB,qCAAqC,CACpD,OAAO,EAAE,2BAA2B,GAClC,2BAA2B,CA0M7B","sourcesContent":["/**\n * Bounded desktop input and replay capability factory, migrated from\n * agent-forge `src/profile/runtime-capabilities-desktop-input.ts`\n * (`desktopInputFactory`, D-075 S3 extraction). Differences from the host\n * original:\n * - The factory type is the public `CapabilityPluginSyncFactory` from the\n * plugin SDK instead of the host-internal `BuiltinCapabilityPlugin`.\n * - Projection helpers live in `projection.ts`, input helpers in `shared.ts`.\n * Tool names, parameters, budgets and result shapes are unchanged, including\n * the readonly-mode refusal to register input tools and the optional replay\n * registration when the runtime exposes a replay method.\n */\nimport type { CapabilityPluginSyncFactory } from \"@agent-forge/plugin-sdk\";\nimport { Type } from \"typebox\";\nimport {\n\tDESKTOP_MAX_POSTCONDITIONS_V1,\n\tDESKTOP_MAX_REPLAY_ACTIONS_V1,\n\tDESKTOP_MAX_STRING_CHARS_V1,\n\tDESKTOP_MAX_TYPE_TEXT_CHARS_V1,\n\tdesktopBoundedString,\n\tdesktopFailure,\n\tdesktopRequiredString,\n\tdesktopResultWithinBudget,\n\tprojectDesktopObservation,\n} from \"./projection.ts\";\nimport { signalFrom, toolInvocation } from \"./shared.ts\";\nimport type {\n\tDesktopFullControlAdapterV1,\n\tDesktopOperationRequestV1,\n\tDesktopPostconditionV1,\n\tDesktopReplayRequestV1,\n} from \"./types.ts\";\n\nfunction desktopTarget(input: unknown): DesktopOperationRequestV1[\"action\"][\"target\"] {\n\tif (input === null || typeof input !== \"object\" || Array.isArray(input)) throw new Error(\"desktop-target-invalid\");\n\tconst target = input as Record<string, unknown>;\n\tif (target.kind === \"window\") {\n\t\tdesktopExactKeys(target, [\"kind\", \"windowId\"]);\n\t\treturn { kind: \"window\", windowId: desktopRequiredString(target.windowId, \"windowId\") };\n\t}\n\tif (target.kind === \"control\") {\n\t\tdesktopExactKeys(target, [\"kind\", \"controlId\", \"windowId\"]);\n\t\treturn {\n\t\t\tkind: \"control\",\n\t\t\tcontrolId: desktopRequiredString(target.controlId, \"controlId\"),\n\t\t\t...(target.windowId === undefined ? {} : { windowId: desktopRequiredString(target.windowId, \"windowId\") }),\n\t\t};\n\t}\n\tthrow new Error(\"desktop-target-invalid\");\n}\n\nfunction desktopExactKeys(value: Record<string, unknown>, allowed: readonly string[]): void {\n\tif (Object.keys(value).some((key) => !allowed.includes(key))) throw new Error(\"desktop-input-invalid\");\n}\n\nfunction desktopPostconditions(input: unknown): readonly DesktopPostconditionV1[] | undefined {\n\tif (input === undefined) return undefined;\n\tif (!Array.isArray(input) || input.length > DESKTOP_MAX_POSTCONDITIONS_V1)\n\t\tthrow new Error(\"desktop-postconditions-invalid\");\n\treturn input.map((entry) => {\n\t\tif (entry === null || typeof entry !== \"object\" || Array.isArray(entry))\n\t\t\tthrow new Error(\"desktop-postcondition-invalid\");\n\t\tconst value = entry as Record<string, unknown>;\n\t\tif (value.kind === \"window-exists\") {\n\t\t\tdesktopExactKeys(value, [\"kind\", \"windowId\"]);\n\t\t\treturn { kind: \"window-exists\", windowId: desktopRequiredString(value.windowId, \"windowId\") } as const;\n\t\t}\n\t\tif (value.kind === \"control-exists\") {\n\t\t\tdesktopExactKeys(value, [\"kind\", \"controlId\"]);\n\t\t\treturn { kind: \"control-exists\", controlId: desktopRequiredString(value.controlId, \"controlId\") } as const;\n\t\t}\n\t\tif (value.kind === \"focused\") {\n\t\t\tdesktopExactKeys(value, [\"kind\", \"controlId\"]);\n\t\t\treturn { kind: \"focused\", controlId: desktopRequiredString(value.controlId, \"controlId\") } as const;\n\t\t}\n\t\tif (value.kind === \"property-equals\") {\n\t\t\tdesktopExactKeys(value, [\"kind\", \"target\", \"property\", \"expected\"]);\n\t\t\tif (value.property !== \"enabled\" && value.property !== \"value\" && value.property !== \"name\")\n\t\t\t\tthrow new Error(\"desktop-postcondition-invalid\");\n\t\t\tif (\n\t\t\t\t(value.property === \"enabled\" && typeof value.expected !== \"boolean\") ||\n\t\t\t\t(value.property !== \"enabled\" && typeof value.expected !== \"string\")\n\t\t\t)\n\t\t\t\tthrow new Error(\"desktop-postcondition-invalid\");\n\t\t\treturn {\n\t\t\t\tkind: \"property-equals\",\n\t\t\t\ttarget: desktopTarget(value.target),\n\t\t\t\tproperty: value.property,\n\t\t\t\texpected:\n\t\t\t\t\ttypeof value.expected === \"string\"\n\t\t\t\t\t\t? desktopBoundedString(value.expected, \"postcondition expected\")\n\t\t\t\t\t\t: value.expected,\n\t\t\t} as DesktopPostconditionV1;\n\t\t}\n\t\tthrow new Error(\"desktop-postcondition-invalid\");\n\t});\n}\n\nfunction desktopExecuteRequest(input: Record<string, unknown>, operationId: string): DesktopOperationRequestV1 {\n\tdesktopExactKeys(input, [\"sessionId\", \"action\", \"postconditions\"]);\n\tconst actionInput = input.action;\n\tif (actionInput === null || typeof actionInput !== \"object\" || Array.isArray(actionInput))\n\t\tthrow new Error(\"desktop-action-invalid\");\n\tconst action = actionInput as Record<string, unknown>;\n\tdesktopExactKeys(action, [\"kind\", \"target\", \"text\", \"key\"]);\n\tif (\n\t\taction.kind !== \"focus\" &&\n\t\taction.kind !== \"click\" &&\n\t\taction.kind !== \"type\" &&\n\t\taction.kind !== \"key\" &&\n\t\taction.kind !== \"scroll\"\n\t)\n\t\tthrow new Error(\"desktop-action-invalid\");\n\tconst target = desktopTarget(action.target);\n\tconst text =\n\t\taction.kind === \"type\" || action.kind === \"scroll\" ? desktopBoundedString(action.text, \"text\") : undefined;\n\tif (text !== undefined && text.length > DESKTOP_MAX_TYPE_TEXT_CHARS_V1) throw new Error(\"desktop-type-text-budget\");\n\tconst key = action.kind === \"key\" ? desktopBoundedString(action.key, \"key\") : undefined;\n\tif (key !== undefined && key.length > DESKTOP_MAX_TYPE_TEXT_CHARS_V1) throw new Error(\"desktop-type-text-budget\");\n\tconst postconditions = desktopPostconditions(input.postconditions);\n\treturn {\n\t\tversion: 1,\n\t\toperationId,\n\t\tsessionId: desktopRequiredString(input.sessionId, \"sessionId\"),\n\t\taction: {\n\t\t\tversion: 1,\n\t\t\tkind: action.kind,\n\t\t\ttarget,\n\t\t\t...(text === undefined ? {} : { text }),\n\t\t\t...(key === undefined ? {} : { key }),\n\t\t},\n\t\t...(postconditions === undefined ? {} : { postconditions }),\n\t\t// Input plugin operations always request a fresh observation; callers may\n\t\t// not turn this off because target identity otherwise goes stale.\n\t\treobserveAfterAction: true,\n\t};\n}\n\nfunction desktopVerification(\n\tvalue: unknown,\n): { readonly status: \"passed\" | \"failed\" | \"unknown\"; readonly checks: readonly string[] } | undefined {\n\tif (value === undefined) return undefined;\n\tif (value === null || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"desktop-result-invalid\");\n\tconst verification = value as Record<string, unknown>;\n\tif (\n\t\t(verification.status !== \"passed\" && verification.status !== \"failed\" && verification.status !== \"unknown\") ||\n\t\t!Array.isArray(verification.checks) ||\n\t\tverification.checks.length > DESKTOP_MAX_POSTCONDITIONS_V1\n\t)\n\t\tthrow new Error(\"desktop-result-invalid\");\n\treturn {\n\t\tstatus: verification.status,\n\t\tchecks: verification.checks.map((check) => desktopBoundedString(check, \"verification check\")),\n\t};\n}\n\nfunction desktopExecuteResult(value: unknown, request: DesktopOperationRequestV1) {\n\tif (value === null || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"desktop-result-invalid\");\n\tconst result = value as Record<string, unknown>;\n\tif (\n\t\tresult.version !== 1 ||\n\t\tresult.operationId !== request.operationId ||\n\t\t(result.status !== \"succeeded\" &&\n\t\t\tresult.status !== \"failed\" &&\n\t\t\tresult.status !== \"cancelled\" &&\n\t\t\tresult.status !== \"needs_user\" &&\n\t\t\tresult.status !== \"unsupported\")\n\t)\n\t\tthrow new Error(\"desktop-result-invalid\");\n\tconst observation = result.observation === undefined ? undefined : projectDesktopObservation(result.observation);\n\tif (\n\t\tresult.status === \"succeeded\" &&\n\t\t(observation === undefined || observation.observation.sessionId !== request.sessionId)\n\t) {\n\t\treturn desktopFailure(\n\t\t\trequest.operationId,\n\t\t\t\"verification_unavailable\",\n\t\t\t\"Desktop action did not produce a fresh observation\",\n\t\t);\n\t}\n\tconst base = {\n\t\tversion: 1 as const,\n\t\toperationId: request.operationId,\n\t\tstatus: result.status,\n\t\tsummary: result.status === \"succeeded\" ? \"Desktop action completed\" : \"Desktop action could not be completed\",\n\t\t...(observation === undefined ? {} : observation),\n\t\t...(desktopVerification(result.verification) === undefined\n\t\t\t? {}\n\t\t\t: { verification: desktopVerification(result.verification) }),\n\t};\n\tif (result.status === \"succeeded\") return desktopResultWithinBudget(base);\n\tconst error = result.error;\n\tif (error === null || typeof error !== \"object\" || Array.isArray(error)) throw new Error(\"desktop-result-invalid\");\n\tconst errorValue = error as Record<string, unknown>;\n\tconst causeCode =\n\t\tresult.status === \"needs_user\" && typeof errorValue.causeCode === \"string\"\n\t\t\t? desktopBoundedString(errorValue.causeCode, \"error causeCode\")\n\t\t\t: undefined;\n\treturn desktopResultWithinBudget({\n\t\t...base,\n\t\terror: {\n\t\t\tversion: 1 as const,\n\t\t\tcode: result.status === \"needs_user\" ? \"needs_user\" : \"desktop_action_failed\",\n\t\t\tmessage: result.status === \"needs_user\" ? \"Desktop action requires user action\" : \"Desktop action failed\",\n\t\t\t...(causeCode === undefined ? {} : { causeCode }),\n\t\t},\n\t});\n}\n\nfunction desktopReplayActionInput(raw: unknown): DesktopOperationRequestV1[\"action\"] {\n\tif (raw === null || typeof raw !== \"object\" || Array.isArray(raw)) throw new Error(\"desktop-action-invalid\");\n\tconst action = raw as Record<string, unknown>;\n\tdesktopExactKeys(action, [\"kind\", \"target\", \"text\", \"key\"]);\n\tif (\n\t\taction.kind !== \"focus\" &&\n\t\taction.kind !== \"click\" &&\n\t\taction.kind !== \"type\" &&\n\t\taction.kind !== \"key\" &&\n\t\taction.kind !== \"scroll\"\n\t)\n\t\tthrow new Error(\"desktop-action-invalid\");\n\tconst target = desktopTarget(action.target);\n\tconst text =\n\t\taction.kind === \"type\" || action.kind === \"scroll\" ? desktopBoundedString(action.text, \"text\") : undefined;\n\tif (text !== undefined && text.length > DESKTOP_MAX_TYPE_TEXT_CHARS_V1) throw new Error(\"desktop-type-text-budget\");\n\tconst key = action.kind === \"key\" ? desktopBoundedString(action.key, \"key\") : undefined;\n\tif (key !== undefined && key.length > DESKTOP_MAX_TYPE_TEXT_CHARS_V1) throw new Error(\"desktop-type-text-budget\");\n\treturn {\n\t\tversion: 1,\n\t\tkind: action.kind,\n\t\ttarget,\n\t\t...(text === undefined ? {} : { text }),\n\t\t...(key === undefined ? {} : { key }),\n\t};\n}\n\nfunction desktopReplayRequest(input: Record<string, unknown>, operationId: string): DesktopReplayRequestV1 {\n\tdesktopExactKeys(input, [\"sessionId\", \"actions\", \"stopOnFailure\"]);\n\tif (\n\t\t!Array.isArray(input.actions) ||\n\t\tinput.actions.length === 0 ||\n\t\tinput.actions.length > DESKTOP_MAX_REPLAY_ACTIONS_V1\n\t)\n\t\tthrow new Error(\"desktop-replay-actions-invalid\");\n\tconst sessionId = desktopRequiredString(input.sessionId, \"sessionId\");\n\tconst operations = input.actions.map((raw, index) => {\n\t\tconst action = desktopReplayActionInput(raw);\n\t\treturn {\n\t\t\tversion: 1 as const,\n\t\t\toperationId: `${operationId}-${index}`,\n\t\t\tsessionId,\n\t\t\taction,\n\t\t\t// Replay always re-observes so per-action results carry fresh state.\n\t\t\treobserveAfterAction: true,\n\t\t} satisfies DesktopOperationRequestV1;\n\t});\n\treturn {\n\t\tversion: 1,\n\t\tsessionId,\n\t\toperations,\n\t\tstopOnFailure: input.stopOnFailure === undefined ? true : input.stopOnFailure === true,\n\t};\n}\n\nfunction desktopReplayResult(value: unknown, operationId: string): unknown {\n\tif (value === null || typeof value !== \"object\" || Array.isArray(value)) throw new Error(\"desktop-result-invalid\");\n\tconst result = value as Record<string, unknown>;\n\tif (\n\t\tresult.version !== 1 ||\n\t\t(result.status !== \"succeeded\" &&\n\t\t\tresult.status !== \"failed\" &&\n\t\t\tresult.status !== \"cancelled\" &&\n\t\t\tresult.status !== \"needs_user\") ||\n\t\t!Array.isArray(result.results)\n\t)\n\t\tthrow new Error(\"desktop-result-invalid\");\n\tconst results = result.results.map((entry) => {\n\t\tif (entry === null || typeof entry !== \"object\" || Array.isArray(entry))\n\t\t\tthrow new Error(\"desktop-result-invalid\");\n\t\tconst record = entry as Record<string, unknown>;\n\t\tif (\n\t\t\trecord.version !== 1 ||\n\t\t\ttypeof record.operationId !== \"string\" ||\n\t\t\t(record.status !== \"succeeded\" &&\n\t\t\t\trecord.status !== \"failed\" &&\n\t\t\t\trecord.status !== \"cancelled\" &&\n\t\t\t\trecord.status !== \"needs_user\" &&\n\t\t\t\trecord.status !== \"unsupported\")\n\t\t)\n\t\t\tthrow new Error(\"desktop-result-invalid\");\n\t\treturn {\n\t\t\toperationId: desktopBoundedString(record.operationId, \"replay operationId\"),\n\t\t\tstatus: record.status,\n\t\t\tsummary: desktopBoundedString(record.summary, \"replay summary\"),\n\t\t};\n\t});\n\tconst last = result.results[result.results.length - 1] as Record<string, unknown> | undefined;\n\tconst observation =\n\t\tlast?.observation === undefined ? undefined : projectDesktopObservation(last.observation).observation;\n\tconst needsUser = result.results.find((entry) => (entry as Record<string, unknown>).status === \"needs_user\") as\n\t\t| Record<string, unknown>\n\t\t| undefined;\n\tconst causeCode =\n\t\tneedsUser !== undefined &&\n\t\tneedsUser.error !== null &&\n\t\ttypeof needsUser.error === \"object\" &&\n\t\t\"causeCode\" in needsUser.error &&\n\t\ttypeof (needsUser.error as Record<string, unknown>).causeCode === \"string\"\n\t\t\t? desktopBoundedString((needsUser.error as Record<string, unknown>).causeCode, \"replay causeCode\")\n\t\t\t: undefined;\n\treturn desktopResultWithinBudget({\n\t\tversion: 1,\n\t\toperationId,\n\t\tstatus: result.status,\n\t\tsummary: result.status === \"succeeded\" ? \"Desktop replay completed\" : \"Desktop replay stopped before completion\",\n\t\tresults,\n\t\t...(observation === undefined ? {} : { observation }),\n\t\t...(result.status === \"needs_user\" && causeCode !== undefined\n\t\t\t? {\n\t\t\t\t\terror: {\n\t\t\t\t\t\tversion: 1 as const,\n\t\t\t\t\t\tcode: \"needs_user\",\n\t\t\t\t\t\tmessage: \"Desktop replay requires user action\",\n\t\t\t\t\t\tcauseCode,\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t: {}),\n\t});\n}\n\nexport function createDesktopInputCapabilityFactoryV1(\n\truntime: DesktopFullControlAdapterV1,\n): CapabilityPluginSyncFactory {\n\treturn (api) => {\n\t\tif (api.host.mode === \"readonly\") return;\n\t\tapi.registerTool({\n\t\t\tname: \"desktop_execute\",\n\t\t\tlabel: \"Execute desktop action\",\n\t\t\tdescription:\n\t\t\t\t\"Execute one bounded UI Automation focus, click, type, key, or scroll action against an observed target and return a fresh bounded observation\",\n\t\t\tparameters: Type.Object(\n\t\t\t\t{\n\t\t\t\t\tsessionId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\taction: Type.Object(\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tkind: Type.Union([\n\t\t\t\t\t\t\t\tType.Literal(\"focus\"),\n\t\t\t\t\t\t\t\tType.Literal(\"click\"),\n\t\t\t\t\t\t\t\tType.Literal(\"type\"),\n\t\t\t\t\t\t\t\tType.Literal(\"key\"),\n\t\t\t\t\t\t\t\tType.Literal(\"scroll\"),\n\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t\ttarget: Type.Union([\n\t\t\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkind: Type.Literal(\"window\"),\n\t\t\t\t\t\t\t\t\t\twindowId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkind: Type.Literal(\"control\"),\n\t\t\t\t\t\t\t\t\t\tcontrolId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t\twindowId: Type.Optional(\n\t\t\t\t\t\t\t\t\t\t\tType.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t\ttext: Type.Optional(Type.String({ maxLength: DESKTOP_MAX_TYPE_TEXT_CHARS_V1 })),\n\t\t\t\t\t\t\tkey: Type.Optional(Type.String({ minLength: 1, maxLength: DESKTOP_MAX_TYPE_TEXT_CHARS_V1 })),\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t),\n\t\t\t\t\tpostconditions: Type.Optional(\n\t\t\t\t\t\tType.Array(\n\t\t\t\t\t\t\tType.Union([\n\t\t\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkind: Type.Literal(\"window-exists\"),\n\t\t\t\t\t\t\t\t\t\twindowId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkind: Type.Literal(\"control-exists\"),\n\t\t\t\t\t\t\t\t\t\tcontrolId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkind: Type.Literal(\"focused\"),\n\t\t\t\t\t\t\t\t\t\tcontrolId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tkind: Type.Literal(\"property-equals\"),\n\t\t\t\t\t\t\t\t\t\ttarget: Type.Union([\n\t\t\t\t\t\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\tkind: Type.Literal(\"window\"),\n\t\t\t\t\t\t\t\t\t\t\t\t\twindowId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\t\t\tkind: Type.Literal(\"control\"),\n\t\t\t\t\t\t\t\t\t\t\t\t\tcontrolId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t\t\t\t\twindowId: Type.Optional(\n\t\t\t\t\t\t\t\t\t\t\t\t\t\tType.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t\t\t\t\tproperty: Type.Union([\n\t\t\t\t\t\t\t\t\t\t\tType.Literal(\"enabled\"),\n\t\t\t\t\t\t\t\t\t\t\tType.Literal(\"value\"),\n\t\t\t\t\t\t\t\t\t\t\tType.Literal(\"name\"),\n\t\t\t\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t\t\t\t\texpected: Type.Union([\n\t\t\t\t\t\t\t\t\t\t\tType.Boolean(),\n\t\t\t\t\t\t\t\t\t\t\tType.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t\t{ maxItems: DESKTOP_MAX_POSTCONDITIONS_V1 },\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t},\n\t\t\t\t{ additionalProperties: false },\n\t\t\t),\n\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\tconst { input, operationId } = toolInvocation(first, second);\n\t\t\t\tconst signal = signalFrom(third);\n\t\t\t\tif (signal?.aborted)\n\t\t\t\t\treturn desktopFailure(operationId, \"cancelled\", \"Desktop operation was cancelled\", \"cancelled\");\n\t\t\t\tlet request: DesktopOperationRequestV1;\n\t\t\t\ttry {\n\t\t\t\t\trequest = desktopExecuteRequest(input, operationId);\n\t\t\t\t} catch {\n\t\t\t\t\treturn desktopFailure(operationId, \"desktop_input_invalid\", \"Desktop action input is invalid\");\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tconst result = runtime.execute(request);\n\t\t\t\t\treturn signal?.aborted\n\t\t\t\t\t\t? desktopFailure(operationId, \"cancelled\", \"Desktop operation was cancelled\", \"cancelled\")\n\t\t\t\t\t\t: desktopExecuteResult(result, request);\n\t\t\t\t} catch {\n\t\t\t\t\treturn desktopFailure(operationId, \"desktop_adapter_failed\", \"The selected desktop adapter failed\");\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t\tconst replay = runtime.replay;\n\t\tif (replay === undefined) return;\n\t\tapi.registerTool({\n\t\t\tname: \"desktop_replay\",\n\t\t\tlabel: \"Replay desktop actions\",\n\t\t\tdescription:\n\t\t\t\t\"Replay a bounded sequence of desktop actions against one session; stops at the first failure, cancellation, or user-confirmation pause by default\",\n\t\t\tparameters: Type.Object(\n\t\t\t\t{\n\t\t\t\t\tsessionId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\tactions: Type.Array(\n\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tkind: Type.Union([\n\t\t\t\t\t\t\t\t\tType.Literal(\"focus\"),\n\t\t\t\t\t\t\t\t\tType.Literal(\"click\"),\n\t\t\t\t\t\t\t\t\tType.Literal(\"type\"),\n\t\t\t\t\t\t\t\t\tType.Literal(\"key\"),\n\t\t\t\t\t\t\t\t\tType.Literal(\"scroll\"),\n\t\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t\t\ttarget: Type.Union([\n\t\t\t\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tkind: Type.Literal(\"window\"),\n\t\t\t\t\t\t\t\t\t\t\twindowId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\tType.Object(\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tkind: Type.Literal(\"control\"),\n\t\t\t\t\t\t\t\t\t\t\tcontrolId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t\t\twindowId: Type.Optional(\n\t\t\t\t\t\t\t\t\t\t\t\tType.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),\n\t\t\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t\t]),\n\t\t\t\t\t\t\t\ttext: Type.Optional(Type.String({ maxLength: DESKTOP_MAX_TYPE_TEXT_CHARS_V1 })),\n\t\t\t\t\t\t\t\tkey: Type.Optional(Type.String({ minLength: 1, maxLength: DESKTOP_MAX_TYPE_TEXT_CHARS_V1 })),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{ additionalProperties: false },\n\t\t\t\t\t\t),\n\t\t\t\t\t\t{ minItems: 1, maxItems: DESKTOP_MAX_REPLAY_ACTIONS_V1 },\n\t\t\t\t\t),\n\t\t\t\t\tstopOnFailure: Type.Optional(Type.Boolean()),\n\t\t\t\t},\n\t\t\t\t{ additionalProperties: false },\n\t\t\t),\n\t\t\texecute: async (first: unknown, second?: unknown, third?: unknown) => {\n\t\t\t\tconst { input, operationId } = toolInvocation(first, second);\n\t\t\t\tconst signal = signalFrom(third);\n\t\t\t\tif (signal?.aborted)\n\t\t\t\t\treturn desktopFailure(operationId, \"cancelled\", \"Desktop operation was cancelled\", \"cancelled\");\n\t\t\t\tlet request: DesktopReplayRequestV1;\n\t\t\t\ttry {\n\t\t\t\t\trequest = desktopReplayRequest(input, operationId);\n\t\t\t\t} catch {\n\t\t\t\t\treturn desktopFailure(operationId, \"desktop_input_invalid\", \"Desktop replay input is invalid\");\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tconst result = replay(request);\n\t\t\t\t\treturn signal?.aborted\n\t\t\t\t\t\t? desktopFailure(operationId, \"cancelled\", \"Desktop operation was cancelled\", \"cancelled\")\n\t\t\t\t\t\t: desktopReplayResult(result, operationId);\n\t\t\t\t} catch {\n\t\t\t\t\treturn desktopFailure(operationId, \"desktop_adapter_failed\", \"The selected desktop adapter failed\");\n\t\t\t\t}\n\t\t\t},\n\t\t});\n\t};\n}\n"]}
@@ -0,0 +1,425 @@
1
+ import { Type } from "typebox";
2
+ import { DESKTOP_MAX_POSTCONDITIONS_V1, DESKTOP_MAX_REPLAY_ACTIONS_V1, DESKTOP_MAX_STRING_CHARS_V1, DESKTOP_MAX_TYPE_TEXT_CHARS_V1, desktopBoundedString, desktopFailure, desktopRequiredString, desktopResultWithinBudget, projectDesktopObservation, } from "./projection.js";
3
+ import { signalFrom, toolInvocation } from "./shared.js";
4
+ function desktopTarget(input) {
5
+ if (input === null || typeof input !== "object" || Array.isArray(input))
6
+ throw new Error("desktop-target-invalid");
7
+ const target = input;
8
+ if (target.kind === "window") {
9
+ desktopExactKeys(target, ["kind", "windowId"]);
10
+ return { kind: "window", windowId: desktopRequiredString(target.windowId, "windowId") };
11
+ }
12
+ if (target.kind === "control") {
13
+ desktopExactKeys(target, ["kind", "controlId", "windowId"]);
14
+ return {
15
+ kind: "control",
16
+ controlId: desktopRequiredString(target.controlId, "controlId"),
17
+ ...(target.windowId === undefined ? {} : { windowId: desktopRequiredString(target.windowId, "windowId") }),
18
+ };
19
+ }
20
+ throw new Error("desktop-target-invalid");
21
+ }
22
+ function desktopExactKeys(value, allowed) {
23
+ if (Object.keys(value).some((key) => !allowed.includes(key)))
24
+ throw new Error("desktop-input-invalid");
25
+ }
26
+ function desktopPostconditions(input) {
27
+ if (input === undefined)
28
+ return undefined;
29
+ if (!Array.isArray(input) || input.length > DESKTOP_MAX_POSTCONDITIONS_V1)
30
+ throw new Error("desktop-postconditions-invalid");
31
+ return input.map((entry) => {
32
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry))
33
+ throw new Error("desktop-postcondition-invalid");
34
+ const value = entry;
35
+ if (value.kind === "window-exists") {
36
+ desktopExactKeys(value, ["kind", "windowId"]);
37
+ return { kind: "window-exists", windowId: desktopRequiredString(value.windowId, "windowId") };
38
+ }
39
+ if (value.kind === "control-exists") {
40
+ desktopExactKeys(value, ["kind", "controlId"]);
41
+ return { kind: "control-exists", controlId: desktopRequiredString(value.controlId, "controlId") };
42
+ }
43
+ if (value.kind === "focused") {
44
+ desktopExactKeys(value, ["kind", "controlId"]);
45
+ return { kind: "focused", controlId: desktopRequiredString(value.controlId, "controlId") };
46
+ }
47
+ if (value.kind === "property-equals") {
48
+ desktopExactKeys(value, ["kind", "target", "property", "expected"]);
49
+ if (value.property !== "enabled" && value.property !== "value" && value.property !== "name")
50
+ throw new Error("desktop-postcondition-invalid");
51
+ if ((value.property === "enabled" && typeof value.expected !== "boolean") ||
52
+ (value.property !== "enabled" && typeof value.expected !== "string"))
53
+ throw new Error("desktop-postcondition-invalid");
54
+ return {
55
+ kind: "property-equals",
56
+ target: desktopTarget(value.target),
57
+ property: value.property,
58
+ expected: typeof value.expected === "string"
59
+ ? desktopBoundedString(value.expected, "postcondition expected")
60
+ : value.expected,
61
+ };
62
+ }
63
+ throw new Error("desktop-postcondition-invalid");
64
+ });
65
+ }
66
+ function desktopExecuteRequest(input, operationId) {
67
+ desktopExactKeys(input, ["sessionId", "action", "postconditions"]);
68
+ const actionInput = input.action;
69
+ if (actionInput === null || typeof actionInput !== "object" || Array.isArray(actionInput))
70
+ throw new Error("desktop-action-invalid");
71
+ const action = actionInput;
72
+ desktopExactKeys(action, ["kind", "target", "text", "key"]);
73
+ if (action.kind !== "focus" &&
74
+ action.kind !== "click" &&
75
+ action.kind !== "type" &&
76
+ action.kind !== "key" &&
77
+ action.kind !== "scroll")
78
+ throw new Error("desktop-action-invalid");
79
+ const target = desktopTarget(action.target);
80
+ const text = action.kind === "type" || action.kind === "scroll" ? desktopBoundedString(action.text, "text") : undefined;
81
+ if (text !== undefined && text.length > DESKTOP_MAX_TYPE_TEXT_CHARS_V1)
82
+ throw new Error("desktop-type-text-budget");
83
+ const key = action.kind === "key" ? desktopBoundedString(action.key, "key") : undefined;
84
+ if (key !== undefined && key.length > DESKTOP_MAX_TYPE_TEXT_CHARS_V1)
85
+ throw new Error("desktop-type-text-budget");
86
+ const postconditions = desktopPostconditions(input.postconditions);
87
+ return {
88
+ version: 1,
89
+ operationId,
90
+ sessionId: desktopRequiredString(input.sessionId, "sessionId"),
91
+ action: {
92
+ version: 1,
93
+ kind: action.kind,
94
+ target,
95
+ ...(text === undefined ? {} : { text }),
96
+ ...(key === undefined ? {} : { key }),
97
+ },
98
+ ...(postconditions === undefined ? {} : { postconditions }),
99
+ // Input plugin operations always request a fresh observation; callers may
100
+ // not turn this off because target identity otherwise goes stale.
101
+ reobserveAfterAction: true,
102
+ };
103
+ }
104
+ function desktopVerification(value) {
105
+ if (value === undefined)
106
+ return undefined;
107
+ if (value === null || typeof value !== "object" || Array.isArray(value))
108
+ throw new Error("desktop-result-invalid");
109
+ const verification = value;
110
+ if ((verification.status !== "passed" && verification.status !== "failed" && verification.status !== "unknown") ||
111
+ !Array.isArray(verification.checks) ||
112
+ verification.checks.length > DESKTOP_MAX_POSTCONDITIONS_V1)
113
+ throw new Error("desktop-result-invalid");
114
+ return {
115
+ status: verification.status,
116
+ checks: verification.checks.map((check) => desktopBoundedString(check, "verification check")),
117
+ };
118
+ }
119
+ function desktopExecuteResult(value, request) {
120
+ if (value === null || typeof value !== "object" || Array.isArray(value))
121
+ throw new Error("desktop-result-invalid");
122
+ const result = value;
123
+ if (result.version !== 1 ||
124
+ result.operationId !== request.operationId ||
125
+ (result.status !== "succeeded" &&
126
+ result.status !== "failed" &&
127
+ result.status !== "cancelled" &&
128
+ result.status !== "needs_user" &&
129
+ result.status !== "unsupported"))
130
+ throw new Error("desktop-result-invalid");
131
+ const observation = result.observation === undefined ? undefined : projectDesktopObservation(result.observation);
132
+ if (result.status === "succeeded" &&
133
+ (observation === undefined || observation.observation.sessionId !== request.sessionId)) {
134
+ return desktopFailure(request.operationId, "verification_unavailable", "Desktop action did not produce a fresh observation");
135
+ }
136
+ const base = {
137
+ version: 1,
138
+ operationId: request.operationId,
139
+ status: result.status,
140
+ summary: result.status === "succeeded" ? "Desktop action completed" : "Desktop action could not be completed",
141
+ ...(observation === undefined ? {} : observation),
142
+ ...(desktopVerification(result.verification) === undefined
143
+ ? {}
144
+ : { verification: desktopVerification(result.verification) }),
145
+ };
146
+ if (result.status === "succeeded")
147
+ return desktopResultWithinBudget(base);
148
+ const error = result.error;
149
+ if (error === null || typeof error !== "object" || Array.isArray(error))
150
+ throw new Error("desktop-result-invalid");
151
+ const errorValue = error;
152
+ const causeCode = result.status === "needs_user" && typeof errorValue.causeCode === "string"
153
+ ? desktopBoundedString(errorValue.causeCode, "error causeCode")
154
+ : undefined;
155
+ return desktopResultWithinBudget({
156
+ ...base,
157
+ error: {
158
+ version: 1,
159
+ code: result.status === "needs_user" ? "needs_user" : "desktop_action_failed",
160
+ message: result.status === "needs_user" ? "Desktop action requires user action" : "Desktop action failed",
161
+ ...(causeCode === undefined ? {} : { causeCode }),
162
+ },
163
+ });
164
+ }
165
+ function desktopReplayActionInput(raw) {
166
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
167
+ throw new Error("desktop-action-invalid");
168
+ const action = raw;
169
+ desktopExactKeys(action, ["kind", "target", "text", "key"]);
170
+ if (action.kind !== "focus" &&
171
+ action.kind !== "click" &&
172
+ action.kind !== "type" &&
173
+ action.kind !== "key" &&
174
+ action.kind !== "scroll")
175
+ throw new Error("desktop-action-invalid");
176
+ const target = desktopTarget(action.target);
177
+ const text = action.kind === "type" || action.kind === "scroll" ? desktopBoundedString(action.text, "text") : undefined;
178
+ if (text !== undefined && text.length > DESKTOP_MAX_TYPE_TEXT_CHARS_V1)
179
+ throw new Error("desktop-type-text-budget");
180
+ const key = action.kind === "key" ? desktopBoundedString(action.key, "key") : undefined;
181
+ if (key !== undefined && key.length > DESKTOP_MAX_TYPE_TEXT_CHARS_V1)
182
+ throw new Error("desktop-type-text-budget");
183
+ return {
184
+ version: 1,
185
+ kind: action.kind,
186
+ target,
187
+ ...(text === undefined ? {} : { text }),
188
+ ...(key === undefined ? {} : { key }),
189
+ };
190
+ }
191
+ function desktopReplayRequest(input, operationId) {
192
+ desktopExactKeys(input, ["sessionId", "actions", "stopOnFailure"]);
193
+ if (!Array.isArray(input.actions) ||
194
+ input.actions.length === 0 ||
195
+ input.actions.length > DESKTOP_MAX_REPLAY_ACTIONS_V1)
196
+ throw new Error("desktop-replay-actions-invalid");
197
+ const sessionId = desktopRequiredString(input.sessionId, "sessionId");
198
+ const operations = input.actions.map((raw, index) => {
199
+ const action = desktopReplayActionInput(raw);
200
+ return {
201
+ version: 1,
202
+ operationId: `${operationId}-${index}`,
203
+ sessionId,
204
+ action,
205
+ // Replay always re-observes so per-action results carry fresh state.
206
+ reobserveAfterAction: true,
207
+ };
208
+ });
209
+ return {
210
+ version: 1,
211
+ sessionId,
212
+ operations,
213
+ stopOnFailure: input.stopOnFailure === undefined ? true : input.stopOnFailure === true,
214
+ };
215
+ }
216
+ function desktopReplayResult(value, operationId) {
217
+ if (value === null || typeof value !== "object" || Array.isArray(value))
218
+ throw new Error("desktop-result-invalid");
219
+ const result = value;
220
+ if (result.version !== 1 ||
221
+ (result.status !== "succeeded" &&
222
+ result.status !== "failed" &&
223
+ result.status !== "cancelled" &&
224
+ result.status !== "needs_user") ||
225
+ !Array.isArray(result.results))
226
+ throw new Error("desktop-result-invalid");
227
+ const results = result.results.map((entry) => {
228
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry))
229
+ throw new Error("desktop-result-invalid");
230
+ const record = entry;
231
+ if (record.version !== 1 ||
232
+ typeof record.operationId !== "string" ||
233
+ (record.status !== "succeeded" &&
234
+ record.status !== "failed" &&
235
+ record.status !== "cancelled" &&
236
+ record.status !== "needs_user" &&
237
+ record.status !== "unsupported"))
238
+ throw new Error("desktop-result-invalid");
239
+ return {
240
+ operationId: desktopBoundedString(record.operationId, "replay operationId"),
241
+ status: record.status,
242
+ summary: desktopBoundedString(record.summary, "replay summary"),
243
+ };
244
+ });
245
+ const last = result.results[result.results.length - 1];
246
+ const observation = last?.observation === undefined ? undefined : projectDesktopObservation(last.observation).observation;
247
+ const needsUser = result.results.find((entry) => entry.status === "needs_user");
248
+ const causeCode = needsUser !== undefined &&
249
+ needsUser.error !== null &&
250
+ typeof needsUser.error === "object" &&
251
+ "causeCode" in needsUser.error &&
252
+ typeof needsUser.error.causeCode === "string"
253
+ ? desktopBoundedString(needsUser.error.causeCode, "replay causeCode")
254
+ : undefined;
255
+ return desktopResultWithinBudget({
256
+ version: 1,
257
+ operationId,
258
+ status: result.status,
259
+ summary: result.status === "succeeded" ? "Desktop replay completed" : "Desktop replay stopped before completion",
260
+ results,
261
+ ...(observation === undefined ? {} : { observation }),
262
+ ...(result.status === "needs_user" && causeCode !== undefined
263
+ ? {
264
+ error: {
265
+ version: 1,
266
+ code: "needs_user",
267
+ message: "Desktop replay requires user action",
268
+ causeCode,
269
+ },
270
+ }
271
+ : {}),
272
+ });
273
+ }
274
+ export function createDesktopInputCapabilityFactoryV1(runtime) {
275
+ return (api) => {
276
+ if (api.host.mode === "readonly")
277
+ return;
278
+ api.registerTool({
279
+ name: "desktop_execute",
280
+ label: "Execute desktop action",
281
+ description: "Execute one bounded UI Automation focus, click, type, key, or scroll action against an observed target and return a fresh bounded observation",
282
+ parameters: Type.Object({
283
+ sessionId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
284
+ action: Type.Object({
285
+ kind: Type.Union([
286
+ Type.Literal("focus"),
287
+ Type.Literal("click"),
288
+ Type.Literal("type"),
289
+ Type.Literal("key"),
290
+ Type.Literal("scroll"),
291
+ ]),
292
+ target: Type.Union([
293
+ Type.Object({
294
+ kind: Type.Literal("window"),
295
+ windowId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
296
+ }, { additionalProperties: false }),
297
+ Type.Object({
298
+ kind: Type.Literal("control"),
299
+ controlId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
300
+ windowId: Type.Optional(Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 })),
301
+ }, { additionalProperties: false }),
302
+ ]),
303
+ text: Type.Optional(Type.String({ maxLength: DESKTOP_MAX_TYPE_TEXT_CHARS_V1 })),
304
+ key: Type.Optional(Type.String({ minLength: 1, maxLength: DESKTOP_MAX_TYPE_TEXT_CHARS_V1 })),
305
+ }, { additionalProperties: false }),
306
+ postconditions: Type.Optional(Type.Array(Type.Union([
307
+ Type.Object({
308
+ kind: Type.Literal("window-exists"),
309
+ windowId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
310
+ }, { additionalProperties: false }),
311
+ Type.Object({
312
+ kind: Type.Literal("control-exists"),
313
+ controlId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
314
+ }, { additionalProperties: false }),
315
+ Type.Object({
316
+ kind: Type.Literal("focused"),
317
+ controlId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
318
+ }, { additionalProperties: false }),
319
+ Type.Object({
320
+ kind: Type.Literal("property-equals"),
321
+ target: Type.Union([
322
+ Type.Object({
323
+ kind: Type.Literal("window"),
324
+ windowId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
325
+ }, { additionalProperties: false }),
326
+ Type.Object({
327
+ kind: Type.Literal("control"),
328
+ controlId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
329
+ windowId: Type.Optional(Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 })),
330
+ }, { additionalProperties: false }),
331
+ ]),
332
+ property: Type.Union([
333
+ Type.Literal("enabled"),
334
+ Type.Literal("value"),
335
+ Type.Literal("name"),
336
+ ]),
337
+ expected: Type.Union([
338
+ Type.Boolean(),
339
+ Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
340
+ ]),
341
+ }, { additionalProperties: false }),
342
+ ]), { maxItems: DESKTOP_MAX_POSTCONDITIONS_V1 })),
343
+ }, { additionalProperties: false }),
344
+ execute: async (first, second, third) => {
345
+ const { input, operationId } = toolInvocation(first, second);
346
+ const signal = signalFrom(third);
347
+ if (signal?.aborted)
348
+ return desktopFailure(operationId, "cancelled", "Desktop operation was cancelled", "cancelled");
349
+ let request;
350
+ try {
351
+ request = desktopExecuteRequest(input, operationId);
352
+ }
353
+ catch {
354
+ return desktopFailure(operationId, "desktop_input_invalid", "Desktop action input is invalid");
355
+ }
356
+ try {
357
+ const result = runtime.execute(request);
358
+ return signal?.aborted
359
+ ? desktopFailure(operationId, "cancelled", "Desktop operation was cancelled", "cancelled")
360
+ : desktopExecuteResult(result, request);
361
+ }
362
+ catch {
363
+ return desktopFailure(operationId, "desktop_adapter_failed", "The selected desktop adapter failed");
364
+ }
365
+ },
366
+ });
367
+ const replay = runtime.replay;
368
+ if (replay === undefined)
369
+ return;
370
+ api.registerTool({
371
+ name: "desktop_replay",
372
+ label: "Replay desktop actions",
373
+ description: "Replay a bounded sequence of desktop actions against one session; stops at the first failure, cancellation, or user-confirmation pause by default",
374
+ parameters: Type.Object({
375
+ sessionId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
376
+ actions: Type.Array(Type.Object({
377
+ kind: Type.Union([
378
+ Type.Literal("focus"),
379
+ Type.Literal("click"),
380
+ Type.Literal("type"),
381
+ Type.Literal("key"),
382
+ Type.Literal("scroll"),
383
+ ]),
384
+ target: Type.Union([
385
+ Type.Object({
386
+ kind: Type.Literal("window"),
387
+ windowId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
388
+ }, { additionalProperties: false }),
389
+ Type.Object({
390
+ kind: Type.Literal("control"),
391
+ controlId: Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 }),
392
+ windowId: Type.Optional(Type.String({ minLength: 1, maxLength: DESKTOP_MAX_STRING_CHARS_V1 })),
393
+ }, { additionalProperties: false }),
394
+ ]),
395
+ text: Type.Optional(Type.String({ maxLength: DESKTOP_MAX_TYPE_TEXT_CHARS_V1 })),
396
+ key: Type.Optional(Type.String({ minLength: 1, maxLength: DESKTOP_MAX_TYPE_TEXT_CHARS_V1 })),
397
+ }, { additionalProperties: false }), { minItems: 1, maxItems: DESKTOP_MAX_REPLAY_ACTIONS_V1 }),
398
+ stopOnFailure: Type.Optional(Type.Boolean()),
399
+ }, { additionalProperties: false }),
400
+ execute: async (first, second, third) => {
401
+ const { input, operationId } = toolInvocation(first, second);
402
+ const signal = signalFrom(third);
403
+ if (signal?.aborted)
404
+ return desktopFailure(operationId, "cancelled", "Desktop operation was cancelled", "cancelled");
405
+ let request;
406
+ try {
407
+ request = desktopReplayRequest(input, operationId);
408
+ }
409
+ catch {
410
+ return desktopFailure(operationId, "desktop_input_invalid", "Desktop replay input is invalid");
411
+ }
412
+ try {
413
+ const result = replay(request);
414
+ return signal?.aborted
415
+ ? desktopFailure(operationId, "cancelled", "Desktop operation was cancelled", "cancelled")
416
+ : desktopReplayResult(result, operationId);
417
+ }
418
+ catch {
419
+ return desktopFailure(operationId, "desktop_adapter_failed", "The selected desktop adapter failed");
420
+ }
421
+ },
422
+ });
423
+ };
424
+ }
425
+ //# sourceMappingURL=capability.js.map