A local table of the Claude Code sessions you care about, cross-referenced to
Jira tickets. Replaces a notepad full of claude --resume <uuid> lines.
bun run server.ts # http://127.0.0.1:4000Everything configurable comes from the environment so no real value enters git:
export CT_JIRA_BASE="https://your-host.atlassian.net/browse" # clickable tickets
export CT_TICKET_PREFIXES="ABC,XYZ" # defaults to NR
export CT_JIRA_EMAIL="you@example.com" # ticket titles and status
export CT_JIRA_TOKEN="…" # id.atlassian.net API tokenThe credential pair can live in ~/.claude-tracker/credentials.json instead, as
{"jiraEmail": "…", "jiraToken": "…"}, for when the server is started from a
desktop launcher that inherits no shell. chmod 600 it; the tool warns if you
have not. That file is in $HOME, never in the repo.
Without any of this the app still works. Ticket keys render as plain text, or as links with no title, and a banner says which.
- Reads
~/.claude/projectsand never writes to it. You never type a UUID. - Resolves each session's Jira ticket and title from the transcript itself,
preferring the
aiTitleClaude Code already writes. - Tracks the sessions you pick: tickets with per-ticket done state, Jira-style priority, a name, and tags.
- Shows the canonical name Claude Code carries for a session (
@api-worker) under the name you typed, when the session has one. Read from the transcript's lastagent-namerecord, so a session renamed mid-run shows its current name. - Lists the pull requests opened during a session in a second tab, read from the
transcript's own
pr-linkrecords. No GitHub call, no token, nogh. - Fills ticket chips with the Jira summary, status and assignee when credentials are set. Fetched after the page renders, so Jira being slow or down costs nothing: the page is exactly what it was before the feature existed.
- Resume opens a terminal running that session, falling back to the clipboard.
- Search covers every session on disk, not just the tracked ones, so deleting a row loses no history. It matches session ids, titles, ticket keys, and the body text of the transcript; a session that owns a ticket ranks above one that merely mentioned it, and body text ranks below both. Full-text hits show the matching phrase in context, which is how you find the sessions that never carried a ticket at all.
- Only what was said is indexed, not tool output or file dumps. That is 2% of the bytes on disk, and searching the other 98% finds the file rather than the session you were trying to remember.
| File | Job |
|---|---|
scan.ts |
Find sessions, resolve real project paths from cwd |
extract.ts |
Transcript to ticket and name |
annotations.ts |
The working set, atomic writes, serialised mutations |
jira.ts |
Credentials, and the one network call in the tool |
server.ts |
Routes, ticket index, guarded resume |
index.html |
The UI |
prs.html |
The pull requests from one session |
tokens.css |
Colour tokens, shared so the AA pairs cannot drift |
MANUAL-CHECKLIST.md |
The UI test pass |
TODOS.md |
Deferred work, with reasons |
VERSION / version.ts |
Current version, semver parsing and bumping |
release.ts |
Cut a release: bump, changelog, commit, tag |
State lives in ~/.claude-tracker/annotations.json and is safe to hand-edit.
Write what changed under ## [Unreleased] in CHANGELOG.md as you go, then:
bun run release.ts minor --dry # show what would happen
bun run release.ts minor # bump, changelog, commit, tagIt refuses to run on a dirty tree, on failing tests, with an empty Unreleased
section, or when the tag already exists. Each of those would record something
untrue. Semver here versions the annotations.json format and the HTTP surface:
major means a stored file stops loading, minor means new capability with old
files still working, patch means nothing observable changed.
bun test # 210 tests, no dependencies