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

@bernierllc/content-flow-adapter

v0.2.1

Published

Optional bridge that mirrors content items and editorial stage changes into @bernierllc/flow-engine flows — kanban visualization, flow analytics, and Nevar automation for the content manager with zero hard coupling

Readme

@bernierllc/content-flow-adapter

Optional bridge that mirrors content items and editorial stage changes into @bernierllc/flow-engine flows.

Install it and your content pipeline shows up as a flow: kanban visualization via flow-board-ui, analytics via flow-metrics, automation via Nevar triggers. Don't install it and nothing changes — the content stack has zero dependency on the flow stack, and vice versa. Both sides are injected through duck-typed ports, so the adapter is the only place the two domains meet.

Install

npm install @bernierllc/content-flow-adapter @bernierllc/flow-engine @bernierllc/flow-storage

@bernierllc/flow-engine (and the content packages) are optional peer dependencies — you bring whichever sides you use.

Quick start

import { createFlowEngine } from '@bernierllc/flow-engine';
import { InMemoryFlowStorage } from '@bernierllc/flow-storage';
import { createContentFlowAdapter } from '@bernierllc/content-flow-adapter';

const engine = createFlowEngine({ storage: new InMemoryFlowStorage() });
await engine.start();

const adapter = createContentFlowAdapter({ flowEngine: engine });
await adapter.initialize(); // registers with NeverHub (if present) + ensures the flow definition

// Wire your content event sources (both optional, both detachable):
const detachWorkflow = adapter.attachWorkflowService(contentWorkflowService);
const detachCpm = adapter.attachContentProjectManager(contentProjectManager);

From then on, stage_transitioned events mirror into the flow (unknown content is auto-tracked), content:added starts tracking, content:removed archives. You can also drive it manually:

await adapter.trackContent({ id: 'post-1', title: 'Launch post', contentType: 'blog-post' });
await adapter.syncContentStage('post-1', 'review', { actorId: 'editor-1', actorType: 'user' });
const status = await adapter.getContentFlowStatus('post-1'); // { item, history }
await adapter.untrackContent('post-1'); // archives the flow item

Bidirectional mode (board moves → content system)

const adapter = createContentFlowAdapter({
  flowEngine: engine,
  syncDirection: 'bidirectional',
  onFlowTransition: async (contentId, toStageKey, record) => {
    await contentWorkflowService.transitionStage({
      contentId,
      targetStageId: toStageKey,
      userId: record.actorId,
    });
  },
});
// feed flow-side transitions (e.g. from a board UI) into:
await adapter.handleFlowTransition(record);

Echo suppression is built in: the adapter stamps its own writes with metadata.source = '@bernierllc/content-flow-adapter' and ignores them coming back, plus a time-window guard for unstamped echoes. Genuine ping-pong raises SYNC_LOOP_DETECTED.

The default flow

createDefaultContentFlowDefinition() mirrors the standard editorial lifecycle:

draft → review → approved → scheduled → publishing → published (terminal)
review → draft (reject) · publishing → failed → publishing (retry) · archived

Stage keys match the content stack's status vocabulary (ContentStatus + ContentState), so the default DEFAULT_CONTENT_STAGE_MAPPING is an identity map. Override either:

createContentFlowAdapter({
  flowEngine: engine,
  flowDefinition: myCustomCreateFlowInput,          // replace the whole flow
  mapping: { stages: { live: 'published' }, fallbackStageKey: 'draft' },
});

Ports (duck-typed)

| Port | Satisfied by | Used for | |------|-------------|----------| | FlowEngineLike | @bernierllc/flow-engine's FlowEngine | all flow operations | | WorkflowEventSourceLike | @bernierllc/content-workflow-service (onEvent/offEvent) | mirroring stage transitions | | ContentEventEmitterLike | @bernierllc/content-project-manager (extends @bernierllc/event-emitter) | track/untrack on add/remove | | ContentRef | client Content, WorkflowContent, or any { id } | tracking input |

Error Handling

All errors are ContentFlowAdapterError with ES2022 Error.cause chains.

| Class | Code | Description | Retryable | |-------|------|-------------|-----------| | ContentFlowAdapterError | CONTENT_FLOW_ADAPTER_ERROR | Generic/wrapped engine failure | Depends on cause | | ContentFlowAdapterError | FLOW_NOT_INITIALIZED | Used before initialize() | No — call initialize() | | ContentFlowAdapterError | CONTENT_NOT_TRACKED | Sync/untrack on unknown content | No — trackContent() first | | ContentFlowAdapterError | STAGE_MAPPING_ERROR | Unmapped stage, no fallback | No — fix mapping | | ContentFlowAdapterError | TRANSITION_FAILED | Engine or callback rejected the transition | Sometimes — check cause | | ContentFlowAdapterError | SYNC_LOOP_DETECTED | Ping-pong between the two systems | No — fix wiring | | ContentFlowAdapterError | INVALID_CONFIG | Bad constructor config | No |

Event-driven mirroring (attach*) is fire-and-forget: failures are logged, never thrown — the same precedent as flow-engine's Nevar emission.

NeverHub

Auto-detected at initialize() (standard NEVERHUB_* env vars). Fully functional without it.

License

SEE LICENSE IN LICENSE — Copyright (c) 2025 Bernier LLC. Limited-use license.