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

crux-act

v0.1.2

Published

ACT — Actionable Communication Transport. A self-describing message standard: every message states its intent, priority, status, and explicit action steps. Framework-agnostic reference implementation for apps, servers, and AI agents.

Readme

crux-act

ACT — Actionable Communication Transport. A self-describing message standard, as a tiny, dependency-free TypeScript package.

npm version types license

npm i crux-act
pnpm add crux-act
yarn add crux-act
bun add crux-act

The problem

Email and chat move prose. Every message lands as free text, and the receiver has to read all of it and reverse-engineer the one question that actually matters:

What, if anything, do I need to do — and by when?

That reverse-engineering is the work nobody signed up for. A subject line is not a priority. "Can you…?" is not a task with an owner and a due date. An FYI and a hard blocker look identical in an inbox. Multiply that across Gmail, Slack, WhatsApp, Linear, Stripe webhooks… and your real to-do list is scattered, buried, and impossible to rank.

The idea

ACT fixes the layer underneath. An ACT message is self-describing — the sender states, in structured fields, exactly what they mean:

| field | what it answers | |---|---| | intent | is this a task, a request, a decision, or an update? | | priority | low · medium · high | | status | where is the work — new · actionable · in_progress · waiting · done? | | action | is action required, and what are the explicit steps? |

Any app can emit ACT and any app can consume it. The receiver no longer guesses — they get a ranked, self-describing list of what needs them. The protocol is the product; the standard outlives any one client.

This is crux-act — the protocol itself, as pure TypeScript: zero runtime dependencies, no React/UI, so any app, server, or AI agent can produce and consume ACT. Clients (a dashboard, an interactive demo) build on top of it.


Two ways messages come in

  • Bridged — a generic channel (Gmail, Slack, WhatsApp) only has raw text, so crux-act's classifier infers the ACT fields for you.
  • Native — a source that already speaks ACT (your app, a Stripe/Linear webhook, an AI agent) declares intent / priority / action.steps directly, and inference is skipped entirely. Strictly higher fidelity.
import { ingestEnvelope } from "crux-act"

// BRIDGED — raw text in, structured Work Object out (fields inferred)
const fromSlack = ingestEnvelope({
  connector: "slack",
  externalId: "slack-91",
  sender: { name: "Priya", handle: "@priya" },
  body: "Can you review my PR before standup? It's blocking the release. https://github.com/acme/app/pull/812",
  receivedAt: new Date().toISOString(),
})
fromSlack.type      // "request"   ← inferred
fromSlack.priority  // "high"      ← inferred ("blocking")
fromSlack.title     // "Can you review my PR before standup"
fromSlack.raw?.links // [{ url: "https://github.com/acme/app/pull/812", label: "github.com" }]

// NATIVE — declare the structure; nothing is inferred
const fromLinear = ingestEnvelope({
  connector: "webhook",
  externalId: "linear-ENG-512",
  sender: { name: "Linear", handle: "[email protected]" },
  subject: "You were assigned ENG-512",
  body: "Decide whether ENG-512 ships this sprint and update the issue.",
  receivedAt: new Date().toISOString(),
  intent: "decision",
  priority: "high",
  action: {
    required: true,
    steps: ["Review the ENG-512 scope", "Decide: ship or defer", "Update the issue"],
  },
})

ingestEnvelope is the single inbound entry point — every channel flows through it identically. Sources are metadata; core logic never branches on which channel a message came from.

Carry ACT through any channel

A sender can embed ACT in a normal message body, so even a Gmail or WhatsApp message arrives natively structured — no platform change required:

import { embedAct, extractActBlock } from "crux-act"

const body = embedAct("Approve the Q3 plan before Monday.", {
  intent: "decision",
  priority: "high",
  action: { required: true, steps: ["Open the plan", "Approve or request changes"] },
})
// body now ends with an invisible block:  <!--act/1.0 {...}-->

extractActBlock(body) // → { intent: "decision", priority: "high", action: { ... } }

ingestEnvelope handles this automatically: an embedded block is treated as native (no inference); a plain message is inferred; a malformed block falls back to inference. (Same idea as iCalendar riding inside an email.)


The message

The unit of the protocol is the ACT message:

interface ActMessage {
  act: string         // protocol version, e.g. "act/1.0"
  id: string
  intent: "task" | "request" | "decision" | "update"
  summary: string     // short, imperative, actionable title
  body: string        // human-readable detail
  priority: "low" | "medium" | "high"
  status: "new" | "actionable" | "in_progress" | "waiting" | "done"
  action: { required: boolean; steps?: string[]; dueAt?: string }
  sender: { name: string; handle: string }
  recipient: { name: string; handle: string }
  thread?: string
  attachments?: { name: string; kind: AttachmentKind; size?: string; url?: string }[]
  links?: { url: string; label?: string }[]
  createdAt: string   // ISO 8601
}

action.steps is the heart of the standard. Instead of "let me know what you think when you get a chance," the receiver sees a checklist with a due date.

ACT hosts nothing. attachment.url points at wherever the producer already hosts the file (a Gmail/Slack link, an S3 URL…). The standard only references files — it never stores them. Same for links.


How to use

Project a Work Object to a canonical ACT message

import { toActMessage } from "crux-act"

const msg = toActMessage(work) // → ActMessage, exactly what an ACT-native app exchanges

Triage: turn a flat list into "what needs me now"

import { bucketOf, itemsForView, countsByView, applyAndAdvance } from "crux-act"

bucketOf(work)                  // "today" | "waiting" | "later" | "done" (derived from status + priority)
itemsForView(items, "today")    // WorkObject[]
countsByView(items)             // { today, waiting, later, done }

// apply a change and advance selection to the next item still in the view
const { items: next, selectedId } = applyAndAdvance(items, "today", work.id, { status: "done" })

Typed replies — a reply is itself an ACT message

You don't send free prose back; you send a typed response, and the reply's intent drives the thread's status.

import { REPLY_INTENTS, buildReplyMessage } from "crux-act"

// REPLY_INTENTS: answer → done · decision → done · acknowledge → waiting · delegate → waiting · decline → done
const reply = buildReplyMessage(
  work,
  "answer",
  "Reviewed and merged — thanks.",
  { name: "Alex Mercer", handle: "[email protected]" }
)
reply.status // "done"

Send an ACT message over HTTP (SDK)

sendEnvelope POSTs to an ACT ingest endpoint (POST /api/v1/ingest). This is the "implement an adapter and you're done" surface.

import { sendEnvelope } from "crux-act"

const res = await sendEnvelope(
  {
    body: "Approve the release",
    intent: "decision",
    priority: "high",
    action: { required: true, steps: ["Review changelog", "Approve or hold"] },
  },
  "https://your-crux-host.example.com"
)
// → { ok: true, workId: "..." }

…or straight from curl:

curl -X POST https://your-crux-host.example.com/api/v1/ingest \
  -H 'content-type: application/json' \
  -d '{ "body": "Build 2291 is green. Approve to ship.", "intent": "decision",
        "priority": "high", "action": { "required": true, "steps": ["Review", "Approve"] } }'

For AI agents — register one tool, emit structured work

ACT_TOOL is a ready-to-register tool/function definition. Its input_schema is Anthropic-shaped and doubles as an OpenAI function parameters — no glue code, no prose to format.

import { ACT_TOOL } from "crux-act"

// Anthropic
await anthropic.messages.create({
  model: "claude-opus-4-8",
  max_tokens: 1024,
  tools: [ACT_TOOL], // { name: "send_act_message", description, input_schema }
  messages,
})

// OpenAI
const openaiTool = {
  type: "function",
  function: { name: ACT_TOOL.name, description: ACT_TOOL.description, parameters: ACT_TOOL.input_schema },
}

When the model calls send_act_message, POST the arguments to your ingest endpoint — the human sees a ranked, self-describing task instead of yet another message to decode.

Discover the protocol at runtime

import { ACT_DESCRIPTOR, ENVELOPE_SCHEMA } from "crux-act"

ACT_DESCRIPTOR // { protocol, version, enums, message (JSON Schema), tool, example, endpoints }
ENVELOPE_SCHEMA // JSON Schema for the inbound message a producer sends

API

| Area | Exports | |---|---| | Types | ActMessage, WorkObject, RawMessage, Intent, Priority, Status, WorkType, AttachmentKind, Attachment, LinkRef, Party, Source, ConnectorId, KnownConnectorId, KNOWN_CONNECTORS, ViewId, VIEWS, TYPE_LABEL, PRIORITY_LABEL | | Protocol | ACT_VERSION, toActMessage, buildReplyMessage, REPLY_INTENTS, replyIntentMeta, ReplyIntent | | Connector contract | InboundEnvelope, OutboundAction, OutboundResult, OutboundKind, OutboundTransport, dispatch, capabilityFor, ConnectorCapabilities, ConnectorMeta, CONNECTORS, getConnector, registerConnector, allConnectors | | Ingest / classify | ingestEnvelope, IngestOptions, heuristicClassifier, Classifier, extractLinks | | Validation | parseEnvelope, validateEnvelope, ParseResult, ParseOptions | | Triage | bucketOf, itemsForView, countsByView, applyAndAdvance, ingest | | Client SDK | sendEnvelope, EnvelopeInput, SendOptions, SendResult | | Schema / agents | ENVELOPE_SCHEMA, ACT_TOOL, ACT_DESCRIPTOR, INTENTS, PRIORITIES, STATUSES, ATTACHMENT_KINDS |

Everything is exported from the root (crux-act); the triage helpers and the schema are also available as the layered subpaths crux-act/board and crux-act/schema if you want only those.

Ships as dual ESM + CommonJS with full type declarations and zero runtime dependencies. Works in Node, the browser, Bun, Deno, and edge runtimes.

Built for humans, apps, and AI agents

  • Humans read a ranked board and act.
  • Apps POST an InboundEnvelope to /api/v1/ingest.
  • AI agents register ACT_TOOL and call it.

Same protocol, three audiences, no second-class citizen.

Full specification

The complete spec — message schema, enums, inbound/outbound contracts, reply semantics, conformance levels, and worked examples — ships in this package as PROTOCOL.md (act/1.0).

License

MIT