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

@miadi/hooks-interpreter

v0.11.0

Published

Interpreter/Observer role over Miadi hook streams: reads another agent's raw sessiondata JSONL, normalizes it into canonical events, projects the session, and emits RISE aspects plus a ceremony entry.

Readme

@miadi/hooks-interpreter

Read another agent's session trace and tell a person what it is doing, and whether to step in.

In the Miadi stack, @miadi/hooks-core records what happened in a session. This package reads those records, and the raw logs other agents write into the same folders, and answers what the session is becoming. It projects a session into its mission, tools, creations, corrections and state, writes four short RISE aspects about it, and builds a ceremony entry. It only reads: it never writes into session data or into the observed agent's repositories. digest runs read-only git commands in those repositories to check commits.

Works with:

  • @miadi/hooks-core: its canonical event map is applied first.
  • @miadi/hooks-gateway: owns transport and delivery, which this package does not.
sessiondata JSONL ──► read.ts ──► InterpreterEvent[] ──► project.ts ──► SessionProjection
                                                             ▲                │
                                                    classify.ts               │
                                              (what each count is worth)      │
                                                                              │
                                                          aspects.ts ◄────────┤
                                                        (4 RISE aspects,      │
                                                         ≤55 words each)      │
                                                         ceremony.ts ◄────────┤
                                                        (built, never written) │
                                                                              │
                       watch.ts ◄── poll ── diff ── emit ONLY what is new ────┤
                       fleet.ts ◄── many sessions, one line each ─────────────┤
                        pane.ts ◄── tmux correlation, confidence not identity ┤
                      digest.ts ◄── what an inventory needs, bounded ─────────┘

binding line ──┐
live registry ─┼──► sessions.ts ──► SessionJoin: live pane, recorded pane, team, episode,
inventory ─────┘                    inventory snapshot, keyed by session id

side ledgers (_terminal_binding, _claude_files_written, _claude_publications)
   └──► read.ts as side_ledger records ──► project.ts: binding, files_written,
        publications, creations with their copies under written/

Install / use

npm install @miadi/hooks-interpreter
import { interpretSession } from "@miadi/hooks-interpreter";

const { projection, aspects, ceremony } = await interpretSession(
  "ace95c7d-9ca8-464b-b4dc-f77157241257"
);
miadi-hooks-interpret session  <id|dir>   projection: mission, tools, creations, corrections, state
miadi-hooks-interpret aspects  <id|dir>   4 RISE aspects + the ceremony entry
miadi-hooks-interpret agents   <id|dir>   launch/output correlation, orphans named
miadi-hooks-interpret ceremony <id|dir>   the ceremony entry alone
miadi-hooks-interpret watch    <id|dir>   poll; print ONLY what is new since the last emission
miadi-hooks-interpret pane     <id|dir>   best-effort tmux correlation, confidence and candidates
miadi-hooks-interpret fleet               one line per session across the whole capture root
miadi-hooks-interpret digest   <id>...    bounded: prompts, last reply, commits vs origin, pushes
miadi-hooks-interpret join     [<id>...]  session id → live pane, team, episode, inventory record
miadi-hooks-interpret memory   [<key>]    what Claude memorized: the newest copy of each memory file
miadi-hooks-interpret search   <words>    which sessions said, wrote, ran or committed it, with the moments

  --json  --root <dir>  --no-dedupe  --include-aggregate  --include-transcripts  --max-bytes <n>
  watch:  --interval <ms>  --once-on-change  --quiet  --ticks <n>
  fleet:  --since <45m|2h|3d>  --limit <n>
  digest: --no-git
  join:   --episode <folder>  --live  --inventory <dir>  --registry <dir> (repeatable)
  search: --since <t>  --until <t>  --team <id>  --session <id>  --kind <k,...>
          --limit <n>  --moments <n>  --budget <ms>

Session root: --root, else $MIADI_SESSIONDATA_ROOT, else /src/_sessiondata (the same default hooks-core uses). The package ships compiled CommonJS in dist/ and runs on Node 24.

The digest

digest answers what an agent needs to judge a session, in one call and a few thousand characters per session, so that agent never reads the raw ledgers or the transcript:

  • prompts: how many came from a person and how many from other sessions or the harness (<cross-session-message>, task notifications, local commands), the mission, and the last twelve a person typed. The last three keep 800 characters, the others 200.
  • last reply: the main session's last assistant.stop, never a subagent's. A long reply keeps its first 600 and last 1,800 characters, because a named next step sits at the end.
  • typed: the time and opening words of the last text typed into the session, from the tail of its transcript. A seat that steers with tmux send-keys lands there exactly as a person does, so it is not called the person's.
  • commits: from Claude Code's own tool_response.gitOperation when it recorded one, else from git's output (a quiet git commit -q records nothing). Each is checked in the tree it was made in: the origin's owner/repo, and whether a remote-tracking branch holds it. A commit that a later rebase replaced is not on origin, and git cherry then says whether its change is on origin's default branch under another sha (rebased, change on origin).
  • state: the session's state, where closed comes from the last start or end on its binding line when it has one. A resumed session is not reported closed because an earlier run ended. A stamped session-end record newer than the binding line's last start closes the session: hermes writes a binding start and no binding end.
  • pushes, pull requests, files written, and how many subagent replies came back.
  • published: the Artifact pages, GitHub issues, comments, pull requests, releases and npm versions in the session's publication index, with the prompt each one answered. Commits from the index join the commit list, checked in the tree the index recorded.
  • created: each path with the newest copy of it under the session's written/ folder, when the session has a files-written index, so a reader opens the file a session wrote even after it was moved or deleted.
  • memory: each memory file the session wrote under <config>/projects/<key>/memory/, with its type and description read from the copy and the path of the copy. MEMORY.md is marked as the index.
  • binding: the session's name, team, episode, pane and host from its binding line, and the miadi-run mission that started it: the pinned tracing prompt (miadi-tracing:<domain>/prompt/<name>@<version>) and its trace.

Measured 2026-10-07 on gaia: four sessions in 1.1 s and 26 KB, where three agents reading the same kind of sessions raw had taken 180 to 210 KB of tool output each.

Search inside sessions

search <words> answers which sessions said, wrote, ran or committed something, so a reader finds a session without opening the capture's files. A moment matches when it holds every word, without regard to case. "a phrase" is one word, and owner/repo#n also matches that issue's or pull request's URL, and #n in a commit of that repository.

| kind | what it is | |---|---| | prompt | what a person typed | | message | a prompt another session or the harness wrote (a task notification, a cross-session message) | | reply | the main agent's text: each stamped turn of the progressive ledger, and a turn's last message when no stamped turn holds it | | command | a command a tool ran | | file | a file written, from the files-written index, else a write tool's input | | commit, push | from the publication index, else from what git printed | | publication | an issue, comment, pull request, page or release |

Each session comes with its name, team, episode and host from the binding line, how many moments matched by kind, and the newest moments (5 by default, --moments), each with its kind, time and an excerpt of at most 200 characters around the first word. A time marked at_inferred is an upper bound: Claude stamps no hook record, so a prompt's time is the first stamped record of the same prompt, else the ledger's last write.

--since and --until take a duration back from now (2h, 3d), a date or an ISO time. --team reads the binding line's team, --session takes ids or prefixes of 8 characters or more, --kind takes kinds from the table.

The scan stats every session directory. The read goes newest first and stops once --limit sessions matched (10 by default) or --budget milliseconds passed, and bounds.not_read names how many candidates it left. A ledger is parsed only when its bytes hold every word, and then only its lines that hold them. Measured 2026-11-10 on gaia: "Konsole" over the newest 380 of 6,047 sessions in 9 s, and a word in no session over all of them (4.2 GB) in 27 s.

import { searchSessions } from "@miadi/hooks-interpreter";

const { sessions, bounds } = await searchSessions({ query: "Konsole", since: "3d", limit: 5 });

What Claude memorized

Claude Code keeps a project's memory in <config>/projects/<key>/memory/, one Markdown file per memory and MEMORY.md as its index, readable by the user that ran the session only. The capture copies each memory file a session writes to that session's written/ folder. memory lists the newest copy of each file across the capture root, by project, with its type, description, the session that wrote it, how many writes the capture holds, and the copy's absolute path, so an agent that is not Claude, or runs as another user, can read it. memory <key> keeps the projects whose key contains <key>.

A session's projection carries the same notes as memories. They come from _claude_memory.jsonl when the capture keeps it, else from the files-written lines whose path is a memory file. A memory file removed later is still listed, because the capture does not see removals. Measured 2026-10-30 on gaia: 68 memory files in 8 projects, read from 149 session folders in 1.5 s.

The join

join answers where a session stands now, keyed by its session id. It reads three sources for what each one is:

  • The binding line, <root>/data/terminal_bindings.jsonl. The session-observability hooks append one line per session start, rename, end or observation, with the pane, team and episode at that moment. This is history: each field takes the newest line that carries it, so an end line with no pane keeps the pane recorded before it. A session asked for by id that this file does not hold is read from its own copy, <root>/<id>/_terminal_binding.jsonl.
  • The live registry, ~/.claude/sessions/<pid>.json by default (--registry adds others). Claude Code keeps one entry per running session with its tmux pane now. An entry counts only while its pid runs and is the same process (procStart equals the start time in /proc/<pid>/stat), so a reused pid never makes a dead session look live.
  • The inventory, <dir>/<session_id>.json with --inventory <dir>. It is read as a dated snapshot (modified_at). Its stored pane is never used. Since 0.6.0 the snapshot carries what the record says by title: mission, episode, work_completed, held_decisions and next_steps, each entry with whom it waits on when the record names one; since 0.7.0 also repo (owner/repo), tmux_session_name and evidence (the refs of links.evidence); since 0.10.0 trace (the owner/repo@sha refs of links.trace), related (episodes, issues, circles, ceremonies, foundations, sessions), missions ({id, how, given_at, episode}) and capabilities ({capability, relation, version, evidence}). The keeper's descriptions and file lists stay in the record. With episode and inventoryDir together, sessionJoins also returns the sessions whose record names that episode folder, including a session whose line carries only the declared fallback and one no line on this host holds.

Missions (0.8.0, jgwill/Miadi#868). linkMissions(joins, missions) gives each session the missions it belongs to, in missions: [{ id, how, of? }]. sessionJoins does the same when you pass missions (what a host reads from miadi-run's records: id, promptName, seats, signers). how is one of four values, strongest first:

  • received: its session.mission line, or the record's seat.
  • signed: it wrote one of the mission's events.
  • fork-of: it was launched with --resume <id> --fork-session from a session that belongs, and of names that session. The join keeps only the parent id, as fork_of, never argv.
  • named-after: its name or tmux session begins with the prompt's name.

The links are made over every session before an episode filter, so a fork's parent outside the episode still counts.

The pane to steer or peek is live.pane_id. recorded stays beside it with the time it was written. A recorded pane is never promoted to a live one, because after a tmux restore another occupant may hold it. An episode whose source is declared is the exported MIADI_CHRONICLE_PROD_EPISODE fallback, so it binds no session to that episode.

import { sessionJoins, observeEpisodeSessions, parseBindingLines } from "@miadi/hooks-interpreter";

const [me] = sessionJoins({ sessionIds: [sessionId], inventoryDir });
me.live?.pane_id;   // where it runs now, or no live block when it does not run here

// pure, no fs: the sessions that worked in one episode, from lines the host read
observeEpisodeSessions(parseBindingLines(jsonl), "2026-10-10-episode-552-what-an-episode-holds");

No join or observation carries argv, cwd, transcript_path or pid.

How it reads

  • Five producers, five shapes. Claude, Codex, Gemini, Hermes and openclaw write different JSONL into the same folders. The reader takes all of them, including pretty-printed JSON inside a .jsonl file. Alias ledgers holding identical records are collapsed. Aggregate ledgers that repeat every other file are skipped unless --include-aggregate is given. Large files are streamed and cut at --max-bytes (64 MiB by default), and a cut file is named in the report.

  • Time is stated, not assumed. Where a producer writes no timestamp (Claude writes none), an event's time is the file's modification time, an upper bound, and is flagged at_inferred. report.ordering says how far the order of events can be trusted, and tool calls are paired without relying on it.

  • Aspects are assembled, not generated. Same projection, same bytes: no model, no network, no key. Each of the four aspects is at most 55 words, by construction. A detail is added only while it fits, so the desired-state sentence is never the part that gets cut.

  • Corrections carry their evidence. Each correction a person made is graded:

    | confidence | meaning | |---|---| | rejected-tool-recorded | the record names the tool the person rejected, with their words | | distinct-feedback-field | a correction-only field carried the text, with no rejected tool | | ledger-filename-only | only the name of the file it landed in says it is a correction |

    A record contradicted by the session itself (it is the first prompt, or it is the mission) moves to corrections_disputed with its reason, unless it names a rejected tool. Nothing is dropped silently. A ledger-filename-only count is a ceiling on redirections, not a count of them.

  • Side ledgers are read for what they are. The session-observability capture keeps four ledgers beside the hook stream: the session's binding line (_terminal_binding.jsonl), the files-written index (_claude_files_written.jsonl, one line per file any tool call created, changed or moved, with its copy under written/), the publication index (_claude_publications.jsonl) and the memory index (_claude_memory.jsonl, one line per memory file Claude wrote). Their records are marked mapping_source: "side_ledger" and counted in report.side_ledgers. Their stamps never make a stream look ordered: for ordering they count as unstamped records, as the binding line did before 0.5.0. They are kept out of the time window and the state's newest record. The projection reads them into binding, files_written, publications, memories and creations. A session without them reads as before: creations come from write tools' inputs alone, and the classification says so.

  • Plans are derived. plan_or_pde.proposed is derived from ExitPlanMode, exit_plan_mode and update_plan tool calls, and kept out of the main stream so a derivation is never mistaken for a capture.

  • watch polls. Producers replace their files by rename, which leaves a file watcher silent. watch checks every --interval (3 s by default), prints only what changed, and has no channel back to the observed agent. A new publication is printed even under --quiet, because it reached people outside the session. Steering belongs to the surface that owns the agent.

  • pane reports confidence. No event on disk carries TMUX_PANE, so a session is matched to a tmux pane by working directory, and a pane is named only when the match is unique.

  • fleet scans everything and projects a bounded number. What --limit left out is named in bounds.not_projected.

  • search scans everything and reads newest first. What --limit or --budget left unread is named in bounds.not_read. readSession's lineFilter and fileFilter are how it reads only what holds the words.

  • The ceremony entry is built, never written. buildCeremonyEntry() returns it with written: false and a store_hint, and the caller decides where it goes.

Reading the report

ReadReport is the honesty surface. Nothing vanishes quietly:

files_read / files_skipped     every skip states its reason
lines_seen / records_parsed
malformed_skipped              lines that never became JSON
multiline_recovered            pretty-printed records rescued
duplicates_collapsed           alias-ledger records collapsed
truncated_files                read cut short; tail NOT read
filtered_out                   one-line records a lineFilter skipped unparsed
unmapped_native_events         native names no map claimed, with counts
side_ledgers                   records read from the capture's side ledgers, by stem
ordering                       how far the array order can be trusted

Boundary

hooks-interpreter owns:

  • reading and normalising heterogeneous sessiondata JSONL into InterpreterEvent
  • projecting a session into mission / tools / creations / corrections / state
  • RISE aspect emission under an enforced word budget
  • subagent launch↔output correlation, orphans included
  • building the ceremony entry

hooks-interpreter does not own:

  • provider hook behaviour or JSONL writing (that is hooks-core)
  • transport, ledger or worker dispatch (that is hooks-gateway)
  • writing into the Medicine-Wheel store, or any act on the Implementer's repos

Limits

It makes a long run legible, not steerable, and it cannot tell who it is watching. "Live" means within --interval, and only for what a producer has already written to disk. There is no trace_id in the data and no cross-session causality: two sessions that worked on the same thing are two rows.