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

@cotera/watchtower-actions

v0.1.2

Published

Context-aware actions for WatchTower: shortcuts, window events, command surfaces

Downloads

327

Readme

@cotera/watchtower-actions

Context-aware actions for WatchTower.

An action declares what it is called, when it applies, and what it does. It reads the models currently in scope through its context, which is what lets one action be invoked from a command palette, a keyboard shortcut, a window event, or a button without knowing which.

Actions read models, so this package depends on @cotera/watchtower-models and provides a model scope for you.

Install

bun add @cotera/watchtower-actions
bun add react zod neverthrow   # peers

@cotera/watchtower-models comes along as a dependency; import Model, ModelScopeFactory, and the model hooks from there.

Actions

RunModel below is a model from @cotera/watchtower-models; the action finds it by type rather than being handed it.

import {
  BaseAction,
  ActionsRegistryProvider,
  useAction,
  type ActionResult,
  type ApplicableContext,
  type ExecuteContext,
} from '@cotera/watchtower-actions';
import { ok } from 'neverthrow';
import { z } from 'zod';

type SavePayload = { note?: string };

class SaveRunAction extends BaseAction<SavePayload> {
  title = 'Save run';
  shortcut = ['ctrl', 's'];
  inputSchema = z.object({ note: z.string().optional() });

  applicable(context: ApplicableContext): boolean {
    return context.isInScope(RunModel);
  }

  async execute(
    payload: SavePayload,
    context: ExecuteContext
  ): Promise<ActionResult<{ t: string }[]>> {
    const run = context.getInScopeModelOfType(RunModel);
    await save(run, payload.note);
    return ok({});
  }
}

<ActionsRegistryProvider actions={[new SaveRunAction()]}>
  <App />
</ActionsRegistryProvider>;

function SaveButton() {
  const { execute, action } = useAction(SaveRunAction);
  return <button onClick={() => execute()}>{action.title}</button>;
}

execute returns a Result from neverthrow: errors are values, and the framework logs and tracks them for you rather than letting them throw past the call site.

An action that needs input can define ask, which renders a dialog and resolves once the user responds; requiresInputArgs() derives from the schema, so an all-optional schema runs straight through. shouldAsk(context) overrides that per invocation.

Actions registers additional actions for a subtree — mount it inside a page to add page-specific actions that disappear when the page unmounts. Registration is refcounted the same way models are.

Shortcuts are bound from the action's shortcut array. shortcutAvailable() is consulted before the keystroke is consumed, which matters for keys the browser also wants: returning false leaves Cmd-C alone entirely, where an inapplicable applicable() would still have swallowed it.

Tracking and logging

trackingAdapter receives every executed action with its payload and result — wire it to your analytics. An action can shape what gets recorded through its own track(context) method.

logger takes any Logger. The default ConsoleLogger writes to the console and, given an ErrorReporter, forwards errors to your tracker:

const logger = new ConsoleLogger({
  captureException: (error, ctx) =>
    Sentry.captureException(error, { extra: ctx.extra, tags: ctx.tags }),
  setUser: (user) => Sentry.setUser(user),
});

<LoggerProvider logger={logger}>
  <ActionsRegistryProvider actions={actions} trackingAdapter={analytics}>
    <App />
  </ActionsRegistryProvider>
</LoggerProvider>;

How it fits with models

ActionsRegistryProvider owns both registries: the actions themselves, and the ModelScopeManager that answers "what is in scope". It publishes that scope to @cotera/watchtower-models internally, so <ModelScopeFactory> and useInScopeModel from that package resolve against the very registry the actions read.

import { ActionsRegistryProvider } from '@cotera/watchtower-actions';
import { ModelScopeFactory } from '@cotera/watchtower-models';

<ActionsRegistryProvider actions={[new SaveRunAction()]}>
  <ModelScopeFactory createModels={() => [new RunModel(runKey)]} deps={[runKey]}>
    <RunPage /> {/* SaveRunAction is now applicable */}
  </ModelScopeFactory>
</ActionsRegistryProvider>;

ActionsManager.fromExisting(context) builds a throwaway manager holding the same actions and models — this is how a shortcut or window event evaluates applicable() against a fresh copy of the current scope.

Development

bun install       # from the workspace root
bun run test:run
bun run typecheck