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

@abraca/orchestrator

v2.15.0

Published

CouShell commercial director — orchestrate simulated actors on Abracadabra for screen recording

Readme

@abraca/orchestrator

Orchestrate simulated actors on an Abracadabra CRDT server for screen recording. Define a cast of actors, script their actions on a timeline, and run the scene — the orchestrator connects to the server and performs everything in real time while you record the dashboard.

Documentation

A structured, code-derived reference lives in docs/ — the define() DSL, the full actions reference, and the drift-corrected timeline / actor-auth runtime model. This README is the hand-written guide; docs/ is the source of truth for exact behaviour.

Installation

pnpm add @abraca/orchestrator

Peer dependencies: @abraca/dabra, yjs, y-protocols

Quick Start

import { defineScene, actor, actions } from '@abraca/orchestrator'

export default defineScene({
  server: { url: 'ws://localhost:8080' },
  actors: [
    actor('Alice', { color: '#F97066' }),
    actor('Bob', { color: '#4A90D9' }),
  ],
  timeline: [
    { at: 0, actor: 'Alice', action: actions.connect() },
    { at: 0, actor: 'Bob', action: actions.connect() },
    { at: 1000, actor: 'Alice', action: actions.navigate('my-doc') },
    { at: 2000, actor: 'Alice', action: actions.type('my-doc', 'Hello from Alice!') },
    { at: 3000, actor: 'Bob', action: actions.navigate('my-doc') },
    { at: 4000, actor: 'Bob', action: actions.type('my-doc', '\nHello from Bob!') },
    { at: 8000, actor: 'Alice', action: actions.disconnect() },
    { at: 8000, actor: 'Bob', action: actions.disconnect() },
  ],
})

CLI Usage

# Build packages first
pnpm build:packages

# Run a scene script (from repo root)
node --experimental-transform-types \
  packages/orchestrator/dist/abracadabra-orchestrator.esm.js ./my-scene.ts

# Validate without connecting
node --experimental-transform-types \
  packages/orchestrator/dist/abracadabra-orchestrator.esm.js --dry-run ./my-scene.ts

Scene scripts are TypeScript files loaded via --experimental-transform-types. The --dry-run flag validates the scene structure (actor names, action types, required fields) and prints the timeline without connecting to the server.

Note: Do not use --conditions=source — Node's type stripping does not support files under node_modules, which would break the @abraca/dabra peer dependency resolution.

Scene Definition

A scene is defined with defineScene() and must export as the default export.

defineScene({
  server: { url: 'ws://localhost:8080', inviteCode: 'optional-invite' },
  actors: [ ... ],
  timeline: [ ... ],
  vars: { hubDocId: 'abc-123' },       // pre-defined variables
  duration: 30000,                      // auto-stop after 30s
  onStart: async () => { ... },        // runs before timeline
  onEnd: async () => { ... },          // runs after timeline
})

Actors

actor('Name', {
  color: '#hex',          // cursor / avatar color
  keyFile: './keys/name', // optional Ed25519 key path (auto-generated if missing)
  avatar: 'https://...',  // optional avatar URL
})

Actors authenticate via Ed25519 challenge-response. If no keyFile is provided, a deterministic keypair is derived from the actor name.

Actions Reference

All actions are created via the actions factory object.

Connection

| Action | Factory | Description | |--------|---------|-------------| | connect | actions.connect() | Connect actor to server, authenticate, sync root doc | | disconnect | actions.disconnect() | Clear awareness and disconnect gracefully |

Navigation

| Action | Factory | Description | |--------|---------|-------------| | navigate | actions.navigate(docId) | Set actor's active document (awareness docId field) |

Text Editing

| Action | Factory | Description | |--------|---------|-------------| | type | actions.type(docId, text, opts?) | Type text character-by-character with realistic timing | | typeDelete | actions.typeDelete(docId, count, opts?) | Delete characters one at a time (backspace) | | select | actions.select(docId, anchor, head) | Set a text selection range | | moveCursor | actions.moveCursor(docId, from, to, duration, easing?) | Animate cursor movement with easing |

Options for type: { speed?: number, variance?: number, position?: number } Options for typeDelete: { speed?: number, variance?: number, position?: number } Easing: 'linear' | 'easeIn' | 'easeOut' | 'easeInOut'

Document Operations

| Action | Factory | Description | |--------|---------|-------------| | createDocument | actions.createDocument(parentId, label, opts?) | Create a new doc in the tree | | moveDocument | actions.moveDocument(docId, newParentId, order?) | Move a doc to a new parent | | renameDocument | actions.renameDocument(docId, label) | Rename a document | | writeContent | actions.writeContent(docId, markdown) | Set doc content from markdown (bulk) | | deleteContent | actions.deleteContent(docId, from, length) | Delete a range of content elements | | setMeta | actions.setMeta(docId, meta) | Merge metadata fields (icon, color, etc.) |

Options for createDocument: { docType?: string, meta?: Record<string, unknown>, assignId?: string }

The assignId option stores the newly created document's ID in the scene vars map, so later actions can reference it with ${varName}.

Awareness & UI

| Action | Factory | Description | |--------|---------|-------------| | setStatus | actions.setStatus(status) | Set actor's status text (or null to clear) | | setAwareness | actions.setAwareness(fields, docId?) | Set arbitrary awareness fields | | clearAwareness | actions.clearAwareness(fields, docId?) | Remove awareness fields | | pointerMove | actions.pointerMove(docId, from, to, duration, easing?) | Animate pointer movement | | scrollTo | actions.scrollTo(docId, position) | Set scroll position (0–1) |

Kanban

| Action | Factory | Description | |--------|---------|-------------| | kanbanHover | actions.kanbanHover(docId, cardId) | Hover a kanban card (or null to clear) | | kanbanDrag | actions.kanbanDrag(docId, cardId, toColumnId, duration) | Animate dragging a card to a column |

Chat

| Action | Factory | Description | |--------|---------|-------------| | sendChat | actions.sendChat(channel, message) | Send a chat message via stateless protocol |

Flow Control

| Action | Factory | Description | |--------|---------|-------------| | wait | actions.wait(duration) | Pause for N milliseconds | | parallel | actions.parallel(entries) | Run timeline entries concurrently | | sequence | actions.sequence(entries) | Run timeline entries one after another | | repeat | actions.repeat(times, entries) | Loop entries N times |

Timeline

Timeline entries are scheduled by their at field (milliseconds from scene start). Entries with the same at value run in parallel. Within parallel and sequence blocks, at is relative to the block's start.

timeline: [
  // These run at the same time (both at 0ms)
  { at: 0, actor: 'Alice', action: actions.connect() },
  { at: 0, actor: 'Bob', action: actions.connect() },

  // Nested parallel block with relative offsets
  {
    at: 2000,
    action: actions.parallel([
      { actor: 'Alice', action: actions.type('doc', 'Hello') },
      { at: 500, actor: 'Bob', action: actions.type('doc', 'World') },
    ]),
  },

  // Sequence: each runs after the previous completes
  {
    at: 5000,
    action: actions.sequence([
      { actor: 'Alice', action: actions.type('doc', 'Line 1\n') },
      { actor: 'Alice', action: actions.type('doc', 'Line 2\n') },
    ]),
  },

  // Repeat: loop 3 times
  {
    at: 10000,
    action: actions.repeat(3, [
      { actor: 'Bob', action: actions.type('doc', '.') },
      { action: actions.wait(1000) },
    ]),
  },
]

Variables

Define variables in vars and reference them with ${name} in string fields (docId, parentId, text, label, markdown, etc.).

defineScene({
  vars: { hubDoc: 'abc-123' },
  // ...
  timeline: [
    // Create a doc and store its ID as "newDoc"
    {
      at: 1000,
      actor: 'Alice',
      action: actions.createDocument('${hubDoc}', 'My Page', { assignId: 'newDoc' }),
    },
    // Reference the created doc later
    {
      at: 3000,
      actor: 'Alice',
      action: actions.type('${newDoc}', 'Content goes here'),
    },
  ],
})

Programmatic API

import { Orchestrator } from '@abraca/orchestrator'

const orchestrator = new Orchestrator()

await orchestrator.load('./my-scene.ts')
orchestrator.prepare()
orchestrator.dryRun()     // optional: validate + print timeline
await orchestrator.run()
await orchestrator.cleanup()