trackerctl 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +222 -0
- package/bin/tracker.mjs +20 -0
- package/dist/tracker.js +1665 -0
- package/package.json +44 -0
- package/tracker.config.example.json +21 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 refo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# tracker
|
|
2
|
+
|
|
3
|
+
One CLI for all issue-tracking needs — create, claim, dependencies, hierarchy, search,
|
|
4
|
+
project memory — built for both humans and AI agents. Provider-agnostic core with a
|
|
5
|
+
**GitLab adapter** (self-managed instances supported); GitHub/Jira/Azure DevOps adapters
|
|
6
|
+
can be added behind the same interface without touching the core.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
Requires [Bun](https://bun.sh) ≥ 1.1 (the cache uses `bun:sqlite`). The npm package is
|
|
11
|
+
**`trackerctl`**; the command it installs is `tracker`.
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
bunx trackerctl help # one-off, no install
|
|
15
|
+
npx trackerctl help # also works (re-executes via bun; tells you if bun is missing)
|
|
16
|
+
|
|
17
|
+
bun add -d trackerctl # per project → `bunx tracker <cmd>` / npm scripts
|
|
18
|
+
bun add -g trackerctl # global → `tracker <cmd>`
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
From a checkout of this repo:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
bun install # dev deps only (typescript, biome); zero runtime deps
|
|
25
|
+
bun run tracker help # run from source
|
|
26
|
+
bun run build # bundle to dist/tracker.js (what the npm package ships)
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Configuration
|
|
30
|
+
|
|
31
|
+
Copy `tracker.config.example.json` to `tracker.config.json` and fill in your instance:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
cp tracker.config.example.json tracker.config.json
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`tracker.config.json` is **gitignored** (it carries instance/project identifiers); the
|
|
38
|
+
committed example holds the shape. The token is also never committed (env var or gitignored
|
|
39
|
+
`.env`). Config discovery walks up from the current directory, so any subdirectory of the
|
|
40
|
+
project works.
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"provider": "gitlab",
|
|
45
|
+
"gitlab": {
|
|
46
|
+
"base_url": "https://gitlab.example.com",
|
|
47
|
+
"project": "group/project",
|
|
48
|
+
"token_env": ["TRACKER_GITLAB_TOKEN", "GITLAB_PERSONAL_ACCESS_TOKEN"],
|
|
49
|
+
"native_blocking": true
|
|
50
|
+
},
|
|
51
|
+
"labels": { "in_progress": "status::in-progress" },
|
|
52
|
+
"memory": { "enabled": true, "title": "📌 Project Memory", "label": "meta::memory" },
|
|
53
|
+
"cache": { "path": ".tracker/cache.sqlite", "stale_minutes": 15 }
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
- `gitlab.project` — path (`group/repo`) or numeric id.
|
|
58
|
+
- `gitlab.token_env` — env var names tried in order, first in the environment, then in a
|
|
59
|
+
`.env` next to the config (see `.env.example`). The token needs the `api` scope. It is
|
|
60
|
+
never put in URLs, argv, or error messages.
|
|
61
|
+
- `gitlab.native_blocking` — `true` on Premium (native blocking links). With `false`,
|
|
62
|
+
dependencies are stored as a `Tracker-Blocked-By: #1, #2` trailer line in the blocked
|
|
63
|
+
issue's description — same semantics, works on any tier, zero extra API calls.
|
|
64
|
+
|
|
65
|
+
Verify a setup with `tracker doctor`.
|
|
66
|
+
|
|
67
|
+
## Commands
|
|
68
|
+
|
|
69
|
+
Read commands serve from a local sqlite cache that auto-syncs when older than
|
|
70
|
+
`stale_minutes`; every read command accepts `--json` (stable shapes, below).
|
|
71
|
+
|
|
72
|
+
| Command | Description |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| `tracker sync` | Full cache refresh (issues + hierarchy + dependency links, batched — no per-issue calls) |
|
|
75
|
+
| `tracker ready [--parent <id>]` | Items that are open, unblocked, unassigned, not in-progress |
|
|
76
|
+
| `tracker show <id>` | Full detail, including blocked-by/blocks |
|
|
77
|
+
| `tracker children <id>` | Direct children (work-item hierarchy) |
|
|
78
|
+
| `tracker epic-status <id>` | `closed/total` progress over children |
|
|
79
|
+
| `tracker search [text] [filters]` | Local FTS5 + filters; `--remote` for server-side |
|
|
80
|
+
| `tracker comments <id>` | List an item's comments, oldest first (system notes hidden) |
|
|
81
|
+
| `tracker users <query>` | Resolve usernames/names to user ids (project members) |
|
|
82
|
+
| `tracker whoami` | Authenticated user |
|
|
83
|
+
| `tracker doctor` | Config/token/connectivity/capability checks with fixes |
|
|
84
|
+
| `tracker create -t <title> …` | Create; `--parent` builds hierarchy, `--blocked-by` adds deps |
|
|
85
|
+
| `tracker claim <id>` | Race-safe claim (see protocol below) |
|
|
86
|
+
| `tracker release <id>` | Clear assignee + label, tombstone claim tokens |
|
|
87
|
+
| `tracker close <id> [--reason <text>]` | Close (clears assignee + in-progress label) |
|
|
88
|
+
| `tracker comment <id> <text>` | Post a comment on an item |
|
|
89
|
+
| `tracker dep <id> --blocked-by <o> \| --blocks <o>` | Add a dependency edge |
|
|
90
|
+
| `tracker parent <child> <parent>` | Re-parent an item |
|
|
91
|
+
| `tracker remember <key> <text>` | Store a project memory |
|
|
92
|
+
| `tracker forget <key>` | Hide a memory key |
|
|
93
|
+
| `tracker memories [filter]` | List memories (latest per key wins) |
|
|
94
|
+
|
|
95
|
+
Examples:
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
tracker ready --parent 12 --json
|
|
99
|
+
tracker search --assignee mehmet # filters work with no text query
|
|
100
|
+
tracker search --state closed # state alone is a valid filter
|
|
101
|
+
tracker search "payment timeout" --label backend --state open
|
|
102
|
+
tracker comment 42 "blocked on design review"
|
|
103
|
+
tracker comments 42 --json
|
|
104
|
+
tracker search checkout --remote --json # fresher, server-side
|
|
105
|
+
tracker create -t "Ship login" -d "OAuth" --parent 12 --blocked-by 7,9 -l auth,backend
|
|
106
|
+
tracker claim 42 && do-the-work || echo "someone else got it"
|
|
107
|
+
tracker close 42 --reason "fixed in MR !17"
|
|
108
|
+
tracker remember deploy-cmd "bun run deploy:prod"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Exit codes: **0** success · **2** domain failure (lost claim race, refused claim, not
|
|
112
|
+
found) · **1** usage/config/network error.
|
|
113
|
+
|
|
114
|
+
## The claim protocol
|
|
115
|
+
|
|
116
|
+
Multiple agents can race for the same issue safely with nothing but issue notes:
|
|
117
|
+
|
|
118
|
+
1. Refuse up-front if the issue is closed, assigned, already labeled in-progress, or the memory issue.
|
|
119
|
+
2. Post a claim note: `🔒 tracker-claim agent=<user> token=<ts-rand> at=<iso>`.
|
|
120
|
+
3. Wait a 2-second settle window, then re-read **all** notes.
|
|
121
|
+
4. Drop claims whose token has a release note (`🔓 tracker-release token=…`) and claims older than 5 minutes (crashed claimers expire).
|
|
122
|
+
5. **Oldest live claim wins** — by timestamp, then note id.
|
|
123
|
+
6. Loser posts a release note for its own token and exits `2`. Winner assigns themself and adds the in-progress label.
|
|
124
|
+
|
|
125
|
+
`tracker release` clears assignee + label and posts release marks for every live token, so
|
|
126
|
+
stale claims can never win a later election.
|
|
127
|
+
|
|
128
|
+
## Agent usage (paste into CLAUDE.md)
|
|
129
|
+
|
|
130
|
+
```markdown
|
|
131
|
+
## Issue tracking (tracker CLI)
|
|
132
|
+
|
|
133
|
+
Use `bun run tracker <cmd>` from the repo (or `tracker` if linked). Exit code 2 means a
|
|
134
|
+
domain refusal (e.g. lost a claim race) — pick different work, don't retry.
|
|
135
|
+
|
|
136
|
+
- Find work: `tracker ready --json` (optionally `--parent <epic-id>`)
|
|
137
|
+
- Take work: `tracker claim <id>` — only proceed if exit code is 0
|
|
138
|
+
- Finish work: `tracker close <id> --reason "<what was done>"`
|
|
139
|
+
- Abandon work: `tracker release <id>`
|
|
140
|
+
- Add a task: `tracker create -t "<title>" -d "<details>" [--parent <epic>] [--blocked-by <ids>] --json`
|
|
141
|
+
- Dependencies: `tracker dep <id> --blocked-by <other>`
|
|
142
|
+
- Find an issue: `tracker search "<text>" --json`, or by person with no text:
|
|
143
|
+
`tracker search --assignee <user> --json`
|
|
144
|
+
- Inspect: `tracker show <id> --json`, `tracker children <id> --json`,
|
|
145
|
+
`tracker epic-status <id> --json`, `tracker comments <id> --json`
|
|
146
|
+
- Discuss: `tracker comment <id> "<note for humans or other agents>"`
|
|
147
|
+
- Project memory: `tracker remember <key> "<fact>"`, `tracker memories --json`,
|
|
148
|
+
`tracker forget <key>`
|
|
149
|
+
|
|
150
|
+
Always pass `--json` on read commands and parse stdout; progress notes go to stderr.
|
|
151
|
+
Never edit the `📌 Project Memory` issue or `🔒/🔓` notes by hand.
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## JSON output shapes
|
|
155
|
+
|
|
156
|
+
`WorkItem` (returned by `ready`, `show`, `children`, `search`, `create`):
|
|
157
|
+
|
|
158
|
+
```jsonc
|
|
159
|
+
{
|
|
160
|
+
"id": "42", // string everywhere (Jira-ready)
|
|
161
|
+
"kind": "task", // "epic" | "task"
|
|
162
|
+
"title": "Ship login",
|
|
163
|
+
"state": "open", // "open" | "closed"
|
|
164
|
+
"labels": ["auth"],
|
|
165
|
+
"assignees": [{ "id": "6377", "username": "alice", "name": "…" }],
|
|
166
|
+
"author": { "id": "6377", "username": "alice" }, // or null
|
|
167
|
+
"parent": "12", // or null
|
|
168
|
+
"blockedBy": ["7", "9"], // ids of open OR closed blockers; `ready` checks openness
|
|
169
|
+
"url": "https://gitlab…/issues/42",
|
|
170
|
+
"description": "…",
|
|
171
|
+
"updatedAt": "2026-06-10T17:21:33.000Z"
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Notes: `show --json` adds `"blocks": ["50"]` (reverse edges from the cache). `--remote`
|
|
176
|
+
search results have `parent: null` and only trailer-derived `blockedBy` (the REST search
|
|
177
|
+
endpoint returns neither).
|
|
178
|
+
|
|
179
|
+
Other shapes:
|
|
180
|
+
|
|
181
|
+
```jsonc
|
|
182
|
+
// epic-status { "parent": "12", "total": 5, "open": 2, "closed": 3, "pctClosed": 60 }
|
|
183
|
+
// memories [{ "key": "deploy-cmd", "text": "bun run deploy:prod", "ts": "2026-…" }]
|
|
184
|
+
// comments [{ "id": "991", "body": "…", "author": { "id": "1", "username": "alice" }, "createdAt": "2026-…" }]
|
|
185
|
+
// users/whoami [{ "id": "6377", "username": "alice", "name": "…" }]
|
|
186
|
+
// doctor { "ok": true, "checks": [{ "name": "auth", "status": "ok", "detail": "…", "fix": "…" }] }
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Architecture
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
src/
|
|
193
|
+
model/ canonical types (WorkItem, Comment, User…) — no provider words here
|
|
194
|
+
adapters/
|
|
195
|
+
types.ts TrackerAdapter port + AdapterCapabilities
|
|
196
|
+
gitlab/ the only concrete adapter: fetch client (REST /api/v4 + GraphQL),
|
|
197
|
+
wire↔canonical mapping, batched hierarchy+links sync, trailer fallback
|
|
198
|
+
core/ provider-neutral policies: claim/release, ready, epic-status, memory,
|
|
199
|
+
local search, sync — built ONLY on the port + cache
|
|
200
|
+
cache/ sqlite (bun:sqlite) canonical snapshot + FTS5 index + meta/staleness
|
|
201
|
+
cli/ command parsing, human + --json output, exit codes
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The core never imports provider code. The contract test suite
|
|
205
|
+
(`test/contract/suite.ts`) runs identically against a `FakeAdapter` and the GitLab
|
|
206
|
+
adapter over mocked HTTP — that is the proof a new backend only needs to implement
|
|
207
|
+
`TrackerAdapter` to get `ready`/`claim`/`search`/memory for free.
|
|
208
|
+
|
|
209
|
+
## Development
|
|
210
|
+
|
|
211
|
+
```sh
|
|
212
|
+
bun test # 100 tests: unit, adapter contract ×2, claim races, CLI
|
|
213
|
+
bun run gate # typecheck + lint (biome) + tests
|
|
214
|
+
bun run smoke # LIVE end-to-end against the configured project (creates+closes
|
|
215
|
+
# clearly marked issues) — sandbox projects only
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Publishing
|
|
219
|
+
|
|
220
|
+
`npm publish` runs the full gate and the build automatically (`prepublishOnly`). The
|
|
221
|
+
package ships only `bin/` (Node-safe launcher), `dist/tracker.js` (bundled CLI), the
|
|
222
|
+
example config, README, and LICENSE — no sources, tests, or local config.
|
package/bin/tracker.mjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Launcher: tracker runs on Bun (bun:sqlite). `bunx trackerctl` lands here
|
|
3
|
+
// already inside Bun; `npx trackerctl` lands here inside Node and re-executes
|
|
4
|
+
// via the bun binary, with a friendly error if Bun is not installed.
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
const cli = fileURLToPath(new URL("../dist/tracker.js", import.meta.url));
|
|
9
|
+
const runtime = typeof Bun !== "undefined" ? process.execPath : "bun";
|
|
10
|
+
const result = spawnSync(runtime, [cli, ...process.argv.slice(2)], { stdio: "inherit" });
|
|
11
|
+
|
|
12
|
+
if (result.error && result.error.code === "ENOENT") {
|
|
13
|
+
console.error(
|
|
14
|
+
"tracker requires the Bun runtime, and `bun` was not found on your PATH.\n" +
|
|
15
|
+
"Install it from https://bun.sh (curl -fsSL https://bun.sh/install | bash),\n" +
|
|
16
|
+
"then re-run, or use `bunx trackerctl` directly.",
|
|
17
|
+
);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
process.exit(result.status ?? 1);
|