Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,47 @@ serves an Apple Silicon `llama.cpp` server; Kev (`node skills/jev-browser/bin/je
tier table with every measured number — scores, latency, disk, memory, each backend's `goal_done`
bar and the known holes — lives in [SKILL.md](skills/jev-browser/SKILL.md#judging-tiers).

A local tier is one command: `setup` fetches whatever is missing through the launcher itself, starts
the server in the background, writes `baseUrl` and the placeholder key into your config, then proves
the endpoint works by asking it one real question.

```bash
jev-browser setup local-readout # llama.cpp + the registry GGUF; prints the log path and the pid
jev-browser setup status # installed / running / configured, per local tier
jev-browser setup stop local-readout
```

`jev-browser setup kev` is the same for the accuracy tier and needs `uv` as well (it clones the Kev
checkout and runs `uv sync --extra serve`). Both write under `~/.jev-browser/`, never into this repo.
The manual route still exists — start `bin/jev-local.mjs` yourself and export the two variables it
prints — and `jev-browser tier use <tier> --persist` still stores that line's values for you.

## WebUI

```bash
node skills/jev-browser/bin/jev-webui.mjs # prints http://127.0.0.1:8765/
node skills/jev-browser/bin/jev-webui.mjs --port 9000 --open
```

One page over the same skill, not a second implementation: the panels call the same `lib/tiers.mjs`,
`lib/doctor.mjs` and `lib/config.mjs` the CLI does — so they cannot disagree with `tier status`,
`doctor` or `config show` — and edits go to the same `~/.config/jev-browser/config.json`. Every
command it starts is one of this repo's own `bin/*.mjs` scripts, spawned with an argv array (never a
shell), so no field on the page can become a command.

The server binds **`127.0.0.1` only** — never `0.0.0.0`, so nothing on it is reachable from your
network — and no route returns your API key: the config panel shows only whether one is set, and
secrets typed into the Run panel are never echoed back into the page or the log pane.

| panel | what it does |
| --- | --- |
| Tiers | the three judging tiers — what each is, what it needs, its port, its 20-item score and its `goal_done` bar, hosted marked as the default — plus what a run would use right now, start/stop for the two local servers with their output streaming into the page, and `tier use`'s text (saving it to the user config is its own labelled button) |
| Config | the effective configuration with its sources, editable, with the diff a save produced and an unset for every key the user file sets |
| Doctor | `doctor()` live or offline: every check with its status, detail and hint |
| Models | the local registry: which GGUF is downloaded, which is the default, which file is serving, and a one-click start with `--model-name` or an existing on-disk `--model <path>` |
| Judge | a state and a question set against the configured endpoint, with each answer's choice, confidence and probability table |
| Run | a real run (`jev-browser run --json`) with live progress, the final status / steps / cost and the per-step journal |

## MCP server

`jev-browser mcp` speaks MCP over stdio with zero dependencies. Tools: `jev_browse`, `jev_observe`,
Expand Down
23 changes: 23 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,29 @@ jev-browser tier use kev # 打印某个 tier 的 export 行和启动命令(
表格与全部实测数字(得分、延迟、磁盘、内存、各后端 `goal_done` 阈值、已知短板)见
[`SKILL.md`](skills/jev-browser/SKILL.md#judging-tiers);`references/config.md` 只保留阈值 profile 细节。

本地 tier 现在是一条命令:`setup` 通过 launcher 自己把缺的文件拉齐、后台起服务、把 `baseUrl` 和占位 key 写进配置,再用一个真实问题验证端点确实会答。

```bash
jev-browser setup local-readout # llama.cpp + 注册表里的 GGUF;打印日志路径和 pid
jev-browser setup status # 每个本地 tier:已安装 / 运行中 / 已配置
jev-browser setup stop local-readout
```

`jev-browser setup kev` 是精度档的同款命令,额外需要 `uv`(它会 clone Kev 检出并跑 `uv sync --extra serve`)。两者只写 `~/.jev-browser/` 之下,不会写进本仓库。手动方式照旧 —— 自己起 `bin/jev-local.mjs` 并 export 它打印的两行 —— `jev-browser tier use <tier> --persist` 也能把那两行存下来。

## WebUI

```bash
node skills/jev-browser/bin/jev-webui.mjs # 打印 http://127.0.0.1:8765/
node skills/jev-browser/bin/jev-webui.mjs --port 9000 --open
```

同一个 skill 之上的一层页面,不是第二套实现:各面板直接调用 CLI 用的 `lib/tiers.mjs`、`lib/doctor.mjs`、`lib/config.mjs`,因此不会和 `tier status` / `doctor` / `config show` 给出不一致的结论,改动也写进同一个 `~/.config/jev-browser/config.json`。它启动的每条命令都是本仓库 `bin/*.mjs` 里的脚本,以 argv 数组方式拉起,绝不经过 shell。

**只绑定回环地址**:服务只监听 `127.0.0.1`(绝不 `0.0.0.0`),同网络的其他机器访问不到;任何路由都不会返回你的 API key —— 配置面板只显示"是否已设置",Run 面板输入的 secret 也不会被回显到页面或日志里。

六个面板(tier / 配置 / doctor / 模型注册表 / judge 试跑 / run)见英文 README 的 [WebUI](README.md#webui) 一节。

## MCP server

`jev-browser mcp` 通过 stdio 提供 MCP,零依赖。工具:`jev_browse`、`jev_observe`、`jev_judge`、`jev_pick`、`jev_doctor`、`jev_config`。结果同时以 JSON 文本和 `structuredContent` 返回。见 [`references/mcp.md`](skills/jev-browser/references/mcp.md)。
Expand Down
53 changes: 52 additions & 1 deletion skills/jev-browser/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,24 @@ somewhere local. A local endpoint never costs anything, so a loopback `baseUrl`

One command serves the same `/v1/systemone` contract from a local llama.cpp server — no API
key, no network, no cost. The answer is read out of the model's first generated token (options
are labelled A, B, C …), so nothing is generated and nothing is parsed:
are labelled A, B, C …), so nothing is generated and nothing is parsed.

`jev-browser setup local-readout` does the whole first run: it finds llama.cpp, fetches the registry
GGUF through the launcher's own `--download-only`, serves it detached (log and pid under
`~/.jev-browser/run/`), writes `baseUrl` + the placeholder key into your user config, and then asks
the endpoint one real question — so "it works" is printed, not assumed:

```bash
jev-browser setup local-readout
# server 127.0.0.1:8092 (pid …)
# config ~/.config/jev-browser/config.json — baseUrl + apiKey=local
# log ~/.jev-browser/run/jev-local-8092.log
# verified noul: P(yes)=0.95 — 569 ms through http://127.0.0.1:8092
# `jev-browser setup status` reports installed / running / configured per tier;
# `jev-browser setup stop local-readout` stops the server it started.
```

Or by hand — the export line the launcher prints is all a run needs:

```bash
node <skill-dir>/bin/jev-local.mjs
Expand All @@ -178,6 +195,11 @@ export TYPESAFE_BASE_URL=http://127.0.0.1:8092 TYPESAFE_API_KEY=local
jev-browser judge --state-file state.json --questions-file questions.json --json # $0
```

A local tier's key is the literal placeholder `local`: the local server ignores the `Authorization`
header, but the client requires a non-empty key. `jev-browser config set apiKey local` stores it by
hand, and both `jev-browser setup <tier>` and `jev-browser tier use <tier> --persist` store it for you
(the export still works and is what the launchers print).

Which model is served is data, not code: `lib/local-models.json` lists each entry with its GGUF
filename, download URL, exact byte size and label, and its `"default"` picks the one the launcher
uses. Switching models is an edit to that JSON — or one flag for a single run:
Expand All @@ -203,6 +225,11 @@ and exits 0, and `doctor` reports the active registry id in its `local model` li

### Kev 4B — the accuracy tier (`jev-kev`, port 8008)

`jev-browser setup kev` is the one-command path for this tier: it checks `uv` (and says
`brew install uv` when it is missing), clones the Kev checkout and runs `uv sync --extra serve`
— skip both with `--skip-deps` — then fetches the pinned files, serves detached, writes
`baseUrl` + the placeholder key and verifies, exactly like `setup local-readout`. By hand:

```bash
node <skill-dir>/bin/jev-kev.mjs
# verifies the checkpoint and its 9.34 GB base in the HF cache (fetching what is missing, resuming
Expand Down Expand Up @@ -289,6 +316,30 @@ instead of ~0.99). When the option labels cannot be read out at all the server a
`experiments/gguf-provider/RESULTS.md` and
`experiments/gguf-provider/results/local-models-4b.md`.

## WebUI

`node skills/jev-browser/bin/jev-webui.mjs` serves one page on `127.0.0.1:8765` and prints its URL
(`--port` moves it, `--open` opens your browser, `-q` prints the URL alone). It is a view over this
skill, not a second implementation: the panels call the same `lib/tiers.mjs`, `lib/doctor.mjs` and
`lib/config.mjs` the CLI does — so they cannot disagree with `tier status`, `doctor` or
`config show` — and every edit is written to the same `~/.config/jev-browser/config.json`. Each
command it starts is one of this skill's own `bin/*.mjs` scripts, spawned with an argv array and
never a shell, so nothing typed into the page can become a command.

The server binds **127.0.0.1 only** (never `0.0.0.0`): nothing on it is reachable from your network.
No route returns your API key — the config panel shows only whether one is set — and secrets typed
into the Run panel are never echoed back into the page or the log pane. Reach for it when you want
to see what a run would use, start a local tier, or drive one goal by hand without a terminal.

| panel | what it does |
| --- | --- |
| Tiers | the three judging tiers (what each needs, its port, its 20-item score, its `goal_done` bar; hosted is marked as the default) and what a run would use right now |
| Config | the effective configuration with its sources, editable, with the diff a save produced |
| Doctor | `doctor()` live or offline: every check with its status, detail and hint |
| Models | the local registry: downloaded? default? serving? — and a start with `--model-name` or an on-disk `--model <path>` |
| Judge | a state and a question set against the configured endpoint, with each answer's choice, confidence and probabilities |
| Run | a real `run` with live progress, the final status / steps / cost and the per-step journal |

## MCP (Claude Desktop, Cursor, Codex)

`jev-browser mcp` is a stdio MCP server with tools `jev_browse`, `jev_observe`,
Expand Down
47 changes: 41 additions & 6 deletions skills/jev-browser/bin/jev-browser.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { loadConfig, describeConfig, saveUserConfig, unsetUserConfig, patchFromK
import { executeJob } from "../lib/runner.mjs";
import { TypeSafeClient } from "../lib/typesafe.mjs";
import { doctor, formatDoctor } from "../lib/doctor.mjs";
import { setupCommand } from "../lib/setup.mjs";
import { DEFAULT_TIER, TIERS, describeTier, fetchModels, formatTierList, formatTierStatus, formatTierUse, launcherCommand, probeEndpoint, tierByName, tierEnv, tierRows } from "../lib/tiers.mjs";
import { installTargets, formatInstall, DEFAULT_TARGETS, DEFAULT_SKILL_DIR } from "../lib/install.mjs";
import { parseKeyValue, rankProbabilities } from "../lib/util.mjs";
Expand All @@ -23,6 +24,7 @@ Usage:
jev-browser judge --state <json|text> --questions <json> (or --state-file / --questions-file)
jev-browser pick --question "<q>" --candidate id=description... [--context <json|text>]
jev-browser doctor [--json] [--offline]
jev-browser setup [local-readout | kev | status | stop <tier>] [--model-name <id>] [--skip-deps]
jev-browser tier [list | status | use <hosted|local-readout|kev>] [--json] [--persist]
jev-browser config show | path | set <key.path> <value> | unset <key.path> | set-key [<key> | --from-env]
jev-browser install [--targets a,b,c] [--dry-run] [--copy] [--uninstall]
Expand All @@ -49,11 +51,27 @@ tier:
thresholds profile and value, and whether a local endpoint answers.
use <tier> print that tier's export line (and the command that starts it) — nothing is
written to your configuration unless you add --persist.
--persist with "use": store that tier's baseUrl in the user config (never automatic)
--persist with "use": store that tier's baseUrl (and, for a local tier, its placeholder
apiKey) in the user config — never automatic
--json machine-readable list / status / use
--offline with "status": skip the loopback probe instead of asking the endpoint
exit codes: 0 ok, 2 unknown tier or bad usage

setup — one command per local tier, from nothing installed to a verified, configured server:
setup what is installed, what is running, what a run uses, and what to type next
setup local-readout fetch the registry model unattended, serve it in the background, write
baseUrl + apiKey into the user config, then ask it one real question
(llama.cpp is required: brew install llama.cpp)
setup kev clone the Kev checkout and uv sync it (unless --skip-deps), fetch the pinned
checkpoint, serve it in the background, write the config, ask it a question
(uv is required: brew install uv)
--model-name with "setup local-readout": serve that registry entry instead of the default
--skip-deps with "setup kev": skip git clone + uv sync, require them to exist already
--json machine-readable result (each setup prints its log file and pid)
setup status per local tier: installed / running / configured, and the log path
setup stop <tier> stop the launcher that setup started (its pid file) — never a foreign process
exit codes: 0 ok, 2 usage or a missing prerequisite, 1 a runtime failure (the log path is named)

Config precedence: defaults < ~/.config/jev-browser/config.json < ./jev-browser.config.json (or $JEV_BROWSER_CONFIG) < env < flags
Env: TYPESAFE_API_KEY TYPESAFE_BASE_URL TYPESAFE_DEFAULT_MODEL JEV_BROWSER_BACKEND JEV_BROWSER_MAX_STEPS JEV_BROWSER_BUDGET_USD JEV_BROWSER_JOURNAL_DIR JEV_BROWSER_CHROME_CDP_URL JEV_BROWSER_HEADLESS JEV_BROWSER_EGO_SERVER_NAME
`;
Expand Down Expand Up @@ -90,6 +108,8 @@ const OPTIONS = {
offline: { type: "boolean" },
targets: { type: "string" },
home: { type: "string" },
"model-name": { type: "string" },
"skip-deps": { type: "boolean" },
copy: { type: "boolean" },
uninstall: { type: "boolean" },
"from-env": { type: "boolean" },
Expand Down Expand Up @@ -180,7 +200,7 @@ async function main(argv) {

switch (command) {
case "run": {
const { config } = await loadConfig({ flags: flagsFromValues(values) });
const { config } = await loadConfig({ flags: flagsFromValues(values), ...(values.home ? { home: values.home } : {}) });
if (!values.goal && !values["dry-run"]) throw new Error("--goal is required");
if (!values.url && !values["space-id"]) throw new Error("--url is required (or --space-id to resume an ego task space)");
if (!config.apiKey && !values["dry-run"]) throw new Error("no TypeSafe API key: export TYPESAFE_API_KEY or run `jev-browser config set-key --from-env`");
Expand Down Expand Up @@ -208,14 +228,14 @@ async function main(argv) {
return result.status === "success" ? 0 : result.status === "needs_user" ? 3 : 2;
}
case "observe": {
const { config } = await loadConfig({ flags: flagsFromValues(values) });
const { config } = await loadConfig({ flags: flagsFromValues(values), ...(values.home ? { home: values.home } : {}) });
if (!values.url) throw new Error("--url is required");
const out = await executeJob({ config, job: { mode: "observe", startUrl: values.url, headless: values.headless || undefined, cdpUrl: values["cdp-url"], screenshotPath: values.screenshot ? path.resolve(values.screenshot) : undefined, keep: values.keep ?? false }, log });
print(values.json ? out.page : { backend: out.backend, page: out.page }, values, `${out.page.title} <${out.page.url}> — ${out.page.elements.length} interactive elements`);
return 0;
}
case "judge": {
const { config } = await loadConfig({ flags: flagsFromValues(values) });
const { config } = await loadConfig({ flags: flagsFromValues(values), ...(values.home ? { home: values.home } : {}) });
const state = values["state-file"] ? parseMaybeJson(await fs.readFile(values["state-file"], "utf8")) : parseMaybeJson(values.state);
const questions = values["questions-file"] ? JSON.parse(await fs.readFile(values["questions-file"], "utf8")) : values.questions ? JSON.parse(values.questions) : undefined;
if (state === undefined || !questions) throw new Error("--state/--state-file and --questions/--questions-file are required");
Expand All @@ -225,7 +245,7 @@ async function main(argv) {
return 0;
}
case "pick": {
const { config } = await loadConfig({ flags: flagsFromValues(values) });
const { config } = await loadConfig({ flags: flagsFromValues(values), ...(values.home ? { home: values.home } : {}) });
if (!values.question || !values.candidate?.length) throw new Error("--question and at least two --candidate id=description are required");
const criteria = toMap(values.candidate);
if (!values["no-none"] && !("none" in criteria)) criteria.none = "No candidate fits.";
Expand All @@ -244,6 +264,19 @@ async function main(argv) {
else process.stdout.write(`${formatDoctor(report)}\n`);
return report.ok ? 0 : 1;
}
case "setup": {
// The one-click path to a local tier: `setupCommand` composes the launchers and throws a
// `config: true` error (exit 2) for anything the user has to change.
const { config } = await loadConfig({ flags: flagsFromValues(values), ...(values.home ? { home: values.home } : {}) });
const out = await setupCommand({
sub: positionals[1],
rest: positionals.slice(2),
options: { home: values.home, modelName: values["model-name"], skipDeps: values["skip-deps"], json: values.json, config, skillDir: SKILL_DIR, env: process.env, log },
});
if (values.json) print(out.json, { json: true });
else process.stdout.write(`${out.text}\n`);
return out.code;
}
case "config": {
const sub = positionals[1] ?? "show";
const home = values.home;
Expand Down Expand Up @@ -304,7 +337,9 @@ async function main(argv) {
const tier = tierByName(name);
if (!tier) throw configError(`unknown tier "${name}" (expected ${TIERS.map((t) => t.name).join(", ")})`);
const home = values.home;
const persisted = values.persist ? await saveUserConfig({ baseUrl: tier.baseUrl }, home ? { home } : {}) : null;
// A local tier is only half configured by baseUrl: its key is the literal placeholder the
// client requires and the server ignores, so --persist stores both. Hosted has no key.
const persisted = values.persist ? await saveUserConfig({ baseUrl: tier.baseUrl, ...(tier.apiKey ? { apiKey: tier.apiKey } : {}) }, home ? { home } : {}) : null;
if (values.json) print({ tier: tier.name, baseUrl: tier.baseUrl, apiKey: tier.apiKey, port: tier.port, command: launcherCommand(tier, SKILL_DIR), env: tierEnv(tier), persisted }, { json: true });
else process.stdout.write(`${formatTierUse(tier, { skillDir: SKILL_DIR, persisted, configPath: userConfigPath(home) })}\n`);
return 0;
Expand Down
Loading
Loading