@obsfx/trekker 1.10.2 → 1.11.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 +92 -32
- package/dist/index.js +306 -109
- package/package.json +14 -14
package/README.md
CHANGED
|
@@ -31,6 +31,51 @@ Or with npm:
|
|
|
31
31
|
npm install -g @obsfx/trekker
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
## Claude Code Plugin
|
|
35
|
+
|
|
36
|
+
<img src="https://omercan.io/trekker/images/claude-color.png" width="24" height="24" alt="Claude" style="vertical-align: middle;" />
|
|
37
|
+
|
|
38
|
+
Install the [trekker-claude-code](https://github.com/obsfx/trekker-claude-code) plugin for seamless integration with [Claude Code](https://claude.ai/code):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
claude plugin marketplace add obsfx/trekker-claude-code
|
|
42
|
+
claude plugin install trekker
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This gives Claude Code native access to Trekker through **26 MCP tools**, **13 slash commands**, **7 skills**, **5 lifecycle hooks**, and an **autonomous task agent**.
|
|
46
|
+
|
|
47
|
+
**Key features:**
|
|
48
|
+
|
|
49
|
+
- Persistent task memory across sessions via SQLite
|
|
50
|
+
- Search-first workflow to restore context
|
|
51
|
+
- 7 skills for guided workflows and best practices
|
|
52
|
+
- 5 lifecycle hooks for automatic state management
|
|
53
|
+
- Autonomous task agent for discovery and completion
|
|
54
|
+
- Blocks internal TaskCreate/TodoWrite — enforces Trekker
|
|
55
|
+
- Multi-instance safe with conflict handling
|
|
56
|
+
|
|
57
|
+
See the [plugin repository](https://github.com/obsfx/trekker-claude-code) for the full list of slash commands, hooks, skills, and agent details.
|
|
58
|
+
|
|
59
|
+
## Codex Plugin
|
|
60
|
+
|
|
61
|
+
<img width="36" height="36" alt="OpenAI-black-monoblossom" src="https://github.com/user-attachments/assets/e761c75d-6012-44b0-bfbe-c6cc235b54b4" />
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
Support for the **Codex** plugin is still in a *very early stage* and is continuing to evolve. However, we already have a plugin for **Trekker** that you can install locally. Please follow the [plugin repository](https://github.com/obsfx/trekker-codex) and the instructions at https://developers.openai.com/codex/plugins/build#how-codex-uses-marketplaces, and you can get features similar to those available in the *Claude Code* plugin.
|
|
65
|
+
|
|
66
|
+
## Web Interface
|
|
67
|
+
|
|
68
|
+
<img src="https://omercan.io/trekker/images/dashboard-kanban.png" width="480" alt="trekker dashboard" style="vertical-align: middle;" />
|
|
69
|
+
|
|
70
|
+
For a visual kanban board, install the separate dashboard package:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm install -g @obsfx/trekker-dashboard
|
|
74
|
+
trekker-dashboard -p 3000 # Start dashboard on port 3000
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
You can find the detail in its [trekker-dashboard](https://github.com/obsfx/trekker-dashboard) repository.
|
|
78
|
+
|
|
34
79
|
## Why Trekker
|
|
35
80
|
|
|
36
81
|
AI coding agents work better when they can track their own progress. A simple CLI-based task manager keeps them on the right path across sessions.
|
|
@@ -40,6 +85,7 @@ I built this after using beads for a while. Beads does the job, but its codebase
|
|
|
40
85
|
My concerns about the future and security of that project led me here. Trekker is my simplified alternative.
|
|
41
86
|
|
|
42
87
|
What you get:
|
|
88
|
+
|
|
43
89
|
- Task and epic tracking with dependencies
|
|
44
90
|
- Ready command to find unblocked tasks and see what they unblock
|
|
45
91
|
- Full-text search across tasks, epics, subtasks, and comments
|
|
@@ -56,6 +102,8 @@ Initialize Trekker in your project:
|
|
|
56
102
|
|
|
57
103
|
```bash
|
|
58
104
|
trekker init
|
|
105
|
+
# or set custom prefixes up front
|
|
106
|
+
trekker init --issue-prefix FEAT --epic-prefix PLAN --comment-prefix NOTE
|
|
59
107
|
```
|
|
60
108
|
|
|
61
109
|
Create an epic for your feature:
|
|
@@ -67,16 +115,16 @@ trekker epic create -t "User Authentication" -d "JWT-based auth with login and r
|
|
|
67
115
|
Add tasks to the epic:
|
|
68
116
|
|
|
69
117
|
```bash
|
|
70
|
-
trekker task create -t "Create user model" -e
|
|
71
|
-
trekker task create -t "Build login endpoint" -e
|
|
72
|
-
trekker task create -t "Build registration endpoint" -e
|
|
118
|
+
trekker task create -t "Create user model" -e <epic-id>
|
|
119
|
+
trekker task create -t "Build login endpoint" -e <epic-id>
|
|
120
|
+
trekker task create -t "Build registration endpoint" -e <epic-id>
|
|
73
121
|
```
|
|
74
122
|
|
|
75
123
|
Set dependencies between tasks:
|
|
76
124
|
|
|
77
125
|
```bash
|
|
78
|
-
trekker dep add
|
|
79
|
-
trekker dep add
|
|
126
|
+
trekker dep add <task-id> <depends-on-id>
|
|
127
|
+
trekker dep add <task-id> <depends-on-id>
|
|
80
128
|
```
|
|
81
129
|
|
|
82
130
|
See what is ready to work on:
|
|
@@ -88,8 +136,8 @@ trekker ready
|
|
|
88
136
|
Update task status as you work:
|
|
89
137
|
|
|
90
138
|
```bash
|
|
91
|
-
trekker task update
|
|
92
|
-
trekker task update
|
|
139
|
+
trekker task update <task-id> -s in_progress
|
|
140
|
+
trekker task update <task-id> -s completed
|
|
93
141
|
```
|
|
94
142
|
|
|
95
143
|
## Commands
|
|
@@ -98,10 +146,32 @@ trekker task update TREK-1 -s completed
|
|
|
98
146
|
|
|
99
147
|
```bash
|
|
100
148
|
trekker init # Initialize in current directory
|
|
149
|
+
trekker init --issue-prefix FEAT --epic-prefix PLAN --comment-prefix NOTE
|
|
101
150
|
trekker wipe # Delete all data
|
|
102
151
|
trekker quickstart # Show full documentation
|
|
103
152
|
```
|
|
104
153
|
|
|
154
|
+
### Project Config
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
trekker config list
|
|
158
|
+
trekker config get issue_prefix
|
|
159
|
+
trekker config set issue_prefix FEAT
|
|
160
|
+
trekker config set epic_prefix PLAN
|
|
161
|
+
trekker config set comment_prefix NOTE
|
|
162
|
+
trekker config unset issue_prefix
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Supported keys:
|
|
166
|
+
|
|
167
|
+
- `issue_prefix` for tasks and subtasks
|
|
168
|
+
- `epic_prefix` for epics
|
|
169
|
+
- `comment_prefix` for comments
|
|
170
|
+
|
|
171
|
+
Use `issue_prefix` when you want task IDs to match your project naming scheme. For example, `trekker init --issue-prefix FEAT` starts new task and subtask IDs as `FEAT-1`, `FEAT-2`, and so on. You can also change it later with `trekker config set issue_prefix BUG`, which only affects newly created tasks and subtasks.
|
|
172
|
+
|
|
173
|
+
Prefix values are normalized to uppercase, must start with a letter, may contain numbers, and must be unique across the three families. Changing a prefix affects only newly created IDs. Existing IDs are never rewritten.
|
|
174
|
+
|
|
105
175
|
### Epics
|
|
106
176
|
|
|
107
177
|
```bash
|
|
@@ -158,6 +228,7 @@ trekker ready [--limit <n>] [--page <n>]
|
|
|
158
228
|
```
|
|
159
229
|
|
|
160
230
|
Example output:
|
|
231
|
+
|
|
161
232
|
```
|
|
162
233
|
2 ready task(s):
|
|
163
234
|
|
|
@@ -168,6 +239,7 @@ TREK-4 | P2 | Write docs
|
|
|
168
239
|
```
|
|
169
240
|
|
|
170
241
|
Tasks are sorted by priority (critical first). A task is considered ready when:
|
|
242
|
+
|
|
171
243
|
- Status is `todo`
|
|
172
244
|
- It is a top-level task (not a subtask)
|
|
173
245
|
- All its dependencies are resolved (`completed`, `wont_fix`, or `archived`)
|
|
@@ -181,6 +253,7 @@ trekker search <query> [--type <types>] [--status <status>] [--limit <n>] [--pag
|
|
|
181
253
|
```
|
|
182
254
|
|
|
183
255
|
Examples:
|
|
256
|
+
|
|
184
257
|
```bash
|
|
185
258
|
trekker search "authentication" # Search all entities
|
|
186
259
|
trekker search "bug fix" --type task,subtask # Search only tasks and subtasks
|
|
@@ -196,9 +269,10 @@ trekker history [--entity <id>] [--type <types>] [--action <actions>] [--since <
|
|
|
196
269
|
```
|
|
197
270
|
|
|
198
271
|
Examples:
|
|
272
|
+
|
|
199
273
|
```bash
|
|
200
274
|
trekker history # All events
|
|
201
|
-
trekker history --entity
|
|
275
|
+
trekker history --entity <entity-id> # Events for specific entity
|
|
202
276
|
trekker history --type task --action update # Only task updates
|
|
203
277
|
trekker history --since 2025-01-01 --limit 20 # Events after date
|
|
204
278
|
```
|
|
@@ -212,6 +286,7 @@ trekker list [--type <types>] [--status <statuses>] [--priority <levels>] [--sor
|
|
|
212
286
|
```
|
|
213
287
|
|
|
214
288
|
Examples:
|
|
289
|
+
|
|
215
290
|
```bash
|
|
216
291
|
trekker list # All items, newest first
|
|
217
292
|
trekker list --type task --status in_progress # Active tasks only
|
|
@@ -219,29 +294,9 @@ trekker list --priority 0,1 --sort priority:asc # Critical/high priority first
|
|
|
219
294
|
trekker list --sort title:asc,created:desc # Sort by title, then by date
|
|
220
295
|
```
|
|
221
296
|
|
|
222
|
-
### Web Interface
|
|
223
|
-
|
|
224
|
-
For a visual kanban board, install the separate dashboard package:
|
|
225
|
-
|
|
226
|
-
```bash
|
|
227
|
-
npm install -g @obsfx/trekker-dashboard
|
|
228
|
-
trekker-dashboard -p 3000 # Start dashboard on port 3000
|
|
229
|
-
```
|
|
230
297
|
|
|
231
298
|
The dashboard shows tasks grouped by status and reads from the same `.trekker/trekker.db` database.
|
|
232
|
-
|
|
233
|
-
## Claude Code Integration
|
|
234
|
-
|
|
235
|
-
For seamless integration with [Claude Code](https://claude.ai/code), install the Trekker plugin:
|
|
236
|
-
|
|
237
|
-
```bash
|
|
238
|
-
claude /plugin marketplace add obsfx/trekker-claude-code
|
|
239
|
-
claude /plugin install trekker
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
This gives Claude Code native access to Trekker commands through MCP. The agent can create tasks, update status, and manage dependencies without running CLI commands directly.
|
|
243
|
-
|
|
244
|
-
See [trekker-claude-code](https://github.com/obsfx/trekker-claude-code) for more details.
|
|
299
|
+
It also lets you update issue, epic, and comment prefixes from the UI. Those changes affect only newly created IDs.
|
|
245
300
|
|
|
246
301
|
## TOON Output
|
|
247
302
|
|
|
@@ -249,7 +304,7 @@ Add the `--toon` flag to any command for structured output in [TOON format](http
|
|
|
249
304
|
|
|
250
305
|
```bash
|
|
251
306
|
trekker --toon task list
|
|
252
|
-
trekker --toon task show
|
|
307
|
+
trekker --toon task show <task-id>
|
|
253
308
|
```
|
|
254
309
|
|
|
255
310
|
## Status Values
|
|
@@ -267,12 +322,16 @@ Epics: `todo`, `in_progress`, `completed`, `archived`
|
|
|
267
322
|
- 4: Backlog
|
|
268
323
|
- 5: Someday
|
|
269
324
|
|
|
270
|
-
## ID
|
|
325
|
+
## ID Prefixes
|
|
326
|
+
|
|
327
|
+
Default prefixes:
|
|
271
328
|
|
|
272
329
|
- Epics: `EPIC-1`, `EPIC-2`
|
|
273
|
-
- Tasks: `TREK-1`, `TREK-2`
|
|
330
|
+
- Tasks and subtasks: `TREK-1`, `TREK-2`
|
|
274
331
|
- Comments: `CMT-1`, `CMT-2`
|
|
275
332
|
|
|
333
|
+
These defaults are project-scoped and configurable through `trekker init --*-prefix` or `trekker config set`. Prefix changes affect only new entities and existing IDs keep their original values.
|
|
334
|
+
|
|
276
335
|
All list commands default to 50 items per page, sorted by newest first. Use `--limit` and `--page` to paginate through large result sets.
|
|
277
336
|
|
|
278
337
|
## Development
|
|
@@ -308,6 +367,7 @@ This is my personal workflow for getting the most out of Trekker with AI agents:
|
|
|
308
367
|
- **Use the dashboard for visibility.** I run [trekker-dashboard](https://github.com/obsfx/trekker-dashboard) to visually track what the agent is doing. It shows tasks on a kanban board and auto-refreshes, so I can monitor progress in real-time.
|
|
309
368
|
|
|
310
369
|
Example prompt snippet:
|
|
370
|
+
|
|
311
371
|
```
|
|
312
372
|
Use trekker to track your work. Run `trekker quickstart` if you need to learn how it works.
|
|
313
373
|
```
|
package/dist/index.js
CHANGED
|
@@ -455,7 +455,7 @@ var require_customParseFormat = __commonJS((exports, module) => {
|
|
|
455
455
|
});
|
|
456
456
|
|
|
457
457
|
// src/index.ts
|
|
458
|
-
import { Command as
|
|
458
|
+
import { Command as Command15 } from "commander";
|
|
459
459
|
|
|
460
460
|
// src/commands/init.ts
|
|
461
461
|
import { Command } from "commander";
|
|
@@ -471,6 +471,7 @@ __export(exports_schema, {
|
|
|
471
471
|
tasks: () => tasks,
|
|
472
472
|
projectsRelations: () => projectsRelations,
|
|
473
473
|
projects: () => projects,
|
|
474
|
+
projectConfig: () => projectConfig,
|
|
474
475
|
idCounters: () => idCounters,
|
|
475
476
|
events: () => events,
|
|
476
477
|
epicsRelations: () => epicsRelations,
|
|
@@ -488,6 +489,10 @@ var projects = sqliteTable("projects", {
|
|
|
488
489
|
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
489
490
|
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull()
|
|
490
491
|
});
|
|
492
|
+
var projectConfig = sqliteTable("project_config", {
|
|
493
|
+
key: text("key").primaryKey(),
|
|
494
|
+
value: text("value").notNull()
|
|
495
|
+
});
|
|
491
496
|
var epics = sqliteTable("epics", {
|
|
492
497
|
id: text("id").primaryKey(),
|
|
493
498
|
projectId: text("project_id").notNull(),
|
|
@@ -590,6 +595,35 @@ var dependenciesRelations = relations(dependencies, ({ one }) => ({
|
|
|
590
595
|
// src/db/client.ts
|
|
591
596
|
import { existsSync, mkdirSync, rmSync } from "fs";
|
|
592
597
|
import { join } from "path";
|
|
598
|
+
|
|
599
|
+
// src/types/index.ts
|
|
600
|
+
var TASK_STATUSES = ["todo", "in_progress", "completed", "wont_fix", "archived"];
|
|
601
|
+
var EPIC_STATUSES = ["todo", "in_progress", "completed", "archived"];
|
|
602
|
+
var DEFAULT_PRIORITY = 2;
|
|
603
|
+
var DEFAULT_TASK_STATUS = "todo";
|
|
604
|
+
var DEFAULT_EPIC_STATUS = "todo";
|
|
605
|
+
var PAGINATION_DEFAULTS = {
|
|
606
|
+
LIST_PAGE_SIZE: 50,
|
|
607
|
+
SEARCH_PAGE_SIZE: 20,
|
|
608
|
+
HISTORY_PAGE_SIZE: 50,
|
|
609
|
+
DEFAULT_PAGE: 1
|
|
610
|
+
};
|
|
611
|
+
var VALID_SORT_FIELDS = ["created", "updated", "title", "priority", "status"];
|
|
612
|
+
var LIST_ENTITY_TYPES = ["epic", "task", "subtask"];
|
|
613
|
+
var SEARCH_ENTITY_TYPES = ["epic", "task", "subtask", "comment"];
|
|
614
|
+
var PROJECT_CONFIG_KEYS = ["issue_prefix", "epic_prefix", "comment_prefix"];
|
|
615
|
+
var PROJECT_CONFIG_DEFAULTS = {
|
|
616
|
+
issue_prefix: "TREK",
|
|
617
|
+
epic_prefix: "EPIC",
|
|
618
|
+
comment_prefix: "CMT"
|
|
619
|
+
};
|
|
620
|
+
var ENTITY_CONFIG_KEY_MAP = {
|
|
621
|
+
task: "issue_prefix",
|
|
622
|
+
epic: "epic_prefix",
|
|
623
|
+
comment: "comment_prefix"
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
// src/db/client.ts
|
|
593
627
|
var TREKKER_DIR = ".trekker";
|
|
594
628
|
var DB_NAME = "trekker.db";
|
|
595
629
|
function getTrekkerDir(cwd = process.cwd()) {
|
|
@@ -621,6 +655,7 @@ function getDb(cwd = process.cwd()) {
|
|
|
621
655
|
dbInstance = drizzle(sqliteInstance, { schema: exports_schema });
|
|
622
656
|
migrateSearchIndex(sqliteInstance);
|
|
623
657
|
migrateHistoryTable(sqliteInstance);
|
|
658
|
+
migrateProjectConfigTable(sqliteInstance);
|
|
624
659
|
return dbInstance;
|
|
625
660
|
}
|
|
626
661
|
function createDb(cwd = process.cwd()) {
|
|
@@ -636,6 +671,12 @@ function createDb(cwd = process.cwd()) {
|
|
|
636
671
|
updated_at INTEGER NOT NULL
|
|
637
672
|
)
|
|
638
673
|
`);
|
|
674
|
+
sqliteInstance.run(`
|
|
675
|
+
CREATE TABLE IF NOT EXISTS project_config (
|
|
676
|
+
key TEXT PRIMARY KEY,
|
|
677
|
+
value TEXT NOT NULL
|
|
678
|
+
)
|
|
679
|
+
`);
|
|
639
680
|
sqliteInstance.run(`
|
|
640
681
|
CREATE TABLE IF NOT EXISTS epics (
|
|
641
682
|
id TEXT PRIMARY KEY,
|
|
@@ -690,6 +731,7 @@ function createDb(cwd = process.cwd()) {
|
|
|
690
731
|
sqliteInstance.run("INSERT OR IGNORE INTO id_counters (entity_type, counter) VALUES ('task', 0)");
|
|
691
732
|
sqliteInstance.run("INSERT OR IGNORE INTO id_counters (entity_type, counter) VALUES ('epic', 0)");
|
|
692
733
|
sqliteInstance.run("INSERT OR IGNORE INTO id_counters (entity_type, counter) VALUES ('comment', 0)");
|
|
734
|
+
seedProjectConfig(sqliteInstance);
|
|
693
735
|
sqliteInstance.run(`
|
|
694
736
|
CREATE TABLE IF NOT EXISTS events (
|
|
695
737
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -953,6 +995,20 @@ function migrateHistoryTable(sqlite) {
|
|
|
953
995
|
createHistoryTriggers(sqlite);
|
|
954
996
|
}
|
|
955
997
|
}
|
|
998
|
+
function seedProjectConfig(sqlite) {
|
|
999
|
+
for (const [key, value] of Object.entries(PROJECT_CONFIG_DEFAULTS)) {
|
|
1000
|
+
sqlite.query("INSERT OR IGNORE INTO project_config (key, value) VALUES (?, ?)").run(key, value);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
function migrateProjectConfigTable(sqlite) {
|
|
1004
|
+
sqlite.run(`
|
|
1005
|
+
CREATE TABLE IF NOT EXISTS project_config (
|
|
1006
|
+
key TEXT PRIMARY KEY,
|
|
1007
|
+
value TEXT NOT NULL
|
|
1008
|
+
)
|
|
1009
|
+
`);
|
|
1010
|
+
seedProjectConfig(sqlite);
|
|
1011
|
+
}
|
|
956
1012
|
function rebuildSearchIndex() {
|
|
957
1013
|
getDb();
|
|
958
1014
|
const sqlite = getSqliteInstance();
|
|
@@ -988,35 +1044,106 @@ function deleteDb(cwd = process.cwd()) {
|
|
|
988
1044
|
}
|
|
989
1045
|
|
|
990
1046
|
// src/utils/id-generator.ts
|
|
991
|
-
import { eq, sql } from "drizzle-orm";
|
|
1047
|
+
import { eq as eq2, sql } from "drizzle-orm";
|
|
992
1048
|
|
|
993
|
-
// src/
|
|
994
|
-
|
|
995
|
-
var
|
|
996
|
-
var
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1049
|
+
// src/services/config.ts
|
|
1050
|
+
import { eq } from "drizzle-orm";
|
|
1051
|
+
var PROJECT_CONFIG_KEY_SET = new Set(PROJECT_CONFIG_KEYS);
|
|
1052
|
+
var PREFIX_PATTERN = /^[A-Z][A-Z0-9]*$/;
|
|
1053
|
+
function assertProjectConfigKey(key) {
|
|
1054
|
+
if (!PROJECT_CONFIG_KEY_SET.has(key)) {
|
|
1055
|
+
throw new Error(`Unsupported config key: ${key}. Valid keys: ${PROJECT_CONFIG_KEYS.join(", ")}`);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
function normalizePrefixValue(value) {
|
|
1059
|
+
const normalized = value.trim().toUpperCase();
|
|
1060
|
+
if (!normalized) {
|
|
1061
|
+
throw new Error("Prefix value is required.");
|
|
1062
|
+
}
|
|
1063
|
+
if (!PREFIX_PATTERN.test(normalized)) {
|
|
1064
|
+
throw new Error("Invalid prefix value. Use uppercase letters and numbers only, and start with a letter.");
|
|
1065
|
+
}
|
|
1066
|
+
return normalized;
|
|
1067
|
+
}
|
|
1068
|
+
function validateUniquePrefixes(config) {
|
|
1069
|
+
const seen = new Map;
|
|
1070
|
+
for (const key of PROJECT_CONFIG_KEYS) {
|
|
1071
|
+
const prefix = config[key];
|
|
1072
|
+
const existingKey = seen.get(prefix);
|
|
1073
|
+
if (existingKey) {
|
|
1074
|
+
throw new Error(`Prefix values must be unique. ${existingKey} and ${key} cannot both use ${prefix}.`);
|
|
1075
|
+
}
|
|
1076
|
+
seen.set(prefix, key);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
function resolveProjectConfig(updates, baseConfig = PROJECT_CONFIG_DEFAULTS) {
|
|
1080
|
+
const normalizedUpdates = {};
|
|
1081
|
+
if (updates) {
|
|
1082
|
+
for (const rawKey of PROJECT_CONFIG_KEYS) {
|
|
1083
|
+
const rawValue = updates[rawKey];
|
|
1084
|
+
if (rawValue === undefined) {
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
normalizedUpdates[rawKey] = normalizePrefixValue(rawValue);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
const nextConfig = {
|
|
1091
|
+
...baseConfig,
|
|
1092
|
+
...normalizedUpdates
|
|
1093
|
+
};
|
|
1094
|
+
validateUniquePrefixes(nextConfig);
|
|
1095
|
+
return nextConfig;
|
|
1096
|
+
}
|
|
1097
|
+
function listProjectConfig() {
|
|
1098
|
+
const db = getDb();
|
|
1099
|
+
const rows = db.select().from(projectConfig).all();
|
|
1100
|
+
const config = { ...PROJECT_CONFIG_DEFAULTS };
|
|
1101
|
+
for (const row of rows) {
|
|
1102
|
+
if (!PROJECT_CONFIG_KEY_SET.has(row.key)) {
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
assertProjectConfigKey(row.key);
|
|
1106
|
+
config[row.key] = row.value;
|
|
1107
|
+
}
|
|
1108
|
+
return config;
|
|
1109
|
+
}
|
|
1110
|
+
function listProjectConfigEntries() {
|
|
1111
|
+
const config = listProjectConfig();
|
|
1112
|
+
return PROJECT_CONFIG_KEYS.map((key) => ({ key, value: config[key] }));
|
|
1113
|
+
}
|
|
1114
|
+
function getProjectConfigValue(key) {
|
|
1115
|
+
return listProjectConfig()[key];
|
|
1116
|
+
}
|
|
1117
|
+
function upsertProjectConfigValue(key, value) {
|
|
1118
|
+
const db = getDb();
|
|
1119
|
+
const existing = db.select().from(projectConfig).where(eq(projectConfig.key, key)).get();
|
|
1120
|
+
if (existing) {
|
|
1121
|
+
db.update(projectConfig).set({ value }).where(eq(projectConfig.key, key)).run();
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
db.insert(projectConfig).values({ key, value }).run();
|
|
1125
|
+
}
|
|
1126
|
+
function setProjectConfigValues(updates) {
|
|
1127
|
+
const currentConfig = listProjectConfig();
|
|
1128
|
+
const nextConfig = resolveProjectConfig(updates, currentConfig);
|
|
1129
|
+
for (const key of PROJECT_CONFIG_KEYS) {
|
|
1130
|
+
const nextValue = nextConfig[key];
|
|
1131
|
+
if (nextValue !== currentConfig[key]) {
|
|
1132
|
+
upsertProjectConfigValue(key, nextValue);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
return nextConfig;
|
|
1136
|
+
}
|
|
1137
|
+
function unsetProjectConfigValue(key) {
|
|
1138
|
+
return setProjectConfigValues({ [key]: PROJECT_CONFIG_DEFAULTS[key] })[key];
|
|
1139
|
+
}
|
|
1013
1140
|
|
|
1014
1141
|
// src/utils/id-generator.ts
|
|
1015
1142
|
function generateId(entityType) {
|
|
1016
1143
|
const db = getDb();
|
|
1017
|
-
const prefix =
|
|
1018
|
-
db.update(idCounters).set({ counter: sql`${idCounters.counter} + 1` }).where(
|
|
1019
|
-
const result = db.select({ counter: idCounters.counter }).from(idCounters).where(
|
|
1144
|
+
const prefix = getProjectConfigValue(ENTITY_CONFIG_KEY_MAP[entityType]);
|
|
1145
|
+
db.update(idCounters).set({ counter: sql`${idCounters.counter} + 1` }).where(eq2(idCounters.entityType, entityType)).run();
|
|
1146
|
+
const result = db.select({ counter: idCounters.counter }).from(idCounters).where(eq2(idCounters.entityType, entityType)).get();
|
|
1020
1147
|
if (!result) {
|
|
1021
1148
|
throw new Error(`Counter not found for entity type: ${entityType}`);
|
|
1022
1149
|
}
|
|
@@ -1028,10 +1155,13 @@ function generateUuid() {
|
|
|
1028
1155
|
|
|
1029
1156
|
// src/services/project.ts
|
|
1030
1157
|
import { basename } from "path";
|
|
1031
|
-
function initProject(cwd = process.cwd()) {
|
|
1158
|
+
function initProject(cwd = process.cwd(), configOverrides) {
|
|
1032
1159
|
if (isTrekkerInitialized(cwd)) {
|
|
1033
1160
|
throw new Error("Trekker is already initialized in this directory.");
|
|
1034
1161
|
}
|
|
1162
|
+
if (configOverrides) {
|
|
1163
|
+
resolveProjectConfig(configOverrides);
|
|
1164
|
+
}
|
|
1035
1165
|
const db = createDb(cwd);
|
|
1036
1166
|
const projectName = basename(cwd);
|
|
1037
1167
|
const now = new Date;
|
|
@@ -1041,6 +1171,9 @@ function initProject(cwd = process.cwd()) {
|
|
|
1041
1171
|
createdAt: now,
|
|
1042
1172
|
updatedAt: now
|
|
1043
1173
|
}).run();
|
|
1174
|
+
if (configOverrides) {
|
|
1175
|
+
setProjectConfigValues(configOverrides);
|
|
1176
|
+
}
|
|
1044
1177
|
}
|
|
1045
1178
|
function wipeProject(cwd = process.cwd()) {
|
|
1046
1179
|
if (!isTrekkerInitialized(cwd)) {
|
|
@@ -1734,15 +1867,26 @@ function formatPaginatedCommentList(result) {
|
|
|
1734
1867
|
return lines.join(`
|
|
1735
1868
|
`);
|
|
1736
1869
|
}
|
|
1870
|
+
function formatProjectConfigList(entries) {
|
|
1871
|
+
if (entries.length === 0) {
|
|
1872
|
+
return "No config values found.";
|
|
1873
|
+
}
|
|
1874
|
+
return entries.map((entry) => `${entry.key}=${entry.value}`).join(`
|
|
1875
|
+
`);
|
|
1876
|
+
}
|
|
1737
1877
|
|
|
1738
1878
|
// src/commands/init.ts
|
|
1739
|
-
var initCommand = new Command("init").description("Initialize Trekker in the current directory").action(() => {
|
|
1879
|
+
var initCommand = new Command("init").description("Initialize Trekker in the current directory").option("--issue-prefix <value>", "Prefix for tasks and subtasks (default: TREK)").option("--epic-prefix <value>", "Prefix for epics (default: EPIC)").option("--comment-prefix <value>", "Prefix for comments (default: CMT)").action((options) => {
|
|
1740
1880
|
try {
|
|
1741
1881
|
if (isTrekkerInitialized()) {
|
|
1742
1882
|
error("Trekker is already initialized in this directory.");
|
|
1743
1883
|
process.exit(1);
|
|
1744
1884
|
}
|
|
1745
|
-
initProject()
|
|
1885
|
+
initProject(process.cwd(), {
|
|
1886
|
+
issue_prefix: options.issuePrefix,
|
|
1887
|
+
epic_prefix: options.epicPrefix,
|
|
1888
|
+
comment_prefix: options.commentPrefix
|
|
1889
|
+
});
|
|
1746
1890
|
success("Trekker initialized successfully.");
|
|
1747
1891
|
} catch (err) {
|
|
1748
1892
|
handleCommandError(err);
|
|
@@ -1788,7 +1932,7 @@ function confirm(prompt) {
|
|
|
1788
1932
|
import { Command as Command3 } from "commander";
|
|
1789
1933
|
|
|
1790
1934
|
// src/services/epic.ts
|
|
1791
|
-
import { eq as
|
|
1935
|
+
import { eq as eq3, and, isNull, desc, sql as sql2 } from "drizzle-orm";
|
|
1792
1936
|
function createEpic(input) {
|
|
1793
1937
|
const db = getDb();
|
|
1794
1938
|
const project = db.select().from(projects).get();
|
|
@@ -1812,7 +1956,7 @@ function createEpic(input) {
|
|
|
1812
1956
|
}
|
|
1813
1957
|
function getEpic(id) {
|
|
1814
1958
|
const db = getDb();
|
|
1815
|
-
return db.select().from(epics).where(
|
|
1959
|
+
return db.select().from(epics).where(eq3(epics.id, id)).get();
|
|
1816
1960
|
}
|
|
1817
1961
|
function listEpics(options) {
|
|
1818
1962
|
const db = getDb();
|
|
@@ -1821,7 +1965,7 @@ function listEpics(options) {
|
|
|
1821
1965
|
const offset = (page - 1) * limit;
|
|
1822
1966
|
let where;
|
|
1823
1967
|
if (options?.status) {
|
|
1824
|
-
where =
|
|
1968
|
+
where = eq3(epics.status, options.status);
|
|
1825
1969
|
}
|
|
1826
1970
|
const countRow = db.select({ count: sql2`count(*)` }).from(epics).where(where).get();
|
|
1827
1971
|
const total = countRow?.count ?? 0;
|
|
@@ -1849,7 +1993,7 @@ function updateEpic(id, input) {
|
|
|
1849
1993
|
if (input.priority !== undefined) {
|
|
1850
1994
|
updates.priority = input.priority;
|
|
1851
1995
|
}
|
|
1852
|
-
db.update(epics).set(updates).where(
|
|
1996
|
+
db.update(epics).set(updates).where(eq3(epics.id, id)).run();
|
|
1853
1997
|
const updated = getEpic(id);
|
|
1854
1998
|
if (!updated) {
|
|
1855
1999
|
throw new Error(`Epic not found after update: ${id}`);
|
|
@@ -1862,7 +2006,7 @@ function deleteEpic(id) {
|
|
|
1862
2006
|
if (!existing) {
|
|
1863
2007
|
throw new Error(`Epic not found: ${id}`);
|
|
1864
2008
|
}
|
|
1865
|
-
db.delete(epics).where(
|
|
2009
|
+
db.delete(epics).where(eq3(epics.id, id)).run();
|
|
1866
2010
|
}
|
|
1867
2011
|
function completeEpic(id) {
|
|
1868
2012
|
const db = getDb();
|
|
@@ -1873,20 +2017,20 @@ function completeEpic(id) {
|
|
|
1873
2017
|
if (existing.status === "completed") {
|
|
1874
2018
|
throw new Error(`Epic is already completed: ${id}`);
|
|
1875
2019
|
}
|
|
1876
|
-
const epicTasks = db.select().from(tasks).where(and(
|
|
2020
|
+
const epicTasks = db.select().from(tasks).where(and(eq3(tasks.epicId, id), isNull(tasks.parentTaskId))).all();
|
|
1877
2021
|
const taskIds = epicTasks.map((t) => t.id);
|
|
1878
2022
|
let subtaskCount = 0;
|
|
1879
2023
|
if (taskIds.length > 0) {
|
|
1880
2024
|
for (const taskId of taskIds) {
|
|
1881
|
-
const subtasks = db.select().from(tasks).where(
|
|
2025
|
+
const subtasks = db.select().from(tasks).where(eq3(tasks.parentTaskId, taskId)).all();
|
|
1882
2026
|
subtaskCount += subtasks.length;
|
|
1883
2027
|
if (subtasks.length > 0) {
|
|
1884
|
-
db.update(tasks).set({ status: "archived", updatedAt: new Date }).where(
|
|
2028
|
+
db.update(tasks).set({ status: "archived", updatedAt: new Date }).where(eq3(tasks.parentTaskId, taskId)).run();
|
|
1885
2029
|
}
|
|
1886
2030
|
}
|
|
1887
|
-
db.update(tasks).set({ status: "archived", updatedAt: new Date }).where(and(
|
|
2031
|
+
db.update(tasks).set({ status: "archived", updatedAt: new Date }).where(and(eq3(tasks.epicId, id), isNull(tasks.parentTaskId))).run();
|
|
1888
2032
|
}
|
|
1889
|
-
db.update(epics).set({ status: "completed", updatedAt: new Date }).where(
|
|
2033
|
+
db.update(epics).set({ status: "completed", updatedAt: new Date }).where(eq3(epics.id, id)).run();
|
|
1890
2034
|
return {
|
|
1891
2035
|
epic: id,
|
|
1892
2036
|
status: "completed",
|
|
@@ -2082,7 +2226,7 @@ epicCommand.command("complete <epic-id>").description("Complete an epic and arch
|
|
|
2082
2226
|
import { Command as Command4 } from "commander";
|
|
2083
2227
|
|
|
2084
2228
|
// src/services/task.ts
|
|
2085
|
-
import { eq as
|
|
2229
|
+
import { eq as eq4, and as and2, isNull as isNull2, desc as desc2, sql as sql3 } from "drizzle-orm";
|
|
2086
2230
|
function createTask(input) {
|
|
2087
2231
|
const db = getDb();
|
|
2088
2232
|
const project = db.select().from(projects).get();
|
|
@@ -2090,13 +2234,13 @@ function createTask(input) {
|
|
|
2090
2234
|
throw new Error("Project not found. Run 'trekker init' first.");
|
|
2091
2235
|
}
|
|
2092
2236
|
if (input.epicId) {
|
|
2093
|
-
const epic = db.select().from(epics).where(
|
|
2237
|
+
const epic = db.select().from(epics).where(eq4(epics.id, input.epicId)).get();
|
|
2094
2238
|
if (!epic) {
|
|
2095
2239
|
throw new Error(`Epic not found: ${input.epicId}`);
|
|
2096
2240
|
}
|
|
2097
2241
|
}
|
|
2098
2242
|
if (input.parentTaskId) {
|
|
2099
|
-
const parent = db.select().from(tasks).where(
|
|
2243
|
+
const parent = db.select().from(tasks).where(eq4(tasks.id, input.parentTaskId)).get();
|
|
2100
2244
|
if (!parent) {
|
|
2101
2245
|
throw new Error(`Parent task not found: ${input.parentTaskId}`);
|
|
2102
2246
|
}
|
|
@@ -2121,7 +2265,7 @@ function createTask(input) {
|
|
|
2121
2265
|
}
|
|
2122
2266
|
function getTask(id) {
|
|
2123
2267
|
const db = getDb();
|
|
2124
|
-
return db.select().from(tasks).where(
|
|
2268
|
+
return db.select().from(tasks).where(eq4(tasks.id, id)).get();
|
|
2125
2269
|
}
|
|
2126
2270
|
function listTasks(options) {
|
|
2127
2271
|
const db = getDb();
|
|
@@ -2130,15 +2274,15 @@ function listTasks(options) {
|
|
|
2130
2274
|
const offset = (page - 1) * limit;
|
|
2131
2275
|
const conditions = [];
|
|
2132
2276
|
if (options?.status) {
|
|
2133
|
-
conditions.push(
|
|
2277
|
+
conditions.push(eq4(tasks.status, options.status));
|
|
2134
2278
|
}
|
|
2135
2279
|
if (options?.epicId) {
|
|
2136
|
-
conditions.push(
|
|
2280
|
+
conditions.push(eq4(tasks.epicId, options.epicId));
|
|
2137
2281
|
}
|
|
2138
2282
|
if (options?.parentTaskId === null) {
|
|
2139
2283
|
conditions.push(isNull2(tasks.parentTaskId));
|
|
2140
2284
|
} else if (options?.parentTaskId) {
|
|
2141
|
-
conditions.push(
|
|
2285
|
+
conditions.push(eq4(tasks.parentTaskId, options.parentTaskId));
|
|
2142
2286
|
}
|
|
2143
2287
|
let where;
|
|
2144
2288
|
if (conditions.length > 0) {
|
|
@@ -2154,7 +2298,7 @@ function listSubtasks(parentTaskId, options) {
|
|
|
2154
2298
|
const limit = options?.limit ?? PAGINATION_DEFAULTS.LIST_PAGE_SIZE;
|
|
2155
2299
|
const page = options?.page ?? PAGINATION_DEFAULTS.DEFAULT_PAGE;
|
|
2156
2300
|
const offset = (page - 1) * limit;
|
|
2157
|
-
const where =
|
|
2301
|
+
const where = eq4(tasks.parentTaskId, parentTaskId);
|
|
2158
2302
|
const countRow = db.select({ count: sql3`count(*)` }).from(tasks).where(where).get();
|
|
2159
2303
|
const total = countRow?.count ?? 0;
|
|
2160
2304
|
const items = db.select().from(tasks).where(where).orderBy(desc2(tasks.createdAt)).limit(limit).offset(offset).all();
|
|
@@ -2167,7 +2311,7 @@ function updateTask(id, input) {
|
|
|
2167
2311
|
throw new Error(`Task not found: ${id}`);
|
|
2168
2312
|
}
|
|
2169
2313
|
if (input.epicId) {
|
|
2170
|
-
const epic = db.select().from(epics).where(
|
|
2314
|
+
const epic = db.select().from(epics).where(eq4(epics.id, input.epicId)).get();
|
|
2171
2315
|
if (!epic) {
|
|
2172
2316
|
throw new Error(`Epic not found: ${input.epicId}`);
|
|
2173
2317
|
}
|
|
@@ -2193,7 +2337,7 @@ function updateTask(id, input) {
|
|
|
2193
2337
|
if (input.epicId !== undefined) {
|
|
2194
2338
|
updates.epicId = input.epicId;
|
|
2195
2339
|
}
|
|
2196
|
-
db.update(tasks).set(updates).where(
|
|
2340
|
+
db.update(tasks).set(updates).where(eq4(tasks.id, id)).run();
|
|
2197
2341
|
const updated = getTask(id);
|
|
2198
2342
|
if (!updated) {
|
|
2199
2343
|
throw new Error(`Task not found after update: ${id}`);
|
|
@@ -2206,7 +2350,7 @@ function deleteTask(id) {
|
|
|
2206
2350
|
if (!existing) {
|
|
2207
2351
|
throw new Error(`Task not found: ${id}`);
|
|
2208
2352
|
}
|
|
2209
|
-
db.delete(tasks).where(
|
|
2353
|
+
db.delete(tasks).where(eq4(tasks.id, id)).run();
|
|
2210
2354
|
}
|
|
2211
2355
|
|
|
2212
2356
|
// src/commands/task.ts
|
|
@@ -2378,10 +2522,10 @@ subtaskCommand.command("delete <subtask-id>").description("Delete a subtask").ac
|
|
|
2378
2522
|
import { Command as Command6 } from "commander";
|
|
2379
2523
|
|
|
2380
2524
|
// src/services/comment.ts
|
|
2381
|
-
import { eq as
|
|
2525
|
+
import { eq as eq5, desc as desc3, sql as sql4 } from "drizzle-orm";
|
|
2382
2526
|
function createComment(input) {
|
|
2383
2527
|
const db = getDb();
|
|
2384
|
-
const task = db.select().from(tasks).where(
|
|
2528
|
+
const task = db.select().from(tasks).where(eq5(tasks.id, input.taskId)).get();
|
|
2385
2529
|
if (!task) {
|
|
2386
2530
|
throw new Error(`Task not found: ${input.taskId}`);
|
|
2387
2531
|
}
|
|
@@ -2400,18 +2544,18 @@ function createComment(input) {
|
|
|
2400
2544
|
}
|
|
2401
2545
|
function getComment(id) {
|
|
2402
2546
|
const db = getDb();
|
|
2403
|
-
return db.select().from(comments).where(
|
|
2547
|
+
return db.select().from(comments).where(eq5(comments.id, id)).get();
|
|
2404
2548
|
}
|
|
2405
2549
|
function listComments(taskId, options) {
|
|
2406
2550
|
const db = getDb();
|
|
2407
2551
|
const limit = options?.limit ?? PAGINATION_DEFAULTS.LIST_PAGE_SIZE;
|
|
2408
2552
|
const page = options?.page ?? PAGINATION_DEFAULTS.DEFAULT_PAGE;
|
|
2409
2553
|
const offset = (page - 1) * limit;
|
|
2410
|
-
const task = db.select().from(tasks).where(
|
|
2554
|
+
const task = db.select().from(tasks).where(eq5(tasks.id, taskId)).get();
|
|
2411
2555
|
if (!task) {
|
|
2412
2556
|
throw new Error(`Task not found: ${taskId}`);
|
|
2413
2557
|
}
|
|
2414
|
-
const where =
|
|
2558
|
+
const where = eq5(comments.taskId, taskId);
|
|
2415
2559
|
const countRow = db.select({ count: sql4`count(*)` }).from(comments).where(where).get();
|
|
2416
2560
|
const total = countRow?.count ?? 0;
|
|
2417
2561
|
const items = db.select().from(comments).where(where).orderBy(desc3(comments.createdAt)).limit(limit).offset(offset).all();
|
|
@@ -2426,7 +2570,7 @@ function updateComment(id, input) {
|
|
|
2426
2570
|
db.update(comments).set({
|
|
2427
2571
|
content: input.content,
|
|
2428
2572
|
updatedAt: new Date
|
|
2429
|
-
}).where(
|
|
2573
|
+
}).where(eq5(comments.id, id)).run();
|
|
2430
2574
|
const updated = getComment(id);
|
|
2431
2575
|
if (!updated) {
|
|
2432
2576
|
throw new Error(`Comment not found after update: ${id}`);
|
|
@@ -2439,7 +2583,7 @@ function deleteComment(id) {
|
|
|
2439
2583
|
if (!existing) {
|
|
2440
2584
|
throw new Error(`Comment not found: ${id}`);
|
|
2441
2585
|
}
|
|
2442
|
-
db.delete(comments).where(
|
|
2586
|
+
db.delete(comments).where(eq5(comments.id, id)).run();
|
|
2443
2587
|
}
|
|
2444
2588
|
|
|
2445
2589
|
// src/commands/comment.ts
|
|
@@ -2491,21 +2635,21 @@ commentCommand.command("delete <comment-id>").description("Delete a comment").ac
|
|
|
2491
2635
|
import { Command as Command7 } from "commander";
|
|
2492
2636
|
|
|
2493
2637
|
// src/services/dependency.ts
|
|
2494
|
-
import { eq as
|
|
2638
|
+
import { eq as eq6 } from "drizzle-orm";
|
|
2495
2639
|
function addDependency(taskId, dependsOnId) {
|
|
2496
2640
|
const db = getDb();
|
|
2497
|
-
const task = db.select().from(tasks).where(
|
|
2641
|
+
const task = db.select().from(tasks).where(eq6(tasks.id, taskId)).get();
|
|
2498
2642
|
if (!task) {
|
|
2499
2643
|
throw new Error(`Task not found: ${taskId}`);
|
|
2500
2644
|
}
|
|
2501
|
-
const dependsOnTask = db.select().from(tasks).where(
|
|
2645
|
+
const dependsOnTask = db.select().from(tasks).where(eq6(tasks.id, dependsOnId)).get();
|
|
2502
2646
|
if (!dependsOnTask) {
|
|
2503
2647
|
throw new Error(`Task not found: ${dependsOnId}`);
|
|
2504
2648
|
}
|
|
2505
2649
|
if (taskId === dependsOnId) {
|
|
2506
2650
|
throw new Error("A task cannot depend on itself.");
|
|
2507
2651
|
}
|
|
2508
|
-
const existing = db.select().from(dependencies).where(
|
|
2652
|
+
const existing = db.select().from(dependencies).where(eq6(dependencies.taskId, taskId)).all().find((d) => d.dependsOnId === dependsOnId);
|
|
2509
2653
|
if (existing) {
|
|
2510
2654
|
throw new Error(`Dependency already exists: ${taskId} \u2192 ${dependsOnId}`);
|
|
2511
2655
|
}
|
|
@@ -2525,22 +2669,22 @@ function addDependency(taskId, dependsOnId) {
|
|
|
2525
2669
|
}
|
|
2526
2670
|
function removeDependency(taskId, dependsOnId) {
|
|
2527
2671
|
const db = getDb();
|
|
2528
|
-
const existing = db.select().from(dependencies).where(
|
|
2672
|
+
const existing = db.select().from(dependencies).where(eq6(dependencies.taskId, taskId)).all().find((d) => d.dependsOnId === dependsOnId);
|
|
2529
2673
|
if (!existing) {
|
|
2530
2674
|
throw new Error(`Dependency not found: ${taskId} \u2192 ${dependsOnId}`);
|
|
2531
2675
|
}
|
|
2532
|
-
db.delete(dependencies).where(
|
|
2676
|
+
db.delete(dependencies).where(eq6(dependencies.id, existing.id)).run();
|
|
2533
2677
|
}
|
|
2534
2678
|
function getDependencies(taskId) {
|
|
2535
2679
|
const db = getDb();
|
|
2536
2680
|
const dependsOn = db.select({
|
|
2537
2681
|
taskId: dependencies.taskId,
|
|
2538
2682
|
dependsOnId: dependencies.dependsOnId
|
|
2539
|
-
}).from(dependencies).where(
|
|
2683
|
+
}).from(dependencies).where(eq6(dependencies.taskId, taskId)).all();
|
|
2540
2684
|
const blocks = db.select({
|
|
2541
2685
|
taskId: dependencies.taskId,
|
|
2542
2686
|
dependsOnId: dependencies.dependsOnId
|
|
2543
|
-
}).from(dependencies).where(
|
|
2687
|
+
}).from(dependencies).where(eq6(dependencies.dependsOnId, taskId)).all();
|
|
2544
2688
|
return { dependsOn, blocks };
|
|
2545
2689
|
}
|
|
2546
2690
|
function wouldCreateCycle(taskId, dependsOnId) {
|
|
@@ -2559,7 +2703,7 @@ function wouldCreateCycle(taskId, dependsOnId) {
|
|
|
2559
2703
|
continue;
|
|
2560
2704
|
}
|
|
2561
2705
|
visited.add(current);
|
|
2562
|
-
const deps = db.select({ dependsOnId: dependencies.dependsOnId }).from(dependencies).where(
|
|
2706
|
+
const deps = db.select({ dependsOnId: dependencies.dependsOnId }).from(dependencies).where(eq6(dependencies.taskId, current)).all();
|
|
2563
2707
|
for (const dep of deps) {
|
|
2564
2708
|
if (!visited.has(dep.dependsOnId)) {
|
|
2565
2709
|
stack.push(dep.dependsOnId);
|
|
@@ -2624,7 +2768,7 @@ trekker wipe -y # Remove all data
|
|
|
2624
2768
|
1. Set status to \`in_progress\` when starting, \`completed\` when done
|
|
2625
2769
|
2. Add summary comment before marking task complete
|
|
2626
2770
|
3. Use \`--toon\` flag for token-efficient output
|
|
2627
|
-
4. When epic is done, use \`trekker epic complete
|
|
2771
|
+
4. When an epic is done, use \`trekker epic complete <epic-id>\` to archive all tasks
|
|
2628
2772
|
5. Write detailed descriptions with implementation plans - future agents need this context
|
|
2629
2773
|
6. Comments are your external memory - add summaries before context resets
|
|
2630
2774
|
|
|
@@ -2633,41 +2777,47 @@ trekker wipe -y # Remove all data
|
|
|
2633
2777
|
### Epics (features/milestones)
|
|
2634
2778
|
trekker epic create -t "Title" [-d "desc"] [-p 0-5]
|
|
2635
2779
|
trekker epic list [--status <status>]
|
|
2636
|
-
trekker epic show
|
|
2637
|
-
trekker epic update
|
|
2638
|
-
trekker epic complete
|
|
2639
|
-
trekker epic delete
|
|
2780
|
+
trekker epic show <epic-id>
|
|
2781
|
+
trekker epic update <epic-id> [-t "Title"] [-d "desc"] [-p 0-5] [-s <status>]
|
|
2782
|
+
trekker epic complete <epic-id> # Complete and archive all tasks
|
|
2783
|
+
trekker epic delete <epic-id>
|
|
2640
2784
|
|
|
2641
2785
|
### Tasks
|
|
2642
|
-
trekker task create -t "Title" [-d "desc"] [-p 0-5] [-e
|
|
2643
|
-
trekker task list [--status <status>] [--epic
|
|
2644
|
-
trekker task show
|
|
2645
|
-
trekker task update
|
|
2646
|
-
trekker task delete
|
|
2786
|
+
trekker task create -t "Title" [-d "desc"] [-p 0-5] [-e <epic-id>] [--tags "a,b"]
|
|
2787
|
+
trekker task list [--status <status>] [--epic <epic-id>]
|
|
2788
|
+
trekker task show <task-id>
|
|
2789
|
+
trekker task update <task-id> [-t "Title"] [-d "desc"] [-p 0-5] [-s <status>] [--tags "a,b"] [-e <epic-id>] [--no-epic]
|
|
2790
|
+
trekker task delete <task-id>
|
|
2647
2791
|
|
|
2648
2792
|
### Subtasks
|
|
2649
|
-
trekker subtask create
|
|
2650
|
-
trekker subtask list
|
|
2651
|
-
trekker subtask update
|
|
2652
|
-
trekker subtask delete
|
|
2793
|
+
trekker subtask create <task-id> -t "Title" [-d "desc"] [-p 0-5]
|
|
2794
|
+
trekker subtask list <task-id>
|
|
2795
|
+
trekker subtask update <subtask-id> [-t "Title"] [-d "desc"] [-p 0-5] [-s <status>]
|
|
2796
|
+
trekker subtask delete <subtask-id>
|
|
2653
2797
|
|
|
2654
2798
|
### Comments (external memory)
|
|
2655
|
-
trekker comment add
|
|
2656
|
-
trekker comment list
|
|
2657
|
-
trekker comment update
|
|
2658
|
-
trekker comment delete
|
|
2799
|
+
trekker comment add <task-id> -a "agent" -c "content"
|
|
2800
|
+
trekker comment list <task-id>
|
|
2801
|
+
trekker comment update <comment-id> -c "new content"
|
|
2802
|
+
trekker comment delete <comment-id>
|
|
2659
2803
|
|
|
2660
2804
|
### Dependencies
|
|
2661
|
-
trekker dep add
|
|
2662
|
-
trekker dep remove
|
|
2663
|
-
trekker dep list
|
|
2805
|
+
trekker dep add <task-id> <depends-on-id>
|
|
2806
|
+
trekker dep remove <task-id> <depends-on-id>
|
|
2807
|
+
trekker dep list <task-id>
|
|
2808
|
+
|
|
2809
|
+
### Project Config
|
|
2810
|
+
trekker config list
|
|
2811
|
+
trekker config get issue_prefix
|
|
2812
|
+
trekker config set issue_prefix ABC
|
|
2813
|
+
trekker config unset issue_prefix
|
|
2664
2814
|
|
|
2665
2815
|
### Search (full-text across all entities)
|
|
2666
2816
|
trekker search "query" [--type epic,task,subtask,comment] [--status <status>]
|
|
2667
2817
|
trekker search "auth bug" --type task --limit 10
|
|
2668
2818
|
|
|
2669
2819
|
### History (audit log of all changes)
|
|
2670
|
-
trekker history [--entity
|
|
2820
|
+
trekker history [--entity <entity-id>] [--type task] [--action create,update,delete]
|
|
2671
2821
|
trekker history --since 2025-01-01 --limit 20
|
|
2672
2822
|
|
|
2673
2823
|
### List (unified view of all items)
|
|
@@ -2706,17 +2856,17 @@ flowchart TD
|
|
|
2706
2856
|
|
|
2707
2857
|
## Session Start
|
|
2708
2858
|
trekker --toon task list --status in_progress
|
|
2709
|
-
trekker --toon comment list
|
|
2859
|
+
trekker --toon comment list <task-id>
|
|
2710
2860
|
|
|
2711
2861
|
## Working
|
|
2712
|
-
trekker task update
|
|
2713
|
-
trekker comment add
|
|
2862
|
+
trekker task update <task-id> -s in_progress
|
|
2863
|
+
trekker comment add <task-id> -a "agent" -c "Analysis: ..."
|
|
2714
2864
|
# ... do work ...
|
|
2715
|
-
trekker comment add
|
|
2716
|
-
trekker task update
|
|
2865
|
+
trekker comment add <task-id> -a "agent" -c "Summary: implemented X in files A, B"
|
|
2866
|
+
trekker task update <task-id> -s completed
|
|
2717
2867
|
|
|
2718
2868
|
## Before Context Reset
|
|
2719
|
-
trekker comment add
|
|
2869
|
+
trekker comment add <task-id> -a "agent" -c "Checkpoint: done A,B. Next: C. Files: x.ts, y.ts"
|
|
2720
2870
|
|
|
2721
2871
|
## Writing Effective Descriptions
|
|
2722
2872
|
|
|
@@ -3228,7 +3378,7 @@ function getHistory(options) {
|
|
|
3228
3378
|
|
|
3229
3379
|
// src/commands/history.ts
|
|
3230
3380
|
import_dayjs.default.extend(import_customParseFormat.default);
|
|
3231
|
-
var historyCommand = new Command11("history").description("View history of all changes (creates, updates, deletes)").option("--entity <id>", "Filter by entity ID
|
|
3381
|
+
var historyCommand = new Command11("history").description("View history of all changes (creates, updates, deletes)").option("--entity <id>", "Filter by entity ID").option("--type <types>", "Filter by type: epic,task,subtask,comment,dependency (comma-separated)").option("--action <actions>", "Filter by action: create,update,delete (comma-separated)").option("--since <date>", "Events after date (YYYY-MM-DD)").option("--until <date>", "Events before date (YYYY-MM-DD)").option("--limit <n>", "Results per page (default: 50)", "50").option("--page <n>", "Page number (default: 1)", "1").action((options) => {
|
|
3232
3382
|
try {
|
|
3233
3383
|
const types = parseCommaSeparated(options.type);
|
|
3234
3384
|
const actions = parseCommaSeparated(options.action);
|
|
@@ -3651,10 +3801,56 @@ Page ${result.page} of ${totalPages}`);
|
|
|
3651
3801
|
return lines.join(`
|
|
3652
3802
|
`);
|
|
3653
3803
|
}
|
|
3804
|
+
|
|
3805
|
+
// src/commands/config.ts
|
|
3806
|
+
import { Command as Command14 } from "commander";
|
|
3807
|
+
var configCommand = new Command14("config").description("Manage project configuration");
|
|
3808
|
+
configCommand.command("list").description("List supported project config values").action(() => {
|
|
3809
|
+
try {
|
|
3810
|
+
if (isToonMode()) {
|
|
3811
|
+
output(listProjectConfig());
|
|
3812
|
+
return;
|
|
3813
|
+
}
|
|
3814
|
+
outputResult(listProjectConfigEntries(), formatProjectConfigList);
|
|
3815
|
+
} catch (err) {
|
|
3816
|
+
handleCommandError(err);
|
|
3817
|
+
}
|
|
3818
|
+
});
|
|
3819
|
+
configCommand.command("get <key>").description("Get a project config value").action((key) => {
|
|
3820
|
+
try {
|
|
3821
|
+
assertProjectConfigKey(key);
|
|
3822
|
+
const value = getProjectConfigValue(key);
|
|
3823
|
+
if (isToonMode()) {
|
|
3824
|
+
output({ key, value });
|
|
3825
|
+
return;
|
|
3826
|
+
}
|
|
3827
|
+
console.log(value);
|
|
3828
|
+
} catch (err) {
|
|
3829
|
+
handleCommandError(err);
|
|
3830
|
+
}
|
|
3831
|
+
});
|
|
3832
|
+
configCommand.command("set <key> <value>").description("Set a project config value").action((key, value) => {
|
|
3833
|
+
try {
|
|
3834
|
+
assertProjectConfigKey(key);
|
|
3835
|
+
const config = setProjectConfigValues({ [key]: value });
|
|
3836
|
+
success(`Config updated: ${key}=${config[key]}`, { key, value: config[key] });
|
|
3837
|
+
} catch (err) {
|
|
3838
|
+
handleCommandError(err);
|
|
3839
|
+
}
|
|
3840
|
+
});
|
|
3841
|
+
configCommand.command("unset <key>").description("Reset a project config value to its default").action((key) => {
|
|
3842
|
+
try {
|
|
3843
|
+
assertProjectConfigKey(key);
|
|
3844
|
+
const value = unsetProjectConfigValue(key);
|
|
3845
|
+
success(`Config reset: ${key}=${value}`, { key, value });
|
|
3846
|
+
} catch (err) {
|
|
3847
|
+
handleCommandError(err);
|
|
3848
|
+
}
|
|
3849
|
+
});
|
|
3654
3850
|
// package.json
|
|
3655
3851
|
var package_default = {
|
|
3656
3852
|
name: "@obsfx/trekker",
|
|
3657
|
-
version: "1.
|
|
3853
|
+
version: "1.11.0",
|
|
3658
3854
|
description: "A CLI-based issue tracker built for AI coding agents. Stores tasks, epics, and dependencies in a local SQLite database.",
|
|
3659
3855
|
type: "module",
|
|
3660
3856
|
main: "dist/index.js",
|
|
@@ -3708,26 +3904,26 @@ var package_default = {
|
|
|
3708
3904
|
bun: ">=1.0.0"
|
|
3709
3905
|
},
|
|
3710
3906
|
dependencies: {
|
|
3711
|
-
"@toon-format/toon": "
|
|
3712
|
-
commander: "
|
|
3713
|
-
dayjs: "
|
|
3714
|
-
"drizzle-orm": "
|
|
3907
|
+
"@toon-format/toon": "2.1.0",
|
|
3908
|
+
commander: "13.1.0",
|
|
3909
|
+
dayjs: "1.11.19",
|
|
3910
|
+
"drizzle-orm": "0.38.4"
|
|
3715
3911
|
},
|
|
3716
3912
|
devDependencies: {
|
|
3717
|
-
"@types/bun": "
|
|
3718
|
-
"drizzle-kit": "
|
|
3719
|
-
eslint: "
|
|
3720
|
-
"eslint-config-prettier": "
|
|
3721
|
-
"eslint-plugin-unicorn": "
|
|
3722
|
-
knip: "
|
|
3723
|
-
prettier: "
|
|
3724
|
-
typescript: "
|
|
3725
|
-
"typescript-eslint": "
|
|
3913
|
+
"@types/bun": "1.2.2",
|
|
3914
|
+
"drizzle-kit": "0.30.4",
|
|
3915
|
+
eslint: "10.0.2",
|
|
3916
|
+
"eslint-config-prettier": "10.1.8",
|
|
3917
|
+
"eslint-plugin-unicorn": "63.0.0",
|
|
3918
|
+
knip: "6.3.1",
|
|
3919
|
+
prettier: "3.8.1",
|
|
3920
|
+
typescript: "5.7.3",
|
|
3921
|
+
"typescript-eslint": "8.56.1"
|
|
3726
3922
|
}
|
|
3727
3923
|
};
|
|
3728
3924
|
|
|
3729
3925
|
// src/index.ts
|
|
3730
|
-
var program = new
|
|
3926
|
+
var program = new Command15;
|
|
3731
3927
|
program.name("trekker").description("CLI-based issue tracker for coding agents").version(package_default.version).option("--toon", "Output in TOON format").hook("preAction", (thisCommand) => {
|
|
3732
3928
|
const opts = thisCommand.opts();
|
|
3733
3929
|
if (opts.toon) {
|
|
@@ -3747,4 +3943,5 @@ program.addCommand(searchCommand);
|
|
|
3747
3943
|
program.addCommand(historyCommand);
|
|
3748
3944
|
program.addCommand(listCommand);
|
|
3749
3945
|
program.addCommand(readyCommand);
|
|
3946
|
+
program.addCommand(configCommand);
|
|
3750
3947
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@obsfx/trekker",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
4
|
"description": "A CLI-based issue tracker built for AI coding agents. Stores tasks, epics, and dependencies in a local SQLite database.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -54,20 +54,20 @@
|
|
|
54
54
|
"bun": ">=1.0.0"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@toon-format/toon": "
|
|
58
|
-
"commander": "
|
|
59
|
-
"dayjs": "
|
|
60
|
-
"drizzle-orm": "
|
|
57
|
+
"@toon-format/toon": "2.1.0",
|
|
58
|
+
"commander": "13.1.0",
|
|
59
|
+
"dayjs": "1.11.19",
|
|
60
|
+
"drizzle-orm": "0.38.4"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@types/bun": "
|
|
64
|
-
"drizzle-kit": "
|
|
65
|
-
"eslint": "
|
|
66
|
-
"eslint-config-prettier": "
|
|
67
|
-
"eslint-plugin-unicorn": "
|
|
68
|
-
"knip": "
|
|
69
|
-
"prettier": "
|
|
70
|
-
"typescript": "
|
|
71
|
-
"typescript-eslint": "
|
|
63
|
+
"@types/bun": "1.2.2",
|
|
64
|
+
"drizzle-kit": "0.30.4",
|
|
65
|
+
"eslint": "10.0.2",
|
|
66
|
+
"eslint-config-prettier": "10.1.8",
|
|
67
|
+
"eslint-plugin-unicorn": "63.0.0",
|
|
68
|
+
"knip": "6.3.1",
|
|
69
|
+
"prettier": "3.8.1",
|
|
70
|
+
"typescript": "5.7.3",
|
|
71
|
+
"typescript-eslint": "8.56.1"
|
|
72
72
|
}
|
|
73
73
|
}
|