@celljs/ta-computer-use 0.0.0-stage → 4.8.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.en.md +37 -0
- package/README.md +36 -2
- package/extension/index.mjs +551 -0
- package/package.json +49 -4
- package/src/cua-driver-access.spec.ts +147 -0
- package/src/cua-driver-access.ts +188 -0
- package/src/cua-driver-daemon.spec.ts +168 -0
- package/src/cua-driver-daemon.ts +283 -0
- package/src/cua-driver.spec.ts +85 -0
- package/src/cua-driver.ts +130 -0
- package/src/extension.spec.ts +279 -0
- package/src/extension.ts +195 -0
- package/src/pi-types.ts +2 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { CuaDriver, type CuaDriverProgram } from './cua-driver';
|
|
3
|
+
import type { CuaDriverAccess, CuaDriverReadiness } from './cua-driver-access';
|
|
4
|
+
import type { CuaDriverDaemon } from './cua-driver-daemon';
|
|
5
|
+
import { activate, type ComputerUseOptions, type CuaDriverSetup } from './extension';
|
|
6
|
+
import type { ExtensionAPI } from './pi-types';
|
|
7
|
+
|
|
8
|
+
type Handler = (event: unknown, ctx: unknown) => unknown;
|
|
9
|
+
|
|
10
|
+
/** The part of pi's extension API the extension uses; a registered server "connects" after `connectMs`. */
|
|
11
|
+
class FakePi {
|
|
12
|
+
readonly handlers = new Map<string, Handler>();
|
|
13
|
+
readonly servers: { name: string; config: Record<string, unknown> }[] = [];
|
|
14
|
+
readonly tools: { name: string; reachesTools?: (input: Record<string, unknown>) => 'all' | readonly string[] }[] = [
|
|
15
|
+
{
|
|
16
|
+
name: 'codemode',
|
|
17
|
+
reachesTools: input => (/searchTools|describeNamespace/.test(String(input.code)) ? 'all' : (String(input.code).match(/\w+/g) ?? [])),
|
|
18
|
+
},
|
|
19
|
+
{ name: 'bash' },
|
|
20
|
+
];
|
|
21
|
+
readonly commands = new Map<string, { handler: (args: string, ctx: unknown) => Promise<void> }>();
|
|
22
|
+
readonly notes: string[] = [];
|
|
23
|
+
readonly events: string[] = [];
|
|
24
|
+
connectMs = 0;
|
|
25
|
+
|
|
26
|
+
get api(): ExtensionAPI {
|
|
27
|
+
return {
|
|
28
|
+
on: (event: string, handler: Handler) => this.handlers.set(event, handler),
|
|
29
|
+
getAllTools: () => this.tools,
|
|
30
|
+
getMcpServers: () => this.servers,
|
|
31
|
+
registerMcpServer: (name: string, config: Record<string, unknown>) => {
|
|
32
|
+
this.events.push('register');
|
|
33
|
+
this.servers.push({ name, config });
|
|
34
|
+
setTimeout(() => this.tools.push({ name: `${CuaDriver.NAMESPACE}__click` }), this.connectMs);
|
|
35
|
+
},
|
|
36
|
+
registerCommand: (name: string, command: { handler: (args: string, ctx: unknown) => Promise<void> }) => this.commands.set(name, command),
|
|
37
|
+
} as unknown as ExtensionAPI;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get ctx(): unknown {
|
|
41
|
+
return { ui: { notify: (text: string) => this.notes.push(text) } };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
prompt(): Record<string, string> {
|
|
45
|
+
const sections: Record<string, string> = {};
|
|
46
|
+
this.handlers.get('before_agent_start')!({ systemPromptOptions: { sections } }, {});
|
|
47
|
+
return sections;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
call(toolName: string, input: Record<string, unknown>, signal?: AbortSignal): Promise<unknown> {
|
|
51
|
+
return Promise.resolve(this.handlers.get('tool_call')!({ toolName, input }, { ...(this.ctx as object), signal }));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const CLICK = { code: 'await tools.mcp__cua_driver__click({ x: 1 })' };
|
|
56
|
+
const READY: CuaDriverReadiness = { ready: true, notes: [], known: true };
|
|
57
|
+
const OWN: CuaDriverProgram = { path: '/bin/cua-driver', bundled: false };
|
|
58
|
+
|
|
59
|
+
type FakeSetup = CuaDriverSetup & { checks: number; ensured: number };
|
|
60
|
+
|
|
61
|
+
/** A setup whose check answers `answers` in turn (the last one again), and whose daemon, if any, counts its starts. */
|
|
62
|
+
function fakeSetup(pi: FakePi, program: CuaDriverProgram, answers: CuaDriverReadiness[], withDaemon = false): FakeSetup {
|
|
63
|
+
const setup = {
|
|
64
|
+
program,
|
|
65
|
+
args: withDaemon ? ['mcp', '--embedded', '--socket', '/c/s.sock'] : ['mcp'],
|
|
66
|
+
ready: false,
|
|
67
|
+
checks: 0,
|
|
68
|
+
ensured: 0,
|
|
69
|
+
} as unknown as FakeSetup;
|
|
70
|
+
setup.access = {
|
|
71
|
+
check: async (notify: (message: string) => void) => {
|
|
72
|
+
pi.events.push('check');
|
|
73
|
+
const answer = answers[Math.min(setup.checks++, answers.length - 1)];
|
|
74
|
+
if (!answer.ready) {
|
|
75
|
+
notify('asked');
|
|
76
|
+
}
|
|
77
|
+
return answer;
|
|
78
|
+
},
|
|
79
|
+
} as unknown as CuaDriverAccess;
|
|
80
|
+
if (withDaemon) {
|
|
81
|
+
setup.daemon = {
|
|
82
|
+
socket: '/c/s.sock',
|
|
83
|
+
ensure: async () => {
|
|
84
|
+
pi.events.push('ensure');
|
|
85
|
+
setup.ensured++;
|
|
86
|
+
},
|
|
87
|
+
reap: async () => undefined,
|
|
88
|
+
} as unknown as CuaDriverDaemon;
|
|
89
|
+
}
|
|
90
|
+
return setup;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function options(
|
|
94
|
+
locate: () => CuaDriverProgram | undefined,
|
|
95
|
+
setup: (program: CuaDriverProgram) => CuaDriverSetup,
|
|
96
|
+
platform: NodeJS.Platform = 'linux',
|
|
97
|
+
): ComputerUseOptions {
|
|
98
|
+
return { platform, locate, setup };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
describe('the computer-use extension', () => {
|
|
102
|
+
it('without Cua Driver, adds nothing to the prompt and registers nothing', async () => {
|
|
103
|
+
const pi = new FakePi();
|
|
104
|
+
activate(
|
|
105
|
+
pi.api,
|
|
106
|
+
options(
|
|
107
|
+
() => undefined,
|
|
108
|
+
() => {
|
|
109
|
+
throw new Error('no setup');
|
|
110
|
+
},
|
|
111
|
+
),
|
|
112
|
+
);
|
|
113
|
+
expect(pi.prompt()).toEqual({});
|
|
114
|
+
await pi.call('codemode', CLICK);
|
|
115
|
+
expect(pi.servers).toEqual([]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('names the driver in the prompt, and starts it only for a call that names its tools', async () => {
|
|
119
|
+
const pi = new FakePi();
|
|
120
|
+
let looked = 0;
|
|
121
|
+
const setup = fakeSetup(pi, OWN, [READY]);
|
|
122
|
+
activate(
|
|
123
|
+
pi.api,
|
|
124
|
+
options(
|
|
125
|
+
() => {
|
|
126
|
+
looked++;
|
|
127
|
+
return OWN;
|
|
128
|
+
},
|
|
129
|
+
() => setup,
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
expect(pi.prompt()[CuaDriver.SECTION]).toBe(CuaDriver.prompt('linux'));
|
|
133
|
+
pi.prompt();
|
|
134
|
+
expect(looked).toBe(1);
|
|
135
|
+
await pi.call('bash', { command: 'ls' });
|
|
136
|
+
await pi.call('codemode', { code: 'await tools.bash({ command: "ls" })' });
|
|
137
|
+
await pi.call('codemode', { code: 'searchTools("click")' });
|
|
138
|
+
expect(pi.servers).toEqual([]);
|
|
139
|
+
pi.connectMs = 40;
|
|
140
|
+
expect(await pi.call('codemode', CLICK)).toBeUndefined();
|
|
141
|
+
expect(pi.servers).toEqual([
|
|
142
|
+
{ name: CuaDriver.SERVER, config: { command: '/bin/cua-driver', args: ['mcp'], env: undefined, description: CuaDriver.DESCRIPTION } },
|
|
143
|
+
]);
|
|
144
|
+
expect(pi.tools.some(tool => tool.name === 'mcp__cua_driver__click')).toBe(true);
|
|
145
|
+
await pi.call('codemode', { code: 'describeNamespace("mcp__cua_driver")' });
|
|
146
|
+
expect(pi.servers.length).toBe(1);
|
|
147
|
+
// The user's own install starts its daemon when it connects: connected first, then checked, once.
|
|
148
|
+
expect(pi.events).toEqual(['register', 'check']);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('registers and checks once for calls at the same time', async () => {
|
|
152
|
+
const pi = new FakePi();
|
|
153
|
+
const setup = fakeSetup(pi, OWN, [READY]);
|
|
154
|
+
activate(
|
|
155
|
+
pi.api,
|
|
156
|
+
options(
|
|
157
|
+
() => OWN,
|
|
158
|
+
() => setup,
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
pi.prompt();
|
|
162
|
+
pi.connectMs = 20;
|
|
163
|
+
await Promise.all([pi.call('codemode', CLICK), pi.call('codemode', CLICK)]);
|
|
164
|
+
expect(pi.servers.length).toBe(1);
|
|
165
|
+
expect(setup.checks).toBe(1);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('looks again on each prompt until the driver is installed', () => {
|
|
169
|
+
const pi = new FakePi();
|
|
170
|
+
let installed: CuaDriverProgram | undefined;
|
|
171
|
+
activate(
|
|
172
|
+
pi.api,
|
|
173
|
+
options(
|
|
174
|
+
() => installed,
|
|
175
|
+
program => fakeSetup(pi, program, [READY]),
|
|
176
|
+
),
|
|
177
|
+
);
|
|
178
|
+
expect(pi.prompt()).toEqual({});
|
|
179
|
+
installed = OWN;
|
|
180
|
+
expect(pi.prompt()[CuaDriver.SECTION]).toBeDefined();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('stops waiting for the server when the call is stopped', async () => {
|
|
184
|
+
const pi = new FakePi();
|
|
185
|
+
activate(
|
|
186
|
+
pi.api,
|
|
187
|
+
options(
|
|
188
|
+
() => OWN,
|
|
189
|
+
program => fakeSetup(pi, program, [READY]),
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
pi.prompt();
|
|
193
|
+
pi.connectMs = 10_000;
|
|
194
|
+
const stop = new AbortController();
|
|
195
|
+
const started = Date.now();
|
|
196
|
+
const call = pi.call('codemode', CLICK, stop.signal);
|
|
197
|
+
setTimeout(() => stop.abort(), 30);
|
|
198
|
+
await call;
|
|
199
|
+
expect(Date.now() - started).toBeLessThan(1000);
|
|
200
|
+
expect(pi.servers.length).toBe(1);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('stops a call with what the system lacks, checks again on the next, and passes the notes on once', async () => {
|
|
204
|
+
const pi = new FakePi();
|
|
205
|
+
const setup = fakeSetup(pi, OWN, [
|
|
206
|
+
{ ready: false, reason: 'no AT-SPI' },
|
|
207
|
+
{ ready: true, notes: ['AT-SPI: slow'], known: true },
|
|
208
|
+
]);
|
|
209
|
+
activate(
|
|
210
|
+
pi.api,
|
|
211
|
+
options(
|
|
212
|
+
() => OWN,
|
|
213
|
+
() => setup,
|
|
214
|
+
),
|
|
215
|
+
);
|
|
216
|
+
pi.prompt();
|
|
217
|
+
expect(await pi.call('codemode', CLICK)).toEqual({ block: true, reason: 'no AT-SPI' });
|
|
218
|
+
expect(await pi.call('codemode', CLICK)).toBeUndefined();
|
|
219
|
+
await pi.call('codemode', CLICK);
|
|
220
|
+
expect(pi.notes).toEqual(['asked', 'AT-SPI: slow']);
|
|
221
|
+
expect(setup.checks).toBe(2);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('asks again when the system could not tell', async () => {
|
|
225
|
+
const pi = new FakePi();
|
|
226
|
+
const setup = fakeSetup(pi, OWN, [{ ready: true, notes: [], known: false }, READY]);
|
|
227
|
+
activate(
|
|
228
|
+
pi.api,
|
|
229
|
+
options(
|
|
230
|
+
() => OWN,
|
|
231
|
+
() => setup,
|
|
232
|
+
),
|
|
233
|
+
);
|
|
234
|
+
pi.prompt();
|
|
235
|
+
await pi.call('codemode', CLICK);
|
|
236
|
+
await pi.call('codemode', CLICK);
|
|
237
|
+
await pi.call('codemode', CLICK);
|
|
238
|
+
expect(setup.checks).toBe(2);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("with Ta Desktop's daemon, starts it and checks before the server connects, and keeps it running", async () => {
|
|
242
|
+
const pi = new FakePi();
|
|
243
|
+
const program = { path: '/T/CuaDriver.app/Contents/MacOS/cua-driver', bundled: true };
|
|
244
|
+
const setup = fakeSetup(pi, program, [READY], true);
|
|
245
|
+
activate(
|
|
246
|
+
pi.api,
|
|
247
|
+
options(
|
|
248
|
+
() => program,
|
|
249
|
+
() => setup,
|
|
250
|
+
'darwin',
|
|
251
|
+
),
|
|
252
|
+
);
|
|
253
|
+
pi.prompt();
|
|
254
|
+
await pi.call('codemode', { code: 'tools.bash({})' });
|
|
255
|
+
expect(setup.ensured).toBe(0);
|
|
256
|
+
await pi.call('codemode', CLICK);
|
|
257
|
+
await pi.call('codemode', CLICK);
|
|
258
|
+
expect(pi.events).toEqual(['ensure', 'check', 'register', 'ensure']);
|
|
259
|
+
expect(pi.servers[0].config.args).toEqual(['mcp', '--embedded', '--socket', '/c/s.sock']);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('/computer-use says where the driver is or how to install it, and what the system lacks', async () => {
|
|
263
|
+
const pi = new FakePi();
|
|
264
|
+
let installed: CuaDriverProgram | undefined;
|
|
265
|
+
const setup = fakeSetup(pi, OWN, [{ ready: false, reason: 'no display' }]);
|
|
266
|
+
activate(
|
|
267
|
+
pi.api,
|
|
268
|
+
options(
|
|
269
|
+
() => installed,
|
|
270
|
+
() => setup,
|
|
271
|
+
),
|
|
272
|
+
);
|
|
273
|
+
await pi.commands.get('computer-use')!.handler('', pi.ctx);
|
|
274
|
+
expect(pi.notes[0]).toContain('cua.ai/driver/install');
|
|
275
|
+
installed = OWN;
|
|
276
|
+
await pi.commands.get('computer-use')!.handler('', pi.ctx);
|
|
277
|
+
expect(pi.notes.slice(1)).toEqual([CuaDriver.status(OWN, 'linux'), 'asked', 'no display']);
|
|
278
|
+
});
|
|
279
|
+
});
|
package/src/extension.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { CuaDriver, type CuaDriverProgram } from './cua-driver';
|
|
5
|
+
import { CuaDriverAccess } from './cua-driver-access';
|
|
6
|
+
import { CuaDriverDaemon } from './cua-driver-daemon';
|
|
7
|
+
import type { ExtensionAPI } from './pi-types';
|
|
8
|
+
|
|
9
|
+
/** How long a call waits the first time for Cua Driver's tools; it connects in tens of milliseconds. */
|
|
10
|
+
const CONNECT_WAIT_MS = 5000;
|
|
11
|
+
const POLL_MS = 15;
|
|
12
|
+
|
|
13
|
+
/** A program of Cua Driver and what goes with it, shared by the sessions of a process. */
|
|
14
|
+
export interface CuaDriverSetup {
|
|
15
|
+
program: CuaDriverProgram;
|
|
16
|
+
/** The daemon Ta runs, for the driver of Ta Desktop on macOS. */
|
|
17
|
+
daemon?: CuaDriverDaemon;
|
|
18
|
+
access: CuaDriverAccess;
|
|
19
|
+
/** The arguments and environment of its MCP server. */
|
|
20
|
+
args: string[];
|
|
21
|
+
env?: Record<string, string>;
|
|
22
|
+
/** Checked, once for the process. */
|
|
23
|
+
ready: boolean;
|
|
24
|
+
checking?: Promise<Awaited<ReturnType<CuaDriverAccess['check']>>>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** What the extension depends on, so that tests can give their own. */
|
|
28
|
+
export interface ComputerUseOptions {
|
|
29
|
+
locate(): CuaDriverProgram | undefined;
|
|
30
|
+
platform: NodeJS.Platform;
|
|
31
|
+
setup(program: CuaDriverProgram, platform: NodeJS.Platform): CuaDriverSetup;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const setups = new Map<string, CuaDriverSetup>();
|
|
35
|
+
|
|
36
|
+
/** One per program and process: the sessions of `ta serve` share its daemon and its check. */
|
|
37
|
+
export function setupOf(found: CuaDriverProgram, platform: NodeJS.Platform): CuaDriverSetup {
|
|
38
|
+
// LaunchServices finds an app by its exact path: one with `..` or a link in it is another app to it.
|
|
39
|
+
const program = { ...found, path: realPath(found.path) };
|
|
40
|
+
let setup = setups.get(program.path);
|
|
41
|
+
if (!setup) {
|
|
42
|
+
const env = program.bundled ? { ...CuaDriver.BUNDLED_ENV_VARS } : undefined;
|
|
43
|
+
const app = program.bundled && platform === 'darwin' ? CuaDriver.appOf(program.path) : undefined;
|
|
44
|
+
const daemon = app ? new CuaDriverDaemon(app, { env }) : undefined;
|
|
45
|
+
const access = new CuaDriverAccess(daemon ? join(daemon.app, 'Contents', 'MacOS', 'cua-driver') : program.path, {
|
|
46
|
+
platform,
|
|
47
|
+
socket: daemon?.socket,
|
|
48
|
+
env,
|
|
49
|
+
beforeLook: daemon ? () => daemon.ensure() : undefined,
|
|
50
|
+
openSettings: pane => execFile('open', [`x-apple.systempreferences:com.apple.preference.security?${pane}`], () => undefined),
|
|
51
|
+
});
|
|
52
|
+
const args = daemon ? ['mcp', '--embedded', '--socket', daemon.socket] : ['mcp'];
|
|
53
|
+
setup = { program, daemon, access, args, env, ready: false };
|
|
54
|
+
setups.set(program.path, setup);
|
|
55
|
+
}
|
|
56
|
+
return setup;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function realPath(path: string): string {
|
|
60
|
+
try {
|
|
61
|
+
return realpathSync(path);
|
|
62
|
+
} catch {
|
|
63
|
+
return path;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const DEFAULTS: ComputerUseOptions = { locate: () => CuaDriver.locate(), platform: process.platform, setup: setupOf };
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Lets the agent use the desktop apps of this computer through Cua Driver, at no cost until it does.
|
|
71
|
+
* Registered up front, an MCP server starts in every session: its process (about 10 MB) and pi's MCP
|
|
72
|
+
* client. So the system prompt only names it, and the server is registered when a call first names its
|
|
73
|
+
* tools; pi connects it before the call runs. Each prompt looks for the program (a few `stat`s, nothing
|
|
74
|
+
* is run) until it is found, so a driver installed meanwhile is used without a restart.
|
|
75
|
+
*
|
|
76
|
+
* The first call also checks what the system needs (CuaDriverAccess): on macOS it waits while the user
|
|
77
|
+
* gives the permissions it asked for, elsewhere it says what is missing, so that the agent's call goes on
|
|
78
|
+
* or fails with what to do, not with an error of the driver.
|
|
79
|
+
*/
|
|
80
|
+
export function activate(pi: ExtensionAPI, options: ComputerUseOptions = DEFAULTS): void {
|
|
81
|
+
let setup: CuaDriverSetup | undefined;
|
|
82
|
+
/** Registered, and waited for once: later calls are pi's to wait for, as for any server it knows. */
|
|
83
|
+
let connected = false;
|
|
84
|
+
let connecting: Promise<void> | undefined;
|
|
85
|
+
const prefix = `${CuaDriver.NAMESPACE}__`;
|
|
86
|
+
const ready = () => pi.getAllTools().some(tool => tool.name.startsWith(prefix));
|
|
87
|
+
|
|
88
|
+
const found = (): CuaDriverSetup | undefined => {
|
|
89
|
+
if (!setup) {
|
|
90
|
+
const program = options.locate();
|
|
91
|
+
setup = program ? options.setup(program, options.platform) : undefined;
|
|
92
|
+
}
|
|
93
|
+
return setup;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const connect = async (driver: CuaDriverSetup, signal: AbortSignal | undefined): Promise<void> => {
|
|
97
|
+
if (!pi.getMcpServers().some(server => server.name === CuaDriver.SERVER)) {
|
|
98
|
+
pi.registerMcpServer(CuaDriver.SERVER, { command: driver.program.path, args: driver.args, env: driver.env, description: CuaDriver.DESCRIPTION });
|
|
99
|
+
}
|
|
100
|
+
// pi's MCP extension waits for a server it already knows; this one may come after this handler.
|
|
101
|
+
const deadline = Date.now() + CONNECT_WAIT_MS;
|
|
102
|
+
while (!ready() && Date.now() < deadline && !signal?.aborted) {
|
|
103
|
+
await new Promise(resolve => setTimeout(resolve, POLL_MS));
|
|
104
|
+
}
|
|
105
|
+
// A call stopped meanwhile leaves the wait to the next one.
|
|
106
|
+
connected = !signal?.aborted;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** Once for the process; the calls at the same time share it. */
|
|
110
|
+
const check = async (driver: CuaDriverSetup, notify: (message: string) => void, signal: AbortSignal | undefined): Promise<string | undefined> => {
|
|
111
|
+
driver.checking ??= driver.access.check(notify, signal).finally(() => {
|
|
112
|
+
driver.checking = undefined;
|
|
113
|
+
});
|
|
114
|
+
const result = await driver.checking;
|
|
115
|
+
if (!result.ready) {
|
|
116
|
+
return result.reason;
|
|
117
|
+
}
|
|
118
|
+
if (result.known && !driver.ready) {
|
|
119
|
+
driver.ready = true;
|
|
120
|
+
for (const note of result.notes) {
|
|
121
|
+
notify(note);
|
|
122
|
+
}
|
|
123
|
+
void driver.daemon?.reap();
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
pi.on('before_agent_start', event => {
|
|
129
|
+
const { sections } = event.systemPromptOptions;
|
|
130
|
+
if (found()) {
|
|
131
|
+
sections[CuaDriver.SECTION] = CuaDriver.prompt(options.platform);
|
|
132
|
+
} else {
|
|
133
|
+
delete sections[CuaDriver.SECTION];
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
pi.on('tool_call', async (event, ctx) => {
|
|
138
|
+
const driver = setup;
|
|
139
|
+
if (!driver) {
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
|
142
|
+
if (driver.ready && connected && !driver.daemon) {
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
const tool = pi.getAllTools().find(candidate => candidate.name === event.toolName);
|
|
146
|
+
if (!CuaDriver.reaches(tool?.reachesTools?.(event.input), event.input)) {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
const notify = (message: string) => ctx.ui.notify(message, 'warning');
|
|
150
|
+
try {
|
|
151
|
+
// Every call that uses it, in case the daemon stopped: a check costs one connection.
|
|
152
|
+
await driver.daemon?.ensure();
|
|
153
|
+
// The daemon of Ta Desktop answers before the server connects: its relay needs the permissions.
|
|
154
|
+
// The user's own install starts its daemon when it connects, so it is asked afterwards.
|
|
155
|
+
const reason = driver.daemon && !driver.ready ? await check(driver, notify, ctx.signal) : undefined;
|
|
156
|
+
if (reason) {
|
|
157
|
+
return { block: true, reason };
|
|
158
|
+
}
|
|
159
|
+
if (!connected) {
|
|
160
|
+
connecting ??= connect(driver, ctx.signal).finally(() => {
|
|
161
|
+
connecting = undefined;
|
|
162
|
+
});
|
|
163
|
+
await connecting;
|
|
164
|
+
}
|
|
165
|
+
const after = !driver.daemon && !driver.ready ? await check(driver, notify, ctx.signal) : undefined;
|
|
166
|
+
return after ? { block: true, reason: after } : undefined;
|
|
167
|
+
} catch (error) {
|
|
168
|
+
return { block: true, reason: (error as Error).message };
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
pi.registerCommand('computer-use', {
|
|
173
|
+
description: 'Whether Ta can use the desktop apps of this computer (Cua Driver), and what the system still needs',
|
|
174
|
+
handler: async (_args, ctx) => {
|
|
175
|
+
const program = options.locate();
|
|
176
|
+
ctx.ui.notify(CuaDriver.status(program, options.platform), program ? 'info' : 'warning');
|
|
177
|
+
if (!program) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const driver = options.setup(program, options.platform);
|
|
181
|
+
await driver.daemon?.ensure();
|
|
182
|
+
const result = await driver.access.check(message => ctx.ui.notify(message, 'warning'), ctx.signal);
|
|
183
|
+
if (!result.ready) {
|
|
184
|
+
ctx.ui.notify(result.reason, 'warning');
|
|
185
|
+
} else if (result.known) {
|
|
186
|
+
driver.ready = true;
|
|
187
|
+
ctx.ui.notify(['Cua Driver has what it needs.', ...result.notes].join('\n'), result.notes.length > 0 ? 'warning' : 'info');
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export default function computerUse(pi: ExtensionAPI): void {
|
|
194
|
+
activate(pi);
|
|
195
|
+
}
|
package/src/pi-types.ts
ADDED