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

throughline

v0.3.22

Published

Claude Code hooks plugin for structured context compression (/clear-safe persistent memory)

Downloads

3,194

Readme

Throughline

Cut ~90% of Claude Code's context usage while keeping nearly all the memory.

In a typical Claude Code session, 80% of the context window is tool I/O — file reads, Bash output, grep results. This data is consumed the moment Claude acts on it, but it stays in the context forever, pushing you toward the window limit.

Throughline fixes this by separating conversation content by type, not time:

Without Throughline (50 turns, no /clear):
  Context = user text + assistant text + tool I/O + system messages
          ≈ 125,000 tokens (80% is tool I/O you'll never re-read)

With Throughline (50 turns → /clear → resume):
  Context = recent 20 turns of conversation text (L2)
          + older 30 turns as one-line summaries (L1)
          + zero tool I/O (L3 — retired to SQLite, on-demand)
          ≈ 13,000 tokens — same decisions, same context, 90% lighter

Unlike MemGPT or LangChain's SummaryBufferMemory which compress by recency (old = summarized), Throughline separates by content type: human-readable conversation stays, machine-generated tool output retires. This is purpose-built for coding assistants where tool I/O is heavy but transient.

The retired L3 data isn't lost — Claude can pull it back on demand via throughline detail <time> when a past turn's tool output becomes relevant again.

Throughline also ships a multi-session token monitor that reads real Anthropic API usage from the transcript JSONL (no length / 4 heuristics).


Quick Start

npm install -g throughline
throughline install

That's it. install registers Throughline's hooks in ~/.claude/settings.json (user scope), so every Claude Code project on your machine picks it up automatically. No per-project wiring required.

Start any Claude Code session and your turns will begin flowing into ~/.throughline/throughline.db in the background.


Three-layer memory model (schema v7)

| Layer | Name | Where it lives | Content | Cost per turn | | ----- | ---------- | --------------------- | --------------------------------------------------------------------- | ------------- | | L1 | Skeleton | injected when old | one-line Haiku-generated summary of the turn | ~10 tok | | L2 | Body | injected when recent | user text + assistant reply, verbatim | full natural | | L3 | Detail | SQLite only | tool I/O, system messages, images, extended thinking (on-demand) | heavy, retired |

The layers are complementary and disjoint — nothing is duplicated across them. Extended thinking blocks are stored at L3 (kind='thinking') so the next session can see what the previous Claude was thinking at the moment it was interrupted, not just what it said aloud. On SessionStart the thinking of the final turn is injected inline above the L2 history; older thinking remains retrievable via throughline detail <time>.

On SessionStart, Throughline rebuilds the context from SQLite and injects it as plain text:

  • The most recent 20 turns are injected as full L2 (bodies) text
  • Older turns are injected as L1 (skeletons) one-liners
  • L3 stays in SQLite and is retrieved on demand via /sc-detail <time>

L1 summaries are generated by Claude Haiku 4.5 via a subprocess (claude -p --model claude-haiku-4-5-*), reusing your Claude Max login — no API key required. Summarization is lazy: for sessions that stay under 20 turns, Haiku is never invoked, so short tasks cost zero summarization time.

All three layers (L1/L2/L3) have working write paths as of schema v5. /sc-detail HH:MM:SS returns user/assistant text (L2) plus a kind-grouped view of tool inputs, tool outputs, and hook output captured at L3 for that turn.


Explicit handoff via /tl (with in-flight memo)

Inheritance is opt-in, not automatic. When you want the next session to pick up where this one left off, type /tl in the current session before you /clear or open a new chat. Without /tl, new sessions start fresh — no memory is carried over.

The /tl slash command does two things:

  1. Writes a handoff baton (the current session_id) into the handoff_batons table via the UserPromptSubmit hook.
  2. Asks the current Claude to write an in-flight memo. /tl instructs Claude to summarize what it was about to do next, its current hypothesis, open questions, and in-progress TODOs, then pipe that Markdown into throughline save-inflight, which attaches it to the baton's memo_text column. This captures the "currently thinking" state that plain transcript replay cannot preserve.

On the next SessionStart, the hook reads the baton, and if it is less than 1 hour old, merges that session's memory into the new session using a deterministic UPDATE session_id = ? inside a BEGIN IMMEDIATE transaction. The baton is consumed (deleted) atomically with the merge, so it cannot fire twice. The injected resume context is reframed as "resuming an interrupted task" rather than "reading past logs", and the in-flight memo plus the final turn's extended thinking appear at the top so the new Claude picks up mid-thought.

Session A (type /tl)  -----------> baton written
                                       |
                      /clear           |
                         |             ▼
                      Session B  ---- reads baton, merges A into B, deletes baton ---->
                         |
                      (type /tl again to hand off further)

Why explicit baton instead of auto-inherit:

  • Zero false positives. A parallel window, a VSCode restart, or a genuine new task in the same repo won't accidentally inherit the previous session's memory. Only an explicit /tl triggers inheritance.
  • VSCode extension compatibility. The SessionStart hook's source field is rewritten to "startup" by the Claude Code VSCode extension even after /clear (see issue #49937), so source-based detection is unreliable. A user-driven baton sidesteps this.
  • Deterministic. No time-window heuristic, no PID guessing, no ancestor walking. The user declares intent; the hook carries it out.

Each merged row keeps its origin_session_id, so repeated /tl handoffs accumulate memory through chains:

S1 (4 turns) --/tl,/clear--> S2 (merges S1, adds 3 turns) --/tl,/clear--> S3 (merges S2, adds 5 turns)
                             origin=S1×4                                  origin=S1×4, S2×3, S3×5

Multi-session token monitor

Run:

throughline monitor            # all active sessions in the current project
throughline monitor --all      # every project, every session
throughline monitor --session <id-prefix>

Example output (real values from a running 1M-context Opus session):

[Throughline] 1 セッション
▶ Throughline       2ed5039c  ████░░░░░░░░░░░░░░░░  205.1k /  21%  残 794.9k  claude-opus-4-6
  • Token counts are accurate. Read straight from the latest message.usage field in the session transcript JSONL, which is what Anthropic's API actually reported (input_tokens + cache_creation_input_tokens + cache_read_input_tokens). No length / 4 approximation.
  • 1M-context detection is automatic. It checks the [1m] suffix in the transcript, falls back to string matching on 1M context, and finally promotes to 1M if observed usage exceeds 200k.
  • Multi-session view. Each Claude Code session writes its own state file (~/.throughline/state/<session_id>.json). The monitor scans the directory every second and displays one row per live session, sorted by last activity. The most recent one is highlighted with .
  • Stale hiding. Sessions that haven't been touched in 15 minutes drop out of the default view; files older than 24 hours are deleted entirely. This is the only time threshold in the system and is used solely for display hygiene — no memory decisions are made from it.
  • Line-wrap safe. Each line is truncated to process.stdout.columns - 1 before drawing, preserving ANSI color codes. The redraw cursor math cannot desync on narrow terminals.
  • Resize resilient via OSC 18t. Windows ConPTY + VS Code task terminals freeze process.stdout.columns at the PTY's initial size and never propagate panel resizes into Node, so polling or resize events can't catch them. Throughline queries the terminal itself with the CSI 18 t escape (\x1b[18t) every tick, parses the \x1b[8;rows;cols t reply off stdin in raw mode, and uses the real current width for truncation. On terminals that don't answer the query, the renderer falls back to process.stdout.columns → env.COLUMNS → 80. When the width changes the viewport is cleared in full (\x1b[2J\x1b[3J\x1b[H) before the next frame so the previous, wrongly-sized frame can't stack beneath it.
  • Per-row "last updated" stamp. Each session row carries an 8-cell just now / 24m ago stamp right after the session id, placed before the bar so narrow terminals don't truncate it. It resets to just now on every Stop hook, so a growing stamp means the session is truly idle — not the monitor stuck. When you need more detail, throughline doctor --session <id-prefix> compares the state file against the actual transcript JSONL and flags drift, idle time, and /clear-induced transcript path staleness.
  • State-backed usage snapshot. When the Stop hook finishes a turn it persists the latest tokens / model / contextWindowSize back into the state file. The monitor prefers this snapshot over re-reading the JSONL, which removes a source of flicker when the transcript path in state drifts from the one Claude Code is currently appending to.

VS Code auto-start (automatic)

After throughline install, any VS Code / Cursor / VSCodium project you work in gets .vscode/tasks.json provisioned automatically on the first session event. The file configures runOn: folderOpen so the monitor appears in a dedicated terminal panel the next time you open that folder.

How it works. ensureMonitorTaskFile is called from all three hooks (SessionStart, UserPromptSubmit, Stop) as of v0.3.18. Whichever one fires first in your environment creates the file; the rest are idempotent no-ops. Once per project it inspects .vscode/tasks.json:

  • No file yet → creates one with a single Throughline Monitor task, and emits a one-time <system-reminder> to stdout so Claude tells you a Developer: Reload Window is needed to activate the folderOpen task once (v0.3.19+).
  • Plain JSON with other tasks → appends the monitor task, preserves your existing entries, version, and indentation (same notice fires once).
  • JSONC (comments or trailing commas) → does not touch the file. Prints a one-time notice to stderr asking you to paste the snippet below.
  • Already contains a Throughline Monitor task → does nothing (idempotent; this is the common path on every subsequent turn; notice is silent).

The generated task uses type: 'shell' with the absolute path to Node and bin/throughline.mjs. VS Code wraps shell tasks in a PTY (xterm.js) so the monitor sees isTTY=true, real columns, and resize events. Windows .cmd shims and missing PATH entries cannot break it because the command is already an absolute Node binary path.

Opt out: set THROUGHLINE_NO_VSCODE=1 in the environment used by Claude Code. Delete .vscode/tasks.json (or just the monitor entry) if you want to stop auto-start for a project that already has one.

Manual snippet for JSONC tasks.json files. If Throughline refused to edit your tasks.json because it contains comments or trailing commas, add this entry to the tasks array yourself:

{
  "label": "Throughline Monitor",
  "type": "shell",
  "command": "throughline monitor",
  "isBackground": true,
  "presentation": {
    "reveal": "always",
    "panel": "dedicated",
    "group": "throughline",
    "close": false,
    "echo": false,
    "focus": false,
    "showReuseMessage": false,
    "clear": true
  },
  "runOptions": { "runOn": "folderOpen" },
  "problemMatcher": []
}

Commands

| Command | What it does | | ---------------------------------------------- | ------------------------------------------------------------ | | throughline install | Register hooks in ~/.claude/settings.json (user scope) | | throughline install --project | Register hooks in .claude/settings.json for this repo only | | throughline uninstall | Remove Throughline hooks from the settings file | | throughline monitor [--all] [--session <id>] | Run the multi-session token monitor | | throughline monitor --diag | Dump TTY/columns/env diagnostics (for debugging monitor render bugs) | | throughline detail <time> | Retrieve L2 body text and L3 tool I/O for a turn (see below) | | throughline save-inflight | Called by /tl to attach an in-flight memo (stdin) to the current baton | | throughline doctor | Check Node version, hook registration, DB writability, PATH | | throughline doctor --session <id-prefix> | Diagnose a specific session — detect state/transcript drift, idle vs. stuck | | throughline status | Print DB statistics (sessions, skeletons, bodies, details) | | throughline --version | Print the installed version |

Slash commands (invoked by the user in Claude Code):

| Command | What it does | | ------------- | ----------------------------------------------------------------- | | /tl | Write a handoff baton + ask Claude to save an in-flight memo for the next session | | /sc-detail <time> | Retrieve L2 body text and L3 tool I/O for a past turn |

When /tl triggers, Claude will call throughline save-inflight via its Bash tool. Claude Code will prompt for permission the first time; add Bash(throughline save-inflight:*) to your allowlist to skip the prompt on subsequent /tl invocations.

Hook subcommands (invoked by Claude Code, not by humans): session-start (SessionStart), process-turn (Stop), prompt-submit (UserPromptSubmit — detects /tl and writes baton).

throughline detail — for AI, not humans

throughline detail is the escape hatch Claude itself uses to pull archived detail back into the context when an L1 summary isn't enough. The injection footer explicitly instructs Claude to run this via its Bash tool when a past turn's tool I/O becomes relevant.

throughline detail 14:23:05          # single timestamp
throughline detail 14:23-14:30       # timestamp range

Output groups records by kind: L2 conversation bodies, then L3 tool input/ output, then system messages (hook output), then images. Records are scoped to the current project's merge chain so Claude only sees turns from its own project history.


Requirements

  • Node.js >= 22.5 (for the built-in node:sqlite module — no native build required, no npm install of SQLite bindings)
  • Claude Code with hooks support (SessionStart, Stop)
  • Claude Max subscription (for Haiku-based L1 summarization via claude -p)
  • Works on Windows, macOS, Linux

Throughline has zero runtime dependencies. The published tarball is just plain .mjs files.


Data layout

~/.throughline/
├── throughline.db          SQLite database (WAL mode)
├── haiku-workdir/            Isolated cwd for Haiku subprocess (recursion guard)
└── state/
    └── <session_id>.json     Per-session activity state for the monitor

Schema v7:

  • sessions — one row per session_id, with project_path and merged_into
  • skeletons — L1 one-liners, keyed by (session_id, origin_session_id, turn, role)
  • bodies — L2 verbatim text (user + assistant), same key shape
  • details — L3 records with kind column (tool_input / tool_output / system / image / thinking) and source_id for idempotent re-processing
  • handoff_batons — one row per project_path, with session_id, created_at, and memo_text (the in-flight memo written by save-inflight after /tl). Consumed and deleted by the next SessionStart if within the 1-hour TTL.
  • injection_log — audit trail of injection events

All memory tables carry an origin_session_id so rebonded rows keep their lineage across a chain of /tl handoffs.


Design principle: no fallback code

Throughline deliberately refuses to swallow unexpected errors. Silent try { … } catch { /* ignore */ } blocks hide bugs; instead, hooks throw and exit with a non-zero status so Claude Code surfaces the failure in stderr.

Specifically:

  • JSON parse failures → throw, not continue
  • Missing required fields → throw new Error(...), not exit(0)
  • DB transactions → explicit BEGIN IMMEDIATE / ROLLBACK / re-throw
  • Hook entry points wrap main() with a single .catch that writes stderr and exits with code 1

The only tolerated silent paths are:

  • JSONL per-line parse tolerance (tail partial writes are part of the format spec)
  • State-file corruption recovery (files are idempotently regenerated next turn)

See docs/PUBLIC_RELEASE_PLAN.md §0 for the full rule.


Haiku recursion defense

L1 summarization spawns claude -p --model claude-haiku-4-5-* as a subprocess. Without precautions this would recursively fire the same Stop hook on the subprocess and infinite-loop. Two defenses stack:

  1. Isolated cwd. The subprocess runs in ~/.throughline/haiku-workdir/, a directory that contains no .claude/settings.json, so project-local hooks are never picked up by the child.
  2. Env var guard. The parent sets THROUGHLINE_IN_HAIKU_SUBPROCESS=1 in the child env. The Stop hook (turn-processor.mjs) exits immediately on line 1 if it sees this variable.

See src/haiku-summarizer.mjs for the implementation.


Troubleshooting

Monitor says 待機中 — アクティブなセッションがありません No session has touched its state file in the last 15 minutes. Send a message in Claude Code and the monitor should pick it up within 1 second. If it still does not, run throughline doctor.

Monitor seems stuck on the same value Each session row ends with a (Nm ago) stamp. If it keeps growing, the session is idle — no assistant turn has finished. For a deeper look, run throughline doctor --session <id-prefix> to compare the state file against the actual transcript JSONL and flag drift, idle time, or /clear-induced transcript path staleness.

throughline install wrote to the wrong settings file By default, Throughline installs to ~/.claude/settings.json (user scope, applies to all projects). Use --project to scope it to the current directory's .claude/settings.json instead.

Hooks never fire Run throughline doctor — it checks Node version, hook registration, DB writability, and PATH resolution. If the binary is not on PATH, reinstall with npm install -g throughline.

node:sqlite warning on startup Node.js prints ExperimentalWarning: SQLite is an experimental feature on stderr. This is cosmetic — the module is stable enough for production and is used unchanged here.

Database got corrupted / want a clean slate Delete ~/.throughline/throughline.db (and the -shm / -wal companion files) and ~/.throughline/state/*.json. A fresh database with schema v7 is created on the next hook fire.

New session didn't inherit memory from the previous one This is the designed behavior — inheritance requires an explicit /tl in the previous session. If you forgot to type it before /clear, the memory is still in SQLite but won't auto-inject. You can still retrieve specific turns with /sc-detail <time>.


Development

git clone https://github.com/kitepon-rgb/Throughline.git
cd Throughline
npm link                              # Put `throughline` on PATH (dev only)
throughline install --project         # Register hooks for this repo only
node --test src/turn-processor.test.mjs src/session-merger.test.mjs

Run the monitor directly without a global install:

node src/token-monitor.mjs

The .vscode/tasks.json in this repo auto-launches the monitor when you open the folder in VS Code.


Design docs

  • docs/L1_L2_L3_REDESIGN.mdcore design spec for the L1/L2/L3 differential layer model (schema v4 base + v5 L3 classification extension). Authoritative for the memory layering rules.
  • docs/INHERITANCE_ON_CLEAR_ONLY.md — design record for the /tl baton handoff system (schema v6–v7). Explains why the current inheritance is opt-in rather than heuristic.
  • docs/PUBLIC_RELEASE_PLAN.md — public release plan, implementation status by version, § 0 fallback rule, and remaining tasks.
  • docs/archive/ — superseded design documents kept for historical reference (original CONCEPT, session-linking experiments, pre-publish action list).

License

MIT — see LICENSE.