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

@aistrike-dev/ui-shell-sdk

v3.5.0

Published

Shell-to-MFE contract for the AiStrike UI shell: manifest, lifecycle, storage conventions, event bus, Vite plugin, conformance kit.

Readme

@aistrike-dev/ui-shell-sdk

The SDK for building a micro front-end (MFE) that runs in the AiStrike UI shell: the types and helpers that implement the shell contract (manifest, storage keys, bus topics), the Vite plugin that builds an MFE and the conformance kit that checks one. Bundle it into your MFE; nothing is shared at runtime. Design: docs/superpowers/specs/2026-10-02-ui-shell-design.md in the ui-shell repository.

Build an MFE

// vite.config.ts
import react from '@vitejs/plugin-react';
import { mfe } from '@aistrike-dev/ui-shell-sdk/vite';
import { defineConfig } from 'vite';

export default defineConfig({ plugins: [react(), mfe({ id: 'detections', devPort: 5201 })] });

devPort sets the dev server's port. In dev, an imported asset's URL resolves against the module that imports it, as in the build, so it points at your server on whichever host the browser used, not at the shell's. Styles must come from Emotion: CSS files imported by the entry are not injected, since there is no HTML in the build.

vite build writes dist/entry.js, dist/chunks/*, dist/assets/* and dist/manifest.json, all with relative URLs, so the folder runs from any origin. Deploy it to <origin>/ui/<version>/ and point <origin>/ui/latest/manifest.json at it with a temporary, uncached redirect.

The entry plus everything it imports statically must stay under 20 KB gzipped (entryBudgetBytes to change). Put the UI behind import() inside mount.

entityRenderers (published path → source, e.g. { 'renderers/alert.js': 'src/renderers/alert.ts' }) builds each entity renderer at its path, outside the budget, as settingsModules does for custom settings elements; a path may be published by only one of the two. In dev the path serves the source.

Page-tool descriptors live in src/tools.ts (tools to move it, false to skip the check), one exported ToolDescriptor constant per tool. vite build loads the file and fails on any export that is not a valid descriptor (a description over 1024 characters or a descriptor over 4096 characters of JSON included) and on a name exported twice. The file may import types only (import type { ToolDescriptor } from '@aistrike-dev/ui-shell-sdk'): the check loads it with Vite's runnerImport, which leaves packages to Node, so a value import may not load and is reported as src/tools.ts could not be loaded. Without the file, the domain has no page tools.

Write the entry

// src/entry.ts
import type { Activate } from '@aistrike-dev/ui-shell-sdk';

export const activate: Activate = (ctx) => {
  let dispose: (() => void) | undefined;
  let gen = 0;                                           // bumps on every mount/unmount; a stale import bails out
  return {
    nav: [{ label: 'Rules', path: '/rules' }],          // shown under your domain in the shell's navigation
    settings: null,                                    // or a settings descriptor
    entities: [],                                      // kinds of thing people can reference; see Entities
    commands: [],                                      // global commands; see The command menu
    async mount(container) {
      gen += 1;
      const my = gen;
      dispose?.();
      dispose = undefined;
      const { mountUi } = await import('./ui.js');       // React, your router, your design system
      if (my !== gen) { return; }                        // unmounted or remounted during the import
      dispose = mountUi(container, ctx);                 // use ctx.domain.route as the router basename
    },
    unmount() { gen += 1; dispose?.(); dispose = undefined; },   // leave the container empty
  };
};

The page-wide tab id lives in the global __aistrikeTabId, which every SDK version in a page must agree on.

An activation states every field: a list is [] for none and settings is null for none. The shell fails a domain whose activation lacks a field, with a reason naming each one, and ignores the fields earlier builds carried (assistant, pane, pageCommands) with one warning. The assistant domain also exports activateSession (an ActivateSession: (ctx) => SessionContribution), whose mount(container, host) mounts one view, a session or the sessions list, reports to the shell through its SessionHost and returns a SessionHandle (docs/superpowers/specs/2026-11-06-assistant-session-pane-design.md, 4.1). It replaces activatePane, which is removed: the shell no longer loads it, and the conformance kit names a leftover activatePane export as a problem.

Inside the MFE: create your own React root; wrap it in your own error boundary; create your own Emotion cache with key: ctx.domain.id; do not render CssBaseline (the shell does). Create the Emotion cache once per MFE module, not per mount: a new cache on every mount injects its styles again each time.

Read shared state

import { getAccessToken, getClaims, getTenant, getThemeMode, getDomain } from '@aistrike-dev/ui-shell-sdk';

const token = getAccessToken();        // read at request time; never cache
const tenant = getTenant();            // this tab's tenant
const other = getDomain('hunting');    // { apiBase, uiBase, version, state } for cross-domain calls

Preferences

@aistrike-dev/ui-shell-sdk/preferences is the one client the shell, every MFE and the design system use. Preferences live in web storage, one JSON object per namespace and scope, under aistrike.prefs.<scope>.<namespace>; an MFE's namespace is its domain id.

import { createPreferences } from '@aistrike-dev/ui-shell-sdk/preferences';

const prefs = createPreferences({ namespace: ctx.domain.id });
prefs.set('density', 'compact');                       // scope 'user' (default): synced by the shell
prefs.set('sidebar.collapsed', true, { scope: 'device' }); // this browser only
prefs.get('density');                                  // tab → device → user
prefs.resolve(['DataGrid[list].columns', 'DataGrid.columns']); // instance before type default, then scope
const stop = prefs.subscribe(({ name }) => { /* re-read */ });

Every change is announced on the bus as preferences.<namespace>.changed (browser scope, payload { namespace, scope, name }, no value); copies of the client in other MFEs and other tabs re-read storage. Only the shell talks to the preferences API: it syncs the user buckets. React: @aistrike-dev/ui-shell-sdk/react exports PreferencesProvider, usePreferencesClient and usePreference.

Publish and subscribe

import { SHELL_TOPICS, createBus, type TenantChangedPayload } from '@aistrike-dev/ui-shell-sdk';

const bus = createBus({ source: 'detections' });
bus.subscribe<TenantChangedPayload>(SHELL_TOPICS.tenantChanged, () => refetch());
bus.publish('detections.rule.saved', { id }, { scope: 'browser' });   // topics start with your domain id

scope is required on every publish; SHELL_TOPICS holds the shell's topic names, with a payload type for each.

Scopes: tab (this tab), browser (every tab), user (reserved for the websocket bridge). Payloads are hints to refetch, not data of record. Additive changes only; a breaking change is a new topic.

Entities

An entity type is a kind of thing people can reference: the assistant's @ mentions, its reference chips and the command menu's search all read the same declaration, at the activation's root.

// src/entry.ts
entities: [{ type: 'alert', label: 'Alert', icon: 'alert', renderer: 'renderers/alert.js', search: '/api/alerts/search' }],
  • renderer: a module path relative to the domain's uiBase, built by the Vite plugin's entityRenderers, or null. The module exports mount(el, { ref, details, presentation }) and returns its cleanup; draw text with textContent, never innerHTML. Presentations are inline, popover and card; one that does not know a presentation draws its card. loadEntityRenderer(url) loads a module, with a 5 s timeout.
  • search: a path under /api/ on the domain's origin that takes ?query= and answers { entities: EntityRef[] }, or null when the type is not searchable. A path that could leave the domain's API (.., a host, a query, a fragment, a backslash, an encoded dot) is refused and the type dropped. The shell calls it with the person's token, not the page.

The command menu

The shell's command bar (⌘K / Ctrl+K, rebindable in Settings → Keyboard shortcuts) opens one menu for the whole page. A domain puts things in it two ways, and is searched from it a third.

// src/entry.ts: global commands, whatever page is open. The shell calls `run` in its own document.
export const activate: Activate = (ctx) => ({
  commands: [{
    id: 'go-cases', title: 'Go to Cases', keywords: ['investigations'], icon: null,
    run: () => navigate('/investigations/cases'),
  }],
  ...
});

A global command states { id, title, keywords, icon, run }, with keywords: [] and icon: null for none. run may return a promise; if it throws, the shell shows <title>: <error message> in a notice. There is no href. An activation carries at most 100 commands, and a list with an invalid command is refused whole.

// Inside a mounted page: what can be done right now, offered by a component with its whole descriptor (see Page context).
// The handler runs in the page; the shell invokes it over the bus.
useCommand({ id: 'dismiss', title: 'Dismiss selected alerts', keywords: [], icon: null }, () => dismiss(selection));

Search uses the entity types' search paths (see Entities). As the person types, the shell calls the path of every active domain's searchable types in parallel, with the person's token, and lists each answer as it arrives, at most 10 per type. An entity is listed when it has an href.

Page context

A routed page tells the assistant and the command menu what is on screen, and what it can do, through one PageController, created when the page mounts. Nothing about page tools or page commands is declared at activation: a component offers each with its whole descriptor while it is mounted. Page-tool descriptors are constants in src/tools.ts, which vite build checks (see Build an MFE); a page command's descriptor is written where it is offered and checked by the controller. Import the hooks from @aistrike-dev/ui-shell-sdk/react.

// src/tools.ts: types only.
import type { ToolDescriptor } from '@aistrike-dev/ui-shell-sdk';

export const selectAlerts: ToolDescriptor = {
  name: 'select_alerts', description: 'Select alerts in the table on screen, by id', effect: 'read',
  input: { type: 'object', properties: { ids: { type: 'array', items: { type: 'string' } } }, required: ['ids'] },
};
export const clearSelection: ToolDescriptor = {
  name: 'clear_selection', description: 'Clear the alert selection', input: { type: 'object', properties: {} }, effect: 'read',
};
function AlertsRoute({ ctx }: { ctx: ActivationContext }): ReactElement {
  const controller = usePageController(() => new PageController({ domainId: ctx.domain.id }), []);
  return <Page controller={controller}><Alerts /></Page>;
}

function Alerts(): ReactElement {
  const ask = useAsk();
  useTitle('Needs triage');
  useContext({
    summary: '96 alerts, sorted by agent score',
    entities: rows,
    selection,
    suggestions: [
      { text: 'Select the cluster', expects: [{ tool: 'select_alerts' }] },
      ...(selection.length > 0 ? [
        { text: `Explain ${selection.map(md).join(', ')}`, expects: [{ tool: 'get_alert' }], details: detailsOf(selection) },
        { text: 'Clear the selection', expects: [{ tool: 'clear_selection' }] },
      ] : []),
    ],
  });
  useTool(selectAlerts, (input) => select(input));
  useTool(clearSelection, () => clear(), { relevant: selection.length > 0 });
  useCommand({ id: 'select-all', title: 'Select every alert shown', keywords: ['all'], icon: null }, () => selectAll());
  // ...
}
  • useContext(object | null, { key? }): this component's part of the page's context. The page merges every mounted part; null or unmounting withdraws it.
  • useTool(descriptor, handler, { relevant? }), useCommand(descriptor, handler, { relevant? }): offer a page tool or command with its descriptor while relevant is true, the default, and run handler when it is called. Handlers always run the latest render's function; a new descriptor object with the same content keeps the registration, a changed one registers again. A page command's descriptor states every field: { id, title, keywords, icon }, with keywords: [] and icon: null for none, and no href.
  • The controller refuses, with a console error, a descriptor that is not valid, and one unlike the descriptor already offered under its name: the first offered wins until every handler that offered it is gone. Equal descriptors share the name, and the most recently registered handler runs. A page offers at most 20 page tools and 20 page commands (MAX_PAGE_TOOLS, MAX_PAGE_COMMANDS); one past the cap is left out of the report, with a console error, until another goes. A handler registered with relevant: false does not count.
  • useAsk(): asks the assistant about refs ({ prompt?, refs? }); does nothing outside a Page.
  • useTitle(title), setTitle(title): the page's title, document.title, which the shell reads. navigate(path, { title }) sets it for SDK navigations. When the person moves to another domain, the shell restores its default title after unmounting the old domain and before mounting yours, so set your title on mount if you want one.
  • Outside React, call PageController directly: contribute(), handleTool(descriptor, …), handleCommand(descriptor, …), ask(), dispose().

Check your build

// src/conformance.test.ts, under Vitest's happy-dom environment
import { runConformance } from '@aistrike-dev/ui-shell-sdk/conformance';

const result = await runConformance({ distDir: 'dist', id: 'detections' });
expect(result.ok).toBe(true);

Local development

Add shellDevHost() from @aistrike-dev/ui-shell-dev/vite after mfe(); vite then serves the shell at / and your MFE at /ui/latest/ from one origin with HMR.