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

@haneinfo/tags

v0.3.9

Published

Human-managed flat instance tags, readable suggestions and read-only MCP.

Readme

@haneinfo/tags

Human-managed, free, flat tags on state-machine instances. Implemented from approved decision 202610-2c5d. See package.json for the current package version. There is no tag catalog, color, tree, frontend, import mapping or MCP mutation.

Register tags() from @haneinfo/tags/plugin, configRef tags, or TagsModule.register({}). The module exports TagsService, TagsMcpTools and INSTANCE_TAGS / haneinfo.instance-tags. Upgrade plugin-api (contract 1.17.0), state-machine, list-view and collaboration together. List-view optionally consumes the port, so it never guesses availability from a missing-table error. Without it, tag filters return TAGS_NOT_AVAILABLE/409 and result rows have tags: [].

Data and migration

Apply prisma/tags.prisma and the complete App migration in prisma/tags.sql before enabling. The App adds tags InstanceTag[] to StateMachineInstance, alongside its optional Attachment reverse relation. The core state-machine fragment stays independent of this optional package.

InstanceTag / instance_tags contains id, instanceId, normalizedKey, label, createdBy, createdAt. The instance FK cascades on deletion. Unique (instanceId, normalizedKey) ensures deduplication; (normalizedKey, instanceId) supports filters/aggregation; (instanceId) supports listing and cascade. There is no independent tag table or user FK. Permanent lifecycle purge needs no tag listener.

The App migration SQL is:

CREATE TABLE instance_tags (
  id TEXT PRIMARY KEY,
  instance_id TEXT NOT NULL,
  normalized_key TEXT NOT NULL,
  label TEXT NOT NULL,
  created_by TEXT NOT NULL,
  created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
  CONSTRAINT instance_tags_instance_id_fkey FOREIGN KEY (instance_id)
    REFERENCES state_machine_instances(id) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE UNIQUE INDEX instance_tags_instance_id_normalized_key_key ON instance_tags(instance_id, normalized_key);
CREATE INDEX instance_tags_normalized_key_instance_id_idx ON instance_tags(normalized_key, instance_id);
CREATE INDEX instance_tags_instance_id_idx ON instance_tags(instance_id);

Normalization

Both writes and list filters call plugin-api's pure normalizeInstanceTag. Reject Cc/Cf characters before whitespace folding, except Tab, which becomes whitespace. Newlines, Unicode line/paragraph separators and format controls are invalid. Apply Unicode NFKC, trim, and fold whitespace to one ASCII space; label retains the resulting first spelling/case. Length is 1–30 normalized Unicode code points by default, configurable. normalizedKey is label.toLowerCase(), locale independent. U+0130 becomes i + U+0307; lowercase expansion is not subjected to the label length limit again. This is simple Unicode lowercasing, without accent removal or language-specific case folding.

Shared valid/invalid vectors are exported by plugin-api and exercised by tags and list-view tests:

| Input | Label | Key | | --- | --- | --- | | Project\t A  | Project A | project a | | URGENT | URGENT | urgent | | 緊急 | 緊急 | 緊急 | | e\u0301 | é | é | | İ | İ | i\u0307 | | 30 standalone emoji | unchanged | unchanged |

Empty/whitespace-only, 31 code points, newline, CR, NUL, VT, DEL, NEL, zero-width controls, ZWJ, BOM, U+2028 and U+2029 are invalid. q\u0307 counts as two; e\u0301 composes to one. Emoji requiring ZWJ are rejected because Cf is forbidden; standalone emoji are allowed.

Rules, HTTP and errors

Every route requires InteractiveAuthGuard. Authorization calls state-machine's shared instanceAccessAllowed: check read first, then write for additions. Removal uses the tag creator or shared instanceManagementAllowed (submitter/effective instance:admin). WRITE alone cannot remove another person's tag. Archived/frozen/final instances retain these rules. Deleted/unreadable/missing instances have identical INSTANCE_NOT_FOUND/404 responses. No new permissions are needed.

| Method and route | Behavior | | --- | --- | | GET /state-machine-instances/:id/tags | { items } | | POST /state-machine-instances/:id/tags | { tags: string[] }, 1–20 inputs; returns { items } | | DELETE /state-machine-instances/:id/tags/:key | URL-encoded normalized key; { removed: true } | | GET /tags/suggest?q= | { items: [{ label, count }] } for the logged-in user |

Items contain id, instanceId, normalizedKey, label, createdBy, nullable createdByName, createdAt and canRemove. Active names use one findActiveNamesByIds lookup; missing/disabled/older providers yield null. Text is data and must be rendered as text. Never interpret names or labels as HTML/instructions. Existing keys preserve label/creator/time, including duplicate inputs in the same batch. Missing-key removal is idempotent. A batch locks the instance, checks all normalized inputs and capacity once, then writes tags/history/events in one transaction. Any failure rolls the entire batch back.

TAG_INVALID/400 covers input, keyword and normalized-length errors; TAG_LIMIT_EXCEEDED/409 covers capacity. FORBIDDEN/403 means readable but lacking add/remove qualification. INSTANCE_NOT_FOUND/404 always uses Instance not found. List-view additionally returns INVALID_QUERY/400 for query shape or more than 10 raw filters, and TAGS_NOT_AVAILABLE/409 without the optional port.

Suggestions, lists and MCP

Keywords allow empty text, at most 100 code points before and after normalization. The same text rules and locale-independent lowercase apply. PostgreSQL LIKE uses contains matching with explicit escaping for %, _ and backslash. The exported list-view readableInstancePredicates helper applies submitter/readAll/USER/ROLE access and excludes deleted rows before aggregation. Effective roles are expanded once before SQL. Counts include only readable instances; labels take the earliest readable row (createdAt, id). Sort count descending, then normalizedKey with C collation; return at most 20. Archived instances participate. No all-tags load or application-side permission filtering occurs. Contains/empty-keyword aggregation may scan readable rows; the btree does not accelerate arbitrary substrings. Equality filters and instance joins use the composite/instance indexes. See validation evidence for PostgreSQL EXPLAIN; this is not a large-dataset performance guarantee.

List queries accept tags (up to 10) and tagMatch ALL (default) or ANY. ALL emits one EXISTS per distinct key; ANY emits one EXISTS/IN. Existing filters, sort, archive modes, total, paging, saved views and export source/batches retain their rules. Authorized result pages add tags: string[], display labels ordered alphabetically in one batch query, with no per-instance lookup. Imports remain Form-only and never create tags.

Register TagsMcpTools.registerInto(registry, { scopeNamespace }). Only list_instance_tags exists, scope <namespace>:instance:read, requiring a bound user. Labels, normalized keys and names use $userContent. query_instance_list accepts tag filters and wraps result tags in $userContent. There are no add/remove tools or notifications.

Activities, events and configuration

Each actual add/remove appends TAG history with decision.tag { action: ADDED|REMOVED, normalizedKey, label }, actor and time. Collaboration adds TAG to its existing SQL timeline and wraps decision through the existing MCP path. Existing fields/state/revision remain intact. Idempotent retries append nothing. Transaction-bound runtime events tags.added / tags.removed include instanceId, historyId, userId, normalizedKey, label and ISO at; listeners receive the same tx. No outbox or new event bus is introduced; a listener failure rolls back the action, as with lifecycle events.

runtime.tags (also validated by direct module registration) has no secrets:

| Setting | Default / bounds | | --- | --- | | maxTagsPerInstance | 20 / integer 1–1000 | | maxTagLength | 30 / integer 1–1000 code points | | suggestLimit | 20 / integer 1–20 |

Capability flags

The tag list reports canAdd (the caller may write the instance, the same rule the add route enforces) next to the per-tag canRemove.

Unified search (approved decision 202610-a3d8)

Implemented under the approved decision. When the optional SEARCH_PROVIDERS registry is present (manifest optionalRequires haneinfo.search), TagsService registers TagsSearchProvider: the instance's display labels (labelsFor in the same transaction), rewritten on the existing tags.added / tags.removed events. Without @haneinfo/search nothing is registered. The manifest and peer range require plugin API 1.19.0.