npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

br0agent

v1.0.5

Published

Local coding agent with a browser UI: zero-dependency Node.js server, OpenAI-compatible backends, built-in file/shell tools, per-turn snapshots and rollback

Readme

br0agent

A minimal local coding agent with a browser UI. Node.js 18+ (ESM, zero third-party dependencies), a single-page client, and an OpenAI-compatible inference backend.

The server binds 127.0.0.1 only. It is a local desktop tool: nothing on the LAN or the internet can reach it, and there is no TLS, no enrollment and no API token. See requirement/webagent.md for the full design.

Security: this tool runs shell commands and reads/writes the folders you add to a workspace. Treat a session exactly like a terminal in that project — only open folders you trust, and only point it at an inference backend you are willing to send that content to.

Install

npm install -g br0agent        # global CLI: `br0agent`
npx br0agent                   # or run it once without installing
npm install br0agent           # or as a dependency; then `npx br0agent`

br0agent serves the UI at http://127.0.0.1:18117/ and opens it in your default browser once the port is bound (config.json → openBrowser, default true). Useful flags: --port NNNN, --config <file>, --models <file>, --no-browser (skip the browser this run). Ctrl+C stops a foreground instance.

br0agent stop                  # stop the instance on the port from config.json (default 18117)
br0agent stop --port 18118     # stop the instance on that port
br0agent stop --config D:\cfg.json

br0agent stop is the cross-platform equivalent of stop.bat: it finds the process listening on the port, refuses to touch anything whose command line is not server.js, kills the tree (so bg_start children go too), waits up to 5 s for the port, and exits 0 when the port ends up free, 1 when another program still holds it (listing that pid instead of killing it). It never starts a server.

User data never lives in the package directory (an npm update would wipe it):

| What | Default location | Override | | --- | --- | --- | | runtime config | ~/.br0agent/config.json (created from the shipped template on first run) | --config, BR0AGENT_CONFIG (legacy WEBAGENT_CONFIG) | | model profiles / API keys | ~/.br0agent/models.json (created from the built-in template on first run) | --models, BR0AGENT_MODELS (legacy WEBAGENT_MODELS) | | sessions, snapshots, workspaces | ~/.br0agent/ | config.json |

So after installing, edit ~/.br0agent/models.json (not the copy inside node_modules). Upgrading from a build that used ~/.webagent (or <project>/.webagent/): rename the directory to .br0agent once; the app no longer moves it automatically. Windows, macOS and Linux are supported; start.bat / stop.bat are Windows-only wrappers around the same stop.js that backs br0agent stop.

Quick start (repo checkout)

node server.js          # plain start: http://127.0.0.1:18117/ (uses ~/.br0agent/config.json)
.\start.bat             # restart: stops the previous instance, starts a new one, opens the browser
.\start.bat --no-open   # same restart without opening a browser tab (forwards --no-browser)
.\stop.bat              # stop only: same stop logic, no restart (also: .\stop.bat 18117)

Both batch files are thin Windows wrappers around stop.js, which owns the port resolution and the stop logic — the same code behind br0agent stop, so there is exactly one implementation:

  • start.bat asks stop.js --print-port for the port (checkout config.json → ~/.br0agent/config.json → 18117), stops the previous instance and then runs node server.js in the foreground — the server itself opens the browser, so start.bat never opens a tab and one start never opens two. start.bat --no-open forwards --no-browser, which skips the browser for that run regardless of the config. In a checkout it passes --config config.json when that file exists; model profiles always stay in ~/.br0agent/models.json, so neither the Settings dialog nor a restart writes into the repo.
  • stop.bat forwards its arguments (stop.bat 18117, stop.bat --port 18117 --config D:\cfg.json) and its exit code to stop.js.

stop.js stops only a process on that port whose command line runs server.js (killing its tree, so background bg_start children go with it), waits up to 5 s for the port to be released, and exits 0 when the port ends up free (also when nothing was running) and 1 when another program still holds it, listing that pid instead of killing it. start.bat refuses to continue on a non-zero exit. Restarting loses an in-flight run but not the session history, which is already on disk.

The page also works from file:// (double-click web/index.html); it talks to http://127.0.0.1:18117 (add ?port=NNNN to the URL if you changed the port).

First run seeds ~/.br0agent/config.json from the shipped config.json and ~/.br0agent/models.json from the repo-root models.json template when it exists, else from the SHIPPED_MODELS constant in models.js (local Qwenox profile only); it then creates ~/projects and ~/.br0agent/sessions.

Inference backends (models.json)

models.json always lives in ~/.br0agent/; that is the file Settings → Model Settings reads and writes, and the only one the server uses by default. A checkout may keep its own copy at the repo root as a template (gitignored): it is copied once when ~/.br0agent/models.json does not exist yet and is never read again, so editing it only affects the next first run. Pass --models <file> (or BR0AGENT_MODELS) to point the server somewhere else entirely. A broken file stops the server at startup (a silently empty dropdown is worse than a loud error); a broken edit while running keeps the last good copy and shows a warning banner.

Shipped content: one local profile. No template JSON ships inside the package — the first-run file is written from SHIPPED_MODELS in models.js when the repo template is absent, so this JSON is the shape of that constant:

{
  "defaultModelId": "qwen3.8-flash-next",
  "titleModelId": null,
  "models": [
    {
      "model-id": "qwen3.8-flash-next",
      "label": "Qwenox Flash (local)",
      "provider": "qwenox",
      "baseUrl": "http://127.0.0.1:1234/v1",
      "apiKey": "",
      "contextLength": 262144,
      "vision": true,
      "thinking": { "enabled": true, "style": "chat_template" }
    }
  ]
}

Add a third-party backend by appending one entry to models (keys are plain text, so never commit this file):

{ "model-id": "deepseek-chat", "label": "DeepSeek Chat", "provider": "deepseek",
  "baseUrl": "https://api.deepseek.com/v1", "apiKey": "sk-...",
  "contextLength": 128000,
  "vision": false, "thinking": { "enabled": false, "style": "none" },
  "cache": { "promptCacheKey": true, "cacheSalt": "none" } },
{ "model-id": "glm-4.6", "label": "GLM-4.6", "provider": "zhipu",
  "baseUrl": "https://open.bigmodel.cn/api/paas/v4", "apiKey": "...",
  "contextLength": 200000,
  "vision": false, "thinking": { "enabled": true, "style": "glm" } }

Fields: model-id, label, baseUrl are required. model-id is the model name sent to the backend; a profile is identified by the pair (model-id, baseUrl), so the same model served by two endpoints (a vendor's CN and US site, say) can appear twice — give each entry its own label. The derived modelKey (<model-id>#<endpoint hash>, never the raw endpoint) is what the dropdown and sessions carry; defaultModelId/titleModelId name the vendor id, and with duplicate ids the first entry in file order wins. Optional: provider, apiKey (""), contextLength (32768), vision (false), thinking ({enabled, style}), cache ({promptCacheKey, cacheSalt}), headers, extraBody, timeoutMs (600000 — connect + headers, and any silence inside a streamed body: a longer gap is treated as a dead connection and the request is replayed). Unknown keys are rejected — including the retired tools flag: every profile must support native function calling.

thinking.style maps to the vendor body key: none (nothing), glm (thinking: {type}), chat_template (chat_template_kwargs.enable_thinking), reasoning_effort (reasoning_effort: "medium", thinking.level overrides). extraBody is merged last, so a vendor quirk never needs a code change.

Keys never appear in logs, errors, messages.jsonl, the SSE stream or GET /api/models (apiKey and baseUrl are never returned).

Configuration (config.json)

host must be exactly "127.0.0.1"; anything else (::1, localhost, 0.0.0.0, a LAN IP) aborts startup. openBrowser (true) opens the UI in the OS default browser as soon as the port is bound; set it to false to keep startup silent, or pass --no-browser for a single run. Other knobs: port (18117), defaultProjectRoot (legacy fallback only), workspace.file (~/.br0agent/workspaces.json) and workspace.defaultFolder (~/.br0agent/project, created on demand and seeded into every new workspace), sessions.dir, userRulesPath, shell, upload, snapshot, agent (maxToolRounds — 0 means unlimited, shipped default; titleTimeoutMs, systemPromptPath), context (mode = extreme default / normal / cyclic, plus per-mode limits), local.allowedOrigins (loopback origins are appended at startup).

String values are expanded at startup: ~ means the user profile, and %VAR% / ${VAR} / $VAR read the environment — so the shipped config is platform-neutral. The default config file is ~/.br0agent/config.json; --config <file> or BR0AGENT_CONFIG overrides it (an explicitly named file must exist, a missing default is seeded from the bundled template).

snapshot controls the per-turn file snapshots and is always on (no enabled switch): dir (default ~/.br0agent/snapshots), maxFileBytes (5 MB per file), maxBytes (2 GB store cap), keepTurns (50) and maxAgeDays (30) — a prompt whose manifest was dropped by GC can no longer be rewritten. agent.systemPromptPath points at the app-level guidance file (default systemPrompt.md next to server.js); it is injected into the system prompt of every mode that can write files (Work) after the built-in text and before the user rules, so the user's CLAUDE.md stays untouched. Plan and Chat do not get it: it documents write and shell tools those modes are not offered.

context.mode is read at startup (restart to apply) and reported by GET /api/health together with staticPrefixTokens { system, tools, tasklist, total, sessionId, estimated }.

API (loopback only)

  • GET /api/health?sessionId= — compaction mode, model count, user rules mtime, static prefix size.
  • GET /api/models — selectable profiles (no apiKey, no baseUrl), mtime-driven re-read.
  • GET /api/models/file — the runtime models.json verbatim, including apiKey / baseUrl (loopback only; the key-free view stays GET /api/models); POST /api/models/file {text} validates with validateModels before writing, then adopts the file immediately, so the next prompt already sees the new profiles. Returns the same shape as GET /api/models.
  • GET /api/config — the runtime config.json and its absolute path, verbatim as stored on disk (expanded values would freeze ~ / %VAR%); POST /api/config {text} validates the JSON and writes it back unchanged. context, agent, shell and upload are swapped into the live config immediately; host, port, openBrowser, defaultProjectRoot, userRulesPath, maxDetachedMs, sessions, snapshot, workspace and local are reported back and need a restart.
  • POST /api/chat {sessionId?, message, modelKey?, modelId?, mode?} — enqueues a run, returns {sessionId, seq, modelId, modelKey}. modelKey selects a profile; modelId is still accepted for old clients (with duplicate ids it resolves to the first profile in file order).
  • GET /api/chat/subscribe?sessionId=&fromSeq= — SSE of the run (token, reason, stream_reset, tool_call, tool_result, usage, session_usage, notice, error, done), replayable from fromSeq. stream_reset marks a replayed request: the partial round is dropped and the same round streams again from the first token. A transport failure (dropped socket, silent connection) is replayed up to 3 attempts before it becomes error.
  • POST /api/cancel {sessionId}.
  • POST /api/compress {sessionId} — manual Compress: one forced compaction pass outside a run (bypasses the usage threshold; the drop persists in the session's compact state). Returns {ok, compacted, kept, groups, tokens}; 409 while a run or another compression is in flight. Open tabs see transcript_divider + usage on the session stream. The UI button sits right of the notification bell and is enabled exactly while Send is (disabled while busy or while compressing).
  • GET /api/sessions — in-memory sessions (disk persistence arrives in P2).
  • GET /api/sessions/<id>/snapshots — per-turn snapshot summaries plus the prompts whose snapshot failed; GET .../snapshots/<turn> the full manifest; POST .../snapshots/<turn>/restore reverts that turn (one-way).
  • GET /api/sessions/<id>/prompts/<seq>/edit-plan — read-only: which turns a prompt rewrite would roll back, the files involved, and accepted gaps. It refuses (409) when the snapshot chain cannot cover the range, before anything is written. POST /api/chat {sessionId, message, editSeq} runs the rewrite first (files back, transcript cut at that prompt) and then the new prompt.
  • DELETE /api/sessions — clear-all: stops every run, deletes every session dir + snapshot manifest, then removes <folder>/.br0agent/uploads and <folder>/.br0agent/screenshots inside every folder those sessions used (workspace definitions and other files are untouched).
  • GET/POST /api/workspaces, PATCH/DELETE /api/workspaces/<id> — named folder groups (%USERPROFILE%\.br0agent\workspaces.json).
  • POST /api/workspaces/<id>/folders {path} / DELETE .../folders?path= / POST /api/workspaces/<id>/primary {path} — folder list and the primary (cwd) folder. A new workspace is seeded with workspace.defaultFolder; the last folder cannot be removed.
  • POST /api/sessions accepts workspaceId (folder list is copied at creation) or the legacy projectRoot, or an explicit {folders, primary}.

Every route enforces Host = loopback, an Origin allowlist (null for file://, 127.0.0.1:PORT, localhost:PORT), a CORS OPTIONS preflight, and a per-route body contract (application/json for JSON routes; application/octet-stream plus X-Filename/X-Session-Id for /api/attachment, which arrives in P3).

Status

P0-P6 are implemented:

  • server with loopback binding, Host/Origin guards, CORS preflight, per-route body contract

  • models.json loading/validation with runtime re-read, LLM client with streaming, usage/cache normalization and per-call timing, model dropdown, usage bar and per-turn footer

  • tools: read_file, write_file, directory_operate (op = create / mkdir / delete / move / rename; everything is snapshotted, create never overwrites, a non-empty directory needs delete {recursive:true} and is stored whole first), str_replace (exact, count-checked, atomic, BOM/CRLF preserving), list_dir, find_files, grep (both cover every write root; hits inside the project stay relative, hits outside are absolute), run_command (timeout + kill tree + capped tail, cwd in any write root), tasklist (durable, dependency-gated, persisted to <sessionsDir>/<id>/tasks.json), webfetch, screenshot (headless Edge/Chrome into <project>/.br0agent/screenshots, then the PNG is attached as an image so a vision profile can actually look at the page)

  • agent loop with tool dispatch, extreme / normal / cyclic context compaction, cancel, disconnect/replay

  • session modes (composer dropdown, persisted in meta.json): work (every tool), chat (no tools, no tasklist injection) and plan (read-only tools plus the tasklist). common.js holds MODE / MODE_TOOLS, the single allow-list the schema and the executor both read: the request carries only that mode's schemas, the built-in prompt only describes those tools (Chat has no Tools/Tasklist section at all), and dispatch_tool still refuses anything outside the list. A switch rebuilds the whole prompt — the user-rules block is carried over from the snapshot, and the provider's prefix cache for that session is lost by design.

  • one place for tool names: common.js exports TOOL (every tool name), TOOL_NAMES (schema order) and DIR_OP/DIR_OPS (directory_operate operations, also used as snapshot op labels). The server imports it, the browser gets the same file at /common.js, and the diagnose scripts import it too — renaming a tool is a one-line change, and p0-budget fails if the schema list drifts from it.

  • sessions on disk under %USERPROFILE%\.br0agent\sessions\<id>\ (meta.json with the system-prompt snapshot, messages.jsonl, tasks.json, events.log), list/rename/delete, paged message load, one-click JSON export, and a session sidebar that survives a server restart. The upload button next to the export icon imports such an archive: an id that already exists is merged (local records keep their seq, only the records the archive adds are appended, so re-importing the same file changes nothing), an unknown id becomes a new session whose prompt snapshot is rebuilt from the current rules file, and a record that is not a session is reported as skipped instead of being written

  • workspaces: the new-session screen is two columns — workspaces on the left (add / rename / delete), the selected workspace's folders on the right (Add folder through the server-side browser, radio = primary, per-row remove). A session copies the folder list when it is created: the primary folder becomes its project root, the rest become write roots, so the sandbox, the snapshot domain and run_command's default cwd all follow the radio button. Editing a workspace later never reaches an existing session. The browser lists dot-directories so %USERPROFILE%\.br0agent\project (the seeded default folder) stays reachable.

  • auto title on the first prompt of a session: a separate tool-free, 5 s-capped call that runs before the agent run; timeout falls back to the prompt prefix

  • settings dialog: the gear next to Export opens a modal with two tabs — Model Settings (models.json, the default tab) and Server Settings (config.json) — each with JSON syntax highlighting (a transparent textarea over a coloured <pre>, no third-party library), inline syntax errors, Ctrl+S, Reload, and an unsaved-changes guard. Saves keep the file byte-for-byte (no reformatting). A saved models.json is adopted immediately and the model dropdown is refilled (current choice kept when it still exists); the server-config response lists which sections were hot-applied and which need a restart. System notices show at the right of the usage row, with a bell that keeps the full history

  • attachments: paste (screenshot) or drag files into the chat; each upload is a raw-body POST with X-Filename/X-Session-Id, written to <primary folder>/.br0agent/uploads/ (temp file → rename, size cap 20 MB, executable extensions refused), given a per-session id (img1, file1). .br0agent/ writes its own * .gitignore on first use (uploads and screenshots do), so the folder never shows up in the project's git status — for this repo it is also listed in .gitignore. Images go to a vision profile as image_url data blocks; a non-vision profile gets a path note and a notice. Later turns never re-send image bytes — history keeps the path only. Reference an attachment from a prompt with @img1.

  • Stop is immediate: cancel aborts the in-flight stream, kills a running run_command process tree (measured ~150 ms on a 60 s command), aborts a pending title call, and skips the rest of the round instead of waiting for the tool to finish

  • background processes: bg_start / bg_status / bg_kill keep long-running commands (dev servers, watchers) per session with an output tail; they are killed when the session is deleted or the server exits

  • ask_user: the run pauses on an SSE ask event, the UI renders the question with option buttons plus a free-text field, and POST /api/chat/answer resumes the run. A question never expires: it is persisted in meta.json, survives an agent restart, blocks new prompts (the Send button stays disabled) until answered, and an answer submitted after the run died resumes the interrupted prompt; only cancel/session deletion rejects it

  • run termination is explicit: done carries reason (normal / max_rounds / cancelled / error); with a positive agent.maxToolRounds the cap shows a red line in the transcript instead of just stopping, while 0 (shipped default) runs until the model is done or the run is cancelled. Every usage / tool_call / tool_result event carries its round, and node diagnose/inspect-session.mjs prints the whole timeline of a stored session

  • file snapshots + write roots (decided 2026-10-17). The write domain is the session's primary folder plus the other folders of its workspace (+ roots and the per-session roots API were removed when workspaces arrived: the folder list is edited in one place and copied at creation). Before every write_file / str_replace / directory_operate operation the previous content goes into a content-addressed store, and in a git root once per turn git status supplies the candidate list while content hashes decide what really changed — git is the index, never the store, and nothing is written into the user's .git. Each turn writes a manifest; every user prompt then offers a pencil that rewrites it: the files of that prompt and of every later turn go back to their state before it (one-way — whatever changed after the snapshot, the user's own edits included, is overwritten or removed), the transcript is cut at the prompt, and the edited prompt runs on the rolled-back state. The rewrite refuses to start when a manifest of the range is missing (GC, old session, a failed snapshot) instead of rolling back half of it. Limits, all deliberate: in a non-git root only the file tools are covered (run_command / bg_start edits are not), paths outside the declared roots are never covered, and a path reaches its .gitignore-ignored state only through the file tools. Paths are canonicalised through realpath, so a junction cannot write outside a root.

Keyboard shortcuts: Ctrl+N new session, Ctrl+L focus the composer, Ctrl+Enter send, Esc closes the folder picker. A manual regression list lives in diagnose/CHECKLIST.md.

Diagnose tests

node diagnose/p0-models.mjs    # validation, re-read, thinking mapping, /api/models contract
node diagnose/p0-cache.mjs     # byte-stable prefix, cache knobs, vendor usage shapes
node diagnose/p0-budget.mjs    # static-prefix ceilings and duplicate-guidance sniff
node diagnose/p0-chat.mjs      # real Qwenox round-trip + guards (skips if :1234 is down)
node diagnose/p1-loop.mjs      # tools, tasklist rules, sandbox, timeouts, output caps, cancel
node diagnose/p1-compact.mjs   # extreme/normal/cyclic on a 200-group history
node diagnose/p1-detach.mjs    # run continues while detached, replay is ordered
node diagnose/p1-real.mjs      # real engine writes a file through write_file
node diagnose/p2-sessions.mjs  # disk store, isolation, browse, restart, export, merge-import, runStartSeq replay
node diagnose/p2-title.mjs     # tool-free title call, once per session, timeout fallback
node diagnose/p2-rules.mjs     # rules snapshot, one notice, new-session handover, read-only guard
node diagnose/p3-attach.mjs    # upload caps/aborts, exe guard, @imgN, vision blocks, T0 stripping
node diagnose/p4-bg.mjs        # bg_start/status/kill through the tool layer and a scripted run
node diagnose/p4-ask.mjs       # ask -> answer round-trip, timeout, cancel, unknown id
node diagnose/p7-snapshot.mjs  # hooks vs git snapshots, create/delete/move revert, write roots, API
node diagnose/p8-workspace.mjs # workspace CRUD, folder guards, primary folder, clear-all purge
node diagnose/p9-mode.mjs      # mode schemas, mode-specific prompt text, tasklist suppression, switching
node diagnose/p6-smoke.mjs     # end-to-end over HTTP against the real engine (skips if offline)
node diagnose/p0-remote.mjs    # opt-in: third-party profiles from models.json

npm test (or pnpm test) runs every offline suite including p7-snapshot and p8-workspace; the real-engine scripts can be run individually.

Every offline suite drives the shared mock server at ../agserver, not a copy in this repo: diagnose/harness.mjs starts it with a generated config whose scripts hold the scripted replies (one step per tool round, plus fault scripts that reset or stall a stream), and reads the recorded request bodies back from GET /__requests. The profile URL selects both: /<usage-shape>/<script>/v1.

Every script self-terminates (per-case timeout + 180 s watchdog), prints durations and exits non-zero on failure. Test servers use ports 18100+ and are killed by the scripts.

License

GPL-3.0-or-later — see LICENSE for the full text. Copyright (C) 2026 mark [email protected]. There are no third-party dependencies, so the GPL covers this code only.

The npm tarball is the complete source, so a plain npm install needs no separate source offer; whoever conveys a modified build must carry the GPLv3 terms and the sources for their changes.