@platless/axom-cli 0.1.0 → 0.3.13

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.
Files changed (109) hide show
  1. package/README.md +1 -1
  2. package/dist/authScopes.js +19 -0
  3. package/dist/axom-patch/cli/README.md +110 -0
  4. package/dist/axom-patch/cli/pi-steering.json +37 -0
  5. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +34 -0
  6. package/dist/axom-patch/cli/probes/pi-steering.mjs +80 -0
  7. package/dist/axom-patch/cli/probes/qoder-steering.mjs +89 -0
  8. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +49 -0
  9. package/dist/axom-patch/cli/qoder-background-terminal.md +81 -0
  10. package/dist/axom-patch/cli/qoder-foreground-output.md +41 -0
  11. package/dist/axom-patch/cli/qoder-steering.json +52 -0
  12. package/dist/axom-patch/cli/qoder-steering.md +30 -0
  13. package/dist/axom-patch/cli/qoder-subagent-origin.json +73 -0
  14. package/dist/axom-patch/cli/qoder-subagent-subwork.md +73 -0
  15. package/dist/axom-patch/cli/runtime.json +37 -0
  16. package/dist/axom-patch/cli/steering/claude-steering-js.patch +89 -0
  17. package/dist/axom-patch/cli/steering/claude-steering-types.patch +28 -0
  18. package/dist/axom-patch/cli/steering/codex-steering.patch +57 -0
  19. package/dist/axom-patch/cli/steering/pi-steering.js +79 -0
  20. package/dist/cli.js +9 -1
  21. package/dist/commands/auth.js +4 -13
  22. package/dist/commands/execute.js +4 -4
  23. package/dist/commands/record.js +6 -6
  24. package/dist/commands/struct.js +4 -4
  25. package/dist/commands/workstation.js +2 -2
  26. package/dist/credentials.js +2 -2
  27. package/dist/install/claude/commands.js +13 -13
  28. package/dist/mcp.js +32 -14
  29. package/dist/workstation/acp/acpAgents.js +447 -166
  30. package/dist/workstation/acp/acpClient.js +232 -18
  31. package/dist/workstation/acp/acpSession.js +502 -74
  32. package/dist/workstation/acp/activityBusy.js +43 -0
  33. package/dist/workstation/acp/activityReporting.js +54 -0
  34. package/dist/workstation/acp/adoption.js +107 -49
  35. package/dist/workstation/acp/authUrlSniffer.js +36 -0
  36. package/dist/workstation/acp/contract.js +11 -6
  37. package/dist/workstation/acp/delegationMcp.js +24 -0
  38. package/dist/workstation/acp/draftInterceptor.js +9 -0
  39. package/dist/workstation/acp/managedPatches.js +228 -0
  40. package/dist/workstation/acp/patchAssets.js +63 -0
  41. package/dist/workstation/acp/permission.js +300 -34
  42. package/dist/workstation/acp/qoderStartup.js +45 -0
  43. package/dist/workstation/acp/registry/catalog.js +263 -4
  44. package/dist/workstation/acp/registry/detect.js +55 -0
  45. package/dist/workstation/acp/sessionScope.js +19 -0
  46. package/dist/workstation/acp/sessionUpdateMapper.js +39 -21
  47. package/dist/workstation/acp/terminalLogin.js +197 -0
  48. package/dist/workstation/acp/test/fakeAgent.js +185 -9
  49. package/dist/workstation/acp/validate.js +33 -5
  50. package/dist/workstation/agentSessions.js +532 -44
  51. package/dist/workstation/channels/agent.js +7 -1
  52. package/dist/workstation/channels/computer.js +152 -4
  53. package/dist/workstation/channels/management.js +3 -3
  54. package/dist/workstation/config.js +3 -1
  55. package/dist/workstation/connection/encryptedChannel.js +88 -21
  56. package/dist/workstation/daemon.js +25 -3
  57. package/dist/workstation/desktop/binding.js +36 -0
  58. package/dist/workstation/desktop/bridge.js +212 -0
  59. package/dist/workstation/desktop/flow-control.js +52 -0
  60. package/dist/workstation/desktop/focused-text.js +130 -0
  61. package/dist/workstation/desktop/protocol.js +35 -0
  62. package/dist/workstation/fileOps.js +277 -0
  63. package/dist/workstation/filePaths.js +174 -0
  64. package/dist/workstation/fileWorker.js +81 -0
  65. package/dist/workstation/fileWorkerClient.js +121 -0
  66. package/dist/workstation/oauth/deviceAuthorization.js +21 -12
  67. package/dist/workstation/protocol/agent.js +154 -5
  68. package/dist/workstation/protocol/common.js +6 -0
  69. package/dist/workstation/protocol/computer.js +92 -3
  70. package/dist/workstation/qoderAdapter/backgroundProcess.js +106 -0
  71. package/dist/workstation/qoderAdapter/backgroundTerminal.js +262 -0
  72. package/dist/workstation/qoderAdapter/completedContinuation.js +139 -0
  73. package/dist/workstation/qoderAdapter/contract.js +78 -0
  74. package/dist/workstation/qoderAdapter/foregroundOutput.js +30 -0
  75. package/dist/workstation/qoderAdapter/history.js +87 -0
  76. package/dist/workstation/qoderAdapter/identityIndex.js +191 -0
  77. package/dist/workstation/qoderAdapter/inputQueue.js +42 -0
  78. package/dist/workstation/qoderAdapter/main.js +93 -0
  79. package/dist/workstation/qoderAdapter/mapper.js +117 -0
  80. package/dist/workstation/qoderAdapter/observerPlugin.js +245 -0
  81. package/dist/workstation/qoderAdapter/observerReceiver.js +187 -0
  82. package/dist/workstation/qoderAdapter/permissions.js +61 -0
  83. package/dist/workstation/qoderAdapter/profile.js +93 -0
  84. package/dist/workstation/qoderAdapter/sdkDriver.js +49 -0
  85. package/dist/workstation/qoderAdapter/session.js +319 -0
  86. package/dist/workstation/qoderAdapter/subagentMeta.js +28 -0
  87. package/dist/workstation/qoderAdapter/subwork.js +77 -0
  88. package/dist/workstation/qoderAdapter/taskControl.js +38 -0
  89. package/dist/workstation/qoderAdapter/taskEvent.js +26 -0
  90. package/dist/workstation/qoderAdapter/taskLedger.js +163 -0
  91. package/dist/workstation/qoderAdapter/types.js +1 -0
  92. package/dist/workstation/qoderAdapter/usage.js +37 -0
  93. package/dist/workstation/register.js +4 -0
  94. package/dist/workstation/tools/browser.js +394 -0
  95. package/dist/workstation/tools/client.js +269 -0
  96. package/dist/workstation/tools/core.js +50 -0
  97. package/dist/workstation/tools/files.js +525 -0
  98. package/dist/workstation/tools/keeperProcess.js +114 -0
  99. package/dist/workstation/tools/terminal.js +214 -0
  100. package/dist/workstation/tools/validation.js +50 -0
  101. package/dist/workstation/update/coordinator.js +29 -0
  102. package/dist/workstation/update/npmInstaller.js +2 -0
  103. package/dist/workstation/update/ptyPreflight.js +24 -0
  104. package/dist/workstation/update/taskRegistry.js +44 -20
  105. package/dist/workstation/update/watchdog.js +66 -0
  106. package/package.json +14 -4
  107. package/skills/axom/SKILL.md +55 -17
  108. package/skills/axom-definition/SKILL.md +32 -32
  109. package/skills/axom-platform/SKILL.md +39 -39
@@ -0,0 +1,37 @@
1
+ export const QODER_CREDITS_LABEL = 'Qoder Credits(厂商报告)';
2
+ const validCredits = (value) => typeof value === 'number' &&
3
+ Number.isFinite(value) && value >= 0;
4
+ /** Qoder's report is cumulative. A missing/invalid report never erases a prior valid value. */
5
+ export function applyCredits(previous, reported) {
6
+ if (validCredits(reported))
7
+ return reported;
8
+ return validCredits(previous) ? previous : undefined;
9
+ }
10
+ function reportedCredits(value) {
11
+ if (!value || typeof value !== 'object' || Array.isArray(value))
12
+ return undefined;
13
+ const input = value;
14
+ for (const key of ['total_credits', 'totalCredits', 'credits'])
15
+ if (validCredits(input[key]))
16
+ return input[key];
17
+ return undefined;
18
+ }
19
+ /**
20
+ * Keeps vendor credits separate from ACP token/USD counters. Qoder's SDK does not
21
+ * provide equivalent token or USD values, so zero placeholders are intentionally omitted.
22
+ */
23
+ export function normalizeQoderUsage(value, previousCredits) {
24
+ const input = value && typeof value === 'object' && !Array.isArray(value)
25
+ ? value : {};
26
+ const scope = input.cumulativeScope === 'unknown' ? 'unknown' : 'session';
27
+ const reported = reportedCredits(value);
28
+ if (scope === 'unknown') {
29
+ return { credits: undefined, creditsLabel: QODER_CREDITS_LABEL, creditsScope: scope,
30
+ inputTokens: undefined, outputTokens: undefined, costUsd: undefined,
31
+ ...(validCredits(previousCredits) ? { previousCredits } : {}) };
32
+ }
33
+ const credits = applyCredits(previousCredits, reported);
34
+ return { credits, creditsLabel: QODER_CREDITS_LABEL, creditsScope: scope,
35
+ inputTokens: undefined, outputTokens: undefined, costUsd: undefined,
36
+ ...(validCredits(previousCredits) ? { previousCredits } : {}) };
37
+ }
@@ -32,6 +32,10 @@ export async function registerWorkstation(server, clientName, token, ws) {
32
32
  + `when the same machine re-registers after an axom-cli upgrade. Delete it under Settings → `
33
33
  + 'Workstations, then run `axom workstation start` again.');
34
34
  }
35
+ if (res.status === 403) {
36
+ throw new UsageError('This credential is not authorized to register a workstation. '
37
+ + 'Run `axom auth` and select workstation, then try again.');
38
+ }
35
39
  throw new Error(`Registration failed: ${res.status} ${await res.text()}`);
36
40
  }
37
41
  const data = (await res.json());
@@ -0,0 +1,394 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { integer, keys, string, validateRequest, TOOL_RESULT_BYTES, } from "./validation.js";
5
+ const TIMEOUT = 4500;
6
+ async function bounded(promise, timeout = TIMEOUT) {
7
+ let timer;
8
+ try {
9
+ return await Promise.race([
10
+ promise,
11
+ new Promise((_, reject) => {
12
+ timer = setTimeout(() => reject(Error("Browser operation timed out; mutation outcome may be unknown")), Math.max(1, timeout));
13
+ }),
14
+ ]);
15
+ }
16
+ finally {
17
+ clearTimeout(timer);
18
+ }
19
+ }
20
+ /** All ancestors are checked: mkdir recursive must never follow a planted profile symlink. */
21
+ async function ownedDirectory(dir, privateMode = false) {
22
+ const absolute = path.resolve(dir);
23
+ const parts = absolute.split(path.sep).filter(Boolean);
24
+ let current = path.parse(absolute).root;
25
+ const uid = process.getuid?.();
26
+ let protectedByOwner = false;
27
+ for (const part of parts) {
28
+ current = path.join(current, part);
29
+ await fs.mkdir(current, { mode: 0o700 }).catch((e) => {
30
+ if (e.code !== "EEXIST")
31
+ throw e;
32
+ });
33
+ const stat = await fs.lstat(current);
34
+ if (!stat.isDirectory() ||
35
+ stat.isSymbolicLink() ||
36
+ (!protectedByOwner &&
37
+ (stat.mode & 0o022) !== 0 &&
38
+ !(stat.mode & 0o1000)) ||
39
+ (protectedByOwner && uid !== undefined && stat.uid !== uid))
40
+ throw Error("Unsafe browser directory");
41
+ // An owned 0700 ancestor blocks other UIDs from reaching owned descendants,
42
+ // even if an existing config directory below that fence has group-write bits.
43
+ // Symlink and ownership checks still apply at every level.
44
+ if (uid !== undefined && stat.uid === uid && (stat.mode & 0o077) === 0)
45
+ protectedByOwner = true;
46
+ }
47
+ const stat = await fs.lstat(absolute);
48
+ if (process.getuid && stat.uid !== process.getuid())
49
+ throw Error("Browser directory is not owned by this user");
50
+ if (privateMode)
51
+ await fs.chmod(absolute, 0o700);
52
+ }
53
+ function allowedUrl(params) {
54
+ const url = string(params, "url", 8192, "about:blank");
55
+ if (url === "about:blank")
56
+ return url;
57
+ try {
58
+ const parsed = new URL(url);
59
+ if (["http:", "https:"].includes(parsed.protocol))
60
+ return parsed.href;
61
+ }
62
+ catch { }
63
+ throw Error("Invalid browser URL: use http(s) or about:blank");
64
+ }
65
+ function browserKey(params) {
66
+ const key = string(params, "key", 64);
67
+ if (!/^(?:(?:Control|Alt|Shift|Meta)\+)*(?:Enter|Backspace|Tab|Escape|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Delete|Home|End|PageUp|PageDown|[a-zA-Z0-9])$/.test(key))
68
+ throw Error("Invalid browser key");
69
+ return key;
70
+ }
71
+ export class BrowserCore {
72
+ options;
73
+ chromium;
74
+ error;
75
+ sessions = new Map();
76
+ queue = Promise.resolve();
77
+ disposed = false;
78
+ constructor(options, chromium, error) {
79
+ this.options = options;
80
+ this.chromium = chromium;
81
+ this.error = error;
82
+ }
83
+ static async create(options) {
84
+ try {
85
+ const { chromium } = await import("playwright-core");
86
+ const executablePath = options.executablePath ?? chromium.executablePath();
87
+ await fs.access(executablePath);
88
+ await ownedDirectory(options.home);
89
+ await ownedDirectory(options.profileRoot, true);
90
+ await ownedDirectory(path.join(options.home, "Downloads"));
91
+ return new BrowserCore({ ...options, executablePath }, chromium);
92
+ }
93
+ catch (error) {
94
+ return new BrowserCore(options, undefined, `Browser unavailable: ${error instanceof Error ? error.message : "missing optional browser dependency"}`);
95
+ }
96
+ }
97
+ dispatch(method, params = {}) {
98
+ const task = this.queue.then(async () => {
99
+ if (this.disposed)
100
+ throw Error("Browser supervisor stopped");
101
+ validateRequest(method, params);
102
+ const result = await this.execute(method, params);
103
+ if (Buffer.byteLength(JSON.stringify(result)) > TOOL_RESULT_BYTES - 1024)
104
+ throw Error("Browser frame exceeds reply limit");
105
+ return result;
106
+ });
107
+ this.queue = task.catch(() => { });
108
+ return task;
109
+ }
110
+ descriptor(s) {
111
+ return {
112
+ id: s.id,
113
+ name: s.name,
114
+ url: s.page.url(),
115
+ controllerId: s.controllerId,
116
+ exited: s.exited,
117
+ };
118
+ }
119
+ async navigate(s, url, deadline = Date.now() + TIMEOUT) {
120
+ try {
121
+ await s.page.goto(url, {
122
+ waitUntil: "domcontentloaded",
123
+ timeout: Math.max(1, Math.min(TIMEOUT, deadline - Date.now())),
124
+ });
125
+ }
126
+ catch (error) {
127
+ if (String(error).includes("Download is starting"))
128
+ return;
129
+ await s.page
130
+ .goto("about:blank", { waitUntil: "commit", timeout: 500 })
131
+ .catch(() => { });
132
+ throw error;
133
+ }
134
+ }
135
+ watch(s, page) {
136
+ page.on("dialog", (dialog) => {
137
+ s.notice = `${dialog.type()}: ${dialog.message().slice(0, 300)} (dismissed)`;
138
+ void dialog.dismiss().catch(() => { });
139
+ });
140
+ page.on("download", (download) => {
141
+ void this.download(s, download).catch((error) => {
142
+ s.notice = `Download failed: ${error.message}`;
143
+ });
144
+ });
145
+ page.on("close", () => {
146
+ if (s.page === page) {
147
+ const others = s.context.pages().filter((p) => !p.isClosed());
148
+ if (others.length) {
149
+ s.page = others[others.length - 1];
150
+ s.notice = "Active tab closed; switched to remaining tab";
151
+ }
152
+ else {
153
+ s.exited = true;
154
+ s.controllerId = null;
155
+ }
156
+ }
157
+ });
158
+ page.on("crash", () => {
159
+ if (s.page === page) {
160
+ s.exited = true;
161
+ s.notice = "Browser page crashed; close and create a new session";
162
+ }
163
+ });
164
+ }
165
+ async download(s, download) {
166
+ const downloads = path.join(this.options.home, "Downloads");
167
+ await ownedDirectory(downloads);
168
+ const before = await fs.lstat(downloads);
169
+ const staging = await fs.mkdtemp(path.join(downloads, ".axom-browser-"));
170
+ await fs.chmod(staging, 0o700);
171
+ const temporary = path.join(staging, "download");
172
+ const timer = setTimeout(() => {
173
+ void download.cancel().catch(() => { });
174
+ }, 30000);
175
+ try {
176
+ await download.saveAs(temporary);
177
+ const failure = await download.failure();
178
+ if (failure)
179
+ throw Error(failure);
180
+ await ownedDirectory(downloads);
181
+ const after = await fs.lstat(downloads);
182
+ if (before.ino !== after.ino || before.dev !== after.dev)
183
+ throw Error("Downloads directory changed");
184
+ let suggested = download
185
+ .suggestedFilename()
186
+ .replace(/[\\/\x00-\x1f\x7f]/g, "_")
187
+ .replace(/^\.+/, "")
188
+ .slice(0, 160) || "download";
189
+ while (Buffer.byteLength(suggested) > 180)
190
+ suggested = [...suggested].slice(0, -1).join("");
191
+ const ext = path.extname(suggested), stem = suggested.slice(0, suggested.length - ext.length);
192
+ for (let n = 0; n < 100; n++) {
193
+ const name = n ? `${stem} (${n})${ext}` : suggested;
194
+ try {
195
+ await fs.link(temporary, path.join(downloads, name));
196
+ s.notice = `Downloaded: ${name}`;
197
+ return;
198
+ }
199
+ catch (error) {
200
+ if (error.code !== "EEXIST")
201
+ throw error;
202
+ }
203
+ }
204
+ throw Error("Download filename collision limit reached");
205
+ }
206
+ finally {
207
+ clearTimeout(timer);
208
+ await fs.rm(staging, { recursive: true, force: true });
209
+ }
210
+ }
211
+ async execute(method, params) {
212
+ const deadline = Date.now() + 6000;
213
+ if (method === "tools.info") {
214
+ keys(params, []);
215
+ return {
216
+ browser: !!this.chromium,
217
+ ...(this.error ? { browserError: this.error } : {}),
218
+ };
219
+ }
220
+ if (!method.startsWith("browser."))
221
+ throw Error("Unsupported browser tool");
222
+ if (!this.chromium)
223
+ throw Error(this.error);
224
+ if (method === "browser.list") {
225
+ keys(params, []);
226
+ return {
227
+ sessions: [...this.sessions.values()].map((s) => this.descriptor(s)),
228
+ };
229
+ }
230
+ const id = string(params, "id", 128), clientId = string(params, "clientId", 128);
231
+ if (method === "browser.create") {
232
+ keys(params, ["id", "clientId", "url", "width", "height"]);
233
+ const url = allowedUrl(params), width = integer(params, "width", 320, 1600, 1024), height = integer(params, "height", 240, 1200, 768);
234
+ const existing = this.sessions.get(id);
235
+ if (existing)
236
+ return this.descriptor(existing);
237
+ if (this.sessions.size >= 2)
238
+ throw Error("Maximum browser session limit reached (2)");
239
+ await ownedDirectory(this.options.profileRoot, true);
240
+ const profile = path.join(this.options.profileRoot, createHash("sha256").update(id).digest("hex"));
241
+ await ownedDirectory(profile, true);
242
+ const context = await this.chromium.launchPersistentContext(profile, {
243
+ executablePath: this.options.executablePath,
244
+ headless: true,
245
+ acceptDownloads: true,
246
+ viewport: { width, height },
247
+ timeout: TIMEOUT,
248
+ ...(this.options.proxy
249
+ ? { proxy: { server: this.options.proxy } }
250
+ : {}),
251
+ args: process.platform === "linux" ? ["--no-sandbox"] : [],
252
+ });
253
+ context.setDefaultTimeout(TIMEOUT);
254
+ context.setDefaultNavigationTimeout(TIMEOUT);
255
+ const page = context.pages()[0] ?? (await context.newPage());
256
+ const s = {
257
+ id,
258
+ name: "Browser",
259
+ context,
260
+ page,
261
+ controllerId: clientId,
262
+ width,
263
+ height,
264
+ notice: "",
265
+ exited: false,
266
+ };
267
+ this.watch(s, page);
268
+ context.on("page", (newPage) => {
269
+ this.watch(s, newPage);
270
+ s.page = newPage;
271
+ s.exited = false;
272
+ s.notice = "Switched to new tab";
273
+ if (context.pages().length > 8)
274
+ void context
275
+ .pages()[0]
276
+ .close()
277
+ .catch(() => { });
278
+ });
279
+ context.on("close", () => {
280
+ s.exited = true;
281
+ s.controllerId = null;
282
+ });
283
+ this.sessions.set(id, s);
284
+ try {
285
+ await this.navigate(s, url, deadline);
286
+ }
287
+ catch (error) {
288
+ this.sessions.delete(id);
289
+ await context.close();
290
+ throw error;
291
+ }
292
+ return this.descriptor(s);
293
+ }
294
+ const s = this.sessions.get(id);
295
+ if (!s)
296
+ throw Error("Browser session not found");
297
+ if ([
298
+ "browser.attach",
299
+ "browser.takeover",
300
+ "browser.detach",
301
+ "browser.close",
302
+ "browser.frame",
303
+ ].includes(method))
304
+ keys(params, ["id", "clientId"]);
305
+ if (method === "browser.attach") {
306
+ if (!s.controllerId)
307
+ s.controllerId = clientId;
308
+ return {
309
+ session: this.descriptor(s),
310
+ control: s.controllerId === clientId,
311
+ };
312
+ }
313
+ if (method === "browser.takeover") {
314
+ s.controllerId = clientId;
315
+ return { ok: true };
316
+ }
317
+ if (method === "browser.detach") {
318
+ if (s.controllerId === clientId)
319
+ s.controllerId = null;
320
+ return { ok: true };
321
+ }
322
+ if (method === "browser.frame") {
323
+ const base = {
324
+ mime: "image/jpeg",
325
+ width: s.width,
326
+ height: s.height,
327
+ url: s.page.url(),
328
+ title: "",
329
+ control: s.controllerId === clientId,
330
+ exited: s.exited,
331
+ notice: s.notice,
332
+ };
333
+ if (s.exited)
334
+ return { ...base, data: "" };
335
+ const jpeg = await s.page.screenshot({
336
+ type: "jpeg",
337
+ quality: 70,
338
+ timeout: TIMEOUT,
339
+ animations: "disabled",
340
+ });
341
+ return {
342
+ ...base,
343
+ title: (await bounded(s.page.title(), 1000)).slice(0, 512),
344
+ data: jpeg.toString("base64"),
345
+ };
346
+ }
347
+ if (s.controllerId !== clientId)
348
+ throw Error("Browser is read-only; take control first");
349
+ if (method === "browser.close") {
350
+ await s.context.close();
351
+ this.sessions.delete(id);
352
+ return { ok: true };
353
+ }
354
+ if (s.exited)
355
+ throw Error("Browser exited");
356
+ if (method === "browser.navigate") {
357
+ keys(params, ["id", "clientId", "url"]);
358
+ await this.navigate(s, allowedUrl(params), deadline);
359
+ return { ok: true };
360
+ }
361
+ if (method === "browser.input") {
362
+ keys(params, [
363
+ "id",
364
+ "clientId",
365
+ "kind",
366
+ "x",
367
+ "y",
368
+ "deltaX",
369
+ "deltaY",
370
+ "key",
371
+ "text",
372
+ ]);
373
+ const kind = string(params, "kind", 16);
374
+ if (kind === "click")
375
+ await bounded(s.page.mouse.click(integer(params, "x", 0, s.width - 1), integer(params, "y", 0, s.height - 1)));
376
+ else if (kind === "scroll")
377
+ await bounded(s.page.mouse.wheel(integer(params, "deltaX", -4000, 4000, 0), integer(params, "deltaY", -4000, 4000, 0)));
378
+ else if (kind === "key")
379
+ await bounded(s.page.keyboard.press(browserKey(params)));
380
+ else if (kind === "text")
381
+ await bounded(s.page.keyboard.insertText(string(params, "text", 64 * 1024)));
382
+ else
383
+ throw Error("Invalid browser input kind");
384
+ return { ok: true };
385
+ }
386
+ throw Error("Unsupported browser tool");
387
+ }
388
+ async dispose() {
389
+ this.disposed = true;
390
+ await this.queue;
391
+ await Promise.all([...this.sessions.values()].map((s) => s.context.close().catch(() => { })));
392
+ this.sessions.clear();
393
+ }
394
+ }