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

@indexlabs/groundwork

v0.1.4

Published

The Groundwork daemon: runs your agents on your own machine, through our harness, on your Claude Code or Codex subscription or any AI SDK provider key, with a packaged signed-out browser.

Readme

@indexlabs/groundwork

The Groundwork daemon runs your agents on your own machine. Convex queues the work and records it; everything that executes happens here, in our own harness loop, with our own tools (see docs/agent-platform.md, the authoritative design).

  • Models: your Claude Code or Codex subscription (the CLIs, driven as models with every native tool of theirs switched off), or any Vercel AI SDK provider you add an API key for in Settings → Large Language Models. Keys arrive sealed to this device for one job at a time.
  • Tools: a signed-out browser for reading the open web and searching, a guarded fetcher for JSON, text and PDFs, a per-task scratch workspace, a sandboxed calculator, the agent's memory, a live plan, delegation, follow-ups, questions to you, notifications, an optional decision model (Jev), and the tools of MCP servers you add on this machine. No built-in tool can click, type, post or act on anything outside your Groundwork account; an MCP tool that is not read-only waits for your approval on every call.
  • The daemon is required. Without a paired, running daemon, agents do not run.
npm install -g @indexlabs/groundwork
groundwork login <code> --origin <web app url>   # Settings → Devices gives you this whole line
groundwork start

Commands

| Command | What it does | |---|---| | groundwork login <code> [--origin <url>] | Pair this machine: sends its public key, stores the device token and id in ~/.groundwork/credentials.json (0600). Each --origin (https, or http://localhost / http://127.0.0.1 for a local web app; no path) pins a web app allowed to use the loopback takeover bridge, kept in ~/.groundwork/config.json as appOrigins. The origins are checked before the code is sent; a new --origin replaces the pin, and logout removes it. | | groundwork start | Run agents. Heartbeats at once and every 30 s, claims jobs as soon as the server's wake subscription says one is queued (polling every 2 min behind it, or every 30 s when the subscription is unavailable; GROUNDWORK_CONVEX_CLOUD_URL names the Convex URL of a self-hosted deployment), reports progress, settles them. Sets up the browser in the background if it is missing. Ctrl-C stops claiming, reports running jobs (and any claim still on its way back) as retryable failures so the server requeues them, and closes the browser. | | groundwork status | Asks the server whether it still knows this device (a probe heartbeat, which changes nothing on the server, so it is safe beside a running start), and shows models, browser, tools and which web apps may take over the browser. | | groundwork logout | Forgets this machine's pairing and its web app origin pin. Revoke it in Settings → Devices to remove it from your account. | | groundwork models | The models each CLI can run, and which are enabled. | | groundwork install | Set up the browser now (find or download a Chrome). | | groundwork takeover-key [--rotate] | Print the key that lets the web app on another machine take over this browser. | | groundwork mcp add <name> [--env NAME]… -- <command> [args…] | Add a local (stdio) MCP server to ~/.groundwork/mcp.json (0600). It gets PATH, HOME and the variables named with --env, nothing else of the daemon's environment. Names: 1–32 of a-z 0-9 _ -, no __, not ending in _. | | groundwork mcp add <name> --url <https url> | Add a remote (Streamable HTTP) MCP server (plain http only to localhost). | | groundwork mcp list / mcp remove <name> | Show or remove them. The daemon connects at start; restart it after a change. | | groundwork doctor | Check pairing (a probe, like status), the device key, each CLI's version and sign-in, each MCP server (starts it, lists its tools, says which ask for approval), the browser, and do one test read in its own browser session (groundwork-doctor-open), never a running daemon's. |

How a job runs

claim ─► parse & check ─► build models ─► build tools ─► harness loop ─► complete | suspend | fail
            protocol.ts     harness/models   harness/tools   @indexlabs/groundwork-harness
  1. Claim (POST /claim). The job is parsed defensively (protocol.ts): the model refs are re-parsed with the daemon's own grammar, model keys must be well-formed, providers must be in the catalog, tool names must look like tool names. A job that fails is reported with fail and the reason; it never runs on a guess.
  2. Models (harness/models.ts). One harness Model per entry in run.models:
    • cli: claude or codex through the harness adapters, in the empty directory ~/.groundwork/run/empty, prompt on stdin. The model key must be one the user enabled on this device (modelFlagFor); there is no fallback list.
    • api: createLanguageModel + apiModel from src/shared/providers.ts. The key is opened with this device's private key (device-key.ts, AAD jobId|deviceId), held in memory for the job, and scrubbed from anything reported. A local server that takes no key gets none.
    • api-proxy: a team-shared key that never leaves the server; each step is one POST /model/step.
  3. Routing. With routing: "auto" and a decision model, Jev picks the model for each step among the run's models (harness/router.ts). Confidence below 0.5, or any error, means the default (first) model. Jev sees the task, the step number and one-line summaries of recent steps, never page text.
  4. Tools (harness/tools.ts): the agent's requested tools intersected with what this machine and job can back. tools: [] is finish only (plus decide while Jev's decide use is on); no list means the default set (DEFAULT_TOOL_NAMES: reads, memory, plan, decide, http_fetch, pdf_read, calculate, never a tool that writes to the account or an MCP tool). A requested tool that cannot be offered is named in the run's first activity line.
  5. Run (@indexlabs/groundwork-harness runHarness). Each step is reported to /progress as it lands (the tool, its one-line summary, tokens, the model, and the error text when it failed), the live activity line at most every 1.5 s, and the plan whenever update_plan publishes one.
  6. Settle. An answer (or an exhausted step budget) is complete with { text, usage, finished, exhausted }, never the steps. A run that paused is suspend (below). A model or CLI error is fail. A cancel is fail { cancelled: true }.

Suspend and resume (§11.1). ask_human, delegate with wait: true and an MCP call that needs approval pause the run: the steps (page text included) are written to ~/.groundwork/checkpoints/<id>.json (0600, a random 32-hex id), the fast path, and the persisted copy (next paragraph) is stored on the server before POST /suspend { jobId, attempt, kind, detail, checkpointId, usage } goes to the server. Only a task can pause. When the wait is answered the server enqueues a resume job pinned to this machine with request.resume = { checkpointId, answer }; the daemon loads the checkpoint and continues, the answer becoming the paused call's result (step numbers continue). For an approval, "approve" makes the call here first (its result is written into the checkpoint before the run goes on, so a retried resume never calls twice) and anything else is "Denied by the user.". The checkpoint is deleted once the resume settles for good, kept while a retry may still need it, and pruned after 30 days. A resume whose checkpoint is in neither place fails, not retryable.

The audit record (§12, persist/). Every run's context, every step in full, its end, its workspace (changed files only) and a checkpoint manifest are persisted to the server: scrubbed first (API keys, the device token, the takeover key, MCP credentials; a file whose name holds one is left out), content-addressed and deduplicated per account (/runs/have before any upload), gzipped, and never able to fail or stall the run (records go in batches and fall back to the settle outbox). An attempt sends at most 500 step and context records; past that, or at the server's 429 (a record limit of the job), it logs once, stops sending steps and sends one truncated marker (the run's end still goes; the run itself carries on). Such a run can still move machines: its checkpoint carries exactly the steps the server's records lack (persist/carried.ts; scrubbed, ≤ 32 MB, outside the record limits), and a restore fills the gap from them. Only a gap too large to carry, or refused by the server, keeps a resume on the machine that paused, and the refusal says so. Snapshots run on suspend (before /suspend), finish, failure, cancel and every 10 steps; one of an unchanged workspace sends nothing. A resume claimed on a machine without the local checkpoint rebuilds it from the server's copy (/runs/restore, every download verified against its sha-256 within a size cap, every path through the workspace jail) into the task's own workspace directory, and makes that directory exactly the snapshot: once every file is downloaded and verified, anything else there (files, folders, links, which are unlinked and never followed) is removed, inside the workspace root only. So a paused task continues on any of your machines.

Jev (§11.4), when the job carries a decider and the use is on (run.decider.uses; each degrades silently to "no Jev"): decide is offered to every run; routing picks the model per step (a switch is noted); memoryRecall scores up to 50 recent unpinned notes (/memory/search with an empty query) and puts the best (≥ 0.6, ≤ 8, ≤ 4k chars) in the system text; finishCheck hands a clearly incomplete answer back once; stuckCheck nudges every 5 steps when the agent repeats itself; injectionFlag warns the model about a page (browser_read, http_fetch, pdf_read) that addresses instructions to an AI; evalJudge grades an eval answer with no model at all ({"score": 0–1, "pass", "confidence"}). Every decision is a step with tool: "jev" (and confidence) on /progress.

Attempts. The claim carries attempt (1 for a job's first claim), and every report about the run carries it back: each /progress event, the /complete result and the /fail body. The server ignores a report from an attempt it has moved on from, and answers its progress with cancel: true so that run stops. At most one run of a job is live in the daemon: if this machine claims a newer attempt of a job it is still running (the server took it back while the machine looked gone, then gave it here again), the older run is aborted and reports nothing (RunningJobs).

Retryable or not. /fail says retryable: false when another attempt would fail the same way, and the server parks the job at once instead of requeueing it: the job deadline, a usage or rate limit, a signed-out CLI or a refused key, a CLI that is missing or too old, a model not enabled on this machine, a sealed key that will not open, a payload the daemon refuses. Crashes, the model's service failing (5xx, overload, a dropped connection) and shutdown stay retryable (modelFailureRetryable).

Cancellation. Each run has an AbortController. It fires when the heartbeat ack lists the job in cancelJobs or a /progress answer says cancel: true, and the harness stops the model call and kills the CLI. A job listed in cancelJobs that no run here holds (its settle was lost, or the daemon restarted mid-run) is settled at once with fail { cancelled: true }. A job that runs past the server's reaper deadline is stopped a minute before it and reported as not retryable.

Lost settles. A complete or fail that still fails after two quick retries is kept in memory and sent again in the background (5 s, 15 s, 30 s, then every minute, and at once after a heartbeat gets through) for up to 30 minutes. A restart loses it; the server's reaper is the backstop.

Tools

| Tool | What it does | Offered when | |---|---|---| | browser_read | Open one https page, signed out, and return its URL, title and text or links. Action-shaped addresses (logout, delete, checkout, follow, settings…) are refused, and so is anything on this machine or a private network, before navigating and on the page it ends on (see The browser). | the browser is ready | | web_search | Search a fixed engine (DuckDuckGo's HTML endpoint) through the browser. | the browser is ready | | memory_search / memory_save / memory_forget | The agent's memory in Convex, scoped by the server to the claimed job's agent. | the job carries run.memory | | update_plan | Publish a checklist the web app shows live. | always | | decide | Ask Jev a choice, yes/no or score question. | the decider's decide use is on (offered even when not requested) | | http_fetch | GET or HEAD one public https URL without Chrome: text, JSON, XML, CSV, markdown. Every connection is resolved and checked (tools/net-guard.ts: loopback, private, link-local, CGNAT, ULA, multicast refused at connect time), redirects followed by hand (≤ 5, each re-checked), fixed UA, no cookies or auth, 5 MB / 20 s, output truncated with a marker. | always | | pdf_read | Fetch a PDF through the same guard (≤ 20 MB) and extract its text with unpdf in a worker thread (30 s, isEvalSupported: false), optionally a page range. | always | | workspace_list / workspace_read / workspace_write | Text files in ~/.groundwork/workspaces/<task or job>/: real-path jail, symlinks refused, O_NOFOLLOW, ≤ 200 files and 100 folders, ≤ 50 MB, ≤ 2 MB each, 0600, never executed. publish: true uploads the file (≤ 5 MB; not HTML/SVG/XML/JS) as a run artifact. | always | | calculate | JavaScript in QuickJS (WASM): no host functions, 32 MB, 2 s; input and rows (CSV via papaparse) are the only values put in. | always | | delegate / check_tasks | Start a child task (tools ⊆ this run's), optionally waiting for it; check children. | a task | | schedule_followup | Queue a task for later: an ISO or plain-language time ("tomorrow 9am"), or with repeat a plain repeat ("every weekday at 9am", "every day at 8:30", "every monday", "every hour") or cron; this machine's timezone is sent. At most hourly, 50 runs, 10 active per agent. | a run with an agent, not an eval | | ask_human | Pause until you answer a question. | a task | | notify | An in-app notification (≤ 5 per run). | not an eval | | mcp__<server>__<tool> | A tool of an MCP server in ~/.groundwork/mcp.json, arguments as one arguments_json object checked against its schema. Read-only (readOnlyHint: true) calls run at once; every other call pauses for your approval (outside a task it is denied); an approved call runs at most once, a denied one returns "Denied by the user.". 60 s per call. | the job names it | | finish | The final answer. Built into the harness loop. | always |

Models

Claude Code and Codex are discovered, not listed in source (models.ts, capabilities.ts): Codex from codex debug models (or its models_cache.json), Claude Code from a seed list plus names you add. You choose which run in Settings → Devices; the choice reaches the daemon on the heartbeat ack and can only narrow what was discovered. GROUNDWORK_MODELS is a ceiling on this machine: it narrows that selection (a model the user turned off stays off), and when none of the selected models is on the list, the listed ones run.

At start the daemon reads claude --help and codex features list once. The Claude adapter refuses a Claude Code without --tools and --strict-mcp-config and adds --safe-mode, --no-session-persistence and --disable-slash-commands when they exist; the Codex adapter disables every denylisted feature this Codex knows. These facts stay on the machine; they are never sent to or taken from the server.

A Codex that cannot list its features (an old Codex without the subcommand, or a first launch slower than the 20 s the probe allows) is not advertised at all, because the adapter would refuse every step on it: the server sends this machine no Codex work until it can. groundwork status says why, and the daemon asks again in the background (after 2 minutes, then backing off to once an hour), advertising Codex as soon as it answers.

API models need nothing on the machine beyond the daemon: the AI SDK providers ship with it, and the key arrives sealed per job.

The browser

agent-browser (Vercel Labs) is an exact-pinned dependency; its npm package ships a native binary per platform, and the daemon runs that binary (browser/driver.ts). The only thing that can be missing is a Chrome: groundwork start (and groundwork install) prefers one you already have (Chrome, Chromium or Brave), remembers its path in ~/.groundwork/config.json, and otherwise runs agent-browser install (Chrome for Testing, about 180 MB, once), printing its progress. Meanwhile the heartbeat reports the browser as installing and jobs run without the browser tools.

Reads are headed but invisible: on Linux without a display agent-browser starts a private Xvfb (install xvfb if groundwork doctor says it is missing); on macOS the window is minimized. Each read is one agent-browser batch --json --bail with the commands on stdin, and reads on a session are serialised so concurrent jobs cannot navigate over each other.

The public web only. Nothing on this machine or its network is read (loopback, RFC 1918, link-local and cloud metadata, CGNAT, unique-local IPv6, localhost/.local/.internal names). The URL is checked before navigating, including what its name resolves to; every batch starts by installing a network route that aborts all plain-http requests, so a redirect to http://127.0.0.1:… or http://169.254.169.254/ never leaves the browser; and the URL the page ended on is checked again, returning nothing if it is private, not https, or (for search) off the engine's host. A redirect to an https host on a private network is still fetched before that last check refuses it (docs/browser-capability.md §2.1).

Isolated from your own agent-browser. Every agent-browser call runs with --config ~/.groundwork/agent-browser.json (always {}), --auto-connect false, no AGENT_BROWSER_* variable from your environment, and its working directory in ~/.groundwork/run/browser. So an autoConnect, cdp, provider or plugins setting you made for your own agent-browser use can never make a read use your logged-in Chrome.

Takeover

When a page puts up a challenge, a person can clear it by hand from the web app (browser/takeover.ts). Convex can request a takeover and can never drive one:

  • Loopback: the bridge listens on 127.0.0.1 only, answers only requests addressed to 127.0.0.1:<port> (no DNS rebinding), and serves its ticket only to a fetch() from an allowed web origin: the ones pinned with login --origin, plus GROUNDWORK_APP_ORIGINS (else DEFAULT_APP_ORIGINS). All of them come from this machine; the server can never add one. A navigation or frame (Sec-Fetch-Mode: navigate, a non-empty Sec-Fetch-Dest) and a browser request without an Origin never get it.
  • WebRTC for a remote daemon: the signalling is sealed under a key you carry by hand (groundwork takeover-key); Convex only brokers, and no frame or input is ever stored there. TURN addresses are pinned in local config.
  • Input is mouse and key events only (toInputCommand).

Environment

| Variable | Default | Meaning | |---|---|---| | GROUNDWORK_CONVEX_URL | production | The deployment's .convex.site URL. | | GROUNDWORK_HOME | ~/.groundwork | Credentials, device key, config, mcp.json, browser profiles, workspaces, checkpoints, the empty run dirs, and the empty agent-browser.json every browser call uses. | | GROUNDWORK_DEVICE_NAME | hostname | This machine's name in the web app. | | GROUNDWORK_MAX_CONCURRENT_JOBS | 3 | Jobs run at once (1–16). | | GROUNDWORK_ACTIVE_HOURS | always | Claim only in this UTC window, e.g. 22-6. | | GROUNDWORK_DAILY_CEILING | none | At most this many jobs per UTC day. | | GROUNDWORK_MODELS | all enabled | Comma-separated model keys this machine may run: a ceiling on the Settings → Devices selection. Only listed models run; if none of the selected ones is listed, the listed ones do. | | GROUNDWORK_SKIP_BROWSER_SETUP=1 | off | Never download a browser automatically. | | GROUNDWORK_BROWSER_EXECUTABLE | found by setup | The Chrome to drive. | | GROUNDWORK_BROWSER_CLI | packaged binary | A different agent-browser (development). | | GROUNDWORK_APP_ORIGINS | DEFAULT_APP_ORIGINS in browser/takeover.ts | More web origins allowed to fetch the loopback takeover ticket, added to the ones pinned with login --origin. The baked-in list is the production web app, https://groundwork.indexlabs.dev (npm run dev sets http://localhost:3000). | | GROUNDWORK_TURN_SERVERS, GROUNDWORK_ICE_SERVERS | STUN only | WebRTC servers for remote takeover. | | GROUNDWORK_CLI_CLAUDE_CODE, GROUNDWORK_CLI_CODEX | on PATH | A different model CLI executable (a .js runs through Node; used by npm run dev). |

Development

npm run build:daemon     # from the repo root: builds @indexlabs/groundwork-harness first, then syncs src/shared and runs tsc
npm run dev:daemon       # the same, then: paired → real loop; otherwise an in-process mock
npx turbo run test --filter=@indexlabs/groundwork
GROUNDWORK_LIVE=1 npx vitest run src/harness/live.test.ts   # real CLIs, real tokens

The daemon compiles against @indexlabs/groundwork-harness's built dist/, so go through turbo (above), not npm run … --workspace @indexlabs/groundwork: that skips the harness build and compiles against a stale one.

npm run dev without credentials runs the in-process MockJobServer (mock/mock-server.ts, the same contract as Convex, including attempts, retries, parking, the probe heartbeat, cancel: true for a run it has moved on from, and the §11 routes: /suspend and resume(jobId, answer), artifacts, child tasks, schedules and notifications) with the mock CLI (mock/mock-cli.ts) standing in for claude and codex, and queues one sample task. The mock CLI plans, saves a memory and searches when those tools are offered, then finishes; GROUNDWORK_MOCK_STEP_MS=<ms> slows every step down so a run can be watched or cancelled. It also works against a real deployment: point GROUNDWORK_CLI_CLAUDE_CODE at dist/mock/mock-cli.js. src/shared/ is generated from packages/llm/src by scripts/sync-shared.mjs; edit it there.

| File | Role | |---|---| | cli.ts, env.ts | Commands and environment knobs. | | daemon.ts | Heartbeat, claim pool, cancellation, progress, settle. | | protocol.ts, job-source.ts, http-source.ts | The wire contract, its parsers, and the production transport. | | harness/run.ts, harness/models.ts, harness/router.ts, harness/tools.ts | From a claimed job to a harness run (resume and the Jev judge included). | | harness/jev-uses.ts | Jev's advisory uses: recall, finish and stuck checks, the injection screen, the judge. | | tools/ | The §11 tools: the fetch guard, http_fetch/pdf_read, the workspace, calculate, and the account tools. | | mcp/ | mcp.json, the schema check, and the bridge (SDK stdio / Streamable HTTP clients). | | checkpoints.ts | Paused runs' steps on this machine (the fast path; the server holds a portable copy, persist/). | | persist/ | The audit record (§12): scrubbing, blobs (dedup, gzip, verified downloads), records, workspace snapshots, checkpoint manifests, restore on another machine. | | capabilities.ts, models.ts, model-settings.ts | CLI detection, model discovery, the user's selection. | | spawn.ts, index.ts | The reviewed executables and the model-key boundary. | | device-key.ts, credentials.ts, config.ts | This machine's key pair, pairing, remembered settings. | | browser/ | Driver, read-only bridge, setup, service, takeover (loopback + WebRTC). | | mock/ | The mock server, the mock CLI, and the server's sealing routine for tests. |