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/onboarding-feature-plugin

v0.2.0

Published

Plugin contract and registry for extending onboarding agents with goals, tools, side-panel slots, and handoffs.

Readme

@bernierllc/onboarding-feature-plugin

Plugin contract and registry for extending onboarding agents with additional goals, questions, tools, side-panel UI slots, and post-completion handoffs.

Overview

This package owns the extension contract and merge logic for the BernierLLC onboarding system. Concrete plugins (e.g. @bernierllc/onboarding-plugin-website-builder) live in their own packages and depend on this contract.

Key capabilities:

  • OnboardingPlugin interface — the complete plugin contract every author must implement
  • PluginRegistry — in-memory registry with duplicate-id detection
  • mergePluginContributions() — deterministic merge of goals, phases, tools, panels, and handoffs into a base OnboardingConfig
  • Bulk collision detection — reports all id/name conflicts at once, not just the first
  • Ordering rules — deterministic phase and handoff ordering with warning logging for inter-base-phase slots

Installation

npm install @bernierllc/onboarding-feature-plugin

Usage

Registering and using a plugin

import {
  PluginRegistry,
  mergePluginContributions,
  OnboardingPluginError,
  OnboardingPluginConflictError,
} from '@bernierllc/onboarding-feature-plugin';
import type {
  OnboardingPlugin,
  PluginContext,
  OnboardingGoal,
  PluginToolDefinition,
  PluginPanelDescriptor,
  HandoffTarget,
  MergedConfig,
} from '@bernierllc/onboarding-feature-plugin';

// 1. Define a plugin (typically imported from a concrete plugin package)
const websiteBuilderPlugin: OnboardingPlugin = {
  id: 'website-builder',
  name: 'Website Builder Plugin',
  version: '1.0.0',

  configSchema: {
    type: 'object',
    properties: { siteTemplate: { type: 'string' } },
    required: ['siteTemplate'],
  },

  validateConfig(config: unknown): asserts config is Record<string, unknown> {
    if (
      typeof config !== 'object' ||
      config === null ||
      typeof (config as Record<string, unknown>)['siteTemplate'] !== 'string'
    ) {
      throw new OnboardingPluginError('siteTemplate (string) is required', {
        code: 'INVALID_PLUGIN_CONFIG',
      });
    }
  },

  contributeGoals(_ctx: PluginContext): OnboardingGoal[] {
    return [
      {
        id: 'website-builder:domain-chosen',
        description: 'Domain preference captured',
        required: false,
        phase: 'getting-specific',
        completionCheck: { type: 'field-present', field: 'preferredDomain' },
      },
    ];
  },

  contributePhases: () => [],

  contributeTools(_ctx: PluginContext): PluginToolDefinition[] {
    return [
      {
        name: 'previewWebsite',
        description: 'Generate a live website preview',
        parameters: {
          type: 'object',
          properties: { template: { type: 'string' } },
          required: ['template'],
        },
        requiredPermissions: ['website:preview'],
      },
    ];
  },

  contributePanel(_ctx: PluginContext): PluginPanelDescriptor | null {
    return {
      slotId: 'website-builder-preview',
      label: 'Website Preview',
      triggerToolNames: ['previewWebsite'],
      metadata: { position: 'right' },
    };
  },

  contributeHandoffs(
    _ctx: PluginContext,
    state: Record<string, unknown>
  ): HandoffTarget[] {
    if (!state['sitePublished']) return [];
    return [
      {
        id: 'website-builder:view-site',
        type: 'external',
        label: 'View your live site',
        order: 10,
      },
    ];
  },
};

// 2. Register the plugin
const registry = new PluginRegistry();
registry.register(websiteBuilderPlugin);

// 3. Validate per-instance config before merge
registry.validatePluginConfig('website-builder', {
  siteTemplate: 'contractor-default',
});

// 4. Merge into a base config
const merged: MergedConfig = mergePluginContributions(
  baseConfig,
  [{ plugin: websiteBuilderPlugin, config: { siteTemplate: 'contractor-default' } }],
  currentSessionState // optional — defaults to {}
);

// merged.config   — OnboardingConfig with combined goals, phases, handoffs
// merged.tools    — PluginToolDefinition[] to register with agent-tool-registry
// merged.panels   — PluginPanelDescriptor[] to pass to the UI host
// merged.pluginIds — ['website-builder'] — registration order for auditability

Handling multiple plugins

import { PluginRegistry, mergePluginContributions } from '@bernierllc/onboarding-feature-plugin';

const registry = new PluginRegistry();
registry.register(websiteBuilderPlugin);
registry.register(calendarPlugin);

console.log(registry.listRegistered()); // ['website-builder', 'calendar']

const merged = mergePluginContributions(
  baseConfig,
  [
    { plugin: websiteBuilderPlugin, config: { siteTemplate: 'default' } },
    { plugin: calendarPlugin, config: { timezone: 'America/New_York' } },
  ]
);

Error handling

import {
  OnboardingPluginError,
  OnboardingPluginConflictError,
} from '@bernierllc/onboarding-feature-plugin';

// Duplicate registration
try {
  registry.register(websiteBuilderPlugin); // already registered
} catch (err) {
  if (err instanceof OnboardingPluginError) {
    console.error(err.code);    // 'PLUGIN_ALREADY_REGISTERED'
    console.error(err.context); // { pluginId: 'website-builder' }
  }
}

// Config validation failure
try {
  registry.validatePluginConfig('website-builder', { siteTemplate: 42 });
} catch (err) {
  if (err instanceof OnboardingPluginError) {
    console.error(err.code);  // 'PLUGIN_CONFIG_INVALID'
    console.error(err.cause); // original error from plugin.validateConfig
  }
}

// Id collision during merge (all conflicts reported at once)
try {
  mergePluginContributions(baseConfig, [pluginWithDuplicateGoalId]);
} catch (err) {
  if (err instanceof OnboardingPluginConflictError) {
    console.error(err.conflicts); // ['goal:goal-name', 'tool:sharedTool']
  }
}

API

PluginRegistry

class PluginRegistry {
  constructor(logger?: Logger)

  /** Registers a plugin. Throws OnboardingPluginError if id is already registered. */
  register(plugin: OnboardingPlugin): void

  /** Returns the plugin for the given id, or undefined if not found. */
  resolve(id: string): OnboardingPlugin | undefined

  /** Validates per-instance config via the plugin's own validateConfig. Wraps errors with plugin context. */
  validatePluginConfig(id: string, config: unknown): void

  /** Returns registered plugin ids in registration order. */
  listRegistered(): string[]
}

mergePluginContributions()

function mergePluginContributions(
  base: OnboardingConfig,
  plugins: ReadonlyArray<{ plugin: OnboardingPlugin; config: unknown }>,
  state?: Record<string, unknown>,
  logger?: Logger
): MergedConfig

Deterministic merge algorithm:

| Item | Strategy | |------|----------| | Goals | Appended after base goals in plugin registration order | | Phases | Appended, then sorted by order; inter-base-phase slots logged as warnings | | Handoffs | Merged, sorted by order; ties broken by registration order (stable sort) | | Tools | Concatenated in registration order | | Panels | Concatenated — each plugin may contribute one or zero panels |

All id/name collisions (goals, phases, handoffs, tools) are collected before throwing OnboardingPluginConflictError, so callers see the complete picture.

OnboardingPlugin interface

interface OnboardingPlugin {
  id: string;
  name: string;
  version: string;
  configSchema: Record<string, unknown>;

  validateConfig(config: unknown): asserts config is Record<string, unknown>;
  contributeGoals(context: PluginContext): OnboardingGoal[];
  contributePhases(context: PluginContext): OnboardingPhase[];
  contributeTools(context: PluginContext): PluginToolDefinition[];
  contributePanel?(context: PluginContext): PluginPanelDescriptor | null;
  contributeHandoffs(context: PluginContext, state: Record<string, unknown>): HandoffTarget[];
}

Error classes

| Class | Code | When thrown | |-------|------|-------------| | OnboardingPluginError | ONBOARDING_PLUGIN_ERROR | Base error for all plugin-related failures | | OnboardingPluginError | PLUGIN_ALREADY_REGISTERED | Duplicate id on register() | | OnboardingPluginError | PLUGIN_NOT_FOUND | Unknown id on validatePluginConfig() | | OnboardingPluginError | PLUGIN_CONFIG_INVALID | Plugin's validateConfig throws | | OnboardingPluginConflictError | ONBOARDING_PLUGIN_CONFLICT | Id/name collision during merge |

All errors follow ES2022 Error.cause chaining — underlying errors are never swallowed.

Integration Documentation

Logger integration

PluginRegistry and mergePluginContributions accept an optional @bernierllc/logger Logger instance. When omitted, a silent no-transport logger is created internally so the package produces no console output by default.

import { Logger, LogLevel, ConsoleTransport } from '@bernierllc/logger';
import { PluginRegistry } from '@bernierllc/onboarding-feature-plugin';

const logger = new Logger({
  level: LogLevel.DEBUG,
  transports: [new ConsoleTransport()],
});
const registry = new PluginRegistry(logger);

Logged events:

  • DEBUG — plugin registered, merge summary
  • WARN — plugin phase order slots between base phases

NeverHub integration

This is a pure core package with no I/O, no React, and no NeverHub dependency. NeverHub integration is handled at the service layer (onboarding-agent-service) which receives the MergedConfig output and can register it with @bernierllc/neverhub-adapter. Graceful degradation applies automatically — core functionality works regardless of NeverHub availability.

Integration flow

onboarding-agent-service
  ↓ registry.resolve(id) + registry.validatePluginConfig(id, pluginConfig[id])
  ↓ mergePluginContributions(baseConfig, enabledPlugins, currentState)
  → MergedConfig
      .config  → onboarding-config-core.compileSystemPrompt()
      .tools   → agent-tool-registry (registered by service layer)
      .panels  → onboarding-chat-ui (host panel mapping)

Writing a plugin package

  1. Depend on @bernierllc/onboarding-feature-plugin
  2. Implement OnboardingPlugin — give your plugin a stable, namespaced id (e.g. 'my-org:feature-name')
  3. Export the plugin object or a factory function
  4. The consuming service registers it: registry.register(myPlugin)

License

Copyright (c) 2025 Bernier LLC. See LICENSE for details.