@anionex/dsh-computer-use 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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +307 -0
- package/README.zh.md +307 -0
- package/assets/computer-use-fixture.png +0 -0
- package/assets/cursor.png +0 -0
- package/cordis.patch.yml +13 -0
- package/docs/interaction-policy.i18n.yaml +4 -0
- package/docs/interaction-policy.md +140 -0
- package/docs/interaction-policy.zh.md +140 -0
- package/lib/approval-policy.js +12 -0
- package/lib/approval-policy.js.map +1 -0
- package/lib/artifacts.js +79 -0
- package/lib/artifacts.js.map +1 -0
- package/lib/backend.js +3 -0
- package/lib/backend.js.map +1 -0
- package/lib/client.js +349 -0
- package/lib/client.js.map +1 -0
- package/lib/config.js +120 -0
- package/lib/config.js.map +1 -0
- package/lib/confirmations.js +99 -0
- package/lib/confirmations.js.map +1 -0
- package/lib/diff.js +58 -0
- package/lib/diff.js.map +1 -0
- package/lib/errors.js +22 -0
- package/lib/errors.js.map +1 -0
- package/lib/exposure.js +231 -0
- package/lib/exposure.js.map +1 -0
- package/lib/index.js +56 -0
- package/lib/index.js.map +1 -0
- package/lib/leases.js +259 -0
- package/lib/leases.js.map +1 -0
- package/lib/providers/macos.js +148 -0
- package/lib/providers/macos.js.map +1 -0
- package/lib/providers/native-helper.js +356 -0
- package/lib/providers/native-helper.js.map +1 -0
- package/lib/service.js +480 -0
- package/lib/service.js.map +1 -0
- package/lib/skill.js +119 -0
- package/lib/skill.js.map +1 -0
- package/lib/target-resolver.js +180 -0
- package/lib/target-resolver.js.map +1 -0
- package/lib/tools.js +493 -0
- package/lib/tools.js.map +1 -0
- package/lib/types/approval-policy.d.ts +13 -0
- package/lib/types/approval-policy.d.ts.map +1 -0
- package/lib/types/artifacts.d.ts +10 -0
- package/lib/types/artifacts.d.ts.map +1 -0
- package/lib/types/backend.d.ts +94 -0
- package/lib/types/backend.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +78 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/config.d.ts +75 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/confirmations.d.ts +23 -0
- package/lib/types/confirmations.d.ts.map +1 -0
- package/lib/types/diff.d.ts +5 -0
- package/lib/types/diff.d.ts.map +1 -0
- package/lib/types/errors.d.ts +15 -0
- package/lib/types/errors.d.ts.map +1 -0
- package/lib/types/exposure.d.ts +30 -0
- package/lib/types/exposure.d.ts.map +1 -0
- package/lib/types/index.d.ts +21 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/leases.d.ts +80 -0
- package/lib/types/leases.d.ts.map +1 -0
- package/lib/types/providers/macos.d.ts +15 -0
- package/lib/types/providers/macos.d.ts.map +1 -0
- package/lib/types/providers/native-helper.d.ts +36 -0
- package/lib/types/providers/native-helper.d.ts.map +1 -0
- package/lib/types/service.d.ts +52 -0
- package/lib/types/service.d.ts.map +1 -0
- package/lib/types/skill.d.ts +9 -0
- package/lib/types/skill.d.ts.map +1 -0
- package/lib/types/target-resolver.d.ts +38 -0
- package/lib/types/target-resolver.d.ts.map +1 -0
- package/lib/types/tools.d.ts +6 -0
- package/lib/types/tools.d.ts.map +1 -0
- package/lib/types/types.d.ts +246 -0
- package/lib/types/types.d.ts.map +1 -0
- package/lib/types/web.d.ts +32 -0
- package/lib/types/web.d.ts.map +1 -0
- package/lib/types.js +9 -0
- package/lib/types.js.map +1 -0
- package/lib/web.js +163 -0
- package/lib/web.js.map +1 -0
- package/native/macos/Sources/Fixture/main.swift +366 -0
- package/native/macos/Sources/Helper/CursorImage.swift +11 -0
- package/native/macos/Sources/Helper/CursorOverlay.swift +488 -0
- package/native/macos/Sources/Helper/TargetedPointer.swift +314 -0
- package/native/macos/Sources/Helper/main.swift +1140 -0
- package/native/macos/Sources/Monitor/main.swift +139 -0
- package/native/macos/bin/dsh-computer-use-helper +0 -0
- package/native/macos/manifest.json +14 -0
- package/package.json +128 -0
- package/scripts/build-client.mjs +24 -0
- package/scripts/build-native.mjs +158 -0
- package/scripts/check-native.mjs +92 -0
- package/scripts/model-e2e.mjs +291 -0
- package/scripts/session-transcript.mjs +117 -0
- package/scripts/validate.mjs +256 -0
- package/src/approval-policy.ts +16 -0
- package/src/artifacts.ts +93 -0
- package/src/backend.ts +103 -0
- package/src/client/index.tsx +539 -0
- package/src/config.ts +196 -0
- package/src/confirmations.ts +128 -0
- package/src/diff.ts +52 -0
- package/src/errors.ts +39 -0
- package/src/exposure.ts +236 -0
- package/src/index.ts +66 -0
- package/src/leases.ts +337 -0
- package/src/providers/macos.ts +185 -0
- package/src/providers/native-helper.ts +393 -0
- package/src/service.ts +583 -0
- package/src/skill.ts +123 -0
- package/src/target-resolver.ts +237 -0
- package/src/tools.ts +528 -0
- package/src/types.ts +299 -0
- package/src/web.ts +180 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/** Managed invocation and integrity checks for the fixed-command Swift helper. */
|
|
2
|
+
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
import { access, chmod, lstat, readFile, realpath, stat } from 'node:fs/promises'
|
|
5
|
+
import { constants, type Stats } from 'node:fs'
|
|
6
|
+
import type { Writable } from 'node:stream'
|
|
7
|
+
import { dirname, resolve } from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
10
|
+
import type { SubprocessHandle, SubprocessOutcome, SubprocessOutputReader } from '@deepseek-ai/dsh-subprocess'
|
|
11
|
+
import type { ResolvedComputerUseConfig } from '../config.ts'
|
|
12
|
+
import { ComputerUseError, computerUseError, type ComputerUseErrorCode } from '../errors.ts'
|
|
13
|
+
|
|
14
|
+
interface NativeManifest {
|
|
15
|
+
schemaVersion: 1
|
|
16
|
+
helperVersion: string
|
|
17
|
+
sourceSha256: string
|
|
18
|
+
binary: {
|
|
19
|
+
path: string
|
|
20
|
+
sha256: string
|
|
21
|
+
architectures: string[]
|
|
22
|
+
minimumMacOS: string
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface HelperFailure {
|
|
27
|
+
ok: false
|
|
28
|
+
error: {
|
|
29
|
+
code: ComputerUseErrorCode
|
|
30
|
+
message: string
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface HelperSuccess<T> {
|
|
35
|
+
ok: true
|
|
36
|
+
value: T
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type HelperEnvelope<T> = HelperFailure | HelperSuccess<T>
|
|
40
|
+
|
|
41
|
+
const CURSOR_READY_TIMEOUT_MS = 2_000
|
|
42
|
+
const CURSOR_PROTOCOL_MAX_BYTES = 64 * 1024
|
|
43
|
+
|
|
44
|
+
interface CursorProcess {
|
|
45
|
+
stdin: Writable
|
|
46
|
+
done: Promise<SubprocessOutcome>
|
|
47
|
+
terminate: () => void
|
|
48
|
+
waitForExit: SubprocessHandle['waitForExit']
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function collected(reader: SubprocessOutputReader | undefined): string {
|
|
52
|
+
if (reader === undefined) return ''
|
|
53
|
+
const value = reader.readFrom(0)
|
|
54
|
+
if (value.lossy) throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', 'native helper output exceeded its protocol limit')
|
|
55
|
+
return value.text
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function sha256(path: string): Promise<string> {
|
|
59
|
+
return createHash('sha256').update(await readFile(path)).digest('hex')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function nativeRoot(): string {
|
|
63
|
+
return fileURLToPath(new URL('../../native/macos/', import.meta.url))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Exact helper paths and integrity data for one active generation. */
|
|
67
|
+
export interface PreparedNativeHelper {
|
|
68
|
+
path: string
|
|
69
|
+
version: string
|
|
70
|
+
sha256: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Invokes only the packaged JSON protocol through `ctx.subprocess`; no source or shell reaches the helper. */
|
|
74
|
+
export class NativeHelperClient {
|
|
75
|
+
private prepared?: PreparedNativeHelper
|
|
76
|
+
private cursor: CursorProcess | undefined
|
|
77
|
+
private cursorStart: { promise: Promise<CursorProcess> } | undefined
|
|
78
|
+
|
|
79
|
+
constructor(
|
|
80
|
+
private readonly ctx: Context,
|
|
81
|
+
private readonly config: ResolvedComputerUseConfig,
|
|
82
|
+
private readonly managedRoot = nativeRoot(),
|
|
83
|
+
) {}
|
|
84
|
+
|
|
85
|
+
/** Absolute executable path selected by explicit override or the packaged managed binary. */
|
|
86
|
+
get helperPath(): string {
|
|
87
|
+
return this.prepared?.path ?? this.config.helper.path ?? resolve(this.managedRoot, 'bin', 'dsh-computer-use-helper')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Verify platform, file type, packaged hash, and executable mode before use. */
|
|
91
|
+
async prepare(signal: AbortSignal): Promise<PreparedNativeHelper> {
|
|
92
|
+
if (process.platform !== 'darwin') {
|
|
93
|
+
throw new ComputerUseError('COMPUTER_UNSUPPORTED_PLATFORM', `macOS provider cannot run on ${process.platform}`)
|
|
94
|
+
}
|
|
95
|
+
const managed = this.config.helper.path === undefined
|
|
96
|
+
let path = this.helperPath
|
|
97
|
+
let selectedInfo: Stats
|
|
98
|
+
try {
|
|
99
|
+
selectedInfo = await lstat(path)
|
|
100
|
+
} catch (error) {
|
|
101
|
+
const missing = (error as NodeJS.ErrnoException).code === 'ENOENT'
|
|
102
|
+
if (!managed || !missing || !this.config.helper.allowSourceBuild) {
|
|
103
|
+
throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', `native helper is missing or unreadable: ${path}`, { cause: error })
|
|
104
|
+
}
|
|
105
|
+
await this.buildManaged(signal)
|
|
106
|
+
selectedInfo = await lstat(path)
|
|
107
|
+
}
|
|
108
|
+
if (!selectedInfo.isFile() || selectedInfo.isSymbolicLink()) {
|
|
109
|
+
throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', 'native helper must be a regular non-symbolic-link executable')
|
|
110
|
+
}
|
|
111
|
+
path = await realpath(path)
|
|
112
|
+
const digest = await sha256(path)
|
|
113
|
+
let version = 'external'
|
|
114
|
+
if (managed) {
|
|
115
|
+
const manifestPath = resolve(this.managedRoot, 'manifest.json')
|
|
116
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8')) as NativeManifest
|
|
117
|
+
if (manifest.schemaVersion !== 1 || manifest.binary.path !== 'bin/dsh-computer-use-helper') {
|
|
118
|
+
throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', 'native helper manifest is malformed')
|
|
119
|
+
}
|
|
120
|
+
if (manifest.binary.sha256 !== digest) {
|
|
121
|
+
throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', 'native helper hash does not match native/macos/manifest.json')
|
|
122
|
+
}
|
|
123
|
+
version = manifest.helperVersion
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
await access(path, constants.X_OK)
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (!managed) {
|
|
129
|
+
throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', `external native helper is not executable: ${path}`, { cause: error })
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
// npm-compatible tarballs normalize non-bin payloads to 0644. Restore
|
|
133
|
+
// only the owner's execute bit after the committed hash is verified.
|
|
134
|
+
await chmod(path, (selectedInfo.mode & 0o777) | 0o100)
|
|
135
|
+
await access(path, constants.X_OK)
|
|
136
|
+
} catch (chmodError) {
|
|
137
|
+
throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', `managed native helper cannot be marked executable: ${path}`, { cause: chmodError })
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
this.prepared = { path, version, sha256: digest }
|
|
141
|
+
return this.prepared
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Invoke one fixed helper command and parse its bounded JSON envelope. */
|
|
145
|
+
async invoke<T>(request: Record<string, unknown>, signal: AbortSignal): Promise<T> {
|
|
146
|
+
const prepared = this.prepared ?? await this.prepare(signal)
|
|
147
|
+
const timeout = AbortSignal.timeout(this.config.actionTimeoutMs)
|
|
148
|
+
const combined = AbortSignal.any([signal, timeout])
|
|
149
|
+
const handle = this.ctx.subprocess.spawn({
|
|
150
|
+
argv: [prepared.path],
|
|
151
|
+
cwd: dirname(prepared.path),
|
|
152
|
+
stdio: {
|
|
153
|
+
stdin: { data: `${JSON.stringify({ protocolVersion: 1, ...request })}\n` },
|
|
154
|
+
stdout: { maxBytes: 4 * 1024 * 1024 },
|
|
155
|
+
stderr: { maxBytes: 64 * 1024 },
|
|
156
|
+
},
|
|
157
|
+
graceMs: 1000,
|
|
158
|
+
signal: combined,
|
|
159
|
+
env: {
|
|
160
|
+
LANG: process.env.LANG ?? 'en_US.UTF-8',
|
|
161
|
+
LC_ALL: process.env.LC_ALL ?? 'en_US.UTF-8',
|
|
162
|
+
},
|
|
163
|
+
})
|
|
164
|
+
let outcome
|
|
165
|
+
try {
|
|
166
|
+
outcome = await handle.done
|
|
167
|
+
} catch (error) {
|
|
168
|
+
throw computerUseError(error, 'native helper failed to start')
|
|
169
|
+
}
|
|
170
|
+
if (combined.aborted) {
|
|
171
|
+
if (signal.aborted) throw new ComputerUseError('COMPUTER_CANCELLED', 'native helper call was cancelled')
|
|
172
|
+
throw new ComputerUseError('COMPUTER_TIMEOUT', `native helper exceeded ${this.config.actionTimeoutMs} milliseconds`)
|
|
173
|
+
}
|
|
174
|
+
const stdout = collected(handle.collected.stdout)
|
|
175
|
+
const stderr = collected(handle.collected.stderr)
|
|
176
|
+
if (outcome.exitCode !== 0 && stdout.trim().length === 0) {
|
|
177
|
+
throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', `native helper exited ${String(outcome.exitCode)}${stderr.trim().length === 0 ? '' : `: ${stderr.trim().slice(0, 1000)}`}`)
|
|
178
|
+
}
|
|
179
|
+
let envelope: HelperEnvelope<T>
|
|
180
|
+
try {
|
|
181
|
+
envelope = JSON.parse(stdout) as HelperEnvelope<T>
|
|
182
|
+
} catch (error) {
|
|
183
|
+
throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', 'native helper returned invalid JSON', { cause: error })
|
|
184
|
+
}
|
|
185
|
+
if (envelope.ok !== true) throw new ComputerUseError(envelope.error.code, envelope.error.message.slice(0, 1000))
|
|
186
|
+
return envelope.value
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Send one best-effort command to the persistent, click-through Agent cursor overlay. */
|
|
190
|
+
async cursorCommand(command: Record<string, unknown>, signal: AbortSignal): Promise<void> {
|
|
191
|
+
const prepared = this.prepared ?? await this.prepare(signal)
|
|
192
|
+
const cursor = await this.getCursor(prepared, signal)
|
|
193
|
+
signal.throwIfAborted()
|
|
194
|
+
try {
|
|
195
|
+
await new Promise<void>((resolveWrite, rejectWrite) => {
|
|
196
|
+
cursor.stdin.write(`${JSON.stringify(command)}\n`, error => {
|
|
197
|
+
if (error === undefined || error === null) resolveWrite()
|
|
198
|
+
else rejectWrite(error)
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (this.cursor === cursor) this.cursor = undefined
|
|
203
|
+
cursor.terminate()
|
|
204
|
+
throw computerUseError(error, 'native cursor overlay command failed')
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Stop the cursor process before a provider generation is replaced or disposed. */
|
|
209
|
+
async dispose(): Promise<void> {
|
|
210
|
+
const cursor = this.cursor ?? await this.cursorStart?.promise.catch(() => undefined)
|
|
211
|
+
this.cursorStart = undefined
|
|
212
|
+
this.cursor = undefined
|
|
213
|
+
if (cursor === undefined) return
|
|
214
|
+
try {
|
|
215
|
+
cursor.stdin.end(`${JSON.stringify({ op: 'stop' })}\n`)
|
|
216
|
+
} catch {
|
|
217
|
+
// A child that already closed its pipe is handled by the tree-exit wait below.
|
|
218
|
+
}
|
|
219
|
+
if (await cursor.waitForExit(AbortSignal.timeout(1_000))) return
|
|
220
|
+
cursor.terminate()
|
|
221
|
+
await cursor.waitForExit()
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Prepared integrity facts used by provider health. */
|
|
225
|
+
preparedInfo(): PreparedNativeHelper {
|
|
226
|
+
if (this.prepared === undefined) throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', 'native helper is not prepared')
|
|
227
|
+
return this.prepared
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private async buildManaged(signal: AbortSignal): Promise<void> {
|
|
231
|
+
const script = fileURLToPath(new URL('../../scripts/build-native.mjs', import.meta.url))
|
|
232
|
+
const handle = this.ctx.subprocess.spawn({
|
|
233
|
+
argv: [process.execPath, script, '--helper-only'],
|
|
234
|
+
cwd: dirname(script),
|
|
235
|
+
stdio: {
|
|
236
|
+
stdin: 'ignore',
|
|
237
|
+
stdout: { maxBytes: 256 * 1024 },
|
|
238
|
+
stderr: { maxBytes: 256 * 1024 },
|
|
239
|
+
},
|
|
240
|
+
graceMs: 1000,
|
|
241
|
+
signal,
|
|
242
|
+
env: {},
|
|
243
|
+
})
|
|
244
|
+
const outcome = await handle.done
|
|
245
|
+
if (outcome.exitCode !== 0) {
|
|
246
|
+
const stderr = collected(handle.collected.stderr)
|
|
247
|
+
throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', `explicit native source build failed: ${stderr.slice(0, 1000)}`)
|
|
248
|
+
}
|
|
249
|
+
const info = await stat(this.helperPath).catch(() => undefined)
|
|
250
|
+
if (info?.isFile() !== true) throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', 'native source build completed without producing the helper')
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private async getCursor(prepared: PreparedNativeHelper, signal: AbortSignal): Promise<CursorProcess> {
|
|
254
|
+
signal.throwIfAborted()
|
|
255
|
+
if (this.cursor !== undefined) return this.cursor
|
|
256
|
+
if (this.cursorStart === undefined) {
|
|
257
|
+
const start: { promise: Promise<CursorProcess> } = { promise: Promise.resolve(undefined as never) }
|
|
258
|
+
start.promise = this.spawnCursor(prepared).then(cursor => {
|
|
259
|
+
this.cursor = cursor
|
|
260
|
+
void cursor.done.catch(() => undefined).finally(() => {
|
|
261
|
+
if (this.cursor === cursor) this.cursor = undefined
|
|
262
|
+
})
|
|
263
|
+
return cursor
|
|
264
|
+
}).finally(() => {
|
|
265
|
+
if (this.cursorStart === start) this.cursorStart = undefined
|
|
266
|
+
})
|
|
267
|
+
this.cursorStart = start
|
|
268
|
+
}
|
|
269
|
+
const cursor = await this.cursorStart.promise
|
|
270
|
+
signal.throwIfAborted()
|
|
271
|
+
return cursor
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private async spawnCursor(prepared: PreparedNativeHelper): Promise<CursorProcess> {
|
|
275
|
+
const cursorSignal = new AbortController()
|
|
276
|
+
const handle = this.ctx.subprocess.spawn({
|
|
277
|
+
argv: [prepared.path, '--cursor-overlay'],
|
|
278
|
+
cwd: dirname(prepared.path),
|
|
279
|
+
stdio: {
|
|
280
|
+
stdin: 'pipe',
|
|
281
|
+
stdout: 'pipe',
|
|
282
|
+
stderr: { maxBytes: 64 * 1024 },
|
|
283
|
+
},
|
|
284
|
+
graceMs: 1000,
|
|
285
|
+
signal: cursorSignal.signal,
|
|
286
|
+
env: {
|
|
287
|
+
LANG: process.env.LANG ?? 'en_US.UTF-8',
|
|
288
|
+
LC_ALL: process.env.LC_ALL ?? 'en_US.UTF-8',
|
|
289
|
+
},
|
|
290
|
+
})
|
|
291
|
+
if (handle.stdin === undefined || handle.stdout === undefined) {
|
|
292
|
+
handle.terminate()
|
|
293
|
+
await handle.waitForExit()
|
|
294
|
+
throw new ComputerUseError('COMPUTER_PROVIDER_FAILURE', 'native cursor overlay protocol pipes are unavailable')
|
|
295
|
+
}
|
|
296
|
+
handle.stdin.on('error', () => {
|
|
297
|
+
// Process outcome and the next command own recovery from a closed protocol pipe.
|
|
298
|
+
})
|
|
299
|
+
try {
|
|
300
|
+
await this.waitForCursorReady(handle)
|
|
301
|
+
} catch (error) {
|
|
302
|
+
handle.terminate()
|
|
303
|
+
await handle.waitForExit()
|
|
304
|
+
const stderr = collected(handle.collected.stderr).trim()
|
|
305
|
+
throw new ComputerUseError(
|
|
306
|
+
'COMPUTER_PROVIDER_FAILURE',
|
|
307
|
+
`native cursor overlay failed to become ready${stderr.length === 0 ? '' : `: ${stderr.slice(0, 1000)}`}`,
|
|
308
|
+
{ cause: error },
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
handle.stdout.resume()
|
|
312
|
+
return {
|
|
313
|
+
stdin: handle.stdin,
|
|
314
|
+
done: handle.done,
|
|
315
|
+
terminate: () => {
|
|
316
|
+
cursorSignal.abort()
|
|
317
|
+
handle.terminate()
|
|
318
|
+
},
|
|
319
|
+
waitForExit: signal => handle.waitForExit(signal),
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private async waitForCursorReady(handle: SubprocessHandle): Promise<void> {
|
|
324
|
+
const stdout = handle.stdout
|
|
325
|
+
if (stdout === undefined) throw new Error('cursor stdout is unavailable')
|
|
326
|
+
const timeout = AbortSignal.timeout(CURSOR_READY_TIMEOUT_MS)
|
|
327
|
+
await new Promise<void>((resolveReady, rejectReady) => {
|
|
328
|
+
let buffer = ''
|
|
329
|
+
let settled = false
|
|
330
|
+
const cleanup = (): void => {
|
|
331
|
+
clearListeners()
|
|
332
|
+
timeout.removeEventListener('abort', onTimeout)
|
|
333
|
+
}
|
|
334
|
+
const succeed = (): void => {
|
|
335
|
+
if (settled) return
|
|
336
|
+
settled = true
|
|
337
|
+
cleanup()
|
|
338
|
+
resolveReady()
|
|
339
|
+
}
|
|
340
|
+
const fail = (error: unknown): void => {
|
|
341
|
+
if (settled) return
|
|
342
|
+
settled = true
|
|
343
|
+
cleanup()
|
|
344
|
+
rejectReady(error)
|
|
345
|
+
}
|
|
346
|
+
const onData = (chunk: Buffer | string): void => {
|
|
347
|
+
buffer += chunk.toString()
|
|
348
|
+
if (Buffer.byteLength(buffer) > CURSOR_PROTOCOL_MAX_BYTES) {
|
|
349
|
+
fail(new Error('cursor ready response exceeded its protocol limit'))
|
|
350
|
+
return
|
|
351
|
+
}
|
|
352
|
+
while (true) {
|
|
353
|
+
const newline = buffer.indexOf('\n')
|
|
354
|
+
if (newline < 0) return
|
|
355
|
+
const line = buffer.slice(0, newline)
|
|
356
|
+
buffer = buffer.slice(newline + 1)
|
|
357
|
+
if (line.length === 0) continue
|
|
358
|
+
let response: unknown
|
|
359
|
+
try {
|
|
360
|
+
response = JSON.parse(line)
|
|
361
|
+
} catch (error) {
|
|
362
|
+
fail(error)
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
if (typeof response === 'object' && response !== null
|
|
366
|
+
&& (response as { ok?: unknown }).ok === true
|
|
367
|
+
&& (response as { ready?: unknown }).ready === true) {
|
|
368
|
+
succeed()
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
fail(new Error('cursor overlay returned an unexpected ready response'))
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
const onEnd = (): void => { fail(new Error('cursor overlay stdout closed before ready')) }
|
|
376
|
+
const onError = (error: Error): void => { fail(error) }
|
|
377
|
+
const onTimeout = (): void => { fail(new Error(`cursor overlay ready timeout after ${CURSOR_READY_TIMEOUT_MS} milliseconds`)) }
|
|
378
|
+
const clearListeners = (): void => {
|
|
379
|
+
stdout.removeListener('data', onData)
|
|
380
|
+
stdout.removeListener('end', onEnd)
|
|
381
|
+
stdout.removeListener('error', onError)
|
|
382
|
+
}
|
|
383
|
+
stdout.on('data', onData)
|
|
384
|
+
stdout.once('end', onEnd)
|
|
385
|
+
stdout.once('error', onError)
|
|
386
|
+
timeout.addEventListener('abort', onTimeout, { once: true })
|
|
387
|
+
void handle.done.then(
|
|
388
|
+
outcome => { fail(new Error(`cursor overlay exited before ready (${String(outcome.exitCode ?? outcome.signal)})`)) },
|
|
389
|
+
error => { fail(error) },
|
|
390
|
+
)
|
|
391
|
+
})
|
|
392
|
+
}
|
|
393
|
+
}
|