saga-mcp 1.6.0 → 1.7.1
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 +199 -7
- package/dist/db.js +20 -0
- package/dist/db.js.map +1 -1
- package/dist/helpers/project-scope.d.ts +31 -0
- package/dist/helpers/project-scope.js +113 -0
- package/dist/helpers/project-scope.js.map +1 -0
- package/dist/helpers/slim.d.ts +17 -0
- package/dist/helpers/slim.js +45 -0
- package/dist/helpers/slim.js.map +1 -0
- package/dist/index.js +37 -3
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +4 -0
- package/dist/schema.js.map +1 -1
- package/dist/tools/activity.js +13 -1
- package/dist/tools/activity.js.map +1 -1
- package/dist/tools/comments.js +78 -4
- package/dist/tools/comments.js.map +1 -1
- package/dist/tools/dashboard.js +29 -9
- package/dist/tools/dashboard.js.map +1 -1
- package/dist/tools/epics.js +7 -6
- package/dist/tools/epics.js.map +1 -1
- package/dist/tools/export-import.js +6 -1
- package/dist/tools/export-import.js.map +1 -1
- package/dist/tools/notes.js +8 -0
- package/dist/tools/notes.js.map +1 -1
- package/dist/tools/projects.js +2 -1
- package/dist/tools/projects.js.map +1 -1
- package/dist/tools/search.js +38 -13
- package/dist/tools/search.js.map +1 -1
- package/dist/tools/tasks.js +15 -4
- package/dist/tools/tasks.js.map +1 -1
- package/dist/web/index.d.ts +2 -0
- package/dist/web/index.js +342 -0
- package/dist/web/index.js.map +1 -0
- package/dist/web/queries.d.ts +32 -0
- package/dist/web/queries.js +186 -0
- package/dist/web/queries.js.map +1 -0
- package/dist/web/ui.d.ts +10 -0
- package/dist/web/ui.js +1270 -0
- package/dist/web/ui.js.map +1 -0
- package/manifest.json +21 -4
- package/package.json +15 -4
package/README.md
CHANGED
|
@@ -1,31 +1,88 @@
|
|
|
1
1
|
# saga-mcp
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/saga-mcp)
|
|
4
|
+
[](https://www.npmjs.com/package/saga-mcp)
|
|
5
|
+
[](https://github.com/spranab/saga-mcp/blob/master/LICENSE)
|
|
3
6
|
[](https://ideacred.com/profile/spranab)
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
Your coding agent loses the plan between sessions. You come back tomorrow and
|
|
9
|
+
it has no idea which of the five things you agreed on are done, which one is
|
|
10
|
+
blocked on which, or why you rejected the second approach — because the plan
|
|
11
|
+
lived in the context window, or in a `TODO.md` nobody updates.
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
saga-mcp gives the agent a real tracker instead: a SQLite file in your project
|
|
14
|
+
holding projects, epics, tasks, subtasks, dependencies, comments, notes and
|
|
15
|
+
decisions, exposed as 33 MCP tools. The agent writes to it as it works and
|
|
16
|
+
reads the dashboard when it comes back. No accounts, no external service, no
|
|
17
|
+
network calls — the database is a file you own.
|
|
18
|
+
|
|
19
|
+
## Install (60 seconds)
|
|
20
|
+
|
|
21
|
+
Claude Code — add to your project's `.mcp.json`:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"mcpServers": {
|
|
26
|
+
"saga": {
|
|
27
|
+
"command": "npx",
|
|
28
|
+
"args": ["-y", "saga-mcp"],
|
|
29
|
+
"env": { "DB_PATH": "/absolute/path/to/your/project/.tracker.db" }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Restart the client. `DB_PATH` is the only setting; the file and schema are
|
|
36
|
+
created on first use.
|
|
37
|
+
|
|
38
|
+
## What it looks like
|
|
39
|
+
|
|
40
|
+
**You:** "Set up tracking for the e-commerce API and plan out auth."
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
tracker_init({ project_name: "E-Commerce API" })
|
|
44
|
+
epic_create({ project_id: 1, name: "Authentication", priority: "high" })
|
|
45
|
+
task_create({ epic_id: 1, title: "Design auth schema", priority: "critical" })
|
|
46
|
+
task_create({ epic_id: 1, title: "Implement JWT auth", depends_on: [1] })
|
|
47
|
+
task_create({ epic_id: 1, title: "Add OAuth2 Google login", depends_on: [2] })
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Tasks 2 and 3 come back **blocked** — their dependencies aren't done. Finish
|
|
51
|
+
task 1 and task 2 unblocks itself.
|
|
52
|
+
|
|
53
|
+
**Next session, you:** "Where were we?"
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
tracker_dashboard({})
|
|
57
|
+
→ "E-Commerce API: 5 tasks across 2 epics. 40% complete.
|
|
58
|
+
Active: Authentication (2/3 done). Next up: Product Catalog (2 tasks).
|
|
59
|
+
1 blocked task(s)."
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Plus the structured data behind it: stats, epics, blocked and overdue tasks,
|
|
63
|
+
recent activity, notes.
|
|
8
64
|
|
|
9
65
|
## Features
|
|
10
66
|
|
|
11
67
|
- **Full hierarchy**: Projects > Epics > Tasks > Subtasks
|
|
12
68
|
- **Task dependencies**: Express sequencing with auto-block/unblock when deps are met
|
|
13
|
-
- **Comments**: Threaded discussions on tasks — leave breadcrumbs across sessions
|
|
69
|
+
- **Comments**: Threaded discussions on tasks — leave breadcrumbs across sessions, with reversible soft-delete
|
|
70
|
+
- **Web UI**: `saga-web` serves a local dashboard for browsing *and* editing the same database
|
|
14
71
|
- **Templates**: Reusable task sets with `{variable}` substitution
|
|
15
72
|
- **Dashboard**: One tool call gives full overview with natural language summary
|
|
16
73
|
- **SQLite**: Self-contained `.tracker.db` file per project — zero setup, no external database
|
|
17
74
|
- **Activity log**: Every mutation is automatically tracked with old/new values
|
|
18
75
|
- **Notes system**: Decisions, context, meeting notes, blockers — all searchable
|
|
19
76
|
- **Batch operations**: Create multiple subtasks or update multiple tasks in one call
|
|
20
|
-
- **
|
|
77
|
+
- **33 focused tools**: With MCP safety annotations on every tool
|
|
21
78
|
- **Import/export**: Full project backup and migration as JSON (with dependencies and comments)
|
|
22
79
|
- **Source references**: Link tasks to specific code locations
|
|
23
80
|
- **Auto time tracking**: Hours computed automatically from activity log
|
|
24
81
|
- **Cross-platform**: Works on macOS, Windows, and Linux
|
|
25
82
|
|
|
26
|
-
##
|
|
83
|
+
## Other clients
|
|
27
84
|
|
|
28
|
-
###
|
|
85
|
+
### Claude Code
|
|
29
86
|
|
|
30
87
|
Add to your project's `.mcp.json`:
|
|
31
88
|
|
|
@@ -75,9 +132,29 @@ saga-mcp requires a single environment variable:
|
|
|
75
132
|
| Variable | Required | Description |
|
|
76
133
|
|----------|----------|-------------|
|
|
77
134
|
| `DB_PATH` | Yes | Absolute path to the `.tracker.db` SQLite file. The file and schema are auto-created on first use. |
|
|
135
|
+
| `SAGA_PROJECT` | No | Scope every tool to one project, by id or name. Set this per repo when several repos share one database. Unset, tools read across the whole file. |
|
|
136
|
+
| `SAGA_TOOLS` | No | `full` (default) lists all 33 tools. `core` lists only the 12 an ordinary tracking session needs, cutting ~3,300 tokens of context per session. Tools left off the list still work if called by name. |
|
|
78
137
|
|
|
79
138
|
No API keys, no accounts, no external services. Everything is stored locally in the SQLite file you specify.
|
|
80
139
|
|
|
140
|
+
### Token cost
|
|
141
|
+
|
|
142
|
+
The tool list is context every session pays before any work happens, and list responses are
|
|
143
|
+
context it pays again on every call. Both are kept deliberately small:
|
|
144
|
+
|
|
145
|
+
- Responses are compact JSON — no pretty-print indentation, which measured 20-27% of every response
|
|
146
|
+
- `task_list` rows omit nulls and `metadata`, and truncate descriptions to 120 characters
|
|
147
|
+
(call `task_get` for a task's full text) — 19-39% smaller depending on how long your descriptions run
|
|
148
|
+
- `activity_log` omits null columns and the row id (no tool takes one) — about 27% smaller
|
|
149
|
+
- `tracker_search` returns previews rather than whole records — about 47% smaller; follow up with
|
|
150
|
+
`task_get` or `note_list` for the full text
|
|
151
|
+
- `SAGA_TOOLS=core` drops the listed tool surface from ~6,000 to ~2,700 tokens
|
|
152
|
+
|
|
153
|
+
`note_list` deliberately keeps full note content — it is the retrieval tool, not a preview.
|
|
154
|
+
|
|
155
|
+
Set `SAGA_TOOLS=core` when an agent only tracks work; leave it unset when you want templates,
|
|
156
|
+
import/export, session diffs and the rest discoverable.
|
|
157
|
+
|
|
81
158
|
## Tools
|
|
82
159
|
|
|
83
160
|
### Getting Started
|
|
@@ -126,7 +203,9 @@ No API keys, no accounts, no external services. Everything is stored locally in
|
|
|
126
203
|
| Tool | Description | Annotations |
|
|
127
204
|
|------|-------------|-------------|
|
|
128
205
|
| `comment_add` | Add a comment to a task (threaded discussion) | `readOnly: false` |
|
|
129
|
-
| `comment_list` | List
|
|
206
|
+
| `comment_list` | List comments on a task (removed ones hidden unless `include_deleted`) | `readOnly: true` |
|
|
207
|
+
| `comment_delete` | Remove a comment — soft delete, row kept for audit | `readOnly: false`, `idempotent: true` |
|
|
208
|
+
| `comment_restore` | Restore a removed comment | `readOnly: false`, `idempotent: true` |
|
|
130
209
|
|
|
131
210
|
### Templates
|
|
132
211
|
|
|
@@ -225,6 +304,100 @@ task_update({ id: 5, status: "done" })
|
|
|
225
304
|
|
|
226
305
|
Comments persist across sessions — next time an agent calls `task_get(5)`, it sees the full discussion thread.
|
|
227
306
|
|
|
307
|
+
If a comment turns out to be wrong, retract it without losing the trail:
|
|
308
|
+
|
|
309
|
+
```
|
|
310
|
+
comment_delete({ id: 12, reason: "Root cause was wrong — it was a proxy timeout", deleted_by: "pranab" })
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
The row stays in the database and in the activity log. `comment_list` and `task_get` skip it,
|
|
314
|
+
`comment_list({ task_id: 5, include_deleted: true })` shows it with its reason, and
|
|
315
|
+
`comment_restore({ id: 12 })` brings it back. Nothing an agent removes is unrecoverable.
|
|
316
|
+
|
|
317
|
+
## One database, many projects
|
|
318
|
+
|
|
319
|
+
saga-mcp works either way: a `.tracker.db` per repo (portable, keeps unrelated work apart),
|
|
320
|
+
or one shared database that every repo points at.
|
|
321
|
+
|
|
322
|
+
The shared setup needs one extra thing. `projects` is the top-level table, so a shared file holds
|
|
323
|
+
several projects — but `task_list`, `note_list`, `activity_log` and `tracker_search` read across
|
|
324
|
+
the whole file unless told otherwise. An agent in repo B would see repo A's tasks. Set
|
|
325
|
+
`SAGA_PROJECT` per repo and each agent sees only its own:
|
|
326
|
+
|
|
327
|
+
```json
|
|
328
|
+
{
|
|
329
|
+
"mcpServers": {
|
|
330
|
+
"saga": {
|
|
331
|
+
"command": "npx",
|
|
332
|
+
"args": ["-y", "saga-mcp"],
|
|
333
|
+
"env": {
|
|
334
|
+
"DB_PATH": "/Users/you/saga/central.tracker.db",
|
|
335
|
+
"SAGA_PROJECT": "Payments platform"
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
`SAGA_PROJECT` takes a project id or a project name (case-insensitive), and fails on startup with
|
|
343
|
+
the list of real projects if it matches neither. Every scoped tool also accepts an explicit
|
|
344
|
+
`project_id` argument, which wins over the environment variable.
|
|
345
|
+
|
|
346
|
+
| Setup | What to set | Result |
|
|
347
|
+
|-------|-------------|--------|
|
|
348
|
+
| One database per repo | `DB_PATH` | Nothing to scope — one project per file |
|
|
349
|
+
| Shared database, per-repo agents | `DB_PATH` + `SAGA_PROJECT` | Each agent sees only its project |
|
|
350
|
+
| Shared database, one agent over everything | `DB_PATH` | Tools read across all projects |
|
|
351
|
+
|
|
352
|
+
With neither `SAGA_PROJECT` nor a `project_id`, `tracker_dashboard` falls back to the first project
|
|
353
|
+
in the file and says so — the response carries `other_projects` and the summary explains that the
|
|
354
|
+
project was a guess, rather than silently reporting on the wrong repo.
|
|
355
|
+
|
|
356
|
+
The web UI is unaffected either way: its project switcher lists every project in the database, and
|
|
357
|
+
each tab is scoped to the selected one.
|
|
358
|
+
|
|
359
|
+
## Web UI
|
|
360
|
+
|
|
361
|
+
Everything above is agent-facing. `saga-web` puts the same database in a browser — for the times
|
|
362
|
+
when reviewing a spec an agent just wrote, or fixing one field by hand, is faster than another prompt.
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
npx -p saga-mcp saga-web ./.tracker.db --open
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Or against a database you already point your MCP server at:
|
|
369
|
+
|
|
370
|
+
```bash
|
|
371
|
+
saga-web --db ~/saga/central.tracker.db --port 8080
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
| Option | Default | Description |
|
|
375
|
+
|--------|---------|-------------|
|
|
376
|
+
| `--db <path>` | `$DB_PATH` | Database to open. A positional path works too. |
|
|
377
|
+
| `--port <n>` | first free from `4319` | Omit it and saga-web takes the first free port, so one instance per project just works. `--port N` binds exactly N and fails if taken; `--port 0` lets the OS choose. Also `SAGA_WEB_PORT`. |
|
|
378
|
+
| `--host <addr>` | `127.0.0.1` | Bind address. Local-only by default. |
|
|
379
|
+
| `--read-only` | off | Serve the UI with every editing control removed. |
|
|
380
|
+
| `--open` | off | Open the UI in your default browser. |
|
|
381
|
+
|
|
382
|
+
What you get:
|
|
383
|
+
|
|
384
|
+
- **Overview** — stats, per-epic progress, blocked and overdue tasks
|
|
385
|
+
- **Board** — kanban across the five task statuses; drag a card to change its status
|
|
386
|
+
- **Epics** — the full Epic → Task → Subtask tree, which is the fastest way to review a spec an agent just wrote
|
|
387
|
+
- **Notes** and **Activity** — decisions and the complete change history
|
|
388
|
+
- **Task drawer** — edit any field, tick subtasks, comment, remove or restore a comment
|
|
389
|
+
- **Project switcher** — every project in the database, so one central `.tracker.db` covers all your repos; every tab, including Activity, is scoped to the selected project
|
|
390
|
+
- **Shareable, refreshable URLs** — the open project, tab and task live in the address bar, so a browser refresh puts you back where you were and back/forward move between tasks. A ⟳ button in the task drawer re-reads that task without a page reload, for picking up what an agent just wrote
|
|
391
|
+
|
|
392
|
+
Writes from the UI call the *same handlers* the MCP tools do, so edits you make by hand are
|
|
393
|
+
validated identically and land in the same activity log as the agent's — an agent calling
|
|
394
|
+
`tracker_dashboard` after you fix something sees the fix and how it happened.
|
|
395
|
+
|
|
396
|
+
A few deliberate limits: it binds to `127.0.0.1` unless you ask otherwise, it has no
|
|
397
|
+
authentication (don't put it on a shared network), and it will not create a database — point it
|
|
398
|
+
at one your MCP server already uses. Separate `.tracker.db` files are not yet aggregated into
|
|
399
|
+
one view; a single database with multiple projects is.
|
|
400
|
+
|
|
228
401
|
## How It Works
|
|
229
402
|
|
|
230
403
|
saga-mcp stores everything in a single SQLite file (`.tracker.db`) per project. The database is auto-created on first use with all tables and indexes — no migration step needed.
|
|
@@ -300,6 +473,9 @@ cd saga-mcp
|
|
|
300
473
|
npm install
|
|
301
474
|
npm run build
|
|
302
475
|
DB_PATH=./test.db npm start
|
|
476
|
+
|
|
477
|
+
# the web UI against the same database
|
|
478
|
+
node dist/web/index.js ./test.db --open
|
|
303
479
|
```
|
|
304
480
|
|
|
305
481
|
## Support
|
|
@@ -307,6 +483,22 @@ DB_PATH=./test.db npm start
|
|
|
307
483
|
- **Issues**: https://github.com/spranab/saga-mcp/issues
|
|
308
484
|
- **Repository**: https://github.com/spranab/saga-mcp
|
|
309
485
|
|
|
486
|
+
## Related projects
|
|
487
|
+
|
|
488
|
+
Part of a set of agent infrastructure built by one person, meant to be used
|
|
489
|
+
together:
|
|
490
|
+
|
|
491
|
+
- [yantrikdb-mcp](https://github.com/yantrikos/yantrikdb-mcp) — persistent
|
|
492
|
+
cognitive memory for the same agent: what it learned, not what it planned.
|
|
493
|
+
- [brainstorm-mcp](https://github.com/spranab/brainstorm-mcp) — multi-model
|
|
494
|
+
debate before you commit a plan to the tracker.
|
|
495
|
+
- [swarmcode](https://github.com/spranab/swarmcode) — real-time channel
|
|
496
|
+
between Claude Code instances on different machines.
|
|
497
|
+
- [truenas-mcp](https://github.com/spranab/truenas-mcp) — 278 TrueNAS SCALE
|
|
498
|
+
actions behind one hierarchical tool.
|
|
499
|
+
- [mcpier](https://github.com/spranab/mcpier) — self-hosted MCP control plane
|
|
500
|
+
that keeps API keys off your clients.
|
|
501
|
+
|
|
310
502
|
## License
|
|
311
503
|
|
|
312
504
|
MIT
|
package/dist/db.js
CHANGED
|
@@ -27,6 +27,26 @@ export function getDb() {
|
|
|
27
27
|
db.exec('CREATE INDEX IF NOT EXISTS idx_epics_branch ON epics(branch)');
|
|
28
28
|
}
|
|
29
29
|
catch { /* index already exists */ }
|
|
30
|
+
try {
|
|
31
|
+
db.exec('ALTER TABLE comments ADD COLUMN is_deleted INTEGER NOT NULL DEFAULT 0');
|
|
32
|
+
}
|
|
33
|
+
catch { /* column already exists */ }
|
|
34
|
+
try {
|
|
35
|
+
db.exec('ALTER TABLE comments ADD COLUMN deleted_at TEXT');
|
|
36
|
+
}
|
|
37
|
+
catch { /* column already exists */ }
|
|
38
|
+
try {
|
|
39
|
+
db.exec('ALTER TABLE comments ADD COLUMN deleted_by TEXT');
|
|
40
|
+
}
|
|
41
|
+
catch { /* column already exists */ }
|
|
42
|
+
try {
|
|
43
|
+
db.exec('ALTER TABLE comments ADD COLUMN delete_reason TEXT');
|
|
44
|
+
}
|
|
45
|
+
catch { /* column already exists */ }
|
|
46
|
+
try {
|
|
47
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_comments_task_live ON comments(task_id, is_deleted)');
|
|
48
|
+
}
|
|
49
|
+
catch { /* index already exists */ }
|
|
30
50
|
return db;
|
|
31
51
|
}
|
|
32
52
|
export function closeDb() {
|
package/dist/db.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,IAAI,EAAE,GAA6B,IAAI,CAAC;AAExC,MAAM,UAAU,KAAK;IACnB,IAAI,EAAE;QAAE,OAAO,EAAE,CAAC;IAElB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;IACnC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACb,mIAAmI,CACpI,CAAC;IACJ,CAAC;IAED,EAAE,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC;IAE1B,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAChC,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;IAC/B,EAAE,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAC;IACjC,EAAE,CAAC,MAAM,CAAC,sBAAsB,CAAC,CAAC;IAElC,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAEpB,oCAAoC;IACpC,IAAI,CAAC;QAAC,EAAE,CAAC,IAAI,CAAC,8CAA8C,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,2BAA2B,CAAC,CAAC;IACtG,IAAI,CAAC;QAAC,EAAE,CAAC,IAAI,CAAC,0CAA0C,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,2BAA2B,CAAC,CAAC;IAClG,IAAI,CAAC;QAAC,EAAE,CAAC,IAAI,CAAC,8DAA8D,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,0BAA0B,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../src/db.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,IAAI,EAAE,GAA6B,IAAI,CAAC;AAExC,MAAM,UAAU,KAAK;IACnB,IAAI,EAAE;QAAE,OAAO,EAAE,CAAC;IAElB,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC;IACnC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CACb,mIAAmI,CACpI,CAAC;IACJ,CAAC;IAED,EAAE,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC;IAE1B,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAChC,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;IAC/B,EAAE,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAC;IACjC,EAAE,CAAC,MAAM,CAAC,sBAAsB,CAAC,CAAC;IAElC,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;IAEpB,oCAAoC;IACpC,IAAI,CAAC;QAAC,EAAE,CAAC,IAAI,CAAC,8CAA8C,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,2BAA2B,CAAC,CAAC;IACtG,IAAI,CAAC;QAAC,EAAE,CAAC,IAAI,CAAC,0CAA0C,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,2BAA2B,CAAC,CAAC;IAClG,IAAI,CAAC;QAAC,EAAE,CAAC,IAAI,CAAC,8DAA8D,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,0BAA0B,CAAC,CAAC;IACrH,IAAI,CAAC;QAAC,EAAE,CAAC,IAAI,CAAC,uEAAuE,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,2BAA2B,CAAC,CAAC;IAC/H,IAAI,CAAC;QAAC,EAAE,CAAC,IAAI,CAAC,iDAAiD,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,2BAA2B,CAAC,CAAC;IACzG,IAAI,CAAC;QAAC,EAAE,CAAC,IAAI,CAAC,iDAAiD,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,2BAA2B,CAAC,CAAC;IACzG,IAAI,CAAC;QAAC,EAAE,CAAC,IAAI,CAAC,oDAAoD,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,2BAA2B,CAAC,CAAC;IAC5G,IAAI,CAAC;QAAC,EAAE,CAAC,IAAI,CAAC,oFAAoF,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,0BAA0B,CAAC,CAAC;IAE3I,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,UAAU,OAAO;IACrB,IAAI,EAAE,EAAE,CAAC;QACP,EAAE,CAAC,KAAK,EAAE,CAAC;QACX,EAAE,GAAG,IAAI,CAAC;IACZ,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type Database from 'better-sqlite3';
|
|
2
|
+
/** The project a call should be scoped to, or undefined for the whole database. */
|
|
3
|
+
export declare function resolveProjectId(db: Database.Database, args: Record<string, unknown>): number | undefined;
|
|
4
|
+
export declare function projectCount(db: Database.Database): number;
|
|
5
|
+
/** Shared JSON-schema property, so every scoped tool describes it identically. */
|
|
6
|
+
export declare const PROJECT_ID_SCHEMA: {
|
|
7
|
+
type: "integer";
|
|
8
|
+
description: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* `WHERE` fragment selecting tasks in a project, given an alias for the epics
|
|
12
|
+
* table already joined by the caller.
|
|
13
|
+
*/
|
|
14
|
+
export declare function taskScopeClause(epicAlias?: string): string;
|
|
15
|
+
/** Notes attached to a project, or to any epic/task inside it. Unattached notes count as global. */
|
|
16
|
+
export declare function noteScopeClause(alias?: string): {
|
|
17
|
+
sql: string;
|
|
18
|
+
paramCount: number;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Activity rows name an entity by type and id, so scoping means walking each
|
|
22
|
+
* type back up to its project.
|
|
23
|
+
*/
|
|
24
|
+
export declare function activityScopeClause(alias?: string): {
|
|
25
|
+
sql: string;
|
|
26
|
+
paramCount: number;
|
|
27
|
+
};
|
|
28
|
+
/** Repeat a project id n times, for the multi-placeholder clauses above. */
|
|
29
|
+
export declare function repeatId(projectId: number, n: number): number[];
|
|
30
|
+
/** Only for tests — SAGA_PROJECT is read once and cached. */
|
|
31
|
+
export declare function resetProjectScopeCache(): void;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One database can hold many projects — the "global" / centralized setup, where
|
|
3
|
+
* every repo points its MCP server at the same .tracker.db.
|
|
4
|
+
*
|
|
5
|
+
* Without scoping, an agent working in repo B sees repo A's tasks: task_list,
|
|
6
|
+
* note_list, activity_log and tracker_search all read across the whole file.
|
|
7
|
+
* These helpers give every such tool the same scoping rule:
|
|
8
|
+
*
|
|
9
|
+
* explicit project_id argument > SAGA_PROJECT env var > unscoped
|
|
10
|
+
*
|
|
11
|
+
* Set SAGA_PROJECT (an id, or a project name) in each repo's MCP config and a
|
|
12
|
+
* shared database behaves exactly like a per-project one.
|
|
13
|
+
*/
|
|
14
|
+
let cachedEnvProject;
|
|
15
|
+
/** Resolve SAGA_PROJECT, which may be a numeric id or a project name. */
|
|
16
|
+
function envProjectId(db) {
|
|
17
|
+
const raw = process.env.SAGA_PROJECT?.trim();
|
|
18
|
+
if (!raw)
|
|
19
|
+
return undefined;
|
|
20
|
+
if (cachedEnvProject && cachedEnvProject.raw === raw)
|
|
21
|
+
return cachedEnvProject.id;
|
|
22
|
+
let row;
|
|
23
|
+
if (/^\d+$/.test(raw)) {
|
|
24
|
+
row = db.prepare('SELECT id FROM projects WHERE id = ?').get(Number(raw));
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
row = db.prepare('SELECT id FROM projects WHERE name = ? COLLATE NOCASE').get(raw);
|
|
28
|
+
}
|
|
29
|
+
if (!row) {
|
|
30
|
+
const known = db.prepare('SELECT id, name FROM projects ORDER BY id').all()
|
|
31
|
+
.map((p) => `${p.id}=${p.name}`)
|
|
32
|
+
.join(', ');
|
|
33
|
+
throw new Error(`SAGA_PROJECT is set to '${raw}', which is not a project in this database. Known projects: ${known || '(none)'}`);
|
|
34
|
+
}
|
|
35
|
+
cachedEnvProject = { raw, id: row.id };
|
|
36
|
+
return row.id;
|
|
37
|
+
}
|
|
38
|
+
/** The project a call should be scoped to, or undefined for the whole database. */
|
|
39
|
+
export function resolveProjectId(db, args) {
|
|
40
|
+
const explicit = args.project_id;
|
|
41
|
+
if (typeof explicit === 'number')
|
|
42
|
+
return explicit;
|
|
43
|
+
if (typeof explicit === 'string' && /^\d+$/.test(explicit))
|
|
44
|
+
return Number(explicit);
|
|
45
|
+
return envProjectId(db);
|
|
46
|
+
}
|
|
47
|
+
export function projectCount(db) {
|
|
48
|
+
return db.prepare('SELECT COUNT(*) as n FROM projects').get().n;
|
|
49
|
+
}
|
|
50
|
+
/** Shared JSON-schema property, so every scoped tool describes it identically. */
|
|
51
|
+
export const PROJECT_ID_SCHEMA = {
|
|
52
|
+
type: 'integer',
|
|
53
|
+
description: 'Scope to one project. Needed when a single database holds several projects; defaults to the SAGA_PROJECT env var if set, otherwise the whole database.',
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* `WHERE` fragment selecting tasks in a project, given an alias for the epics
|
|
57
|
+
* table already joined by the caller.
|
|
58
|
+
*/
|
|
59
|
+
export function taskScopeClause(epicAlias = 'e') {
|
|
60
|
+
return `${epicAlias}.project_id = ?`;
|
|
61
|
+
}
|
|
62
|
+
/** Notes attached to a project, or to any epic/task inside it. Unattached notes count as global. */
|
|
63
|
+
export function noteScopeClause(alias = 'notes') {
|
|
64
|
+
return {
|
|
65
|
+
sql: `(
|
|
66
|
+
(${alias}.related_entity_type = 'project' AND ${alias}.related_entity_id = ?)
|
|
67
|
+
OR (${alias}.related_entity_type = 'epic' AND ${alias}.related_entity_id IN
|
|
68
|
+
(SELECT id FROM epics WHERE project_id = ?))
|
|
69
|
+
OR (${alias}.related_entity_type = 'task' AND ${alias}.related_entity_id IN
|
|
70
|
+
(SELECT t.id FROM tasks t JOIN epics e ON e.id = t.epic_id WHERE e.project_id = ?))
|
|
71
|
+
OR ${alias}.related_entity_type IS NULL
|
|
72
|
+
)`,
|
|
73
|
+
paramCount: 3,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Activity rows name an entity by type and id, so scoping means walking each
|
|
78
|
+
* type back up to its project.
|
|
79
|
+
*/
|
|
80
|
+
export function activityScopeClause(alias = 'activity_log') {
|
|
81
|
+
return {
|
|
82
|
+
sql: `(
|
|
83
|
+
(${alias}.entity_type = 'project' AND ${alias}.entity_id = ?)
|
|
84
|
+
OR (${alias}.entity_type = 'epic' AND ${alias}.entity_id IN
|
|
85
|
+
(SELECT id FROM epics WHERE project_id = ?))
|
|
86
|
+
OR (${alias}.entity_type = 'task' AND ${alias}.entity_id IN
|
|
87
|
+
(SELECT t.id FROM tasks t JOIN epics e ON e.id = t.epic_id WHERE e.project_id = ?))
|
|
88
|
+
OR (${alias}.entity_type = 'subtask' AND ${alias}.entity_id IN
|
|
89
|
+
(SELECT s.id FROM subtasks s JOIN tasks t ON t.id = s.task_id
|
|
90
|
+
JOIN epics e ON e.id = t.epic_id WHERE e.project_id = ?))
|
|
91
|
+
OR (${alias}.entity_type = 'comment' AND ${alias}.entity_id IN
|
|
92
|
+
(SELECT c.id FROM comments c JOIN tasks t ON t.id = c.task_id
|
|
93
|
+
JOIN epics e ON e.id = t.epic_id WHERE e.project_id = ?))
|
|
94
|
+
OR (${alias}.entity_type = 'note' AND ${alias}.entity_id IN
|
|
95
|
+
(SELECT n.id FROM notes n WHERE
|
|
96
|
+
(n.related_entity_type = 'project' AND n.related_entity_id = ?)
|
|
97
|
+
OR (n.related_entity_type = 'epic' AND n.related_entity_id IN
|
|
98
|
+
(SELECT id FROM epics WHERE project_id = ?))
|
|
99
|
+
OR (n.related_entity_type = 'task' AND n.related_entity_id IN
|
|
100
|
+
(SELECT t.id FROM tasks t JOIN epics e ON e.id = t.epic_id WHERE e.project_id = ?))))
|
|
101
|
+
)`,
|
|
102
|
+
paramCount: 8,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** Repeat a project id n times, for the multi-placeholder clauses above. */
|
|
106
|
+
export function repeatId(projectId, n) {
|
|
107
|
+
return Array.from({ length: n }, () => projectId);
|
|
108
|
+
}
|
|
109
|
+
/** Only for tests — SAGA_PROJECT is read once and cached. */
|
|
110
|
+
export function resetProjectScopeCache() {
|
|
111
|
+
cachedEnvProject = undefined;
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=project-scope.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"project-scope.js","sourceRoot":"","sources":["../../src/helpers/project-scope.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List responses are the tokens an agent pays over and over. Detail responses
|
|
3
|
+
* (task_get) stay complete; list rows drop what a scan does not need.
|
|
4
|
+
*
|
|
5
|
+
* Measured on a 40-task project: task_list went from ~5,300 to ~3,600 tokens.
|
|
6
|
+
*/
|
|
7
|
+
/** Longest description kept in a list row. Longer text is cut with an ellipsis. */
|
|
8
|
+
export declare const LIST_DESCRIPTION_CHARS = 120;
|
|
9
|
+
export declare function truncate(text: string, max?: number): string;
|
|
10
|
+
/**
|
|
11
|
+
* Strip a list row down: no nulls, no metadata blob, no empty tags array, and
|
|
12
|
+
* long text fields truncated. Everything removed here is still available from
|
|
13
|
+
* the matching *_get tool.
|
|
14
|
+
*/
|
|
15
|
+
export declare function slimListRow(row: Record<string, unknown>, truncateFields?: string[], drop?: string[]): Record<string, unknown>;
|
|
16
|
+
/** Apply slimListRow across a result set. */
|
|
17
|
+
export declare function slimList(rows: Array<Record<string, unknown>>, truncateFields?: string[], drop?: string[]): Array<Record<string, unknown>>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List responses are the tokens an agent pays over and over. Detail responses
|
|
3
|
+
* (task_get) stay complete; list rows drop what a scan does not need.
|
|
4
|
+
*
|
|
5
|
+
* Measured on a 40-task project: task_list went from ~5,300 to ~3,600 tokens.
|
|
6
|
+
*/
|
|
7
|
+
/** Longest description kept in a list row. Longer text is cut with an ellipsis. */
|
|
8
|
+
export const LIST_DESCRIPTION_CHARS = 120;
|
|
9
|
+
export function truncate(text, max = LIST_DESCRIPTION_CHARS) {
|
|
10
|
+
if (text.length <= max)
|
|
11
|
+
return text;
|
|
12
|
+
// Prefer a word boundary so the cut reads as prose rather than mid-token.
|
|
13
|
+
const cut = text.slice(0, max);
|
|
14
|
+
const lastSpace = cut.lastIndexOf(' ');
|
|
15
|
+
return (lastSpace > max * 0.6 ? cut.slice(0, lastSpace) : cut).trimEnd() + '…';
|
|
16
|
+
}
|
|
17
|
+
/** Fields never worth their bytes in a list response. */
|
|
18
|
+
const ALWAYS_DROP = new Set(['metadata']);
|
|
19
|
+
/**
|
|
20
|
+
* Strip a list row down: no nulls, no metadata blob, no empty tags array, and
|
|
21
|
+
* long text fields truncated. Everything removed here is still available from
|
|
22
|
+
* the matching *_get tool.
|
|
23
|
+
*/
|
|
24
|
+
export function slimListRow(row, truncateFields = ['description'], drop = []) {
|
|
25
|
+
const out = {};
|
|
26
|
+
for (const [key, value] of Object.entries(row)) {
|
|
27
|
+
if (value === null || value === undefined)
|
|
28
|
+
continue;
|
|
29
|
+
if (ALWAYS_DROP.has(key) || drop.includes(key))
|
|
30
|
+
continue;
|
|
31
|
+
if (key === 'tags' && (value === '[]' || value === ''))
|
|
32
|
+
continue;
|
|
33
|
+
if (truncateFields.includes(key) && typeof value === 'string') {
|
|
34
|
+
out[key] = truncate(value);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
out[key] = value;
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
/** Apply slimListRow across a result set. */
|
|
42
|
+
export function slimList(rows, truncateFields, drop) {
|
|
43
|
+
return rows.map((row) => slimListRow(row, truncateFields, drop));
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=slim.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slim.js","sourceRoot":"","sources":["../../src/helpers/slim.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,mFAAmF;AACnF,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAAG,CAAC;AAE1C,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,GAAG,GAAG,sBAAsB;IACjE,IAAI,IAAI,CAAC,MAAM,IAAI,GAAG;QAAE,OAAO,IAAI,CAAC;IACpC,0EAA0E;IAC1E,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC/B,MAAM,SAAS,GAAG,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACvC,OAAO,CAAC,SAAS,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,GAAG,GAAG,CAAC;AACjF,CAAC;AAED,yDAAyD;AACzD,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;AAE1C;;;;GAIG;AACH,MAAM,UAAU,WAAW,CACzB,GAA4B,EAC5B,iBAA2B,CAAC,aAAa,CAAC,EAC1C,OAAiB,EAAE;IAEnB,MAAM,GAAG,GAA4B,EAAE,CAAC;IACxC,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/C,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,SAAS;YAAE,SAAS;QACpD,IAAI,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;YAAE,SAAS;QACzD,IAAI,GAAG,KAAK,MAAM,IAAI,CAAC,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;YAAE,SAAS;QACjE,IAAI,cAAc,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;YAC9D,GAAG,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;YAC3B,SAAS;QACX,CAAC;QACD,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IACnB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,6CAA6C;AAC7C,MAAM,UAAU,QAAQ,CACtB,IAAoC,EACpC,cAAyB,EACzB,IAAe;IAEf,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,CAAC,CAAC,CAAC;AACnE,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -40,9 +40,39 @@ const ALL_HANDLERS = {
|
|
|
40
40
|
...activityHandlers,
|
|
41
41
|
...exportImportHandlers,
|
|
42
42
|
};
|
|
43
|
+
/**
|
|
44
|
+
* The full tool list costs ~6,000 tokens of context in every session before any
|
|
45
|
+
* work happens. SAGA_TOOLS=core exposes the subset that covers ordinary tracking
|
|
46
|
+
* (~2,300 tokens); handlers stay registered either way, so a client that already
|
|
47
|
+
* knows a tool name can still call it. Unset (or "full") keeps every tool listed.
|
|
48
|
+
*/
|
|
49
|
+
const CORE_TOOLS = new Set([
|
|
50
|
+
'tracker_init',
|
|
51
|
+
'tracker_dashboard',
|
|
52
|
+
'project_list',
|
|
53
|
+
'epic_create',
|
|
54
|
+
'epic_list',
|
|
55
|
+
'task_create',
|
|
56
|
+
'task_list',
|
|
57
|
+
'task_get',
|
|
58
|
+
'task_update',
|
|
59
|
+
'subtask_create',
|
|
60
|
+
'note_save',
|
|
61
|
+
'comment_add',
|
|
62
|
+
]);
|
|
63
|
+
function listedTools() {
|
|
64
|
+
const mode = (process.env.SAGA_TOOLS ?? 'full').trim().toLowerCase();
|
|
65
|
+
if (mode === 'full' || mode === '')
|
|
66
|
+
return ALL_TOOLS;
|
|
67
|
+
if (mode !== 'core') {
|
|
68
|
+
console.error(`Unknown SAGA_TOOLS value '${mode}' — expected 'core' or 'full'. Listing all tools.`);
|
|
69
|
+
return ALL_TOOLS;
|
|
70
|
+
}
|
|
71
|
+
return ALL_TOOLS.filter((t) => CORE_TOOLS.has(t.name));
|
|
72
|
+
}
|
|
43
73
|
const server = new Server({ name: 'tracker', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
44
74
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
45
|
-
return { tools:
|
|
75
|
+
return { tools: listedTools() };
|
|
46
76
|
});
|
|
47
77
|
function friendlyError(msg) {
|
|
48
78
|
if (msg.includes('UNIQUE constraint failed')) {
|
|
@@ -70,7 +100,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
70
100
|
}
|
|
71
101
|
const result = handler(args ?? {});
|
|
72
102
|
return {
|
|
73
|
-
|
|
103
|
+
// Compact, not pretty-printed: indentation costs ~22% of every response
|
|
104
|
+
// in tokens and buys the model nothing.
|
|
105
|
+
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
74
106
|
};
|
|
75
107
|
}
|
|
76
108
|
catch (error) {
|
|
@@ -90,7 +122,9 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
90
122
|
async function main() {
|
|
91
123
|
const transport = new StdioServerTransport();
|
|
92
124
|
await server.connect(transport);
|
|
93
|
-
|
|
125
|
+
const listed = listedTools().length;
|
|
126
|
+
console.error(`Tracker MCP Server running on stdio (${listed} of ${ALL_TOOLS.length} tools listed` +
|
|
127
|
+
`${listed < ALL_TOOLS.length ? ' — SAGA_TOOLS=core' : ''})`);
|
|
94
128
|
}
|
|
95
129
|
process.on('SIGINT', () => {
|
|
96
130
|
closeDb();
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EACL,qBAAqB,EACrB,sBAAsB,GACvB,MAAM,oCAAoC,CAAC;AAG5C,OAAO,EAAE,WAAW,IAAI,WAAW,EAAE,QAAQ,IAAI,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC9F,OAAO,EAAE,WAAW,IAAI,QAAQ,EAAE,QAAQ,IAAI,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrF,OAAO,EAAE,WAAW,IAAI,QAAQ,EAAE,QAAQ,IAAI,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrF,OAAO,EAAE,WAAW,IAAI,WAAW,EAAE,QAAQ,IAAI,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC9F,OAAO,EAAE,WAAW,IAAI,QAAQ,EAAE,QAAQ,IAAI,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrF,OAAO,EAAE,WAAW,IAAI,aAAa,EAAE,QAAQ,IAAI,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACnG,OAAO,EAAE,WAAW,IAAI,UAAU,EAAE,QAAQ,IAAI,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAC1F,OAAO,EAAE,WAAW,IAAI,YAAY,EAAE,QAAQ,IAAI,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAChG,OAAO,EAAE,WAAW,IAAI,WAAW,EAAE,QAAQ,IAAI,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC9F,OAAO,EAAE,WAAW,IAAI,YAAY,EAAE,QAAQ,IAAI,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACjG,OAAO,EAAE,WAAW,IAAI,gBAAgB,EAAE,QAAQ,IAAI,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAC7G,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAElC,MAAM,SAAS,GAAW;IACxB,GAAG,WAAW;IACd,GAAG,QAAQ;IACX,GAAG,QAAQ;IACX,GAAG,WAAW;IACd,GAAG,QAAQ;IACX,GAAG,WAAW;IACd,GAAG,YAAY;IACf,GAAG,aAAa;IAChB,GAAG,UAAU;IACb,GAAG,YAAY;IACf,GAAG,gBAAgB;CACpB,CAAC;AAEF,MAAM,YAAY,GAA+D;IAC/E,GAAG,eAAe;IAClB,GAAG,YAAY;IACf,GAAG,YAAY;IACf,GAAG,eAAe;IAClB,GAAG,YAAY;IACf,GAAG,eAAe;IAClB,GAAG,gBAAgB;IACnB,GAAG,iBAAiB;IACpB,GAAG,cAAc;IACjB,GAAG,gBAAgB;IACnB,GAAG,oBAAoB;CACxB,CAAC;AAEF,MAAM,MAAM,GAAG,IAAI,MAAM,CACvB,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,EACrC,EAAE,YAAY,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,CAChC,CAAC;AAEF,MAAM,CAAC,iBAAiB,CAAC,sBAAsB,EAAE,KAAK,IAAI,EAAE;IAC1D,OAAO,EAAE,KAAK,EAAE,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/schema.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SCHEMA_SQL = "\n-- Core hierarchy: projects > epics > tasks > subtasks\n\nCREATE TABLE IF NOT EXISTS projects (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n description TEXT,\n status TEXT NOT NULL DEFAULT 'active'\n CHECK (status IN ('active', 'on_hold', 'completed', 'archived')),\n tags TEXT NOT NULL DEFAULT '[]',\n metadata TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS epics (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n description TEXT,\n status TEXT NOT NULL DEFAULT 'planned'\n CHECK (status IN ('planned', 'in_progress', 'completed', 'cancelled')),\n priority TEXT NOT NULL DEFAULT 'medium'\n CHECK (priority IN ('low', 'medium', 'high', 'critical')),\n sort_order INTEGER NOT NULL DEFAULT 0,\n branch TEXT,\n tags TEXT NOT NULL DEFAULT '[]',\n metadata TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS tasks (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n epic_id INTEGER NOT NULL REFERENCES epics(id) ON DELETE CASCADE,\n title TEXT NOT NULL,\n description TEXT,\n status TEXT NOT NULL DEFAULT 'todo'\n CHECK (status IN ('todo', 'in_progress', 'review', 'done', 'blocked')),\n priority TEXT NOT NULL DEFAULT 'medium'\n CHECK (priority IN ('low', 'medium', 'high', 'critical')),\n sort_order INTEGER NOT NULL DEFAULT 0,\n assigned_to TEXT,\n estimated_hours REAL,\n actual_hours REAL,\n due_date TEXT,\n source_ref TEXT,\n tags TEXT NOT NULL DEFAULT '[]',\n metadata TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS subtasks (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,\n title TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'todo'\n CHECK (status IN ('todo', 'in_progress', 'done')),\n sort_order INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\n-- Task dependencies (junction table)\n\nCREATE TABLE IF NOT EXISTS task_dependencies (\n task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,\n depends_on_task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n PRIMARY KEY (task_id, depends_on_task_id)\n);\n\n-- Comments (threaded discussions on tasks)\n\nCREATE TABLE IF NOT EXISTS comments (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,\n author TEXT,\n content TEXT NOT NULL,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\n-- Task templates\n\nCREATE TABLE IF NOT EXISTS templates (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL UNIQUE,\n description TEXT,\n template_data TEXT NOT NULL DEFAULT '[]',\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\n-- Unified notes (replaces summaries + status_updates + context)\n\nCREATE TABLE IF NOT EXISTS notes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n title TEXT NOT NULL,\n content TEXT NOT NULL,\n note_type TEXT NOT NULL DEFAULT 'general'\n CHECK (note_type IN (\n 'general', 'decision', 'context', 'meeting',\n 'technical', 'blocker', 'progress', 'release'\n )),\n related_entity_type TEXT CHECK (related_entity_type IN ('project', 'epic', 'task') OR related_entity_type IS NULL),\n related_entity_id INTEGER,\n tags TEXT NOT NULL DEFAULT '[]',\n metadata TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\n-- Automatic activity log\n\nCREATE TABLE IF NOT EXISTS activity_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n entity_type TEXT NOT NULL,\n entity_id INTEGER NOT NULL,\n action TEXT NOT NULL,\n field_name TEXT,\n old_value TEXT,\n new_value TEXT,\n summary TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\n-- Indexes\n\nCREATE INDEX IF NOT EXISTS idx_epics_project_id ON epics(project_id);\nCREATE INDEX IF NOT EXISTS idx_tasks_epic_id ON tasks(epic_id);\nCREATE INDEX IF NOT EXISTS idx_subtasks_task_id ON subtasks(task_id);\n\nCREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status);\nCREATE INDEX IF NOT EXISTS idx_epics_status ON epics(status);\nCREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);\nCREATE INDEX IF NOT EXISTS idx_subtasks_status ON subtasks(status);\n\nCREATE INDEX IF NOT EXISTS idx_epics_priority ON epics(priority);\nCREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority);\n\nCREATE INDEX IF NOT EXISTS idx_epics_sort ON epics(project_id, sort_order);\nCREATE INDEX IF NOT EXISTS idx_tasks_sort ON tasks(epic_id, sort_order);\nCREATE INDEX IF NOT EXISTS idx_subtasks_sort ON subtasks(task_id, sort_order);\n\nCREATE INDEX IF NOT EXISTS idx_notes_type ON notes(note_type);\nCREATE INDEX IF NOT EXISTS idx_notes_entity ON notes(related_entity_type, related_entity_id);\nCREATE INDEX IF NOT EXISTS idx_notes_created ON notes(created_at DESC);\n\nCREATE INDEX IF NOT EXISTS idx_activity_entity ON activity_log(entity_type, entity_id);\nCREATE INDEX IF NOT EXISTS idx_activity_created ON activity_log(created_at DESC);\nCREATE INDEX IF NOT EXISTS idx_activity_action ON activity_log(action);\n\nCREATE INDEX IF NOT EXISTS idx_tasks_assigned ON tasks(assigned_to);\nCREATE INDEX IF NOT EXISTS idx_tasks_due ON tasks(due_date);\n\nCREATE INDEX IF NOT EXISTS idx_task_deps_depends ON task_dependencies(depends_on_task_id);\nCREATE INDEX IF NOT EXISTS idx_comments_task ON comments(task_id);\n";
|
|
1
|
+
export declare const SCHEMA_SQL = "\n-- Core hierarchy: projects > epics > tasks > subtasks\n\nCREATE TABLE IF NOT EXISTS projects (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL,\n description TEXT,\n status TEXT NOT NULL DEFAULT 'active'\n CHECK (status IN ('active', 'on_hold', 'completed', 'archived')),\n tags TEXT NOT NULL DEFAULT '[]',\n metadata TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS epics (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE,\n name TEXT NOT NULL,\n description TEXT,\n status TEXT NOT NULL DEFAULT 'planned'\n CHECK (status IN ('planned', 'in_progress', 'completed', 'cancelled')),\n priority TEXT NOT NULL DEFAULT 'medium'\n CHECK (priority IN ('low', 'medium', 'high', 'critical')),\n sort_order INTEGER NOT NULL DEFAULT 0,\n branch TEXT,\n tags TEXT NOT NULL DEFAULT '[]',\n metadata TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS tasks (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n epic_id INTEGER NOT NULL REFERENCES epics(id) ON DELETE CASCADE,\n title TEXT NOT NULL,\n description TEXT,\n status TEXT NOT NULL DEFAULT 'todo'\n CHECK (status IN ('todo', 'in_progress', 'review', 'done', 'blocked')),\n priority TEXT NOT NULL DEFAULT 'medium'\n CHECK (priority IN ('low', 'medium', 'high', 'critical')),\n sort_order INTEGER NOT NULL DEFAULT 0,\n assigned_to TEXT,\n estimated_hours REAL,\n actual_hours REAL,\n due_date TEXT,\n source_ref TEXT,\n tags TEXT NOT NULL DEFAULT '[]',\n metadata TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\nCREATE TABLE IF NOT EXISTS subtasks (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,\n title TEXT NOT NULL,\n status TEXT NOT NULL DEFAULT 'todo'\n CHECK (status IN ('todo', 'in_progress', 'done')),\n sort_order INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\n-- Task dependencies (junction table)\n\nCREATE TABLE IF NOT EXISTS task_dependencies (\n task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,\n depends_on_task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n PRIMARY KEY (task_id, depends_on_task_id)\n);\n\n-- Comments (threaded discussions on tasks)\n\nCREATE TABLE IF NOT EXISTS comments (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n task_id INTEGER NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,\n author TEXT,\n content TEXT NOT NULL,\n is_deleted INTEGER NOT NULL DEFAULT 0,\n deleted_at TEXT,\n deleted_by TEXT,\n delete_reason TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\n-- Task templates\n\nCREATE TABLE IF NOT EXISTS templates (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n name TEXT NOT NULL UNIQUE,\n description TEXT,\n template_data TEXT NOT NULL DEFAULT '[]',\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\n-- Unified notes (replaces summaries + status_updates + context)\n\nCREATE TABLE IF NOT EXISTS notes (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n title TEXT NOT NULL,\n content TEXT NOT NULL,\n note_type TEXT NOT NULL DEFAULT 'general'\n CHECK (note_type IN (\n 'general', 'decision', 'context', 'meeting',\n 'technical', 'blocker', 'progress', 'release'\n )),\n related_entity_type TEXT CHECK (related_entity_type IN ('project', 'epic', 'task') OR related_entity_type IS NULL),\n related_entity_id INTEGER,\n tags TEXT NOT NULL DEFAULT '[]',\n metadata TEXT NOT NULL DEFAULT '{}',\n created_at TEXT NOT NULL DEFAULT (datetime('now')),\n updated_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\n-- Automatic activity log\n\nCREATE TABLE IF NOT EXISTS activity_log (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n entity_type TEXT NOT NULL,\n entity_id INTEGER NOT NULL,\n action TEXT NOT NULL,\n field_name TEXT,\n old_value TEXT,\n new_value TEXT,\n summary TEXT,\n created_at TEXT NOT NULL DEFAULT (datetime('now'))\n);\n\n-- Indexes\n\nCREATE INDEX IF NOT EXISTS idx_epics_project_id ON epics(project_id);\nCREATE INDEX IF NOT EXISTS idx_tasks_epic_id ON tasks(epic_id);\nCREATE INDEX IF NOT EXISTS idx_subtasks_task_id ON subtasks(task_id);\n\nCREATE INDEX IF NOT EXISTS idx_projects_status ON projects(status);\nCREATE INDEX IF NOT EXISTS idx_epics_status ON epics(status);\nCREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status);\nCREATE INDEX IF NOT EXISTS idx_subtasks_status ON subtasks(status);\n\nCREATE INDEX IF NOT EXISTS idx_epics_priority ON epics(priority);\nCREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority);\n\nCREATE INDEX IF NOT EXISTS idx_epics_sort ON epics(project_id, sort_order);\nCREATE INDEX IF NOT EXISTS idx_tasks_sort ON tasks(epic_id, sort_order);\nCREATE INDEX IF NOT EXISTS idx_subtasks_sort ON subtasks(task_id, sort_order);\n\nCREATE INDEX IF NOT EXISTS idx_notes_type ON notes(note_type);\nCREATE INDEX IF NOT EXISTS idx_notes_entity ON notes(related_entity_type, related_entity_id);\nCREATE INDEX IF NOT EXISTS idx_notes_created ON notes(created_at DESC);\n\nCREATE INDEX IF NOT EXISTS idx_activity_entity ON activity_log(entity_type, entity_id);\nCREATE INDEX IF NOT EXISTS idx_activity_created ON activity_log(created_at DESC);\nCREATE INDEX IF NOT EXISTS idx_activity_action ON activity_log(action);\n\nCREATE INDEX IF NOT EXISTS idx_tasks_assigned ON tasks(assigned_to);\nCREATE INDEX IF NOT EXISTS idx_tasks_due ON tasks(due_date);\n\nCREATE INDEX IF NOT EXISTS idx_task_deps_depends ON task_dependencies(depends_on_task_id);\nCREATE INDEX IF NOT EXISTS idx_comments_task ON comments(task_id);\n";
|