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

@projectpac/flow-skills

v0.6.0

Published

Part of PAC: @projectpac/flow-skills.

Readme

@projectpac/flow-skills

Each node holds instruction documents, advertises some of them, and a peer can ask for one to be applied to something.

A skill arrives as a markdown document with a little front matter, because a skill is something a principal writes and edits. The two halves then live apart, and that split is the design: the body becomes a source, so this flow names it and never holds it, and the front matter stays a declaration the flow can read without reading the skill. Describing a skill must not mean reading it, the same way describing a plugin must not mean running it.

Every call runs in a box both sides verify and neither owns. The caller stakes its request and whichever of its own private sources the principal authorized to; the owner stakes two things, the skill and the model access that pays for running it. Each side gets back only what it is entitled to: the caller an answer, the owner the fact that a run happened. Neither node's material passes through the other, and this flow holds none of it -- every step that touches material is an apply, and the box program is proposed byte for byte so both sides approve the same bytes. The program is ordinary python, the same bytes for every call: each side derives it from its own operation-llm rather than taking a peer's copy, and where it may reach is the session's declared egress, signed into the approval beside the program's hash.

There is no local mode. A skill run on the owner's own model would mean the caller's input leaving the caller's machine, and the body sitting in flow memory to be put in a prompt -- so removing it is what lets the body be a source at all.

What a skill declares

A skill is not this flow's to keep. @projectpac/source-skills holds the documents and answers what this node offers; this flow calls one. The document lives there:

---
description: Builds a music-taste profile from someone's listening history.
provider: openrouter.ai
needs: spotify_songs
---

All three are public, and that is the whole declaration. A skill used to also name the model it ran on and the file holding the key that paid for it; both are @projectpac/source-llm's now, because a credential pinned to a skill means only that skill's owner can ever pay to run one. provider stays because it is the disclosure -- the host a peer consents to, and the one word the box session's declared egress speaks.

needs names the caller's sources, not the owner's: it is what a caller is being asked to disclose, published so it can be read before anything is disclosed. A call may narrow that list and may never widen it, and naming a source outside it is refused rather than quietly dropped.

provider is a hostname, and it is the same word three times over: what a peer reads on the board and consents to, what this node's box adapter checks its own egress allowlist against, and what the box session declares its program may reach.

Starting a call takes three answers. A list is exactly those sources. An empty list is none of them, and the call runs on its request text alone. Omitted is what the skill declares and this node holds -- the router's case, since it picks the skill and names no sources.

Nothing here asks a principal, and no layer under it does either: there is no policy service on a node, so what a call may apply is decided by what the principal authorized when they made it. The one real refusal on a principal's behalf is the box adapter's egress allowlist, which is why a call to a provider this node has not allowed is refused by the side that holds the data. What is refused where it starts is a skill that reads only sources this node does not hold, because a call over none of the data comes back saying the data was missing and costs a box run to learn it.

A skill whose body is gone is declared but not offered: a peer that called it would get a failure this node could have predicted. Whether anybody can pay for it is a separate question, asked when a call is made -- a node that holds a skill and no access to its provider still offers it and refuses the call by name, because the skill is genuinely this principal's and hiding it would say something false.

The flow speaks for itself, and the host says nothing on its behalf. It declares its route to the network adapter, so an envelope addressed to it is dispatched into its own context and one addressed to a flow nobody declared is refused. It advertises on discovery that this node speaks it, under kind: "flow", with the version, the package, and the payload types a peer needs to join in or fetch it -- the rows the router's GET /flows/router/available reports. It declares its one intent to the router from an optional scope, so a node with no router still holds skills and answers its board, and one that gains a router later hears from it the moment the service comes up. It keeps its records through ctx.records, the record store it waits for before it serves anything. And it runs its own timers, each an effect of its scope, so disabling the flow stops them with it.

| | | | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | routes | POST /calls, GET /calls, GET /board -- writing a skill down and listing what this node holds are source-skills' routes | | payloads | call.request, call.failed, call.accepted, call.session (this flow's own: the caller's first message carries the skill and its party key together) | | records | calls, kept through ctx.records -- the record store the flow waits for | | background | advertise every 5s (what its sources offer), advance every 2s (the box state machine), sweep every 30s (terminate what stalled); timers of its own | | needs | the sources named in sources (skills for what it offers, whatever holds the caller's own data), @projectpac/operation-llm to derive the program, and a box adapter whose egress allows the provider | | intents | skill, declared to the router from an optional scope -- picked only for something the board is offering right now | | results | one call per call, both sides, declared to ctx.results from an optional scope: GET /flows/results/results reads them beside every flow's |

Five files: flow.ts is what the flow declares about itself and what every step has in hand, calls.ts a call from the ask to the answer -- the record, the box spec, the wire -- board.ts what the network is offering, program.ts the python both sides approve, and index.ts the wiring and the routes. Driving it by hand: devtools/README.md.

Running it

pnpm check       # includes tests/skills.spec.ts: two whole nodes, box double included
just demo        # headless: a directory, two seeded nodes, the board, down
just dev         # the lived-in lane: panes, pages, and the real box -- then, in another terminal:
just skill "three days in Tokyo, and I like food"

just dev owns the terminal it runs in, so the call goes in another one, once the seed pane rests. The text is the call's input; a second argument names any skill the board is offering (just skill "what do I listen to?" music-taste-profile), and it defaults to the trip-planner agent2 offers. just a1 api GET /flows/skills/calls shows the call and the answer on the same row; devtools/README.md has the other direction.

The call itself runs on somebody's model inside the box, so it needs a real key: copy .env.example to .env and put an OPENROUTER_API_KEY in it before seeding. What that variable is for is declared in pac-plugins/devtools/fixtures/<agent>/models/, which is committed; only the value is not. Without one the nodes seed with a placeholder and say so -- the skills advertise and the board fills, but the call fails in the box.

The sibling layout

Cross-repo dependencies are link: paths into checkouts beside this one -- nothing of PAC's is fetched from a registry. Clone the repos this one links (the link: values in the package manifests) into the same parent directory, run pnpm install, and everything resolves from source. The flow's whole-node suite lives in tests/, standing up real nodes from the sibling pac-node. The design repo (pac) is the front door: what PAC is, and how the repos fit together.