@nijaru/tk 0.0.3 → 0.0.5

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.
@@ -0,0 +1,266 @@
1
+ import { version } from "../../package.json";
2
+
3
+ export const MAIN_HELP = `tk v${version} - Task tracker for AI agents
4
+
5
+ USAGE:
6
+ tk <command> [options]
7
+
8
+ COMMANDS:
9
+ init Initialize .tasks/ directory
10
+ add Create task
11
+ ls, list List tasks
12
+ ready List ready tasks (active/open + unblocked)
13
+ show Show task details
14
+ start Start working (open -> active)
15
+ done Complete task
16
+ reopen Reopen task
17
+ edit Edit task
18
+ log Add log entry
19
+ block Add blocker
20
+ unblock Remove blocker
21
+ mv, move Move task to different project
22
+ rm, remove Delete task
23
+ clean Remove old done tasks
24
+ check Check for data issues
25
+ config Show/set configuration
26
+ completions Output shell completions
27
+
28
+ GLOBAL OPTIONS:
29
+ -C <dir> Run in different directory
30
+ --json Output as JSON
31
+ -h, --help Show help
32
+ -V Show version
33
+
34
+ Run 'tk <command> --help' for command-specific options.
35
+ `;
36
+
37
+ export const COMMAND_HELP: Record<string, string> = {
38
+ init: `tk init - Initialize .tasks/ directory
39
+
40
+ USAGE:
41
+ tk init [options]
42
+
43
+ OPTIONS:
44
+ -P, --project <name> Set default project name
45
+ `,
46
+ add: `tk add - Create a new task
47
+
48
+ USAGE:
49
+ tk add <title> [options]
50
+
51
+ OPTIONS:
52
+ -p, --priority <0-4> Priority (0=none, 1=urgent, 2=high, 3=medium, 4=low)
53
+ -P, --project <name> Project prefix for ID
54
+ -d, --description <text> Description
55
+ -l, --labels <csv> Labels (comma-separated)
56
+ -A, --assignees <csv> Assignees (comma-separated, @me for git user)
57
+ --parent <id> Parent task ID
58
+ --estimate <n> Estimate (user-defined units)
59
+ --due <date> Due date (YYYY-MM-DD or +Nh/+Nd/+Nw/+Nm)
60
+
61
+ EXAMPLES:
62
+ tk add "Fix login bug" -p 1
63
+ tk add "New feature" -P api -l bug,urgent
64
+ tk add "Sprint task" --due +7d
65
+ `,
66
+ ls: `tk ls - List tasks
67
+
68
+ USAGE:
69
+ tk ls [options]
70
+
71
+ OPTIONS:
72
+ -s, --status <status> Filter by status (open, active, done)
73
+ -p, --priority <0-4> Filter by priority
74
+ -P, --project <name> Filter by project
75
+ -l, --label <label> Filter by label
76
+ --assignee <name> Filter by assignee
77
+ --parent <id> Filter by parent
78
+ --roots Show only root tasks (no parent)
79
+ --overdue Show only overdue tasks
80
+ -n, --limit <n> Limit results (default: 20)
81
+ -a, --all Show all (no limit)
82
+
83
+ EXAMPLES:
84
+ tk ls -s open -p 1 # Urgent open tasks
85
+ tk ls --overdue # Overdue tasks
86
+ tk ls -P api --roots # Root tasks in api project
87
+ `,
88
+ list: `tk list - List tasks (alias for 'ls')
89
+
90
+ Run 'tk ls --help' for options.
91
+ `,
92
+ ready: `tk ready - List ready tasks (active/open + unblocked)
93
+
94
+ USAGE:
95
+ tk ready
96
+
97
+ Shows active or open tasks that are not blocked by any incomplete task.
98
+ `,
99
+ show: `tk show - Show task details
100
+
101
+ USAGE:
102
+ tk show <id>
103
+
104
+ EXAMPLES:
105
+ tk show tk-1
106
+ tk show 1 # If unambiguous
107
+ `,
108
+ start: `tk start - Start working on a task
109
+
110
+ USAGE:
111
+ tk start <id>
112
+
113
+ Changes status from open to active.
114
+ `,
115
+ done: `tk done - Complete a task
116
+
117
+ USAGE:
118
+ tk done <id>
119
+
120
+ Changes status to done.
121
+ `,
122
+ reopen: `tk reopen - Reopen a task
123
+
124
+ USAGE:
125
+ tk reopen <id>
126
+
127
+ Changes status back to open.
128
+ `,
129
+ edit: `tk edit - Edit a task
130
+
131
+ USAGE:
132
+ tk edit <id> [options]
133
+
134
+ OPTIONS:
135
+ -t, --title <text> New title
136
+ -d, --description <text> New description
137
+ -p, --priority <0-4> New priority
138
+ -l, --labels <csv> Replace labels (use +tag/-tag to add/remove)
139
+ -A, --assignees <csv> Replace assignees
140
+ --parent <id> Set parent (use - to clear)
141
+ --estimate <n> Set estimate (use - to clear)
142
+ --due <date> Set due date (use - to clear)
143
+
144
+ EXAMPLES:
145
+ tk edit tk-1 -t "New title"
146
+ tk edit tk-1 -l +urgent # Add label
147
+ tk edit tk-1 --due - # Clear due date
148
+ `,
149
+ log: `tk log - Add a log entry to a task
150
+
151
+ USAGE:
152
+ tk log <id> "<message>"
153
+
154
+ Message must be quoted.
155
+
156
+ EXAMPLES:
157
+ tk log tk-1 "Started implementation"
158
+ tk log tk-1 "Blocked on API changes"
159
+ `,
160
+ block: `tk block - Add a blocker dependency
161
+
162
+ USAGE:
163
+ tk block <task> <blocker>
164
+
165
+ The first task becomes blocked by the second.
166
+
167
+ EXAMPLES:
168
+ tk block tk-2 tk-1 # tk-2 is blocked by tk-1
169
+ `,
170
+ unblock: `tk unblock - Remove a blocker dependency
171
+
172
+ USAGE:
173
+ tk unblock <task> <blocker>
174
+
175
+ EXAMPLES:
176
+ tk unblock tk-2 tk-1
177
+ `,
178
+ mv: `tk mv - Move a task to a different project
179
+
180
+ USAGE:
181
+ tk mv <id> <project>
182
+
183
+ Moves a task to a new project, keeping the same ref.
184
+ Errors if the new ID already exists.
185
+ Updates all blocked_by and parent references.
186
+
187
+ EXAMPLES:
188
+ tk mv api-a1b2 web # Moves api-a1b2 → web-a1b2
189
+ tk mv a1b2 archive # Partial ID resolution
190
+ `,
191
+ move: `tk move - Move a task to a different project (alias for 'mv')
192
+
193
+ USAGE:
194
+ tk move <id> <project>
195
+
196
+ Run 'tk mv --help' for options.
197
+ `,
198
+ rm: `tk rm - Delete a task
199
+
200
+ USAGE:
201
+ tk rm <id>
202
+
203
+ EXAMPLES:
204
+ tk rm tk-1
205
+ `,
206
+ remove: `tk remove - Delete a task (alias for 'rm')
207
+
208
+ USAGE:
209
+ tk remove <id>
210
+ `,
211
+ clean: `tk clean - Remove old completed tasks
212
+
213
+ USAGE:
214
+ tk clean [options]
215
+
216
+ OPTIONS:
217
+ --older-than <days> Age threshold in days (default: from config, 14)
218
+ -f, --force Remove all done tasks (ignores age and disabled state)
219
+
220
+ EXAMPLES:
221
+ tk clean # Remove done tasks older than config.clean_after days
222
+ tk clean --older-than 30 # Remove done tasks older than 30 days
223
+ tk clean --force # Remove all done tasks regardless of age
224
+ `,
225
+ check: `tk check - Check for data issues
226
+
227
+ USAGE:
228
+ tk check
229
+
230
+ Scans all tasks for issues. Auto-fixable issues (orphaned references, ID
231
+ mismatches) are fixed automatically. Unfixable issues (corrupted JSON) are
232
+ reported for manual intervention.
233
+
234
+ Note: Auto-fixing also happens during normal task operations (show, done,
235
+ edit, etc.) - this command is for bulk cleanup or diagnostics.
236
+
237
+ EXAMPLES:
238
+ tk check # Scan and fix all tasks
239
+ `,
240
+ config: `tk config - Show or set configuration
241
+
242
+ USAGE:
243
+ tk config Show all config
244
+ tk config project Show default project
245
+ tk config project <name> Set default project
246
+ tk config project <new> --rename <old> Rename project (old-* → new-*)
247
+
248
+ EXAMPLES:
249
+ tk config project api
250
+ tk config project lsmvec --rename cloudlsmvec
251
+ `,
252
+ completions: `tk completions - Output shell completions
253
+
254
+ USAGE:
255
+ tk completions <shell>
256
+
257
+ SHELLS:
258
+ bash Bash completion script
259
+ zsh Zsh completion script
260
+ fish Fish completion script
261
+
262
+ EXAMPLES:
263
+ eval "$(tk completions bash)" # Add to ~/.bashrc
264
+ eval "$(tk completions zsh)" # Add to ~/.zshrc
265
+ `,
266
+ };
@@ -0,0 +1,222 @@
1
+ import { test, expect, describe } from "bun:test";
2
+ import {
3
+ isTaskOverdue,
4
+ daysUntilDue,
5
+ formatRelativeTime,
6
+ formatDate,
7
+ formatLocalDate,
8
+ parseDueDate,
9
+ parseEstimate,
10
+ DUE_SOON_THRESHOLD,
11
+ } from "./time";
12
+
13
+ describe("isTaskOverdue", () => {
14
+ test("returns false when no due date", () => {
15
+ expect(isTaskOverdue(null, "open")).toBe(false);
16
+ });
17
+
18
+ test("returns false for done tasks regardless of date", () => {
19
+ expect(isTaskOverdue("2020-01-01", "done")).toBe(false);
20
+ });
21
+
22
+ test("returns false for active done tasks", () => {
23
+ expect(isTaskOverdue("2020-01-01", "done")).toBe(false);
24
+ });
25
+
26
+ test("returns true for past dates (open)", () => {
27
+ expect(isTaskOverdue("2020-01-01", "open")).toBe(true);
28
+ });
29
+
30
+ test("returns true for past dates (active)", () => {
31
+ expect(isTaskOverdue("2020-06-15", "active")).toBe(true);
32
+ });
33
+
34
+ test("returns false for today", () => {
35
+ const today = new Date().toISOString().split("T")[0]!;
36
+ expect(isTaskOverdue(today, "open")).toBe(false);
37
+ });
38
+
39
+ test("returns false for future dates", () => {
40
+ const future = new Date();
41
+ future.setDate(future.getDate() + 7);
42
+ const dateStr = future.toISOString().split("T")[0]!;
43
+ expect(isTaskOverdue(dateStr, "open")).toBe(false);
44
+ });
45
+
46
+ test("returns false for invalid date string", () => {
47
+ expect(isTaskOverdue("not-a-date", "open")).toBe(false);
48
+ });
49
+ });
50
+
51
+ describe("daysUntilDue", () => {
52
+ test("returns null when no due date", () => {
53
+ expect(daysUntilDue(null, "open")).toBeNull();
54
+ });
55
+
56
+ test("returns null when no due date (active)", () => {
57
+ expect(daysUntilDue(null, "active")).toBeNull();
58
+ });
59
+
60
+ test("returns null for done tasks", () => {
61
+ const future = new Date();
62
+ future.setDate(future.getDate() + 3);
63
+ const dateStr = future.toISOString().split("T")[0]!;
64
+ expect(daysUntilDue(dateStr, "done")).toBeNull();
65
+ });
66
+
67
+ test("returns 0 for due today (open)", () => {
68
+ const today = new Date().toISOString().split("T")[0]!;
69
+ expect(daysUntilDue(today, "open")).toBe(0);
70
+ });
71
+
72
+ test("returns 0 for due today (active)", () => {
73
+ const today = new Date().toISOString().split("T")[0]!;
74
+ expect(daysUntilDue(today, "active")).toBe(0);
75
+ });
76
+
77
+ test("returns N for due in N days", () => {
78
+ const future = new Date();
79
+ future.setDate(future.getDate() + 3);
80
+ const dateStr = future.toISOString().split("T")[0]!;
81
+ expect(daysUntilDue(dateStr, "open")).toBe(3);
82
+ });
83
+
84
+ test("returns null for past dates (already overdue)", () => {
85
+ expect(daysUntilDue("2020-01-01", "open")).toBeNull();
86
+ });
87
+
88
+ test("returns null for invalid date string", () => {
89
+ expect(daysUntilDue("not-a-date", "open")).toBeNull();
90
+ });
91
+
92
+ test("DUE_SOON_THRESHOLD is 7", () => {
93
+ expect(DUE_SOON_THRESHOLD).toBe(7);
94
+ });
95
+ });
96
+
97
+ describe("formatRelativeTime", () => {
98
+ test("returns 'now' for very recent timestamps", () => {
99
+ const ts = new Date().toISOString();
100
+ expect(formatRelativeTime(ts)).toBe("now");
101
+ });
102
+
103
+ test("returns minutes for timestamps minutes ago", () => {
104
+ const ts = new Date(Date.now() - 5 * 60 * 1000).toISOString();
105
+ expect(formatRelativeTime(ts)).toBe("5m");
106
+ });
107
+
108
+ test("returns hours for timestamps hours ago", () => {
109
+ const ts = new Date(Date.now() - 3 * 60 * 60 * 1000).toISOString();
110
+ expect(formatRelativeTime(ts)).toBe("3h");
111
+ });
112
+
113
+ test("returns days for timestamps days ago", () => {
114
+ const ts = new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString();
115
+ expect(formatRelativeTime(ts)).toBe("2d");
116
+ });
117
+ });
118
+
119
+ describe("formatDate", () => {
120
+ test("returns a non-empty string for a valid ISO timestamp", () => {
121
+ const result = formatDate("2024-01-15T10:30:00.000Z");
122
+ expect(typeof result).toBe("string");
123
+ expect(result.length).toBeGreaterThan(0);
124
+ });
125
+ });
126
+
127
+ describe("formatLocalDate", () => {
128
+ test("formats date as YYYY-MM-DD", () => {
129
+ const date = new Date(2026, 0, 15); // Jan 15 2026
130
+ expect(formatLocalDate(date)).toBe("2026-01-15");
131
+ });
132
+
133
+ test("zero-pads month and day", () => {
134
+ const date = new Date(2026, 1, 5); // Feb 5 2026
135
+ expect(formatLocalDate(date)).toBe("2026-02-05");
136
+ });
137
+ });
138
+
139
+ describe("parseDueDate", () => {
140
+ test("returns undefined for undefined input", () => {
141
+ expect(parseDueDate(undefined)).toBeUndefined();
142
+ });
143
+
144
+ test("returns undefined for '-' (clear sentinel)", () => {
145
+ expect(parseDueDate("-")).toBeUndefined();
146
+ });
147
+
148
+ test("returns valid YYYY-MM-DD unchanged", () => {
149
+ expect(parseDueDate("2026-06-15")).toBe("2026-06-15");
150
+ });
151
+
152
+ test("parses +Nd relative date", () => {
153
+ const result = parseDueDate("+7d");
154
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
155
+ const resultDate = new Date(result!);
156
+ const today = new Date();
157
+ const diffDays = Math.round((resultDate.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
158
+ expect(diffDays).toBeGreaterThanOrEqual(6);
159
+ expect(diffDays).toBeLessThanOrEqual(8);
160
+ });
161
+
162
+ test("parses +Nw relative date", () => {
163
+ const result = parseDueDate("+2w");
164
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
165
+ });
166
+
167
+ test("parses +Nm relative date", () => {
168
+ const result = parseDueDate("+1m");
169
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
170
+ });
171
+
172
+ test("parses +Nh relative date", () => {
173
+ const result = parseDueDate("+3h");
174
+ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/);
175
+ });
176
+
177
+ test("throws for invalid relative format", () => {
178
+ expect(() => parseDueDate("+7x")).toThrow("Invalid relative date");
179
+ });
180
+
181
+ test("throws for bare + with no content", () => {
182
+ expect(() => parseDueDate("+")).toThrow("Invalid relative date");
183
+ });
184
+
185
+ test("throws for non-date string", () => {
186
+ expect(() => parseDueDate("not-a-date")).toThrow("Invalid date format");
187
+ });
188
+
189
+ test("throws for invalid calendar date (month 13)", () => {
190
+ expect(() => parseDueDate("2026-13-01")).toThrow("Invalid date format");
191
+ });
192
+
193
+ test("throws for invalid calendar date (day 32)", () => {
194
+ expect(() => parseDueDate("2026-01-32")).toThrow("Invalid date format");
195
+ });
196
+ });
197
+
198
+ describe("parseEstimate", () => {
199
+ test("returns undefined for undefined input", () => {
200
+ expect(parseEstimate(undefined)).toBeUndefined();
201
+ });
202
+
203
+ test("parses valid integer string", () => {
204
+ expect(parseEstimate("42")).toBe(42);
205
+ });
206
+
207
+ test("parses zero", () => {
208
+ expect(parseEstimate("0")).toBe(0);
209
+ });
210
+
211
+ test("throws for non-numeric string", () => {
212
+ expect(() => parseEstimate("abc")).toThrow("Invalid estimate");
213
+ });
214
+
215
+ test("throws for decimal", () => {
216
+ expect(() => parseEstimate("3.5")).toThrow("Invalid estimate");
217
+ });
218
+
219
+ test("throws for negative number", () => {
220
+ expect(() => parseEstimate("-1")).toThrow("Invalid estimate");
221
+ });
222
+ });
package/src/lib/time.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import type { Status } from "../types";
2
2
 
3
+ export const DUE_SOON_THRESHOLD = 7;
4
+
3
5
  /**
4
6
  * Determines if a task is overdue.
5
7
  * Done tasks are never overdue.
@@ -12,11 +14,32 @@ export function isTaskOverdue(dueDate: string | null, status: Status): boolean {
12
14
  const year = parts[0];
13
15
  const month = parts[1];
14
16
  const day = parts[2];
15
- if (!year || !month || !day) return false;
17
+ if (year === undefined || !month || !day) return false;
16
18
  const due = new Date(year, month - 1, day);
17
19
  return due < today;
18
20
  }
19
21
 
22
+ /**
23
+ * Returns the number of days until a task is due (0 = today), or null if:
24
+ * - No due date
25
+ * - Task is done
26
+ * - Task is already overdue
27
+ */
28
+ export function daysUntilDue(dueDate: string | null, status: Status): number | null {
29
+ if (!dueDate || status === "done") return null;
30
+ const today = new Date();
31
+ today.setHours(0, 0, 0, 0);
32
+ const parts = dueDate.split("-").map(Number);
33
+ const year = parts[0];
34
+ const month = parts[1];
35
+ const day = parts[2];
36
+ if (year === undefined || !month || !day) return null;
37
+ const due = new Date(year, month - 1, day);
38
+ const diffMs = due.getTime() - today.getTime();
39
+ if (diffMs < 0) return null; // already overdue
40
+ return Math.round(diffMs / (1000 * 60 * 60 * 24));
41
+ }
42
+
20
43
  /**
21
44
  * Format a timestamp into a human-readable relative time (e.g., "2d", "5h").
22
45
  */
package/src/types.ts CHANGED
@@ -51,7 +51,8 @@ export function parseId(id: string): { project: string; ref: string } | null {
51
51
  // Generate random 4-char ref (a-z0-9)
52
52
  export function generateRef(): string {
53
53
  const chars = "abcdefghijklmnopqrstuvwxyz0123456789"; // 36 chars
54
- const maxValid = 252; // 36 * 7 - reject >= 252 to avoid modulo bias
54
+ const charsetLen = chars.length; // 36
55
+ const maxValid = charsetLen * Math.floor(256 / charsetLen); // 252: reject >= maxValid to avoid modulo bias
55
56
  const result: string[] = [];
56
57
 
57
58
  while (result.length < 4) {
@@ -59,7 +60,7 @@ export function generateRef(): string {
59
60
  crypto.getRandomValues(bytes);
60
61
  for (const b of bytes) {
61
62
  if (b < maxValid && result.length < 4) {
62
- result.push(chars[b % 36]!);
63
+ result.push(chars[b % charsetLen]!);
63
64
  }
64
65
  }
65
66
  }
@@ -70,6 +71,7 @@ export interface TaskWithMeta extends Task {
70
71
  id: string; // computed from project-ref
71
72
  blocked_by_incomplete: boolean;
72
73
  is_overdue: boolean;
74
+ days_until_due: number | null;
73
75
  }
74
76
 
75
77
  export interface Config {
@@ -80,7 +82,6 @@ export interface Config {
80
82
  labels: string[];
81
83
  assignees: string[];
82
84
  };
83
- aliases: Record<string, string>; // alias -> path for auto-project detection
84
85
  clean_after: number | false; // days to keep done tasks, or false to disable
85
86
  }
86
87
 
@@ -92,7 +93,6 @@ export const DEFAULT_CONFIG: Config = {
92
93
  labels: [],
93
94
  assignees: [],
94
95
  },
95
- aliases: {},
96
96
  clean_after: 14,
97
97
  };
98
98