claude-task-tracker 1.0.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/README.md ADDED
@@ -0,0 +1,151 @@
1
+ # Claude Task Tracker (ctt)
2
+
3
+ A CLI task tracker with dependency management designed to extend memory for agentic code workflows like Claude Code.
4
+
5
+ ## Features
6
+
7
+ - **Task Dependencies**: Blocking dependencies that prevent task execution until completed
8
+ - **Soft References**: Non-blocking references between related tasks
9
+ - **Task Overviews**: Context summaries filled on completion for dependent tasks to reference
10
+ - **Ready Task Detection**: `next` command finds tasks ready to work on (open + all blockers completed)
11
+ - **Multiple Output Formats**: Default, JSON, and compact (token-efficient for LLMs)
12
+ - **Claude Code Integration**: Auto-generates `CLAUDE.md` with usage instructions on init
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ # Clone and install
18
+ cd claude-task-tracker
19
+ npm install
20
+ npm run build
21
+
22
+ # Link globally to use 'ctt' anywhere
23
+ npm link
24
+ ```
25
+
26
+ ## Quick Start
27
+
28
+ ```bash
29
+ # Initialize in your project (creates .tasks/tasks.json and CLAUDE.md)
30
+ ctt init
31
+
32
+ # Add tasks with dependencies
33
+ ctt add "Design API schema" -d "Define REST endpoints"
34
+ ctt add "Implement auth" --blocked-by t1
35
+ ctt add "Write tests" --blocked-by t2 --ref t1
36
+
37
+ # Get next ready task
38
+ ctt next
39
+ # Output: t1 - Design API schema
40
+
41
+ # Start working
42
+ ctt start t1
43
+
44
+ # Complete with overview (context for dependent tasks)
45
+ ctt complete t1 -o "Created OpenAPI spec with /users, /auth endpoints. JWT-based auth."
46
+
47
+ # Get context for dependent task (includes blocker overviews)
48
+ ctt context t2
49
+ ```
50
+
51
+ ## Commands
52
+
53
+ | Command | Description |
54
+ |---------|-------------|
55
+ | `ctt init` | Initialize `.tasks/tasks.json` in current directory |
56
+ | `ctt add <title>` | Add task (`-d` description, `-b` blockedBy, `-r` refs) |
57
+ | `ctt list` | List all tasks (`-s` filter by status) |
58
+ | `ctt show <id>` | Show task details |
59
+ | `ctt next` | Get next ready task (open + all blockers completed) |
60
+ | `ctt start <id>` | Mark as in progress |
61
+ | `ctt complete <id>` | Mark as completed (`-o` overview) |
62
+ | `ctt block <id> --by <id>` | Add blocking dependency |
63
+ | `ctt unblock <id> --by <id>` | Remove blocking dependency |
64
+ | `ctt ref <id> --to <id>` | Add soft reference |
65
+ | `ctt unref <id> --to <id>` | Remove soft reference |
66
+ | `ctt context <id>` | Show task + all dependency overviews |
67
+ | `ctt overview <id> <text>` | Update task overview |
68
+ | `ctt delete <id>` | Delete a task |
69
+ | `ctt cleanup` | Remove old completed tasks (`-d` days, default 7, `--dry-run`) |
70
+
71
+ ## Output Formats
72
+
73
+ ### Default (Human-readable)
74
+ ```bash
75
+ ctt list
76
+ # ○ t1 - Design API
77
+ # ○ t2 - Implement auth [blocked by: t1]
78
+ # ○ t3 - Write tests [blocked by: t2]
79
+ ```
80
+
81
+ ### JSON (Structured)
82
+ ```bash
83
+ ctt --json next
84
+ ```
85
+ ```json
86
+ {
87
+ "id": "t1",
88
+ "title": "Design API",
89
+ "status": "open",
90
+ "blockedBy": [],
91
+ "references": [],
92
+ "overview": null
93
+ }
94
+ ```
95
+
96
+ ### Compact (Token-efficient for LLMs)
97
+ ```bash
98
+ ctt --compact list
99
+ # t1:Design API[O]
100
+ # t2:Implement auth[O] <t1
101
+ # t3:Write tests[O] <t2 ~t1
102
+
103
+ ctt --compact context t3
104
+ # t3:Write tests[O] <t2 ~t1
105
+ # --blockers--
106
+ # t2:Implement auth[O] <t1
107
+ # --refs--
108
+ # t1:Design API[C] >Created OpenAPI spec...
109
+ ```
110
+
111
+ **Compact format legend:**
112
+ - `[O]` = open, `[P]` = progress, `[C]` = completed
113
+ - `<t1,t2` = blocked by t1 and t2
114
+ - `~t1` = references t1
115
+ - `>text` = overview
116
+
117
+ ## Task States
118
+
119
+ | Status | Symbol | Description |
120
+ |--------|--------|-------------|
121
+ | `open` | ○ / O | Not started |
122
+ | `progress` | ◐ / P | In progress |
123
+ | `completed` | ● / C | Done |
124
+
125
+ ## Agentic Workflow Example
126
+
127
+ ```bash
128
+ # Agent picks up next task
129
+ TASK=$(ctt --compact next)
130
+ # t1:Design API[O]
131
+
132
+ # Agent starts working
133
+ ctt start t1
134
+
135
+ # Agent completes with context for future tasks
136
+ ctt complete t1 -o "Defined /users CRUD, /auth/login, /auth/refresh. Using JWT with 1h expiry."
137
+
138
+ # Agent picks up next task and gets context
139
+ ctt --compact context t2
140
+ # t2:Implement auth[O] <t1
141
+ # --blockers--
142
+ # t1:Design API[C] >Defined /users CRUD, /auth/login...
143
+ ```
144
+
145
+ ## Storage
146
+
147
+ Tasks are stored in `.tasks/tasks.json` in the current directory. This file can be version-controlled.
148
+
149
+ ## License
150
+
151
+ MIT
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/index.js ADDED
@@ -0,0 +1,738 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, appendFileSync } from "fs";
5
+ import { join as join2 } from "path";
6
+ import { Command } from "commander";
7
+
8
+ // src/store/taskStore.ts
9
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
10
+ import { dirname, join } from "path";
11
+
12
+ // src/models/task.ts
13
+ function createTask(id, title, options = {}) {
14
+ const now = (/* @__PURE__ */ new Date()).toISOString();
15
+ return {
16
+ id,
17
+ title,
18
+ description: options.description,
19
+ status: "open",
20
+ blockedBy: options.blockedBy ?? [],
21
+ references: options.references ?? [],
22
+ overview: void 0,
23
+ createdAt: now,
24
+ updatedAt: now
25
+ };
26
+ }
27
+ function createEmptyStore() {
28
+ return {
29
+ version: "1.0.0",
30
+ nextId: 1,
31
+ tasks: []
32
+ };
33
+ }
34
+
35
+ // src/store/taskStore.ts
36
+ var TASKS_DIR = ".tasks";
37
+ var TASKS_FILE = "tasks.json";
38
+ function getTasksPath(cwd = process.cwd()) {
39
+ return join(cwd, TASKS_DIR, TASKS_FILE);
40
+ }
41
+ function loadStore(cwd = process.cwd()) {
42
+ const path = getTasksPath(cwd);
43
+ if (!existsSync(path)) {
44
+ throw new Error('Task store not initialized. Run "ctt init" first.');
45
+ }
46
+ const data = readFileSync(path, "utf-8");
47
+ return JSON.parse(data);
48
+ }
49
+ function saveStore(store, cwd = process.cwd()) {
50
+ const path = getTasksPath(cwd);
51
+ const dir = dirname(path);
52
+ if (!existsSync(dir)) {
53
+ mkdirSync(dir, { recursive: true });
54
+ }
55
+ writeFileSync(path, JSON.stringify(store, null, 2));
56
+ }
57
+ function initStore(cwd = process.cwd()) {
58
+ const path = getTasksPath(cwd);
59
+ if (existsSync(path)) {
60
+ throw new Error("Task store already exists.");
61
+ }
62
+ saveStore(createEmptyStore(), cwd);
63
+ }
64
+ function generateId(store) {
65
+ return `t${store.nextId}`;
66
+ }
67
+ function addTask(store, title, options = {}) {
68
+ const id = generateId(store);
69
+ const task = createTask(id, title, options);
70
+ store.tasks.push(task);
71
+ store.nextId++;
72
+ return task;
73
+ }
74
+ function getTask(store, id) {
75
+ return store.tasks.find((t) => t.id === id);
76
+ }
77
+ function updateTask(store, id, updates) {
78
+ const task = getTask(store, id);
79
+ if (!task) {
80
+ throw new Error(`Task ${id} not found.`);
81
+ }
82
+ Object.assign(task, updates, { updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
83
+ return task;
84
+ }
85
+ function isTaskReady(store, task) {
86
+ if (task.status !== "open") {
87
+ return false;
88
+ }
89
+ for (const blockerId of task.blockedBy) {
90
+ const blocker = getTask(store, blockerId);
91
+ if (!blocker || blocker.status !== "completed") {
92
+ return false;
93
+ }
94
+ }
95
+ return true;
96
+ }
97
+ function getNextTask(store) {
98
+ return store.tasks.find((task) => isTaskReady(store, task));
99
+ }
100
+ function getBlockingTasks(store, task) {
101
+ return task.blockedBy.map((id) => getTask(store, id)).filter((t) => t !== void 0);
102
+ }
103
+ function getReferencedTasks(store, task) {
104
+ return task.references.map((id) => getTask(store, id)).filter((t) => t !== void 0);
105
+ }
106
+
107
+ // src/utils/display.ts
108
+ var STATUS_ICONS = {
109
+ open: "\u25CB",
110
+ progress: "\u25D0",
111
+ completed: "\u25CF"
112
+ };
113
+ function formatStatus(status) {
114
+ return `${STATUS_ICONS[status] || "?"} ${status}`;
115
+ }
116
+ function formatTaskShort(task) {
117
+ const icon = STATUS_ICONS[task.status] || "?";
118
+ const blockers = task.blockedBy.length > 0 ? ` [blocked by: ${task.blockedBy.join(", ")}]` : "";
119
+ return `${icon} ${task.id} - ${task.title}${blockers}`;
120
+ }
121
+ function formatTaskDetail(task, store) {
122
+ const lines = [];
123
+ lines.push(`ID: ${task.id}`);
124
+ lines.push(`Title: ${task.title}`);
125
+ lines.push(`Status: ${formatStatus(task.status)}`);
126
+ if (task.description) {
127
+ lines.push(`Description: ${task.description}`);
128
+ }
129
+ if (task.blockedBy.length > 0) {
130
+ lines.push(`Blocked by: ${task.blockedBy.join(", ")}`);
131
+ }
132
+ if (task.references.length > 0) {
133
+ lines.push(`References: ${task.references.join(", ")}`);
134
+ }
135
+ if (task.overview) {
136
+ lines.push(`Overview: ${task.overview}`);
137
+ }
138
+ lines.push(`Created: ${task.createdAt}`);
139
+ lines.push(`Updated: ${task.updatedAt}`);
140
+ return lines.join("\n");
141
+ }
142
+ function formatContext(task, store) {
143
+ const lines = [];
144
+ lines.push("=== Current Task ===");
145
+ lines.push(formatTaskDetail(task, store));
146
+ const blockers = getBlockingTasks(store, task);
147
+ if (blockers.length > 0) {
148
+ lines.push("\n=== Blocking Dependencies ===");
149
+ for (const blocker of blockers) {
150
+ lines.push(`
151
+ [${blocker.id}] ${blocker.title}`);
152
+ lines.push(`Status: ${formatStatus(blocker.status)}`);
153
+ if (blocker.overview) {
154
+ lines.push(`Overview: ${blocker.overview}`);
155
+ }
156
+ }
157
+ }
158
+ const refs = getReferencedTasks(store, task);
159
+ if (refs.length > 0) {
160
+ lines.push("\n=== Referenced Tasks ===");
161
+ for (const ref of refs) {
162
+ lines.push(`
163
+ [${ref.id}] ${ref.title}`);
164
+ lines.push(`Status: ${formatStatus(ref.status)}`);
165
+ if (ref.overview) {
166
+ lines.push(`Overview: ${ref.overview}`);
167
+ }
168
+ }
169
+ }
170
+ return lines.join("\n");
171
+ }
172
+ function formatTaskJson(task) {
173
+ return JSON.stringify(task, null, 2);
174
+ }
175
+ function formatContextJson(task, store) {
176
+ const blockers = getBlockingTasks(store, task);
177
+ const refs = getReferencedTasks(store, task);
178
+ return JSON.stringify({
179
+ task,
180
+ blockingDependencies: blockers,
181
+ references: refs
182
+ }, null, 2);
183
+ }
184
+ var STATUS_SHORT = {
185
+ open: "O",
186
+ progress: "P",
187
+ completed: "C"
188
+ };
189
+ function formatTaskCompact(task) {
190
+ const parts = [`${task.id}:${task.title}[${STATUS_SHORT[task.status] || "?"}]`];
191
+ if (task.blockedBy.length > 0) {
192
+ parts.push(`<${task.blockedBy.join(",")}`);
193
+ }
194
+ if (task.references.length > 0) {
195
+ parts.push(`~${task.references.join(",")}`);
196
+ }
197
+ if (task.overview) {
198
+ parts.push(`>${task.overview}`);
199
+ }
200
+ return parts.join(" ");
201
+ }
202
+ function formatTasksCompact(tasks) {
203
+ return tasks.map(formatTaskCompact).join("\n");
204
+ }
205
+ function formatContextCompact(task, store) {
206
+ const lines = [];
207
+ lines.push(formatTaskCompact(task));
208
+ if (task.description) {
209
+ lines.push(`desc:${task.description}`);
210
+ }
211
+ const blockers = getBlockingTasks(store, task);
212
+ if (blockers.length > 0) {
213
+ lines.push("--blockers--");
214
+ for (const b of blockers) {
215
+ lines.push(formatTaskCompact(b));
216
+ }
217
+ }
218
+ const refs = getReferencedTasks(store, task);
219
+ if (refs.length > 0) {
220
+ lines.push("--refs--");
221
+ for (const r of refs) {
222
+ lines.push(formatTaskCompact(r));
223
+ }
224
+ }
225
+ return lines.join("\n");
226
+ }
227
+
228
+ // src/index.ts
229
+ var program = new Command();
230
+ program.name("ctt").description("CLI task tracker with dependency management for agentic code workflows").version("1.0.0");
231
+ program.option("--json", "Output in JSON format");
232
+ program.option("--compact", "Output in compact format (token-efficient for LLMs)");
233
+ function getOutputFormat() {
234
+ const opts = program.opts();
235
+ if (opts.compact) return "compact";
236
+ if (opts.json) return "json";
237
+ return "default";
238
+ }
239
+ var CTT_CLAUDE_INSTRUCTIONS = `
240
+ ## Task Tracking with ctt
241
+
242
+ This project uses \`ctt\` (Claude Task Tracker) for task management with dependencies.
243
+
244
+ ### Workflow
245
+ 1. Get the next ready task: \`ctt --compact next\`
246
+ 2. Start working on it: \`ctt start <id>\`
247
+ 3. When done, complete with overview: \`ctt complete <id> -o "summary of what was done"\`
248
+ 4. Get context for a task (includes dependency overviews): \`ctt --compact context <id>\`
249
+
250
+ ### Key Commands
251
+ - \`ctt --compact list\` - List all tasks
252
+ - \`ctt --compact next\` - Get next ready task (open + all blockers completed)
253
+ - \`ctt --compact context <id>\` - Get task with dependency overviews
254
+ - \`ctt add "title" -b <blocker-id>\` - Add task blocked by another
255
+ - \`ctt complete <id> -o "overview"\` - Complete with context for dependent tasks
256
+
257
+ ### Compact Format
258
+ - \`[O]\`/\`[P]\`/\`[C]\` = open/progress/completed
259
+ - \`<t1,t2\` = blocked by tasks
260
+ - \`~t1\` = references task
261
+ - \`>text\` = overview
262
+ `;
263
+ function setupClaudeMd(cwd = process.cwd()) {
264
+ const claudeMdPath = join2(cwd, "CLAUDE.md");
265
+ const marker = "## Task Tracking with ctt";
266
+ if (!existsSync2(claudeMdPath)) {
267
+ writeFileSync2(claudeMdPath, CTT_CLAUDE_INSTRUCTIONS.trim() + "\n");
268
+ return { created: true, appended: false };
269
+ }
270
+ const content = readFileSync2(claudeMdPath, "utf-8");
271
+ if (content.includes(marker)) {
272
+ return { created: false, appended: false };
273
+ }
274
+ appendFileSync(claudeMdPath, "\n" + CTT_CLAUDE_INSTRUCTIONS);
275
+ return { created: false, appended: true };
276
+ }
277
+ program.command("init").description("Initialize task store and setup CLAUDE.md").action(() => {
278
+ try {
279
+ initStore();
280
+ console.log("Initialized task store in .tasks/tasks.json");
281
+ const { created, appended } = setupClaudeMd();
282
+ if (created) {
283
+ console.log("Created CLAUDE.md with ctt instructions");
284
+ } else if (appended) {
285
+ console.log("Appended ctt instructions to CLAUDE.md");
286
+ } else {
287
+ console.log("CLAUDE.md already has ctt instructions");
288
+ }
289
+ } catch (err) {
290
+ console.error(err.message);
291
+ process.exit(1);
292
+ }
293
+ });
294
+ program.command("add <title>").description("Add a new task").option("-d, --description <desc>", "Task description").option("-b, --blocked-by <ids>", "Comma-separated IDs of blocking tasks").option("-r, --ref <ids>", "Comma-separated IDs of referenced tasks").action((title, options) => {
295
+ try {
296
+ const store = loadStore();
297
+ const blockedBy = options.blockedBy ? options.blockedBy.split(",").map((s) => s.trim()) : [];
298
+ const references = options.ref ? options.ref.split(",").map((s) => s.trim()) : [];
299
+ for (const id of [...blockedBy, ...references]) {
300
+ if (!getTask(store, id)) {
301
+ console.error(`Task ${id} not found.`);
302
+ process.exit(1);
303
+ }
304
+ }
305
+ const task = addTask(store, title, {
306
+ description: options.description,
307
+ blockedBy,
308
+ references
309
+ });
310
+ saveStore(store);
311
+ const fmt = getOutputFormat();
312
+ if (fmt === "json") {
313
+ console.log(formatTaskJson(task));
314
+ } else if (fmt === "compact") {
315
+ console.log(formatTaskCompact(task));
316
+ } else {
317
+ console.log(`Created task: ${formatTaskShort(task)}`);
318
+ }
319
+ } catch (err) {
320
+ console.error(err.message);
321
+ process.exit(1);
322
+ }
323
+ });
324
+ program.command("list").description("List all tasks").option("-s, --status <status>", "Filter by status (open, progress, completed)").action((options) => {
325
+ try {
326
+ const store = loadStore();
327
+ let tasks = store.tasks;
328
+ if (options.status) {
329
+ tasks = tasks.filter((t) => t.status === options.status);
330
+ }
331
+ const fmt = getOutputFormat();
332
+ if (fmt === "json") {
333
+ console.log(JSON.stringify(tasks, null, 2));
334
+ } else if (fmt === "compact") {
335
+ if (tasks.length === 0) {
336
+ console.log("none");
337
+ } else {
338
+ console.log(formatTasksCompact(tasks));
339
+ }
340
+ } else {
341
+ if (tasks.length === 0) {
342
+ console.log("No tasks found.");
343
+ } else {
344
+ for (const task of tasks) {
345
+ console.log(formatTaskShort(task));
346
+ }
347
+ }
348
+ }
349
+ } catch (err) {
350
+ console.error(err.message);
351
+ process.exit(1);
352
+ }
353
+ });
354
+ program.command("show <id>").description("Show task details").action((id) => {
355
+ try {
356
+ const store = loadStore();
357
+ const task = getTask(store, id);
358
+ if (!task) {
359
+ console.error(`Task ${id} not found.`);
360
+ process.exit(1);
361
+ }
362
+ const fmt = getOutputFormat();
363
+ if (fmt === "json") {
364
+ console.log(formatTaskJson(task));
365
+ } else if (fmt === "compact") {
366
+ console.log(formatTaskCompact(task));
367
+ } else {
368
+ console.log(formatTaskDetail(task, store));
369
+ }
370
+ } catch (err) {
371
+ console.error(err.message);
372
+ process.exit(1);
373
+ }
374
+ });
375
+ program.command("next").description("Get the next ready task (open with all blockers completed)").action(() => {
376
+ try {
377
+ const store = loadStore();
378
+ const task = getNextTask(store);
379
+ const fmt = getOutputFormat();
380
+ if (!task) {
381
+ if (fmt === "json") {
382
+ console.log(JSON.stringify(null));
383
+ } else if (fmt === "compact") {
384
+ console.log("none");
385
+ } else {
386
+ console.log("No ready tasks available.");
387
+ }
388
+ return;
389
+ }
390
+ if (fmt === "json") {
391
+ console.log(formatTaskJson(task));
392
+ } else if (fmt === "compact") {
393
+ console.log(formatTaskCompact(task));
394
+ } else {
395
+ console.log(formatTaskShort(task));
396
+ }
397
+ } catch (err) {
398
+ console.error(err.message);
399
+ process.exit(1);
400
+ }
401
+ });
402
+ program.command("start <id>").description("Mark a task as in progress").action((id) => {
403
+ try {
404
+ const store = loadStore();
405
+ const task = getTask(store, id);
406
+ if (!task) {
407
+ console.error(`Task ${id} not found.`);
408
+ process.exit(1);
409
+ }
410
+ if (task.status === "completed") {
411
+ console.error(`Task ${id} is already completed.`);
412
+ process.exit(1);
413
+ }
414
+ updateTask(store, id, { status: "progress" });
415
+ saveStore(store);
416
+ const fmt = getOutputFormat();
417
+ const updated = getTask(store, id);
418
+ if (fmt === "json") {
419
+ console.log(formatTaskJson(updated));
420
+ } else if (fmt === "compact") {
421
+ console.log(formatTaskCompact(updated));
422
+ } else {
423
+ console.log(`Started: ${formatTaskShort(updated)}`);
424
+ }
425
+ } catch (err) {
426
+ console.error(err.message);
427
+ process.exit(1);
428
+ }
429
+ });
430
+ program.command("complete <id>").description("Mark a task as completed").option("-o, --overview <text>", "Overview/summary of what was accomplished").action((id, options) => {
431
+ try {
432
+ const store = loadStore();
433
+ const task = getTask(store, id);
434
+ if (!task) {
435
+ console.error(`Task ${id} not found.`);
436
+ process.exit(1);
437
+ }
438
+ if (task.status === "completed") {
439
+ console.error(`Task ${id} is already completed.`);
440
+ process.exit(1);
441
+ }
442
+ const updates = { status: "completed" };
443
+ if (options.overview) {
444
+ updates.overview = options.overview;
445
+ }
446
+ updateTask(store, id, updates);
447
+ saveStore(store);
448
+ const fmt = getOutputFormat();
449
+ const updated = getTask(store, id);
450
+ if (fmt === "json") {
451
+ console.log(formatTaskJson(updated));
452
+ } else if (fmt === "compact") {
453
+ console.log(formatTaskCompact(updated));
454
+ } else {
455
+ console.log(`Completed: ${formatTaskShort(updated)}`);
456
+ if (!options.overview) {
457
+ console.log("Tip: Use -o to add an overview for dependent tasks.");
458
+ }
459
+ }
460
+ } catch (err) {
461
+ console.error(err.message);
462
+ process.exit(1);
463
+ }
464
+ });
465
+ program.command("block <id>").description("Add a blocking dependency").requiredOption("--by <blocker-id>", "ID of the blocking task").action((id, options) => {
466
+ try {
467
+ const store = loadStore();
468
+ const task = getTask(store, id);
469
+ if (!task) {
470
+ console.error(`Task ${id} not found.`);
471
+ process.exit(1);
472
+ }
473
+ const blocker = getTask(store, options.by);
474
+ if (!blocker) {
475
+ console.error(`Blocker task ${options.by} not found.`);
476
+ process.exit(1);
477
+ }
478
+ if (task.blockedBy.includes(options.by)) {
479
+ console.error(`Task ${id} is already blocked by ${options.by}.`);
480
+ process.exit(1);
481
+ }
482
+ if (blocker.blockedBy.includes(id)) {
483
+ console.error(`Circular dependency: ${options.by} is already blocked by ${id}.`);
484
+ process.exit(1);
485
+ }
486
+ task.blockedBy.push(options.by);
487
+ updateTask(store, id, { blockedBy: task.blockedBy });
488
+ saveStore(store);
489
+ if (program.opts().json) {
490
+ console.log(formatTaskJson(getTask(store, id)));
491
+ } else {
492
+ console.log(`Added blocker: ${id} is now blocked by ${options.by}`);
493
+ }
494
+ } catch (err) {
495
+ console.error(err.message);
496
+ process.exit(1);
497
+ }
498
+ });
499
+ program.command("unblock <id>").description("Remove a blocking dependency").requiredOption("--by <blocker-id>", "ID of the blocking task to remove").action((id, options) => {
500
+ try {
501
+ const store = loadStore();
502
+ const task = getTask(store, id);
503
+ if (!task) {
504
+ console.error(`Task ${id} not found.`);
505
+ process.exit(1);
506
+ }
507
+ const idx = task.blockedBy.indexOf(options.by);
508
+ if (idx === -1) {
509
+ console.error(`Task ${id} is not blocked by ${options.by}.`);
510
+ process.exit(1);
511
+ }
512
+ task.blockedBy.splice(idx, 1);
513
+ updateTask(store, id, { blockedBy: task.blockedBy });
514
+ saveStore(store);
515
+ if (program.opts().json) {
516
+ console.log(formatTaskJson(getTask(store, id)));
517
+ } else {
518
+ console.log(`Removed blocker: ${id} is no longer blocked by ${options.by}`);
519
+ }
520
+ } catch (err) {
521
+ console.error(err.message);
522
+ process.exit(1);
523
+ }
524
+ });
525
+ program.command("ref <id>").description("Add a soft reference to another task").requiredOption("--to <ref-id>", "ID of the task to reference").action((id, options) => {
526
+ try {
527
+ const store = loadStore();
528
+ const task = getTask(store, id);
529
+ if (!task) {
530
+ console.error(`Task ${id} not found.`);
531
+ process.exit(1);
532
+ }
533
+ const refTask = getTask(store, options.to);
534
+ if (!refTask) {
535
+ console.error(`Reference task ${options.to} not found.`);
536
+ process.exit(1);
537
+ }
538
+ if (task.references.includes(options.to)) {
539
+ console.error(`Task ${id} already references ${options.to}.`);
540
+ process.exit(1);
541
+ }
542
+ task.references.push(options.to);
543
+ updateTask(store, id, { references: task.references });
544
+ saveStore(store);
545
+ if (program.opts().json) {
546
+ console.log(formatTaskJson(getTask(store, id)));
547
+ } else {
548
+ console.log(`Added reference: ${id} now references ${options.to}`);
549
+ }
550
+ } catch (err) {
551
+ console.error(err.message);
552
+ process.exit(1);
553
+ }
554
+ });
555
+ program.command("unref <id>").description("Remove a soft reference").requiredOption("--to <ref-id>", "ID of the reference to remove").action((id, options) => {
556
+ try {
557
+ const store = loadStore();
558
+ const task = getTask(store, id);
559
+ if (!task) {
560
+ console.error(`Task ${id} not found.`);
561
+ process.exit(1);
562
+ }
563
+ const idx = task.references.indexOf(options.to);
564
+ if (idx === -1) {
565
+ console.error(`Task ${id} does not reference ${options.to}.`);
566
+ process.exit(1);
567
+ }
568
+ task.references.splice(idx, 1);
569
+ updateTask(store, id, { references: task.references });
570
+ saveStore(store);
571
+ if (program.opts().json) {
572
+ console.log(formatTaskJson(getTask(store, id)));
573
+ } else {
574
+ console.log(`Removed reference: ${id} no longer references ${options.to}`);
575
+ }
576
+ } catch (err) {
577
+ console.error(err.message);
578
+ process.exit(1);
579
+ }
580
+ });
581
+ program.command("overview <id> <text>").description("Set or update task overview").action((id, text) => {
582
+ try {
583
+ const store = loadStore();
584
+ const task = getTask(store, id);
585
+ if (!task) {
586
+ console.error(`Task ${id} not found.`);
587
+ process.exit(1);
588
+ }
589
+ updateTask(store, id, { overview: text });
590
+ saveStore(store);
591
+ if (program.opts().json) {
592
+ console.log(formatTaskJson(getTask(store, id)));
593
+ } else {
594
+ console.log(`Updated overview for ${id}`);
595
+ }
596
+ } catch (err) {
597
+ console.error(err.message);
598
+ process.exit(1);
599
+ }
600
+ });
601
+ program.command("context <id>").description("Show task with all dependency overviews for quick context lookup").action((id) => {
602
+ try {
603
+ const store = loadStore();
604
+ const task = getTask(store, id);
605
+ if (!task) {
606
+ console.error(`Task ${id} not found.`);
607
+ process.exit(1);
608
+ }
609
+ const fmt = getOutputFormat();
610
+ if (fmt === "json") {
611
+ console.log(formatContextJson(task, store));
612
+ } else if (fmt === "compact") {
613
+ console.log(formatContextCompact(task, store));
614
+ } else {
615
+ console.log(formatContext(task, store));
616
+ }
617
+ } catch (err) {
618
+ console.error(err.message);
619
+ process.exit(1);
620
+ }
621
+ });
622
+ program.command("delete <id>").description("Delete a task").action((id) => {
623
+ try {
624
+ const store = loadStore();
625
+ const task = getTask(store, id);
626
+ if (!task) {
627
+ console.error(`Task ${id} not found.`);
628
+ process.exit(1);
629
+ }
630
+ const dependents = store.tasks.filter(
631
+ (t) => t.blockedBy.includes(id) || t.references.includes(id)
632
+ );
633
+ if (dependents.length > 0) {
634
+ console.error(
635
+ `Cannot delete ${id}: it is referenced by ${dependents.map((t) => t.id).join(", ")}`
636
+ );
637
+ process.exit(1);
638
+ }
639
+ store.tasks = store.tasks.filter((t) => t.id !== id);
640
+ saveStore(store);
641
+ if (program.opts().json) {
642
+ console.log(JSON.stringify({ deleted: id }));
643
+ } else {
644
+ console.log(`Deleted task ${id}`);
645
+ }
646
+ } catch (err) {
647
+ console.error(err.message);
648
+ process.exit(1);
649
+ }
650
+ });
651
+ program.command("cleanup").description("Remove completed tasks older than specified days").option("-d, --days <days>", "Minimum age in days for completed tasks to be removed", "7").option("--dry-run", "Show what would be removed without actually removing").action((options) => {
652
+ try {
653
+ const store = loadStore();
654
+ const days = parseInt(options.days, 10);
655
+ if (isNaN(days) || days < 0) {
656
+ console.error("Invalid days value. Must be a non-negative number.");
657
+ process.exit(1);
658
+ }
659
+ const cutoffDate = /* @__PURE__ */ new Date();
660
+ cutoffDate.setDate(cutoffDate.getDate() - days);
661
+ const candidates = store.tasks.filter((t) => {
662
+ if (t.status !== "completed") return false;
663
+ const updatedAt = new Date(t.updatedAt);
664
+ return updatedAt < cutoffDate;
665
+ });
666
+ const toRemove = [];
667
+ const skipped = [];
668
+ for (const task of candidates) {
669
+ const dependents = store.tasks.filter(
670
+ (t) => t.status !== "completed" && (t.blockedBy.includes(task.id) || t.references.includes(task.id))
671
+ );
672
+ if (dependents.length > 0) {
673
+ skipped.push({
674
+ task,
675
+ reason: `referenced by: ${dependents.map((d) => d.id).join(", ")}`
676
+ });
677
+ } else {
678
+ toRemove.push(task);
679
+ }
680
+ }
681
+ const fmt = getOutputFormat();
682
+ if (options.dryRun) {
683
+ if (fmt === "json") {
684
+ console.log(JSON.stringify({ wouldRemove: toRemove, skipped }, null, 2));
685
+ } else if (fmt === "compact") {
686
+ if (toRemove.length === 0) {
687
+ console.log("none to remove");
688
+ } else {
689
+ console.log("would remove:");
690
+ console.log(formatTasksCompact(toRemove));
691
+ }
692
+ if (skipped.length > 0) {
693
+ console.log("skipped:");
694
+ for (const { task, reason } of skipped) {
695
+ console.log(`${task.id}:${task.title} (${reason})`);
696
+ }
697
+ }
698
+ } else {
699
+ if (toRemove.length === 0) {
700
+ console.log("No tasks to remove.");
701
+ } else {
702
+ console.log("Would remove:");
703
+ for (const task of toRemove) {
704
+ console.log(` ${formatTaskShort(task)}`);
705
+ }
706
+ }
707
+ if (skipped.length > 0) {
708
+ console.log("\nSkipped (still referenced):");
709
+ for (const { task, reason } of skipped) {
710
+ console.log(` ${task.id} - ${task.title} (${reason})`);
711
+ }
712
+ }
713
+ }
714
+ return;
715
+ }
716
+ const removedIds = toRemove.map((t) => t.id);
717
+ store.tasks = store.tasks.filter((t) => !removedIds.includes(t.id));
718
+ saveStore(store);
719
+ if (fmt === "json") {
720
+ console.log(JSON.stringify({ removed: removedIds, skipped }, null, 2));
721
+ } else if (fmt === "compact") {
722
+ console.log(`removed:${removedIds.length > 0 ? removedIds.join(",") : "none"}`);
723
+ } else {
724
+ if (removedIds.length === 0) {
725
+ console.log("No tasks removed.");
726
+ } else {
727
+ console.log(`Removed ${removedIds.length} task(s): ${removedIds.join(", ")}`);
728
+ }
729
+ if (skipped.length > 0) {
730
+ console.log(`Skipped ${skipped.length} task(s) (still referenced by open tasks)`);
731
+ }
732
+ }
733
+ } catch (err) {
734
+ console.error(err.message);
735
+ process.exit(1);
736
+ }
737
+ });
738
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "claude-task-tracker",
3
+ "version": "1.0.0",
4
+ "description": "CLI task tracker with dependency management for agentic code workflows",
5
+ "type": "module",
6
+ "bin": {
7
+ "ctt": "./dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsup src/index.ts --format esm --dts --clean",
14
+ "build:binary": "npm run build && pkg dist/index.js --targets node20-macos-arm64,node20-macos-x64,node20-linux-x64,node20-win-x64 --output bin/ctt",
15
+ "dev": "tsx src/index.ts",
16
+ "start": "node dist/index.js",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "keywords": [
20
+ "cli",
21
+ "task-tracker",
22
+ "agentic",
23
+ "claude",
24
+ "claude-code",
25
+ "ai",
26
+ "llm",
27
+ "task-management",
28
+ "dependencies"
29
+ ],
30
+ "author": "farisphp",
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/farisphp/claude-task-tracker.git"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/farisphp/claude-task-tracker/issues"
38
+ },
39
+ "homepage": "https://github.com/farisphp/claude-task-tracker#readme",
40
+ "engines": {
41
+ "node": ">=18"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^20.10.0",
45
+ "@yao-pkg/pkg": "^5.11.0",
46
+ "tsup": "^8.0.1",
47
+ "tsx": "^4.7.0",
48
+ "typescript": "^5.3.0"
49
+ },
50
+ "dependencies": {
51
+ "commander": "^12.0.0"
52
+ }
53
+ }