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

@danypops/papyrus

v0.60.13

Published

Daemon-backed graph artifacts, evidence-bearing tasks, rules, skills, and native TUI workflows for Pi

Readme

@danypops/papyrus

The daemon, CLI, and domain services behind Papyrus's graph artifact store: evidence-bearing Tasks, Docs, Rules, Playbooks, and Notes over one authenticated local database. Runs standalone as a plain daemon and CLI. @danypops/pi-papyrus projects the same daemon into native Pi tools.

Contents

Install

bun add @danypops/papyrus

The papyrus binary ships with the package.

Quick start

papyrus service install   # install, enable, and start the user service
papyrus service status
papyrus service restart

# Task operations (add --json for machine output)
papyrus tasks plan
papyrus tasks depend <task-id> <prerequisite-id>
papyrus tasks update <task-id> --title "Revised task"
papyrus tasks focus <task-id>
papyrus tasks complete <task-id>

# Resolve a registered project name before scoped operations
papyrus tasks projects --query lector --json
papyrus tasks resolve-project Lector --json

# Deferred human-intent inbox
papyrus notes capture "Review release provenance later"
papyrus notes list --json

Schema protocol

Papyrus enforces four artifact kinds, each with its own status vocabulary:

  • doc — knowledge: specifications, decisions, and research
  • task — work: desired outcomes, gates, checklists, and dependencies
  • rule — governance injected into the Pi system prompt
  • playbook — a trigger and an ordered list of steps whose validated arguments render a connected collection of deterministic Tasks plus contextual Rules and Docs

Every edge endpoint resolves to a real artifact, and every edge relation is registered in relation_names. Relations are universal: any kind can link to any other kind.

Hierarchy and traversal

contains/part_of express parent/child structure; depends_on expresses execution ordering. Dependency edges form an executable DAG: a self-dependency or cycle is rejected, fan-in waits for every prerequisite, and fan-out can expose several ready successors while active focus stays singular. Graph reads are cycle-safe and bounded by depth/max_nodes (default 4/100, ceiling 20/1,000). Executable task plans are bounded to 1,000 tasks and 10,000 relationships.

Playbooks

A Playbook step is a plain prose string (a Task) or a structured object: {kind:'doc',...} creates a Doc, {kind:'rule',...} creates a Rule, {kind:'call',...} nests another Playbook's own run as a pipeline step. playbooks.invoke validates and normalizes arguments, renders placeholders in memory, validates the complete graph, then persists artifacts and edges in one transaction. Dependencies, containment, gates, checklists, and context all survive rendering. A run's Rules are active only while focus belongs to that run; Docs keep their invocation context and provenance.

A run result carries a stable schema: Playbook id, run id, normalized arguments, created ids grouped by kind, ready root task ids, and the bounded execution plan. An explicit run id produces deterministic artifact ids (<run-id>-<blueprint-ref>); a collision rolls the whole run back.

papyrus playbooks invoke <playbook-id> \
  --arguments-json '{"project":"Papyrus"}' \
  --json

Project scope

A Doc, Rule, or Playbook is either global (applies everywhere) or bound to a bounded, non-empty set of registered projects — never both, and never inferred from an accidentally empty membership. scope inspects an artifact's own mode and membership; add_project/remove_project mutate one membership at a time (add_project is idempotent for an already-present project; remove_project for an absent one); replace_projects swaps the whole set atomically; set_global is the only way back to global — removing an artifact's last remaining membership through remove_project is rejected instead of silently widening it. assign_project remains a documented compatibility delegate for the pre-multi-project single-root shape (replace, not add).

Listing follows the same distinction: an omitted project filter keeps the existing bounded all-artifacts search; project_root alone means exact membership (audit semantics — only artifacts actually bound to that project); project_root plus applicable: true means every artifact applicable to that project instead — global artifacts plus artifacts whose membership includes it. pi-papyrus's own context injection uses applicable for both Rules (rules.injectable) and Playbooks, so a project-bound artifact never leaks into an unrelated project's prompt. Project scope governs applicability and discovery, never authorization: exact-id access works regardless of scope, the same as it always has.

A Playbook's own definition scope is a separate concern from where playbooks.invoke sends its generated artifacts. An unscoped or global Playbook can still be invoked with a destination project_root; the generated Tasks, Docs, and Rules inherit that destination, while the Playbook definition itself is untouched. A run-created Rule's injection requires both its own run to be active and its generated project membership to match — either alone is not enough.

The shared project catalog behind all of this (projects.list/projects.resolve/projects.register) is the exact one Tasks has always used, and tasks.projects/tasks.resolve_project/tasks.register_project remain fully working, documented compatibility delegates over it.

papyrus docs scope <doc-id> --json
papyrus rules add-project <rule-id> Lector --json
papyrus playbooks list --project-root <root> --applicable --json
papyrus projects register /path/to/project --name Lector --json

Naming vs. ids

Every agent-facing domain (tasks, docs, rules, playbooks, notes, discuss) addresses artifacts by name (the exact title) anywhere id would otherwise be required — dependency_name/parent_name/child_name/root_task_name/depends_on_names (tasks), target_name (docs link, searched across every kind), task_name (rules gate, discuss block/unblock), and blocks_task_names (discuss open). Resolution is an exact, case-insensitive, trimmed title match scoped like a plain list call; an ambiguous name's error lists the real ids, the one place disambiguation needs them. A returned result leads with name and status; id surfaces only when two artifacts in the same result share a title. id itself keeps working, in every tool.

Mutability

Tasks, Docs, Rules, and Playbooks all support update (title/body/labels, at least one field required) alongside creation. Every update shares creation's own bounds (Rules keep their own tighter condition+action+body ceiling) and lands on the artifact's append-only mutation history, queryable via graph.history. An artifact carrying a source:<system> label (e.g. source:web-spider) is a read-only projection owned by that system; update is refused with a clear error, and a correction belongs in a new linked Doc until that system ships its own write-back path. Notes route every content change through their own facade.

Idempotent lifecycle mutations

tasks.create accepts an optional idempotency_key, scoped to the caller and canonical project root. Replaying the same key with the same payload returns the original response; a changed payload is rejected. Keys expire after seven days.

Task lifecycle mutations (start, submit, reject, retry, cancel, reopen, complete, pause, unpause) are destination-state idempotent: repeating an already-reached transition is a changed: false no-op with no duplicate history. Pass idempotency_key on any mutation whose response might get lost; after an unclear outcome, call tasks.show and tasks.mutation_status with that same key before deciding the next action. Completed receipts are retained for seven days; concurrent duplicate completion calls share one gate run. An incompatible transition returns typed invalid-transition details with current/intended status, allowed actions, and recovery guidance.

tasks.claim, tasks.heartbeat_lease, and tasks.lease return the reusable artifact alias as taskName plus taskTitle; use taskName for later Task operations and keep the lease token for heartbeat/release.

Task project names are registered identities, resolved explicitly rather than guessed from a working directory. tasks.projects searches bounded registered identities; tasks.resolve_project matches one case-insensitive exact id, name, alias, or canonical root, and reports unknown or ambiguous references directly. Pass the returned projectRoot into subsequent task operations. tasks.register_project renames or moves an existing identity while keeping its stable id and folding the prior name into its aliases.

Removing an artifact

An artifact gets a permanent, immutable created row in the mutation event log the moment it exists, so removal is a time-gated trash entry. remove (the shared artifact.remove/artifact.remove_subtree operations every domain routes through, or papyrus artifact remove <id> [--reason <text>]) moves an artifact to the trash: excluded from every list/query immediately, still reachable directly by id, and recoverable via restore for 30 days. remove on a Task currently holding live Focus in any scope is refused.

Past the 30-day deadline, the daemon's periodic sweep performs a real, cascading delete — the one deliberate exception to Papyrus's append-only history, enforced by a database trigger checked at delete time.

Context Mesh persistence model

artifacts is the shared graph-identity supertype; edges references that single identity table at both endpoints, keeping foreign-key integrity across domains. Domain extension tables exist only where application invariants need indexed relational state — Task chronology/focus/scope, Discourse posts/events/session cursors/projection checkpoints — a class-table/table-per-type layout with explicit child-to-parent foreign keys.

The owning application stays the mutation authority for its own extension rows: Discourse commits its rows and context-thread/context-message Doc projections atomically through discourse.store, while generic artifact/document/lifecycle/graph-link operations reject those owned subtypes and the reply_to/discusses relations. SQLite triggers verify each extension row references its expected Doc subtype. Domain tables stay canonical for domain invariants; graph bodies and metadata are read-oriented projections committed in the same transaction.

papyrus discourse store read_thread --store-id team-forum \
  --input-json '{"forumId":"engineering","topicId":"reviews","threadId":"mesh","limit":25}' \
  --json

Storage and service

$XDG_DATA_HOME/papyrus/papyrus.db       # durable graph
$XDG_RUNTIME_DIR/papyrus/{port,token}   # private daemon discovery

The daemon runs SQLite with WAL, foreign keys, a bounded busy timeout, versioned migrations, periodic passive checkpoints, and periodic PRAGMA optimize. Keep the database on a local filesystem — WAL depends on real local file locking.

Application services depend on the ArtifactStore and GateRunner ports; SQLite and subprocess execution are adapters the daemon composes, so task behavior is unit-tested against fakes without a database. Task visualization projects the same TaskGraph into semantic display graphs through a GraphRenderer port — the Pi adapter in @danypops/pi-papyrus renders terminal Unicode via beautiful-mermaid behind that port, so the task domain carries no Mermaid syntax.

For repository work, install the versioned ownership guard from the workspace root:

bun run guard:install

It checks a push's destination against DanyPops/papyrus, including an explicit fallback URL, so a push bound for the wrong remote fails locally before it reaches GitHub.

src/index.ts is this package's public surface for @danypops/pi-papyrus and any other consumer: explicit, named exports curated for outside use.

Related packages

  • @danypops/pi-papyrus — the Pi extension: native tools, TUI panels, and context injection over this daemon's authenticated loopback connection.