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

@gsjmedia-os/gsjos

v0.5.1

Published

Headless access to a gsjmedia OS workspace — CLI and MCP server over one API core.

Readme

gsjos — the OS client

Headless access to a gsjmedia OS workspace, with two faces over one core:

gsjos people.list --q "acme"      # CLI — humans, scripts, cron, CI, agents that shell out
gsjos pipeline.move <id> proposal_sent
gsjos mcp                         # MCP server (stdio) — Claude Code, Codex, Cursor, OpenClaw

This is Layer 1 of architecture/11-agent-runtime-and-fleet-plan.md: the one door through which any agent reads context and takes actions. Runtimes are replaceable; this is not.

Why it is shaped like this

One capability set, two syntaxes. src/verbs.mjs defines every verb once — name, arguments, route, human rendering. The CLI parses flags into it; the MCP server generates JSON Schema from it. Adding a verb there adds a CLI command and an MCP tool simultaneously, with identical semantics. Neither face can drift from the other because neither has its own logic.

Everything goes through /api/v1, never Postgres directly. So an agent gets the same guardrails a human gets: legal status transitions enforced, the same triggers fired, the same row-level security applied. An agent can be wrong in judgment; it cannot be wrong in data integrity.

Zero dependencies. An agent host can npx @gsjmedia-os/gsjos mcp with nothing else installed, and there is no SDK version to drift. The MCP stdio transport is ~120 lines of JSON-RPC.

Setup

npm i -g @gsjmedia-os/gsjos
gsjos login          # asks for API URL, workspace slug, and an API key

login prompts only for what you did not pass, so it also runs where nothing can answer a prompt — a provisioning script, or an agent session with no TTY:

gsjos login --url https://os.gsjmedia.co --workspace gsjmedia --key gsjos_live_…
op read op://vault/gsjos/key | gsjos login --url … --workspace … --key -   # keeps it out of history
GSJOS_API_KEY=gsjos_live_… gsjos login --url … --workspace …

The key is read from --key, then GSJOS_API_KEY, then a prompt. With no TTY and neither of the first two it fails naming them, rather than hanging on a question nothing will answer.

Node 20+ for the CLI and the MCP server. Node 22+ for gsjos daemon, which needs a global WebSocket to reach the local agent runtime — it fails at startup with that message rather than midway through a run.

Create the key in the OS: Settings → Agent runtime → API keys. It is shown once.

A key is not a password and not a service-role key: the server binds it to a workspace agent identity that sits in workspace_members with role='agent', so requests made with it are held to exactly the same row-level permissions as a person's. A key cannot reach another workspace.

Credentials resolve from the environment first, then ~/.gsjos/config.json — so a laptop uses gsjos login and a provisioned droplet gets them injected:

GSJOS_API_URL=https://app.example.com
GSJOS_API_KEY=gsjos_live_…
GSJOS_WORKSPACE=gsjmedia

As an MCP server

Any MCP client, e.g. Claude Code:

claude mcp add gsjos -- npx -y @gsjmedia-os/gsjos mcp

Or by config, for a client that reads one:

{
  "mcpServers": {
    "gsjos": {
      "command": "npx",
      "args": ["-y", "@gsjmedia-os/gsjos", "mcp"],
      "env": {
        "GSJOS_API_URL": "https://app.example.com",
        "GSJOS_API_KEY": "gsjos_live_…",
        "GSJOS_WORKSPACE": "gsjmedia"
      }
    }
  }
}

The server's initialize response tells the agent to call schema first. That verb returns the workspace's status vocabularies, legal pipeline stages, and writable fields — generated from the same modules the UI and the database constraints use, so it cannot go stale.

Verbs

Run gsjos help for the list, gsjos help <verb> for one in detail.

| | | |---|---| | schema | status vocabularies, legal transitions, writable fields | | people.list · people.get · people.update | the prospect warehouse | | companies.list | firmographics | | pipeline.list · pipeline.move | deals by stage | | search | people + companies by name | | interactions.log | log a touch — inbound promotes to a lead via a DB trigger | | tam.list · tam.create · icp.list · icp.create | targeting | | runs.list · runs.enqueue · agents.dispatch | the agent work queue | | webhooks.list · webhooks.create · webhooks.delete | subscribe a URL to workspace events |

Add --json to any verb for raw output instead of the human rendering.

Walking a large workspace

List verbs are paginated. --limit sets the page size and --cursor takes you to the next page, but you rarely need either — --all walks every page for you:

gsjos people.list --all --json > people.json

The pagination is keyset, not offset, so a walk of 50k people will not skip or repeat a row even while the workspace is being written to underneath it. That is what makes --all safe to point at an export.

List values, and commas that are data

A list argument splits on commas. Plenty of real values contain one — the LinkedIn industry Technology, Information & Internet is a targeting profile away from becoming two industries nobody chose. Escape it, or repeat the flag:

gsjos tam.create --name "Agencies" --industries 'Marketing Services,Technology\, Information & Internet'
gsjos tam.create --name "Agencies" --industries "Marketing Services" --industries "Technology, Information & Internet"

Both produce two entries, with the comma intact in the second. The MCP face takes a real array, so this is a shell problem only.

Re-running a write

The API replays a POST that carries an Idempotency-Key it has already seen, for 24h, rather than running the handler again. --idempotency-key is how you send one:

gsjos tam.create --name "Agencies" --idempotency-key tam-agencies-v1

Run that script twice and you get one TAM and the same response, not two TAMs. Key it per logical operation — not per retry — and a re-run of the whole script is safe. Reusing a key with a different body is a 409, deliberately: that is a caller bug and hiding it would make it permanent.

Permissions

A key can be narrowed to individual verbs. If a call comes back with

this API key is missing the 'pipeline.move' scope

the key was issued for an agent whose agent.md does not declare that verb. Widen the grant in the agent definition and re-issue the key — do not reach for a broader key.

Tests

node test/smoke.mjs

Drives the MCP server over a real stdio pipe against a stub API: protocol handshake, tool listing, tool calls, error shapes, plus CLI argument parsing and auth headers. It asserts on the request body the stub receives, not just the exit code — an argument that parsed into the wrong shape is invisible from a 200. It covers every non-interactive login path, walks a paginated stub end to end, and checks every verb here against the server's own scope catalog in app/src/lib/api/verbs.ts — a verb the CLI can call but the server cannot scope would be a hole in the permission model, so the two lists are not allowed to drift.