@nijaru/tk 0.0.2 → 0.0.4

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/src/cli.ts CHANGED
@@ -4,6 +4,7 @@ import { version } from "../package.json";
4
4
 
5
5
  import * as storage from "./db/storage";
6
6
  import { parsePriority } from "./lib/priority";
7
+ import { parseDueDate, parseEstimate } from "./lib/time";
7
8
  import {
8
9
  formatTaskList,
9
10
  formatTaskDetail,
@@ -12,19 +13,36 @@ import {
12
13
  green,
13
14
  red,
14
15
  yellow,
16
+ dim,
15
17
  } from "./lib/format";
16
18
  import { findRoot, setWorkingDir } from "./lib/root";
17
19
  import { parseId } from "./types";
18
- import type { Status } from "./types";
20
+ import type { Status, TaskWithMeta } from "./types";
19
21
  import { BASH_COMPLETION, ZSH_COMPLETION, FISH_COMPLETION } from "./lib/completions";
22
+ import { MAIN_HELP, COMMAND_HELP } from "./lib/help";
20
23
 
21
24
  const VALID_STATUSES: Status[] = ["open", "active", "done"];
22
25
  const PROJECT_PATTERN = /^[a-z][a-z0-9]*$/;
23
26
 
27
+ const COMMON_OPTIONS = {
28
+ project: { type: "string", short: "P" },
29
+ priority: { type: "string", short: "p" },
30
+ labels: { type: "string", short: "l" },
31
+ } as const;
32
+
33
+ const TASK_MUTATION_OPTIONS = {
34
+ ...COMMON_OPTIONS,
35
+ description: { type: "string", short: "d" },
36
+ assignees: { type: "string", short: "A" },
37
+ parent: { type: "string" },
38
+ estimate: { type: "string" },
39
+ due: { type: "string" },
40
+ } as const;
41
+
24
42
  function validateProject(name: string): void {
25
43
  if (!PROJECT_PATTERN.test(name)) {
26
44
  throw new Error(
27
- `Invalid project name: ${name}. Use lowercase letters and numbers, starting with a letter.`,
45
+ `Invalid project name: ${name}. Use lowercase letters and numbers, starting with a letter (e.g., 'api', 'web2').`,
28
46
  );
29
47
  }
30
48
  }
@@ -39,74 +57,16 @@ function parseStatus(input: string | undefined): Status | undefined {
39
57
 
40
58
  function parseLimit(input: string | undefined): number | undefined {
41
59
  if (!input) return undefined;
42
- const n = parseInt(input, 10);
43
- if (isNaN(n) || n < 1) {
60
+ if (!/^\d+$/.test(input)) {
44
61
  throw new Error(`Invalid limit: ${input}. Must be a positive number.`);
45
62
  }
46
- return n;
47
- }
48
-
49
- function parseEstimate(input: string | undefined): number | undefined {
50
- if (!input) return undefined;
51
- const n = parseInt(input, 10);
52
- if (isNaN(n) || n < 0) {
53
- throw new Error(`Invalid estimate: ${input}. Must be a non-negative number.`);
63
+ const n = Number(input);
64
+ if (n < 1) {
65
+ throw new Error(`Invalid limit: ${input}. Must be a positive number.`);
54
66
  }
55
67
  return n;
56
68
  }
57
69
 
58
- function formatLocalDate(date: Date): string {
59
- const year = date.getFullYear();
60
- const month = String(date.getMonth() + 1).padStart(2, "0");
61
- const day = String(date.getDate()).padStart(2, "0");
62
- return `${year}-${month}-${day}`;
63
- }
64
-
65
- function parseDueDate(input: string | undefined): string | undefined {
66
- if (!input) return undefined;
67
- if (input === "-") return undefined; // clear
68
-
69
- // Handle relative dates like +7d
70
- if (input.startsWith("+")) {
71
- const match = input.match(/^\+(\d+)([dwmh])$/);
72
- if (match && match[1] && match[2]) {
73
- const num = match[1];
74
- const unit = match[2];
75
- const n = parseInt(num, 10);
76
- const now = new Date();
77
- switch (unit) {
78
- case "h":
79
- now.setHours(now.getHours() + n);
80
- break;
81
- case "d":
82
- now.setDate(now.getDate() + n);
83
- break;
84
- case "w":
85
- now.setDate(now.getDate() + n * 7);
86
- break;
87
- case "m":
88
- now.setMonth(now.getMonth() + n);
89
- break;
90
- }
91
- return formatLocalDate(now);
92
- }
93
- throw new Error(`Invalid relative date: ${input}. Use format like +7d, +2w, +1m`);
94
- }
95
-
96
- // Validate YYYY-MM-DD format - return as-is to avoid timezone issues
97
- const dateMatch = input.match(/^(\d{4})-(\d{2})-(\d{2})$/);
98
- if (dateMatch) {
99
- const [, , month, day] = dateMatch;
100
- const m = parseInt(month!, 10);
101
- const d = parseInt(day!, 10);
102
- // Basic validation: month 1-12, day 1-31
103
- if (m >= 1 && m <= 12 && d >= 1 && d <= 31) {
104
- return input; // Return as-is, already in correct format
105
- }
106
- }
107
- throw new Error(`Invalid date: ${input}. Use YYYY-MM-DD or +Nd format.`);
108
- }
109
-
110
70
  function parseLabels(input: string | undefined): string[] | undefined {
111
71
  if (!input) return undefined;
112
72
  return input
@@ -146,18 +106,32 @@ function resolveId(input: string | undefined, context: string): string {
146
106
  const resolved = storage.resolveId(input);
147
107
  if (resolved) return resolved;
148
108
 
149
- // Check if it's a valid full ID format
150
- if (parseId(input)) return input;
151
-
152
109
  // Check for ambiguous matches
153
110
  const matches = storage.findMatchingIds(input);
154
111
  if (matches.length > 1) {
155
112
  throw new Error(
156
- `Ambiguous ID '${input}' matches ${matches.length} tasks: ${matches.join(", ")}`,
113
+ `Ambiguous ID '${input}' matches ${matches.length} tasks: ${matches.join(", ")}. Use more characters to narrow it down.`,
157
114
  );
158
115
  }
159
116
 
160
- throw new Error(`Task not found: ${input}`);
117
+ // If it's a valid full ID format but doesn't exist, we still return it
118
+ // and let getTask handle the "not found" error consistently.
119
+ if (parseId(input)) return input;
120
+
121
+ throw new Error(`Task not found: ${input}. Run 'tk ls' to see available tasks.`);
122
+ }
123
+
124
+ /**
125
+ * Resolves an ID and fetches the task.
126
+ * Handles "Task not found" and cleanup output automatically.
127
+ */
128
+ function resolveTask(input: string | undefined, context: string): TaskWithMeta {
129
+ const id = resolveId(input, context);
130
+ const result = storage.getTask(id);
131
+ if (!result) error(`Task not found: ${id}. Run 'tk ls' to see available tasks.`);
132
+
133
+ outputCleanup(id, result.cleanup);
134
+ return result.task;
161
135
  }
162
136
 
163
137
  const rawArgs = process.argv.slice(2);
@@ -223,258 +197,12 @@ function error(message: string): never {
223
197
  process.exit(1);
224
198
  }
225
199
 
226
- function showHelp() {
227
- console.log(`tk v${version} - Task tracker for AI agents
228
-
229
- USAGE:
230
- tk <command> [options]
231
-
232
- COMMANDS:
233
- init Initialize .tasks/ directory
234
- add Create task
235
- ls, list List tasks
236
- ready List ready tasks (open + unblocked)
237
- show Show task details
238
- start Start working (open -> active)
239
- done Complete task
240
- reopen Reopen task
241
- edit Edit task
242
- log Add log entry
243
- block Add blocker
244
- unblock Remove blocker
245
- rm, remove Delete task
246
- clean Remove old done tasks
247
- check Check for data issues
248
- config Show/set configuration
249
- completions Output shell completions
250
-
251
- GLOBAL OPTIONS:
252
- -C <dir> Run in different directory
253
- --json Output as JSON
254
- -h, --help Show help
255
- -V Show version
256
-
257
- Run 'tk <command> --help' for command-specific options.
258
- `);
200
+ function showHelp(): void {
201
+ console.log(MAIN_HELP);
259
202
  }
260
203
 
261
- function showCommandHelp(cmd: string) {
262
- const helps: Record<string, string> = {
263
- init: `tk init - Initialize .tasks/ directory
264
-
265
- USAGE:
266
- tk init [options]
267
-
268
- OPTIONS:
269
- -P, --project <name> Set default project name
270
- `,
271
- add: `tk add - Create a new task
272
-
273
- USAGE:
274
- tk add <title> [options]
275
-
276
- OPTIONS:
277
- -p, --priority <0-4> Priority (0=none, 1=urgent, 2=high, 3=medium, 4=low)
278
- -P, --project <name> Project prefix for ID
279
- -d, --description <text> Description
280
- -l, --labels <csv> Labels (comma-separated)
281
- -A, --assignees <csv> Assignees (comma-separated, @me for git user)
282
- --parent <id> Parent task ID
283
- --estimate <n> Estimate (user-defined units)
284
- --due <date> Due date (YYYY-MM-DD or +Nh/+Nd/+Nw/+Nm)
285
-
286
- EXAMPLES:
287
- tk add "Fix login bug" -p 1
288
- tk add "New feature" -P api -l bug,urgent
289
- tk add "Sprint task" --due +7d
290
- `,
291
- ls: `tk ls - List tasks
292
-
293
- USAGE:
294
- tk ls [options]
295
-
296
- OPTIONS:
297
- -s, --status <status> Filter by status (open, active, done)
298
- -p, --priority <0-4> Filter by priority
299
- -P, --project <name> Filter by project
300
- -l, --label <label> Filter by label
301
- --assignee <name> Filter by assignee
302
- --parent <id> Filter by parent
303
- --roots Show only root tasks (no parent)
304
- --overdue Show only overdue tasks
305
- -n, --limit <n> Limit results (default: 20)
306
- -a, --all Show all (no limit)
307
-
308
- EXAMPLES:
309
- tk ls -s open -p 1 # Urgent open tasks
310
- tk ls --overdue # Overdue tasks
311
- tk ls -P api --roots # Root tasks in api project
312
- `,
313
- list: `tk list - List tasks (alias for 'ls')
314
-
315
- Run 'tk ls --help' for options.
316
- `,
317
- ready: `tk ready - List ready tasks (open + unblocked)
318
-
319
- USAGE:
320
- tk ready
321
-
322
- Shows open tasks that are not blocked by any incomplete task.
323
- `,
324
- show: `tk show - Show task details
325
-
326
- USAGE:
327
- tk show <id>
328
-
329
- EXAMPLES:
330
- tk show tk-1
331
- tk show 1 # If unambiguous
332
- `,
333
- start: `tk start - Start working on a task
334
-
335
- USAGE:
336
- tk start <id>
337
-
338
- Changes status from open to active.
339
- `,
340
- done: `tk done - Complete a task
341
-
342
- USAGE:
343
- tk done <id>
344
-
345
- Changes status to done.
346
- `,
347
- reopen: `tk reopen - Reopen a task
348
-
349
- USAGE:
350
- tk reopen <id>
351
-
352
- Changes status back to open.
353
- `,
354
- edit: `tk edit - Edit a task
355
-
356
- USAGE:
357
- tk edit <id> [options]
358
-
359
- OPTIONS:
360
- -t, --title <text> New title
361
- -d, --description <text> New description
362
- -p, --priority <0-4> New priority
363
- -l, --labels <csv> Replace labels (use +tag/-tag to add/remove)
364
- -A, --assignees <csv> Replace assignees
365
- --parent <id> Set parent (use - to clear)
366
- --estimate <n> Set estimate (use - to clear)
367
- --due <date> Set due date (use - to clear)
368
-
369
- EXAMPLES:
370
- tk edit tk-1 -t "New title"
371
- tk edit tk-1 -l +urgent # Add label
372
- tk edit tk-1 --due - # Clear due date
373
- `,
374
- log: `tk log - Add a log entry to a task
375
-
376
- USAGE:
377
- tk log <id> "<message>"
378
-
379
- Message must be quoted.
380
-
381
- EXAMPLES:
382
- tk log tk-1 "Started implementation"
383
- tk log tk-1 "Blocked on API changes"
384
- `,
385
- block: `tk block - Add a blocker dependency
386
-
387
- USAGE:
388
- tk block <task> <blocker>
389
-
390
- The first task becomes blocked by the second.
391
-
392
- EXAMPLES:
393
- tk block tk-2 tk-1 # tk-2 is blocked by tk-1
394
- `,
395
- unblock: `tk unblock - Remove a blocker dependency
396
-
397
- USAGE:
398
- tk unblock <task> <blocker>
399
-
400
- EXAMPLES:
401
- tk unblock tk-2 tk-1
402
- `,
403
- rm: `tk rm - Delete a task
404
-
405
- USAGE:
406
- tk rm <id>
407
-
408
- EXAMPLES:
409
- tk rm tk-1
410
- `,
411
- remove: `tk remove - Delete a task (alias for 'rm')
412
-
413
- USAGE:
414
- tk remove <id>
415
- `,
416
- clean: `tk clean - Remove old completed tasks
417
-
418
- USAGE:
419
- tk clean [options]
420
-
421
- OPTIONS:
422
- --older-than <days> Age threshold in days (default: from config, 14)
423
- -f, --force Remove all done tasks (ignores age and disabled state)
424
-
425
- EXAMPLES:
426
- tk clean # Remove done tasks older than config.clean_after days
427
- tk clean --older-than 30 # Remove done tasks older than 30 days
428
- tk clean --force # Remove all done tasks regardless of age
429
- `,
430
- check: `tk check - Check for data issues
431
-
432
- USAGE:
433
- tk check
434
-
435
- Scans all tasks for issues. Auto-fixable issues (orphaned references, ID
436
- mismatches) are fixed automatically. Unfixable issues (corrupted JSON) are
437
- reported for manual intervention.
438
-
439
- Note: Auto-fixing also happens during normal task operations (show, done,
440
- edit, etc.) - this command is for bulk cleanup or diagnostics.
441
-
442
- EXAMPLES:
443
- tk check # Scan and fix all tasks
444
- `,
445
- config: `tk config - Show or set configuration
446
-
447
- USAGE:
448
- tk config Show all config
449
- tk config project Show default project
450
- tk config project <name> Set default project
451
- tk config project <new> --rename <old> Rename project (old-* → new-*)
452
- tk config alias List aliases
453
- tk config alias <name> <path> Add alias
454
- tk config alias --rm <name> Remove alias
455
-
456
- EXAMPLES:
457
- tk config project api
458
- tk config project lsmvec --rename cloudlsmvec
459
- tk config alias web src/web
460
- `,
461
- completions: `tk completions - Output shell completions
462
-
463
- USAGE:
464
- tk completions <shell>
465
-
466
- SHELLS:
467
- bash Bash completion script
468
- zsh Zsh completion script
469
- fish Fish completion script
470
-
471
- EXAMPLES:
472
- eval "$(tk completions bash)" # Add to ~/.bashrc
473
- eval "$(tk completions zsh)" # Add to ~/.zshrc
474
- `,
475
- };
476
-
477
- const help = helps[cmd];
204
+ function showCommandHelp(cmd: string): void {
205
+ const help = COMMAND_HELP[cmd];
478
206
  if (help) {
479
207
  console.log(help);
480
208
  } else {
@@ -526,7 +254,7 @@ function main() {
526
254
  if (info.exists) {
527
255
  output(
528
256
  { path: info.tasksDir, created: false },
529
- green(`Already initialized: ${info.tasksDir}`),
257
+ dim(`Already initialized: ${info.tasksDir}`),
530
258
  );
531
259
  } else {
532
260
  const path = storage.initTasks(values.project);
@@ -538,16 +266,7 @@ function main() {
538
266
  case "add": {
539
267
  const { values, positionals } = parseArgs({
540
268
  args,
541
- options: {
542
- description: { type: "string", short: "d" },
543
- priority: { type: "string", short: "p" },
544
- project: { type: "string", short: "P" },
545
- labels: { type: "string", short: "l" },
546
- assignees: { type: "string", short: "A" },
547
- parent: { type: "string" },
548
- estimate: { type: "string" },
549
- due: { type: "string" },
550
- },
269
+ options: TASK_MUTATION_OPTIONS,
551
270
  allowPositionals: true,
552
271
  });
553
272
  const title = positionals[0]?.trim();
@@ -562,7 +281,8 @@ function main() {
562
281
  let parentId: string | undefined;
563
282
  if (values.parent) {
564
283
  const resolved = storage.resolveId(values.parent);
565
- if (!resolved) error(`Parent task not found: ${values.parent}`);
284
+ if (!resolved)
285
+ error(`Parent task not found: ${values.parent}. Run 'tk ls' to see available tasks.`);
566
286
  parentId = resolved;
567
287
  const parentResult = storage.validateParent(parentId);
568
288
  if (!parentResult.ok) error(parentResult.error!);
@@ -588,10 +308,9 @@ function main() {
588
308
  const { values } = parseArgs({
589
309
  args,
590
310
  options: {
311
+ ...COMMON_OPTIONS,
312
+ label: COMMON_OPTIONS.labels, // alias for consistency
591
313
  status: { type: "string", short: "s" },
592
- priority: { type: "string", short: "p" },
593
- project: { type: "string", short: "P" },
594
- label: { type: "string", short: "l" },
595
314
  assignee: { type: "string" },
596
315
  parent: { type: "string" },
597
316
  roots: { type: "boolean" },
@@ -609,7 +328,8 @@ function main() {
609
328
  let parentFilter: string | undefined;
610
329
  if (values.parent) {
611
330
  const resolved = storage.resolveId(values.parent);
612
- if (!resolved) error(`Parent task not found: ${values.parent}`);
331
+ if (!resolved)
332
+ error(`Parent task not found: ${values.parent}. Run 'tk ls' to see available tasks.`);
613
333
  parentFilter = resolved;
614
334
  }
615
335
 
@@ -617,7 +337,7 @@ function main() {
617
337
  status,
618
338
  priority,
619
339
  project: values.project,
620
- label: values.label,
340
+ label: values.label ?? values.labels,
621
341
  assignee: values.assignee,
622
342
  parent: parentFilter,
623
343
  roots: values.roots,
@@ -630,49 +350,40 @@ function main() {
630
350
 
631
351
  case "ready": {
632
352
  const list = storage.listReadyTasks();
633
- output(list, formatTaskList(list));
353
+ output(
354
+ list,
355
+ formatTaskList(list, undefined, "No ready tasks. All tasks are either done or blocked."),
356
+ );
634
357
  break;
635
358
  }
636
359
 
637
360
  case "show": {
638
- const id = resolveId(args[0], "show");
639
- const result = storage.getTaskWithMeta(id);
640
- if (!result) error(`Task not found: ${id}`);
641
- outputCleanup(id, result.cleanup);
642
- output(result.task, formatTaskDetail(result.task, result.task.logs));
361
+ const task = resolveTask(args[0], "show");
362
+ output(task, formatTaskDetail(task, task.logs));
643
363
  break;
644
364
  }
645
365
 
646
366
  case "start": {
647
- const id = resolveId(args[0], "start");
648
- const result = storage.getTask(id);
649
- if (!result) error(`Task not found: ${id}`);
650
- outputCleanup(id, result.cleanup);
651
- if (result.task.status === "active")
652
- error(`Task already active. Use 'tk done ${id}' to complete it.`);
653
- if (result.task.status === "done") error(`Task already done. Use 'tk reopen ${id}' first.`);
654
- const updated = storage.updateTaskStatus(id, "active");
655
- output(updated, green(`Started: ${id}`));
367
+ const task = resolveTask(args[0], "start");
368
+ if (task.status === "active")
369
+ error(`Task already active. Use 'tk done ${task.id}' to complete it.`);
370
+ if (task.status === "done") error(`Task already done. Use 'tk reopen ${task.id}' first.`);
371
+ const updated = storage.updateTaskStatus(task.id, "active");
372
+ output(updated, green(`Started: ${task.id}`));
656
373
  break;
657
374
  }
658
375
 
659
376
  case "done": {
660
- const id = resolveId(args[0], "done");
661
- const result = storage.getTask(id);
662
- if (!result) error(`Task not found: ${id}`);
663
- outputCleanup(id, result.cleanup);
664
- const updated = storage.updateTaskStatus(id, "done");
665
- output(updated, green(`Completed: ${id}`));
377
+ const task = resolveTask(args[0], "done");
378
+ const updated = storage.updateTaskStatus(task.id, "done");
379
+ output(updated, green(`Completed: ${task.id}`));
666
380
  break;
667
381
  }
668
382
 
669
383
  case "reopen": {
670
- const id = resolveId(args[0], "reopen");
671
- const result = storage.getTask(id);
672
- if (!result) error(`Task not found: ${id}`);
673
- outputCleanup(id, result.cleanup);
674
- const updated = storage.updateTaskStatus(id, "open");
675
- output(updated, green(`Reopened: ${id}`));
384
+ const task = resolveTask(args[0], "reopen");
385
+ const updated = storage.updateTaskStatus(task.id, "open");
386
+ output(updated, green(`Reopened: ${task.id}`));
676
387
  break;
677
388
  }
678
389
 
@@ -680,21 +391,12 @@ function main() {
680
391
  const { values, positionals } = parseArgs({
681
392
  args,
682
393
  options: {
394
+ ...TASK_MUTATION_OPTIONS,
683
395
  title: { type: "string", short: "t" },
684
- description: { type: "string", short: "d" },
685
- priority: { type: "string", short: "p" },
686
- labels: { type: "string", short: "l" },
687
- assignees: { type: "string", short: "A" },
688
- parent: { type: "string" },
689
- estimate: { type: "string" },
690
- due: { type: "string" },
691
396
  },
692
397
  allowPositionals: true,
693
398
  });
694
- const id = resolveId(positionals[0], "edit");
695
- const result = storage.getTask(id);
696
- if (!result) error(`Task not found: ${id}`);
697
- outputCleanup(id, result.cleanup);
399
+ const task = resolveTask(positionals[0], "edit");
698
400
 
699
401
  // Handle label modifications (+tag, -tag)
700
402
  let labels: string[] | undefined;
@@ -702,13 +404,11 @@ function main() {
702
404
  if (values.labels.startsWith("+")) {
703
405
  // Add label (avoid duplicates)
704
406
  const newLabel = values.labels.slice(1);
705
- labels = result.task.labels.includes(newLabel)
706
- ? result.task.labels
707
- : [...result.task.labels, newLabel];
407
+ labels = task.labels.includes(newLabel) ? task.labels : [...task.labels, newLabel];
708
408
  } else if (values.labels.startsWith("-")) {
709
409
  // Remove label
710
410
  const removeLabel = values.labels.slice(1);
711
- labels = result.task.labels.filter((l) => l !== removeLabel);
411
+ labels = task.labels.filter((l: string) => l !== removeLabel);
712
412
  } else {
713
413
  // Replace labels
714
414
  labels = parseLabels(values.labels);
@@ -721,13 +421,14 @@ function main() {
721
421
  resolvedParent = null;
722
422
  } else if (values.parent) {
723
423
  const resolved = storage.resolveId(values.parent);
724
- if (!resolved) error(`Parent task not found: ${values.parent}`);
424
+ if (!resolved)
425
+ error(`Parent task not found: ${values.parent}. Run 'tk ls' to see available tasks.`);
725
426
  resolvedParent = resolved;
726
- const parentResult = storage.validateParent(resolvedParent, id);
427
+ const parentResult = storage.validateParent(resolvedParent, task.id);
727
428
  if (!parentResult.ok) error(parentResult.error!);
728
429
  }
729
430
 
730
- const updated = storage.updateTask(id, {
431
+ const updated = storage.updateTask(task.id, {
731
432
  title: values.title?.trim() || undefined,
732
433
  description: values.description,
733
434
  priority: values.priority ? parsePriority(values.priority) : undefined,
@@ -737,12 +438,12 @@ function main() {
737
438
  estimate: values.estimate === "-" ? null : (parseEstimate(values.estimate) ?? undefined),
738
439
  due_date: values.due === "-" ? null : (parseDueDate(values.due) ?? undefined),
739
440
  });
740
- output(updated, green(`Updated: ${id}`));
441
+ output(updated, green(`Updated: ${task.id}`));
741
442
  break;
742
443
  }
743
444
 
744
445
  case "log": {
745
- const id = resolveId(args[0], "log");
446
+ const task = resolveTask(args[0], "log");
746
447
  const message = args[1]?.trim();
747
448
  if (!message) error('Message required: tk log <id> "<message>"');
748
449
  if (args.length > 2) {
@@ -751,19 +452,19 @@ function main() {
751
452
  ` Got ${args.length - 1} arguments instead of 1`,
752
453
  );
753
454
  }
754
- const result = storage.getTask(id);
755
- if (!result) error(`Task not found: ${id}`);
756
- outputCleanup(id, result.cleanup);
757
- const entry = storage.addLogEntry(id, message);
758
- output(entry, green(`Logged: ${id}`));
455
+ const entry = storage.addLogEntry(task.id, message);
456
+ output(entry, green(`Logged: ${task.id}`));
759
457
  break;
760
458
  }
761
459
 
762
460
  case "block": {
763
- if (!args[0] || !args[1]) error("Usage: tk block <task> <blocker>");
461
+ if (!args[0] || !args[1])
462
+ error(
463
+ "Two IDs required: tk block <task> <blocker>. The first task becomes blocked by the second.",
464
+ );
764
465
  const taskId = resolveId(args[0], "block");
765
466
  const blockerId = resolveId(args[1], "block");
766
- if (taskId === blockerId) error("Task cannot block itself");
467
+ if (taskId === blockerId) error("Task cannot block itself.");
767
468
  const result = storage.addBlock(taskId, blockerId);
768
469
  if (!result.ok) error(result.error!);
769
470
  output(
@@ -774,7 +475,7 @@ function main() {
774
475
  }
775
476
 
776
477
  case "unblock": {
777
- if (!args[0] || !args[1]) error("Usage: tk unblock <task> <blocker>");
478
+ if (!args[0] || !args[1]) error("Two IDs required: tk unblock <task> <blocker>.");
778
479
  const taskId = resolveId(args[0], "unblock");
779
480
  const blockerId = resolveId(args[1], "unblock");
780
481
  const removed = storage.removeBlock(taskId, blockerId);
@@ -790,7 +491,7 @@ function main() {
790
491
  case "remove": {
791
492
  const id = resolveId(args[0], "rm");
792
493
  const deleted = storage.deleteTask(id);
793
- if (!deleted) error(`Task not found: ${id}`);
494
+ if (!deleted) error(`Task not found: ${id}. Run 'tk ls' to see available tasks.`);
794
495
  output({ id, deleted: true }, green(`Deleted: ${id}`));
795
496
  break;
796
497
  }
@@ -809,11 +510,10 @@ function main() {
809
510
  // Get days from CLI or config
810
511
  let days: number | false;
811
512
  if (values["older-than"] !== undefined) {
812
- const n = parseInt(values["older-than"], 10);
813
- if (isNaN(n) || n < 0) {
513
+ if (!/^\d+$/.test(values["older-than"])) {
814
514
  error(`Invalid --older-than: ${values["older-than"]}. Use a number of days.`);
815
515
  }
816
- days = n;
516
+ days = Number(values["older-than"]);
817
517
  } else {
818
518
  days = config.clean_after;
819
519
  // Validate config value at runtime
@@ -933,7 +633,7 @@ function main() {
933
633
  const alias = positionals[0];
934
634
  const path = positionals[1];
935
635
  if (!alias || !path || !alias.trim()) {
936
- error("Alias name and path are required");
636
+ error("Alias name and path required: tk config alias <name> <path>");
937
637
  }
938
638
  const updated = storage.setAlias(alias, path);
939
639
  output(updated, green(`Added alias: ${alias} → ${path}`));
@@ -941,7 +641,10 @@ function main() {
941
641
  // List aliases
942
642
  const aliases = config.aliases;
943
643
  if (Object.keys(aliases).length === 0) {
944
- output({ aliases: {} }, "No aliases defined.");
644
+ output(
645
+ { aliases: {} },
646
+ "No aliases defined. Add one with: tk config alias <name> <path>",
647
+ );
945
648
  } else {
946
649
  const lines = Object.entries(aliases)
947
650
  .map(([a, p]) => `${a} → ${p}`)
@@ -952,7 +655,7 @@ function main() {
952
655
  break;
953
656
  }
954
657
  default:
955
- error(`Unknown config command: ${subcommand}`);
658
+ error(`Unknown config command: ${subcommand}. Valid: project, alias.`);
956
659
  }
957
660
  break;
958
661
  }
@@ -970,7 +673,7 @@ function main() {
970
673
  console.log(FISH_COMPLETION);
971
674
  break;
972
675
  default:
973
- error("Shell required: tk completions <bash|zsh|fish>");
676
+ error("Shell required: tk completions <bash|zsh|fish>. Add to your shell's rc file.");
974
677
  }
975
678
  break;
976
679
  }