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,173 @@
1
+ """Small unittest fixture adapter for the retained regression functions."""
2
+ from contextlib import ExitStack, redirect_stdout, redirect_stderr
3
+ import inspect
4
+ import io
5
+ import itertools
6
+ import os
7
+ from pathlib import Path
8
+ import re
9
+ import socket
10
+ import sys
11
+ import tempfile
12
+ from types import SimpleNamespace
13
+ import unittest
14
+ from unittest import mock
15
+
16
+ fixtures = {}
17
+
18
+
19
+ def fixture(fn=None, autouse=False):
20
+ def decorate(function):
21
+ fixtures[function.__name__] = (function, autouse)
22
+ return function
23
+ return decorate(fn) if fn else decorate
24
+
25
+
26
+ class Mark:
27
+ def parametrize(self, names, values):
28
+ def decorate(function):
29
+ existing = getattr(function, '_parameters', [])
30
+ function._parameters = existing + [(names.replace(' ', '').split(','), values)]
31
+ return function
32
+ return decorate
33
+
34
+
35
+ mark = Mark()
36
+ skip = lambda reason: (_ for _ in ()).throw(unittest.SkipTest(reason))
37
+
38
+
39
+ class raises:
40
+ def __init__(self, exception, match=None):
41
+ self.exception, self.match = exception, match
42
+
43
+ def __enter__(self):
44
+ return self
45
+
46
+ def __exit__(self, kind, value, tb):
47
+ if kind is None:
48
+ raise AssertionError('Expected exception')
49
+ if not isinstance(value, self.exception):
50
+ return False
51
+ if self.match and not re.search(self.match, str(value)):
52
+ raise AssertionError('Exception text did not match')
53
+ self.value = value
54
+ return True
55
+
56
+
57
+ class MonkeyPatch:
58
+ def __init__(self, stack):
59
+ self.stack = stack
60
+
61
+ def setenv(self, name, value):
62
+ self.stack.enter_context(mock.patch.dict(os.environ, {name: str(value)}))
63
+
64
+ def delenv(self, name, raising=True):
65
+ if raising and name not in os.environ:
66
+ raise KeyError(name)
67
+ self.stack.enter_context(mock.patch.dict(os.environ))
68
+ os.environ.pop(name, None)
69
+
70
+ def setattr(self, obj, name, value):
71
+ original = getattr(obj, name)
72
+ # The command facade re-exports functions from focused reader modules.
73
+ # Patch their imported bindings too, keeping the original injection tests.
74
+ if getattr(obj, '__name__', '') == 'page_reader':
75
+ for module_name, module in list(sys.modules.items()):
76
+ if module_name in ('reader_fetch', 'reader_crawl', 'reader_extract', 'reader_compile', 'reader_runtime') and getattr(module, name, object()) is original:
77
+ self.stack.enter_context(mock.patch.object(module, name, value))
78
+ self.stack.enter_context(mock.patch.object(obj, name, value))
79
+
80
+ def setitem(self, mapping, key, value):
81
+ self.stack.enter_context(mock.patch.dict(mapping, {key: value}))
82
+
83
+
84
+ class Capture:
85
+ def __init__(self, stack):
86
+ self.out, self.err = io.StringIO(), io.StringIO()
87
+ stack.enter_context(redirect_stdout(self.out))
88
+ stack.enter_context(redirect_stderr(self.err))
89
+
90
+ def readouterr(self):
91
+ result = SimpleNamespace(out=self.out.getvalue(), err=self.err.getvalue())
92
+ for stream in (self.out, self.err):
93
+ stream.seek(0)
94
+ stream.truncate()
95
+ return result
96
+
97
+
98
+ def crawler_available():
99
+ try:
100
+ import importlib.util
101
+ return importlib.util.find_spec('crawl4ai') is not None
102
+ except (ImportError, ValueError):
103
+ return False
104
+
105
+
106
+ def local_sockets_available():
107
+ try:
108
+ with socket.socket() as sock:
109
+ sock.bind(('127.0.0.1', 0))
110
+ return True
111
+ except PermissionError:
112
+ return False
113
+
114
+
115
+ class RegressionCase(unittest.TestCase):
116
+ def __init__(self, function, parameters, label):
117
+ super().__init__('runTest')
118
+ self.function, self.parameters, self.label = function, parameters, label
119
+
120
+ def id(self):
121
+ return self.label
122
+
123
+ def __str__(self):
124
+ return self.label
125
+
126
+ def runTest(self):
127
+ name = self.function.__name__
128
+ needs_crawler = name.startswith('test_browser_') or name in (
129
+ 'test_real_crawl4ai_pricing_snapshot', 'test_compile_css_refinement_carries_failing_rows',
130
+ 'test_css_hostile_html_extracts_without_browser_or_requests')
131
+ if needs_crawler and not crawler_available():
132
+ self.skipTest('optional crawl4ai/browser dependency is absent; offline contracts still run')
133
+ needs_socket = name.startswith('test_proxy_') or 'local_http' in inspect.signature(self.function).parameters
134
+ if needs_socket and not local_sockets_available():
135
+ self.skipTest('sandbox blocks local sockets; public-address unit checks still run')
136
+ with ExitStack() as stack:
137
+ directory = stack.enter_context(tempfile.TemporaryDirectory(prefix='lbu-reader-test-'))
138
+ cache = {'tmp_path': Path(directory), 'monkeypatch': MonkeyPatch(stack), 'capsys': Capture(stack)}
139
+ cache.update(self.parameters)
140
+
141
+ def resolve(key):
142
+ if key in cache:
143
+ return cache[key]
144
+ factory = fixtures[key][0]
145
+ value = factory(**{arg: resolve(arg) for arg in inspect.signature(factory).parameters})
146
+ if inspect.isgenerator(value):
147
+ generator = value
148
+ value = next(generator)
149
+ stack.callback(lambda: next(generator, None))
150
+ cache[key] = value
151
+ return value
152
+
153
+ for key, (_, automatic) in fixtures.items():
154
+ if automatic:
155
+ resolve(key)
156
+ self.function(**{arg: resolve(arg) for arg in inspect.signature(self.function).parameters})
157
+
158
+
159
+ def suite(namespace):
160
+ cases = []
161
+ for name, function in namespace.items():
162
+ if not name.startswith('test_') or not callable(function):
163
+ continue
164
+ parameters = [{}]
165
+ for names, values in getattr(function, '_parameters', []):
166
+ expanded = []
167
+ for previous, value in itertools.product(parameters, values):
168
+ values_tuple = value if len(names) > 1 else (value,)
169
+ expanded.append({**previous, **dict(zip(names, values_tuple))})
170
+ parameters = expanded
171
+ for index, values in enumerate(parameters):
172
+ cases.append(RegressionCase(function, values, f'{name}[{index}]'))
173
+ return unittest.TestSuite(cases)
@@ -0,0 +1,214 @@
1
+ """Offline reader regression checks using the standard library."""
2
+
3
+
4
+ import asyncio
5
+
6
+
7
+ import importlib.machinery
8
+
9
+
10
+ import importlib.util
11
+
12
+
13
+ import hashlib
14
+
15
+
16
+ import io
17
+
18
+
19
+ import json
20
+
21
+
22
+ import os
23
+
24
+
25
+ from pathlib import Path
26
+
27
+
28
+ import runpy
29
+
30
+
31
+ import shlex
32
+
33
+
34
+ import socket
35
+
36
+
37
+ import subprocess
38
+
39
+
40
+ import sys
41
+
42
+
43
+ import threading
44
+
45
+
46
+ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
47
+
48
+
49
+ from unittest import mock
50
+
51
+
52
+ from urllib.error import HTTPError
53
+
54
+
55
+ from urllib.request import Request
56
+
57
+
58
+ import reader_test_harness as checks
59
+
60
+
61
+ ROOT = Path(__file__).resolve().parent
62
+
63
+
64
+ DATA = ROOT / "testdata"
65
+
66
+
67
+ REAL_SOCKET = socket.socket
68
+
69
+
70
+ REAL_GETADDRINFO = socket.getaddrinfo
71
+
72
+
73
+ loader = importlib.machinery.SourceFileLoader("page_reader", str(ROOT / "reader"))
74
+
75
+
76
+ module_spec = importlib.util.spec_from_loader(loader.name, loader)
77
+
78
+
79
+ reader = importlib.util.module_from_spec(module_spec)
80
+
81
+
82
+ loader.exec_module(reader)
83
+
84
+
85
+ helper = runpy.run_path(str(ROOT / "crawl_helper.py"))
86
+
87
+
88
+ @checks.fixture(autouse=True)
89
+ def offline(monkeypatch, tmp_path):
90
+ monkeypatch.setenv("READER_SPECS_DIR", str(tmp_path / "specs"))
91
+ monkeypatch.setenv("READER_STATE_DIR", str(tmp_path / "state"))
92
+ monkeypatch.setenv("READER_COMPILE_CMD", shlex.join([sys.executable, str(DATA / "compile_stub.py")]))
93
+ monkeypatch.setenv("STUB_CALL_LOG", str(tmp_path / "calls.jsonl"))
94
+ monkeypatch.setenv("STUB_MODE", "npm")
95
+ # Any accidental live network access fails the offline suite.
96
+ monkeypatch.setattr(socket, "getaddrinfo", mock.Mock(side_effect=AssertionError("offline DNS")))
97
+ monkeypatch.setattr(socket, "socket", mock.Mock(side_effect=AssertionError("offline socket")))
98
+
99
+
100
+ def calls():
101
+ path = Path(os.environ["STUB_CALL_LOG"])
102
+ return [json.loads(line) for line in path.read_text().splitlines()] if path.exists() else []
103
+
104
+
105
+ def html(package="react"):
106
+ return (DATA / f"npm-{package}.html").read_text()
107
+
108
+
109
+ def compile_npm():
110
+ return reader.compile_spec("npm-package", "https://www.npmjs.com/package/{name}",
111
+ {"name": "react"}, "package name, latest version, weekly downloads, license, repository URL",
112
+ {"name": "react", "latest_version": "19.2.0", "weekly_downloads": 42000000,
113
+ "license": "MIT", "repository_url": "git+https://github.com/facebook/react.git"}, html())
114
+
115
+
116
+ PUBLIC_IP = ".".join(["93", "184", "216", "34"])
117
+
118
+
119
+ def public_address(ip=PUBLIC_IP):
120
+ return (socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_TCP, "", (ip, 443))
121
+
122
+
123
+ @checks.fixture
124
+ def anchor_compile(monkeypatch, tmp_path):
125
+ schema = {"type": "embedded_json", "source": "__NEXT_DATA__", "rows": "models",
126
+ "paths": {"model": "model", "input": "input", "output": "output"},
127
+ "required": ["model", "input", "output"]}
128
+ command = tmp_path / "anchor_compile.py"
129
+ command.write_text('''import json, os, sys
130
+ payload = json.loads(sys.stdin.read().split("INPUT JSON:\\n", 1)[1])
131
+ with open(os.environ["STUB_CALL_LOG"], "a") as stream:
132
+ stream.write(json.dumps(payload) + "\\n")
133
+ schema = json.loads(sys.argv[1])
134
+ if not payload.get("validation_failures") or os.environ["STUB_MODE"] == "anchor-broken":
135
+ schema["transforms"] = {"model": {"regex": r"(Claude .* [0-9]+)"}}
136
+ print(json.dumps(schema))
137
+ ''')
138
+ monkeypatch.setenv("READER_COMPILE_CMD", shlex.join([sys.executable, str(command), json.dumps(schema)]))
139
+ rows = [{"model": "Claude Fable 5.1", "input": 10, "output": 50},
140
+ {"model": "Claude Sonnet 5.5", "input": 2, "output": 10}]
141
+ page = '<script id="__NEXT_DATA__">' + json.dumps({"models": rows}) + '</script>'
142
+ monkeypatch.setattr(reader, "fetch", lambda url, **kwargs: (page, "urllib"))
143
+ return rows
144
+
145
+
146
+ @checks.fixture
147
+ def local_http(monkeypatch):
148
+ # Only these integration tests open sockets, all against local servers.
149
+ monkeypatch.setattr(socket, "socket", REAL_SOCKET)
150
+ monkeypatch.setattr(socket, "getaddrinfo", REAL_GETADDRINFO)
151
+ pages = {}
152
+ requests = []
153
+
154
+ class Handler(BaseHTTPRequestHandler):
155
+ def do_GET(self):
156
+ requests.append((f"http://127.0.0.1:{self.server.server_address[1]}", self.path))
157
+ status, headers, body = pages.get(self.path, (200, {}, "<p>private</p>"))
158
+ self.send_response(status)
159
+ self.send_header("Content-Type", "text/html; charset=utf-8")
160
+ for key, value in headers.items():
161
+ self.send_header(key, value)
162
+ self.send_header("Content-Length", str(len(body.encode())))
163
+ self.end_headers()
164
+ self.wfile.write(body.encode())
165
+
166
+ def log_message(self, *args):
167
+ pass
168
+
169
+ allowed = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
170
+ port = allowed.server_address[1]
171
+ denied = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
172
+ servers = [allowed, denied]
173
+ threads = [threading.Thread(target=server.serve_forever, daemon=True) for server in servers]
174
+ for thread in threads:
175
+ thread.start()
176
+ origin = f"http://127.0.0.1:{port}"
177
+ denied_origin = f"http://127.0.0.1:{denied.server_address[1]}"
178
+ monkeypatch.setenv("READER_TEST_ALLOW_ORIGIN", origin)
179
+ monkeypatch.setenv("INSTABOX_HEADLESS", "1")
180
+ try:
181
+ yield origin, denied_origin, pages, requests
182
+ finally:
183
+ for server in servers:
184
+ server.shutdown()
185
+ server.server_close()
186
+ for thread in threads:
187
+ thread.join(timeout=5)
188
+
189
+
190
+ @checks.fixture
191
+ def crwl_fetch_stub(monkeypatch, tmp_path):
192
+ log = tmp_path / "crwl-calls.jsonl"
193
+ executable = tmp_path / "crawl-python"
194
+ executable.write_text(f'''#!{sys.executable}
195
+ import json
196
+ import os
197
+ from pathlib import Path
198
+ import sys
199
+ assert sys.argv[2:] == ["fetch"]
200
+ request = json.load(sys.stdin)
201
+ with open(os.environ["CRWL_FETCH_LOG"], "a") as stream:
202
+ stream.write(json.dumps(request) + "\\n")
203
+ package = request["url"].rsplit("/", 1)[-1]
204
+ page = (Path({str(DATA)!r}) / ("npm-" + package + ".html")).read_text()
205
+ print("crwl: browser ready")
206
+ print(json.dumps({{"html": page, "success": True, "status_code": 200, "url": request["url"]}}))
207
+ ''')
208
+ executable.chmod(0o755)
209
+ monkeypatch.setenv("READER_CRAWL4AI_PYTHON", str(executable))
210
+ monkeypatch.setattr(reader, "checked_address", lambda url: public_address())
211
+ monkeypatch.setenv("CRWL_FETCH_LOG", str(log))
212
+ return log
213
+
214
+
@@ -0,0 +1,223 @@
1
+ {
2
+ "type": "css",
3
+ "schema": {
4
+ "name": "claude_pricing",
5
+ "baseSelector": "#content-container table:has(colgroup[span=\"3\"]) > tbody > tr:has(td[rowspan]):not(:has(button[aria-label*=\"Retired\"]))",
6
+ "fields": [
7
+ {
8
+ "name": "model",
9
+ "selector": "td:nth-child(1)",
10
+ "type": "text"
11
+ },
12
+ {
13
+ "name": "input",
14
+ "selector": "td:nth-child(2)",
15
+ "type": "text"
16
+ },
17
+ {
18
+ "name": "output",
19
+ "selector": "td:nth-child(3)",
20
+ "type": "text"
21
+ },
22
+ {
23
+ "name": "cache_write_5m",
24
+ "selector": "td:nth-child(4)",
25
+ "type": "text"
26
+ },
27
+ {
28
+ "name": "cache_write_1h",
29
+ "selector": "td:nth-child(5)",
30
+ "type": "text"
31
+ },
32
+ {
33
+ "name": "cache_read",
34
+ "selector": "td:nth-child(6)",
35
+ "type": "text"
36
+ }
37
+ ]
38
+ },
39
+ "required": [
40
+ "model",
41
+ "input",
42
+ "output"
43
+ ],
44
+ "transforms": {
45
+ "model": {
46
+ "regex": "(Claude\\s+\\w+\\s+\\d+(?:\\.\\d+)?)"
47
+ },
48
+ "input": {
49
+ "regex": "\\$([\\d.]+)",
50
+ "number": true
51
+ },
52
+ "output": {
53
+ "regex": "\\$([\\d.]+)",
54
+ "number": true
55
+ },
56
+ "cache_write_5m": {
57
+ "regex": "\\$([\\d.]+)",
58
+ "number": true
59
+ },
60
+ "cache_write_1h": {
61
+ "regex": "\\$([\\d.]+)",
62
+ "number": true
63
+ },
64
+ "cache_read": {
65
+ "regex": "\\$([\\d.]+)",
66
+ "number": true
67
+ }
68
+ },
69
+ "example": {
70
+ "model": "Claude Sonnet 5.5",
71
+ "input": 2,
72
+ "output": 10,
73
+ "cache_write_5m": 2.5,
74
+ "cache_write_1h": 4,
75
+ "cache_read": 0.1
76
+ },
77
+ "name": "claude-pricing",
78
+ "url_template": "https://platform.claude.com/docs/en/about-claude/pricing",
79
+ "params": {},
80
+ "want": "One row for each current Claude model with model name, numeric input and output USD per million tokens, cache write 5m and 1h and cache read prices where shown. Exclude retired models and use baseline context pricing.",
81
+ "sample_output": [
82
+ {
83
+ "model": "Claude Fable 5.1",
84
+ "input": 10.0,
85
+ "output": 50.0,
86
+ "cache_write_5m": 12.5,
87
+ "cache_write_1h": 20.0,
88
+ "cache_read": 0.25
89
+ },
90
+ {
91
+ "model": "Claude Opus 5.5",
92
+ "input": 4.0,
93
+ "output": 20.0,
94
+ "cache_write_5m": 5.0,
95
+ "cache_write_1h": 8.0,
96
+ "cache_read": 0.2
97
+ },
98
+ {
99
+ "model": "Claude Sonnet 5.5",
100
+ "input": 2.0,
101
+ "output": 10.0,
102
+ "cache_write_5m": 2.5,
103
+ "cache_write_1h": 4.0,
104
+ "cache_read": 0.1
105
+ },
106
+ {
107
+ "model": "Claude Haiku 5.5",
108
+ "input": 0.1,
109
+ "output": 0.5,
110
+ "cache_write_5m": 0.125,
111
+ "cache_write_1h": 0.2,
112
+ "cache_read": 0.01
113
+ },
114
+ {
115
+ "model": "Claude Mythos 5.1",
116
+ "input": 10.0,
117
+ "output": 50.0,
118
+ "cache_write_5m": 12.5,
119
+ "cache_write_1h": 20.0,
120
+ "cache_read": 0.25
121
+ },
122
+ {
123
+ "model": "Claude Fable 5",
124
+ "input": 10.0,
125
+ "output": 50.0,
126
+ "cache_write_5m": 12.5,
127
+ "cache_write_1h": 20.0,
128
+ "cache_read": 1.0
129
+ },
130
+ {
131
+ "model": "Claude Mythos 5",
132
+ "input": 10.0,
133
+ "output": 50.0,
134
+ "cache_write_5m": 12.5,
135
+ "cache_write_1h": 20.0,
136
+ "cache_read": 1.0
137
+ },
138
+ {
139
+ "model": "Claude Opus 5",
140
+ "input": 5.0,
141
+ "output": 25.0,
142
+ "cache_write_5m": 6.25,
143
+ "cache_write_1h": 10.0,
144
+ "cache_read": 0.5
145
+ },
146
+ {
147
+ "model": "Claude Opus 4.8",
148
+ "input": 5.0,
149
+ "output": 25.0,
150
+ "cache_write_5m": 6.25,
151
+ "cache_write_1h": 10.0,
152
+ "cache_read": 0.5
153
+ },
154
+ {
155
+ "model": "Claude Opus 4.7",
156
+ "input": 5.0,
157
+ "output": 25.0,
158
+ "cache_write_5m": 6.25,
159
+ "cache_write_1h": 10.0,
160
+ "cache_read": 0.5
161
+ },
162
+ {
163
+ "model": "Claude Opus 4.6",
164
+ "input": 5.0,
165
+ "output": 25.0,
166
+ "cache_write_5m": 6.25,
167
+ "cache_write_1h": 10.0,
168
+ "cache_read": 0.5
169
+ },
170
+ {
171
+ "model": "Claude Opus 4.5",
172
+ "input": 5.0,
173
+ "output": 25.0,
174
+ "cache_write_5m": 6.25,
175
+ "cache_write_1h": 10.0,
176
+ "cache_read": 0.5
177
+ },
178
+ {
179
+ "model": "Claude Sonnet 5",
180
+ "input": 2.0,
181
+ "output": 10.0,
182
+ "cache_write_5m": 2.5,
183
+ "cache_write_1h": 4.0,
184
+ "cache_read": 0.2
185
+ },
186
+ {
187
+ "model": "Claude Sonnet 4.6",
188
+ "input": 3.0,
189
+ "output": 15.0,
190
+ "cache_write_5m": 3.75,
191
+ "cache_write_1h": 6.0,
192
+ "cache_read": 0.3
193
+ },
194
+ {
195
+ "model": "Claude Sonnet 4.5",
196
+ "input": 3.0,
197
+ "output": 15.0,
198
+ "cache_write_5m": 3.75,
199
+ "cache_write_1h": 6.0,
200
+ "cache_read": 0.3
201
+ },
202
+ {
203
+ "model": "Claude Haiku 4.5",
204
+ "input": 1.0,
205
+ "output": 5.0,
206
+ "cache_write_5m": 1.25,
207
+ "cache_write_1h": 2.0,
208
+ "cache_read": 0.1
209
+ }
210
+ ],
211
+ "compiled_at": "2026-10-23T09:13:35+00:00",
212
+ "compiled_by": "python3 reader/testdata/compile_stub.py",
213
+ "crawl4ai_version": "0.9.2",
214
+ "page_hash": "580850369340da5cce9998a0b9f7f6c1dca3e15fc0280e596a0d1da301669a4c",
215
+ "last_ok": "2026-10-23T09:14:26+00:00",
216
+ "healed_at": null,
217
+ "source_discovery": {
218
+ "overview_url": "https://platform.claude.com/docs/en/models/overview.md",
219
+ "pricing_url": "https://platform.claude.com/docs/en/about-claude/pricing",
220
+ "checked_at": "2026-10-23",
221
+ "note": "Pricing link followed from official overview. Live public page compiled and validated with the documented compile-command fixture, not a live AI lane."
222
+ }
223
+ }
@@ -0,0 +1,67 @@
1
+ {
2
+ "type": "embedded_json",
3
+ "source": "__context__",
4
+ "rows": "context",
5
+ "paths": {
6
+ "name": "packument.name",
7
+ "latest_version": "packument.version",
8
+ "weekly_downloads": "downloads.-1.downloads",
9
+ "license": "packument.license",
10
+ "repository_url": "packument.repository"
11
+ },
12
+ "required": [
13
+ "name",
14
+ "latest_version",
15
+ "weekly_downloads",
16
+ "license",
17
+ "repository_url"
18
+ ],
19
+ "transforms": {
20
+ "weekly_downloads": "number"
21
+ },
22
+ "example": {
23
+ "name": "react",
24
+ "latest_version": "19.2.0",
25
+ "weekly_downloads": 42000000,
26
+ "license": "MIT",
27
+ "repository_url": "git+https://github.com/facebook/react.git"
28
+ },
29
+ "name": "npm-package",
30
+ "url_template": "https://www.npmjs.com/package/{name}",
31
+ "params": {
32
+ "name": "react"
33
+ },
34
+ "want": "Return package name, latest version, numeric weekly downloads, license, and repository URL.",
35
+ "sample_output": [
36
+ {
37
+ "name": "react",
38
+ "latest_version": "19.2.0",
39
+ "weekly_downloads": 42000000,
40
+ "license": "MIT",
41
+ "repository_url": "git+https://github.com/facebook/react.git"
42
+ }
43
+ ],
44
+ "compiled_at": "2026-10-23T09:14:06+00:00",
45
+ "compiled_by": "python3 reader/testdata/compile_stub.py",
46
+ "fetch_method": "crwl",
47
+ "crawl4ai_version": "0.9.2",
48
+ "page_hash": "92237ff233695cbf21545d7fddf95cc51981e1f2874e26310c8522e2eb8d126d",
49
+ "last_ok": "2026-10-23T09:14:06+00:00",
50
+ "healed_at": null,
51
+ "verification": {
52
+ "status": "live_zod_snapshot",
53
+ "url": "https://www.npmjs.com/package/zod",
54
+ "verified_at": "2026-10-23T09:45:06+00:00",
55
+ "page_hash": "9c860f2f365e31322abab76710ab3c85694cef021d13b5c8fc1e4ec61c820890",
56
+ "sample_output": [
57
+ {
58
+ "name": "zod",
59
+ "latest_version": "4.6.7",
60
+ "weekly_downloads": 144621339,
61
+ "license": "MIT",
62
+ "repository_url": "https://github.com/colinhacks/zod"
63
+ }
64
+ ],
65
+ "note": "Paths manually corrected against the public zod HTML fetched with bundled crwl -o all. Original compiled_by and compile sample retained. React live fetch and a real compile lane were not rerun in this fix."
66
+ }
67
+ }