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/source-intents

v0.5.2

Published

Part of PAC: @projectpac/source-intents.

Readme

@projectpac/source-intents

What the principal wants done, as one source this adapter keeps in its own folder: intents a model extracted from the other sources on the node -- the coding-agent history and the conversation notes -- kept up to date as that material grows, each resting on short verbatim excerpts of what was said, and never the material itself.

Why a third source over the other two's material

agent-history reads the transcripts and answers who this person is. conversation-notes mirrors a file a skill inside Claude writes and answers what they talked about this week. Neither answers the question a flow could act on without being asked: what does this person want done? That is what this keeps.

It reads nothing itself. Each source it derives from implements @projectpac/material -- units(), which lists what moves as one thing, and read(), which hands one unit over as observations a model may cite -- and this plugin asks. It opens no file the principal's material lives in, stages nothing, and carries no readers of its own. That is why it is not a consent of its own on the capture step but comes along with whichever source is chosen: installing a source is the consent to read that source, and a node that installs this without one has nothing to extract from and says so.

The inputs

An input is a row in the config naming a source by the service key it claims:

{
  "inputs": [
    { "id": "agent-history", "service": "agentHistory" },
    { "id": "conversation-notes", "service": "conversationNotes" }
  ]
}

Those two are the default. Each is reached from an optional child scope rather than injected, the way the flows reach the router: a node with only one of them still runs this, and one that installs the other later contributes it on the next pass. A source that is not installed contributes no units and one log line. A third kind of material is a source that implements the contract and one more row here -- no code in this plugin.

| input | material | a unit | evidence is | | -------------------- | ------------------------------------------------- | -------------- | --------------------- | | agent-history | Claude Code transcripts, through its own readers | a session file | user or assistant | | conversation-notes | the notes the daily-profile skill writes hourly | one note | summary |

A unit can be told apart without being read -- what stat says of a session file, a digest of a day's text. A pass lists every unit, reads only the ones that moved, and packs whole units into chunks, so a session that grew or a note the skill added dirties exactly one chunk and every other slice, already paid for, stands. The one thing this plugin does to what it is handed is defuse the intelligence loop's own delimiters, because it is the plugin that puts the text in a prompt and the loop refuses a context carrying one. A summary is an account an assistant wrote about the person's conversations, not their words; the model is told so, twice, and the evidence says so.

What an intent is

The shape is a General User Model's proposition (arXiv 2505.10831) bent to one question, with what a flow needs to act on the answer.

| field | what it is | | ----------------------- | --------------------------------------------------------------------------------------------------------- | | id | twelve hex characters, minted here and never by the model | | intent | one sentence, in the person's own terms, specific enough to act on | | reasoning | how the evidence supports it; describes, never quotes | | basis | explicit when the person asked in as many words, inferred when it is a fair reading | | confidence | 1 to 10, asked of the model directly, which GUM finds calibrates better than anything read off its logits | | status | open, done or dropped | | projects | the sources' names for the directories the sessions ran in, matched rather than trusted | | evidence | up to eight citations: which input, which unit, which message, whose words, when, and the excerpt | | firstSeen, lastSeen | the earliest and latest time the material showed it, from the material's own timestamps | | revised | a digest of the pass that last changed the row |

Three things GUM does are left out on purpose. Its retrieval -- BM25, decay, a classifier labelling pairs identical or similar -- is one revise run handed the whole list, because the list fits in one context. Its decay is the status the revise run sets instead, with the timestamps beside it. And its "at least five propositions" quota is gone: an empty answer is the right one for a slice that shows nothing, and a quota manufactures work out of an ordinary conversation.

Propose, then revise

Each changed chunk gets one run. It answers with up to twelve intents, each citing one to three messages by id with an excerpt copied verbatim. A citation is checked rather than trusted: the excerpt has to be a substring of the message it names, or it is dropped, and an intent left with no citation that passes is dropped with it. Nothing this adapter writes down as evidence is something the model composed.

Then one run is handed the list as it stands and every fresh proposal, and answers with what should change: a row with an existing id is that intent, updated; a row with none is new; several proposals for one outcome become one row. What the model may not do is enforced in merge.ts rather than asked for. An id counts only if the run was shown it. A row the answer leaves out is kept exactly as it was, so leaving one out is never how to remove it. A new row has to draw on a proposal, because the run may consolidate but not originate. And a done or dropped intent reopens only on evidence newer than the last time it was seen, so a slice re-read for some other reason cannot revive what the person has since finished.

With nothing held and one slice proposing, the revise run is skipped: it would only be asked to repeat the propose run's answer, and dedupe within one slice is that run's own job.

What comes out

One source, intents: the open rows, newest first. Done and dropped rows are the principal's history rather than work, so they stay in the record store for the next revise and out of the file a flow can stake.

Two verbs, the same two the other sources offer: inventory() answers with the name and a sentence about it -- counts, never a row -- and stage() copies the file into the calling plugin's folder and answers with a relative path. That is what a flow hands to a box. No route reads the list out, for the same reason no route reads the profile out: it is a source. And it offers no Material of its own: the list is a model's conclusions, and what can be reasoned over is the sources this one reads.

Running it

It runs on its own: a first pass a minute after activation, then one every everyMinutes, an hour by default. A tick that finds nothing moved is a survey and no run, and one that finds a pass still out does nothing. The log says when a scheduled pass starts and what it is reading, and nothing when it had nothing to do. everyMinutes: 0 is a node that would rather be asked.

Asking is the same pass, with an answer:

pac api POST /flows/intents/extract

answers 202 {extracting, chunks, toReason, projects, inputs, intents} once it has asked what moved and knows what it will cost, and leaves the runs going. Asking again while a run is out is a 409; asking after a pass finished, with nothing changed since, is a 200 {extracting: false} rather than a bill. {"force": true} reads everything again and folds it into the list that stands -- which is how a prompt change reaches a history that has not moved.

pac api GET /flows/intents/extract

is how far it got: the pass state, every chunk's, which inputs contributed, and how many intents are open, done and dropped. Never a row.

What the schedule costs. The first pass over a real history is agent-history's ten runs plus one. After that a tick pays for exactly what moved, and the thing that moves most reliably is the transcript of the session the principal is in right now: while they work, an hourly tick is a propose run over that session and a revise, and while they do not, it is nothing. That is the bound an hour buys, and why the default is not a minute.

It needs a model, and it keeps its rows through @projectpac/records, so GET /plugins says waitingFor: ["records"] until that is installed. The sources it reads are not waited on: each contributes when it is there.

| field | default | why | | ------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------- | | inputs | agent-history and conversation-notes, as above | which sources to reason over, each by the service key it claims | | maxChunkBytes | 640 KiB | a packing size, under agent-history's: every message carries an id and a time | | maxConcurrentRuns | 2 | politeness toward every other flow's runs, not parallelism | | maxChunkAttempts | 3 | a chunk that keeps failing is not retried by every pass forever | | executor | cli | the executor row every run names; cli is what every setup writes | | maxRevisedIntents | 200 | how many intents the revise run is shown: open first, newest first | | everyMinutes | 60 | how often a pass runs on its own; 0 is only when asked |

What this writes down

More than the profile does, and it should be said plainly. The source carries verbatim excerpts of what the principal typed to their agent and of what the skill wrote about their conversations. The transcripts arrive through agent-history's redaction and with its tool output already dropped, which is where credentials actually live -- but anything that stages this source receives those excerpts. And the intelligence store keeps every run's prompt regardless of what this plugin writes: the material is never a source, but a chunk of it is a run's context, and that is the node's to keep.

Not yet

No route dismisses an intent, because no route lists them for a person to choose from; that surface comes with a face that can show ids. Two existing intents that turn out to be one cannot be merged, since leaving a row out keeps it. A claude.ai data export, or a Codex or Cursor transcript, is a reader in agent-history, and reaches this plugin the moment agent-history has it.