@ycheung5/pi-computer-use 0.0.0-stage → 0.1.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/GLOSSARY.md +38 -0
- package/LICENSE +21 -0
- package/README.md +165 -2
- package/assets/accessibility.cs +208 -0
- package/assets/browser-indicators.cs +37 -0
- package/assets/capture.cs +215 -0
- package/assets/clipboard.cs +197 -0
- package/assets/controls.cs +249 -0
- package/assets/indicator-preview.png +0 -0
- package/assets/indicators.cs +252 -0
- package/assets/node-LICENSE.txt +2732 -0
- package/assets/ocr.cs +86 -0
- package/assets/ownership.cs +59 -0
- package/assets/pointer.cs +229 -0
- package/assets/puppeteer-LICENSE.txt +202 -0
- package/assets/readme-banner.svg +33 -0
- package/assets/stop.cs +197 -0
- package/assets/text.cs +249 -0
- package/assets/wgc.cs +167 -0
- package/assets/window-management.cs +217 -0
- package/assets/window-marker.cs +23 -0
- package/assets/window-references.cs +80 -0
- package/assets/windows.cs +127 -0
- package/dist/accessibility.d.ts +29 -0
- package/dist/accessibility.js +258 -0
- package/dist/browser-actions.d.ts +21 -0
- package/dist/browser-actions.js +213 -0
- package/dist/browser-indicators.d.ts +23 -0
- package/dist/browser-indicators.js +69 -0
- package/dist/browser-profiles.d.ts +10 -0
- package/dist/browser-profiles.js +174 -0
- package/dist/browser.d.ts +11 -0
- package/dist/browser.js +2071 -0
- package/dist/capture.d.ts +74 -0
- package/dist/capture.js +199 -0
- package/dist/client.d.ts +38 -0
- package/dist/client.js +331 -0
- package/dist/desktop.d.ts +20 -0
- package/dist/desktop.js +42 -0
- package/dist/extension.d.ts +4 -0
- package/dist/extension.js +409 -0
- package/dist/launcher.d.ts +6 -0
- package/dist/launcher.js +85 -0
- package/dist/native.d.ts +11 -0
- package/dist/native.js +145 -0
- package/dist/ocr.d.ts +52 -0
- package/dist/ocr.js +246 -0
- package/dist/ownership.d.ts +4 -0
- package/dist/ownership.js +24 -0
- package/dist/pointer.d.ts +108 -0
- package/dist/pointer.js +174 -0
- package/dist/protocol.d.ts +15 -0
- package/dist/protocol.js +1 -0
- package/dist/runtime.d.ts +8 -0
- package/dist/runtime.js +72 -0
- package/dist/settings.d.ts +85 -0
- package/dist/settings.js +449 -0
- package/dist/stop.d.ts +24 -0
- package/dist/stop.js +156 -0
- package/dist/text.d.ts +100 -0
- package/dist/text.js +183 -0
- package/dist/waits.d.ts +19 -0
- package/dist/waits.js +262 -0
- package/dist/window-management.d.ts +25 -0
- package/dist/window-management.js +167 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +540 -0
- package/docs/README.md +54 -0
- package/docs/adr/0001-preserve-reference-behavior.md +9 -0
- package/docs/adr/0002-window-lifetime-references.md +11 -0
- package/docs/adr/0003-exclusive-desktop-control.md +11 -0
- package/docs/adr/0004-pi-first-acceptance.md +9 -0
- package/docs/adr/0005-default-apps-and-workspace-reuse.md +7 -0
- package/docs/agents/domain.md +9 -0
- package/docs/agents/issue-tracker.md +10 -0
- package/docs/agents/triage-labels.md +9 -0
- package/docs/browser.md +113 -0
- package/docs/implementation-plan.md +139 -0
- package/docs/indicators.md +53 -0
- package/docs/native-interop-research.md +17 -0
- package/docs/ownership.md +25 -0
- package/docs/pi-extension-research.md +57 -0
- package/docs/pointer.md +65 -0
- package/docs/runtime.md +42 -0
- package/docs/settings.md +33 -0
- package/docs/spec.md +161 -0
- package/docs/stopping.md +83 -0
- package/docs/text.md +79 -0
- package/docs/tickets.md +84 -0
- package/docs/validation.md +1372 -0
- package/docs/windows.md +171 -0
- package/package.json +134 -4
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { WorkerResult } from "./protocol.js";
|
|
2
|
+
export declare const screenshotParameters: {
|
|
3
|
+
type: string;
|
|
4
|
+
additionalProperties: boolean;
|
|
5
|
+
properties: {
|
|
6
|
+
scope: {
|
|
7
|
+
type: string;
|
|
8
|
+
enum: string[];
|
|
9
|
+
default: string;
|
|
10
|
+
};
|
|
11
|
+
browserTab: {
|
|
12
|
+
type: string;
|
|
13
|
+
minLength: number;
|
|
14
|
+
};
|
|
15
|
+
windowRef: {
|
|
16
|
+
type: string;
|
|
17
|
+
minLength: number;
|
|
18
|
+
};
|
|
19
|
+
windowTitle: {
|
|
20
|
+
type: string;
|
|
21
|
+
minLength: number;
|
|
22
|
+
};
|
|
23
|
+
processId: {
|
|
24
|
+
type: string;
|
|
25
|
+
minimum: number;
|
|
26
|
+
};
|
|
27
|
+
nativeWindowHandle: {
|
|
28
|
+
type: string;
|
|
29
|
+
minimum: number;
|
|
30
|
+
};
|
|
31
|
+
activate: {
|
|
32
|
+
type: string;
|
|
33
|
+
default: boolean;
|
|
34
|
+
};
|
|
35
|
+
windowOnly: {
|
|
36
|
+
type: string;
|
|
37
|
+
default: boolean;
|
|
38
|
+
description: string;
|
|
39
|
+
};
|
|
40
|
+
maxWidth: {
|
|
41
|
+
type: string;
|
|
42
|
+
minimum: number;
|
|
43
|
+
maximum: number;
|
|
44
|
+
default: number;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
export declare function screenshot(args: Record<string, unknown>, signal?: AbortSignal): Promise<WorkerResult>;
|
|
49
|
+
export declare function screenshotContent(result: WorkerResult): {
|
|
50
|
+
content: {
|
|
51
|
+
type: "text";
|
|
52
|
+
text: string;
|
|
53
|
+
}[];
|
|
54
|
+
details: WorkerResult;
|
|
55
|
+
isError: boolean;
|
|
56
|
+
} | {
|
|
57
|
+
content: ({
|
|
58
|
+
type: "text";
|
|
59
|
+
text: string;
|
|
60
|
+
data?: undefined;
|
|
61
|
+
mimeType?: undefined;
|
|
62
|
+
} | {
|
|
63
|
+
type: "image";
|
|
64
|
+
data: string;
|
|
65
|
+
mimeType: string;
|
|
66
|
+
text?: undefined;
|
|
67
|
+
})[];
|
|
68
|
+
details: {
|
|
69
|
+
screenshot: {};
|
|
70
|
+
ok: boolean;
|
|
71
|
+
action: string;
|
|
72
|
+
};
|
|
73
|
+
isError: boolean;
|
|
74
|
+
};
|
package/dist/capture.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { WorkerClient } from "./client.js";
|
|
4
|
+
import { compileNative, nativeDesktop } from "./native.js";
|
|
5
|
+
export const screenshotParameters = {
|
|
6
|
+
type: "object",
|
|
7
|
+
additionalProperties: false,
|
|
8
|
+
properties: {
|
|
9
|
+
scope: {
|
|
10
|
+
type: "string",
|
|
11
|
+
enum: ["active_window", "desktop"],
|
|
12
|
+
default: "active_window",
|
|
13
|
+
},
|
|
14
|
+
browserTab: { type: "string", minLength: 1 },
|
|
15
|
+
windowRef: { type: "string", minLength: 1 },
|
|
16
|
+
windowTitle: { type: "string", minLength: 1 },
|
|
17
|
+
processId: { type: "integer", minimum: 1 },
|
|
18
|
+
nativeWindowHandle: { type: "integer", minimum: 1 },
|
|
19
|
+
activate: { type: "boolean", default: false },
|
|
20
|
+
windowOnly: {
|
|
21
|
+
type: "boolean",
|
|
22
|
+
default: false,
|
|
23
|
+
description: "Native window scope only: use PrintWindow/WGC; refuse screen-copy and desktop fallbacks.",
|
|
24
|
+
},
|
|
25
|
+
maxWidth: { type: "integer", minimum: 0, maximum: 7680, default: 1600 },
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
function createWgc() {
|
|
29
|
+
return new WorkerClient({
|
|
30
|
+
launch: async (signal) => {
|
|
31
|
+
const windows = process.env.WINDIR;
|
|
32
|
+
if (!windows)
|
|
33
|
+
throw new Error("WGC_UNAVAILABLE: Windows directory is unknown");
|
|
34
|
+
const framework = join(windows, "Microsoft.NET", "Framework64", "v4.0.30319");
|
|
35
|
+
const metadata = join(windows, "System32", "WinMetadata");
|
|
36
|
+
const graphics = [];
|
|
37
|
+
for (const name of [
|
|
38
|
+
"Windows.Graphics.winmd",
|
|
39
|
+
"Windows.Graphics.Capture.winmd",
|
|
40
|
+
"Windows.Graphics.DirectX.winmd",
|
|
41
|
+
"Windows.Graphics.DirectX.Direct3D11.winmd",
|
|
42
|
+
"Windows.Graphics.Imaging.winmd",
|
|
43
|
+
]) {
|
|
44
|
+
const candidate = join(metadata, name);
|
|
45
|
+
try {
|
|
46
|
+
await access(candidate);
|
|
47
|
+
graphics.push(candidate);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
/* Optional split/consolidated metadata differs across Windows versions. */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (graphics.length === 0)
|
|
54
|
+
throw new Error("WGC_UNAVAILABLE: Windows graphics metadata is missing");
|
|
55
|
+
return compileNative(["wgc.cs", "window-marker.cs"], [
|
|
56
|
+
"System.Drawing.dll",
|
|
57
|
+
join(framework, "System.Runtime.dll"),
|
|
58
|
+
join(framework, "System.Runtime.WindowsRuntime.dll"),
|
|
59
|
+
join(metadata, "Windows.Foundation.winmd"),
|
|
60
|
+
...graphics,
|
|
61
|
+
], signal);
|
|
62
|
+
},
|
|
63
|
+
timeoutMs: 8000,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
export async function screenshot(args, signal) {
|
|
67
|
+
signal?.throwIfAborted();
|
|
68
|
+
const maxWidth = args.maxWidth ?? 1600;
|
|
69
|
+
const scope = args.scope ?? "active_window";
|
|
70
|
+
const allowed = new Set([
|
|
71
|
+
"windowRef",
|
|
72
|
+
"maxWidth",
|
|
73
|
+
"scope",
|
|
74
|
+
"windowTitle",
|
|
75
|
+
"processId",
|
|
76
|
+
"nativeWindowHandle",
|
|
77
|
+
"activate",
|
|
78
|
+
"windowOnly",
|
|
79
|
+
]);
|
|
80
|
+
const invalid = (args.windowRef !== undefined &&
|
|
81
|
+
(typeof args.windowRef !== "string" ||
|
|
82
|
+
!args.windowRef.trim() ||
|
|
83
|
+
scope === "desktop")) ||
|
|
84
|
+
Object.keys(args).some((key) => !allowed.has(key)) ||
|
|
85
|
+
!Number.isSafeInteger(maxWidth) ||
|
|
86
|
+
typeof maxWidth !== "number" ||
|
|
87
|
+
maxWidth < 0 ||
|
|
88
|
+
maxWidth > 7680 ||
|
|
89
|
+
(scope !== "active_window" && scope !== "desktop") ||
|
|
90
|
+
(args.windowOnly !== undefined && typeof args.windowOnly !== "boolean") ||
|
|
91
|
+
(args.windowOnly === true && scope === "desktop") ||
|
|
92
|
+
(args.windowTitle !== undefined &&
|
|
93
|
+
(typeof args.windowTitle !== "string" || !args.windowTitle.trim())) ||
|
|
94
|
+
[args.processId, args.nativeWindowHandle].some((value) => value !== undefined &&
|
|
95
|
+
(typeof value !== "number" ||
|
|
96
|
+
!Number.isSafeInteger(value) ||
|
|
97
|
+
value < 1)) ||
|
|
98
|
+
(args.activate !== undefined && typeof args.activate !== "boolean");
|
|
99
|
+
if (invalid)
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
action: "screenshot",
|
|
103
|
+
error: "INVALID_ARGUMENTS: invalid screenshot target, scope, windowOnly or maxWidth (integer 0–7680)",
|
|
104
|
+
};
|
|
105
|
+
let nativeCaptureError;
|
|
106
|
+
let wgcError;
|
|
107
|
+
try {
|
|
108
|
+
const result = await nativeDesktop.execute("screenshot", { ...args, scope, maxWidth }, signal);
|
|
109
|
+
nativeCaptureError = result.nativeCaptureError;
|
|
110
|
+
const target = result.wgcTarget;
|
|
111
|
+
delete result.wgcTarget;
|
|
112
|
+
if ((result.ok ||
|
|
113
|
+
((result.error === "SCREEN_REGION_CAPTURE_FAILED" ||
|
|
114
|
+
(args.windowOnly === true &&
|
|
115
|
+
result.error === "WINDOW_CAPTURE_UNAVAILABLE")) &&
|
|
116
|
+
result.cleanupConfirmed !== false &&
|
|
117
|
+
result.identityWarning === undefined)) &&
|
|
118
|
+
target &&
|
|
119
|
+
typeof target === "object" &&
|
|
120
|
+
!Array.isArray(target)) {
|
|
121
|
+
let failure;
|
|
122
|
+
const wgc = createWgc();
|
|
123
|
+
try {
|
|
124
|
+
const composited = await wgc.execute("screenshot", { ...target, maxWidth }, signal);
|
|
125
|
+
if (result.nativeCaptureError !== undefined)
|
|
126
|
+
composited.nativeCaptureError = result.nativeCaptureError;
|
|
127
|
+
if (composited.ok) {
|
|
128
|
+
if (result.windowRefUnavailable !== undefined) {
|
|
129
|
+
composited.windowRefUnavailable = result.windowRefUnavailable;
|
|
130
|
+
if (composited.screenshot &&
|
|
131
|
+
typeof composited.screenshot === "object")
|
|
132
|
+
composited.screenshot.windowRefUnavailable = result.windowRefUnavailable;
|
|
133
|
+
}
|
|
134
|
+
return composited;
|
|
135
|
+
}
|
|
136
|
+
wgcError = composited.error;
|
|
137
|
+
if (!result.screenshot ||
|
|
138
|
+
composited.cleanupConfirmed === false ||
|
|
139
|
+
/WINDOW_REFERENCE|CANCEL|STOP|CLEANUP_UNCONFIRMED/.test(String(composited.error)))
|
|
140
|
+
return composited;
|
|
141
|
+
failure = String(composited.error);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (signal?.aborted)
|
|
145
|
+
throw error;
|
|
146
|
+
failure = error instanceof Error ? error.message : String(error);
|
|
147
|
+
wgcError = failure;
|
|
148
|
+
if (/WINDOW_REFERENCE|CANCEL|STOP|CLEANUP_UNCONFIRMED/.test(failure))
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
await wgc.dispose();
|
|
153
|
+
}
|
|
154
|
+
result.wgcError = failure;
|
|
155
|
+
if (result.screenshot && typeof result.screenshot === "object") {
|
|
156
|
+
const capture = result.screenshot;
|
|
157
|
+
capture.captureWarnings = [
|
|
158
|
+
...(Array.isArray(capture.captureWarnings)
|
|
159
|
+
? capture.captureWarnings
|
|
160
|
+
: []),
|
|
161
|
+
`WGC_UNAVAILABLE: ${failure}`,
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
action: "screenshot",
|
|
171
|
+
error: error instanceof Error ? error.message : String(error),
|
|
172
|
+
...(nativeCaptureError === undefined ? {} : { nativeCaptureError }),
|
|
173
|
+
...(wgcError === undefined ? {} : { wgcError }),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
export function screenshotContent(result) {
|
|
178
|
+
const capture = result.screenshot;
|
|
179
|
+
if (!capture ||
|
|
180
|
+
typeof capture !== "object" ||
|
|
181
|
+
!("base64" in capture) ||
|
|
182
|
+
typeof capture.base64 !== "string") {
|
|
183
|
+
return {
|
|
184
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
185
|
+
details: result,
|
|
186
|
+
isError: !result.ok,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const { base64, ...metadata } = capture;
|
|
190
|
+
const details = { ...result, screenshot: metadata };
|
|
191
|
+
return {
|
|
192
|
+
content: [
|
|
193
|
+
{ type: "text", text: JSON.stringify(details) },
|
|
194
|
+
{ type: "image", data: base64, mimeType: "image/png" },
|
|
195
|
+
],
|
|
196
|
+
details,
|
|
197
|
+
isError: !result.ok,
|
|
198
|
+
};
|
|
199
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { WorkerResult } from "./protocol.js";
|
|
2
|
+
export interface WorkerLaunch {
|
|
3
|
+
executable: string;
|
|
4
|
+
args: string[];
|
|
5
|
+
cwd?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface WorkerClientOptions {
|
|
8
|
+
launch: (signal?: AbortSignal) => Promise<WorkerLaunch>;
|
|
9
|
+
timeoutMs?: number;
|
|
10
|
+
shutdownMs?: number;
|
|
11
|
+
canTerminate?: () => boolean;
|
|
12
|
+
onStop?: (operationId: string) => void;
|
|
13
|
+
onInputState?: (active: boolean, operationId?: string) => void;
|
|
14
|
+
onExit?: () => void;
|
|
15
|
+
}
|
|
16
|
+
export declare class WorkerClient {
|
|
17
|
+
private readonly options;
|
|
18
|
+
private child;
|
|
19
|
+
private starting;
|
|
20
|
+
private readonly pending;
|
|
21
|
+
private nextId;
|
|
22
|
+
private disposed;
|
|
23
|
+
private failure;
|
|
24
|
+
private inputActive;
|
|
25
|
+
private retirementFailure;
|
|
26
|
+
private retirement;
|
|
27
|
+
private shutdownChanged;
|
|
28
|
+
private readonly lifetime;
|
|
29
|
+
private queue;
|
|
30
|
+
constructor(options: WorkerClientOptions);
|
|
31
|
+
private start;
|
|
32
|
+
private fail;
|
|
33
|
+
execute(action: string, args: Record<string, unknown>, signal?: AbortSignal, operationId?: string): Promise<WorkerResult>;
|
|
34
|
+
private executeNow;
|
|
35
|
+
stopOperation(operationId: string): void;
|
|
36
|
+
retire(reason?: string): Promise<void>;
|
|
37
|
+
dispose(): Promise<void>;
|
|
38
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
function abortable(operation, signal) {
|
|
3
|
+
return new Promise((resolve, reject) => {
|
|
4
|
+
const abort = () => reject(signal.reason);
|
|
5
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
6
|
+
if (signal.aborted)
|
|
7
|
+
abort();
|
|
8
|
+
operation
|
|
9
|
+
.then(resolve, reject)
|
|
10
|
+
.finally(() => signal.removeEventListener("abort", abort));
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export class WorkerClient {
|
|
14
|
+
options;
|
|
15
|
+
child;
|
|
16
|
+
starting;
|
|
17
|
+
pending = new Map();
|
|
18
|
+
nextId = 0;
|
|
19
|
+
disposed = false;
|
|
20
|
+
failure;
|
|
21
|
+
inputActive = false;
|
|
22
|
+
retirementFailure;
|
|
23
|
+
retirement;
|
|
24
|
+
shutdownChanged;
|
|
25
|
+
lifetime = new AbortController();
|
|
26
|
+
queue = Promise.resolve();
|
|
27
|
+
constructor(options) {
|
|
28
|
+
this.options = options;
|
|
29
|
+
}
|
|
30
|
+
async start(signal) {
|
|
31
|
+
if (this.disposed)
|
|
32
|
+
throw new Error("WORKER_DISPOSED: session has closed");
|
|
33
|
+
signal?.throwIfAborted();
|
|
34
|
+
if (this.retirement)
|
|
35
|
+
await this.retirement;
|
|
36
|
+
if (this.retirementFailure)
|
|
37
|
+
throw this.retirementFailure;
|
|
38
|
+
if (this.inputActive && !this.child)
|
|
39
|
+
throw new Error("INPUT_CLEANUP_UNCONFIRMED: input owner exited without cleanup acknowledgment");
|
|
40
|
+
if (this.child && this.failure)
|
|
41
|
+
await this.retire(this.failure.message);
|
|
42
|
+
if (this.child)
|
|
43
|
+
return this.child;
|
|
44
|
+
const launchSignal = signal
|
|
45
|
+
? AbortSignal.any([signal, this.lifetime.signal])
|
|
46
|
+
: this.lifetime.signal;
|
|
47
|
+
this.starting ??= abortable(this.options.launch(launchSignal), launchSignal)
|
|
48
|
+
.then((launch) => {
|
|
49
|
+
if (this.disposed)
|
|
50
|
+
throw new Error("WORKER_DISPOSED: session has closed");
|
|
51
|
+
signal?.throwIfAborted();
|
|
52
|
+
const child = spawn(launch.executable, launch.args, {
|
|
53
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
54
|
+
windowsHide: true,
|
|
55
|
+
cwd: launch.cwd,
|
|
56
|
+
});
|
|
57
|
+
this.child = child;
|
|
58
|
+
let output = "";
|
|
59
|
+
let diagnostic = "";
|
|
60
|
+
child.stdout.setEncoding("utf8");
|
|
61
|
+
child.stderr.setEncoding("utf8");
|
|
62
|
+
child.stderr.on("data", (chunk) => {
|
|
63
|
+
diagnostic = (diagnostic + chunk).slice(-4096);
|
|
64
|
+
});
|
|
65
|
+
child.stdout.on("data", (chunk) => {
|
|
66
|
+
output += chunk;
|
|
67
|
+
if (output.length > 32 * 1024 * 1024) {
|
|
68
|
+
this.fail(child, new Error("WORKER_PROTOCOL_ERROR: response exceeds 32 MiB"));
|
|
69
|
+
if (!this.inputActive && this.options.canTerminate?.() !== false)
|
|
70
|
+
child.kill();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
for (;;) {
|
|
74
|
+
const newline = output.indexOf("\n");
|
|
75
|
+
if (newline < 0)
|
|
76
|
+
break;
|
|
77
|
+
const line = output.slice(0, newline);
|
|
78
|
+
output = output.slice(newline + 1);
|
|
79
|
+
try {
|
|
80
|
+
const response = JSON.parse(line);
|
|
81
|
+
if (response.event === "shutdown_state") {
|
|
82
|
+
if (this.child === child &&
|
|
83
|
+
(response.phase === "started" ||
|
|
84
|
+
response.phase === "complete" ||
|
|
85
|
+
response.phase === "failed"))
|
|
86
|
+
this.shutdownChanged?.(response.phase, typeof response.error === "string"
|
|
87
|
+
? response.error.slice(0, 2048)
|
|
88
|
+
: undefined);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (response.event === "input_prepare") {
|
|
92
|
+
const pending = Number.isSafeInteger(response.requestId)
|
|
93
|
+
? this.pending.get(Number(response.requestId))
|
|
94
|
+
: undefined;
|
|
95
|
+
if (!pending ||
|
|
96
|
+
typeof response.token !== "string" ||
|
|
97
|
+
response.token.length < 1 ||
|
|
98
|
+
response.token.length > 200 ||
|
|
99
|
+
typeof response.operationId !== "string" ||
|
|
100
|
+
(pending.operationId !== undefined &&
|
|
101
|
+
pending.operationId !== response.operationId))
|
|
102
|
+
continue;
|
|
103
|
+
this.inputActive = true;
|
|
104
|
+
this.options.onInputState?.(true, response.operationId);
|
|
105
|
+
child.stdin.write(`${JSON.stringify({ inputAck: response.token, requestId: response.requestId })}\n`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (response.event === "input_state" &&
|
|
109
|
+
typeof response.active === "boolean") {
|
|
110
|
+
this.inputActive = response.active;
|
|
111
|
+
this.options.onInputState?.(response.active, response.operationId);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (response.event === "stopped" &&
|
|
115
|
+
typeof response.operationId === "string") {
|
|
116
|
+
this.options.onStop?.(response.operationId);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if (!Number.isSafeInteger(response.id) ||
|
|
120
|
+
typeof response.result?.ok !== "boolean" ||
|
|
121
|
+
typeof response.result.action !== "string")
|
|
122
|
+
throw new Error("Invalid response");
|
|
123
|
+
const pending = this.pending.get(response.id);
|
|
124
|
+
if (pending) {
|
|
125
|
+
this.pending.delete(response.id);
|
|
126
|
+
pending.cleanup();
|
|
127
|
+
pending.resolve(response.result);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
this.fail(child, new Error("WORKER_PROTOCOL_ERROR: invalid response"));
|
|
132
|
+
if (!this.inputActive && this.options.canTerminate?.() !== false)
|
|
133
|
+
child.kill();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
child.once("error", (error) => this.fail(child, new Error(`WORKER_START_FAILED: ${error.message}`), child.pid === undefined));
|
|
138
|
+
child.once("close", (code) => {
|
|
139
|
+
const current = this.child === child;
|
|
140
|
+
this.fail(child, new Error(`WORKER_EXITED: code ${code}${diagnostic ? `; ${diagnostic.trim()}` : ""}`), true);
|
|
141
|
+
if (current)
|
|
142
|
+
this.options.onExit?.();
|
|
143
|
+
});
|
|
144
|
+
child.stdin.on("error", (error) => this.fail(child, new Error(`WORKER_WRITE_FAILED: ${error.message}`)));
|
|
145
|
+
return child;
|
|
146
|
+
})
|
|
147
|
+
.finally(() => {
|
|
148
|
+
this.starting = undefined;
|
|
149
|
+
});
|
|
150
|
+
return this.starting;
|
|
151
|
+
}
|
|
152
|
+
fail(child, error, confirmedExit = false) {
|
|
153
|
+
if (this.child !== child)
|
|
154
|
+
return;
|
|
155
|
+
if (confirmedExit) {
|
|
156
|
+
this.child = undefined;
|
|
157
|
+
this.failure = undefined;
|
|
158
|
+
// Process death is not an acknowledgement that its resources were freed.
|
|
159
|
+
if (!this.retirementFailure?.message.startsWith("WORKER_CLEANUP_UNCONFIRMED:"))
|
|
160
|
+
this.retirementFailure = undefined;
|
|
161
|
+
}
|
|
162
|
+
else
|
|
163
|
+
this.failure = error;
|
|
164
|
+
for (const [id, pending] of this.pending) {
|
|
165
|
+
this.pending.delete(id);
|
|
166
|
+
pending.cleanup();
|
|
167
|
+
pending.reject(error);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
execute(action, args, signal, operationId) {
|
|
171
|
+
if (this.disposed)
|
|
172
|
+
return Promise.reject(new Error("WORKER_DISPOSED: session has closed"));
|
|
173
|
+
const callSignal = signal
|
|
174
|
+
? AbortSignal.any([signal, this.lifetime.signal])
|
|
175
|
+
: this.lifetime.signal;
|
|
176
|
+
const result = this.queue
|
|
177
|
+
.catch(() => undefined)
|
|
178
|
+
.then(() => {
|
|
179
|
+
callSignal.throwIfAborted();
|
|
180
|
+
return this.executeNow(action, args, callSignal, operationId);
|
|
181
|
+
});
|
|
182
|
+
this.queue = result.catch(() => undefined);
|
|
183
|
+
return abortable(result, callSignal);
|
|
184
|
+
}
|
|
185
|
+
async executeNow(action, args, signal, operationId) {
|
|
186
|
+
const child = await this.start(signal);
|
|
187
|
+
if (this.disposed)
|
|
188
|
+
throw new Error("WORKER_DISPOSED: session has closed");
|
|
189
|
+
signal?.throwIfAborted();
|
|
190
|
+
const id = ++this.nextId;
|
|
191
|
+
return new Promise((resolve, reject) => {
|
|
192
|
+
const timer = setTimeout(() => {
|
|
193
|
+
this.fail(child, new Error(`WORKER_TIMEOUT: ${action} exceeded the execution deadline; input was not replayed`));
|
|
194
|
+
if (!this.inputActive && this.options.canTerminate?.() !== false)
|
|
195
|
+
child.kill();
|
|
196
|
+
}, Math.max(this.options.timeoutMs ?? 30_000, action === "wait" &&
|
|
197
|
+
Number.isSafeInteger(args.milliseconds ?? 500) &&
|
|
198
|
+
Number(args.milliseconds ?? 500) >= 1 &&
|
|
199
|
+
Number(args.milliseconds ?? 500) <= 30000
|
|
200
|
+
? Number(args.milliseconds ?? 500) + 2000
|
|
201
|
+
: 0, action === "wait_for" &&
|
|
202
|
+
Number.isSafeInteger(args.timeoutMs ?? 5000) &&
|
|
203
|
+
Number(args.timeoutMs ?? 5000) >= 100 &&
|
|
204
|
+
Number(args.timeoutMs ?? 5000) <= 60000
|
|
205
|
+
? Number(args.timeoutMs ?? 5000) + 10000
|
|
206
|
+
: 0));
|
|
207
|
+
const abort = () => {
|
|
208
|
+
const pending = this.pending.get(id);
|
|
209
|
+
if (!pending)
|
|
210
|
+
return;
|
|
211
|
+
this.pending.delete(id);
|
|
212
|
+
pending.cleanup();
|
|
213
|
+
// Health has no held input. Cooperative input stopping is added before input tools.
|
|
214
|
+
child.stdin.write(`${JSON.stringify({ cancel: id })}\n`);
|
|
215
|
+
pending.reject(new Error("COMPUTER_USE_CANCELLED"));
|
|
216
|
+
};
|
|
217
|
+
this.pending.set(id, {
|
|
218
|
+
operationId,
|
|
219
|
+
resolve,
|
|
220
|
+
reject,
|
|
221
|
+
cleanup: () => {
|
|
222
|
+
clearTimeout(timer);
|
|
223
|
+
signal?.removeEventListener("abort", abort);
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
227
|
+
if (signal?.aborted) {
|
|
228
|
+
abort();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
child.stdin.write(`${JSON.stringify({ id, action, args, operationId })}\n`, (error) => {
|
|
232
|
+
if (error)
|
|
233
|
+
this.fail(child, new Error(`WORKER_WRITE_FAILED: ${error.message}`));
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
stopOperation(operationId) {
|
|
238
|
+
// Control messages bypass queued calls, including a busy native provider.
|
|
239
|
+
this.child?.stdin.write(`${JSON.stringify({ stop: operationId })}\n`);
|
|
240
|
+
}
|
|
241
|
+
async retire(reason = "WORKER_RETIRED: operation stopped") {
|
|
242
|
+
if (this.inputActive || this.options.canTerminate?.() === false)
|
|
243
|
+
throw new Error("INPUT_CLEANUP_UNCONFIRMED: active input prevents bridge retirement");
|
|
244
|
+
if (this.retirementFailure)
|
|
245
|
+
throw this.retirementFailure;
|
|
246
|
+
if (this.retirement)
|
|
247
|
+
return this.retirement;
|
|
248
|
+
const child = this.child;
|
|
249
|
+
if (!child)
|
|
250
|
+
return;
|
|
251
|
+
this.fail(child, new Error(reason));
|
|
252
|
+
this.retirement = new Promise((resolve, reject) => {
|
|
253
|
+
let forced;
|
|
254
|
+
let timer;
|
|
255
|
+
let started = false;
|
|
256
|
+
let complete = false;
|
|
257
|
+
let settled = false;
|
|
258
|
+
const fail = (error) => {
|
|
259
|
+
settled = true;
|
|
260
|
+
clearTimeout(timer);
|
|
261
|
+
clearTimeout(forced);
|
|
262
|
+
reject(error);
|
|
263
|
+
};
|
|
264
|
+
const expire = () => {
|
|
265
|
+
if (this.inputActive || this.options.canTerminate?.() === false) {
|
|
266
|
+
fail(new Error("INPUT_CLEANUP_UNCONFIRMED: active input prevents bridge retirement"));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
child.kill();
|
|
270
|
+
forced = setTimeout(() => fail(new Error("WORKER_RETIRE_FAILED: child exit was not confirmed; desktop ownership must stay held")), 1000);
|
|
271
|
+
};
|
|
272
|
+
const arm = (milliseconds) => {
|
|
273
|
+
clearTimeout(timer);
|
|
274
|
+
timer = setTimeout(expire, milliseconds);
|
|
275
|
+
};
|
|
276
|
+
this.shutdownChanged = (phase, error) => {
|
|
277
|
+
if (settled)
|
|
278
|
+
return;
|
|
279
|
+
if (phase === "started" && !started) {
|
|
280
|
+
started = true;
|
|
281
|
+
// Resource cleanup is an operation, not an idle exit. Give it the
|
|
282
|
+
// existing execution budget; fast completion adds no fixed delay.
|
|
283
|
+
arm(this.options.timeoutMs ?? 30_000);
|
|
284
|
+
}
|
|
285
|
+
else if (started && phase === "complete" && !complete) {
|
|
286
|
+
complete = true;
|
|
287
|
+
arm(this.options.shutdownMs ?? 1000);
|
|
288
|
+
}
|
|
289
|
+
else if (started && phase === "failed") {
|
|
290
|
+
fail(new Error(`WORKER_CLEANUP_UNCONFIRMED: ${error ?? "worker resource cleanup failed"}`));
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
arm(this.options.shutdownMs ?? 1000);
|
|
294
|
+
child.once("close", () => {
|
|
295
|
+
clearTimeout(timer);
|
|
296
|
+
clearTimeout(forced);
|
|
297
|
+
if (settled)
|
|
298
|
+
return;
|
|
299
|
+
if (this.inputActive || this.options.canTerminate?.() === false) {
|
|
300
|
+
fail(new Error("INPUT_CLEANUP_UNCONFIRMED: input owner exited without release"));
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (started && !complete) {
|
|
304
|
+
fail(new Error("WORKER_CLEANUP_UNCONFIRMED: worker exited before shutdown completed"));
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
settled = true;
|
|
308
|
+
resolve();
|
|
309
|
+
});
|
|
310
|
+
child.stdin.end();
|
|
311
|
+
});
|
|
312
|
+
try {
|
|
313
|
+
await this.retirement;
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
this.retirementFailure =
|
|
317
|
+
error instanceof Error ? error : new Error(String(error));
|
|
318
|
+
throw error;
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
this.shutdownChanged = undefined;
|
|
322
|
+
this.retirement = undefined;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
async dispose() {
|
|
326
|
+
this.disposed = true;
|
|
327
|
+
this.lifetime.abort(new Error("WORKER_DISPOSED: session has closed"));
|
|
328
|
+
await this.starting?.catch(() => undefined);
|
|
329
|
+
await this.retire("WORKER_DISPOSED: session has closed");
|
|
330
|
+
}
|
|
331
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { WorkerResult } from "./protocol.js";
|
|
2
|
+
export declare const listWindowsParameters: {
|
|
3
|
+
type: string;
|
|
4
|
+
additionalProperties: boolean;
|
|
5
|
+
properties: {
|
|
6
|
+
includeInvisible: {
|
|
7
|
+
type: string;
|
|
8
|
+
default: boolean;
|
|
9
|
+
};
|
|
10
|
+
maxWindows: {
|
|
11
|
+
type: string;
|
|
12
|
+
minimum: number;
|
|
13
|
+
maximum: number;
|
|
14
|
+
default: number;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export declare function listWindows(args: Record<string, unknown>, signal?: AbortSignal): Promise<WorkerResult>;
|
|
19
|
+
export declare function disposeDesktop(): Promise<void>;
|
|
20
|
+
export declare function retireDesktop(): Promise<void>;
|