@avasapp/agent-bridge 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -11
- package/dist/adapters/tanstack-query.cjs +3 -0
- package/dist/adapters/tanstack-query.d.cts +12 -3
- package/dist/{chunk-TA2OLPPT.js → chunk-3QIG7A5W.js} +247 -142
- package/dist/cli.js +1198 -12
- package/dist/client/index.cjs +210 -111
- package/dist/client/index.js +1 -1
- package/dist/runtime/index.cjs +69 -16
- package/dist/runtime/index.d.cts +2 -0
- package/package.json +3 -2
- package/skills/agent-bridge/SKILL.md +32 -15
- package/skills/agent-bridge/references/flows.md +6 -5
- package/skills/agent-bridge/references/large-data.md +6 -6
- package/skills/agent-bridge/references/network.md +7 -7
- package/skills/agent-bridge/references/realtime.md +5 -5
- package/skills/agent-bridge/references/restore.md +4 -4
- package/skills/agent-bridge/references/screen.md +18 -10
- package/skills/agent-bridge/references/state.md +11 -11
- package/skills/agent-bridge/references/webview.md +9 -9
- package/skills/agent-bridge/references/writing-scenarios.md +3 -3
package/dist/client/index.cjs
CHANGED
|
@@ -76,11 +76,11 @@ async function listCdpTargets(metro, signal) {
|
|
|
76
76
|
);
|
|
77
77
|
return await res.json();
|
|
78
78
|
}
|
|
79
|
-
function pickOne(items, filter,
|
|
79
|
+
function pickOne(items, filter, label2) {
|
|
80
80
|
const matching = filter ? items.filter(
|
|
81
|
-
(item) =>
|
|
81
|
+
(item) => label2(item).toLowerCase().includes(filter.toLowerCase())
|
|
82
82
|
) : items;
|
|
83
|
-
const connected = items.map(
|
|
83
|
+
const connected = items.map(label2).join("; ") || "none";
|
|
84
84
|
if (matching.length === 0) {
|
|
85
85
|
throw new Error(
|
|
86
86
|
filter ? `No app matches "${filter}". Connected: ${connected}` : "No app is connected to Metro."
|
|
@@ -93,11 +93,11 @@ function pickOne(items, filter, label) {
|
|
|
93
93
|
}
|
|
94
94
|
if (matching.length > 1) {
|
|
95
95
|
const exact = matching.filter(
|
|
96
|
-
(item) =>
|
|
96
|
+
(item) => label2(item).toLowerCase() === filter?.toLowerCase()
|
|
97
97
|
);
|
|
98
98
|
if (exact.length === 1) return exact[0];
|
|
99
99
|
throw new Error(
|
|
100
|
-
`${matching.length} apps match "${filter}"; narrow --device. Matching: ${matching.map(
|
|
100
|
+
`${matching.length} apps match "${filter}"; narrow --device. Matching: ${matching.map(label2).join("; ")}`
|
|
101
101
|
);
|
|
102
102
|
}
|
|
103
103
|
return matching[0];
|
|
@@ -295,6 +295,206 @@ async function connectExpo(metro, device, discoveryMs = 600, signal, pin) {
|
|
|
295
295
|
};
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
+
// src/client/session/state.ts
|
|
299
|
+
var import_node_crypto = require("crypto");
|
|
300
|
+
var import_node_fs = require("fs");
|
|
301
|
+
var import_node_os = require("os");
|
|
302
|
+
var import_node_path = require("path");
|
|
303
|
+
function projectRoot(from = process.cwd()) {
|
|
304
|
+
for (let dir = (0, import_node_path.resolve)(from); ; dir = (0, import_node_path.dirname)(dir)) {
|
|
305
|
+
if ((0, import_node_fs.existsSync)((0, import_node_path.join)(dir, "package.json"))) return dir;
|
|
306
|
+
if ((0, import_node_path.dirname)(dir) === dir) return (0, import_node_path.resolve)(from);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
var uid = () => process.getuid?.() ?? (0, import_node_os.userInfo)().username;
|
|
310
|
+
function privateDir(dir) {
|
|
311
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
312
|
+
const stat = (0, import_node_fs.lstatSync)(dir);
|
|
313
|
+
if (!stat.isDirectory())
|
|
314
|
+
throw new Error(`${dir} is not a directory; remove it and retry`);
|
|
315
|
+
if (process.getuid && stat.uid !== process.getuid())
|
|
316
|
+
throw new Error(`${dir} belongs to another user; refusing to use it`);
|
|
317
|
+
if (process.platform !== "win32" && (stat.mode & 63) !== 0)
|
|
318
|
+
(0, import_node_fs.chmodSync)(dir, 448);
|
|
319
|
+
return dir;
|
|
320
|
+
}
|
|
321
|
+
function stateDir() {
|
|
322
|
+
return privateDir(
|
|
323
|
+
process.env.AGENT_BRIDGE_STATE_DIR ?? (process.env.XDG_RUNTIME_DIR ? (0, import_node_path.join)(process.env.XDG_RUNTIME_DIR, "agent-bridge") : (0, import_node_path.join)((0, import_node_os.tmpdir)(), `agent-bridge-${uid()}`))
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
var shortHash = (text) => (0, import_node_crypto.createHash)("sha256").update(text).digest("hex").slice(0, 16);
|
|
327
|
+
function socketPath(name, dir) {
|
|
328
|
+
if (process.platform === "win32")
|
|
329
|
+
return `\\\\.\\pipe\\agent-bridge-${shortHash(dir)}-${name}`;
|
|
330
|
+
const path = (0, import_node_path.join)(dir, `${name}.sock`);
|
|
331
|
+
if (Buffer.byteLength(path) <= 100) return path;
|
|
332
|
+
const short = privateDir(`/tmp/agent-bridge-${uid()}`);
|
|
333
|
+
return (0, import_node_path.join)(short, `${shortHash(path)}.sock`);
|
|
334
|
+
}
|
|
335
|
+
var sessionFiles = (name, dir = stateDir()) => ({
|
|
336
|
+
state: (0, import_node_path.join)(dir, `${name}.json`),
|
|
337
|
+
error: (0, import_node_path.join)(dir, `${name}.error`),
|
|
338
|
+
log: (0, import_node_path.join)(dir, `${name}.log`),
|
|
339
|
+
socket: socketPath(name, dir)
|
|
340
|
+
});
|
|
341
|
+
function checkName(name) {
|
|
342
|
+
if (!/^[\w.-]{1,40}$/.test(name))
|
|
343
|
+
throw new Error(
|
|
344
|
+
`Session name "${name}" must be 1-40 letters, digits, ".", "_" or "-"`
|
|
345
|
+
);
|
|
346
|
+
return name;
|
|
347
|
+
}
|
|
348
|
+
function isAlive(pid) {
|
|
349
|
+
try {
|
|
350
|
+
process.kill(pid, 0);
|
|
351
|
+
return true;
|
|
352
|
+
} catch (error) {
|
|
353
|
+
return error.code === "EPERM";
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function removeSessionFiles(name, dir = stateDir()) {
|
|
357
|
+
const files = sessionFiles(name, dir);
|
|
358
|
+
(0, import_node_fs.rmSync)(files.state, { force: true });
|
|
359
|
+
if (process.platform !== "win32") (0, import_node_fs.rmSync)(files.socket, { force: true });
|
|
360
|
+
}
|
|
361
|
+
function readState(file) {
|
|
362
|
+
try {
|
|
363
|
+
return JSON.parse((0, import_node_fs.readFileSync)(file, "utf8"));
|
|
364
|
+
} catch {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function listSessions(dir = stateDir()) {
|
|
369
|
+
const live = [];
|
|
370
|
+
for (const entry of (0, import_node_fs.readdirSync)(dir)) {
|
|
371
|
+
if (!entry.endsWith(".json")) continue;
|
|
372
|
+
const state = readState((0, import_node_path.join)(dir, entry));
|
|
373
|
+
if (!state) continue;
|
|
374
|
+
if (isAlive(state.pid)) live.push(state);
|
|
375
|
+
else removeSessionFiles(state.name, dir);
|
|
376
|
+
}
|
|
377
|
+
return live.sort((a, b) => a.startedAt - b.startedAt);
|
|
378
|
+
}
|
|
379
|
+
function readSession(name, dir = stateDir()) {
|
|
380
|
+
return listSessions(dir).find((s) => s.name === name) ?? null;
|
|
381
|
+
}
|
|
382
|
+
function pickSession(filter) {
|
|
383
|
+
if (filter.name) {
|
|
384
|
+
const named = readSession(checkName(filter.name));
|
|
385
|
+
if (!named) throw new Error(`No session named "${filter.name}" is running`);
|
|
386
|
+
return named;
|
|
387
|
+
}
|
|
388
|
+
const device = filter.device?.toLowerCase();
|
|
389
|
+
const explicit = !!(filter.metro || device || filter.transport && filter.transport !== "auto");
|
|
390
|
+
const project = projectRoot();
|
|
391
|
+
const matching = listSessions().filter(
|
|
392
|
+
(s) => (explicit || s.project === project) && (!filter.metro || s.metro === filter.metro) && (!device || s.device.name.toLowerCase().includes(device) || s.device.deviceId.toLowerCase().includes(device)) && (!filter.transport || filter.transport === "auto" || filter.transport === s.transport)
|
|
393
|
+
);
|
|
394
|
+
if (matching.length > 1)
|
|
395
|
+
throw new Error(
|
|
396
|
+
`Several sessions match (${matching.map((s) => `${s.name}: ${s.device.name} on ${s.metro}`).join("; ")}); pick one by name (--session, or --name for session stop/status), or narrow with --metro/--device`
|
|
397
|
+
);
|
|
398
|
+
return matching[0] ?? null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// src/client/metro-hint.ts
|
|
402
|
+
var range = (from, to) => Array.from({ length: to - from + 1 }, (_, i) => from + i);
|
|
403
|
+
var probe = {
|
|
404
|
+
ports: [...range(8081, 8090), ...range(8100, 8199)],
|
|
405
|
+
eachMs: 300,
|
|
406
|
+
totalMs: 1e3
|
|
407
|
+
};
|
|
408
|
+
var hostOf = (metro) => {
|
|
409
|
+
const colon = metro.lastIndexOf(":");
|
|
410
|
+
return colon > 0 && !metro.endsWith("]") ? metro.slice(0, colon) : metro;
|
|
411
|
+
};
|
|
412
|
+
async function answers(metro, signal) {
|
|
413
|
+
try {
|
|
414
|
+
const res = await fetch(`http://${metro}/status`, { signal });
|
|
415
|
+
return (await res.text()).includes("packager-status:running");
|
|
416
|
+
} catch {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
async function appsOn(metro, signal) {
|
|
421
|
+
try {
|
|
422
|
+
const res = await fetch(`http://${metro}/json/list`, { signal });
|
|
423
|
+
const targets = await res.json();
|
|
424
|
+
const names = targets.map(
|
|
425
|
+
(t) => `${t.title ?? "?"}${t.deviceName ? ` [${t.deviceName}]` : ""}`
|
|
426
|
+
);
|
|
427
|
+
return [...new Set(names)];
|
|
428
|
+
} catch {
|
|
429
|
+
return [];
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
async function findMetros(metro) {
|
|
433
|
+
const host = hostOf(metro);
|
|
434
|
+
const candidates = new Set(probe.ports.map((port) => `${host}:${port}`));
|
|
435
|
+
try {
|
|
436
|
+
for (const s of listSessions()) candidates.add(s.metro);
|
|
437
|
+
} catch {
|
|
438
|
+
}
|
|
439
|
+
candidates.delete(metro);
|
|
440
|
+
const deadline = AbortSignal.timeout(probe.totalMs);
|
|
441
|
+
const within2 = () => AbortSignal.any([deadline, AbortSignal.timeout(probe.eachMs)]);
|
|
442
|
+
const running = (await Promise.all(
|
|
443
|
+
[...candidates].map(async (m) => await answers(m, within2()) ? m : null)
|
|
444
|
+
)).filter((m) => m !== null);
|
|
445
|
+
return Promise.all(
|
|
446
|
+
running.map(async (m) => ({ metro: m, apps: await appsOn(m, within2()) }))
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
var label = (found) => `${found.metro} (${found.apps.length ? `${found.apps.length === 1 ? "app" : "apps"}: ${found.apps.join(", ")}` : "no app connected"})`;
|
|
450
|
+
var howToSet = (metro) => `Pass --metro ${metro} or set AGENT_BRIDGE_METRO=${metro}.`;
|
|
451
|
+
function elsewhere(found) {
|
|
452
|
+
const [only] = found;
|
|
453
|
+
if (found.length === 1 && only)
|
|
454
|
+
return `Metro is running on ${label(only)}. ${howToSet(only.metro)}`;
|
|
455
|
+
return `Metro is running on ${found.map(label).join("; ")}. ${howToSet("<host:port>")}`;
|
|
456
|
+
}
|
|
457
|
+
var errorText = (error) => {
|
|
458
|
+
if (!(error instanceof Error)) return String(error);
|
|
459
|
+
const code = error.code ?? "";
|
|
460
|
+
return `${error.message} ${code} ${error.cause ? errorText(error.cause) : ""}`;
|
|
461
|
+
};
|
|
462
|
+
var isRefused = (error) => /ECONNREFUSED|ConnectionRefused/.test(errorText(error));
|
|
463
|
+
var NO_APP = /No app is connected to Metro|No app matches/;
|
|
464
|
+
async function refusedMessage(metro) {
|
|
465
|
+
const found = await findMetros(metro);
|
|
466
|
+
if (found.length)
|
|
467
|
+
return `Nothing is listening on ${metro}. ${elsewhere(found)}`;
|
|
468
|
+
const host = hostOf(metro);
|
|
469
|
+
return `Nothing is listening on ${metro}, and no Metro answered on ${host} ports 8081-8090 or 8100-8199. Start Metro, or if it runs elsewhere, pass --metro <host:port> or set AGENT_BRIDGE_METRO=<host:port>.`;
|
|
470
|
+
}
|
|
471
|
+
async function noAppHint(metro, device) {
|
|
472
|
+
const causes = `Likely: the app isn't open or hasn't reloaded since the bridge was mounted, it uses another Metro${device ? `, or --device "${device}" doesn't match` : ""}.`;
|
|
473
|
+
const withApps = (await findMetros(metro)).filter((f) => f.apps.length);
|
|
474
|
+
return withApps.length ? `${causes} ${elsewhere(withApps)}` : causes;
|
|
475
|
+
}
|
|
476
|
+
async function refused(metro) {
|
|
477
|
+
try {
|
|
478
|
+
await fetch(`http://${metro}/status`, {
|
|
479
|
+
signal: AbortSignal.timeout(probe.eachMs)
|
|
480
|
+
});
|
|
481
|
+
return false;
|
|
482
|
+
} catch (error) {
|
|
483
|
+
return isRefused(error);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
async function explainConnectError(error, metro, device) {
|
|
487
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
488
|
+
if (NO_APP.test(message))
|
|
489
|
+
return new Error(`${message}
|
|
490
|
+
${await noAppHint(metro, device)}`, {
|
|
491
|
+
cause: error
|
|
492
|
+
});
|
|
493
|
+
if (await refused(metro))
|
|
494
|
+
return new Error(await refusedMessage(metro), { cause: error });
|
|
495
|
+
return error;
|
|
496
|
+
}
|
|
497
|
+
|
|
298
498
|
// src/client/cdp.ts
|
|
299
499
|
var import_ws2 = __toESM(require("ws"), 1);
|
|
300
500
|
var CannotEvaluate = class extends Error {
|
|
@@ -529,7 +729,7 @@ async function runFlow(bridge, flow, options = {}) {
|
|
|
529
729
|
}
|
|
530
730
|
};
|
|
531
731
|
const call = async (tool, ...args) => (await timed(tool, ...args)).value;
|
|
532
|
-
const step = async (
|
|
732
|
+
const step = async (label2, tool, ...given) => {
|
|
533
733
|
const args = given.length === 1 && Array.isArray(given[0]) ? given[0] : given;
|
|
534
734
|
const { value, ms, logs, notice } = await bridge.timed(tool, ...args).catch((error) => {
|
|
535
735
|
report(failedLogs(error), failedNotice(error));
|
|
@@ -537,7 +737,7 @@ async function runFlow(bridge, flow, options = {}) {
|
|
|
537
737
|
});
|
|
538
738
|
total += ms;
|
|
539
739
|
print(
|
|
540
|
-
`${String(++n).padStart(2, "0")} ${
|
|
740
|
+
`${String(++n).padStart(2, "0")} ${label2.padEnd(30)} ${ms.toFixed(1).padStart(7)} ms`
|
|
541
741
|
);
|
|
542
742
|
report(logs, notice);
|
|
543
743
|
return value;
|
|
@@ -589,109 +789,6 @@ var import_node_readline = require("readline");
|
|
|
589
789
|
var encodeLine = (message) => `${JSON.stringify(message).replace(/[\u2028\u2029]/g, (c) => `\\u${c.charCodeAt(0).toString(16)}`)}
|
|
590
790
|
`;
|
|
591
791
|
|
|
592
|
-
// src/client/session/state.ts
|
|
593
|
-
var import_node_crypto = require("crypto");
|
|
594
|
-
var import_node_fs = require("fs");
|
|
595
|
-
var import_node_os = require("os");
|
|
596
|
-
var import_node_path = require("path");
|
|
597
|
-
function projectRoot(from = process.cwd()) {
|
|
598
|
-
for (let dir = (0, import_node_path.resolve)(from); ; dir = (0, import_node_path.dirname)(dir)) {
|
|
599
|
-
if ((0, import_node_fs.existsSync)((0, import_node_path.join)(dir, "package.json"))) return dir;
|
|
600
|
-
if ((0, import_node_path.dirname)(dir) === dir) return (0, import_node_path.resolve)(from);
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
var uid = () => process.getuid?.() ?? (0, import_node_os.userInfo)().username;
|
|
604
|
-
function privateDir(dir) {
|
|
605
|
-
(0, import_node_fs.mkdirSync)(dir, { recursive: true, mode: 448 });
|
|
606
|
-
const stat = (0, import_node_fs.lstatSync)(dir);
|
|
607
|
-
if (!stat.isDirectory())
|
|
608
|
-
throw new Error(`${dir} is not a directory; remove it and retry`);
|
|
609
|
-
if (process.getuid && stat.uid !== process.getuid())
|
|
610
|
-
throw new Error(`${dir} belongs to another user; refusing to use it`);
|
|
611
|
-
if (process.platform !== "win32" && (stat.mode & 63) !== 0)
|
|
612
|
-
(0, import_node_fs.chmodSync)(dir, 448);
|
|
613
|
-
return dir;
|
|
614
|
-
}
|
|
615
|
-
function stateDir() {
|
|
616
|
-
return privateDir(
|
|
617
|
-
process.env.AGENT_BRIDGE_STATE_DIR ?? (process.env.XDG_RUNTIME_DIR ? (0, import_node_path.join)(process.env.XDG_RUNTIME_DIR, "agent-bridge") : (0, import_node_path.join)((0, import_node_os.tmpdir)(), `agent-bridge-${uid()}`))
|
|
618
|
-
);
|
|
619
|
-
}
|
|
620
|
-
var shortHash = (text) => (0, import_node_crypto.createHash)("sha256").update(text).digest("hex").slice(0, 16);
|
|
621
|
-
function socketPath(name, dir) {
|
|
622
|
-
if (process.platform === "win32")
|
|
623
|
-
return `\\\\.\\pipe\\agent-bridge-${shortHash(dir)}-${name}`;
|
|
624
|
-
const path = (0, import_node_path.join)(dir, `${name}.sock`);
|
|
625
|
-
if (Buffer.byteLength(path) <= 100) return path;
|
|
626
|
-
const short = privateDir(`/tmp/agent-bridge-${uid()}`);
|
|
627
|
-
return (0, import_node_path.join)(short, `${shortHash(path)}.sock`);
|
|
628
|
-
}
|
|
629
|
-
var sessionFiles = (name, dir = stateDir()) => ({
|
|
630
|
-
state: (0, import_node_path.join)(dir, `${name}.json`),
|
|
631
|
-
error: (0, import_node_path.join)(dir, `${name}.error`),
|
|
632
|
-
log: (0, import_node_path.join)(dir, `${name}.log`),
|
|
633
|
-
socket: socketPath(name, dir)
|
|
634
|
-
});
|
|
635
|
-
function checkName(name) {
|
|
636
|
-
if (!/^[\w.-]{1,40}$/.test(name))
|
|
637
|
-
throw new Error(
|
|
638
|
-
`Session name "${name}" must be 1-40 letters, digits, ".", "_" or "-"`
|
|
639
|
-
);
|
|
640
|
-
return name;
|
|
641
|
-
}
|
|
642
|
-
function isAlive(pid) {
|
|
643
|
-
try {
|
|
644
|
-
process.kill(pid, 0);
|
|
645
|
-
return true;
|
|
646
|
-
} catch (error) {
|
|
647
|
-
return error.code === "EPERM";
|
|
648
|
-
}
|
|
649
|
-
}
|
|
650
|
-
function removeSessionFiles(name, dir = stateDir()) {
|
|
651
|
-
const files = sessionFiles(name, dir);
|
|
652
|
-
(0, import_node_fs.rmSync)(files.state, { force: true });
|
|
653
|
-
if (process.platform !== "win32") (0, import_node_fs.rmSync)(files.socket, { force: true });
|
|
654
|
-
}
|
|
655
|
-
function readState(file) {
|
|
656
|
-
try {
|
|
657
|
-
return JSON.parse((0, import_node_fs.readFileSync)(file, "utf8"));
|
|
658
|
-
} catch {
|
|
659
|
-
return null;
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
function listSessions(dir = stateDir()) {
|
|
663
|
-
const live = [];
|
|
664
|
-
for (const entry of (0, import_node_fs.readdirSync)(dir)) {
|
|
665
|
-
if (!entry.endsWith(".json")) continue;
|
|
666
|
-
const state = readState((0, import_node_path.join)(dir, entry));
|
|
667
|
-
if (!state) continue;
|
|
668
|
-
if (isAlive(state.pid)) live.push(state);
|
|
669
|
-
else removeSessionFiles(state.name, dir);
|
|
670
|
-
}
|
|
671
|
-
return live.sort((a, b) => a.startedAt - b.startedAt);
|
|
672
|
-
}
|
|
673
|
-
function readSession(name, dir = stateDir()) {
|
|
674
|
-
return listSessions(dir).find((s) => s.name === name) ?? null;
|
|
675
|
-
}
|
|
676
|
-
function pickSession(filter) {
|
|
677
|
-
if (filter.name) {
|
|
678
|
-
const named = readSession(checkName(filter.name));
|
|
679
|
-
if (!named) throw new Error(`No session named "${filter.name}" is running`);
|
|
680
|
-
return named;
|
|
681
|
-
}
|
|
682
|
-
const device = filter.device?.toLowerCase();
|
|
683
|
-
const explicit = !!(filter.metro || device || filter.transport && filter.transport !== "auto");
|
|
684
|
-
const project = projectRoot();
|
|
685
|
-
const matching = listSessions().filter(
|
|
686
|
-
(s) => (explicit || s.project === project) && (!filter.metro || s.metro === filter.metro) && (!device || s.device.name.toLowerCase().includes(device) || s.device.deviceId.toLowerCase().includes(device)) && (!filter.transport || filter.transport === "auto" || filter.transport === s.transport)
|
|
687
|
-
);
|
|
688
|
-
if (matching.length > 1)
|
|
689
|
-
throw new Error(
|
|
690
|
-
`Several sessions match (${matching.map((s) => `${s.name}: ${s.device.name} on ${s.metro}`).join("; ")}); pick one by name (--session, or --name for session stop/status), or narrow with --metro/--device`
|
|
691
|
-
);
|
|
692
|
-
return matching[0] ?? null;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
792
|
// src/client/session/client.ts
|
|
696
793
|
function openSessionLink(state) {
|
|
697
794
|
return new Promise((resolve2, reject) => {
|
|
@@ -813,7 +910,9 @@ Warning: ${notice}` : ""}`);
|
|
|
813
910
|
};
|
|
814
911
|
async function connect(options = {}) {
|
|
815
912
|
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
816
|
-
const conn = await openConnection(options)
|
|
913
|
+
const conn = await openConnection(options).catch(async (error) => {
|
|
914
|
+
throw await explainConnectError(error, metroHost(options.metro), options.device);
|
|
915
|
+
});
|
|
817
916
|
const timed = async (tool, ...args) => {
|
|
818
917
|
const t0 = performance.now();
|
|
819
918
|
const result = await conn.call(tool, args, timeoutMs);
|
package/dist/client/index.js
CHANGED
package/dist/runtime/index.cjs
CHANGED
|
@@ -515,7 +515,12 @@ var SWITCH_HOSTS = /* @__PURE__ */ new Set(["RCTSwitch", "AndroidSwitch"]);
|
|
|
515
515
|
var isFn = (v) => typeof v === "function";
|
|
516
516
|
var isInputProps = (p) => isFn(p.onChangeText) || isFn(p.onChange) && typeof p.value !== "boolean" && ("value" in p || "defaultValue" in p || "placeholder" in p);
|
|
517
517
|
var labelOf = (p) => p.accessibilityLabel ?? p["aria-label"];
|
|
518
|
-
|
|
518
|
+
function actionNamesOf(p) {
|
|
519
|
+
if (!isFn(p.onAccessibilityAction) || !Array.isArray(p.accessibilityActions)) return null;
|
|
520
|
+
return p.accessibilityActions.map((a) => a?.name).filter((name) => typeof name === "string");
|
|
521
|
+
}
|
|
522
|
+
var activates = (rec) => !!rec.actions && !!rec.element.actions?.includes("activate");
|
|
523
|
+
var interesting = (p) => isFn(p.onPress) || actionNamesOf(p) !== null || isFn(p.onValueChange) || isInputProps(p) || typeof p.testID === "string" || typeof labelOf(p) === "string" || webViewNameOf(p) !== void 0;
|
|
519
524
|
var ICON_GLYPHS = /[\uE000-\uF8FF]/g;
|
|
520
525
|
function firstHost(fiber) {
|
|
521
526
|
if (fiber.tag === HOST_COMPONENT) return fiber;
|
|
@@ -532,6 +537,11 @@ function absorb(rec, fiber, p) {
|
|
|
532
537
|
const e = rec.element;
|
|
533
538
|
if (!rec.input && isInputProps(p)) rec.input = fiber;
|
|
534
539
|
if (!rec.press && isFn(p.onPress)) rec.press = fiber;
|
|
540
|
+
const actions = actionNamesOf(p);
|
|
541
|
+
if (!rec.actions && actions) {
|
|
542
|
+
rec.actions = fiber;
|
|
543
|
+
if (actions.length) e.actions = actions;
|
|
544
|
+
}
|
|
535
545
|
const fill = (key, v) => {
|
|
536
546
|
if (e[key] === void 0 && typeof v === "string") e[key] = v;
|
|
537
547
|
};
|
|
@@ -617,8 +627,8 @@ var round = (r) => r && {
|
|
|
617
627
|
var handles = (control, p, name) => !!control && isFn(p[name]) && propsOf(control)?.[name] === p[name];
|
|
618
628
|
function sameControl(rec, p) {
|
|
619
629
|
for (let r = rec; r; r = r.parent) {
|
|
620
|
-
if (!r.press && !r.input) continue;
|
|
621
|
-
const same = handles(r.press, p, "onPress") || handles(r.input, p, "onChangeText") || handles(r.input, p, "onChange");
|
|
630
|
+
if (!r.press && !r.input && !r.actions) continue;
|
|
631
|
+
const same = handles(r.press, p, "onPress") || handles(r.actions, p, "onAccessibilityAction") || handles(r.input, p, "onChangeText") || handles(r.input, p, "onChange");
|
|
622
632
|
return same ? r : null;
|
|
623
633
|
}
|
|
624
634
|
return null;
|
|
@@ -648,6 +658,7 @@ function collectScreen(roots, window) {
|
|
|
648
658
|
host,
|
|
649
659
|
press: null,
|
|
650
660
|
input: null,
|
|
661
|
+
actions: null,
|
|
651
662
|
hidden,
|
|
652
663
|
scroller,
|
|
653
664
|
modal,
|
|
@@ -661,7 +672,7 @@ function collectScreen(roots, window) {
|
|
|
661
672
|
return rec;
|
|
662
673
|
};
|
|
663
674
|
const buttonOf = (rec) => {
|
|
664
|
-
for (let r = rec; r; r = r.parent) if (r.press) return r;
|
|
675
|
+
for (let r = rec; r; r = r.parent) if (r.press || activates(r)) return r;
|
|
665
676
|
return null;
|
|
666
677
|
};
|
|
667
678
|
const textHost = (host) => {
|
|
@@ -752,7 +763,7 @@ function collectScreen(roots, window) {
|
|
|
752
763
|
rec.hidden || (rec.hidden = covers2(rec));
|
|
753
764
|
const text = rec.parts.map((part) => part.text.replace(ICON_GLYPHS, "").replace(/\s+/g, " ").trim()).filter(Boolean).join(" ");
|
|
754
765
|
if (text) e.text = text;
|
|
755
|
-
e.kind = e.webview !== void 0 ? "webview" : rec.input ? "input" : rec.press ? "button" : rec.parts.length ? "text" : "view";
|
|
766
|
+
e.kind = e.webview !== void 0 ? "webview" : rec.input ? "input" : rec.press || activates(rec) ? "button" : rec.parts.length ? "text" : "view";
|
|
756
767
|
if (e.kind === "input" && e.editable === void 0) e.editable = true;
|
|
757
768
|
const rect = measureHost(rec.host);
|
|
758
769
|
e.rect = round(rect);
|
|
@@ -777,10 +788,21 @@ function collectScreen(roots, window) {
|
|
|
777
788
|
}
|
|
778
789
|
var focused = (found) => found.filter((f) => !f.hidden);
|
|
779
790
|
function pressFiberOf(found) {
|
|
780
|
-
if (found.press) return found.press;
|
|
791
|
+
if (found.press) return { fiber: found.press, via: "onPress" };
|
|
792
|
+
if (activates(found)) return { fiber: found.actions, via: "accessibilityAction" };
|
|
793
|
+
for (let f = found.fiber.return; f; f = f.return) {
|
|
794
|
+
const p = propsOf(f);
|
|
795
|
+
if (!p) continue;
|
|
796
|
+
if (isFn(p.onPress)) return { fiber: f, via: "onPress" };
|
|
797
|
+
if (actionNamesOf(p)?.includes("activate")) return { fiber: f, via: "accessibilityAction" };
|
|
798
|
+
}
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
function actionFiberOf(found, name) {
|
|
802
|
+
if (found.actions && found.element.actions?.includes(name)) return found.actions;
|
|
781
803
|
for (let f = found.fiber.return; f; f = f.return) {
|
|
782
804
|
const p = propsOf(f);
|
|
783
|
-
if (p &&
|
|
805
|
+
if (p && actionNamesOf(p)?.includes(name)) return f;
|
|
784
806
|
}
|
|
785
807
|
return null;
|
|
786
808
|
}
|
|
@@ -824,7 +846,7 @@ function tiers(target) {
|
|
|
824
846
|
(e) => fields(e) && !!e.text?.includes(text)
|
|
825
847
|
];
|
|
826
848
|
}
|
|
827
|
-
var labelOnly = ({ element: e, press, input }) => e.kind === "view" && !e.testID && !e.text && !e.placeholder && !press && !input;
|
|
849
|
+
var labelOnly = ({ element: e, press, input, actions }) => e.kind === "view" && !e.testID && !e.text && !e.placeholder && !press && !input && !actions;
|
|
828
850
|
function matchTarget(found, target) {
|
|
829
851
|
checkTarget(target);
|
|
830
852
|
for (const pool of [found.filter((f) => !labelOnly(f)), found.filter(labelOnly)]) {
|
|
@@ -942,13 +964,36 @@ function fillInput(found, text, options = {}) {
|
|
|
942
964
|
return value;
|
|
943
965
|
}
|
|
944
966
|
var center = (rect) => rect ? { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 } : { x: 0, y: 0 };
|
|
967
|
+
var disabledProps = (fiber) => {
|
|
968
|
+
const p = propsOf(fiber);
|
|
969
|
+
const state = p?.accessibilityState;
|
|
970
|
+
return p?.disabled === true || state?.disabled === true || p?.["aria-disabled"] === true;
|
|
971
|
+
};
|
|
972
|
+
function pressTargetOf(found, action) {
|
|
973
|
+
if (action === void 0) return pressFiberOf(found);
|
|
974
|
+
const fiber = actionFiberOf(found, action);
|
|
975
|
+
return fiber && { fiber, via: "accessibilityAction" };
|
|
976
|
+
}
|
|
945
977
|
function pressElement(found, options = {}) {
|
|
946
|
-
const
|
|
947
|
-
|
|
948
|
-
if (!
|
|
978
|
+
const { action } = options;
|
|
979
|
+
const target = pressTargetOf(found, action);
|
|
980
|
+
if (!target) {
|
|
981
|
+
const names = found.element.actions ?? [];
|
|
982
|
+
const has = names.length ? `; it has ${names.map((a) => JSON.stringify(a)).join(", ")}` : "";
|
|
983
|
+
throw new Error(
|
|
984
|
+
action === void 0 ? `${describe(found.element)} has no onPress or "activate" accessibility action` : `${describe(found.element)} has no accessibility action ${JSON.stringify(action)}${has}`
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
const { fiber, via } = target;
|
|
988
|
+
if (!options.force && (found.element.disabled || disabledProps(fiber)))
|
|
949
989
|
throw new Error(
|
|
950
990
|
`${describe(found.element)} is disabled; pass {"force":true} as the last argument to press anyway`
|
|
951
991
|
);
|
|
992
|
+
if (via === "accessibilityAction") {
|
|
993
|
+
const actionName = action ?? "activate";
|
|
994
|
+
handler(fiber, "onAccessibilityAction")?.(event({ actionName }));
|
|
995
|
+
return `accessibilityAction:${actionName}`;
|
|
996
|
+
}
|
|
952
997
|
const { x, y } = center(found.element.rect);
|
|
953
998
|
const e = () => event({
|
|
954
999
|
locationX: found.element.rect ? found.element.rect.width / 2 : 0,
|
|
@@ -960,6 +1005,7 @@ function pressElement(found, options = {}) {
|
|
|
960
1005
|
handler(fiber, "onPressIn")?.(e());
|
|
961
1006
|
handler(fiber, "onPress")?.(e());
|
|
962
1007
|
handler(fiber, "onPressOut")?.(e());
|
|
1008
|
+
return "onPress";
|
|
963
1009
|
}
|
|
964
1010
|
|
|
965
1011
|
// src/runtime/screen/find-text.ts
|
|
@@ -1424,10 +1470,17 @@ function createScreen(env) {
|
|
|
1424
1470
|
return { filled, element: reread(found).element };
|
|
1425
1471
|
},
|
|
1426
1472
|
async press(target, options = {}) {
|
|
1427
|
-
const
|
|
1428
|
-
|
|
1473
|
+
const { action } = options;
|
|
1474
|
+
if (action !== void 0 && typeof action !== "string")
|
|
1475
|
+
throw new Error(`"action" must be an accessibility action name; got ${JSON.stringify(action)}`);
|
|
1476
|
+
const found = await resolve(
|
|
1477
|
+
target,
|
|
1478
|
+
(f) => action === void 0 ? !!f.press || f.element.kind === "button" : !!f.element.actions?.includes(action),
|
|
1479
|
+
options
|
|
1480
|
+
);
|
|
1481
|
+
const via = pressElement(found, options);
|
|
1429
1482
|
await settle();
|
|
1430
|
-
return found.element;
|
|
1483
|
+
return via === "onPress" ? found.element : { ...found.element, via };
|
|
1431
1484
|
},
|
|
1432
1485
|
/**
|
|
1433
1486
|
* Scrolls the nearest scrollable of a target (or `within`, or the main one
|
|
@@ -1525,7 +1578,7 @@ function screenTools() {
|
|
|
1525
1578
|
run: (text, options) => screen.findText(text, options)
|
|
1526
1579
|
},
|
|
1527
1580
|
"screen.snapshot": {
|
|
1528
|
-
description:
|
|
1581
|
+
description: 'Buttons, inputs, text, and testID or labelled views on screen, with rects and checked/selected/expanded state (a Switch\'s value is checked). `actions` lists the accessibility actions an element handles; one with "activate" and no onPress (a swipe-to-confirm) is a button screen.press can press. Only the focused screen: unfocused tabs and stack screens, content under an open modal, display:none and accessibility-hidden subtrees are left out, and `hidden` counts the elements skipped. {all:true} lists everything, marking off-screen (onScreen:false) and hidden (hidden:true) ones. A WebView registered with the react-native-webview adapter shows as one `webview` element with its name: use webview.* inside it.',
|
|
1529
1582
|
run: (options) => screen.snapshot(options)
|
|
1530
1583
|
},
|
|
1531
1584
|
"screen.fill": {
|
|
@@ -1533,7 +1586,7 @@ function screenTools() {
|
|
|
1533
1586
|
run: (target, text, options) => screen.fill(target, text, options)
|
|
1534
1587
|
},
|
|
1535
1588
|
"screen.press": {
|
|
1536
|
-
description: `Press a button, then wait for the render. ${TARGET_HELP} Disabled elements are refused; {force:true} presses anyway, to see what the app does on tap. {scroll:true} scrolls an off-screen target into view first.`,
|
|
1589
|
+
description: `Press a button, then wait for the render. ${TARGET_HELP} Disabled elements are refused; {force:true} presses anyway, to see what the app does on tap. {scroll:true} scrolls an off-screen target into view first. With no onPress it sends the "activate" accessibility action, as a screen reader's double tap does (swipe-to-confirm buttons, sliders), and the reply says via:"accessibilityAction:activate". {action:"increment"} (or any name in the element's snapshot \`actions\`) sends that action instead.`,
|
|
1537
1590
|
run: (target, options) => screen.press(target, options)
|
|
1538
1591
|
},
|
|
1539
1592
|
"screen.scroll": {
|
package/dist/runtime/index.d.cts
CHANGED
|
@@ -82,6 +82,8 @@ type ScreenElement = {
|
|
|
82
82
|
expanded?: boolean;
|
|
83
83
|
/** A WebView registered with the react-native-webview adapter: drive it with `webview.*`. */
|
|
84
84
|
webview?: string;
|
|
85
|
+
/** `accessibilityActions` it handles with `onAccessibilityAction`, e.g. `activate` on a swipe button. */
|
|
86
|
+
actions?: string[];
|
|
85
87
|
rect: Rect | null;
|
|
86
88
|
/** Only in `screen.snapshot({ all: true })`. */
|
|
87
89
|
onScreen?: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avasapp/agent-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Let coding agents drive a running React Native app directly: seed state, flip flags, navigate and check the screen in about a millisecond per call, with no taps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -103,8 +103,9 @@
|
|
|
103
103
|
"build": "bun run generate && rm -rf dist && tsup",
|
|
104
104
|
"test": "bun run generate && bun test src/",
|
|
105
105
|
"typecheck": "bun run generate && tsc -p tsconfig.json --noEmit",
|
|
106
|
-
"check": "bun run typecheck && bun run test && bun run build && bun run check:gates",
|
|
106
|
+
"check": "bun run typecheck && bun run test && bun run build && bun run check:gates && bun run check:consumer-types",
|
|
107
107
|
"check:gates": "node scripts/check-gates.mjs",
|
|
108
|
+
"check:consumer-types": "node scripts/check-consumer-types.mjs",
|
|
108
109
|
"changeset": "changeset",
|
|
109
110
|
"release": "bun run check && changeset publish",
|
|
110
111
|
"prepublishOnly": "bun run check"
|