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.
- package/CHANGELOG.md +21 -0
- package/CREDITS.md +18 -0
- package/LICENSE +21 -0
- package/README.md +159 -2
- package/agents/aun-browser-runner.md +48 -0
- package/bin/lean-browser-use.mjs +3 -0
- package/briefs/browser-read.md +63 -0
- package/briefs/helper-read.md +63 -0
- package/docs/PROVENANCE.md +32 -0
- package/docs/benchmark.md +91 -0
- package/docs/guards.md +54 -0
- package/docs/how-it-works.md +27 -0
- package/docs/install.md +78 -0
- package/docs/measure.md +53 -0
- package/docs/reader.md +39 -0
- package/docs/search.md +16 -0
- package/hooks/batch-nudge.mjs +14 -0
- package/hooks/batch-nudge.sh +8 -0
- package/hooks/common.mjs +71 -0
- package/hooks/hooks.settings.json +58 -0
- package/hooks/pick-guard.mjs +27 -0
- package/hooks/pick-guard.sh +8 -0
- package/hooks/retry-guard.mjs +69 -0
- package/hooks/retry-guard.sh +8 -0
- package/llms.txt +40 -0
- package/package.json +61 -4
- package/proof/benchmark.json +166 -0
- package/proof/results.json +241 -0
- package/reader/README.md +48 -0
- package/reader/crawl_helper.py +256 -0
- package/reader/reader +14 -0
- package/reader/reader_base.py +106 -0
- package/reader/reader_compile.py +106 -0
- package/reader/reader_crawl.py +67 -0
- package/reader/reader_extract.py +261 -0
- package/reader/reader_fetch.py +148 -0
- package/reader/reader_runtime.py +199 -0
- package/reader/reader_test_harness.py +173 -0
- package/reader/reader_test_support.py +214 -0
- package/reader/specs/claude-pricing.json +223 -0
- package/reader/specs/npm-package.json +67 -0
- package/reader/test_reader.py +345 -0
- package/reader/test_reader_1.py +309 -0
- package/reader/test_reader_2.py +348 -0
- package/reader/test_reader_3.py +254 -0
- package/reader/test_reader_contract.py +57 -0
- package/reader/testdata/claude-pricing.html +4 -0
- package/reader/testdata/compile_stub.py +24 -0
- package/reader/testdata/npm-react.html +4 -0
- package/reader/testdata/npm-schema.json +14 -0
- package/reader/testdata/npm-zod.html +4 -0
- package/reader/testdata/pricing-schema.json +24 -0
- package/searxng/README.md +29 -0
- package/searxng/compose.yml +21 -0
- package/searxng/core-config/settings.yml +26 -0
- package/searxng/deploy.sh +36 -0
- package/searxng/searx +73 -0
- package/searxng/test_searx.py +107 -0
- package/searxng/test_searx_offline.py +67 -0
- package/skills/lean-browser-use/SKILL.md +35 -0
- package/skills/lean-browser-use/references/cleanup.md +17 -0
- package/skills/lean-browser-use/references/cost-model.md +11 -0
- package/skills/lean-browser-use/references/draft-offline.md +9 -0
- package/skills/lean-browser-use/references/failure-modes.md +16 -0
- package/skills/lean-browser-use/references/logged-in-run.md +23 -0
- package/skills/lean-browser-use/references/packet-principle.md +15 -0
- package/skills/lean-browser-use/references/route-by-job.md +18 -0
- package/skills/lean-browser-use/references/runner-rules.md +27 -0
- package/skills/lean-browser-use/references/verify.md +9 -0
- package/src/cli.mjs +42 -0
- package/src/doctor.mjs +60 -0
- package/src/install.mjs +116 -0
- package/src/measure.mjs +64 -0
- package/src/paths.mjs +51 -0
- package/src/uninstall.mjs +68 -0
- package/src/util.mjs +35 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { configDir, loadManifest, manifestPath, safePath, noSymlinks } from './paths.mjs';
|
|
4
|
+
import { readSettings, ownedHook } from './install.mjs';
|
|
5
|
+
import { exists, sha256, atomicWrite, refusal } from './util.mjs';
|
|
6
|
+
|
|
7
|
+
export function uninstall(options = {}) {
|
|
8
|
+
const cfg = noSymlinks(configDir(options));
|
|
9
|
+
const manifest = loadManifest(cfg);
|
|
10
|
+
const settings = readSettings(cfg);
|
|
11
|
+
const removals = [], edited = [];
|
|
12
|
+
for (const item of manifest.files) {
|
|
13
|
+
const file = safePath(cfg, item.path);
|
|
14
|
+
if (!exists(file)) continue;
|
|
15
|
+
if (!fs.lstatSync(file).isFile()) throw refusal('Refused non-file manifest entry.');
|
|
16
|
+
(sha256(fs.readFileSync(file)) === item.sha256 ? removals : edited).push(item);
|
|
17
|
+
}
|
|
18
|
+
let settingsChanged = false;
|
|
19
|
+
for (const [event, entries] of Object.entries(settings.hooks || {})) {
|
|
20
|
+
const kept = [];
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const hooks = entry.hooks.filter(hook => !ownedHook(hook));
|
|
23
|
+
if (hooks.length !== entry.hooks.length) settingsChanged = true;
|
|
24
|
+
if (hooks.length || !entry.hooks.length) kept.push({ ...entry, hooks });
|
|
25
|
+
}
|
|
26
|
+
settings.hooks[event] = kept;
|
|
27
|
+
}
|
|
28
|
+
const removalSet = new Set(removals.map(f => f.path));
|
|
29
|
+
const directories = [];
|
|
30
|
+
for (const dir of [...manifest.directories].sort((a, b) => b.length - a.length)) {
|
|
31
|
+
const full = safePath(cfg, dir);
|
|
32
|
+
if (!exists(full)) continue;
|
|
33
|
+
if (!fs.lstatSync(full).isDirectory()) throw refusal('Refused non-directory manifest entry.');
|
|
34
|
+
const emptyAfter = fs.readdirSync(full).every(name => {
|
|
35
|
+
const child = `${dir}/${name}`;
|
|
36
|
+
return removalSet.has(child) || directories.includes(child) || child === 'lean-browser-use/manifest.json';
|
|
37
|
+
});
|
|
38
|
+
if (emptyAfter) directories.push(dir);
|
|
39
|
+
}
|
|
40
|
+
for (const item of removals) console.log(`Remove ${item.path}`);
|
|
41
|
+
for (const item of edited) console.log(`Keep edited ${item.path}`);
|
|
42
|
+
if (settingsChanged) console.log('Remove owned hook entries from settings.json.');
|
|
43
|
+
for (const dir of directories) console.log(`Remove empty directory ${dir}`);
|
|
44
|
+
const removeConfig = manifest.configDirectoryCreated && fs.readdirSync(cfg).every(name => directories.includes(name));
|
|
45
|
+
console.log('Remove lean-browser-use/manifest.json last.');
|
|
46
|
+
if (removeConfig) console.log('Remove empty config directory.');
|
|
47
|
+
if (manifest.manualHooks) console.log('Manual step: remove any hook entries pasted elsewhere by hand.');
|
|
48
|
+
if (manifest.backupWritten) console.log('Saved settings backup remains at settings.json.lean-browser-use.bak.');
|
|
49
|
+
if (options.dry) return edited.length ? 3 : 0;
|
|
50
|
+
for (const item of removals) {
|
|
51
|
+
const file = safePath(cfg, item.path);
|
|
52
|
+
// Recheck both the object and its bytes immediately before unlinking.
|
|
53
|
+
if (!fs.lstatSync(file).isFile() || sha256(fs.readFileSync(file)) !== item.sha256) throw refusal('File changed during uninstall; preserved.');
|
|
54
|
+
fs.unlinkSync(file);
|
|
55
|
+
}
|
|
56
|
+
if (settingsChanged) atomicWrite(noSymlinks(path.join(cfg, 'settings.json')), `${JSON.stringify(settings, null, 2)}\n`);
|
|
57
|
+
const removeEmpty = dir => {
|
|
58
|
+
const full = safePath(cfg, dir);
|
|
59
|
+
if (exists(full) && fs.lstatSync(full).isDirectory() && fs.readdirSync(full).length === 0) fs.rmdirSync(full);
|
|
60
|
+
};
|
|
61
|
+
for (const dir of directories) if (dir !== 'lean-browser-use') removeEmpty(dir);
|
|
62
|
+
const file = noSymlinks(manifestPath(cfg));
|
|
63
|
+
if (!fs.lstatSync(file).isFile()) throw refusal('Manifest changed during uninstall.');
|
|
64
|
+
fs.unlinkSync(file);
|
|
65
|
+
if (directories.includes('lean-browser-use')) removeEmpty('lean-browser-use');
|
|
66
|
+
if (removeConfig && fs.readdirSync(noSymlinks(cfg)).length === 0) fs.rmdirSync(noSymlinks(cfg));
|
|
67
|
+
return edited.length ? 3 : 0;
|
|
68
|
+
}
|
package/src/util.mjs
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { randomBytes, createHash } from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
export const sha256 = value => createHash('sha256').update(value).digest('hex');
|
|
8
|
+
export const readJSON = file => JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
9
|
+
export const exists = file => { try { fs.lstatSync(file); return true; } catch (e) { if (e.code === 'ENOENT') return false; throw e; } };
|
|
10
|
+
export function atomicWrite(file, data, mode = 0o600) {
|
|
11
|
+
const tmp = `${file}.${randomBytes(8).toString('hex')}.tmp`;
|
|
12
|
+
try {
|
|
13
|
+
fs.writeFileSync(tmp, data, { mode, flag: 'wx' });
|
|
14
|
+
fs.renameSync(tmp, file);
|
|
15
|
+
} finally { if (exists(tmp)) fs.unlinkSync(tmp); }
|
|
16
|
+
}
|
|
17
|
+
export function walk(dir) {
|
|
18
|
+
if (!exists(dir)) return [];
|
|
19
|
+
return fs.readdirSync(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))
|
|
20
|
+
.flatMap(e => e.isDirectory() ? walk(path.join(dir, e.name)) : e.isFile() ? [path.join(dir, e.name)] : []);
|
|
21
|
+
}
|
|
22
|
+
export function refusal(message) { const error = new Error(message); error.exitCode = 2; return error; }
|
|
23
|
+
export const shellQuote = text => `'${String(text).replaceAll("'", "'\\''")}'`;
|
|
24
|
+
|
|
25
|
+
// Regular files also work in sandboxes that restrict child-process pipe handles.
|
|
26
|
+
export function captured(command, args = [], options = {}) {
|
|
27
|
+
const dir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'lbu-process-'));
|
|
28
|
+
const input = path.join(dir, 'stdin'), output = path.join(dir, 'stdout'), errors = path.join(dir, 'stderr');
|
|
29
|
+
fs.writeFileSync(input, options.input ?? '');
|
|
30
|
+
const fds = [fs.openSync(input, 'r'), fs.openSync(output, 'w'), fs.openSync(errors, 'w')];
|
|
31
|
+
try {
|
|
32
|
+
const result = spawnSync(command, args, { ...options, input: undefined, stdio: fds });
|
|
33
|
+
return { ...result, stdout: fs.readFileSync(output, 'utf8'), stderr: fs.readFileSync(errors, 'utf8') };
|
|
34
|
+
} finally { for (const fd of fds) fs.closeSync(fd); fs.rmSync(dir, { recursive: true, force: true }); }
|
|
35
|
+
}
|