@avasapp/agent-bridge 0.4.1 → 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-AJD3XTNT.js → chunk-3QIG7A5W.js} +292 -161
- package/dist/cli.js +1216 -22
- package/dist/client/index.cjs +255 -130
- package/dist/client/index.js +1 -1
- package/dist/runtime/index.cjs +102 -32
- package/dist/runtime/index.d.cts +2 -0
- package/package.json +3 -2
- package/skills/agent-bridge/SKILL.md +67 -252
- package/skills/agent-bridge/references/flows.md +66 -0
- package/skills/agent-bridge/references/large-data.md +38 -0
- package/skills/agent-bridge/references/network.md +34 -0
- package/skills/agent-bridge/references/realtime.md +20 -0
- package/skills/agent-bridge/references/restore.md +52 -0
- package/skills/agent-bridge/references/screen.md +38 -0
- package/skills/agent-bridge/references/state.md +48 -0
- package/skills/agent-bridge/references/webview.md +42 -0
- package/skills/agent-bridge/references/writing-scenarios.md +97 -0
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
|
|
@@ -1266,9 +1312,19 @@ var POLL_MS = 50;
|
|
|
1266
1312
|
function nearMissNote(found, target) {
|
|
1267
1313
|
const text = typeof target === "string" ? target : target.text;
|
|
1268
1314
|
const near = text ? nearMisses(found, text) : [];
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1315
|
+
if (!near.length) return "";
|
|
1316
|
+
const offscreen = matchTarget(
|
|
1317
|
+
found.filter((f) => !f.onScreen && !f.hidden),
|
|
1318
|
+
target
|
|
1319
|
+
).length;
|
|
1320
|
+
const hint = offscreen ? ". {scroll:true} scrolls an off-screen match into view" : "";
|
|
1321
|
+
return `. Near misses: ${near.join("; ")}${hint}`;
|
|
1322
|
+
}
|
|
1323
|
+
function waitForTarget(collect, target, options = {}, reveal) {
|
|
1324
|
+
if (options.gone && options.scroll)
|
|
1325
|
+
return Promise.reject(
|
|
1326
|
+
new Error("{gone:true} and {scroll:true} can't be combined: gone waits for the target to leave the screen")
|
|
1327
|
+
);
|
|
1272
1328
|
const timeoutMs = options.timeoutMs ?? 5e3;
|
|
1273
1329
|
const t0 = performance.now();
|
|
1274
1330
|
const ms = () => Math.round((performance.now() - t0) * 100) / 100;
|
|
@@ -1285,12 +1341,17 @@ function waitForTarget(collect, target, options = {}) {
|
|
|
1285
1341
|
if (finished) return;
|
|
1286
1342
|
try {
|
|
1287
1343
|
const found = collect();
|
|
1288
|
-
const
|
|
1289
|
-
|
|
1344
|
+
const reachable = found.filter((f) => !f.hidden);
|
|
1345
|
+
let matches = matchTarget(
|
|
1346
|
+
reachable.filter((f) => f.onScreen),
|
|
1290
1347
|
target
|
|
1291
1348
|
);
|
|
1349
|
+
if (!matches.length && options.scroll && reveal) matches = matchTarget(reachable, target);
|
|
1292
1350
|
const hit = matches[indexOf(target) ?? 0];
|
|
1293
|
-
if (
|
|
1351
|
+
if (hit && !hit.onScreen && reveal) {
|
|
1352
|
+
finish();
|
|
1353
|
+
reveal(hit).then((f) => resolve({ ms: ms(), element: f.element }), reject);
|
|
1354
|
+
} else if (options.gone ? !hit : hit) {
|
|
1294
1355
|
finish();
|
|
1295
1356
|
resolve(hit ? { ms: ms(), element: hit.element } : { ms: ms() });
|
|
1296
1357
|
} else if (performance.now() - t0 >= timeoutMs) {
|
|
@@ -1366,18 +1427,20 @@ function createScreen(env) {
|
|
|
1366
1427
|
if (!metrics && current.scroller) metrics = metricsOf(current.scroller);
|
|
1367
1428
|
return { found: current, metrics };
|
|
1368
1429
|
};
|
|
1430
|
+
const reveal = async (found) => {
|
|
1431
|
+
const shown = (await bringIntoView(found)).found;
|
|
1432
|
+
if (!shown.onScreen)
|
|
1433
|
+
throw new Error(
|
|
1434
|
+
`${describe(shown.element)} is not on screen and no scrollable around it can bring it into view`
|
|
1435
|
+
);
|
|
1436
|
+
return shown;
|
|
1437
|
+
};
|
|
1369
1438
|
const resolve = async (target, prefer, options) => {
|
|
1370
|
-
|
|
1439
|
+
const found = resolveTarget(collect(), target, prefer, {
|
|
1371
1440
|
...options,
|
|
1372
1441
|
offscreen: options.scroll
|
|
1373
1442
|
});
|
|
1374
|
-
|
|
1375
|
-
found = (await bringIntoView(found)).found;
|
|
1376
|
-
if (!found.onScreen)
|
|
1377
|
-
throw new Error(
|
|
1378
|
-
`${describe(found.element)} is not on screen and no scrollable around it can bring it into view`
|
|
1379
|
-
);
|
|
1380
|
-
return found;
|
|
1443
|
+
return found.onScreen ? found : reveal(found);
|
|
1381
1444
|
};
|
|
1382
1445
|
return {
|
|
1383
1446
|
snapshot(options = {}) {
|
|
@@ -1407,10 +1470,17 @@ function createScreen(env) {
|
|
|
1407
1470
|
return { filled, element: reread(found).element };
|
|
1408
1471
|
},
|
|
1409
1472
|
async press(target, options = {}) {
|
|
1410
|
-
const
|
|
1411
|
-
|
|
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);
|
|
1412
1482
|
await settle();
|
|
1413
|
-
return found.element;
|
|
1483
|
+
return via === "onPress" ? found.element : { ...found.element, via };
|
|
1414
1484
|
},
|
|
1415
1485
|
/**
|
|
1416
1486
|
* Scrolls the nearest scrollable of a target (or `within`, or the main one
|
|
@@ -1490,7 +1560,7 @@ function createScreen(env) {
|
|
|
1490
1560
|
await settle();
|
|
1491
1561
|
return { refreshed: true };
|
|
1492
1562
|
},
|
|
1493
|
-
waitFor: (target, options) => waitForTarget(collect, target, options)
|
|
1563
|
+
waitFor: (target, options) => waitForTarget(collect, target, options, reveal)
|
|
1494
1564
|
};
|
|
1495
1565
|
}
|
|
1496
1566
|
|
|
@@ -1508,7 +1578,7 @@ function screenTools() {
|
|
|
1508
1578
|
run: (text, options) => screen.findText(text, options)
|
|
1509
1579
|
},
|
|
1510
1580
|
"screen.snapshot": {
|
|
1511
|
-
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.',
|
|
1512
1582
|
run: (options) => screen.snapshot(options)
|
|
1513
1583
|
},
|
|
1514
1584
|
"screen.fill": {
|
|
@@ -1516,7 +1586,7 @@ function screenTools() {
|
|
|
1516
1586
|
run: (target, text, options) => screen.fill(target, text, options)
|
|
1517
1587
|
},
|
|
1518
1588
|
"screen.press": {
|
|
1519
|
-
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.`,
|
|
1520
1590
|
run: (target, options) => screen.press(target, options)
|
|
1521
1591
|
},
|
|
1522
1592
|
"screen.scroll": {
|
|
@@ -1528,7 +1598,7 @@ function screenTools() {
|
|
|
1528
1598
|
run: (target, options) => screen.refresh(target, options)
|
|
1529
1599
|
},
|
|
1530
1600
|
"screen.waitFor": {
|
|
1531
|
-
description: "Wait until a target is on screen, or gone with {gone:true}. Matches testID, label, placeholder, then the same joined text screen.snapshot shows. Default timeout 5000 ms; a timeout lists near misses.",
|
|
1601
|
+
description: "Wait until a target is on screen, or gone with {gone:true}. Matches testID, label, placeholder, then the same joined text screen.snapshot shows. {scroll:true} also accepts a match rendered off screen and scrolls it into view (not with gone). Default timeout 5000 ms; a timeout lists near misses.",
|
|
1532
1602
|
run: (target, options) => screen.waitFor(target, options)
|
|
1533
1603
|
}
|
|
1534
1604
|
};
|
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"
|