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

tickle-stick

v0.4.2

Published

Opinionated cost-hierarchy pipeline for agentic workflows. Triage cheaply before committing expensive intelligence.

Readme

Tickle-Stick

Cost-hierarchy pipeline for agentic workflows.

Named after the diving tool used to gently probe before committing.

Tickle-Stick Hero


The Problem

You're running your agents wrong.

Every scheduled task — email check, calendar sync, dependabot alerts — triggers a full agent loop at ~$0.15 per invocation. That's $216/month just for email checks. Most of those tasks don't need intelligence.

The Solution: Stage-Based Pipelines

Run cheap scripts first. Classify with a cheap model. Only invoke expensive reasoning when items actually need it. Apply side effects at each step.

Scheduled Task (cron trigger from host)
       │
  ┌────▼─────┐
  │  Script   │  Shell/Python — fetch raw data
  │   FREE    │  Output: WorkItem[] (JSON stdout)
  │   <50ms   │  Empty = pipeline stops here ($0)
  └────┬──────┘
       │ items found
  ┌────▼─────┐
  │  Model   │  Cheap model — classify items
  │  (cheap)  │  routine / urgent / needs-reasoning
  │  ~$0.001  │  Post-hook: apply labels immediately
  └────┬──────┘
       │ filtered items
  ┌────▼─────┐
  │  Model   │  Expensive model — host callback
  │(expensive)│  Reason, synthesize, draft responses
  │  ~$0.05+  │  Post-hook: create drafts, apply labels
  └────┬──────┘
       │
  ┌────▼─────┐
  │ Callback  │  Host-provided function
  │   FREE    │  Deliver, label, escalate — host decides
  └───────────┘

Cost Comparison

| Scenario | Without Tickle-Stick | With Tickle-Stick | Savings | | ------------------------------ | -------------------- | ----------------- | ------- | | Daily email check (60% empty) | $15.00/day | $0.60/day | 96% | | 5 cron tasks/day (80% no data) | $3.75/day | $0.15/day | 96% | | Weekly retro + daily briefing | $1.80/week | $0.30/week | 83% |

Requirements

  • Node.js ≥ 20 (declared in engines).
  • ESM-only. Consumers must import, not require().
  • TypeScript consumers must use "moduleResolution": "bundler" or "node16" (or newer) in tsconfig.json. The library ships .js-suffixed import specifiers per Node16 convention.

Quick Start

Install:

npm install tickle-stick

This end-to-end example runs with no external services — it uses an inline fake triage provider and an echo shell command. Paste into quickstart.mjs, then node quickstart.mjs.

import { Pipeline } from "tickle-stick";

// A fake "cheap model" — classifies everything as needs-reasoning.
// Replace with HttpTriageProvider (see below) for real OpenAI/Anthropic.
const triageProvider = {
  name: "fake-triage",
  async classify(text) {
    return {
      classification: "needs-reasoning",
      response: `stub for: ${text.slice(0, 40)}`,
      confidence: 0.9,
    };
  },
};

const pipeline = new Pipeline({
  name: "demo",
  config: {
    stages: [
      {
        name: "gather",
        type: "script",
        command: "echo",
        args: [
          '[{"id":"demo-1","source":"chat","type":"message","summary":"Hello world","timestamp":"2026-04-22T10:00:00Z"}]',
        ],
        timeout: 5000,
      },
      {
        name: "classify",
        type: "model",
        provider: "cheap",
        systemPrompt: "Classify the item.",
        confidenceThreshold: 0.7,
      },
      {
        name: "reason",
        type: "model",
        provider: "expensive",
        prompt: "Synthesize a response for: {{items}}",
        input: "classified:needs-reasoning",
      },
      { name: "deliver", type: "callback" },
    ],
  },
  triageProvider,
  stageCallbacks: {
    reason: async (items) => `reasoned ${items.length} item(s)`,
    deliver: async (items) => {
      console.log("delivered:", items.map((i) => i.summary).join(", "));
      return "";
    },
  },
});

const result = await pipeline.run();
console.log(
  `items=${result.totalItems} cost=$${result.costEstimate.toFixed(4)}`,
);

Expected output (with telemetry logs elided):

delivered: Hello world
items=1 cost=$0.0010

That's the full loop: script stage produces one item, the fake triage provider classifies it, the reason stage runs the callback, and the deliver callback prints the summary.

YAML Configuration

For production use, move the stage definitions into a YAML file and load them with loadConfig():

# tickle-stick.yaml
tickleStick:
  pipelines:
    email-check:
      stages:
        - name: gather
          type: script
          command: "python3"
          args: ["scripts/check-email.py"]
          timeout: 30000

        - name: classify
          type: model
          provider: cheap
          systemPrompt: |
            Classify this item as JSON:
            {"classification": "routine"|"urgent"|"needs-reasoning",
             "response": "one-line summary", "confidence": 0.0-1.0}
          confidenceThreshold: 0.7
          postHook:
            command: "python3"
            args: ["scripts/apply-labels.py"]

        - name: reason
          type: model
          provider: expensive
          prompt: |
            Here are items that need reasoning:
            {{items}}
            Synthesize a response.
          input: "classified:needs-reasoning,classified:urgent"

        - name: deliver
          type: callback
import { Pipeline, loadConfig } from "tickle-stick";

const config = loadConfig(); // reads ./tickle-stick.yaml or ./config/tickle-stick.yaml
const pipelineConfig = config.tickleStick.pipelines["email-check"];

const pipeline = new Pipeline({
  name: "email-check",
  config: pipelineConfig,
  triageProvider,
  stageCallbacks: {
    reason: async (items, prompt) => callYourReasoningModel(prompt),
    deliver: async (items) => {
      await sendToChannel(items.map((i) => i.summary).join("\n"));
      return "";
    },
  },
});

const result = await pipeline.run();

Script Stages

Script stages are shell commands that output JSON WorkItem[] to stdout:

#!/usr/bin/env python3
import json, sys

items = [
    {
        "id": "email-001",
        "source": "gmail",
        "type": "email",
        "summary": "Meeting tomorrow at 10am",
        "body": "Full email body here...",
        "timestamp": "2026-03-25T10:00:00Z"
    }
]

json.dump(items, sys.stdout)

If the script outputs [] or fails, the pipeline stops at the first stage with $0 cost.

Provider Injection

Tickle-stick does not manage model providers. The host passes in a TriageProvider implementation for cheap model stages:

import type { TriageProvider } from "tickle-stick";
import { parseClassificationResponse } from "tickle-stick";

const myProvider: TriageProvider = {
  name: "my-provider",
  async classify(text, systemPrompt) {
    const raw = await callYourModel(text, systemPrompt);
    return parseClassificationResponse(raw);
  },
};

Or use the built-in HttpTriageProvider for OpenAI/Anthropic APIs:

import { HttpTriageProvider } from "tickle-stick";

const provider = new HttpTriageProvider({
  apiKey: process.env.OPENAI_API_KEY!,
  model: "gpt-4o-mini",
  provider: "openai",
});

Cheap vs. expensive stages

The two kinds of type: model stage wire to different host-supplied mechanisms:

  • provider: cheap — uses the triageProvider option passed to Pipeline. The provider returns a structured {classification, response, confidence} triple. Single wiring point for every cheap-model stage.
  • provider: expensive — uses stageCallbacks[stageName]. The callback receives filtered items and the rendered prompt; it returns a string. Keyed by stage name so you can wire multiple expensive stages independently.

The asymmetry is deliberate: cheap stages share a classifier contract; expensive stages are free-form per-stage reasoning calls.

Post-Hooks

Any stage can have a postHook — a script that runs after the stage completes. The stage output is piped to stdin as JSON. Use post-hooks for side effects:

- name: classify
  type: model
  provider: cheap
  systemPrompt: "..."
  postHook:
    command: "python3"
    args: ["scripts/apply-spam-labels.py"]
    timeout: 15000

Post-hook errors are logged but don't fail the pipeline.

Input Filters

Control which items a stage sees with the input field:

  • all — everything from all previous stages
  • classified:needs-reasoning — only items classified as needs-reasoning
  • classified:urgent,classified:needs-reasoning — comma-separated union
  • (omitted) — all items from previous stages

The classified:* label values must match the classification field returned by your TriageProvider.

Budget & Alerts

Cap cheap model spend and get notified when thresholds are crossed:

tickleStick:
  budget:
    maxDailySpend: 1.00
    maxWeeklySpend: 5.00
    alerts:
      - at: "80%"
      - at: 0.50
    retentionDays: 30

When a budget cap is reached, cheap model stages are skipped — all items pass through to downstream stages.

Storage Adapter

Budget tracking requires a storage adapter to persist events:

import type { StorageAdapter } from "tickle-stick";

const storage: StorageAdapter = {
  writeEvent(event) {
    db.run("INSERT INTO pipeline_events ...", event);
  },
  getSpendSince(since) {
    return db.get("SELECT SUM(cost_estimate) ...", since);
  },
  prune(before) {
    return db.run("DELETE FROM pipeline_events WHERE timestamp < ?", before);
  },
};

Alert Sink

import type { AlertSink } from "tickle-stick";

const alertSink: AlertSink = (alert) => {
  sendToMyChannel(`[Budget] ${alert.message}`);
};

Wiring Budget

const pipeline = new Pipeline({
  name: "email-check",
  config: pipelineConfig,
  triageProvider: myProvider,
  stageCallbacks: { reason: reasoningCallback },
  storage,
  alertSink,
  budgetConfig: config.tickleStick.budget,
  timezone: "America/New_York",
});

// Retention enforcement is automatic — the BudgetManager prunes
// events older than `retentionDays` on the first budget check of a
// process and on each subsequent day rollover. The call below is an
// escape hatch for hosts that want to prune on demand (e.g. tests,
// migrations).
await pipeline.pruneBudgetEvents();

Budget Status API

const status = await pipeline.getBudgetStatus();
if (status) {
  console.log(`Today: $${status.dailySpend.toFixed(2)}`);
  console.log(`This week: $${status.weeklySpend.toFixed(2)}`);
  console.log(`Exceeded: ${status.exceeded}`);
}

Public-beta notice (0.3.x)

Tickle-stick 0.3.x is a public beta. The core pipeline contract (Pipeline, TriageProvider, StorageAdapter, YAML schema) is stable, but a handful of internal-plumbing exports and config edge cases are still being narrowed before 1.0. See the 1.0 tracking issue (label moscow:could) for the full list.

License

MIT — see LICENSE.


Built by Kromatic. We help teams build better products through innovation accounting and experimentation.