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

@flashcatcloud/ai-kit

v0.5.0

Published

Headless runtime primitives shared by Flashcat AI surfaces. Entry /actions: UI action registry, executor, policy gate and visual feedback.

Readme

@flashcatcloud/ai-kit

Headless runtime primitives shared by Flashcat's AI surfaces.

The package deliberately contains no UI. Chat rendering, layout, session management and transport stay with each host — this is only the piece all of them would otherwise write again.

@flashcatcloud/ai-kit/actions

Turns a model-issued tool call into a validated, policy-gated, time-bounded call of host code, and turns the outcome back into a payload the model can reason about.

A host wires two lines:

import { createActionRuntime, createDomFeedback } from '@flashcatcloud/ai-kit/actions';

const runtime = createActionRuntime({
  feedback: createDomFeedback(),
  confirm: (req) => showConfirmDialog(req),   // required for policy: 'confirm'
  onAudit: (e) => log('ui-action', e),
});

// 1. the event stream sees a tool call
if (runtime.shouldHandle(event.name)) {
  const response = await runtime.execute({
    callId: event.callId,
    name: event.name,
    args: event.input,
  });
  // 2. hand it back over the host's existing function_response channel
  sendMessage(null, { functionResponse: { call_id: event.callId, name: event.name, response } });
}

plus runtime.manifest() on the outgoing run request, so the backend knows which actions exist for this turn.

Pages register what they can do while they are mounted:

useEffect(() => runtime.register([
  {
    name: 'set_integration_field',
    description: 'Fill one field of the "new integration" form.',
    inputSchema: { type: 'object', properties: { field: { type: 'string', enum: ['name'] }, value: { type: 'string' } }, required: ['field', 'value'] },
    run: async ({ field, value }, ctx) => {
      await ctx.feedback.moveCursor(`#field-${field}`);
      ctx.feedback.highlight(`#field-${field}`);
      form.setFieldValue(field, value);
      // Read back what the form actually holds — see "What run() returns".
      return { field, value: form.getFieldValue(field) };
    },
  },
]), []);

Registration is scoped on purpose: when the page unmounts, the action goes away, and a call that arrives afterwards comes back as unsupported with a written fallback instead of firing at a page that is no longer there.

One page, several bundles

createActionRuntime gives each caller its own registry, which is right when one host owns the page. It stops being right when a page is assembled from parts that were bundled separately: the part that registers an action and the part whose chat surface executes it each carry their own module state, so they end up with two registries that never see each other — and every call comes back unsupported with nothing wrong in either half.

getSharedActionRuntime is the same runtime pinned to globalThis under a registered symbol, so every bundle on the page resolves to one instance:

import { getSharedActionRuntime, createDomFeedback } from '@flashcatcloud/ai-kit/actions';

// Each host asks the same way. The first call creates the runtime with these
// defaults; every later call, from any bundle, gets that same instance.
export const runtime = getSharedActionRuntime({ feedback: createDomFeedback() });

The argument is defaults, and means it: options only take effect on the call that creates the runtime. A later call cannot reconfigure a registry other code is already registered on, and its onStep/onAudit observers never fire — passing any options to a later call is warned about for that reason. Prefer createActionRuntime whenever one host owns the whole page.

Two things the word "shared" does not cover. The instance lives on the page's globalThis, so a worker or an iframe is a different realm with its own instance — the pin does not cross that boundary. And the key carries the release's compatibility range, so a copy of this library whose ActionRuntime may differ (another 0.x minor, or another major from 1.0) gets its own instance rather than someone else's object: that is deliberately the same outcome as not sharing at all. Call it in browser code only; at module scope in something server-rendered it would pin to the server process instead.

resetSharedActionRuntime() drops the pin so the next call starts fresh — for test isolation across files, mainly. Instances already handed out keep working.

What run() returns

Whatever run() resolves to is handed to the model verbatim as result, and it is the model's only evidence about the outcome. Return state read back after the change — the field's current value, the id the server assigned, the row count that came back — not the arguments echoed.

// ✗ The model can only repeat its own intention.
run: ({ field, value }) => { form.setFieldValue(field, value); return { field, value }; }

// ✓ The model can say what is actually true now.
run: ({ field, value }) => { form.setFieldValue(field, value); return { field, value: form.getFieldValue(field) }; }

The difference only shows up in the case that matters: when something downstream clamped, normalized or ignored the value, and the field does not say what the model asked for. An echoed argument reports success there; a read-back reports the truth.

Asking the user

policy: 'confirm' gates an action behind the host's confirm handler, and fails closed: an action that declares it on a runtime with no handler wired is refused rather than run unattended.

By default the question is asked on every call. For the repetitive steps of one task, confirmScope: 'turn' asks once and lets the rest of that turn through:

{ name: 'set_field', policy: 'confirm', confirmScope: 'turn', /* … */ }
// five fields, one question — instead of interrupting the user five times

A turn ends when the host next calls manifest(), which it already does on every outgoing run request. There is no turn object to open and, more to the point, none to forget to close — an approval cannot outlive the turn by accident. A decline is never remembered, so saying no once does not become a standing answer either way.

Widen the scope for filling and selecting. Leave it at 'call' for anything the user would want to be asked about every single time — which is most of what spends money or deletes data.

Two things follow from the boundary being manifest(), and they are the price of not having a lifecycle to manage:

  • Call manifest() once per outgoing run request, and not for anything else. A host that caches the declarations and stops calling it leaves turn-scoped approvals standing for as long as the tab is open; a host that calls it to populate a debug panel ends them early. The first of those errs in the unsafe direction, which is why 'turn' is opt-in per action rather than a runtime-wide setting.
  • An approval never crosses a boundary in either direction: one granted after its own turn has ended is discarded, and one granted before a page unmounts goes with it, so a remounted action object cannot inherit it.

Two parallel calls of the same action can both reach the gate before either is answered, and the user is asked twice. It fails closed, so this is redundancy rather than a hole, and collapsing it would mean holding one call's answer against a call the user has not been shown.

Telling the user what is happening

An action that runs invisibly is indistinguishable from one that did not run. Three surfaces cover that, and they are independent — take any of them alone.

Per-step visuals. ctx.feedback reveals the target, rings it, glides a virtual cursor onto it and plays a press. createDomFeedback() draws them; noopFeedback() is the default. controlledRegion frames the whole area the assistant is working inside, raised for the duration of an action and not a moment longer.

A session banner. With controlledRegion set, createDomFeedback can also put a strip at the top of the frame saying what is happening, with a button that stops the call in flight. Off unless you pass the words — they are the user's language, not the runtime's:

createActionRuntime({
  controlledRegion: () => document.body,
  feedback: createDomFeedback({
    accent: '108,77,245',                                   // r,g,b — your product's colour
    banner: () => ({ acting: t('ai.acting'), done: t('ai.done'), cancel: t('ai.stop') }),
  }),
});

Stopping aborts the call's signal; an action that honours it comes back as declined with a message saying the user stopped it, and one that ignores it finishes as it would have. The banner is re-worded as done when the call settles and comes down with the rest of the dwell.

A live step log. onStep fires synchronously as each step begins — after every gate has passed, before run() is called — so the host can put the step on screen while it is happening rather than after:

const runtime = createActionRuntime({
  onStep: ({ action, args, label }) => appendStepToChat(label),   // label, or your own line from action + args
  onAudit: (e) => log('ui-action', e),                            // settled outcome, for the record
});

onStep and onAudit are the two halves of one turn: onStep says "this is happening", onAudit says "this is what happened". A step is announced only if it actually ran, so a declined or malformed call never appears in the log.

A tab-title marker. For a user who has tabbed away — and above all for one the assistant is silently waiting on:

createActionRuntime({ titleStatus: { acting: '[AI working]', awaitingUser: '[Needs you]' } });

Off unless you pass labels; there is no language the runtime could default to that is right for every console. The original title is captured when the first mark goes up and restored exactly when the last one comes down.

Why registered actions and not DOM driving

These are our own React applications, so an action can call the same business API the user's own click would. Server-side RBAC therefore applies to the AI exactly as it applies to the user — which a simulated click cannot offer. It is also cheaper: no page snapshot per step, and a UI redesign does not break an action whose signature is unchanged.

Development

npm install
npm test          # vitest, jsdom
npm run build     # tsc → dist/