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

thinkwork-cli

v2.0.0

Published

thinkwork — operator CLI over the Brain Operator API (also installed as `brain`)

Readme

brain

The operator CLI over the Brain Operator API (platform/ops_api). One binary, no AWS credentials, no repo checkout — everything a person or an agent needs to land a source, map it, project it, and check that it is actually right.

It lives next to the platform because it is version-matched to ops_api: the CLI refuses to send a request it cannot be sure the connected API understands (see Version handshake).


Install

From npm (public package thinkwork-cli; installs both thinkwork and brain):

npm install -g thinkwork-cli
# or
brew install thinkwork-ai/tap/thinkwork

Or the standalone binary, one command (needs the gh CLI, authenticated — brew install gh && gh auth login once):

gh api -H "Accept: application/vnd.github.raw" \
  repos/thinkwork-ai/thinkwork/contents/apps/cli/install.sh | bash

That runs apps/cli/install.sh, which:

  1. downloads the newest cli-v* release binary for your platform and verifies it against SHA256SUMS;
  2. installs it as brain into /usr/local/bin (if writable) or ~/.local/bin;
  3. syncs the Build-a-Brain skill from the same tag into ~/.claude/skills/build-a-brain (and ~/.codex/skills when a ~/.codex exists), so the agent playbook ships with the tool;
  4. migrates an old ~/.brainctl config dir to ~/.brain.

Re-running it upgrades everything — binary and skill together. BRAIN_INSTALL_DIR overrides the install location. Assets exist for darwin-arm64/x64 and linux-x64/arm64.

Why gh and not plain curl: the repo is private, so unauthenticated release URLs 404 — and gh downloads carry no macOS quarantine xattr, so Gatekeeper never fires (the binaries are ad-hoc signed, not notarized — THINK-574). If a browser downloaded a binary, rescue it with xattr -d com.apple.quarantine ./brain-darwin-arm64.

brain version prints both the CLI version and the connected API's surface version, and the CLI refuses to talk to an API newer than itself, so a stale binary tells you loudly rather than misbehaving.

From a checkout (development):

cd cli
pnpm install
pnpm build          # → dist/cli.js
pnpm link --global  # then: brain --help

During development, pnpm dev -- <args> runs it from source via tsx.

Cutting a release (maintainers): bump version in cli/package.json (and src/version.ts), merge, then tag the merge commit cli-v<version> and push the tag — .github/workflows/cli-release.yml compiles all four targets with bun build --compile, smoke-tests the Linux binary, and attaches the binaries plus SHA256SUMS to a GitHub Release. The tag must match the package version or the workflow refuses.

Auth

The operator key comes from the environment or the config file. Never a flag — flags land in shell history and in ps output, so --key, --operator-key, --token, --api-key and --bearer are all refused by name with an explanation.

Resolution order:

  1. $BRAIN_OPERATOR_KEY
  2. the profile's keyEnv variable (e.g. keyEnv: "BRAIN_OPERATOR_KEY_TEI")
  3. the profile's stored key (last resort — for containers with nowhere else to put it)
export BRAIN_OPERATOR_KEY=bk_…

The key goes out as Authorization: Bearer <key>. That is the entire contract with the operator-keys work (THINK-511); the CLI never parses or interprets it, so a Cognito JWT works today wherever an operator key will work tomorrow.

The Cognito M2M fallback (THINK-573)

When no static key resolves, a profile may carry an auth block — the token endpoint, the app client id, and the name of the env var holding the client secret (the secret itself never touches the config or the command line):

brain config set-profile tei --api-url https://… \
  --token-url https://….amazoncognito.com/oauth2/token \
  --client-id 3abc… --client-secret-env TEI_M2M_SECRET
export TEI_M2M_SECRET=…    # from the account's Cognito app client
brain auth login        # verifies the wiring, caches the token

After that, every invocation checks the cached token's own exp and silently re-runs the client-credentials grant when it is within two minutes of expiry (plus one forced re-grant + retry after any 401) — an hourly token expiry never kills a scripted loop. brain auth status says which credential the next command would send without printing a byte of it; brain auth logout drops the cache (~/.brain/tokens.json, mode 0600). When a refresh is impossible the failure is exit 4 saying "token expired mid-run", not a bare 401 one call later. A long-lived bk_ key is still the recommended credential.

Profiles

Picking the wrong account is the expensive mistake, so brain never guesses one, and the active profile appears in every output — a banner line in human mode, a "profile" key in the JSON envelope.

brain config set-profile dev       --api-url https://…  --description "thinkwork dev" --default
brain config set-profile tei       --api-url https://…  --key-env BRAIN_OPERATOR_KEY_TEI
brain config set-profile mcpherson --api-url https://…
brain config list
brain config current      # which account this invocation would hit, and why

Config lives at ~/.brain/config.json (override with $BRAIN_CONFIG). Selection order: --profile$BRAIN_PROFILEdefaultProfile$BRAIN_API_URL (an explicit ad-hoc profile, reported as env) → error.

Output

--json works on every command. Machine-readable is the default posture; the pretty renderer is the extra.

  • Data on stdout. Everything else on stderr — warnings, banners, and all errors. On failure stdout is empty, so | jq never sees half a document.

  • Success envelope:

    {"ok": true, "command": "pipeline validate", "profile": "tei",
     "dryRun": true, "data": { … }}
  • Failure envelope (on stderr):

    {"ok": false, "command": "pipeline save", "profile": "tei",
     "error": {"code": "validation_failed",
               "message": "extract.cursor_field is required in incremental mode",
               "field": "extract.cursor_field",
               "hint": "set extract.mode to \"snapshot\" if this source has no cursor",
               "retryable": false, "status": 400},
     "exitCode": 3}

Adding keys to data is fine; renaming envelope keys is a breaking change for every agent parsing us, and is pinned by tests.

Human mode is a bounded view

--json is the whole document. Human mode is a reading of it, and it is allowed to be shorter: nested structures are shown two levels deep, lists are capped, long strings are truncated, and naturally large result sets have a --limit. Whenever anything was left out, a note says so on stderr — human mode never silently shows you part of an answer, and it never prints raw JSON as if it were a rendering. Commands with a table (profile column, iceberg tables, ontology get, duplicates list, sql, lake batches, run list, …) render one; everything else gets the bounded summary.

An empty cell prints . A blank cell used to mean "the renderer read a field the API does not send" — that is how profile column printed eight unlabelled rows — so blank is never a legitimate rendering now.

Dry run by default

Every write is a dry run unless you pass --apply. This deliberately inverts the API, where dry-run is opt-in (?dry_run=true). Different callers, different failure modes: an HTTP caller who forgets a query param should not silently no-op, and a human at a terminal who forgets a flag should not silently mutate a live account.

brain pipeline save dtn_prices --spec ./dtn.json          # shows the diff, writes nothing
brain pipeline save dtn_prices --spec ./dtn.json --apply  # commits

Consequence worth knowing: against an API that does not implement ?dry_run= (surface version 0 — that is ops_api today), a dry-run write is not sent at all and exits 8. Sending it and hoping the unknown query param were ignored would mean a bare pipeline save writes to production, which is precisely what this default exists to prevent.

Destructive verbs (connection delete, pipeline delete, derived delete, run backfill --force) additionally require --yes or an interactive confirmation. On --apply the CLI also mints the server-side confirmation token operator keys require (POST /operator-keys/confirmX-Brain-Confirm, bound to your key and that exact path, five-minute TTL) and sends it with the DELETE. A Cognito session (403 "does not need one") and an API predating operator keys (404) proceed without one; a role that may not run destructive verbs fails there, rather than as a confusing 403 one call later.

collection ingest's dry run is entirely local — it computes the manifest, checksums included, and makes no HTTP request at all — so it works on every API version.

pipeline validate has no --apply form. It cannot mutate, whatever you type.

Exit codes

| Code | Name | Meaning | Retry? | |---|---|---|---| | 0 | ok | Success. A dry run that produced a clean diff exits 0 too. | — | | 1 | error | Unexpected / unclassified failure. | investigate | | 2 | usage | You invoked the CLI wrong: bad flag, missing argument, unknown command, destructive verb without --yes. | no | | 3 | validation_failed | HTTP 400/422. The payload is wrong; error.field says where. | no — fix the input | | 4 | auth_failed | No key, bad key, or the key's role may not do this (401/403). | no | | 5 | not_found | The named thing does not exist (404). | no | | 6 | conflict | Blocked by state — a reference guard, a running batch (409). | after fixing the blocker | | 7 | transient | Timeout, throttle, 502/503/504, network. | yes | | 8 | unsupported | The connected API does not implement this yet, or this CLI is stale. | no — upgrade one side | | 9 | config_error | No profile selected, unknown profile, unreadable config. | no |

brain exit-codes prints the table. The numbers are pinned by __tests__/exit-codes.test.ts — never renumber one, add a new code instead.

A retryable field in the API's error body overrides the status mapping, so an agent can branch on one field instead of parsing prose.

A bare 500 is exit 1, not exit 7. "5xx is transient" used to be wholesale, and a deterministic 500 told an operator to retry three times in a row. Only the statuses that describe infrastructure — 408, 425, 429, 502, 503, 504 — are retry evidence on their own; 500 is an unclassified server failure and 501 is exit 8 (unsupported), which is what the API is actually saying. The body's retryable: true still promotes any of them to exit 7, and retryable: false still demotes.

Version handshake

The API reports its operator surface version on GET /health, either as the x-brain-surface-version header or as a surface_version body field. An API that reports neither is version 0 — today's ops_api, before operator keys and the /operator/* capabilities land. Version 0 is fully supported: every command whose endpoint already exists works against it.

The CLI declares which version it speaks (SURFACE_VERSION_SUPPORTED in src/version.ts) and which version each capability needs:

| Capability | Needs | |---|---| | ?dry_run=true on writes | 1 | | profile column (POST /operator/profile) | 1 | | sql (POST /operator/sql) | 1 | | duplicates list (GET /operator/duplicates) | 1 | | ontology get (GET /operator/ontology) | 1 | | diagnostics * | 1 | | mapspec draft | 1 | | mapspec get / mapspec save (GET/PUT /operator/mapspec/{name}) | 2 | | ontology create-type (POST /operator/ontology/entity-types) | 2 | | facet-drift (GET /operator/facet-drift) | 2 | | derived history (GET /lake/history) | 3 | | derived state-at (GET /lake/history/{name}) | 3 |

Two failure directions, both exit 8, both before a byte is sent:

  • API older than the command needs → "operator.sql is not available on the connected API (it reports operator surface version 0; needs version 1)"
  • API newer than the CLI speaks → "this brain speaks operator surface version 1, but the connected API reports version 2 — refusing to send a request it may not understand"

brain version prints both sides. The handshake result is cached per API URL for five minutes ($BRAIN_SURFACE_TTL seconds; 0 disables), so a shell loop does not pay for it per iteration.

Command inventory

brain connection    list | get | test | save | delete
brain pipeline      list | get | schema | validate | save | preview | trigger | enable | disable | delete
brain mapspec       validate | preview | draft* | get** | save**
brain derived       list | get | preview | history*** | state-at*** | save | delete
brain projection    list | get | run | shadows
brain run           list | get | retry | backfill
brain collection    list | documents | ingest | sync | settings
brain catalog       list | get
brain lake          batches | files | preview
brain iceberg       tables | table | files
brain graph         search | counts | ontology | cypher
brain identity      canonicals | resolve | duplicates | census | crosswalks | conflicts
brain profile       column*
brain sql*
brain duplicates    list*
brain ontology      get* | create-type**
brain facet-drift**
brain diagnostics   freshness | failures | crosswalks*
brain evals         runs (list | show | results | launch | cancel | watch)
                    cases (list | show | pin) | seed | profiles (list | create | set-default | archive)
                    schedules list | baseline pin | compare | candidates (list | promote)
                    ab (launch | list | show)
brain manifest      resolve | latest
brain config        list | current | set-profile | use
brain health | version | exit-codes

* = needs operator surface version 1 (the WP3 endpoints). ** = needs version 2 (the build half). *** = needs version 3 (THINK-547 bi-temporal history). All fail with an instructive message — exit 8, nothing sent — against an API that is older.

derived history lists the datasets whose spec declares a temporal block, with each one's version census; derived state-at <name> reads a dataset's rows as of a moment on either clock — --at is recorded time (what did we believe then; omit it for the current belief) and --valid-at is valid time (what was in effect then, on the source's clock). The two compose: both together is "what did we then believe was then in effect".

The build half exists because a credential-free dogfood on 2026-08-02 could author a mapping and not save one, read an ontology and not extend it, and see none of the drift between the two. mapspec save is the verb that closes the authoring loop; facet-drift is the one that keeps it honest, because "add the build verbs without it and you have only let agents ship wrong things faster."

Two things mapspec save is deliberate about. It writes targets.neptune and nothing else in the spec, so a mapping edit cannot resend a stale extract block — and it writes only that language: the retired mapspec (facet/product_load) block is refused by name rather than dropped silently. ontology create-type needs an admin key on purpose; a builder gets a 403 that tells it to stop and ask a human, which is what the skill already says.

evals runs watch <id> is the CI gate: it polls a run to a terminal status and exits 0 only when the run completed and cleared --min-pass-rate (when given). The failure codes are distinct so a script can branch — exit 3 when the run completed below the gate, exit 1 when the run failed or was cancelled, exit 7 when it was still not terminal at --timeout-seconds (the run keeps going server-side; re-running the watch is safe). Every /evals route is gated, reads included, so the configured credential either works everywhere or 403s everywhere.

manifest resolve <id> turns a snapshot manifest — the pinned data version a run report or projection pass recorded (THINK-798) — into the per-table SELECT * FROM "db"."table" FOR VERSION AS OF <snapshot> queries that reproduce what that claim read. It is emit mode: the query text is printed, never run, so raw-data access rides your own Athena entitlements and no field mask is applied. Pins carry no existence check, so the command cannot know what expired; pins older than the 90-day ice_* retention window may be gone, and an expired pin still prints in full because the manifest's identity survives its snapshots. See docs/runbooks/snapshot-manifest.md.

Three spellings of help all resolve to the same page: brain help <group> <sub>, brain <group> help <sub> and brain <group> <sub> --help. A path that does not exist is a usage error (exit 2), never the parent's help page.

sql runs on Athena over the lake — the Glue/Iceberg tables, not a source system and not the Aurora platform database. The engine and database it hit are in the JSON and in the human header.

pipeline preview, mapspec preview and mapspec validate share one endpoint and differ in what is under test: the pipeline, the MapSpec, or the MapSpec with a pass/fail verdict (the only one of the three that can exit 3). See brain pipeline preview --help.

Note the two similarly-named nouns: config manages account profiles; profile column is column profiling. Likewise identity duplicates covers duplicate canonicals in the Register while duplicates list covers duplicate nodes in the projected graph.

Worked example — onboarding a source as a shell loop

The design bias is composability: building twenty pipelines is a loop, not twenty conversational turns.

#!/usr/bin/env bash
set -uo pipefail
export BRAIN_OPERATOR_KEY=bk_…
PROFILE=tei

# 1. Point at the account once, then never think about it again.
brain --profile "$PROFILE" config current --json | jq -r '.data.apiUrl'

# 2. Does the connection work? (Shape only — no credential is ever returned.)
brain --profile "$PROFILE" connection test dtn --json || exit $?

# 3. Validate every generated spec. Nothing is written; failures are per-spec.
failed=()
for spec in specs/*.json; do
  name=$(basename "$spec" .json)
  if out=$(brain --profile "$PROFILE" --json pipeline validate "$name" --spec "$spec" 2>err.json); then
    echo "ok   $name  $(jq -c '.data.would_change // []' <<<"$out")"
  else
    case $? in
      3) echo "bad  $name  $(jq -r '.error.field' err.json): $(jq -r '.error.message' err.json)"
         failed+=("$name") ;;
      7) echo "retry later: $name" ;;
      *) echo "stop: $(jq -r '.error.message' err.json)"; exit 1 ;;
    esac
  fi
done
[[ ${#failed[@]} -eq 0 ]] || { echo "${#failed[@]} spec(s) need fixing"; exit 3; }

# 4. Commit them, one apply each.
for spec in specs/*.json; do
  brain --profile "$PROFILE" --json pipeline save "$(basename "$spec" .json)" \
    --spec "$spec" --apply >/dev/null || exit $?
done

# 5. Land one, then verify it before mapping anything.
brain --profile "$PROFILE" --json pipeline trigger dtn_prices --apply
brain --profile "$PROFILE" --json run list --source dtn --dataset prices --limit 1 \
  | jq -r '.data.runs[0].status'

# 6. Profile BEFORE mapping a column — this is the check that catches a field
#    that is present in the schema, extracted, mapped, and populated on 0% of rows.
brain --profile "$PROFILE" --json profile column \
  --source dtn --dataset prices --columns item_id,fuel_terminal_id \
  | jq -r '.data.columns[] | "\(.column) fill=\(.fill_rate) blanks=\(.blank_count)"'

# 7. Map: draft against the EXISTING ontology, validate, then SAVE it. The save
#    is the step that used to have nowhere to go.
brain --profile "$PROFILE" --json mapspec draft dtn_prices \
  --entity-type Terminal --connection brain-neptune | jq '.data.draft' > map.json
brain --profile "$PROFILE" --json mapspec validate dtn_prices --mapspec ./map.json
brain --profile "$PROFILE" --json mapspec save dtn_prices --mapspec ./map.json --apply

# 8. Project, then check BOTH failure shapes: duplicate nodes the projection may
#    have minted, and declarations the graph does not actually carry.
brain --profile "$PROFILE" --json projection run terminal_price --apply
brain --profile "$PROFILE" --json duplicates list --entity-type Terminal \
  | jq -r '.data.groups | length'
brain --profile "$PROFILE" --json facet-drift Terminal \
  | jq -r '.data.entity_types[].findings[]'

Step 8's facet-drift is the one that answers "is the thing I just shipped actually there?" — a mapping can validate, save, project and report loaded while every node it claims to write is missing the property. If it prints declared_never_projected for a property you just mapped, stop: the lane is green and the data is not.

Steps 6–8 need surface version 1 (profile, duplicates) or 2 (mapspec get/save, facet-drift); until the API ships them they exit 8 and say so, which is the loop's cue to stop rather than to proceed on unverified data.

Development

pnpm typecheck
pnpm test          # 174 tests; the HTTP layer is mocked and no live API is touched
pnpm build

Tests are the deliverable as much as the code. They pin: the exit-code table, stdout/stderr separation (including one genuinely out-of-process run against a local HTTP server), dry-run-by-default on every write, profile selection and the refusal to guess, missing-key handling, the JSON envelope shape, the version handshake in both failure directions, the confirmation-token flow, the command inventory, the shell-loop composability case, and the human renderers — a table that prints a blank column is a failing test, because it is not something a reader notices.