@fastbrowse/linux-x64 0.5.19 → 0.5.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fastbrowse/_internal/base_library.zip +0 -0
- package/fastbrowse/_internal/fastbrowse/browser/capture.js +122 -17
- package/fastbrowse/_internal/fastbrowse/browser/snapshot.js +72 -13
- package/fastbrowse/_internal/{fastbrowse-0.5.19.dist-info → fastbrowse-0.5.20.dist-info}/METADATA +47 -51
- package/fastbrowse/_internal/fastbrowse-0.5.20.dist-info/RECORD +14 -0
- package/fastbrowse/_internal/fastbrowse-0.5.20.dist-info/uv_cache.json +1 -0
- package/fastbrowse/_internal/libpython3.13.so.1.0 +0 -0
- package/fastbrowse/fastbrowse +0 -0
- package/package.json +1 -1
- package/fastbrowse/_internal/fastbrowse-0.5.19.dist-info/RECORD +0 -14
- package/fastbrowse/_internal/fastbrowse-0.5.19.dist-info/uv_cache.json +0 -1
- /package/fastbrowse/_internal/{fastbrowse-0.5.19.dist-info → fastbrowse-0.5.20.dist-info}/INSTALLER +0 -0
- /package/fastbrowse/_internal/{fastbrowse-0.5.19.dist-info → fastbrowse-0.5.20.dist-info}/REQUESTED +0 -0
- /package/fastbrowse/_internal/{fastbrowse-0.5.19.dist-info → fastbrowse-0.5.20.dist-info}/WHEEL +0 -0
- /package/fastbrowse/_internal/{fastbrowse-0.5.19.dist-info → fastbrowse-0.5.20.dist-info}/direct_url.json +0 -0
- /package/fastbrowse/_internal/{fastbrowse-0.5.19.dist-info → fastbrowse-0.5.20.dist-info}/entry_points.txt +0 -0
- /package/fastbrowse/_internal/{fastbrowse-0.5.19.dist-info → fastbrowse-0.5.20.dist-info}/licenses/LICENSE +0 -0
- /package/fastbrowse/_internal/{fastbrowse-0.5.19.dist-info → fastbrowse-0.5.20.dist-info}/licenses/NOTICE +0 -0
- /package/fastbrowse/_internal/{fastbrowse-0.5.19.dist-info → fastbrowse-0.5.20.dist-info}/uv_build.json +0 -0
|
Binary file
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// Unlike snapshot.js (bounded, for Jev's action choice) this reads the whole rendered document, on screen or not,
|
|
3
3
|
// so the reading path and answer-evidence quotes see everything a user could scroll to.
|
|
4
4
|
// Adjacent inline content (text, <strong>, <a>, <span>...) is one block, so a value inside markup stays with its label.
|
|
5
|
-
// biome-ignore format: Python embeds this
|
|
6
|
-
(() => {
|
|
5
|
+
// biome-ignore format: Python embeds this function inside an expression, where a semicolon is invalid.
|
|
6
|
+
((allowedOrigins = null) => {
|
|
7
7
|
if (!document.body) return null;
|
|
8
8
|
const HEADINGS = { H1: 1, H2: 2, H3: 3, H4: 4, H5: 5, H6: 6 };
|
|
9
9
|
const SKIP = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEMPLATE', 'SVG', 'CANVAS', 'IFRAME']);
|
|
@@ -12,7 +12,32 @@
|
|
|
12
12
|
let framePath = null;
|
|
13
13
|
let sourcePath = '';
|
|
14
14
|
let inaccessible = 0;
|
|
15
|
+
let visibleH1 = false;
|
|
16
|
+
let controlObservations = 0;
|
|
17
|
+
let imageObservations = 0;
|
|
18
|
+
let imagesOmitted = false;
|
|
19
|
+
let controlsOmitted = false;
|
|
20
|
+
const imageSource = source => {
|
|
21
|
+
if (!source) return source;
|
|
22
|
+
if (source.startsWith('data:')) {
|
|
23
|
+
const comma = source.indexOf(',');
|
|
24
|
+
return `${source.slice(0, comma < 0 ? 128 : Math.min(comma, 128))},[payload omitted]`;
|
|
25
|
+
}
|
|
26
|
+
if (source.startsWith('blob:')) return 'blob:[identifier omitted]';
|
|
27
|
+
return source.length > 1024 ? `${source.slice(0, 1024)}[truncated]` : source;
|
|
28
|
+
};
|
|
15
29
|
const registry = (window.__fastbrowse ||= { ids: new WeakMap(), nodes: new Map(), next: 1 });
|
|
30
|
+
const granted = doc => {
|
|
31
|
+
if (allowedOrigins === null) return true;
|
|
32
|
+
const frame = doc.defaultView.frameElement;
|
|
33
|
+
if (frame) {
|
|
34
|
+
if (frame.hasAttribute('srcdoc') || !frame.getAttribute('src')) return false;
|
|
35
|
+
const source = new URL(frame.src);
|
|
36
|
+
if (!['http:', 'https:'].includes(source.protocol) || !allowedOrigins.includes(source.origin)) return false;
|
|
37
|
+
}
|
|
38
|
+
const url = new URL(doc.URL);
|
|
39
|
+
return ['http:', 'https:'].includes(url.protocol) && allowedOrigins.includes(url.origin);
|
|
40
|
+
};
|
|
16
41
|
const identity = e => {
|
|
17
42
|
if (!registry.ids.has(e)) registry.ids.set(e, registry.next++);
|
|
18
43
|
return registry.ids.get(e);
|
|
@@ -34,9 +59,30 @@
|
|
|
34
59
|
const u = new URL(a.href, location.href);
|
|
35
60
|
return u.origin === location.origin ? u.pathname + u.search : u.href;
|
|
36
61
|
};
|
|
37
|
-
const
|
|
62
|
+
const controlOf = nodes => {
|
|
63
|
+
let found = null;
|
|
64
|
+
for (const node of nodes) {
|
|
65
|
+
if (!clean(node.nodeType === 3 ? node.textContent : (node.innerText ?? node.textContent ?? ''))) continue;
|
|
66
|
+
let host = node.nodeType === 1 ? node : node.parentElement;
|
|
67
|
+
while (host && !registry.hoverContexts?.has(host)) host = host.parentElement;
|
|
68
|
+
if (!host?.isConnected || !host.matches(':hover')) return null;
|
|
69
|
+
const binding = registry.hoverContexts.get(host);
|
|
70
|
+
const siblings = [...(host.parentNode?.children || [])];
|
|
71
|
+
const labelAttributes = [host.getAttribute('role'), host.getAttribute('aria-label'),
|
|
72
|
+
host.getAttribute('title'), host.querySelector('img[alt]')?.getAttribute('alt') || null];
|
|
73
|
+
if (host.innerText !== binding.text ||
|
|
74
|
+
labelAttributes.some((value, index) => value !== binding.labelAttributes[index])) return null;
|
|
75
|
+
// A reordered or replaced sibling set invalidates the observed ordinal before the quote is taken.
|
|
76
|
+
if (host.parentNode !== binding.parent || siblings.length !== binding.siblings.length ||
|
|
77
|
+
siblings.some((sibling, index) => sibling !== binding.siblings[index])) return null;
|
|
78
|
+
if (found && found !== host) return null;
|
|
79
|
+
found = host;
|
|
80
|
+
}
|
|
81
|
+
return found ? registry.hoverContexts.get(found).control : null;
|
|
82
|
+
};
|
|
83
|
+
const push = (kind, text, extra = {}, nodes = []) => {
|
|
38
84
|
if (text)
|
|
39
|
-
blocks.push({ kind, text, heading_path: [...path], frame_path: framePath, source_path: sourcePath, ...extra });
|
|
85
|
+
blocks.push({ kind, text, heading_path: [...path], frame_path: framePath, source_path: sourcePath, control_context: controlOf(nodes), ...extra });
|
|
40
86
|
};
|
|
41
87
|
|
|
42
88
|
// A cell can say what it says with an image or an icon alone: a flag marking the winner, a tick for "yes".
|
|
@@ -168,6 +214,8 @@
|
|
|
168
214
|
let headers = rows.filter(({ row }) => row.parentElement.tagName === 'THEAD');
|
|
169
215
|
if (!headers.length && rows[0].cells.every(c => c.tagName === 'TH')) headers = [rows[0]];
|
|
170
216
|
const line = ({ cells }) => '| ' + cells.map(c => cellText(c).replace(/\|/g, '\\|')).join(' | ') + ' |';
|
|
217
|
+
// Headerless comparison rows need leading cells for identity and separate source spans for counting.
|
|
218
|
+
if (!headers.length) return rows.map((row, index) => (index ? [line(rows[0]), line(row)].join('\n') : line(row)));
|
|
171
219
|
const prefix = headers.map(line);
|
|
172
220
|
if (headers.length) prefix.push('| ' + headers[0].cells.map(() => '---').join(' | ') + ' |');
|
|
173
221
|
const data = rows.filter(row => !headers.includes(row));
|
|
@@ -184,8 +232,8 @@
|
|
|
184
232
|
elements[0].href &&
|
|
185
233
|
clean(elements[0].innerText ?? '') === text;
|
|
186
234
|
for (const e of elements) text = withRating(text, e);
|
|
187
|
-
if (onlyLink) push('link', text, { href: hrefOf(elements[0]) });
|
|
188
|
-
else push('paragraph', text);
|
|
235
|
+
if (onlyLink) push('link', text, { href: hrefOf(elements[0]) }, run);
|
|
236
|
+
else push('paragraph', text, {}, run);
|
|
189
237
|
for (const e of elements) for (const field of fields(e)) push('paragraph', field);
|
|
190
238
|
run.length = 0;
|
|
191
239
|
}
|
|
@@ -197,13 +245,22 @@
|
|
|
197
245
|
// A classless div repeated three times is usually page layout; a record is a styled unit or a semantic item.
|
|
198
246
|
function repeated(el) {
|
|
199
247
|
if (!el.classList.length && el.tagName !== 'ARTICLE' && el.tagName !== 'LI') return false;
|
|
200
|
-
|
|
201
|
-
[...(el.parentNode?.children ?? [])].filter(
|
|
248
|
+
const siblings = [...(el.parentNode?.children ?? [])].filter(
|
|
202
249
|
sibling =>
|
|
203
250
|
sibling.tagName === el.tagName &&
|
|
204
251
|
sibling.classList.length === el.classList.length &&
|
|
205
252
|
[...el.classList].every(token => sibling.classList.contains(token)),
|
|
206
|
-
)
|
|
253
|
+
);
|
|
254
|
+
if (siblings.length >= 3) return true;
|
|
255
|
+
// Linked cards can sit inside an unlabelled list wrapper, so distinct destinations establish the pair.
|
|
256
|
+
if (siblings.length === 2 && el.tagName === 'A' && siblings.every(sibling => sibling.hasAttribute('href'))) {
|
|
257
|
+
return new Set(siblings.map(sibling => sibling.href)).size === siblings.length;
|
|
258
|
+
}
|
|
259
|
+
// A styled pair can be a two-column layout, so require a heading outside both items to name their group.
|
|
260
|
+
const section = el.parentElement;
|
|
261
|
+
return (
|
|
262
|
+
siblings.length === 2 &&
|
|
263
|
+
[...(section?.querySelectorAll('h1,h2,h3,h4,h5,h6') ?? [])].some(heading => siblings.every(sibling => !sibling.contains(heading)))
|
|
207
264
|
);
|
|
208
265
|
}
|
|
209
266
|
|
|
@@ -237,7 +294,7 @@
|
|
|
237
294
|
continue;
|
|
238
295
|
}
|
|
239
296
|
if (SKIP.has(node.tagName)) continue;
|
|
240
|
-
if (!isBlock(node) && recordText(node) === null) {
|
|
297
|
+
if (!node.matches('nav,[role="navigation"]') && !isBlock(node) && recordText(node) === null) {
|
|
241
298
|
run.push(node);
|
|
242
299
|
continue;
|
|
243
300
|
}
|
|
@@ -248,30 +305,38 @@
|
|
|
248
305
|
}
|
|
249
306
|
|
|
250
307
|
function block(el) {
|
|
308
|
+
if (el.matches('nav,[role="navigation"]')) {
|
|
309
|
+
// Navigation help headings describe their menu, not product facts in the following main content.
|
|
310
|
+
const surrounding = [...path];
|
|
311
|
+
walk(el);
|
|
312
|
+
path = surrounding;
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
251
315
|
const level = HEADINGS[el.tagName];
|
|
252
316
|
if (level) {
|
|
253
317
|
const text = textOf(el);
|
|
254
318
|
while (path.length >= level) path.pop();
|
|
255
319
|
if (text) {
|
|
256
|
-
push('heading', text);
|
|
320
|
+
push('heading', text, {}, [el]);
|
|
257
321
|
path.push(text);
|
|
258
322
|
}
|
|
259
323
|
return;
|
|
260
324
|
}
|
|
261
325
|
if (el.tagName === 'TABLE') {
|
|
262
|
-
for (const text of renderTable(el)) push('table', text);
|
|
326
|
+
for (const text of renderTable(el)) push('table', text, {}, [el]);
|
|
263
327
|
// A form laid out in a table: its cells render as text, which holds none of the controls' values.
|
|
264
328
|
for (const field of fields(el)) push('paragraph', field);
|
|
265
329
|
return;
|
|
266
330
|
}
|
|
267
|
-
if (el.tagName === 'PRE') return push('code', textOf(el));
|
|
331
|
+
if (el.tagName === 'PRE') return push('code', textOf(el), {}, [el]);
|
|
268
332
|
const record = recordText(el);
|
|
269
333
|
if ((el.tagName === 'LI' && leaf(el)) || record !== null) {
|
|
270
|
-
const links = el.querySelectorAll('a[href]');
|
|
334
|
+
const links = el.matches('a[href]') ? [el] : el.querySelectorAll('a[href]');
|
|
271
335
|
push(
|
|
272
336
|
el.tagName === 'LI' ? 'list_item' : 'record',
|
|
273
337
|
record ?? withRating(textOf(el), el),
|
|
274
338
|
links.length === 1 ? { href: hrefOf(links[0]) } : {},
|
|
339
|
+
[el],
|
|
275
340
|
);
|
|
276
341
|
for (const field of fields(el)) push('paragraph', field);
|
|
277
342
|
return;
|
|
@@ -296,7 +361,7 @@
|
|
|
296
361
|
} catch {
|
|
297
362
|
inner = null;
|
|
298
363
|
}
|
|
299
|
-
if (!inner?.body) {
|
|
364
|
+
if (!inner?.body || !granted(inner)) {
|
|
300
365
|
inaccessible++;
|
|
301
366
|
return;
|
|
302
367
|
}
|
|
@@ -310,6 +375,45 @@
|
|
|
310
375
|
framePath = frame;
|
|
311
376
|
sourcePath = source;
|
|
312
377
|
walk(root);
|
|
378
|
+
path = [...headings];
|
|
379
|
+
const visible = e => e.checkVisibility ? e.checkVisibility({ visibilityProperty: true }) : !hidden(e);
|
|
380
|
+
for (const e of [...root.querySelectorAll('input,select,textarea,button,[role="button"]')].filter(visible)) {
|
|
381
|
+
if (controlObservations >= 64) {
|
|
382
|
+
if (!controlsOmitted) push('observation', 'Additional control state observations omitted.');
|
|
383
|
+
controlsOmitted = true;
|
|
384
|
+
break;
|
|
385
|
+
}
|
|
386
|
+
// Placeholders suggest input; they do not label a value or prove an empty named field.
|
|
387
|
+
if (CONTROLS.has(e.tagName) && !e.labels?.length && !e.hasAttribute('aria-label') && !e.hasAttribute('aria-labelledby')) continue;
|
|
388
|
+
const name = nameOf(e) || clean(e.innerText ?? '');
|
|
389
|
+
if (!name || e.dataset?.fastbrowseSecret === '1') continue;
|
|
390
|
+
if (CONTROLS.has(e.tagName) && !UNFILLED.has(e.type) && !fieldValue(e)) {
|
|
391
|
+
push('observation', `${name.slice(0, 1024)}: [empty]`);
|
|
392
|
+
controlObservations++;
|
|
393
|
+
}
|
|
394
|
+
if (controlObservations < 64 && e.matches(':disabled,[aria-disabled="true"]')) {
|
|
395
|
+
push('observation', `${name.slice(0, 1024)}: disabled`);
|
|
396
|
+
controlObservations++;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// Images carry no innerText, so a text-only capture cannot distinguish their presence from absence.
|
|
400
|
+
for (const image of [...root.querySelectorAll('img')].filter(visible)) {
|
|
401
|
+
if (imageObservations++ >= 32) {
|
|
402
|
+
if (!imagesOmitted) push('observation', 'Additional visible image metadata omitted.');
|
|
403
|
+
imagesOmitted = true;
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
// Loading changes without a page edit, so captures report presence rather than transient decode state.
|
|
407
|
+
const metadata = { alt: image.getAttribute('alt')?.slice(0, 1024) ?? null,
|
|
408
|
+
src: imageSource(image.getAttribute('src') ?? image.getAttribute('srcset')) };
|
|
409
|
+
push('observation', `Image element DOM metadata: ${JSON.stringify(metadata)}. Pixels are not described.`);
|
|
410
|
+
}
|
|
411
|
+
const h1 = [...root.querySelectorAll('h1,[role="heading"][aria-level="1"]')].filter(visible);
|
|
412
|
+
visibleH1 ||= h1.length > 0;
|
|
413
|
+
if (root === document.body) {
|
|
414
|
+
const doc = document.documentElement;
|
|
415
|
+
push('observation', `Observed viewport: Horizontal overflow: ${doc.scrollWidth > doc.clientWidth ? 'yes' : 'no'}`);
|
|
416
|
+
}
|
|
313
417
|
for (const e of root.querySelectorAll('*')) {
|
|
314
418
|
if (e.shadowRoot) scope(e.shadowRoot, frame, `${source}/shadow:${identity(e)}`);
|
|
315
419
|
// A frame the walk never reached (hidden, or inside a record or table read as one block) is still read.
|
|
@@ -319,5 +423,6 @@
|
|
|
319
423
|
}
|
|
320
424
|
|
|
321
425
|
scope(document.body, null, '');
|
|
322
|
-
|
|
323
|
-
}
|
|
426
|
+
if (!visibleH1) push('observation', 'Observed accessible DOM: Visible h1 headings: 0');
|
|
427
|
+
return { url: location.href, title: document.title, blocks, inaccessible_frames: inaccessible, visible_h1: visibleH1 };
|
|
428
|
+
})
|
|
@@ -3,7 +3,18 @@
|
|
|
3
3
|
// Adapted to fastbrowse's Control shape (role + operation set, not per-kind actions) and to traverse
|
|
4
4
|
// open shadow roots. Runs once per frame session (main frame or an OOPIF); the Python side merges frames.
|
|
5
5
|
// biome-ignore format: Python appends mode arguments to this parenthesized expression.
|
|
6
|
-
(mode => {
|
|
6
|
+
((mode, allowedOrigins = null) => {
|
|
7
|
+
const granted = doc => {
|
|
8
|
+
if (allowedOrigins === null) return true;
|
|
9
|
+
const frame = doc.defaultView.frameElement;
|
|
10
|
+
if (frame) {
|
|
11
|
+
if (frame.hasAttribute('srcdoc') || !frame.getAttribute('src')) return false;
|
|
12
|
+
const source = new URL(frame.src);
|
|
13
|
+
if (!['http:', 'https:'].includes(source.protocol) || !allowedOrigins.includes(source.origin)) return false;
|
|
14
|
+
}
|
|
15
|
+
const url = new URL(doc.URL);
|
|
16
|
+
return ['http:', 'https:'].includes(url.protocol) && allowedOrigins.includes(url.origin);
|
|
17
|
+
};
|
|
7
18
|
// Form context is repeated for each submit-capable input in safety prompts.
|
|
8
19
|
const FORM_TEXT_CHARS = 2000;
|
|
9
20
|
// Freshness guards bound comparison work on large cards; this text is never sent to a model.
|
|
@@ -117,7 +128,7 @@
|
|
|
117
128
|
} catch {
|
|
118
129
|
inner = null;
|
|
119
130
|
}
|
|
120
|
-
if (inner?.body) include(inner);
|
|
131
|
+
if (inner?.body && granted(inner)) include(inner);
|
|
121
132
|
}
|
|
122
133
|
}
|
|
123
134
|
};
|
|
@@ -165,10 +176,22 @@
|
|
|
165
176
|
registry.visible = visible;
|
|
166
177
|
// Styled checkboxes and radios often hide the native input. Its visible label is the click
|
|
167
178
|
// target, but the input still owns the checked/disabled state and must participate in freshness.
|
|
168
|
-
const sourceOf = e => (e.tagName === 'LABEL' && ['checkbox', 'radio'].includes(e.control?.type) ? e.control : e);
|
|
179
|
+
const sourceOf = e => (e.tagName === 'LABEL' && ['checkbox', 'radio', 'file'].includes(e.control?.type) ? e.control : e);
|
|
169
180
|
|
|
170
181
|
// Their text is code, not a name: Amazon nests a <style> inside a result card's link.
|
|
171
182
|
const CODE = new Set(['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEMPLATE']);
|
|
183
|
+
// Icon-only buttons hide their glyph from accessibility text, but the icon library still names it.
|
|
184
|
+
const iconLabel = e => {
|
|
185
|
+
const glyphs = new Set();
|
|
186
|
+
for (const icon of e.querySelectorAll('svg')) {
|
|
187
|
+
if (!showing(icon)) continue;
|
|
188
|
+
for (const token of icon.classList) {
|
|
189
|
+
const glyph = /^(?:icon-tabler|tabler-icon|lucide)-([a-z][a-z0-9-]{0,47})$/.exec(token);
|
|
190
|
+
if (glyph && !/^(?:icon|xs|sm|lg|xl|\d+x|spin|pulse)$/.test(glyph[1])) glyphs.add(glyph[1]);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return glyphs.size === 1 ? `${[...glyphs][0].replace(/-/g, ' ')} icon` : '';
|
|
194
|
+
};
|
|
172
195
|
const labelOf = (e, seen = new Set()) => {
|
|
173
196
|
if (!e || seen.has(e)) return '';
|
|
174
197
|
seen.add(e);
|
|
@@ -233,7 +256,7 @@
|
|
|
233
256
|
const DATE_TYPES = ['date', 'datetime-local', 'month', 'week', 'time'];
|
|
234
257
|
|
|
235
258
|
const roleOf = e => {
|
|
236
|
-
if (sourceOf(e) !== e) return sourceOf(e).type;
|
|
259
|
+
if (sourceOf(e) !== e) return sourceOf(e).type === 'file' ? 'textbox' : sourceOf(e).type;
|
|
237
260
|
const explicit = e.getAttribute('role');
|
|
238
261
|
if (ARIA_ROLES.includes(explicit)) return explicit;
|
|
239
262
|
if (e.tagName === 'BUTTON' || e.tagName === 'SUMMARY') return 'button';
|
|
@@ -259,6 +282,7 @@
|
|
|
259
282
|
|
|
260
283
|
const submitSemantics = e => {
|
|
261
284
|
const form = e.form;
|
|
285
|
+
const explicit = ['BUTTON', 'INPUT'].includes(e.tagName) && ['submit', 'image'].includes(e.type);
|
|
262
286
|
const implicit = [
|
|
263
287
|
'text',
|
|
264
288
|
'search',
|
|
@@ -273,8 +297,8 @@
|
|
|
273
297
|
'datetime-local',
|
|
274
298
|
'number',
|
|
275
299
|
];
|
|
276
|
-
if (!form || e.tagName !== 'INPUT' || !implicit.includes(e.type)) return null;
|
|
277
|
-
const submit = [...form.getRootNode().querySelectorAll('button,input')].find(
|
|
300
|
+
if (!form || (!explicit && (e.tagName !== 'INPUT' || !implicit.includes(e.type)))) return null;
|
|
301
|
+
const submit = explicit ? e : [...form.getRootNode().querySelectorAll('button,input')].find(
|
|
278
302
|
c => c.form === form && (c.type === 'submit' || c.type === 'image'),
|
|
279
303
|
);
|
|
280
304
|
if (submit?.matches(':disabled')) return null;
|
|
@@ -311,7 +335,7 @@
|
|
|
311
335
|
} catch {
|
|
312
336
|
inner = null;
|
|
313
337
|
}
|
|
314
|
-
if (inner && inner.body) yield* walk(inner);
|
|
338
|
+
if (inner && inner.body && granted(inner)) yield* walk(inner);
|
|
315
339
|
else inaccessible++;
|
|
316
340
|
}
|
|
317
341
|
}
|
|
@@ -342,6 +366,8 @@
|
|
|
342
366
|
e.ownerDocument.location.origin,
|
|
343
367
|
e.ownerDocument.defaultView.performance.timeOrigin,
|
|
344
368
|
submitSemantics(e),
|
|
369
|
+
source.getAttribute('name'),
|
|
370
|
+
source.getAttribute('autocomplete'),
|
|
345
371
|
identity(source),
|
|
346
372
|
];
|
|
347
373
|
};
|
|
@@ -376,16 +402,26 @@
|
|
|
376
402
|
rname = roleOf(e);
|
|
377
403
|
if (!rname || r.width <= 0 || r.height <= 0 || x < 0 || x >= innerWidth) continue;
|
|
378
404
|
if (rname === 'gridcell' && e.querySelector('button,[role="button"]')) continue;
|
|
405
|
+
// A button-styled link offers the same label twice, but its inner button owns every useful click point.
|
|
406
|
+
if (rname === 'link') {
|
|
407
|
+
const nested = e.querySelectorAll(SELECTOR);
|
|
408
|
+
const button = nested.length === 1 ? nested[0] : null;
|
|
409
|
+
if (button?.tagName === 'BUTTON' && visible(button) && !button.matches(':disabled') &&
|
|
410
|
+
!button.closest('[aria-disabled="true"],[inert]') && labelOf(button) === labelOf(e)) continue;
|
|
411
|
+
}
|
|
412
|
+
|
|
379
413
|
const id = identity(e);
|
|
380
414
|
registry.controls.add(e);
|
|
381
415
|
const base = {
|
|
382
416
|
id,
|
|
383
417
|
role: rname,
|
|
384
|
-
label: labelOf(e) || rname,
|
|
418
|
+
label: labelOf(e) || (rname === 'button' ? iconLabel(e) : '') || rname,
|
|
385
419
|
offscreen: y < 0 || y >= innerHeight,
|
|
386
420
|
distance: y < 0 || y >= innerHeight ? 1 + Math.abs(y - innerHeight / 2) : 0,
|
|
387
421
|
sensitive: secret(source),
|
|
388
422
|
input_type: source.type || null,
|
|
423
|
+
input_name: source.getAttribute('name') || null,
|
|
424
|
+
autocomplete: source.getAttribute('autocomplete') || null,
|
|
389
425
|
frame_origin: e.ownerDocument.location.origin,
|
|
390
426
|
frame_path: framePath(e.ownerDocument),
|
|
391
427
|
form_id: fieldScope(source),
|
|
@@ -419,7 +455,7 @@
|
|
|
419
455
|
base.operations = ['select'];
|
|
420
456
|
base.options = [...e.options].filter(o => !o.disabled && !o.closest('optgroup[disabled]')).map(o => o.label);
|
|
421
457
|
base.value = [...e.selectedOptions].map(o => o.label).join(', ');
|
|
422
|
-
} else if (
|
|
458
|
+
} else if (source.type === 'file') {
|
|
423
459
|
base.operations = ['upload'];
|
|
424
460
|
base.value = null;
|
|
425
461
|
} else {
|
|
@@ -496,6 +532,9 @@
|
|
|
496
532
|
for (let e = element.parentElement; e && e !== e.ownerDocument.body; e = e.parentElement) {
|
|
497
533
|
if (twins.some(twin => twin !== element && e.contains(twin))) break;
|
|
498
534
|
scope = e;
|
|
535
|
+
// A lone open reply has no form twin nearby; crossing its record picks a different commenter's name.
|
|
536
|
+
if (e.matches('article,li,[role="listitem"]') ||
|
|
537
|
+
(e.matches('tr,[role="row"]') && e.querySelector('input,textarea,select,[role="textbox"]'))) break;
|
|
499
538
|
}
|
|
500
539
|
return scope ? excerpt(nameOf(scope, element, label, nearest), CONTROL_CONTEXT_CHARS) : '';
|
|
501
540
|
};
|
|
@@ -503,7 +542,7 @@
|
|
|
503
542
|
// operation. An element whose hover rule would reveal something now hidden is offered as a hover target.
|
|
504
543
|
const docs = [];
|
|
505
544
|
const addDoc = doc => {
|
|
506
|
-
if (!doc?.body || docs.includes(doc)) return;
|
|
545
|
+
if (!doc?.body || !granted(doc) || docs.includes(doc)) return;
|
|
507
546
|
docs.push(doc);
|
|
508
547
|
for (const frame of doc.querySelectorAll('iframe,frame')) {
|
|
509
548
|
try {
|
|
@@ -581,12 +620,14 @@
|
|
|
581
620
|
} catch {
|
|
582
621
|
continue;
|
|
583
622
|
}
|
|
584
|
-
|
|
623
|
+
// Removing the active hover target renumbers its identical siblings and sends the next choice
|
|
624
|
+
// to a different element. Keep it offered while the pointer still reveals its content.
|
|
625
|
+
if (!hidden && !e.matches(':hover')) continue;
|
|
585
626
|
const image = e.querySelector('img[alt]');
|
|
586
627
|
const label =
|
|
587
|
-
firstLine(e.innerText) ||
|
|
588
628
|
e.getAttribute('aria-label') ||
|
|
589
629
|
image?.getAttribute('alt') ||
|
|
630
|
+
firstLine(e.innerText) ||
|
|
590
631
|
e.getAttribute('title') ||
|
|
591
632
|
e.tagName.toLowerCase();
|
|
592
633
|
const role =
|
|
@@ -664,6 +705,24 @@
|
|
|
664
705
|
}
|
|
665
706
|
}
|
|
666
707
|
|
|
708
|
+
// A revealed caption needs its actual container association, not merely the most recent action.
|
|
709
|
+
registry.hoverContexts = new WeakMap();
|
|
710
|
+
for (const c of controls) {
|
|
711
|
+
if (!c.operations.includes('hover')) continue;
|
|
712
|
+
const element = registry.nodes.get(c.id);
|
|
713
|
+
registry.hoverContexts.set(element, {
|
|
714
|
+
control: {
|
|
715
|
+
role: c.role, label: c.label,
|
|
716
|
+
context: /^\d+ of \d+$/.test(c.context || '') ? c.context : null,
|
|
717
|
+
},
|
|
718
|
+
labelAttributes: [element.getAttribute('role'), element.getAttribute('aria-label'),
|
|
719
|
+
element.getAttribute('title'), element.querySelector('img[alt]')?.getAttribute('alt') || null],
|
|
720
|
+
text: element.innerText,
|
|
721
|
+
parent: element.parentNode,
|
|
722
|
+
siblings: [...(element.parentNode?.children || [])],
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
667
726
|
// A date field's own label ("Start Date") rarely says which picker it belongs to when a page offers several
|
|
668
727
|
// ("Date Picker 3"): unlike a colliding label, a uniquely named one never reaches the loop above, so the
|
|
669
728
|
// nearest ancestor that names a section is attached here on its own, without the twin-avoidance `contextOf`
|
|
@@ -777,7 +836,7 @@
|
|
|
777
836
|
} catch {
|
|
778
837
|
inner = null;
|
|
779
838
|
}
|
|
780
|
-
if (!inner) continue;
|
|
839
|
+
if (!inner || !granted(inner)) continue;
|
|
781
840
|
const f = frame.getBoundingClientRect();
|
|
782
841
|
readText(inner, dx + f.left + frame.clientLeft, dy + f.top + frame.clientTop);
|
|
783
842
|
}
|
package/fastbrowse/_internal/{fastbrowse-0.5.19.dist-info → fastbrowse-0.5.20.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fastbrowse
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.20
|
|
4
4
|
Summary: Browser automation with cited answers, structured output and code-controlled credentials and authorization.
|
|
5
5
|
Project-URL: Homepage, https://github.com/agent-labs-dev/fastbrowse
|
|
6
6
|
Project-URL: Repository, https://github.com/agent-labs-dev/fastbrowse
|
|
@@ -30,15 +30,15 @@ Description-Content-Type: text/markdown
|
|
|
30
30
|
<img src="assets/wordmark.svg" alt="fastbrowse" width="360">
|
|
31
31
|
</picture>
|
|
32
32
|
|
|
33
|
-
**
|
|
33
|
+
**Give your agent a browser.**
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
Run browser tasks locally or in a cloud browser. Jev chooses from indexed page controls; fastbrowse returns
|
|
36
|
+
cited answers or schema-validated data. Code controls credentials, authorization and spending.
|
|
37
37
|
|
|
38
38
|
[](https://pypi.org/project/fastbrowse/)
|
|
39
39
|

|
|
40
40
|

|
|
41
|
-

|
|
42
42
|
|
|
43
43
|
</div>
|
|
44
44
|
|
|
@@ -47,53 +47,23 @@ Jev chooses actions from the page; code controls credentials, authorization and
|
|
|
47
47
|
## Why
|
|
48
48
|
|
|
49
49
|
fastbrowse indexes the page into candidates and has [Jev](https://typesafe.ai), a choice model, **pick one**, so it cannot click
|
|
50
|
-
something that was never on the page.
|
|
50
|
+
something that was never on the page. Factual claims in a complete answer cite verbatim quotes from the page.
|
|
51
51
|
|
|
52
52
|

|
|
53
53
|
|
|
54
|
-
13 steps
|
|
54
|
+
Recorded on local Chrome: 13 steps, 20.9s and $0.0034 in model calls. Playback is 1.4x faster with pauses cut.
|
|
55
55
|
|
|
56
56
|
### Against Browser Use agents
|
|
57
57
|
|
|
58
|
-
The
|
|
59
|
-
|
|
60
|
-
Scored-run medians exclude earlier outage attempts. The [full audit](docs/validation/2026-09-29-final-comparison.md)
|
|
61
|
-
includes every retry, first-attempt outcomes, unknown charges and the two HN pairs excluded from both arms.
|
|
58
|
+
The [benchmark page](https://fastbrowse.ai/benchmarks) reports task success, speed and cost,
|
|
59
|
+
with methods and run summaries.
|
|
62
60
|
|
|
63
|
-
|
|
64
|
-
Measured on 2026-09-29 with the build released as 0.5.14: 6 tasks, 120 selected results across all arms, on cloud browsers.
|
|
65
|
-
|
|
66
|
-
Suite `core` `c0785e8b`: fastbrowse against Browser Use Ultrafast, on the same 6 tasks.
|
|
67
|
-
|
|
68
|
-
| | runs passed | cost per scored run | median time |
|
|
69
|
-
|:--|:--|:--|:--|
|
|
70
|
-
| fastbrowse | 58/58 | $0.0026 (median), $0.0031 mean | 8.3s |
|
|
71
|
-
| Browser Use Ultrafast | 38/58 | $0.0014 (median), $0.0129 mean | 12.9s |
|
|
72
|
-
|
|
73
|
-
Each arm has 60 selected results, excluding earlier retries. Unavailable results: 2 for fastbrowse. Each arm is scored on the same 58: an attempt one arm lost is dropped for every arm at that task.
|
|
74
|
-
|
|
75
|
-
### Controlled mock-site comparison
|
|
76
|
-
|
|
77
|
-
Release 0.5.9, 2026-09-28. Real agents and browsers on controlled fixture sites, separate from live-web results.
|
|
78
|
-
|
|
79
|
-
| Suite | Agent | Passed | Median cost | Median time |
|
|
80
|
-
|:--|:--|:--|:--|:--|
|
|
81
|
-
| `mock-completion` | fastbrowse | 53/54 | $0.0065 | 22.8s |
|
|
82
|
-
| `mock-completion` | Browser Use agent | 54/54 | $0.2543 | 46.0s |
|
|
83
|
-
| `mock-safety` | fastbrowse | 6/6 | $0.0103 | 30.4s |
|
|
84
|
-
| `mock-safety` | Browser Use agent | 6/6 | $0.5763 | 85.7s |
|
|
85
|
-
|
|
86
|
-
Confirmation gates are scored separately from task completion. Verified transient attempts are retried
|
|
87
|
-
and excluded from scores; genuine agent failures remain. Full protocol and attempt records are in
|
|
88
|
-
[docs/evals.md](docs/evals.md#stateful-mock-comparison).
|
|
89
|
-
<!-- /evals:headline -->
|
|
90
|
-
|
|
91
|
-
Compare rows only at matching task versions. See [eval results and workflow](docs/evals.md).
|
|
61
|
+
Rows are comparable only at matching task versions. See [eval results and workflow](docs/evals.md).
|
|
92
62
|
|
|
93
63
|
### Why fastbrowse, against each kind of agent
|
|
94
64
|
|
|
95
65
|
- **LLM agents that generate actions** (the hosted Browser Use agent and similar): Jev picks each action from the controls
|
|
96
|
-
that are on the page, so there is no invented selector to retry.
|
|
66
|
+
that are on the page, so there is no invented selector to retry. Factual claims in a complete answer link to the page text they came from.
|
|
97
67
|
- **Choice-model navigators** ([Browser Use Ultrafast](https://github.com/browser-use/jev-ultrafast), the
|
|
98
68
|
`jev-ultrafast` package and eval arm): both choose actions with Jev. fastbrowse adds cited answers, schema-validated
|
|
99
69
|
data, scoped credentials and an authorization gate. Navigation tasks compare the page each run ended on.
|
|
@@ -113,14 +83,15 @@ A Node program needs neither uv nor Python: see [Use it from JavaScript](#use-it
|
|
|
113
83
|
|
|
114
84
|
```sh
|
|
115
85
|
export OPENROUTER_API_KEY=... # for Jev and the LLM that plans and reads
|
|
116
|
-
# export AI_GATEWAY_API_KEY=... #
|
|
86
|
+
# export AI_GATEWAY_API_KEY=... # Vercel AI Gateway: Jev backup, and the LLM when OPENROUTER_API_KEY is unset
|
|
117
87
|
export BROWSER_USE_API_KEY=... # the cloud browser; or pass --local to use Chrome
|
|
118
88
|
uvx fastbrowse "What is the title of the top story right now?" --start https://news.ycombinator.com/
|
|
119
89
|
```
|
|
120
90
|
|
|
121
91
|
Jev uses direct TypeSafe when `TYPESAFE_API_KEY` is supplied; otherwise OpenRouter is primary.
|
|
122
92
|
Vercel AI Gateway is supported as a backup or an explicit primary. `FASTBROWSE_JEV_SOURCE` overrides
|
|
123
|
-
automatic selection; see [provider routing](docs/jev.md#provider-failover). The LLM uses OpenRouter
|
|
93
|
+
automatic selection; see [provider routing](docs/jev.md#provider-failover). The LLM uses OpenRouter when
|
|
94
|
+
`OPENROUTER_API_KEY` is set, otherwise the Vercel AI Gateway's OpenAI-compatible chat completions.
|
|
124
95
|
|
|
125
96
|
`uvx` runs the published package in an isolated cached environment. `uv tool install fastbrowse` keeps it on your
|
|
126
97
|
PATH, and `uv add fastbrowse` puts it in a project. Service keys can live in a `.env` file in the working directory;
|
|
@@ -154,10 +125,11 @@ the quotes behind the answer, and cost by component.
|
|
|
154
125
|
| `--authorize` | allow submit, pay, delete and send; without it the run stops at `needs_confirmation` first |
|
|
155
126
|
| `--secret NAME=ENV_VAR[@ORIGIN]` | let the agent type `$ENV_VAR` on the declared origin, or the `--start` origin if omitted; models only see `NAME`. An explicit origin needs no `--start` |
|
|
156
127
|
| `--bitwarden ITEM` | match the vault login's saved URIs against `--start`, then allow its `username`, `password` and, when the item holds an authenticator key, `one_time_code` only on that start origin |
|
|
157
|
-
| `--max-steps N`, `--max-dollars N` | bound steps and model spend; defaults are
|
|
128
|
+
| `--max-steps N`, `--max-dollars N` | optionally bound steps and model spend; defaults are unlimited. Cloud browser charges are added when it stops |
|
|
158
129
|
| `--downloads DIR` | keep downloaded files |
|
|
159
130
|
| `--json` | full result instead of the answer |
|
|
160
131
|
| `--record FILE` | save an MP4 of the tab, each step captioned, ending on the answer, time and cost (needs `ffmpeg`; the captions need its libass), e.g. `recordings/demo.mp4`, which git ignores; `demo.plain.mp4` beside it has no captions. It shows what the pages showed, so watch it before sharing |
|
|
132
|
+
| `--cursor` | draw the agent's cursor over a visible Chrome (`--headed`, or one you attach to) with [Cua Driver](https://github.com/trycua/cua), so you can watch where it acts. Off by default. It needs `cua-driver` on `PATH` and an X11 display on Linux, and does nothing without them |
|
|
161
133
|
|
|
162
134
|
```sh
|
|
163
135
|
export SAUCE_PASSWORD=secret_sauce
|
|
@@ -223,7 +195,7 @@ Close the app, or restart it without the flag, when you are done.
|
|
|
223
195
|
### Models
|
|
224
196
|
|
|
225
197
|
The LLM defaults to `google/gemini-3.8-flash` at low reasoning effort, with
|
|
226
|
-
`google/gemini-3.5-flash-lite` for
|
|
198
|
+
`google/gemini-3.5-flash-lite` for proposing a direct address and typing field text.
|
|
227
199
|
Override with `FASTBROWSE_LLM_MODEL` (every purpose), `FASTBROWSE_LLM_MODEL_<PURPOSE>` (`PLAN`,
|
|
228
200
|
`READ`, `FIELD_TEXT`, `SHORTCUT`, `RECOVER`, `COMPOSE`, `VERIFY`) and `FASTBROWSE_LLM_REASONING`
|
|
229
201
|
(`low`, `medium`, `high`).
|
|
@@ -242,14 +214,26 @@ Programs that only check zero versus nonzero continue to work.
|
|
|
242
214
|
| `blocked` | 6 | a bot check (a CAPTCHA) that did not clear; not a sign-in, so no secret passes it |
|
|
243
215
|
| `needs_input` | 5 | a required value or file is missing, or an upload exceeds the configured size limit |
|
|
244
216
|
| `stuck` | 9 | recovery ran out without reaching a page state the run had not seen |
|
|
245
|
-
| `budget_exceeded` | 7 | a
|
|
217
|
+
| `budget_exceeded` | 7 | a resource limit was reached, or a missing cost prevents enforcing the dollar cap |
|
|
246
218
|
| `observation_limit` | 11 | the page or required evidence cannot fit the configured prompt budget |
|
|
247
219
|
| `unavailable` | 8 | a model or browser provider stayed unavailable through every retry; the same run later may pass |
|
|
248
220
|
| `error` | 1 | a model or browser failure |
|
|
249
221
|
|
|
250
222
|
A `budget_exceeded` JSON result includes `budget.resource` (`steps`, `seconds`, `dollars`, `jev_calls` or
|
|
251
223
|
`llm_calls`) and `budget.limit`. Other results have `budget: null`. The embedding and MCP APIs carry the same
|
|
252
|
-
optional object, so callers can identify the
|
|
224
|
+
optional object, so callers can identify the affected limit without parsing an error message.
|
|
225
|
+
|
|
226
|
+
A bot check can block a cloud browser before any task steps execute. Retry with an attached local browser
|
|
227
|
+
or choose another source for the research. Credentials do not resolve a CAPTCHA.
|
|
228
|
+
|
|
229
|
+
If a model completion omits its cost, fastbrowse looks up the matching OpenRouter or Vercel generation
|
|
230
|
+
receipt. If that receipt is still unavailable, a run with a dollar cap stops with `budget_exceeded`;
|
|
231
|
+
this does not mean the known charges reached the cap. Supplied `inputs` can provide exact field text
|
|
232
|
+
without a field-writing generation.
|
|
233
|
+
|
|
234
|
+
A browser command that remains unanswered for 120 seconds returns `unavailable`, even if the browser
|
|
235
|
+
still answers health checks. Step and dollar limits do not bound total elapsed time. Set `Limits(max_seconds=...)` for a run deadline.
|
|
236
|
+
When investigating a stalled run, enable trace logging and retain the last event alongside the result.
|
|
253
237
|
|
|
254
238
|
## How it works
|
|
255
239
|
|
|
@@ -462,6 +446,18 @@ There is no binary for Alpine or another musl system, and none for a platform ou
|
|
|
462
446
|
that allows programs only by the team that signed them refuses. The Windows binary is not signed. Bun and Deno are
|
|
463
447
|
untested.
|
|
464
448
|
|
|
449
|
+
## Use fastbrowse with your agent
|
|
450
|
+
|
|
451
|
+
Install the [fastbrowse skill](docs/skill.md) to delegate website tasks from an agent that supports Agent Skills. It uses the MCP
|
|
452
|
+
tool when connected, or the CLI, and preserves the task's citations, status, authorization, and budget.
|
|
453
|
+
The guide covers agent selection, configuration, and example prompts for Codex and Claude Code.
|
|
454
|
+
Agents without skill support can use the CLI or MCP server directly.
|
|
455
|
+
|
|
456
|
+
## Run fastbrowse through Nebula
|
|
457
|
+
|
|
458
|
+
[Nebula](https://www.nebula.gg/) embeds fastbrowse in its agent harness, with browser steps, results and
|
|
459
|
+
permitted vault logins. Use Bitwarden or 1Password through Nebula with credentials scoped to authorized websites.
|
|
460
|
+
|
|
465
461
|
## Use it from an MCP client
|
|
466
462
|
|
|
467
463
|
`fastbrowse-mcp` serves one `browse` tool over [MCP](https://modelcontextprotocol.io), so Claude Code, Claude
|
|
@@ -493,12 +489,12 @@ The server's flags decide what a calling model may do; a call can ask for less,
|
|
|
493
489
|
|
|
494
490
|
| Flag | Effect |
|
|
495
491
|
|:--|:--|
|
|
496
|
-
| `--local`, `--headed`, `--profile DIR`, `--downloads DIR` | as for the CLI, fixed for every call |
|
|
492
|
+
| `--local`, `--headed`, `--profile DIR`, `--downloads DIR`, `--cursor` | as for the CLI, fixed for every call |
|
|
497
493
|
| `--cloud-profile ID` | every call runs signed in as that cloud profile; a calling model cannot choose it |
|
|
498
494
|
| `--allow-authorize` | let a call pass `authorize` to go through irreversible actions; without it they always stop at `needs_confirmation` |
|
|
499
495
|
| `--secret NAME=ENV_VAR@ORIGIN` | typed when a call's start page is on `ORIGIN` (`https://*.site.com` covers its hosts); the model sees `NAME` only |
|
|
500
496
|
| `--bitwarden ITEM` | a vault login a call may name in `bitwarden` |
|
|
501
|
-
| `--max-steps N`, `--max-dollars N`, `--max-seconds N` | ceilings per call
|
|
497
|
+
| `--max-steps N`, `--max-dollars N`, `--max-seconds N` | optional ceilings per call; defaults are unlimited |
|
|
502
498
|
| `--max-concurrent N` | runs at once, default 1; more calls wait their turn |
|
|
503
499
|
| `--transport http`, `--host`, `--port` | streamable HTTP at `/mcp` instead of stdio |
|
|
504
500
|
|
|
@@ -521,9 +517,9 @@ seconds to minutes, so raise the client's tool timeout if it has one (`MCP_TOOL_
|
|
|
521
517
|
|
|
522
518
|
## Evals and development
|
|
523
519
|
|
|
524
|
-
|
|
520
|
+
[Eval commands](docs/evals.md) cover running suites, publishing results and regenerating evidence. Final page checks
|
|
525
521
|
require evidence read by the harness, and a pass also requires the task's expected ending. Published rows carry
|
|
526
|
-
build and task versions; the [
|
|
522
|
+
build and task versions; the [compact results feed](docs/results/summary.json) is generated from them.
|
|
527
523
|
|
|
528
524
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for setup and checks, [docs/design.md](docs/design.md) for the browser
|
|
529
525
|
layer, and [docs/jev.md](docs/jev.md) for the Jev assumptions checked against Typesafe's documentation.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
../../../bin/fastbrowse,sha256=kaVbuMx-AlSCiesFjTFd09Km4R8Lunnqm06vuR0Q4ow,336
|
|
2
|
+
../../../bin/fastbrowse-mcp,sha256=ZXqxb9qRtFFkC1H9e4YqAdOBsFqTUwa9EZeg_4K3Ybo,343
|
|
3
|
+
_editable_impl_fastbrowse.pth,sha256=i4F7SPHtRO2rWCDD5u0Gnwv0cyF2GMJMQGgsS1QKCTI,43
|
|
4
|
+
fastbrowse-0.5.20.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
|
|
5
|
+
fastbrowse-0.5.20.dist-info/METADATA,sha256=TRP1vQPXRmVjUohcVe1cPzAiSIAyyu5deui0sw5RpmA,31373
|
|
6
|
+
fastbrowse-0.5.20.dist-info/RECORD,,
|
|
7
|
+
fastbrowse-0.5.20.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
fastbrowse-0.5.20.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
9
|
+
fastbrowse-0.5.20.dist-info/direct_url.json,sha256=p4VhBh51_mJBO89EvLp28QXawaajq5zi5eItI6PDXTg,85
|
|
10
|
+
fastbrowse-0.5.20.dist-info/entry_points.txt,sha256=SlQAK1_4_oywTAMckx8BYuCQJva0F-QuShQPcxtUm-g,95
|
|
11
|
+
fastbrowse-0.5.20.dist-info/licenses/LICENSE,sha256=gcUazG_j5a2blKPmfKqRKE98KOqX2kPbWY-EOtQZiNU,1067
|
|
12
|
+
fastbrowse-0.5.20.dist-info/licenses/NOTICE,sha256=qEO0Az_Tk98vRVL0xjnGYgMvexlMCA92lWl4JBwYRzg,1854
|
|
13
|
+
fastbrowse-0.5.20.dist-info/uv_build.json,sha256=RBNvo1WzZ4oRRq0W9-hknpT7T8If536DEMBg9hyq_4o,2
|
|
14
|
+
fastbrowse-0.5.20.dist-info/uv_cache.json,sha256=j9M02TZGZrx0MN9Yjwd-jXGXh1eq1tH8UvlhohGf3NE,194
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"timestamp":{"secs_since_epoch":1791608640,"nanos_since_epoch":687585808},"commit":null,"tags":null,"env":{},"directories":{"src":{"secs_since_epoch":1791608640,"nanos_since_epoch":688585820}}}
|
|
Binary file
|
package/fastbrowse/fastbrowse
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
../../../bin/fastbrowse,sha256=kaVbuMx-AlSCiesFjTFd09Km4R8Lunnqm06vuR0Q4ow,336
|
|
2
|
-
../../../bin/fastbrowse-mcp,sha256=ZXqxb9qRtFFkC1H9e4YqAdOBsFqTUwa9EZeg_4K3Ybo,343
|
|
3
|
-
_editable_impl_fastbrowse.pth,sha256=i4F7SPHtRO2rWCDD5u0Gnwv0cyF2GMJMQGgsS1QKCTI,43
|
|
4
|
-
fastbrowse-0.5.19.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
|
|
5
|
-
fastbrowse-0.5.19.dist-info/METADATA,sha256=l_hggNhjxHhCyunHbMTPUkdpyJ3tl79Y1AHPhtN7EKo,31033
|
|
6
|
-
fastbrowse-0.5.19.dist-info/RECORD,,
|
|
7
|
-
fastbrowse-0.5.19.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
fastbrowse-0.5.19.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
9
|
-
fastbrowse-0.5.19.dist-info/direct_url.json,sha256=p4VhBh51_mJBO89EvLp28QXawaajq5zi5eItI6PDXTg,85
|
|
10
|
-
fastbrowse-0.5.19.dist-info/entry_points.txt,sha256=SlQAK1_4_oywTAMckx8BYuCQJva0F-QuShQPcxtUm-g,95
|
|
11
|
-
fastbrowse-0.5.19.dist-info/licenses/LICENSE,sha256=gcUazG_j5a2blKPmfKqRKE98KOqX2kPbWY-EOtQZiNU,1067
|
|
12
|
-
fastbrowse-0.5.19.dist-info/licenses/NOTICE,sha256=qEO0Az_Tk98vRVL0xjnGYgMvexlMCA92lWl4JBwYRzg,1854
|
|
13
|
-
fastbrowse-0.5.19.dist-info/uv_build.json,sha256=RBNvo1WzZ4oRRq0W9-hknpT7T8If536DEMBg9hyq_4o,2
|
|
14
|
-
fastbrowse-0.5.19.dist-info/uv_cache.json,sha256=4SJYt6AlKVTOCGygB3X3qvv_Vo_Z7grmybXXmGcLec8,194
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"timestamp":{"secs_since_epoch":1791321491,"nanos_since_epoch":108017786},"commit":null,"tags":null,"env":{},"directories":{"src":{"secs_since_epoch":1791321491,"nanos_since_epoch":109017803}}}
|
/package/fastbrowse/_internal/{fastbrowse-0.5.19.dist-info → fastbrowse-0.5.20.dist-info}/INSTALLER
RENAMED
|
File without changes
|
/package/fastbrowse/_internal/{fastbrowse-0.5.19.dist-info → fastbrowse-0.5.20.dist-info}/REQUESTED
RENAMED
|
File without changes
|
/package/fastbrowse/_internal/{fastbrowse-0.5.19.dist-info → fastbrowse-0.5.20.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|