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

@tenb/skill-telemetry

v0.1.4

Published

Lightweight telemetry SDK for SkillForge skills — instant or batched reporting

Readme

@tenb/skill-telemetry

Lightweight telemetry SDK for SkillForge skills. Zero dependencies, two modes.

Install

npm install @tenb/skill-telemetry

Slug format

The slug must be author/skill-name — analytics endpoints require both segments. Single-segment values like dating-pilot will cause analytics to return zero results.

// ✅ Correct
createReporter('jaydy/dating-pilot');

// ❌ Wrong — analytics will show 0 calls
createReporter('dating-pilot');

Instant Mode vs Batched Mode

| | Instant | Batched | |---|---|---| | When to use | Short-lived skills, one-off operations | Long-running skills, background processes | | How it works | One HTTP request per event | Buffers events, flushes every N min or when buffer is full | | Cost | Higher per-event overhead | Amortized — better for high-frequency events |

Use instant for: swipe actions, search queries, one-time API calls.
Use batched for: chat systems, polling loops, any skill that runs for minutes or longer.

Short-lived CLI tools — await flush before exit

Instant mode is fire-and-forget. If your CLI process exits immediately after calling report(), the HTTP request may not complete. Use flushAsync() to drain all in-flight requests before exiting:

const reporter = createReporter('author/my-cli');

// ... run your command ...
reporter.report({ toolName: 'run', success: true, duration: elapsed });

// Ensure delivery before exit
await reporter.flushAsync();
process.exit(0);

Instant Mode

import { createReporter } from '@tenb/skill-telemetry';

const report = createReporter('author/skill-name', {
  userId: 'machine-uuid-or-hashed-id',  // optional but recommended
});

// Simple report
report.report({ toolName: 'search', success: true, duration: 150 });

// With business metadata
report.report({
  toolName: 'search',
  success: true,
  duration: 150,
  metadata: { resultCount: 42, query: 'typescript' },
});

// Wrap a function (automatic timing + error classification)
const result = await report.wrap('analyze', async () => {
  return await doAnalysis(input);
});

// Timer handle — explicit success/fail, supports extra fields
const t = report.start('generate');
try {
  const result = await generate(input);
  t.success({ metadata: { tokensUsed: result.tokens } });
} catch (e) {
  t.fail(e, {
    errorCode: 'QUOTA_EXCEEDED',
    errorMessage: 'monthly token quota reached',  // sanitized, no PII
  });
}
// Calling success() or fail() more than once is a no-op (one-shot semantics)

Batched Mode

import { createReporter } from '@tenb/skill-telemetry';

const report = createReporter('author/skill-name', {
  userId: 'machine-uuid-or-hashed-id',
});

// Start batched: buffer events, flush every 5 min or when buffer hits 50
report.startBatched();

// Enqueue events (non-blocking, auto-deduped within 1-second windows)
report.enqueue({ toolName: 'chat-reply', success: true, duration: 200 });
report.enqueue({ toolName: 'message-received', success: true, duration: 0 });

// Auto-flushes on:
// - Buffer reaches maxBufferSize (default 50)
// - Timer interval (default 5 min)
// - Process exit (SIGINT/SIGTERM/beforeExit)

// Manual flush + stop
report.stop();

User Identity

Set userId at construction or later with identify():

// At construction (preferred — applies to all events)
const report = createReporter('my-skill', { userId: 'device-uuid' });

// After construction (e.g. resolved after auth)
report.identify('resolved-user-id');

Use an opaque identifier (machine UUID, hashed email, session token). Never pass raw PII.
You can also set SKILLFORGE_USER_ID env var as a fallback.

Session Grouping

Every reporter instance auto-generates a sessionId (crypto.randomUUID). All events from the same instance share that session ID, so the backend can reconstruct per-session funnels (swipe → match → message).

Override it if you need cross-process session continuity:

const report = createReporter('my-skill', { sessionId: existingSessionId });

Example: dating-pilot integration

import { createReporter } from '@tenb/skill-telemetry';

const telemetry = createReporter('jaydy/dating-pilot', {
  userId: getMachineId(),   // stable device identifier
});

// Swipe (instant mode — one-off operation)
export async function swipeWithFilter(params) {
  return telemetry.wrap('swipe', async () => {
    const result = await doSwipe(params);
    return result;
  }, { metadata: { liked: result.likedCount, skipped: result.skippedCount } });
}

// Chat system (batched mode — long-running)
export async function initChatSystem() {
  telemetry.startBatched(300000); // flush every 5 min

  onMessageReceived((msg) => {
    telemetry.enqueue({ toolName: 'message-received', success: true, duration: 0 });
  });

  onAIReplySent((reply) => {
    telemetry.enqueue({
      toolName: 'ai-reply',
      success: true,
      duration: reply.latency,
      metadata: { model: reply.model },
    });
  });
}

export async function stopChatSystem() {
  telemetry.stop(); // flush remaining buffer
}

Options

createReporter('slug', {
  apiUrl: 'http://localhost:3002',  // default: https://skillforge.ibea.ai
  disabled: false,                  // or set SKILLFORGE_TELEMETRY=off
  flushInterval: 300000,            // batched mode: 5 min
  maxBufferSize: 50,                // batched mode: auto-flush threshold
  userId: 'opaque-user-id',        // per-user analytics (never PII)
  sessionId: 'session-uuid',       // override auto-generated session ID
});

Privacy

  • Only sends: skillSlug, toolName, success, duration, errorType, errorCode, errorMessage (if provided), sessionId, userId (if provided), metadata (if provided)
  • Never sends: user content, API keys, personal data
  • errorMessage and metadata are caller-provided — you are responsible for stripping PII before passing them
  • Set SKILLFORGE_TELEMETRY=off to disable
  • All requests are fire-and-forget (never blocks your skill)