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

trackerctl

v0.5.0

Published

Multi-backend issue-tracking CLI for humans and AI agents — claim races, dependencies, hierarchy, local FTS search, project memory. GitLab adapter first.

Readme

tracker

One CLI for all issue-tracking needs — create, claim, dependencies, hierarchy, search, project memory — built for both humans and AI agents. Provider-agnostic core with a GitLab adapter (self-managed instances supported); GitHub/Jira/Azure DevOps adapters can be added behind the same interface without touching the core.

Install

Requires Bun ≥ 1.1 (the cache uses bun:sqlite). The npm package is trackerctl; the command it installs is tracker.

bunx trackerctl help            # one-off, no install
npx trackerctl help             # also works (re-executes via bun; tells you if bun is missing)

bun add -d trackerctl           # per project → `bunx tracker <cmd>` / npm scripts
bun add -g trackerctl           # global → `tracker <cmd>`

From a checkout of this repo:

bun install            # dev deps only (typescript, biome); zero runtime deps
bun run tracker help   # run from source
bun run build          # bundle to dist/tracker.js (what the npm package ships)

Configuration

In the project root, scaffold a config:

tracker init --base-url https://gitlab.example.com --project group/project
# or `tracker init` alone, then edit the placeholders

init writes tracker.config.json and — inside a git repository — git-ignores the local-only files (tracker.config.json, .tracker/, .env) so instance and project identifiers can never be committed. The committed tracker.config.example.json holds the same shape if you prefer copying it by hand. The token is also never committed (env var or gitignored .env). Config discovery walks up from the current directory, so any subdirectory of the project works.

{
  "provider": "gitlab",
  "gitlab": {
    "base_url": "https://gitlab.example.com",
    "project": "group/project",
    "token_env": ["TRACKER_GITLAB_TOKEN", "GITLAB_PERSONAL_ACCESS_TOKEN"],
    "native_blocking": true,
    "native_status": false
  },
  "labels": { "in_progress": "status::in-progress" },
  "memory": { "enabled": true, "title": "📌 Project Memory", "label": "meta::memory" },
  "cache": { "path": ".tracker/cache.sqlite", "stale_minutes": 15 }
}
  • gitlab.project — path (group/repo) or numeric id.
  • gitlab.token_env — env var names tried in order, first in the environment, then in a .env next to the config (see .env.example). The token needs the api scope. It is never put in URLs, argv, or error messages.
  • gitlab.native_blockingtrue on Premium (native blocking links). With false, dependencies are stored as a Tracker-Blocked-By: #1, #2 trailer line in the blocked issue's description — same semantics, works on any tier, zero extra API calls.
  • gitlab.native_status — opt-in (Premium/Ultimate with the work-item Status feature): claim also moves the item's native Status to In progress and release back to To do, so GitLab boards/filters reflect agent activity without label columns. Statuses are matched by lifecycle category, so custom-named lifecycles work. Close/reopen need nothing: GitLab itself moves closed items to Done and reopened ones back to To do. The label remains the canonical claim signal either way.

Verify a setup with tracker doctor.

Commands

Read commands serve from a local sqlite cache that auto-syncs when older than stale_minutes; every read command accepts --json (stable shapes, below).

| Command | Description | | --- | --- | | tracker sync | Full cache refresh (issues + hierarchy + dependency links, batched — no per-issue calls) | | tracker ready [--parent <id>] | Items that are open, unblocked, unassigned, not in-progress | | tracker show <id> | Full detail, including blocked-by/blocks | | tracker children <id> | Direct children (work-item hierarchy) | | tracker epic-status <id> | closed/total progress over children | | tracker search [text] [filters] | Local FTS5 + filters; --remote for server-side | | tracker comments <id> | List an item's comments, oldest first (system notes hidden) | | tracker users <query> | Resolve usernames/names to user ids (project members) | | tracker whoami | Authenticated user | | tracker doctor | Config/token/connectivity/capability checks with fixes | | tracker create -t <title> … | Create; --parent builds hierarchy, --blocked-by adds deps | | tracker claim <id> | Race-safe claim (see protocol below) | | tracker release <id> | Clear assignee + label, tombstone claim tokens | | tracker close <id> [--reason <text>] | Close; removes the in-progress label, and clears assignee + tombstones tokens only when a live claim exists (human assignees survive) | | tracker comment <id> <text> | Post a comment on an item | | tracker attach <id> <file...> [-m <msg>] | Upload files, reference them from one comment; prints markdown | | tracker label <id> [--add a,b] [--remove c,d] | Add/remove labels without clobbering the rest | | tracker spend <id> <duration> | Add time spent (1h30m, 2d; -30m subtracts) | | tracker spend <id> --since-claim | Log wall-clock elapsed since the latest 🔒 claim note (claim-to-done timing) | | tracker estimate <id> <duration> | Set the time estimate (0 clears it) | | tracker dep <id> --blocked-by <o> \| --blocks <o> | Add a dependency edge | | tracker parent <child> <parent> | Re-parent an item | | tracker remember <key> <text> | Store a project memory | | tracker forget <key> | Hide a memory key | | tracker memories [filter] | List memories (latest per key wins) | | tracker pr create -t <title> --target <b> … | Open a PR/MR (mr is an alias); -i 42,43 records Closes trailers | | tracker pr status <id> | State + provider-neutral CI signal (none\|pending\|green\|red) | | tracker pr merge <id> [--close-issues] | Merge; --close-issues closes trailer-referenced issues explicitly, with the same claim hygiene as tracker close | | tracker pr comment/comments/close/reopen | Discuss, reject (close -m <reason>), reopen |

Issues and PRs are separate capability ports: provider selects where issues live, merge_provider (defaults to provider) selects where PRs live — so a Jira + GitHub mix needs no core changes, only adapters. Issue closing on merge is always explicit via the issues port: GitLab's Closes #N magic only fires on default-branch targets, and a GitHub PR can never auto-close a Jira issue, so tracker never relies on provider magic.

Examples:

tracker ready --parent 12 --json
tracker search --assignee mehmet              # filters work with no text query
tracker search --state closed                 # state alone is a valid filter
tracker search "payment timeout" --label backend --state open
tracker comment 42 "blocked on design review"
tracker comments 42 --json
tracker attach 42 before.png after.png -m "reference screenshots"
tracker spend 42 1h30m                        # durations: w/d/h/m/s, 1d=8h, 1w=5d
tracker estimate 42 2d
tracker search checkout --remote --json       # fresher, server-side
tracker create -t "Ship login" -d "OAuth" --parent 12 --blocked-by 7,9 -l auth,backend
tracker claim 42 && do-the-work || echo "someone else got it"
tracker close 42 --reason "fixed in MR !17"
tracker remember deploy-cmd "bun run deploy:prod"
tracker pr create -t "Fix login" --target dev -i 42 --json
tracker pr status 5 --json                    # poll for ci green/red
tracker pr merge 5 --close-issues

Exit codes: 0 success · 2 domain failure (lost claim race, refused claim, not found) · 1 usage/config/network error.

The claim protocol

Multiple agents can race for the same issue safely with nothing but issue notes:

  1. Refuse up-front if the issue is closed, assigned, already labeled in-progress, or the memory issue.
  2. Post a claim note: 🔒 tracker-claim agent=<user> token=<ts-rand> at=<iso>.
  3. Wait a 2-second settle window, then re-read all notes.
  4. Drop claims whose token has a release note (🔓 tracker-release token=…) and claims older than 5 minutes (crashed claimers expire).
  5. Oldest live claim wins — by timestamp, then note id.
  6. Loser posts a release note for its own token and exits 2. Winner assigns themself and adds the in-progress label.

tracker release clears assignee + label and posts release marks for every live token, so stale claims can never win a later election.

Agent usage (paste into CLAUDE.md)

## Issue tracking (tracker CLI)

Use `bun run tracker <cmd>` from the repo (or `tracker` if linked). Exit code 2 means a
domain refusal (e.g. lost a claim race) — pick different work, don't retry.

- Find work:        `tracker ready --json` (optionally `--parent <epic-id>`)
- Take work:        `tracker claim <id>`   — only proceed if exit code is 0
- Finish work:      `tracker close <id> --reason "<what was done>"`
- Abandon work:     `tracker release <id>`
- Add a task:       `tracker create -t "<title>" -d "<details>" [--parent <epic>] [--blocked-by <ids>] --json`
- Dependencies:     `tracker dep <id> --blocked-by <other>`
- Find an issue:    `tracker search "<text>" --json`, or by person with no text:
                    `tracker search --assignee <user> --json`
- Inspect:          `tracker show <id> --json`, `tracker children <id> --json`,
                    `tracker epic-status <id> --json`, `tracker comments <id> --json`
- Discuss:          `tracker comment <id> "<note for humans or other agents>"`
- Attach evidence:  `tracker attach <id> <files...> -m "<context>" --json` — uploads
                    screenshots/files and references them from a comment; reuse the
                    returned markdown in descriptions
- Open a PR/MR:     `tracker pr create -t "<title>" --target <branch> -i <issue-ids> --json`
                    (source defaults to the current branch)
- Watch the CI:     `tracker pr status <id> --json` — poll until `.ci` is "green" or "red"
- Land it:          `tracker pr merge <id> --close-issues` — also closes the `-i` issues
                    with a comment explaining why; never rely on provider auto-close

- Track time:       `tracker spend <id> --since-claim` after landing work — logs the
                    elapsed claim-to-done wall clock from server timestamps (or an
                    explicit `tracker spend <id> 1h30m`; durations use 1d=8h, -30m subtracts)
- Project memory:   `tracker remember <key> "<fact>"`, `tracker memories --json`,
                    `tracker forget <key>`

Always pass `--json` on read commands and parse stdout; progress notes go to stderr.
Never edit the `📌 Project Memory` issue or `🔒/🔓` notes by hand.

JSON output shapes

WorkItem (returned by ready, show, children, search, create):

{
  "id": "42",                  // string everywhere (Jira-ready)
  "kind": "task",              // "epic" | "task"
  "title": "Ship login",
  "state": "open",             // "open" | "closed"
  "labels": ["auth"],
  "assignees": [{ "id": "6377", "username": "alice", "name": "…" }],
  "author": { "id": "6377", "username": "alice" },  // or null
  "parent": "12",              // or null
  "blockedBy": ["7", "9"],     // ids of open OR closed blockers; `ready` checks openness
  "url": "https://gitlab…/issues/42",
  "description": "…",
  "updatedAt": "2026-06-10T17:21:33.000Z",
  "timeSpentSeconds": 3600,        // 0 = none recorded
  "timeEstimateSeconds": 57600     // 0 = no estimate
}

Notes: show --json adds "blocks": ["50"] (reverse edges from the cache). --remote search results have parent: null and only trailer-derived blockedBy (the REST search endpoint returns neither).

Other shapes:

// epic-status   { "parent": "12", "total": 5, "open": 2, "closed": 3, "pctClosed": 60 }
// memories      [{ "key": "deploy-cmd", "text": "bun run deploy:prod", "ts": "2026-…" }]
// comments      [{ "id": "991", "body": "…", "author": { "id": "1", "username": "alice" }, "createdAt": "2026-…" }]
// users/whoami  [{ "id": "6377", "username": "alice", "name": "…" }]
// doctor        { "ok": true, "checks": [{ "name": "auth", "status": "ok", "detail": "…", "fix": "…" }] }

Architecture

src/
  model/      canonical types (WorkItem, Comment, User…) — no provider words here
  adapters/
    types.ts  TrackerAdapter port + AdapterCapabilities
    gitlab/   the only concrete adapter: fetch client (REST /api/v4 + GraphQL),
              wire↔canonical mapping, batched hierarchy+links sync, trailer fallback
  core/       provider-neutral policies: claim/release, ready, epic-status, memory,
              local search, sync — built ONLY on the port + cache
  cache/      sqlite (bun:sqlite) canonical snapshot + FTS5 index + meta/staleness
  cli/        command parsing, human + --json output, exit codes

The core never imports provider code. The contract test suite (test/contract/suite.ts) runs identically against a FakeAdapter and the GitLab adapter over mocked HTTP — that is the proof a new backend only needs to implement TrackerAdapter to get ready/claim/search/memory for free.

Development

bun test           # 100 tests: unit, adapter contract ×2, claim races, CLI
bun run gate       # typecheck + lint (biome) + tests
bun run smoke      # LIVE end-to-end against the configured project (creates+closes
                   # clearly marked issues) — sandbox projects only

Publishing

npm publish runs the full gate and the build automatically (prepublishOnly). The package ships only bin/ (Node-safe launcher), dist/tracker.js (bundled CLI), the example config, README, and LICENSE — no sources, tests, or local config.