@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/orchestratorPeer 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.tsScene 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 undernode_modules, which would break the@abraca/dabrapeer 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()