ai-browser-toolkit 0.2.0__tar.gz → 0.2.2__tar.gz

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 (140) hide show
  1. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/PKG-INFO +2 -2
  2. ai_browser_toolkit-0.2.2/guidelines/toolkit-workflow.md +390 -0
  3. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/packaging/aur/PKGBUILD.template +1 -1
  4. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/packaging/homebrew/formula.rb.template +1 -1
  5. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/packaging/nfpm.yaml +1 -1
  6. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/packaging/scoop/manifest.json.template +1 -1
  7. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/pyproject.toml +3 -3
  8. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/browser.py +41 -4
  9. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/cli.py +137 -8
  10. ai_browser_toolkit-0.2.2/src/abt/errors.py +99 -0
  11. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/guidelines.py +10 -0
  12. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/ops/navigate.py +6 -1
  13. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/server.py +14 -0
  14. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_guidelines.py +106 -0
  15. ai_browser_toolkit-0.2.0/guidelines/toolkit-workflow.md +0 -433
  16. ai_browser_toolkit-0.2.0/src/abt/errors.py +0 -34
  17. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/.gitattributes +0 -0
  18. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/.github/workflows/check-secrets.yml +0 -0
  19. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/.github/workflows/ci.yml +0 -0
  20. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/.github/workflows/release.yml +0 -0
  21. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/.gitignore +0 -0
  22. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/.sheet-url +0 -0
  23. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/AGENTS.md +0 -0
  24. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/CLAUDE.md +0 -0
  25. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/GEMINI.md +0 -0
  26. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/LICENSE +0 -0
  27. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/README.md +0 -0
  28. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/TODO.md +0 -0
  29. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/engine-choice-for-profile-sessions.md +0 -0
  30. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/known-issues.md +0 -0
  31. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/packaging.md +0 -0
  32. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/playwright-spike-2026-08-19.md +0 -0
  33. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/session-review-20260806-linkedin.md +0 -0
  34. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/superpowers/plans/2026-08-19-browser-server-decoupling.md +0 -0
  35. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/superpowers/plans/2026-08-20-packaging-and-distribution.md +0 -0
  36. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/superpowers/specs/2026-08-03-selenium-browser-api-design.md +0 -0
  37. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/superpowers/specs/2026-08-19-browser-server-decoupling-design.md +0 -0
  38. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/superpowers/specs/2026-08-19-profile-sessions-design.md +0 -0
  39. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/docs/superpowers/specs/2026-08-20-packaging-and-distribution-design.md +0 -0
  40. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/guidelines/README.md +0 -0
  41. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/guidelines/docs.google.com/docs.md +0 -0
  42. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/guidelines/docs.google.com/meta.json +0 -0
  43. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/guidelines/docs.google.com/sheets.md +0 -0
  44. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/guidelines/index.json +0 -0
  45. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/guidelines/messenger.com/messenger.md +0 -0
  46. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/guidelines/messenger.com/meta.json +0 -0
  47. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/guidelines/script.google.com/forms-apps-script.md +0 -0
  48. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/guidelines/script.google.com/meta.json +0 -0
  49. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/packaging/aur/aibrowsertoolkit-bin.install +0 -0
  50. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/packaging/build_index.py +0 -0
  51. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/packaging/bundle.py +0 -0
  52. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/packaging/postinstall.sh +0 -0
  53. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/packaging/preremove.sh +0 -0
  54. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/packaging/smoke.sh +0 -0
  55. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/packaging/windows/abt.iss +0 -0
  56. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/__init__.py +0 -0
  57. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/__main__.py +0 -0
  58. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/autostart.py +0 -0
  59. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/diff.py +0 -0
  60. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/doctor.py +0 -0
  61. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/engine.py +0 -0
  62. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/frames.py +0 -0
  63. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/launch.py +0 -0
  64. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/mcp.py +0 -0
  65. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/messenger.py +0 -0
  66. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/ops/__init__.py +0 -0
  67. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/ops/control.py +0 -0
  68. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/ops/inspect.py +0 -0
  69. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/ops/interact.py +0 -0
  70. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/ops/read.py +0 -0
  71. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/ops/tabs.py +0 -0
  72. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/paths.py +0 -0
  73. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/proc.py +0 -0
  74. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/pwdriver.py +0 -0
  75. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/recorder.py +0 -0
  76. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/refs.py +0 -0
  77. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/schema.py +0 -0
  78. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/shadow.py +0 -0
  79. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/shots.py +0 -0
  80. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/targeting.py +0 -0
  81. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/src/abt/viewer.py +0 -0
  82. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/start-server.bat +0 -0
  83. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/start-server.sh +0 -0
  84. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/conftest.py +0 -0
  85. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/actionable.html +0 -0
  86. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/canvas.html +0 -0
  87. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/cards.html +0 -0
  88. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/chainfetch.html +0 -0
  89. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/controlled.html +0 -0
  90. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/controlled_trusted.html +0 -0
  91. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/delayed.html +0 -0
  92. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/form.html +0 -0
  93. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/framed_button.html +0 -0
  94. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/framed_preload.html +0 -0
  95. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/frames.html +0 -0
  96. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/frames_cross.html +0 -0
  97. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/frames_order.html +0 -0
  98. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/frames_swap.html +0 -0
  99. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/inspect.html +0 -0
  100. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/late.html +0 -0
  101. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/links.html +0 -0
  102. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/messenger.html +0 -0
  103. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/nav.html +0 -0
  104. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/overlay.html +0 -0
  105. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/repeated.html +0 -0
  106. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/shadow.html +0 -0
  107. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/fixtures/slowfetch.html +0 -0
  108. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_actionable.py +0 -0
  109. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_autostart.py +0 -0
  110. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_bundle.py +0 -0
  111. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_channel_names.py +0 -0
  112. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_click_at.py +0 -0
  113. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_diff.py +0 -0
  114. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_doctor.py +0 -0
  115. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_engine.py +0 -0
  116. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_frames.py +0 -0
  117. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_hit_test.py +0 -0
  118. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_inspect.py +0 -0
  119. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_launch.py +0 -0
  120. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_lifecycle.py +0 -0
  121. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_lifecycle_live.py +0 -0
  122. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_mcp.py +0 -0
  123. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_messenger.py +0 -0
  124. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_ops_click_options.py +0 -0
  125. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_ops_interact.py +0 -0
  126. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_ops_navigate.py +0 -0
  127. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_ops_read.py +0 -0
  128. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_ops_tabs.py +0 -0
  129. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_paths.py +0 -0
  130. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_proc.py +0 -0
  131. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_recorder.py +0 -0
  132. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_schema.py +0 -0
  133. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_server.py +0 -0
  134. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_server_logs.py +0 -0
  135. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_settle.py +0 -0
  136. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_shadow.py +0 -0
  137. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_shots.py +0 -0
  138. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_shots_live.py +0 -0
  139. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_targeting.py +0 -0
  140. {ai_browser_toolkit-0.2.0 → ai_browser_toolkit-0.2.2}/tests/test_workflows.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ai-browser-toolkit
3
- Version: 0.2.0
4
- Summary: Browser API for AI agents, over HTTP
3
+ Version: 0.2.2
4
+ Summary: Agentic browser automation CLI for AI agents
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
7
7
  Requires-Python: >=3.11
@@ -0,0 +1,390 @@
1
+ # Toolkit workflow
2
+
3
+ Concepts for driving the AI Browser Toolkit well. Read this before touching any
4
+ site. These are the lessons that took the longest to learn.
5
+
6
+ ## The mental model
7
+
8
+ A long-running server owns a real Chrome window, and **`abt` drives it**. You
9
+ do not write WebDriver code; you run small commands and read JSON back. The
10
+ browser stays up between commands, so tabs, logins and focus persist.
11
+
12
+ ```bash
13
+ abt status # a server up? usually yes. URL, tabs, refs
14
+ abt up # start one if not; returns immediately
15
+ abt goto https://example.com
16
+ abt find --text "Sign in"
17
+ abt click --ref el_3
18
+ abt input --css "#email" --value "someone@example.com"
19
+ abt ops # every op and its exact parameters
20
+ abt exec '{"op":"scroll","to":"bottom"}' # any op, raw
21
+ abt exec-batch '[{"op":"click","text":"Edit"},{"op":"get_text","css":"h1"}]'
22
+ abt guidelines show toolkit-workflow # this document
23
+ abt guidelines search <domain> # a playbook for the site you are on?
24
+ ```
25
+
26
+ `abt exec` reaches **every** op, including those with no named subcommand — so
27
+ the op tables below are the real vocabulary. `abt --help` lists subcommands;
28
+ `abt <command> --help` lists flags. On PowerShell, pipe JSON rather than
29
+ quoting it inline: `'{"op":"press","key":"Enter"}' | abt exec -`.
30
+
31
+ **Never run `abt serve` from a tool call.** That is the command loop itself: it
32
+ never returns, so whatever launched it hangs until killed. `abt up` is the one
33
+ you want — safe at any time, no-ops when a server already answers, and spawns
34
+ the server outside your job object so your call returns while it keeps running.
35
+ The server usually *is* already running, holding tabs and logins that must not
36
+ be thrown away, so check before starting anything.
37
+
38
+ **Batch what you already know.** `abt exec-batch` runs a list in order and stops
39
+ at the first error. Two round trips become one, and you stop guessing between
40
+ them.
41
+
42
+ **The viewer.** `/viewer` in a browser tab replays every command and response as
43
+ it happens — the best debugging tool here. Open it beside your work.
44
+
45
+ **Underneath is HTTP**, and that surface is supported: `POST /command`,
46
+ `POST /commands`, `GET /status`, `GET /ops`. Every `abt` subcommand except
47
+ `serve` is one of those requests. Use it directly when you are already making
48
+ HTTP calls and want to avoid a process launch per command; otherwise prefer
49
+ `abt`.
50
+
51
+ ## The op groups
52
+
53
+ | Group | Ops |
54
+ |---|---|
55
+ | Navigate | `goto` `back` `forward` `reload` `current_url` |
56
+ | Read | `find` `find_full` `get_text` `get_html` `run_js` `screenshot` |
57
+ | Inspect | `read_console` `read_network` |
58
+ | Interact | `click` `input` `press` `select` `hover` `scroll` `wait_for` |
59
+ | Tabs | `tab_new` `tab_switch` `tab_close` `tab_list` |
60
+ | Control | `diff` `status` `shutdown` |
61
+
62
+ A few sites have sequences that always run together, and those are packaged as
63
+ shortcuts of their own. They are shortcuts over these same ops, never a
64
+ replacement: when one does not fit what you need, drive the page with the ops
65
+ directly. Which sites have them, and what they do, belongs in that site's
66
+ playbook — `abt guidelines search <domain>`.
67
+
68
+ ## Targeting and refs
69
+
70
+ - **`near` qualifies a selector that matches too much.** A table whose every
71
+ row has an `Edit` button is the case: `text` alone matches all of them, and
72
+ an `index` is a guess that breaks the moment a row is added.
73
+
74
+ ```json
75
+ {"op": "click", "text": "Edit", "near": "Medication"}
76
+ ```
77
+
78
+ It picks the match sharing the *smallest* container with that text — the row
79
+ on a table, the section on a card. **Do not reach for `run_js` to stamp an
80
+ attribute on the right row and click that.** An agent doing exactly that
81
+ spent 14 of its 33 `run_js` calls on it, and when one stamp failed it left a
82
+ selector matching nothing and retried the same click ten times.
83
+
84
+ A `near` that matches nothing lists the qualifiers that do exist, so a miss
85
+ tells you what to ask for instead.
86
+
87
+ - Ops that touch an element take exactly **one** of `css`, `xpath`, `text`
88
+ (exact visible text), or `ref`. An `index` picks the Nth match.
89
+ - `find` returns compact element **shells** (tag + attributes, no children) —
90
+ perfect for surveying a page cheaply. `find_full` returns inner content.
91
+ - Every match carries a `ref`. Act on it directly:
92
+ `find` then `click {"ref": "el_0"}` — no re-selecting.
93
+ - Refs die on navigation or when the element leaves the DOM. A dead ref returns
94
+ `stale_ref`, never silently hits a different element.
95
+ - Numbering never restarts, so ref numbers climb through a session. That is
96
+ deliberate: a reused name would let a new page's `el_0` answer to a handle you
97
+ were holding for the old one. Do not read meaning into the numbers.
98
+ - The `actionable` track on a diff also hands out refs — see below. Reaching for
99
+ `find` right after a click is usually a wasted round trip.
100
+
101
+ ## Diffs: your primary feedback loop
102
+
103
+ > **Trust the diff. Do not re-read the page to check what a command did.**
104
+ >
105
+ > This is the most common and most expensive mistake made with this toolkit. A
106
+ > click succeeds, returns a `dom_diff` saying exactly what changed — and the
107
+ > agent fires `get_text {"css": "body"}` to "see what happened". You already
108
+ > have what happened. That follow-up costs a round trip and can cost tens of
109
+ > thousands of tokens, to learn what was in the response you just received.
110
+ >
111
+ > The diff is snapshotted from the live DOM either side of the command. It is
112
+ > not a hint to be confirmed. **Read it and act.** (It was once worth
113
+ > distrusting — navigations snapshotted before an SPA had rendered. Navigation
114
+ > now waits for network idle and a still DOM. A `Loading…` in a diff today is a
115
+ > bug to report, not a reason to re-read.)
116
+
117
+ Interactive ops (`click input press select hover scroll wait_for run_js`) and
118
+ navigation ops (`goto back forward reload`) snapshot before/after and return
119
+ `dom_diff`. Three tracks:
120
+
121
+ **`text` — always on, no budget.** Strings that appeared on screen, one entry
122
+ per element, plus form-control values. Read this first; on most pages it is the
123
+ whole answer.
124
+
125
+ ```json
126
+ "text": {"added": ["Widgets", "Gadgets"], "removed_count": 1, "truncated": false}
127
+ ```
128
+
129
+ Only *rendered* text counts, so a hover that reveals a menu reads as its items
130
+ appearing, and an attribute-only change (`aria-expanded`, a class flip) shows as
131
+ an empty diff. That is the trade: text stays clean because it drops that churn.
132
+ Removals are counted, not listed — on a page that rewrites itself they are the
133
+ whole old document. Add `"include_removed": true` when the count says it matters.
134
+
135
+ **After a navigation, `added` is the whole destination page.** `goto` `back`
136
+ `forward` `reload`, and any click that redirected, return the text of the page
137
+ they landed on. So you never need a `find` or `get_text` merely to see what is
138
+ on a page you just opened. The element track is skipped there.
139
+
140
+ **`actionable` — on by default.** The controls among those additions, each with
141
+ a **ref you can act on immediately**. This is the shortest path in the toolkit:
142
+ click, read what appeared, click what appeared — no `find` in between.
143
+
144
+ ```json
145
+ {"op": "click", "css": "#insert-menu"}
146
+ → "text": {"added": ["Chart", "Pivot table", "Macro"]},
147
+ "actionable": {"added": [
148
+ {"ref": "el_7", "role": "menuitem", "name": "Chart"},
149
+ {"ref": "el_9", "role": "button", "name": "Macro", "disabled": true}]}
150
+
151
+ {"op": "click", "ref": "el_7"}
152
+ ```
153
+
154
+ Every `name` is also a string in `text.added`, so the two line up: read the text
155
+ to decide, use the ref to act. `role` disambiguates, `disabled` warns you off.
156
+
157
+ When several controls share a name each carries `near` — the nearest text that
158
+ is not its own label, so a row of identical `Edit` buttons tells you which row
159
+ it belongs to: `{"ref": "el_9", "name": "Edit", "near": "Medication"}`. **This
160
+ exists so you do not reach for `run_js` to match a button to its row.** That
161
+ DOM-walking is expensive and wrong more often than it looks — it has opened the
162
+ wrong row's dialog and returned `ok: true`.
163
+
164
+ It skips navigations (on a new page every control is "new") and drops controls
165
+ with no accessible name, so you never get a ref you cannot tie to something you
166
+ read. **Exception: file inputs are reported even when invisible**, because sites
167
+ hide the real `<input type=file>` behind a custom uploader. Look for
168
+ `role: "file"` and write the path straight to it — `input` handles the hiding:
169
+
170
+ ```json
171
+ {"op": "input", "ref": "el_4", "value": "C:/shots/page.png"}
172
+ ```
173
+
174
+ Unlike the text track this one is not free — roughly a quarter again on a diffed
175
+ op. Pass `"actionable": false` on batch steps whose new controls you will never
176
+ click.
177
+
178
+ **`elements` — pass `element_diff: true`.** A line-per-element unified diff with
179
+ tags, ids, classes and attributes. For a change with no visible text, or when
180
+ you need a selector for something the text track reported. Budget it with
181
+ `diff_max_tokens`; passing a budget implies `element_diff: true`.
182
+
183
+ - Navigation and tab ops reset the baseline automatically.
184
+ - Suppress noise on one command with `"diff": false`; disable entirely with
185
+ `--no-diff`.
186
+ - Set a manual baseline with `{"op": "diff", "reset": true}`, then re-check with
187
+ `{"op": "diff"}` to catch async SPA updates. The manual `diff` is explicit, so
188
+ it returns everything: both tracks, removals listed.
189
+
190
+ ### When the diff looks empty
191
+
192
+ The one case that tempts you back into re-reading the page. Don't — there are
193
+ three causes and each has a cheaper answer:
194
+
195
+ 1. **No visible text changed** — a class flip, `aria-expanded`, a `data-`
196
+ attribute. Ask for the element track:
197
+ `{"op": "click", "css": "…", "element_diff": true}`.
198
+ 2. **It has not landed yet** — an SPA updating after the command returned. Take
199
+ a second look with `{"op": "diff"}`, which compares against the state the
200
+ last command left. That is what it is for.
201
+ 3. **Nothing happened** — a real outcome, and worth knowing. `click` hit-tests
202
+ first and raises `not_interactable` naming what covered it, so `ok: true`
203
+ with an empty diff genuinely means the click landed and the page did not
204
+ react.
205
+
206
+ `get_text {"css": "body"}` answers none of these better, and costs more than all
207
+ three together. There used to be a fourth cause — a change inside an iframe,
208
+ invisible to every selector. All three tracks now read into frames and refs from
209
+ inside them act normally; you never switch frames yourself.
210
+
211
+ **Verify effects with the diff, not with screenshots or downloads.** Downloads
212
+ and exports lag; the diff is live.
213
+
214
+ **The loop, in one line:** act, read `dom_diff.text.added` to see what appeared,
215
+ act on `dom_diff.actionable.added[].ref` to use it. A `find` between those steps
216
+ is usually a round trip you did not need.
217
+
218
+ ## Searching for something that should be there
219
+
220
+ The rule above is about *verifying what a command did*. This one is about
221
+ *looking for something*, and its failure mode is the opposite: a search comes
222
+ back empty, the agent reads that as "my selector was wrong", and starts
223
+ guessing. On a live LinkedIn profile an agent searched for `input[type=file]`,
224
+ got `count: 0`, and spent **fifteen commands and six minutes** widening
225
+ `run_js` scans. There was no file input — the page creates it when you engage
226
+ the drop zone.
227
+
228
+ **A search that finds nothing is an answer.** Climb this ladder once, then
229
+ believe it:
230
+
231
+ | | step | what it covers |
232
+ |---|---|---|
233
+ | 1 | **Re-read the response you have** | A navigation already returned the whole page in `text.added` |
234
+ | 2 | **`find`** — `css`, then `text` | The document and every frame |
235
+ | 3 | **`get_text`** | Rendered text, *including open shadow roots* |
236
+ | 4 | **`find` with `"shadow": true`** | Turns shadow content into a ref you can act on |
237
+ | 5 | **Stop** | It is not there |
238
+
239
+ Step 3 follows the composed tree, so a component's internals are in it with no
240
+ flag — discovery is free. Step 4 exists because reading a label is not the same
241
+ as being able to click it.
242
+
243
+ **You are told when step 4 is worth taking.** Shadow roots are counted on every
244
+ snapshot but never walked:
245
+
246
+ ```json
247
+ {"op": "find", "css": "#resume"}
248
+ → {"count": 0, "shadow_hosts": 2,
249
+ "note": "nothing matched, but this page has 2 shadow root(s) … retry with \"shadow\": true"}
250
+ ```
251
+
252
+ No hosts, no note — so on most pages this costs and says nothing.
253
+
254
+ **Do not `run_js` a `querySelectorAll` to "look harder".** Steps 2–4 already
255
+ searched the document, its frames and its open shadow roots. JavaScript finds
256
+ the same nothing, a round trip and a few thousand tokens later.
257
+
258
+ **The usual real cause:** the page has not created the control yet. Hidden file
259
+ inputs are the classic — mounted when you click the visible upload button or
260
+ drop zone. Act on what the page is showing you, then look again.
261
+
262
+ **The one honest limit:** a `mode: "closed"` shadow root returns `null` from
263
+ `.shadowRoot`, so no JavaScript can read it or prove it exists, and neither can
264
+ this. "Not there" means *nothing reachable has it*. Closed roots are rare
265
+ outside browser internals like `<video>` controls.
266
+
267
+ ## Types of work
268
+
269
+ - **Reading a page:** `find` shells → `find_full` the interesting ones →
270
+ `get_text`/`get_html` for specifics. `run_js` for anything the ops don't cover.
271
+ - **Typing:** `input` clears then types into a **visible** target (use
272
+ `"clear": false` to append). The one exception is `<input type=file>`, which
273
+ sites hide on purpose — `input` writes a path to it anyway. `press` sends keystrokes to whatever has focus —
274
+ use it when there is no visible input element (see google-docs.md).
275
+ - **Press chords:** `press` accepts `ctrl+v`, `ctrl+alt+1`, `shift+enter`, …
276
+ modifiers are `ctrl/control`, `shift`, `alt/option`, `meta/command/cmd/windows`.
277
+ - **Waiting:** `wait_for` with `state` = `present|visible|clickable|absent` and
278
+ a `timeout`. Use it before acting on slow-loading content.
279
+
280
+ ## Clicking what the DOM cannot address
281
+
282
+ `click` takes `at: [x, y]` for a canvas, a closed shadow root, an image map —
283
+ anything a selector cannot reach. It is a real synthesized pointer sequence, so
284
+ `event.isTrusted` is true and a page cannot tell it from a person.
285
+
286
+ ```json
287
+ {"op": "click", "css": "canvas#pad", "at": [120, 80]} // inside that element
288
+ {"op": "click", "at": [640, 400]} // in the viewport
289
+ ```
290
+
291
+ **Prefer the element-relative form.** With a target, `at` is an offset from the
292
+ element's top-left and the element is scrolled into view first, so the numbers
293
+ mean the same thing however the page is scrolled. A bare viewport point is
294
+ whole-pixel while layout is fractional, so it is only good to about a pixel.
295
+
296
+ The response reports `hit` — what `elementFromPoint` found there. A coordinate
297
+ click is blind by nature; check `hit` rather than assume. A point off screen
298
+ raises `not_interactable` instead of clicking nothing.
299
+
300
+ Reach for this only when targeting genuinely cannot work. A `css` selector
301
+ survives a redesign; a coordinate does not.
302
+
303
+ ## When the page won't say what went wrong
304
+
305
+ The DOM tells you what a page *is*. `read_console` and `read_network` tell you
306
+ what it *did* — and a failed request usually leaves nothing in the DOM at all.
307
+
308
+ - `read_console` — everything the page logged, plus uncaught errors and
309
+ unhandled rejections. Captured from **document start**, so a reload gives you
310
+ what a page said while loading, which is where the useful errors are. Filter
311
+ with `pattern` (regex on the text) and `levels`.
312
+ - `read_network` — every request with its status, duration, and size.
313
+ `failures_only: true` is the one you want: it keeps 4xx/5xx and anything the
314
+ browser refused to disclose. `pattern` filters on URL.
315
+
316
+ A real case: a site showed "Failed to load PDF" and nothing else. The DOM had no
317
+ more to give. `read_network` with `failures_only` showed a 404 on a storage key
318
+ — the whole diagnosis in one call.
319
+
320
+ Statuses and URLs, not bodies. A cross-origin response without
321
+ `Timing-Allow-Origin` reports `status: null` and `opaque: true`; the browser
322
+ genuinely will not say, so neither does this.
323
+
324
+ ## Typing into dates
325
+
326
+ `input` on `<input type="date|time|month|week|datetime-local">` sets the value
327
+ directly rather than typing, and fires the events a framework listens for.
328
+ Typing into these is a trap: the browser feeds keystrokes to *locale* segment
329
+ boxes, so `2026-08-03` on an en-US date input lands as `60803-02-20` and the
330
+ form submits silently wrong.
331
+
332
+ Use the field's own format — `YYYY-MM-DD`, `HH:MM`, `YYYY-MM`, `YYYY-Www`,
333
+ `YYYY-MM-DDTHH:MM` — and anything the field rejects raises `not_interactable`
334
+ instead of quietly emptying itself.
335
+
336
+ ## Errors
337
+
338
+ A closed set of `error.type` values to branch on: `invalid_op`,
339
+ `element_not_found`, `stale_ref`, `not_interactable`, `not_a_select`, `timeout`,
340
+ `navigation_failed`, `js_error`, `last_tab`, `tab_not_found`, `browser_dead`,
341
+ `bad_browser`.
342
+
343
+ Failed ops never quietly continue: batches stop unless `continue_on_error`.
344
+
345
+ ### A tab that closes itself takes the session with it
346
+
347
+ OAuth popups, payment frames and "you may close this window" tabs close
348
+ themselves when they finish. **If such a tab was the active one, the session
349
+ dies** — every later call returns `browser_dead` with `no such window: target
350
+ window already closed`, and neither `tab_switch` nor `tab_list` can recover it,
351
+ even though the other windows are still on screen.
352
+
353
+ Avoid it by not standing there: `tab_switch` back to the tab you came from
354
+ *before* clicking the control that completes the flow, when the flow allows it.
355
+
356
+ Once it has happened, send `{"op": "browser_restart"}`. Like `status` and
357
+ `shutdown` it skips the health check, so it works precisely when everything else
358
+ returns `browser_dead`. The server stays up, the session log continues, and you
359
+ get a fresh browser on the same profile — so you are still logged in, but
360
+ **every tab and every ref is gone** and you must navigate back.
361
+
362
+ You no longer have to check by hand that nothing else holds the profile: `stop`
363
+ waits for the old browser to release it, and `start` probes the new session and
364
+ fails loudly rather than handing you one that dies on first use.
365
+
366
+ Still worth fixing properly: falling back to a surviving window handle on
367
+ `NoSuchWindowException` would keep the tabs, which `browser_restart` cannot.
368
+
369
+ ## Site traps to remember
370
+
371
+ - **Canvas-rendered editors** (Google Docs, Figma, Excalidraw) put the text on
372
+ a `<canvas>`, not in the DOM. `find`/`get_text`/diff cannot see the typed
373
+ text — the diff still catches structural and save-indicator changes.
374
+ - **Offscreen focus targets** (`input` on a hidden editor iframe) fail with
375
+ "not visible within Ns" even though the element exists and has focus.
376
+ Drive them with `press` instead.
377
+ - **Exports/downloads can be stale** — a doc's export endpoint may serve old
378
+ content while the live editor is ahead. Cache-bust the URL and wait for save.
379
+
380
+ ## Hygiene
381
+
382
+ - Keep bulk operations on `"diff": false` and do one verification pass at the
383
+ end — huge diffs are noise. **Except across a form fill:** the text track
384
+ captures input values, so a diff after typing is the cheapest way to catch a
385
+ field that took something other than what you sent. A whole form filled on
386
+ `"diff": false` once hid a mangled date until the server rejected it twice.
387
+ - Tabs you create for downloads/export are often closed by the human operator;
388
+ never assume a tab id survives. Create a fresh tab for each export check and
389
+ read the newest file in the Downloads folder.
390
+ - Log everything. If a run goes wrong, `/viewer` shows exactly what happened.
@@ -2,7 +2,7 @@
2
2
  pkgname=aibrowsertoolkit-bin
3
3
  pkgver=@VERSION@
4
4
  pkgrel=1
5
- pkgdesc="Browser API for AI agents, over HTTP"
5
+ pkgdesc="Agentic browser automation CLI for AI agents"
6
6
  arch=('x86_64' 'aarch64')
7
7
  url="https://github.com/skssmd/Ai-Browser-Toolkit"
8
8
  license=('Apache-2.0')
@@ -1,5 +1,5 @@
1
1
  class Aibrowsertoolkit < Formula
2
- desc "Browser API for AI agents, over HTTP"
2
+ desc "Agentic browser automation CLI for AI agents"
3
3
  homepage "https://github.com/skssmd/Ai-Browser-Toolkit"
4
4
  version "@VERSION@"
5
5
  license "Apache-2.0"
@@ -5,7 +5,7 @@ version: ${PKG_VERSION}
5
5
  section: utils
6
6
  priority: optional
7
7
  maintainer: skssmd <skssmd78475@gmail.com>
8
- description: Browser API for AI agents, over HTTP
8
+ description: Agentic browser automation CLI for AI agents
9
9
  vendor: skssmd
10
10
  homepage: https://github.com/skssmd/Ai-Browser-Toolkit
11
11
  license: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "version": "@VERSION@",
3
- "description": "Browser API for AI agents, over HTTP",
3
+ "description": "Agentic browser automation CLI for AI agents",
4
4
  "homepage": "https://github.com/skssmd/Ai-Browser-Toolkit",
5
5
  "license": "Apache-2.0",
6
6
  "architecture": {
@@ -4,11 +4,11 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ai-browser-toolkit"
7
- version = "0.2.0"
8
- description = "Browser API for AI agents, over HTTP"
7
+ version = "0.2.2"
8
+ description = "Agentic browser automation CLI for AI agents"
9
9
  # PyPI renders this as the whole project page. Without it the page is a blank
10
10
  # stub carrying only the one-line description above -- which is what every
11
- # `pip install aibrowsertoolkit` sends people to look at.
11
+ # `pip install ai-browser-toolkit` sends people to look at.
12
12
  readme = "README.md"
13
13
  license = "Apache-2.0"
14
14
  requires-python = ">=3.11"
@@ -571,13 +571,33 @@ class BrowserSession:
571
571
  except WebDriverException:
572
572
  return None
573
573
 
574
- def goto(self, url: str) -> None:
574
+ def goto(self, url: str) -> bool:
575
+ """Navigate. Returns False when it landed but overran the budget.
576
+
577
+ A redirect chain can outrun the navigation timeout while the
578
+ navigation itself succeeds -- `https://sheets.new` is the canonical
579
+ case: it 302s to a freshly created document, the wait expires, and the
580
+ page is nevertheless there. Reporting that as `navigation_failed` sends
581
+ a caller off to retry something that already worked.
582
+
583
+ So a timeout is only a failure if the browser did not actually move.
584
+ """
585
+ before = None
586
+ try:
587
+ before = self.driver.current_url
588
+ except WebDriverException:
589
+ pass
590
+
591
+ overran = False
575
592
  try:
576
593
  self.driver.get(url)
577
594
  except WebDriverException as exc:
578
- raise OpError(
579
- "navigation_failed", f"could not load {url!r}: {exc.msg or exc}"
580
- ) from exc
595
+ if not self._moved_from(before):
596
+ raise OpError(
597
+ "navigation_failed", f"could not load {url!r}: {exc.msg or exc}"
598
+ ) from exc
599
+ overran = True
600
+
581
601
  self.refs.invalidate(self.active_tab)
582
602
  code = self.error_page_code()
583
603
  if code:
@@ -585,6 +605,23 @@ class BrowserSession:
585
605
  "navigation_failed", f"could not load {url!r}: chrome reported {code}"
586
606
  )
587
607
  self.settle()
608
+ return not overran
609
+
610
+ def _moved_from(self, before: str | None) -> bool:
611
+ """Did the browser actually end up somewhere new and usable?
612
+
613
+ `about:blank` and a chrome error page both count as not having moved:
614
+ one means nothing happened, the other means something did and failed.
615
+ """
616
+ try:
617
+ after = self.driver.current_url
618
+ except WebDriverException:
619
+ return False
620
+ if not after or after.startswith("about:"):
621
+ return False
622
+ if after == before:
623
+ return False
624
+ return not self.error_page_code()
588
625
 
589
626
  def settle(self, timeout: float | None = None) -> bool:
590
627
  """Wait for the DOM to stop changing. Returns whether it did.