lean-browser-use 0.0.0-stage → 0.1.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 (76) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/CREDITS.md +18 -0
  3. package/LICENSE +21 -0
  4. package/README.md +159 -2
  5. package/agents/aun-browser-runner.md +48 -0
  6. package/bin/lean-browser-use.mjs +3 -0
  7. package/briefs/browser-read.md +63 -0
  8. package/briefs/helper-read.md +63 -0
  9. package/docs/PROVENANCE.md +32 -0
  10. package/docs/benchmark.md +91 -0
  11. package/docs/guards.md +54 -0
  12. package/docs/how-it-works.md +27 -0
  13. package/docs/install.md +78 -0
  14. package/docs/measure.md +53 -0
  15. package/docs/reader.md +39 -0
  16. package/docs/search.md +16 -0
  17. package/hooks/batch-nudge.mjs +14 -0
  18. package/hooks/batch-nudge.sh +8 -0
  19. package/hooks/common.mjs +71 -0
  20. package/hooks/hooks.settings.json +58 -0
  21. package/hooks/pick-guard.mjs +27 -0
  22. package/hooks/pick-guard.sh +8 -0
  23. package/hooks/retry-guard.mjs +69 -0
  24. package/hooks/retry-guard.sh +8 -0
  25. package/llms.txt +40 -0
  26. package/package.json +61 -4
  27. package/proof/benchmark.json +166 -0
  28. package/proof/results.json +241 -0
  29. package/reader/README.md +48 -0
  30. package/reader/crawl_helper.py +256 -0
  31. package/reader/reader +14 -0
  32. package/reader/reader_base.py +106 -0
  33. package/reader/reader_compile.py +106 -0
  34. package/reader/reader_crawl.py +67 -0
  35. package/reader/reader_extract.py +261 -0
  36. package/reader/reader_fetch.py +148 -0
  37. package/reader/reader_runtime.py +199 -0
  38. package/reader/reader_test_harness.py +173 -0
  39. package/reader/reader_test_support.py +214 -0
  40. package/reader/specs/claude-pricing.json +223 -0
  41. package/reader/specs/npm-package.json +67 -0
  42. package/reader/test_reader.py +345 -0
  43. package/reader/test_reader_1.py +309 -0
  44. package/reader/test_reader_2.py +348 -0
  45. package/reader/test_reader_3.py +254 -0
  46. package/reader/test_reader_contract.py +57 -0
  47. package/reader/testdata/claude-pricing.html +4 -0
  48. package/reader/testdata/compile_stub.py +24 -0
  49. package/reader/testdata/npm-react.html +4 -0
  50. package/reader/testdata/npm-schema.json +14 -0
  51. package/reader/testdata/npm-zod.html +4 -0
  52. package/reader/testdata/pricing-schema.json +24 -0
  53. package/searxng/README.md +29 -0
  54. package/searxng/compose.yml +21 -0
  55. package/searxng/core-config/settings.yml +26 -0
  56. package/searxng/deploy.sh +36 -0
  57. package/searxng/searx +73 -0
  58. package/searxng/test_searx.py +107 -0
  59. package/searxng/test_searx_offline.py +67 -0
  60. package/skills/lean-browser-use/SKILL.md +35 -0
  61. package/skills/lean-browser-use/references/cleanup.md +17 -0
  62. package/skills/lean-browser-use/references/cost-model.md +11 -0
  63. package/skills/lean-browser-use/references/draft-offline.md +9 -0
  64. package/skills/lean-browser-use/references/failure-modes.md +16 -0
  65. package/skills/lean-browser-use/references/logged-in-run.md +23 -0
  66. package/skills/lean-browser-use/references/packet-principle.md +15 -0
  67. package/skills/lean-browser-use/references/route-by-job.md +18 -0
  68. package/skills/lean-browser-use/references/runner-rules.md +27 -0
  69. package/skills/lean-browser-use/references/verify.md +9 -0
  70. package/src/cli.mjs +42 -0
  71. package/src/doctor.mjs +60 -0
  72. package/src/install.mjs +116 -0
  73. package/src/measure.mjs +64 -0
  74. package/src/paths.mjs +51 -0
  75. package/src/uninstall.mjs +68 -0
  76. package/src/util.mjs +35 -0
@@ -0,0 +1,78 @@
1
+ # Install and manage
2
+
3
+ Use Node 20 or newer. From a terminal:
4
+
5
+ ```sh
6
+ npx lean-browser-use
7
+ ```
8
+
9
+ The installer prints its plan, then asks `Proceed? [Y/n]` on a terminal. Non-interactive runs proceed automatically; `--yes` also skips the prompt. Undo the install with `npx lean-browser-use --uninstall`.
10
+
11
+ ## Choose the target
12
+
13
+ The config directory is selected by `--claude-dir DIR`, then `$CLAUDE_CONFIG_DIR`, then `~/.claude`. `--project` targets `./.claude` in the current project. Use one explicit target choice per invocation.
14
+
15
+ ```sh
16
+ npx lean-browser-use install --project
17
+ npx lean-browser-use install --claude-dir ./sandbox-config --dry
18
+ npx lean-browser-use install --with-reader --yes
19
+ ```
20
+
21
+ | Install flag | Effect |
22
+ | --- | --- |
23
+ | `--claude-dir DIR` | Use the named Claude config directory. |
24
+ | `--project` | Install into the current project's `.claude`. |
25
+ | `--no-hooks` | Copy the hooks and print registration to paste by hand; skip the settings merge. |
26
+ | `--with-reader` | Also copy the optional reader and SearXNG files. |
27
+ | `--dry` | Print the real plan while leaving files unchanged. |
28
+ | `--yes` | Proceed after the plan without an interactive confirmation. |
29
+ | `--help` | Print command usage. |
30
+ | `--version` | Print the package version. |
31
+
32
+ ## Files and settings
33
+
34
+ Here `<cfg>` means the selected config directory.
35
+
36
+ | Destination | Contents |
37
+ | --- | --- |
38
+ | `<cfg>/skills/lean-browser-use/` | Method entrypoint and focused references. |
39
+ | `<cfg>/agents/aun-browser-runner.md` | Browser subagent definition. |
40
+ | `<cfg>/lean-browser-use/hooks/` | Guard scripts and registration source. |
41
+ | `<cfg>/lean-browser-use/briefs/` | Browser and cheap-helper packet templates. |
42
+ | `<cfg>/lean-browser-use/reader/` | Optional compile-once reader and specs. |
43
+ | `<cfg>/lean-browser-use/searxng/` | Optional search script and container configuration. |
44
+ | `<cfg>/lean-browser-use/manifest.json` | Install-time hashes, created directories, and settings changes. |
45
+ | `<cfg>/settings.json` | Merged hook registrations, preserving other settings and hooks. |
46
+ | `<cfg>/settings.json.lean-browser-use.bak` | Backup of existing settings, written only if the backup is absent. |
47
+
48
+ Repeated installs avoid duplicate registrations. The installer substitutes real hook paths into the registration template. It never installs third-party tools, runs a vendor login, contacts the network, or prints secrets. Optional tool installation belongs to the user; see [reader](reader.md) and [search](search.md).
49
+
50
+ ## Activate and inspect
51
+
52
+ Start a fresh Claude Code session so it can discover the installed skill and agent. Ask it to use `lean-browser-use` for a browser task. Pick your Chrome profile by name when logged-in work needs it. A Chrome connection and tool permissions come from your own Claude Code setup.
53
+
54
+ ```sh
55
+ npx lean-browser-use doctor
56
+ npx lean-browser-use doctor --claude-dir ./sandbox-config --json
57
+ ```
58
+
59
+ Doctor checks installed file hashes, registered hook paths, executable shims, and synthetic hook responses. Edited files are reported. Its capability table explains what Python, `crwl`, Docker, Claude Code, and local transcripts enable; missing optional tools are capabilities to add, not installation errors. JSON output has `ok` and `checks`, each with `id`, `status`, and `detail`. Hook behavior on your Claude Code version is confirmed by `lean-browser-use doctor`.
60
+
61
+ ## Measure and remove
62
+
63
+ ```sh
64
+ npx lean-browser-use measure --dir ./transcripts --since 2026-10-25 --json
65
+ npx lean-browser-use uninstall --claude-dir ./sandbox-config --dry
66
+ npx lean-browser-use uninstall --claude-dir ./sandbox-config
67
+ ```
68
+
69
+ `measure` defaults to the selected config directory's `projects` folder; `--dir` overrides it, `--since YYYY-MM-DD` filters timestamps, and `--json` returns structured results. See [the meter](measure.md).
70
+
71
+ Uninstall compares each file with its install-time hash. It removes matching files, preserves and lists edited files, removes only its hook entries from settings, and removes only empty directories this install created. It refuses traversal, absolute manifest paths, and symlink entries. The manifest is removed last. Follow printed manual steps for registration pasted by hand. For a project install, point uninstall and doctor at `--claude-dir ./.claude`.
72
+
73
+ | Exit | Meaning |
74
+ | --- | --- |
75
+ | 0 | Success. |
76
+ | 1 | A check failed. |
77
+ | 2 | Usage error or refused target; a missing uninstall manifest names its path. |
78
+ | 3 | Uninstall preserved edited files. |
@@ -0,0 +1,53 @@
1
+ # Measure browser context
2
+
3
+ Run the meter on your own Claude Code transcripts:
4
+
5
+ ```sh
6
+ lean-browser-use measure
7
+ lean-browser-use measure --dir ./transcripts --since 2026-10-25 --json
8
+ ```
9
+
10
+ The default directory is the selected Claude config directory's `projects` folder. The meter walks JSONL files recursively, including subagent transcripts. `--dir` selects another tree, `--since YYYY-MM-DD` filters line timestamps, and `--json` returns the same measurements in structured form.
11
+
12
+ ## What counts
13
+
14
+ A browser step is a unique assistant `message.id` with a browser `tool_use` block. Recognized prefixes are `mcp__claude-in-chrome__`, `mcp__Claude_Browser__`, `mcp__Control_Chrome__`, `mcp__playwright__`, and `mcp__chrome-devtools__`. A batch counts once, even when it contains many browser actions.
15
+
16
+ For each step the meter adds `input_tokens`, `cache_creation_input_tokens`, and `cache_read_input_tokens`. An assistant message can appear on several transcript lines. The meter keeps the largest total for that id as final usage, preserving the browser-step classification across its lines.
17
+
18
+ A line belongs to the runner group when `isSidechain` is true or its file path contains `/subagents/`. Other lines belong to main. Unreadable lines are skipped and counted in `skipped_lines`.
19
+
20
+ ## Read the result
21
+
22
+ | Field | Meaning |
23
+ | --- | --- |
24
+ | Group | Main session or runner. |
25
+ | Steps | Distinct assistant messages using browser tools. |
26
+ | Median | Typical input-context tokens per step. |
27
+ | p90 | Input-context tokens near the upper end of the observed steps. |
28
+ | Total | Sum of input-context tokens across browser steps. |
29
+ | `reread_share` | Cache-read plus cache-creation tokens divided by total input-context tokens. |
30
+ | `ratio` | Main median divided by runner median, reported when each has at least 3 steps. |
31
+ | `skipped_lines` | Transcript lines the meter could not read. |
32
+
33
+ The default output is a short table for main and runner, followed by summary information. With an empty transcript tree it prints `no browser steps found` and exits 0. Generate a real example from the synthetic fixtures with:
34
+
35
+ ```sh
36
+ node bin/lean-browser-use.mjs measure --dir test/fixtures/transcripts
37
+ ```
38
+
39
+ Output from the synthetic fixtures, not a token-saving claim:
40
+
41
+ ```text
42
+ group steps median p90 total reread_share
43
+ main 3 400 600 1200 0.5000
44
+ runner 3 200 300 600 0.5000
45
+ ratio 2
46
+ skipped_lines 1
47
+ ```
48
+
49
+ This is a context-volume comparison, not a currency bill. Cached tokens can have different pricing. Different jobs, model behavior, and transcript coverage also affect the result. Compare similar work and keep the counts beside the ratio. The meter prints counts and token values, never message text or tool inputs, and makes no network calls.
50
+
51
+ ## Package measurements
52
+
53
+ [proof/results.json](../proof/results.json) measures the package: tarball size, file count, dependencies, passing tests, guard timings, install time, and covered guard rules. Reproduce it with `npm run proof`. These generated rows give their command, measurement date, and expiry. An expired row warns during `npm run check`; expiry does not block checks or a release. The twenty paired benchmark rows are copied or derived from the saved run in [proof/benchmark.json](../proof/benchmark.json), carry no expiry, and are never re-measured by the proof script. Read [the benchmark](benchmark.md) for conditions, exact counts, and reproduction steps. They give a ballpark for that run; use the meter on your own transcripts to compare your work.
package/docs/reader.md ADDED
@@ -0,0 +1,39 @@
1
+ # Compile once, run as code
2
+
3
+ The page reader extracts repeatable fields from public pages. A model writes the extraction spec during compilation. Successful later runs fetch and extract with code, without a model call. Validation can trigger one self-heal when a page changes.
4
+
5
+ Copy the companion with `npx lean-browser-use --with-reader`. The examples below run from a repository checkout; installed copies live under the chosen config directory's `lean-browser-use/reader/`.
6
+
7
+ ## Prepare the reader
8
+
9
+ Python 3 runs the reader. Crawl4AI supplies CSS extraction and the browser fetch fallback. The user installs it separately with `pip install crawl4ai` in their chosen Python environment. See the [reader guide](../reader/README.md) for interpreter discovery and offline tests.
10
+
11
+ Choose a command that reads a prompt on stdin and prints schema JSON on stdout. For example:
12
+
13
+ ```sh
14
+ export READER_COMPILE_CMD='claude -p'
15
+ reader/reader compile packages --url 'https://www.npmjs.com/package/{name}' \
16
+ --param name=react --want 'Name, version, license, repository URL'
17
+ reader/reader run packages --param name=zod
18
+ reader/reader ls
19
+ ```
20
+
21
+ The compile command is parsed with `shlex` and run without a shell. It can be an agent CLI, an API client script, or a local model command. The reader supplies the prompt on stdin. An unset `READER_COMPILE_CMD` makes compilation or self-heal exit 2 with setup guidance. Running an already compiled valid spec needs neither the variable nor a model.
22
+
23
+ ## Verify and reuse
24
+
25
+ Give `compile` an observed `--example` row when possible. Verify the resulting values independently before reusing the spec. The bundled [pricing spec](../reader/specs/claude-pricing.json) and [package spec](../reader/specs/npm-package.json) include their provenance; saved fixtures demonstrate extraction, while current public values require a fresh read.
26
+
27
+ `run` returns `rows` and fetch metadata. `ls` shows available specs and their state. Use `--no-heal` to keep a run to code-only extraction even when validation fails. A successful heal is stored in runtime state, preserving shipped specs. `READER_SPECS_DIR` selects specs and `READER_STATE_DIR` selects state.
28
+
29
+ Only public HTTP and HTTPS pages are eligible. Private targets and authenticated browsing belong outside this reader. Use the user-selected browser route for logged-in work.
30
+
31
+ | Exit | Meaning |
32
+ | --- | --- |
33
+ | 0 | Success. |
34
+ | 2 | Usage error, refused target, or missing compile command when needed. |
35
+ | 10 | Fetch failed. |
36
+ | 11 | Compile failed. |
37
+ | 12 | Extraction failed after self-heal or with `--no-heal`. |
38
+
39
+ Preserve the error and fall through to an eligible route from the [route table](../skills/lean-browser-use/references/route-by-job.md). Avoid repeating an unchanged failed fetch.
package/docs/search.md ADDED
@@ -0,0 +1,16 @@
1
+ # Find links, then read pages
2
+
3
+ Use search when the URLs are unknown. The optional `searx` script returns links from a SearXNG instance so a helper or runner can read the selected pages in a packet. SearXNG is a self-hosted search service; hosting and upstream availability depend on your environment.
4
+
5
+ Install the companion files with `npx lean-browser-use --with-reader`. Docker is a separate user-provided capability. From a repository checkout:
6
+
7
+ ```sh
8
+ bash searxng/deploy.sh
9
+ searxng/searx 'browser automation documentation'
10
+ ```
11
+
12
+ The default endpoint is `http://127.0.0.1:8888`. `SEARXNG_URL` selects another instance. The compose configuration binds to loopback; expose it to a private network only as a deliberate deployment choice. The deployment script keeps its generated secret in `${XDG_CONFIG_HOME:-$HOME/.config}/lean-browser-use/searxng-secret.env` with restricted permissions. Keep that file out of briefs and reports.
13
+
14
+ Search finds candidate URLs. A search snippet is a discovery aid; read the selected page and save the requested fields as evidence. Batch independent public reads within a helper session or a subagent-runner session. See the [helper packet](../briefs/helper-read.md).
15
+
16
+ This repository ships configuration and a container reference. SearXNG itself is AGPL-3.0 and is not bundled here. See [the companion guide](../searxng/README.md) and [credits](../CREDITS.md).
@@ -0,0 +1,14 @@
1
+ import { browser, native, shortName, probe, stateFile, readState, writeState, input, guard } from './common.mjs';
2
+
3
+ guard(() => {
4
+ const p = input();
5
+ if (p.hook_event_name !== 'PostToolUse' || (!browser(p.tool_name) && !native(p.tool_name))) return;
6
+ const id = p.agent_id || p.session_id;
7
+ if (!id) return;
8
+ const file = stateFile('batch', id);
9
+ const state = readState(file);
10
+ const reset = probe(p.tool_name) || shortName(p.tool_name) === 'browser_batch';
11
+ state.streak = reset ? 0 : (state.streak || 0) + 1;
12
+ writeState(file, state);
13
+ if (state.streak === 3) process.stderr.write('Chain these actions into one browser_batch; return the needed fields together.\n');
14
+ });
@@ -0,0 +1,8 @@
1
+ #!/bin/sh
2
+ d=$(cd "$(dirname "$0")" && pwd) || exit 0
3
+ command -v node >/dev/null 2>&1 || exit 0
4
+ [ -f "$d/batch-nudge.mjs" ] || exit 0
5
+ node "$d/batch-nudge.mjs" "$@"
6
+ status=$?
7
+ [ "$status" -eq 2 ] && exit 2
8
+ exit 0
@@ -0,0 +1,71 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { createHash, randomBytes } from 'node:crypto';
5
+
6
+ export const hash = data => createHash('sha256').update(String(data)).digest('hex');
7
+ export const browser = name => /^mcp__(claude-in-chrome|Claude_Browser|Control_Chrome)__/.test(name || '');
8
+ export const native = name => /^mcp__(computer-use|cua-driver)__/.test(name || '');
9
+ export const shortName = name => String(name || '').split('__').at(-1);
10
+ const probes = new Set(['list_connected_browsers', 'tabs_context_mcp', 'tabs_context', 'read_console_messages', 'read_network_requests', 'shortcuts_list', 'preview_list', 'preview_logs', 'preview_stop', 'list_tabs', 'get_current_tab', 'list_apps', 'list_windows']);
11
+ export const probe = name => probes.has(shortName(name)) || shortName(name).startsWith('tabs_context');
12
+ export function responseText(value) {
13
+ if (typeof value === 'string') return value.trim();
14
+ if (value == null) return '';
15
+ if (Array.isArray(value)) return value.map(responseText).join('\n').trim();
16
+ if (Array.isArray(value.content)) return responseText(value.content);
17
+ if (typeof value.text === 'string') return value.text.trim();
18
+ return JSON.stringify(value);
19
+ }
20
+ export function isError(value) {
21
+ if (value && typeof value === 'object' && (value.isError || value.is_error || Object.hasOwn(value, 'error'))) return true;
22
+ return /^error/i.test(responseText(value));
23
+ }
24
+ export const fruitless = value => isError(value) || /^(?:\[\]|\{\}|null|not found)?$/i.test(responseText(value));
25
+ export function stable(value) {
26
+ if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
27
+ if (value && typeof value === 'object') return `{${Object.keys(value).sort().map(k => `${JSON.stringify(k)}:${stable(value[k])}`).join(',')}}`;
28
+ return JSON.stringify(value);
29
+ }
30
+ function secureDir(dir) {
31
+ let current = path.parse(path.resolve(dir)).root;
32
+ for (const part of path.resolve(dir).slice(current.length).split(path.sep)) {
33
+ current = path.join(current, part);
34
+ try { if (fs.lstatSync(current).isSymbolicLink()) throw new Error('Unsafe state'); }
35
+ catch (e) { if (e.code !== 'ENOENT') throw e; }
36
+ }
37
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
38
+ fs.chmodSync(dir, 0o700);
39
+ }
40
+ export function stateFile(kind, id) {
41
+ const cfg = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
42
+ const dir = process.env.LBU_STATE_DIR || path.join(cfg, 'lean-browser-use', 'state');
43
+ secureDir(dir);
44
+ const subdir = path.join(dir, kind);
45
+ secureDir(subdir);
46
+ for (const name of fs.readdirSync(subdir)) {
47
+ const file = path.join(subdir, name);
48
+ const stat = fs.lstatSync(file);
49
+ if (stat.isFile() && Date.now() - stat.mtimeMs > 86400000) fs.unlinkSync(file);
50
+ }
51
+ const file = path.join(subdir, hash(id).slice(0, 24));
52
+ try { if (fs.lstatSync(file).isSymbolicLink()) throw new Error('Unsafe state'); }
53
+ catch (e) { if (e.code !== 'ENOENT') throw e; }
54
+ return file;
55
+ }
56
+ export function readState(file) {
57
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); }
58
+ catch (e) { if (e.code === 'ENOENT') return {}; throw e; }
59
+ }
60
+ export function writeState(file, value) {
61
+ const tmp = `${file}.${randomBytes(6).toString('hex')}`;
62
+ try {
63
+ fs.writeFileSync(tmp, JSON.stringify(value), { mode: 0o600, flag: 'wx' });
64
+ fs.renameSync(tmp, file);
65
+ } finally { try { fs.unlinkSync(tmp); } catch {} }
66
+ }
67
+ export function input() { return JSON.parse(fs.readFileSync(0, 'utf8')); }
68
+ export function block(message) { process.stderr.write(`${message}\n`); process.exitCode = 2; }
69
+ export function guard(run) {
70
+ try { run(); } catch { process.exitCode = 0; }
71
+ }
@@ -0,0 +1,58 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "mcp__claude-in-chrome__.*|mcp__Claude_Browser__.*|mcp__Control_Chrome__.*",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "\"${HOOKS_DIR}/pick-guard.sh\"",
10
+ "timeout": 10
11
+ }
12
+ ]
13
+ },
14
+ {
15
+ "matcher": "mcp__claude-in-chrome__.*|mcp__Claude_Browser__.*|mcp__Control_Chrome__.*|mcp__computer-use__.*|mcp__cua-driver__.*",
16
+ "hooks": [
17
+ {
18
+ "type": "command",
19
+ "command": "\"${HOOKS_DIR}/retry-guard.sh\"",
20
+ "timeout": 10
21
+ }
22
+ ]
23
+ }
24
+ ],
25
+ "PostToolUse": [
26
+ {
27
+ "matcher": "mcp__claude-in-chrome__select_browser|mcp__claude-in-chrome__switch_browser|mcp__claude-in-chrome__tabs_close_mcp",
28
+ "hooks": [
29
+ {
30
+ "type": "command",
31
+ "command": "\"${HOOKS_DIR}/pick-guard.sh\"",
32
+ "timeout": 10
33
+ }
34
+ ]
35
+ },
36
+ {
37
+ "matcher": "mcp__claude-in-chrome__.*|mcp__Claude_Browser__.*|mcp__Control_Chrome__.*|mcp__computer-use__.*|mcp__cua-driver__.*",
38
+ "hooks": [
39
+ {
40
+ "type": "command",
41
+ "command": "\"${HOOKS_DIR}/retry-guard.sh\"",
42
+ "timeout": 10
43
+ }
44
+ ]
45
+ },
46
+ {
47
+ "matcher": "mcp__claude-in-chrome__.*|mcp__Claude_Browser__.*|mcp__Control_Chrome__.*|mcp__computer-use__.*|mcp__cua-driver__.*",
48
+ "hooks": [
49
+ {
50
+ "type": "command",
51
+ "command": "\"${HOOKS_DIR}/batch-nudge.sh\"",
52
+ "timeout": 10
53
+ }
54
+ ]
55
+ }
56
+ ]
57
+ }
58
+ }
@@ -0,0 +1,27 @@
1
+ import fs from 'node:fs';
2
+ import { browser, shortName, probe, isError, stateFile, readState, writeState, input, block, guard } from './common.mjs';
3
+
4
+ const message = 'List connected browsers, ask the user to pick by profile name, then select that browser. For the in-app pane, record the pick with node pick-guard.mjs --pick <label> --session <id>. A runner re-picks only the deviceId named in its brief.';
5
+ guard(() => {
6
+ const args = process.argv.slice(2);
7
+ if (args.length) {
8
+ const session = args[args.indexOf('--session') + 1];
9
+ if (!args.includes('--session') || !session || session.startsWith('--')) return block(message);
10
+ const file = stateFile('pick', session);
11
+ if (args.includes('--reset')) { if (fs.existsSync(file)) fs.unlinkSync(file); return; }
12
+ const label = args[args.indexOf('--pick') + 1];
13
+ if (!args.includes('--pick') || !label || label.startsWith('--')) return block(message);
14
+ writeState(file, { picked: true });
15
+ return;
16
+ }
17
+ const p = input();
18
+ if (!browser(p.tool_name) || probe(p.tool_name)) return;
19
+ if (!p.session_id) return block(message);
20
+ const file = stateFile('pick', p.session_id);
21
+ const name = shortName(p.tool_name);
22
+ const selecting = ['select_browser', 'switch_browser'].includes(name);
23
+ if (p.hook_event_name === 'PostToolUse') {
24
+ if (name === 'tabs_close_mcp') { if (fs.existsSync(file)) fs.unlinkSync(file); }
25
+ else if (selecting && !isError(p.tool_response)) writeState(file, { picked: true });
26
+ } else if (p.hook_event_name === 'PreToolUse' && !selecting && !readState(file).picked) block(message);
27
+ });
@@ -0,0 +1,8 @@
1
+ #!/bin/sh
2
+ d=$(cd "$(dirname "$0")" && pwd) || exit 0
3
+ command -v node >/dev/null 2>&1 || exit 0
4
+ [ -f "$d/pick-guard.mjs" ] || exit 0
5
+ node "$d/pick-guard.mjs" "$@"
6
+ status=$?
7
+ [ "$status" -eq 2 ] && exit 2
8
+ exit 0
@@ -0,0 +1,69 @@
1
+ import { browser, native, shortName, probe, fruitless, isError, responseText, stable, hash, stateFile, readState, writeState, input, block, guard } from './common.mjs';
2
+
3
+ export function waitSeconds(name, args) {
4
+ const code = String(args.code || args.javascript || args.script || args.text || '');
5
+ const isWait = args.action === 'wait' || /wait|sleep/i.test(name) || Object.hasOwn(args, 'duration') || Object.hasOwn(args, 'ms') ||
6
+ (name === 'javascript_tool' && /sleep|timeout|waitFor/i.test(code));
7
+ if (!isWait) return null;
8
+ if (Number.isFinite(Number(args.ms))) return Math.max(0, Number(args.ms) / 1000);
9
+ if (Number.isFinite(Number(args.duration))) return Math.max(0, Number(args.duration));
10
+ const delays = [...code.matchAll(/(?:setTimeout\s*\([^,]*,\s*|(?:sleep|waitFor|timeout)\s*\(\s*)(\d+)/gi)].map(m => Number(m[1]) / 1000);
11
+ if (delays.length) return Math.max(...delays);
12
+ // An unbounded scripted delay consumes the budget instead of bypassing it.
13
+ return 60;
14
+ }
15
+ const empty = () => ({ waits: 0, nav: '', navCount: 0, origin: '', fruitless: 0, readKey: '', readCount: 0, text: '', nativeKey: '', nativeError: '', nativeCount: 0 });
16
+ function origin(url) { try { return new URL(url).origin; } catch { return ''; } }
17
+ guard(() => {
18
+ const p = input();
19
+ if ((!browser(p.tool_name) && !native(p.tool_name)) || probe(p.tool_name)) return;
20
+ const id = p.agent_id || p.session_id;
21
+ if (!id || !['PreToolUse', 'PostToolUse'].includes(p.hook_event_name)) return;
22
+ const file = stateFile('retry', id);
23
+ const data = readState(file);
24
+ data.tabs ||= {};
25
+ const args = p.tool_input || {};
26
+ const tab = String(args.tabId ?? '-');
27
+ let s = Object.hasOwn(data.tabs, tab) ? data.tabs[tab] : empty();
28
+ const name = shortName(p.tool_name);
29
+ const key = hash(`${p.tool_name}:${stable(args)}`);
30
+ const wait = waitSeconds(name, args);
31
+ const nav = name === 'navigate' || args.action === 'navigate';
32
+ const url = String(args.url || '');
33
+ const read = /^(read_page|find|get_page_text|get_page_content|javascript_tool|read|screenshot)$/.test(name) && wait === null;
34
+ const targeted = /click|type|fill/.test(name) || /click|type|fill/.test(args.action || '');
35
+ const movedOrigin = nav && origin(url) && s.origin && origin(url) !== s.origin;
36
+ if (p.hook_event_name === 'PreToolUse') {
37
+ if (browser(p.tool_name) && (movedOrigin || targeted)) s = empty();
38
+ if (native(p.tool_name) && s.nativeKey === key && s.nativeCount >= 2) return block('Stop and report the repeated native error. Choose a new approach before trying again.');
39
+ if (wait !== null && s.waits + wait > (Number(process.env.LBU_WAIT_CAP_SECONDS) || 60)) return block('Wait for an element, not for time. Stop and report if the page stays unchanged.');
40
+ if (s.fruitless >= 2 && (wait !== null || nav || (read && s.readKey === key && s.readCount >= 2))) return block('Stop and report the fruitless reads. Use the supplied read code once or choose a different query.');
41
+ if (nav && s.nav === url && s.navCount >= 2) return block('Stop and report the repeated navigation. Use the supplied read code once.');
42
+ if (wait !== null) s.waits += wait;
43
+ if (nav) { s.navCount = s.nav === url ? s.navCount + 1 : 1; s.nav = url; s.origin = origin(url); }
44
+ else { s.nav = ''; s.navCount = 0; }
45
+ } else {
46
+ if (native(p.tool_name)) {
47
+ const error = hash(responseText(p.tool_response));
48
+ if (isError(p.tool_response)) {
49
+ s.nativeCount = s.nativeKey === key && s.nativeError === error ? s.nativeCount + 1 : 1;
50
+ s.nativeKey = key; s.nativeError = error;
51
+ } else { s.nativeCount = 0; s.nativeKey = ''; }
52
+ }
53
+ if (read) {
54
+ const text = hash(responseText(p.tool_response));
55
+ if (fruitless(p.tool_response)) {
56
+ s.fruitless += 1;
57
+ s.readCount = s.readKey === key ? s.readCount + 1 : 1;
58
+ s.readKey = key;
59
+ } else {
60
+ if (text !== s.text) s = empty();
61
+ s.fruitless = 0; s.readCount = 0; s.text = text;
62
+ }
63
+ } else if (wait === null && !nav && !native(p.tool_name)) { s.fruitless = 0; s.readCount = 0; }
64
+ }
65
+ Object.defineProperty(data.tabs, tab, { value: s, enumerable: true, configurable: true, writable: true });
66
+ // Bound state even when a session sees many tabs.
67
+ for (const old of Object.keys(data.tabs).slice(0, -64)) delete data.tabs[old];
68
+ writeState(file, data);
69
+ });
@@ -0,0 +1,8 @@
1
+ #!/bin/sh
2
+ d=$(cd "$(dirname "$0")" && pwd) || exit 0
3
+ command -v node >/dev/null 2>&1 || exit 0
4
+ [ -f "$d/retry-guard.mjs" ] || exit 0
5
+ node "$d/retry-guard.mjs" "$@"
6
+ status=$?
7
+ [ "$status" -eq 2 ] && exit 2
8
+ exit 0
package/llms.txt ADDED
@@ -0,0 +1,40 @@
1
+ # lean-browser-use
2
+
3
+ > Cut browser use cost by about 25% in Claude Code: fresh runner subagent, bulk reads, three guards and a token meter. One npx command
4
+
5
+ Install: `npx lean-browser-use`
6
+
7
+ Two measured runs cost 3.15% and 25.26% less with the package at list API prices, about 14.02% less pooled. About 25% cheaper is the rounded best of those two runs on one setup. Savings vary with model, task and session size; measure your own transcripts. A browser step re-reads about 3x fewer tokens per browser step. Everything run 1 read totalled 2,657,290 tokens in the plain arm and 2,846,792 in the runner arm: about 7% higher overall. Model, task and session size affect cost. The rough token break-even of about 16 browser steps is an estimate.
8
+
9
+ ## Docs
10
+
11
+ - [Dollar costs, browser-step savings, whole-run totals, estimated break-even, and reproduction](https://github.com/aunysillyme/lean-browser-use/blob/main/docs/benchmark.md)
12
+
13
+ - [Installation, flags, health checks, and removal](https://github.com/aunysillyme/lean-browser-use/blob/main/docs/install.md)
14
+ - [How routing, packets, verification, and cleanup work](https://github.com/aunysillyme/lean-browser-use/blob/main/docs/how-it-works.md)
15
+ - [Guard rules, registration, and payload checks](https://github.com/aunysillyme/lean-browser-use/blob/main/docs/guards.md)
16
+ - [How to measure main and runner browser context](https://github.com/aunysillyme/lean-browser-use/blob/main/docs/measure.md)
17
+
18
+ ## Reference
19
+
20
+ - [Skill entrypoint and task routing](https://github.com/aunysillyme/lean-browser-use/blob/main/skills/lean-browser-use/SKILL.md)
21
+ - [Route table with a complete subagent-only path](https://github.com/aunysillyme/lean-browser-use/blob/main/skills/lean-browser-use/references/route-by-job.md)
22
+ - [Profile selection and logged-in tab handoff](https://github.com/aunysillyme/lean-browser-use/blob/main/skills/lean-browser-use/references/logged-in-run.md)
23
+ - [Runner execution and evidence contract](https://github.com/aunysillyme/lean-browser-use/blob/main/agents/aun-browser-runner.md)
24
+ - [Browser read packet template](https://github.com/aunysillyme/lean-browser-use/blob/main/briefs/browser-read.md)
25
+ - [Cheap-helper read packet template](https://github.com/aunysillyme/lean-browser-use/blob/main/briefs/helper-read.md)
26
+ - [Repository commands and tested rules](https://github.com/aunysillyme/lean-browser-use/blob/main/AGENTS.md)
27
+ - [Package measurements and reproduction commands](https://github.com/aunysillyme/lean-browser-use/blob/main/proof/results.json)
28
+ - [Public concept names and provenance](https://github.com/aunysillyme/lean-browser-use/blob/main/docs/PROVENANCE.md)
29
+
30
+ ## Reader and search companions
31
+
32
+ - [Compile-once reader setup and command contract](https://github.com/aunysillyme/lean-browser-use/blob/main/docs/reader.md)
33
+ - [SearXNG setup and link discovery](https://github.com/aunysillyme/lean-browser-use/blob/main/docs/search.md)
34
+ - [Upstream contributions and licenses](https://github.com/aunysillyme/lean-browser-use/blob/main/CREDITS.md)
35
+ - [Contribution workflow and useful changes](https://github.com/aunysillyme/lean-browser-use/blob/main/CONTRIBUTING.md)
36
+
37
+ ## Part of a set
38
+
39
+ - [lean-browser-use: Cut browser use cost by about 25% in Claude Code: fresh runner subagent, bulk reads, three guards and a token meter. One npx command](https://github.com/aunysillyme/lean-browser-use/blob/main/README.md)
40
+ - [lean-computer-use: Background Mac computer control for AI agents: desk tools, a fresh runner subagent, retry guards and a transcript token meter, installed with one command](https://github.com/aunysillyme/lean-computer-use)
package/package.json CHANGED
@@ -1,6 +1,63 @@
1
1
  {
2
2
  "name": "lean-browser-use",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Cut browser use cost by about 25% in Claude Code: fresh runner subagent, bulk reads, three guards and a token meter. One npx command",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "engines": {
8
+ "node": ">=20"
9
+ },
10
+ "bin": {
11
+ "lean-browser-use": "bin/lean-browser-use.mjs"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "src",
16
+ "skills",
17
+ "agents",
18
+ "briefs",
19
+ "hooks",
20
+ "reader",
21
+ "searxng",
22
+ "proof",
23
+ "docs",
24
+ "!docs/trailer.gif",
25
+ "README.md",
26
+ "LICENSE",
27
+ "CHANGELOG.md",
28
+ "CREDITS.md",
29
+ "llms.txt"
30
+ ],
31
+ "scripts": {
32
+ "test": "node --test test/",
33
+ "check": "node scripts/check.mjs",
34
+ "proof": "node scripts/proof.mjs"
35
+ },
36
+ "repository": "git+https://github.com/aunysillyme/lean-browser-use.git",
37
+ "homepage": "https://github.com/aunysillyme/lean-browser-use#readme",
38
+ "bugs": {
39
+ "url": "https://github.com/aunysillyme/lean-browser-use/issues"
40
+ },
41
+ "funding": "https://github.com/sponsors/aunysillyme",
42
+ "keywords": [
43
+ "browser-use",
44
+ "browser-automation",
45
+ "claude-code",
46
+ "ai-agents",
47
+ "subagent",
48
+ "token-efficiency",
49
+ "mcp",
50
+ "claude-in-chrome",
51
+ "web-scraping",
52
+ "hooks",
53
+ "llm-cost",
54
+ "context-window",
55
+ "claude-code-hooks",
56
+ "claude-code-subagent",
57
+ "browser-agent",
58
+ "token-usage",
59
+ "agent-skill",
60
+ "crawl4ai",
61
+ "searxng"
62
+ ]
63
+ }