@ishlabs/cli 0.24.0 → 0.25.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.
Files changed (40) hide show
  1. package/dist/commands/ask.js +3 -3
  2. package/dist/commands/iteration.js +1 -1
  3. package/dist/commands/study-analyze.js +1 -1
  4. package/dist/commands/study-run.js +83 -15
  5. package/dist/commands/study.js +11 -7
  6. package/dist/lib/alias-store.js +1 -1
  7. package/dist/lib/api-client.d.ts +2 -0
  8. package/dist/lib/billing.d.ts +30 -16
  9. package/dist/lib/billing.js +77 -27
  10. package/dist/lib/docs.js +57 -42
  11. package/dist/lib/local-sim/actions.d.ts +10 -2
  12. package/dist/lib/local-sim/actions.js +16 -11
  13. package/dist/lib/local-sim/adb.d.ts +103 -0
  14. package/dist/lib/local-sim/adb.js +352 -0
  15. package/dist/lib/local-sim/android.d.ts +111 -0
  16. package/dist/lib/local-sim/android.js +499 -0
  17. package/dist/lib/local-sim/apk-manifest.d.ts +22 -0
  18. package/dist/lib/local-sim/apk-manifest.js +210 -0
  19. package/dist/lib/local-sim/browser.d.ts +22 -0
  20. package/dist/lib/local-sim/browser.js +65 -0
  21. package/dist/lib/local-sim/coordinates.d.ts +69 -0
  22. package/dist/lib/local-sim/coordinates.js +59 -0
  23. package/dist/lib/local-sim/device.d.ts +143 -0
  24. package/dist/lib/local-sim/device.js +152 -0
  25. package/dist/lib/local-sim/ios.d.ts +168 -0
  26. package/dist/lib/local-sim/ios.js +546 -0
  27. package/dist/lib/local-sim/loop.d.ts +14 -2
  28. package/dist/lib/local-sim/loop.js +166 -73
  29. package/dist/lib/local-sim/native-a11y.d.ts +97 -0
  30. package/dist/lib/local-sim/native-a11y.js +384 -0
  31. package/dist/lib/local-sim/simctl.d.ts +85 -0
  32. package/dist/lib/local-sim/simctl.js +273 -0
  33. package/dist/lib/local-sim/types.d.ts +37 -2
  34. package/dist/lib/local-sim/upload.d.ts +1 -1
  35. package/dist/lib/local-sim/upload.js +9 -6
  36. package/dist/lib/modality.d.ts +10 -1
  37. package/dist/lib/modality.js +21 -0
  38. package/dist/lib/output.js +58 -12
  39. package/dist/lib/skill-content.js +10 -9
  40. package/package.json +2 -1
package/dist/lib/docs.js CHANGED
@@ -339,7 +339,7 @@ pick was wrong.
339
339
  - \`guides/slicing-results\` — filter / project \`study results\` by frame,
340
340
  segment, turn, sentiment, assignment, step.
341
341
  - \`reference/billing-limits\` — \`maxStudiesPerProduct\` cap on study creation.
342
- - \`reference/credits\` — per-run credit cost & how to preview before dispatch.
342
+ - \`reference/credits\` — per-run credit draw & how to preview before dispatch.
343
343
  `;
344
344
  const CONCEPT_ITERATION = `# concept: iteration
345
345
 
@@ -806,7 +806,7 @@ Treat this as actionable, not transient — re-running won't change anything.
806
806
  - \`concepts/run-verbs\` — how \`ish study run\` selects the iteration.
807
807
  - \`concepts/people\` — how participants are picked for a run.
808
808
  - \`reference/billing-limits\` — \`maxIterationsPerStudy\` cap on iteration creation.
809
- - \`reference/credits\` — per-iteration-run credit cost & preview shape (\`pair_preview.credit_estimate\` for participant-pair, top-level \`credit_estimate\` otherwise).
809
+ - \`reference/credits\` — per-iteration-run credit draw & preview shape (\`pair_preview.credit_estimate\` for participant-pair, top-level \`credit_estimate\` otherwise).
810
810
  `;
811
811
  const CONCEPT_ASSIGNMENT = `# concept: assignment
812
812
 
@@ -987,7 +987,7 @@ ish ask results a-6ec --json | jq '.rounds[0].aggregates'
987
987
 
988
988
  Asks carry a top-level \`status\`:
989
989
 
990
- - \`draft\` — created but not dispatched yet. No credits charged. Created
990
+ - \`draft\` — created but not dispatched yet. No credits drawn. Created
991
991
  by \`ish ask create --no-dispatch\`.
992
992
  - \`running\` — dispatched; the round is executing or queued.
993
993
  - \`completed\` — round 1 (or the most recent round) finished.
@@ -1000,10 +1000,10 @@ intact — no \`--verbose\` needed to see it.
1000
1000
  ## Stage-then-dispatch (draft asks)
1001
1001
 
1002
1002
  When you want a human to review the people and prompt **before** any
1003
- credits are spent, separate creation from dispatch:
1003
+ credits are drawn, separate creation from dispatch:
1004
1004
 
1005
1005
  \`\`\`
1006
- # 1. Stage — materializes participants, no worker enqueue, no bill yet
1006
+ # 1. Stage — materializes participants, no worker enqueue, no credits drawn yet
1007
1007
  ish ask create --workspace w-6ec --name "tagline AB" \\
1008
1008
  --prompt "Which sounds better?" \\
1009
1009
  --variant text:"Short and punchy." \\
@@ -1013,18 +1013,20 @@ ish ask create --workspace w-6ec --name "tagline AB" \\
1013
1013
 
1014
1014
  # Returns an ask with status="draft". Hand the alias back to the user.
1015
1015
 
1016
- # 2. Dispatch — flips DRAFT → RUNNING and enqueues the round (BILLABLE)
1016
+ # 2. Dispatch — flips DRAFT → RUNNING and enqueues the round (draws credits)
1017
1017
  ish ask dispatch a-6ec --wait
1018
1018
  \`\`\`
1019
1019
 
1020
1020
  \`--no-dispatch\` requires people flags (participants are still materialized
1021
- at create time — only the worker enqueue and billing are deferred). It
1022
- is incompatible with \`--wait\` since there is nothing to wait for.
1021
+ at create time — only the worker enqueue and the credit draw are
1022
+ deferred). It is incompatible with \`--wait\` since there is nothing to
1023
+ wait for.
1023
1024
 
1024
1025
  \`ish ask dispatch\` is idempotent on the server: a non-DRAFT ask returns
1025
1026
  HTTP 409 (\`already dispatched\`) which the CLI maps to a usage error, so
1026
1027
  re-running the command is safe. The user who calls \`dispatch\` is the
1027
- billing principal — keep that in mind for shared workspaces.
1028
+ principal whose credits are drawn — keep that in mind for shared
1029
+ workspaces.
1028
1030
 
1029
1031
  ## Reading the verdict
1030
1032
 
@@ -1170,7 +1172,7 @@ deleted ask was the active one.
1170
1172
  - \`concepts/round\` — what a round is and how it executes.
1171
1173
  - \`concepts/people\` — how participants are chosen at ask creation.
1172
1174
  - \`concepts/run-verbs\` — \`ish ask run\` vs \`ish study run\`.
1173
- - \`reference/credits\` — ask rounds bill **one credit per successful participant per round**, regardless of how many \`questions\` were included. The backend's asks worker bills \`amount=succeeded\` once per round dispatch; questions and round-summary synthesis don't trigger separate debits. A 3-person panel with 2 follow-up questions costs \`3\` credits when all complete, the same as a no-questions run. Failed participant responses (pre-flight errors, refusals) don't bill.
1175
+ - \`reference/credits\` — ask rounds draw **one credit per successful participant per round**, regardless of how many \`questions\` were included. The backend's asks worker draws \`amount=succeeded\` once per round dispatch; questions and round-summary synthesis don't draw separately. A 3-person panel with 2 follow-up questions draws \`3\` credits when all complete, the same as a no-questions run. Failed participant responses (pre-flight errors, refusals) draw nothing.
1174
1176
  `;
1175
1177
  const CONCEPT_ROUND = `# concept: round
1176
1178
 
@@ -1197,7 +1199,7 @@ ish ask results a-6ec --round 1
1197
1199
 
1198
1200
  Appending questions to a completed round preserves prior data — variant
1199
1201
  comments, picks, ratings, and earlier-question answers all stay. Only
1200
- the new question(s) get dispatched to the existing participants. Cost is
1202
+ the new question(s) get dispatched to the existing participants. Usage is
1201
1203
  roughly N phase-2 LLM calls instead of 2N (no phase-1 re-run). Errored
1202
1204
  responses are skipped entirely; completed responses flip to PENDING and
1203
1205
  re-finalize after the new question is answered.
@@ -1902,16 +1904,16 @@ load-bearing return value — same exception \`study run\` makes.
1902
1904
  | Source not terminal (RUNNING / QUEUED) | \`Participant is still running — cancel it first or wait for completion.\` | 2 |
1903
1905
  | Source participant not found | \`Participant not found: <id>\` | 4 |
1904
1906
  | \`additional_steps\` out of range | Client-side parser rejects before the network call | 2 |
1905
- | Insufficient credits | Bubbles the server message; retry only after topping up | 5 |
1907
+ | Insufficient credits | Bubbles the server message; retry only after the balance is replenished | 5 |
1906
1908
  | Wait timed out (\`--wait\` only) | \`WaitTimeoutError\` envelope with current status under \`progress.rows[0]\` — the run keeps going server-side; resume with \`study wait <new-participant>\` | 5 |
1907
1909
 
1908
- ## Cost model
1910
+ ## Credit model
1909
1911
 
1910
- \`extend\` charges credits for **only \`additional_steps\`**, not for
1912
+ \`extend\` draws credits for **only \`additional_steps\`**, not for
1911
1913
  the source's original \`max_interactions\` cap. The formula is the same
1912
1914
  as \`study run\` for interactive runs: \`max(1, round(N / 10))\` per
1913
- participant. So \`--add-steps 10\` costs **1 credit**; \`--add-steps 50\`
1914
- costs **5 credits**. See \`reference/credits\` for the full table.
1915
+ participant. So \`--add-steps 10\` draws **1 credit**; \`--add-steps 50\`
1916
+ draws **5 credits**. See \`reference/credits\` for the full table.
1915
1917
 
1916
1918
  ## Worked example — push past the step cap
1917
1919
 
@@ -1945,7 +1947,7 @@ ish study extend pt-072 \\
1945
1947
 
1946
1948
  - \`concepts/run-verbs\` — the top-level decision rule (\`study run\` vs
1947
1949
  \`ask run\`); extend is a lifecycle verb downstream of either.
1948
- - \`reference/credits\` — per-modality cost formulas. \`extend\` follows
1950
+ - \`reference/credits\` — per-modality credit-draw formulas. \`extend\` follows
1949
1951
  the interactive formula scaled to \`additional_steps\`.
1950
1952
  - \`reference/aliases\` — the \`t-…\` prefix and how aliases resolve.
1951
1953
  - \`reference/json-mode\` — capture-mode (\`--get participant_alias\`) for
@@ -2366,7 +2368,7 @@ The CLI guarantees these contracts so agents can chain safely:
2366
2368
  \`study update <id> --modality text\` before adding iterations.
2367
2369
  - **\`ask add-questions\` is additive by default.** Appending questions
2368
2370
  preserves variant comments / picks / ratings / prior-question
2369
- answers; only the new question(s) get dispatched. Cost: roughly N
2371
+ answers; only the new question(s) get dispatched. Usage: roughly N
2370
2372
  phase-2 LLM calls instead of 2N. Pass \`--redispatch-all\` for the
2371
2373
  legacy reset behavior when you want fresh first impressions.
2372
2374
  - **\`ask results --json\` includes \`cross_round_summary\` for 2+
@@ -2990,49 +2992,62 @@ of scope: \`workspace\`, \`config\`, \`docs\`, \`init\`, \`login\`,
2990
2992
  including \`--get workspace.alias\` to capture the active workspace
2991
2993
  without piping \`ish status --json\` through \`jq\`.
2992
2994
  `;
2993
- const REFERENCE_CREDITS = `# reference: credits & cost preview
2995
+ const REFERENCE_CREDITS = `# reference: credits & usage preview
2994
2996
 
2995
- Every billable run (study, ask, insight) costs **credits**. The CLI
2996
- surfaces a cost upper bound *before* you dispatch so you can budget. The
2997
- backend is the authoritative source its rejection envelope on
2998
- \`insufficient_credits\` carries the live required/available pair.
2997
+ Credits are ish's **usage allowance** a resource that runs draw, not
2998
+ money you pay per call. Each run (study, ask, insight) **draws credits**
2999
+ from the account's balance. Drawing credits to run studies and asks is
3000
+ the normal, expected way to use ish — an agent should run them without
3001
+ hesitation, the same as making any other tool call. The job here is just
3002
+ to be *aware* of the balance, not to ration it.
2999
3003
 
3000
- ## How costs are shaped
3004
+ **Where credits come from:**
3005
+ - **Paid plans (Starter / Pro / Enterprise)** get a **monthly credit
3006
+ allowance** that refills each billing cycle.
3007
+ - **The free tier** gets a **one-time signup grant** — it is *not*
3008
+ refilled monthly. Once it's drawn down, the user adds more or upgrades.
3009
+
3010
+ The CLI surfaces a usage upper bound *before* you dispatch so you can see
3011
+ how much a run will draw. The backend is the authoritative source — its
3012
+ rejection envelope on \`insufficient_credits\` carries the live
3013
+ required/available pair.
3014
+
3015
+ ## How usage is shaped
3001
3016
 
3002
3017
  The formula has the same shape across modalities — \`max(1, round(N / 10))\`
3003
3018
  per principal — but the inputs differ. **Treat the rates below as the
3004
3019
  current calibration**; they will evolve as we differentiate per-modality
3005
- compute cost. Agents should:
3020
+ compute. Agents should:
3006
3021
 
3007
- - For prospective cost preview: read \`credit_estimate\` from \`study run\`'s
3022
+ - For prospective usage preview: read \`credit_estimate\` from \`study run\`'s
3008
3023
  JSON envelope (top-level for solo/media runs; under \`pair_preview\` for
3009
3024
  participant-pair chat).
3010
- - For hard budget checks: catch the backend's \`insufficient_credits\`
3025
+ - For hard balance checks: catch the backend's \`insufficient_credits\`
3011
3026
  rejection (HTTP 402; envelope shape below) and react to
3012
3027
  \`required\` / \`available\`.
3013
3028
 
3014
- | Surface | Per-principal cost | Total formula | Example |
3029
+ | Surface | Per-principal draw | Total formula | Example |
3015
3030
  |---------------------|---------------------------------|--------------------------------------------------|--------------------------------------|
3016
3031
  | Interactive (URL) | \`max(1, round(steps/10))\` | \`participants × per-participant\` | 10 participants × 30 steps → 30 credits |
3017
3032
  | Text/image/video/audio/document | same | same | 5 participants × 20 steps → 10 credits |
3018
3033
  | Chat (external chatbot, solo) | \`max(1, round(turns/10))\` | \`participants × per-participant\` | 5 participants × 12 turns → 10 credits |
3019
3034
  | Chat (participant pair) | \`max(1, round(turns/10))\` × 2 | \`conv × per-side × 2\` | 3 conv × 14 turns → 6 credits |
3020
3035
  | Ask round | 1 / successful response | \`successful_participants\` | 50 responses → 50 credits |
3021
- | Study insights | first free, then **10 flat** | n/a | 2nd analysis → 10 credits |
3036
+ | Study insights | first included, then **10 flat** | n/a | 2nd analysis → 10 credits |
3022
3037
 
3023
3038
  All numbers are **upper bounds**. Early termination, refusals, or
3024
- backend trimming can reduce actual charge.
3039
+ backend trimming can reduce the actual draw.
3025
3040
 
3026
- ## Capping interactive/media spend (\`--max-interactions\`)
3041
+ ## Capping interactive/media usage (\`--max-interactions\`)
3027
3042
 
3028
3043
  \`ish study run\` always sends \`max_interactions\` to the backend for
3029
3044
  interactive and media runs. Precedence: \`--max-interactions <n>\` flag
3030
3045
  > the iteration's stored \`details.max_interactions\` > **CLI default
3031
- of 20**. The default exists to prevent runaway spend when a participant
3032
- gets stuck on a broken or non-responsive surface — without a cap, one
3033
- stuck participant can rack up 100+ steps before the SDK gives up. Pass
3034
- \`--max-interactions\` to override (e.g. \`--max-interactions 50\` for
3035
- deeper exploration, \`--max-interactions 5\` for a cheap smoke test).
3046
+ of 20**. The default exists to prevent runaway credit draw when a
3047
+ participant gets stuck on a broken or non-responsive surface — without a
3048
+ cap, one stuck participant can rack up 100+ steps before the SDK gives
3049
+ up. Pass \`--max-interactions\` to override (e.g. \`--max-interactions 50\`
3050
+ for deeper exploration, \`--max-interactions 5\` for a quick smoke test).
3036
3051
  The confirmation block shows the resolved value and where it came
3037
3052
  from (flag / iteration / CLI default). The JSON envelope's
3038
3053
  \`credit_estimate.breakdown\` reflects the dispatched value.
@@ -3121,7 +3136,7 @@ HTTP 402. The CLI surfaces it as a structured error envelope:
3121
3136
  \`\`\`
3122
3137
 
3123
3138
  Exit code \`1\` (non-retryable). Don't poll — the user has to upgrade or
3124
- free credits before re-dispatch.
3139
+ free up credits before re-dispatch.
3125
3140
 
3126
3141
  ## Agent recipe
3127
3142
 
@@ -3135,7 +3150,7 @@ free credits before re-dispatch.
3135
3150
 
3136
3151
  ## Caveats
3137
3152
 
3138
- - The CLI's preview uses the **same formula** the backend bills with,
3153
+ - The CLI's preview uses the **same formula** the backend draws against,
3139
3154
  but does **not** make a network preflight call — it's pure math
3140
3155
  client-side. If the backend formula changes mid-version, the preview
3141
3156
  will drift until the CLI is updated. The \`insufficient_credits\`
@@ -3238,9 +3253,9 @@ upgrade or delete an existing resource to free up headroom.
3238
3253
 
3239
3254
  ## Related
3240
3255
 
3241
- - \`reference/credits\` — per-run credit cost & preview (separate from
3256
+ - \`reference/credits\` — per-run credit draw & preview (separate from
3242
3257
  these entity caps; this page is about *how many things you can have*,
3243
- that page is about *how much each run costs*).
3258
+ that page is about *how many credits each run draws*).
3244
3259
  - \`concepts/workspace\` — \`maxProducts\` is per-account.
3245
3260
  - \`concepts/study\` — \`maxStudiesPerProduct\` gates study creation.
3246
3261
  - \`concepts/iteration\` — \`maxIterationsPerStudy\` gates iteration creation.
@@ -4442,8 +4457,8 @@ const PAGES = [
4442
4457
  },
4443
4458
  {
4444
4459
  slug: "reference/credits",
4445
- title: "reference: credits & cost preview",
4446
- description: "Per-modality credit cost formulas, where the CLI surfaces cost estimates (Scale line, pair_preview.credit_estimate, top-level credit_estimate), tier allotments, insufficient_credits error shape.",
4460
+ title: "reference: credits & usage preview",
4461
+ description: "Credits as a usage allowance (paid plans refill monthly; free tier is a one-time signup grant), per-modality credit draw formulas, where the CLI surfaces usage estimates (Scale line, pair_preview.credit_estimate, top-level credit_estimate), tier allotments, insufficient_credits error shape.",
4447
4462
  body: REFERENCE_CREDITS,
4448
4463
  },
4449
4464
  {
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * Action executor — resolves elements and executes Playwright actions.
3
3
  *
4
- * Resolution strategy:
4
+ * Element resolution strategy (browser only):
5
5
  * 1. CDP node resolution (using node_id from tree data)
6
6
  * 2. Playwright locator fallback (using element_name + element_type)
7
- * 3. Coordinate fallback (if returned by backend)
7
+ *
8
+ * Native (Android) targets are vision-located by the backend and tapped via
9
+ * normalized coordinates in AndroidDevice.executeAction — that coordinate path
10
+ * lives there, not here.
8
11
  */
9
12
  import type { Page } from "playwright-core";
10
13
  import type { LocalStepAction, ActionResult, ContextValue, TreeData } from "./types.js";
@@ -13,6 +16,11 @@ import type { TabManager } from "./tabs.js";
13
16
  * Execute a single action on the page.
14
17
  */
15
18
  export declare function executeAction(page: Page, action: LocalStepAction, treeData: TreeData, contextValues: ContextValue[], tabs?: TabManager): Promise<ActionResult>;
19
+ /**
20
+ * Resolve the actual text to type from an action, handling var/secret value types.
21
+ * Exported so the native (Android) executor can resolve values the same way.
22
+ */
23
+ export declare function resolveTextValue(action: LocalStepAction, contextValues: ContextValue[]): string;
16
24
  /**
17
25
  * Compare two base64 screenshots to detect visible change.
18
26
  */
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * Action executor — resolves elements and executes Playwright actions.
3
3
  *
4
- * Resolution strategy:
4
+ * Element resolution strategy (browser only):
5
5
  * 1. CDP node resolution (using node_id from tree data)
6
6
  * 2. Playwright locator fallback (using element_name + element_type)
7
- * 3. Coordinate fallback (if returned by backend)
7
+ *
8
+ * Native (Android) targets are vision-located by the backend and tapped via
9
+ * normalized coordinates in AndroidDevice.executeAction — that coordinate path
10
+ * lives there, not here.
8
11
  */
9
12
  import { resolveNodeToBoundingBox } from "./browser.js";
10
13
  import { isDebugEnabled } from "./debug.js";
@@ -78,7 +81,7 @@ export async function executeAction(page, action, treeData, contextValues, tabs)
78
81
  coordinates = await executeTextInput(page, action, treeData, contextValues);
79
82
  break;
80
83
  case "scroll":
81
- await executeScroll(page, action, treeData);
84
+ await executeScroll(page, action);
82
85
  break;
83
86
  case "swipe":
84
87
  case "pull_to_refresh":
@@ -87,9 +90,6 @@ export async function executeAction(page, action, treeData, contextValues, tabs)
87
90
  case "wait":
88
91
  await page.waitForTimeout(action.duration_ms ?? 1000);
89
92
  break;
90
- case "navigate_back":
91
- await page.goBack({ timeout: 10_000 }).catch(() => { });
92
- break;
93
93
  case "long_press":
94
94
  coordinates = await executeLongPress(page, action, treeData);
95
95
  break;
@@ -162,7 +162,7 @@ async function resolveElement(page, action, treeData) {
162
162
  /**
163
163
  * Resolve to a Playwright Locator (for fill/type operations that need a Locator).
164
164
  */
165
- async function resolveLocator(page, action, treeData) {
165
+ async function resolveLocator(page, action) {
166
166
  return findElement(page, action);
167
167
  }
168
168
  /**
@@ -237,7 +237,7 @@ async function executeTextInput(page, action, treeData, contextValues) {
237
237
  // Resolve the actual text to type
238
238
  const text = resolveTextValue(action, contextValues);
239
239
  // Try to get a Playwright locator for fill operations
240
- const locator = await resolveLocator(page, action, treeData);
240
+ const locator = await resolveLocator(page, action);
241
241
  if (locator) {
242
242
  if (action.mode === "click_type") {
243
243
  await locator.click({ timeout: 5000 });
@@ -273,7 +273,7 @@ async function executeTextInput(page, action, treeData, contextValues) {
273
273
  }
274
274
  }
275
275
  }
276
- async function executeScroll(page, action, treeData) {
276
+ async function executeScroll(page, action) {
277
277
  const viewport = page.viewportSize() ?? { width: 1440, height: 900 };
278
278
  const amountMap = {
279
279
  small: 0.5, medium: 0.8, large: 1.5, extra_large: 3.0,
@@ -378,8 +378,9 @@ async function executeKeyboardShortcut(page, action) {
378
378
  // --- Helpers ---
379
379
  /**
380
380
  * Resolve the actual text to type from an action, handling var/secret value types.
381
+ * Exported so the native (Android) executor can resolve values the same way.
381
382
  */
382
- function resolveTextValue(action, contextValues) {
383
+ export function resolveTextValue(action, contextValues) {
383
384
  if (action.value_type === "var" || action.value_type === "secret") {
384
385
  const cv = contextValues.find(v => v.name === action.value);
385
386
  if (cv?.value)
@@ -433,7 +434,11 @@ export function describeAction(action) {
433
434
  case "double_tap":
434
435
  return `double_tap on '${element}'${modSuffix}`;
435
436
  case "drag":
436
- return `drag '${element}'`;
437
+ return action.drag
438
+ ? `drag '${element}' (${action.drag.startX},${action.drag.startY}→${action.drag.endX},${action.drag.endY})`
439
+ : `drag '${element}'`;
440
+ case "rotate_device":
441
+ return `rotate_device ${action.orientation ?? "?"}`;
437
442
  case "think":
438
443
  return `think: "${(action.thoughts ?? "").slice(0, 50)}"`;
439
444
  case "pull_to_refresh":
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Thin async wrappers over the `adb` CLI for the native-Android sim path.
3
+ *
4
+ * One emulator/device is assumed (the lead coordinates a single shared
5
+ * emulator). Every call shells out to a resolved `adb` binary; binary output
6
+ * (screencap) is captured without a utf-8 round-trip so PNG bytes survive.
7
+ *
8
+ * Coordinate space: `adb shell screencap` and `adb shell input tap` share ONE
9
+ * pixel space — there is NO DPR correction. The native sim de-normalizes the
10
+ * backend's 0-1000 coordinates against the screencap pixel size and taps
11
+ * directly. (Verified by the Layer-1 driver smoke; see scripts/mobile-e2e.)
12
+ */
13
+ export declare class AdbError extends Error {
14
+ constructor(message: string);
15
+ }
16
+ /** Run `adb <args>` and return trimmed stdout. Throws AdbError on failure. */
17
+ export declare function adb(args: string[], timeoutMs?: number): Promise<string>;
18
+ /** Run `adb shell <args>` and return trimmed stdout. */
19
+ export declare function adbShell(args: string[], timeoutMs?: number): Promise<string>;
20
+ /**
21
+ * Capture the current screen as raw PNG bytes via `adb exec-out screencap -p`.
22
+ * `exec-out` (not `shell`) avoids the CRLF translation that corrupts binary
23
+ * output. Returns the PNG buffer at full device resolution.
24
+ */
25
+ export declare function screencapPng(): Promise<Buffer>;
26
+ /** Assert exactly one device/emulator is in the `device` state. */
27
+ export declare function requireOneDevice(): Promise<void>;
28
+ export declare function inputTap(x: number, y: number): Promise<void>;
29
+ export declare function inputSwipe(x1: number, y1: number, x2: number, y2: number, durationMs?: number): Promise<void>;
30
+ /**
31
+ * A drag GRABS an element and drops it elsewhere — press, HOLD to pick up,
32
+ * move, release. We use `input draganddrop`, NOT a slow `input swipe`: a swipe
33
+ * never holds at the start, so on the surfaces a drag actually targets
34
+ * (launcher icons, reorderable list rows — all `long-clickable`) the framework
35
+ * reads a slow swipe as a directional SWIPE (e.g. it opens the app drawer)
36
+ * instead of picking the element up. `draganddrop` dwells at the press point
37
+ * first, triggering the long-press pickup, then moves and releases — verified
38
+ * on-device to actually rearrange a launcher icon where the slow swipe didn't.
39
+ * Requires API 30+ (`input draganddrop`); on the dev emulators/sim devices the
40
+ * native driver targets, that's always present. Coordinates are screencap
41
+ * pixels (the same space as tap/swipe — no DPR correction).
42
+ */
43
+ export declare function inputDrag(x1: number, y1: number, x2: number, y2: number, durationMs?: number): Promise<void>;
44
+ /** A long-press is a zero-distance swipe held for `durationMs`. */
45
+ export declare function inputLongPress(x: number, y: number, durationMs?: number): Promise<void>;
46
+ export declare function pressKeyEvent(keyevent: string): Promise<void>;
47
+ /**
48
+ * Force a device orientation. We first disable auto-rotation
49
+ * (`accelerometer_rotation 0`) — otherwise the sensor immediately overrides
50
+ * our fixed `user_rotation`. `user_rotation` is 0=portrait, 1=landscape (90°).
51
+ */
52
+ export declare function setUserRotation(orientation: "portrait" | "landscape"): Promise<void>;
53
+ export declare const ADB_KEYBOARD_PKG = "com.android.adbkeyboard";
54
+ /** True if the ADBKeyboard IME is installed on the device. */
55
+ export declare function isAdbKeyboardInstalled(): Promise<boolean>;
56
+ /** Currently-selected IME id (so we can restore it after the run). */
57
+ export declare function currentIme(): Promise<string | null>;
58
+ /** Select an IME by its component id. */
59
+ export declare function setIme(imeId: string): Promise<void>;
60
+ /** Reset the IME to the system default (used when we had no prior IME to restore). */
61
+ export declare function resetIme(): Promise<void>;
62
+ /** Make ADBKeyboard the active IME. */
63
+ export declare function enableAdbKeyboard(): Promise<void>;
64
+ /**
65
+ * Type text into the focused field via ADBKeyboard's base64 broadcast.
66
+ * base64 keeps spaces/unicode/quotes intact across the adb shell boundary.
67
+ * Note: the text transits as a base64 argv arg, so it's briefly visible in the
68
+ * device-side process list (`ps`) — fine for sim input, not a secret channel
69
+ * (context secrets are resolved here but this is local-dev/emulator only).
70
+ */
71
+ export declare function adbKeyboardType(text: string): Promise<void>;
72
+ /** Clear the focused field (ADBKeyboard ADB_CLEAR_TEXT broadcast). */
73
+ export declare function adbKeyboardClear(): Promise<void>;
74
+ /**
75
+ * Read width/height from a PNG buffer's IHDR chunk. The IHDR is always the
76
+ * first chunk: 8-byte signature, 4-byte length, 4-byte "IHDR", then width and
77
+ * height as big-endian uint32 at byte offsets 16 and 20. Avoids pulling in an
78
+ * image library just to learn the screencap's pixel size.
79
+ */
80
+ export declare function pngDimensions(png: Buffer): {
81
+ width: number;
82
+ height: number;
83
+ };
84
+ export declare function forceStop(pkg: string): Promise<void>;
85
+ /** Launch the app's default launchable activity via monkey (no activity name needed). */
86
+ export declare function launchApp(pkg: string): Promise<void>;
87
+ export declare function installApk(apkPath: string): Promise<void>;
88
+ export declare function isPackageInstalled(pkg: string): Promise<boolean>;
89
+ /**
90
+ * Dump the current view hierarchy as uiautomator XML and return it. Mirrors the
91
+ * oracle's `ui_dump`: uiautomator fails transiently with "window in transition"
92
+ * (right after a UI change, writes nothing) or "could not get idle state" (a
93
+ * view animates forever), so we retry a few times, nudging the device awake
94
+ * between tries since a screen-off device never goes idle-with-content.
95
+ *
96
+ * Uses `exec-out uiautomator dump /dev/tty` so the XML comes back on stdout in
97
+ * one shot (no pull); falls back to dump-to-file + pull if the device writes the
98
+ * "dumped to" line to a path instead. Throws AdbError if every attempt fails so
99
+ * the caller can degrade to the vision path.
100
+ */
101
+ export declare function dumpUiautomatorXml(): Promise<string>;
102
+ /** All installed package names (the set of `package:<name>` lines). */
103
+ export declare function listPackages(): Promise<Set<string>>;