@ghostpaw/questlog 0.7.0 → 0.9.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 +95 -18
- package/SKILL.md +29 -15
- package/dist/chunk-3B6FMHZU.js +384 -0
- package/dist/chunk-3Y5F3SMB.js +4308 -0
- package/dist/chunk-RUSE7ZFF.js +58 -0
- package/dist/chunk-SVWAEAKC.js +5 -0
- package/dist/cli.js +11 -7468
- package/dist/cli_main-ED5BJPSR.js +626 -0
- package/dist/index.cjs +64 -45
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +64 -45
- package/dist/mcp-server.js +71 -46
- package/dist/mcp.cjs +65 -46
- package/dist/mcp.js +65 -46
- package/dist/open_questlog_db-SH3DAU4C.js +2 -0
- package/dist/questlog_skill_spec-JLZH3JL2.js +807 -0
- package/dist/serve_questlog_mcp-J4ZHNEOT.js +212 -0
- package/dist/skill_markdown-7HCMSJYR.js +80 -0
- package/package.json +7 -3
- package/skills/questlog/SKILL.md +29 -15
package/README.md
CHANGED
|
@@ -65,23 +65,27 @@ initialize-based revisions 2025-11-25, 2025-06-18, 2025-03-26 and 2024-11-05.
|
|
|
65
65
|
|
|
66
66
|
## CLI
|
|
67
67
|
|
|
68
|
-
Install globally for
|
|
68
|
+
Install globally for `questlog` (`ghostpaw-questlog` is the same binary): `npm i -g @ghostpaw/questlog`.
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
questlog add --title "Review release notes" --tag release --due 2030-01-02
|
|
72
|
+
questlog list --limit 10
|
|
73
|
+
questlog search release notes --limit 5
|
|
74
|
+
questlog show 1
|
|
75
|
+
questlog done 1 --outcome "Reviewed changes"
|
|
76
|
+
questlog list --view quests.resolved --json
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
`add` accepts repeated `--tag` flags, `--objective` (defaults to the title),
|
|
80
|
-
`--questline <id>`, and `--due` as
|
|
79
|
+
`add` accepts comma-separated tags (`--tag a,b`) and repeated `--tag` flags, `--objective` (defaults to the title),
|
|
80
|
+
`--questline <id>`, and `--due` as YYYY-MM-DD, DD.MM.YYYY, `today`, `tomorrow`,
|
|
81
|
+
`+3d`, an ISO date-time with timezone, or milliseconds since epoch. Date-only
|
|
82
|
+
values use local end of day.
|
|
81
83
|
`list` defaults to available quests; `list --help` lists all supported views.
|
|
82
84
|
Its `--limit` caps text output; JSON preserves the full review tool result.
|
|
83
|
-
`search --limit` caps matches in both formats. `show`
|
|
84
|
-
|
|
85
|
+
`search --limit` caps matches in both formats. `show` prints full quest detail.
|
|
86
|
+
Lists show local due dates, overdue markers, and recorded outcomes. Plain JSON
|
|
87
|
+
adds ISO UTC siblings for numeric timestamp fields ending in `At`.
|
|
88
|
+
`done` stores `--outcome` with the finished quest; without it the command asks for one and exits 3.
|
|
85
89
|
|
|
86
90
|
Plain commands print short text by default. Use `--json` for the full tool result
|
|
87
91
|
and `--compact` for compact JSON. `--db path` works before or after the command.
|
|
@@ -91,12 +95,17 @@ or a tool error, 2 for usage errors, and 3 when clarification is needed.
|
|
|
91
95
|
The `call` command and the per-tool form still reach every tool and parameter:
|
|
92
96
|
|
|
93
97
|
```bash
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
questlog tools
|
|
99
|
+
questlog call capture_rumor '{"title":"Review release notes"}'
|
|
100
|
+
questlog search_questlog --query "release notes"
|
|
97
101
|
```
|
|
98
102
|
|
|
99
|
-
`
|
|
103
|
+
`questlog mcp` starts the stdio server.
|
|
104
|
+
|
|
105
|
+
The default database is `~/.ghostpaw/questlog.db`. Override it with `--db <path>`
|
|
106
|
+
or `GHOSTPAW_QUESTLOG_DB`; top-level help shows the selected path. New database
|
|
107
|
+
files are reported on stderr. The npm entry requires Node 24+ and exits 2 with
|
|
108
|
+
an installation hint on older Node versions, including for help and version.
|
|
100
109
|
|
|
101
110
|
## Agent Skill
|
|
102
111
|
|
|
@@ -110,11 +119,79 @@ npx -y @ghostpaw/questlog skill > SKILL.md
|
|
|
110
119
|
|
|
111
120
|
## Install
|
|
112
121
|
|
|
113
|
-
|
|
114
|
-
|
|
122
|
+
The [npm package](https://www.npmjs.com/package/@ghostpaw/questlog) requires Node 24+:
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
npm install -g @ghostpaw/questlog
|
|
126
|
+
npx -y @ghostpaw/questlog --help
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
As a library dependency: `npm install @ghostpaw/questlog`. It uses the built-in
|
|
130
|
+
`node:sqlite` module, so there are no runtime dependencies.
|
|
131
|
+
|
|
132
|
+
Standalone binaries are available from [GitHub Releases](https://github.com/GhostPawJS/questlog/releases)
|
|
133
|
+
for linux-x64, linux-arm64, darwin-arm64, darwin-x64 and windows-x64.
|
|
134
|
+
|
|
135
|
+
```sh
|
|
136
|
+
curl -fsSL https://raw.githubusercontent.com/GhostPawJS/questlog/main/install.sh | sh
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```powershell
|
|
140
|
+
irm https://raw.githubusercontent.com/GhostPawJS/questlog/main/install.ps1 | iex
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Other release installers:
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
mise use -g github:GhostPawJS/questlog
|
|
147
|
+
ubi --project GhostPawJS/questlog --exe ghostpaw-questlog --in ~/.local/bin
|
|
148
|
+
eget GhostPawJS/questlog
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
For [aqua](https://aquaproj.github.io/docs/reference/registry-config/), define a
|
|
152
|
+
`github_release` package in `aqua.yaml`:
|
|
153
|
+
|
|
154
|
+
```yaml
|
|
155
|
+
registries:
|
|
156
|
+
- name: questlog
|
|
157
|
+
type: local
|
|
158
|
+
path: questlog-registry.yaml
|
|
159
|
+
packages:
|
|
160
|
+
- name: GhostPawJS/questlog@v0.8.0
|
|
161
|
+
registry: questlog
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`questlog-registry.yaml`:
|
|
165
|
+
|
|
166
|
+
```yaml
|
|
167
|
+
packages:
|
|
168
|
+
- type: github_release
|
|
169
|
+
repo_owner: GhostPawJS
|
|
170
|
+
repo_name: questlog
|
|
171
|
+
asset: ghostpaw-questlog-{{trimV .Version}}-{{.OS}}-{{.Arch}}.tar.gz
|
|
172
|
+
format: tar.gz
|
|
173
|
+
replacements: { amd64: x64 }
|
|
174
|
+
overrides:
|
|
175
|
+
- goos: windows
|
|
176
|
+
asset: ghostpaw-questlog-{{trimV .Version}}-windows-{{.Arch}}.zip
|
|
177
|
+
format: zip
|
|
178
|
+
files:
|
|
179
|
+
- name: ghostpaw-questlog
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Install scripts verify SHA256SUMS. To verify a downloaded archive's
|
|
183
|
+
[build attestation](https://cli.github.com/manual/gh_attestation_verify):
|
|
184
|
+
|
|
185
|
+
```sh
|
|
186
|
+
gh attestation verify <archive> -R GhostPawJS/questlog
|
|
115
187
|
```
|
|
116
188
|
|
|
117
|
-
|
|
189
|
+
Set `GHOSTPAW_VERIFY=attestation` for attestation verification in `install.sh`.
|
|
190
|
+
Binaries are unsigned; macOS binaries carry an ad-hoc signature. For a browser
|
|
191
|
+
download on macOS, remove quarantine with
|
|
192
|
+
`xattr -d com.apple.quarantine /path/to/ghostpaw-questlog`.
|
|
193
|
+
Linux binaries require glibc 2.28+; use npx on Alpine.
|
|
194
|
+
See the [binary build workflow](.github/workflows/release-binaries.yml).
|
|
118
195
|
|
|
119
196
|
## Quick Start
|
|
120
197
|
|
package/SKILL.md
CHANGED
|
@@ -7,9 +7,23 @@ compatibility: "Node.js 24 or newer. MCP stdio or ghostpaw-questlog CLI."
|
|
|
7
7
|
|
|
8
8
|
# questlog
|
|
9
9
|
|
|
10
|
+
`questlog` and `ghostpaw-questlog` are the same binary.
|
|
11
|
+
|
|
12
|
+
- `questlog add --title "..." --tag a,b --due tomorrow`
|
|
13
|
+
- `questlog list [--view <view>]`: questlines, quests.active, quests.available, quests.blocked, quests.deferred, quests.due_soon, quests.in_progress, quests.missed_scheduled, quests.open, quests.overdue, quests.resolved, quests.scheduled_for_day, quests.scheduled_now, repeatables.due_anchors, rumors.
|
|
14
|
+
- `questlog done <id> --outcome "..."`
|
|
15
|
+
- `questlog search <words>`
|
|
16
|
+
- `questlog show <id>`
|
|
17
|
+
- Use `--json` on any plain command.
|
|
18
|
+
- Exit codes: 0 ok / 1 error / 2 usage / 3 needs input.
|
|
19
|
+
- Database: `~/.ghostpaw/questlog.db`; override with `--db` or `GHOSTPAW_QUESTLOG_DB`.
|
|
20
|
+
- Use one database per agent or project.
|
|
21
|
+
|
|
22
|
+
## Appendix: MCP and tool calls
|
|
23
|
+
|
|
10
24
|
Questlog manages local tasks, calendar commitments, and recurring work. Search before creating work, inspect before changing it, and keep uncertain work as rumors. Use unlock prerequisites to sequence work. run_quest refuses blocked starts unless force is true. Review commitments and resolve work with an honest outcome.
|
|
11
25
|
|
|
12
|
-
|
|
26
|
+
### Calling tools
|
|
13
27
|
|
|
14
28
|
MCP: connect to ghostpaw-questlog, then call a tool by name with its JSON arguments.
|
|
15
29
|
|
|
@@ -17,7 +31,7 @@ CLI: `ghostpaw-questlog call <tool> '<JSON object>'` or `ghostpaw-questlog <tool
|
|
|
17
31
|
|
|
18
32
|
Inspect schemas with `ghostpaw-questlog describe <tool>`. Use `--db <path>` to select a database.
|
|
19
33
|
|
|
20
|
-
|
|
34
|
+
### search_questlog
|
|
21
35
|
|
|
22
36
|
Search quests and rumors by text when you need to find an existing item before inspecting or changing it.
|
|
23
37
|
|
|
@@ -34,7 +48,7 @@ Read-only.
|
|
|
34
48
|
| kinds | array | no | Optional entity kinds to search. |
|
|
35
49
|
| mode | string | no | browse, identify_one. Whether to browse results normally or ask for clarification when several matches are found. |
|
|
36
50
|
|
|
37
|
-
|
|
51
|
+
### review_questlog
|
|
38
52
|
|
|
39
53
|
Review one operational questlog surface such as available quests, open rumors, blocked work, or due repeatable anchors.
|
|
40
54
|
|
|
@@ -50,7 +64,7 @@ Read-only.
|
|
|
50
64
|
| filters | object | no | Optional filters for supported review surfaces. |
|
|
51
65
|
| now | integer | no | Optional evaluation timestamp for time-sensitive views. |
|
|
52
66
|
|
|
53
|
-
|
|
67
|
+
### inspect_questlog_item
|
|
54
68
|
|
|
55
69
|
Inspect one specific questlog item in depth and return its richer structured detail and important related refs.
|
|
56
70
|
|
|
@@ -66,7 +80,7 @@ Read-only.
|
|
|
66
80
|
| detailLevel | string | no | compact, full. How much detail to return. |
|
|
67
81
|
| now | integer | no | Optional evaluation timestamp for time-sensitive detail. |
|
|
68
82
|
|
|
69
|
-
|
|
83
|
+
### capture_rumor
|
|
70
84
|
|
|
71
85
|
Capture uncertain incoming work as a rumor so it can be triaged later without prematurely turning it into committed execution.
|
|
72
86
|
|
|
@@ -82,7 +96,7 @@ Writes: writes_state.
|
|
|
82
96
|
| details | string | no | Optional supporting context that explains why the rumor might matter. |
|
|
83
97
|
| now | integer | no | Optional creation timestamp for the rumor. |
|
|
84
98
|
|
|
85
|
-
|
|
99
|
+
### shape_work
|
|
86
100
|
|
|
87
101
|
Create a quest (with timing, tags, and objective in one call), create a questline, settle a rumor into concrete work, or manage quest-questline membership. Use create_quest for clear commitments — include tags and timing directly to avoid follow-up calls.
|
|
88
102
|
|
|
@@ -107,7 +121,7 @@ Writes: writes_state.
|
|
|
107
121
|
| title | string | no | New title for the update_rumor action. |
|
|
108
122
|
| details | string | no | New or enriched context text for the update_rumor action. |
|
|
109
123
|
|
|
110
|
-
|
|
124
|
+
### plan_quest
|
|
111
125
|
|
|
112
126
|
Refine a quest by revising its objective before actual start or by changing its temporal planning fields.
|
|
113
127
|
|
|
@@ -130,7 +144,7 @@ Writes: writes_state.
|
|
|
130
144
|
| estimateSeconds | integer | no | Optional active-effort estimate in seconds. |
|
|
131
145
|
| now | integer | no | Optional update timestamp. |
|
|
132
146
|
|
|
133
|
-
|
|
147
|
+
### run_quest
|
|
134
148
|
|
|
135
149
|
Run quest lifecycle actions such as starting, logging effort, finishing, abandoning, or abandoning and spawning follow-up quests atomically.
|
|
136
150
|
|
|
@@ -152,7 +166,7 @@ Writes: writes_state.
|
|
|
152
166
|
| resolvedAt | integer | no | Optional resolved timestamp for finish or abandon actions. |
|
|
153
167
|
| followups | array | no | Follow-up quests to create atomically. Required for abandon_and_spawn_followups. |
|
|
154
168
|
|
|
155
|
-
|
|
169
|
+
### organize_work
|
|
156
170
|
|
|
157
171
|
Organize grouping and dependency structure by updating questlines or changing hard unlock prerequisites between concrete quests.
|
|
158
172
|
|
|
@@ -176,7 +190,7 @@ Writes: writes_state.
|
|
|
176
190
|
| fromQuestIds | array | no | Full prerequisite quest id set. |
|
|
177
191
|
| now | integer | no | Optional timestamp. |
|
|
178
192
|
|
|
179
|
-
|
|
193
|
+
### manage_repeatable
|
|
180
194
|
|
|
181
195
|
Manage recurring work definitions by creating, updating, previewing, spawning, or archiving repeatable quests.
|
|
182
196
|
|
|
@@ -205,7 +219,7 @@ Writes: writes_state.
|
|
|
205
219
|
| archivedAt | integer | no | Optional archive timestamp. |
|
|
206
220
|
| now | integer | no | Evaluation or update timestamp. |
|
|
207
221
|
|
|
208
|
-
|
|
222
|
+
### tag_work
|
|
209
223
|
|
|
210
224
|
Classify work by adding, removing, or replacing tags on quests and by replacing the future tag template set on repeatable quests.
|
|
211
225
|
|
|
@@ -222,7 +236,7 @@ Writes: writes_state.
|
|
|
222
236
|
| tagNames | array | yes | Tag names to add, remove, or replace. |
|
|
223
237
|
| now | integer | no | Optional timestamp for the tag mutation. |
|
|
224
238
|
|
|
225
|
-
|
|
239
|
+
### reward_work
|
|
226
240
|
|
|
227
241
|
Manage rewards by adding, updating, removing, or claiming quest rewards, or by replacing future reward templates on repeatable quests.
|
|
228
242
|
|
|
@@ -241,7 +255,7 @@ Writes: writes_state.
|
|
|
241
255
|
| claimedAt | integer | no | Optional claim timestamp. |
|
|
242
256
|
| now | integer | no | Optional timestamp for removal or template replacement. |
|
|
243
257
|
|
|
244
|
-
|
|
258
|
+
### retire_work
|
|
245
259
|
|
|
246
260
|
Hide an item from normal active operation without rewriting history by soft-deleting a rumor, quest, questline, or repeatable quest.
|
|
247
261
|
|
|
@@ -257,7 +271,7 @@ Writes: writes_state.
|
|
|
257
271
|
| target | object | yes | The target to hide. |
|
|
258
272
|
| deletedAt | integer | no | Optional timestamp to record when the item became hidden. |
|
|
259
273
|
|
|
260
|
-
|
|
274
|
+
### Workflow guides
|
|
261
275
|
|
|
262
276
|
- [archive-delete-and-prune](skills/questlog/references/archive-delete-and-prune.md): Choose the right retirement action for each kind of work so history stays honest and hiding does not get confused with archival or terminal execution truth.
|
|
263
277
|
- [choose-work-shape](skills/questlog/references/choose-work-shape.md): Choose the right structure for new work by separating uncertainty, one-shot execution, multi-step arcs, and recurring patterns.
|
|
@@ -285,7 +299,7 @@ Writes: writes_state.
|
|
|
285
299
|
- [settle-rumor-into-structure](skills/questlog/references/settle-rumor-into-structure.md): Turn a ready rumor into committed quest or questline structure without losing provenance or pretending vague intake is settled.
|
|
286
300
|
- [tag-and-filter-portfolios](skills/questlog/references/tag-and-filter-portfolios.md): Use tags for cross-cutting classification and portfolio filtering without confusing tags for status, timing, or dependency modeling.
|
|
287
301
|
|
|
288
|
-
|
|
302
|
+
### Principles
|
|
289
303
|
|
|
290
304
|
- Store the least committed truth first.
|
|
291
305
|
- Read before you prescribe.
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdirSync } from 'node:fs';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
|
|
5
|
+
// src/questlines/init_questline_tables.ts
|
|
6
|
+
function initQuestlineTables(db) {
|
|
7
|
+
db.exec(`
|
|
8
|
+
CREATE TABLE IF NOT EXISTS questlines (
|
|
9
|
+
id INTEGER PRIMARY KEY,
|
|
10
|
+
source_rumor_id INTEGER REFERENCES rumors(id),
|
|
11
|
+
title TEXT NOT NULL,
|
|
12
|
+
description TEXT,
|
|
13
|
+
starts_at INTEGER,
|
|
14
|
+
due_at INTEGER,
|
|
15
|
+
created_at INTEGER NOT NULL,
|
|
16
|
+
updated_at INTEGER NOT NULL,
|
|
17
|
+
archived_at INTEGER,
|
|
18
|
+
deleted_at INTEGER,
|
|
19
|
+
CHECK(length(trim(title)) > 0),
|
|
20
|
+
CHECK(due_at IS NULL OR starts_at IS NULL OR due_at >= starts_at)
|
|
21
|
+
)
|
|
22
|
+
`);
|
|
23
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_questlines_due_at ON questlines(due_at)");
|
|
24
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_questlines_deleted_at ON questlines(deleted_at)");
|
|
25
|
+
db.exec(
|
|
26
|
+
"CREATE INDEX IF NOT EXISTS idx_questlines_source_rumor_id ON questlines(source_rumor_id) WHERE deleted_at IS NULL"
|
|
27
|
+
);
|
|
28
|
+
db.exec(
|
|
29
|
+
"CREATE INDEX IF NOT EXISTS idx_questlines_created_at_active ON questlines(created_at DESC, id DESC) WHERE deleted_at IS NULL"
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/quests/init_quest_search.ts
|
|
34
|
+
function initQuestSearch(db) {
|
|
35
|
+
db.exec(`
|
|
36
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS quests_fts USING fts5(
|
|
37
|
+
title,
|
|
38
|
+
objective,
|
|
39
|
+
outcome,
|
|
40
|
+
content=quests,
|
|
41
|
+
content_rowid=id
|
|
42
|
+
)
|
|
43
|
+
`);
|
|
44
|
+
db.exec(`
|
|
45
|
+
CREATE TRIGGER IF NOT EXISTS quests_fts_insert
|
|
46
|
+
AFTER INSERT ON quests
|
|
47
|
+
WHEN new.deleted_at IS NULL
|
|
48
|
+
BEGIN
|
|
49
|
+
INSERT INTO quests_fts(rowid, title, objective, outcome)
|
|
50
|
+
VALUES (new.id, new.title, new.objective, COALESCE(new.outcome, ''));
|
|
51
|
+
END
|
|
52
|
+
`);
|
|
53
|
+
db.exec(`
|
|
54
|
+
CREATE TRIGGER IF NOT EXISTS quests_fts_update
|
|
55
|
+
AFTER UPDATE OF title, objective, outcome, deleted_at ON quests
|
|
56
|
+
BEGIN
|
|
57
|
+
INSERT INTO quests_fts(quests_fts, rowid, title, objective, outcome)
|
|
58
|
+
VALUES ('delete', old.id, old.title, old.objective, COALESCE(old.outcome, ''));
|
|
59
|
+
INSERT INTO quests_fts(rowid, title, objective, outcome)
|
|
60
|
+
SELECT new.id, new.title, new.objective, COALESCE(new.outcome, '')
|
|
61
|
+
WHERE new.deleted_at IS NULL;
|
|
62
|
+
END
|
|
63
|
+
`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/quests/init_quest_tables.ts
|
|
67
|
+
function initQuestTables(db) {
|
|
68
|
+
db.exec(`
|
|
69
|
+
CREATE TABLE IF NOT EXISTS quests (
|
|
70
|
+
id INTEGER PRIMARY KEY,
|
|
71
|
+
questline_id INTEGER REFERENCES questlines(id),
|
|
72
|
+
source_rumor_id INTEGER REFERENCES rumors(id),
|
|
73
|
+
spawned_by_quest_id INTEGER REFERENCES quests(id),
|
|
74
|
+
spawned_from_repeatable_id INTEGER REFERENCES repeatable_quests(id),
|
|
75
|
+
spawned_for_at INTEGER,
|
|
76
|
+
title TEXT NOT NULL,
|
|
77
|
+
objective TEXT NOT NULL,
|
|
78
|
+
outcome TEXT,
|
|
79
|
+
created_at INTEGER NOT NULL,
|
|
80
|
+
updated_at INTEGER NOT NULL,
|
|
81
|
+
started_at INTEGER,
|
|
82
|
+
resolved_at INTEGER,
|
|
83
|
+
success INTEGER CHECK(success IN (0, 1) OR success IS NULL),
|
|
84
|
+
effort_seconds INTEGER NOT NULL DEFAULT 0 CHECK(effort_seconds >= 0),
|
|
85
|
+
estimate_seconds INTEGER CHECK(estimate_seconds IS NULL OR estimate_seconds >= 0),
|
|
86
|
+
not_before_at INTEGER,
|
|
87
|
+
due_at INTEGER,
|
|
88
|
+
scheduled_start_at INTEGER,
|
|
89
|
+
scheduled_end_at INTEGER,
|
|
90
|
+
all_day INTEGER NOT NULL DEFAULT 0 CHECK(all_day IN (0, 1)),
|
|
91
|
+
deleted_at INTEGER,
|
|
92
|
+
CHECK(length(trim(title)) > 0),
|
|
93
|
+
CHECK(length(trim(objective)) > 0),
|
|
94
|
+
CHECK(
|
|
95
|
+
(resolved_at IS NULL AND success IS NULL AND outcome IS NULL)
|
|
96
|
+
OR (resolved_at IS NOT NULL AND success IS NOT NULL AND outcome IS NOT NULL)
|
|
97
|
+
),
|
|
98
|
+
CHECK(started_at IS NULL OR started_at >= created_at),
|
|
99
|
+
CHECK(resolved_at IS NULL OR resolved_at >= created_at),
|
|
100
|
+
CHECK(resolved_at IS NULL OR started_at IS NULL OR resolved_at >= started_at),
|
|
101
|
+
CHECK(scheduled_end_at IS NULL OR scheduled_start_at IS NOT NULL),
|
|
102
|
+
CHECK(scheduled_end_at IS NULL OR scheduled_end_at >= scheduled_start_at),
|
|
103
|
+
CHECK(not_before_at IS NULL OR due_at IS NULL OR not_before_at <= due_at),
|
|
104
|
+
CHECK(
|
|
105
|
+
(spawned_from_repeatable_id IS NULL AND spawned_for_at IS NULL)
|
|
106
|
+
OR (spawned_from_repeatable_id IS NOT NULL AND spawned_for_at IS NOT NULL)
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
`);
|
|
110
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_quests_deleted_at ON quests(deleted_at)");
|
|
111
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_quests_questline_id ON quests(questline_id)");
|
|
112
|
+
db.exec("CREATE INDEX IF NOT EXISTS idx_quests_source_rumor_id ON quests(source_rumor_id)");
|
|
113
|
+
db.exec(
|
|
114
|
+
"CREATE INDEX IF NOT EXISTS idx_quests_spawned_by_quest_id ON quests(spawned_by_quest_id)"
|
|
115
|
+
);
|
|
116
|
+
db.exec(
|
|
117
|
+
"CREATE INDEX IF NOT EXISTS idx_quests_repeatable_anchor ON quests(spawned_from_repeatable_id, spawned_for_at)"
|
|
118
|
+
);
|
|
119
|
+
db.exec(
|
|
120
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_quests_repeatable_anchor_unique ON quests(spawned_from_repeatable_id, spawned_for_at) WHERE spawned_from_repeatable_id IS NOT NULL AND spawned_for_at IS NOT NULL AND deleted_at IS NULL"
|
|
121
|
+
);
|
|
122
|
+
db.exec(
|
|
123
|
+
"CREATE INDEX IF NOT EXISTS idx_quests_due_at ON quests(due_at) WHERE deleted_at IS NULL"
|
|
124
|
+
);
|
|
125
|
+
db.exec(
|
|
126
|
+
"CREATE INDEX IF NOT EXISTS idx_quests_not_before_at ON quests(not_before_at) WHERE deleted_at IS NULL"
|
|
127
|
+
);
|
|
128
|
+
db.exec(
|
|
129
|
+
"CREATE INDEX IF NOT EXISTS idx_quests_scheduled_window ON quests(scheduled_start_at, scheduled_end_at) WHERE deleted_at IS NULL"
|
|
130
|
+
);
|
|
131
|
+
db.exec(
|
|
132
|
+
"CREATE INDEX IF NOT EXISTS idx_quests_lifecycle ON quests(started_at, resolved_at, success) WHERE deleted_at IS NULL"
|
|
133
|
+
);
|
|
134
|
+
db.exec(
|
|
135
|
+
"CREATE INDEX IF NOT EXISTS idx_quests_created_at_active ON quests(created_at DESC, id DESC) WHERE deleted_at IS NULL"
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/repeatable_quests/init_repeatable_quest_tables.ts
|
|
140
|
+
function initRepeatableQuestTables(db) {
|
|
141
|
+
db.exec(`
|
|
142
|
+
CREATE TABLE IF NOT EXISTS repeatable_quests (
|
|
143
|
+
id INTEGER PRIMARY KEY,
|
|
144
|
+
questline_id INTEGER REFERENCES questlines(id),
|
|
145
|
+
title TEXT NOT NULL,
|
|
146
|
+
objective TEXT NOT NULL,
|
|
147
|
+
rrule TEXT NOT NULL,
|
|
148
|
+
anchor_at INTEGER NOT NULL,
|
|
149
|
+
not_before_offset_seconds INTEGER,
|
|
150
|
+
due_offset_seconds INTEGER,
|
|
151
|
+
scheduled_start_offset_seconds INTEGER,
|
|
152
|
+
scheduled_end_offset_seconds INTEGER,
|
|
153
|
+
all_day INTEGER NOT NULL DEFAULT 0 CHECK(all_day IN (0, 1)),
|
|
154
|
+
estimate_seconds INTEGER CHECK(estimate_seconds IS NULL OR estimate_seconds >= 0),
|
|
155
|
+
created_at INTEGER NOT NULL,
|
|
156
|
+
updated_at INTEGER NOT NULL,
|
|
157
|
+
archived_at INTEGER,
|
|
158
|
+
deleted_at INTEGER,
|
|
159
|
+
CHECK(length(trim(title)) > 0),
|
|
160
|
+
CHECK(length(trim(objective)) > 0),
|
|
161
|
+
CHECK(scheduled_end_offset_seconds IS NULL OR scheduled_start_offset_seconds IS NOT NULL),
|
|
162
|
+
CHECK(
|
|
163
|
+
scheduled_end_offset_seconds IS NULL
|
|
164
|
+
OR scheduled_end_offset_seconds >= scheduled_start_offset_seconds
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
`);
|
|
168
|
+
db.exec(
|
|
169
|
+
"CREATE INDEX IF NOT EXISTS idx_repeatable_quests_deleted_at ON repeatable_quests(deleted_at)"
|
|
170
|
+
);
|
|
171
|
+
db.exec(
|
|
172
|
+
"CREATE INDEX IF NOT EXISTS idx_repeatable_quests_archived_at ON repeatable_quests(archived_at)"
|
|
173
|
+
);
|
|
174
|
+
db.exec(
|
|
175
|
+
"CREATE INDEX IF NOT EXISTS idx_repeatable_quests_active_anchor ON repeatable_quests(anchor_at) WHERE deleted_at IS NULL AND archived_at IS NULL"
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// src/rewards/init_reward_tables.ts
|
|
180
|
+
function initRewardTables(db) {
|
|
181
|
+
db.exec(`
|
|
182
|
+
CREATE TABLE IF NOT EXISTS quest_rewards (
|
|
183
|
+
id INTEGER PRIMARY KEY,
|
|
184
|
+
quest_id INTEGER NOT NULL REFERENCES quests(id),
|
|
185
|
+
kind TEXT NOT NULL,
|
|
186
|
+
name TEXT NOT NULL,
|
|
187
|
+
description TEXT,
|
|
188
|
+
quantity REAL CHECK(quantity IS NULL OR quantity >= 0),
|
|
189
|
+
created_at INTEGER NOT NULL,
|
|
190
|
+
claimed_at INTEGER,
|
|
191
|
+
deleted_at INTEGER,
|
|
192
|
+
CHECK(length(trim(kind)) > 0),
|
|
193
|
+
CHECK(length(trim(name)) > 0)
|
|
194
|
+
)
|
|
195
|
+
`);
|
|
196
|
+
db.exec(
|
|
197
|
+
"CREATE INDEX IF NOT EXISTS idx_quest_rewards_quest_id ON quest_rewards(quest_id) WHERE deleted_at IS NULL"
|
|
198
|
+
);
|
|
199
|
+
db.exec(
|
|
200
|
+
"CREATE INDEX IF NOT EXISTS idx_quest_rewards_claimed_at ON quest_rewards(claimed_at) WHERE deleted_at IS NULL"
|
|
201
|
+
);
|
|
202
|
+
db.exec(`
|
|
203
|
+
CREATE TABLE IF NOT EXISTS repeatable_quest_rewards (
|
|
204
|
+
id INTEGER PRIMARY KEY,
|
|
205
|
+
repeatable_quest_id INTEGER NOT NULL REFERENCES repeatable_quests(id),
|
|
206
|
+
kind TEXT NOT NULL,
|
|
207
|
+
name TEXT NOT NULL,
|
|
208
|
+
description TEXT,
|
|
209
|
+
quantity REAL CHECK(quantity IS NULL OR quantity >= 0),
|
|
210
|
+
created_at INTEGER NOT NULL,
|
|
211
|
+
deleted_at INTEGER,
|
|
212
|
+
CHECK(length(trim(kind)) > 0),
|
|
213
|
+
CHECK(length(trim(name)) > 0)
|
|
214
|
+
)
|
|
215
|
+
`);
|
|
216
|
+
db.exec(
|
|
217
|
+
"CREATE INDEX IF NOT EXISTS idx_repeatable_quest_rewards_repeatable_quest_id ON repeatable_quest_rewards(repeatable_quest_id) WHERE deleted_at IS NULL"
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/rumors/init_rumor_search.ts
|
|
222
|
+
function initRumorSearch(db) {
|
|
223
|
+
db.exec(`
|
|
224
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS rumors_fts USING fts5(
|
|
225
|
+
title,
|
|
226
|
+
details,
|
|
227
|
+
content=rumors,
|
|
228
|
+
content_rowid=id
|
|
229
|
+
)
|
|
230
|
+
`);
|
|
231
|
+
db.exec(`
|
|
232
|
+
CREATE TRIGGER IF NOT EXISTS rumors_fts_insert
|
|
233
|
+
AFTER INSERT ON rumors
|
|
234
|
+
WHEN new.deleted_at IS NULL
|
|
235
|
+
BEGIN
|
|
236
|
+
INSERT INTO rumors_fts(rowid, title, details)
|
|
237
|
+
VALUES (new.id, new.title, COALESCE(new.details, ''));
|
|
238
|
+
END
|
|
239
|
+
`);
|
|
240
|
+
db.exec(`
|
|
241
|
+
CREATE TRIGGER IF NOT EXISTS rumors_fts_update
|
|
242
|
+
AFTER UPDATE OF title, details, deleted_at ON rumors
|
|
243
|
+
BEGIN
|
|
244
|
+
INSERT INTO rumors_fts(rumors_fts, rowid, title, details)
|
|
245
|
+
VALUES ('delete', old.id, old.title, COALESCE(old.details, ''));
|
|
246
|
+
INSERT INTO rumors_fts(rowid, title, details)
|
|
247
|
+
SELECT new.id, new.title, COALESCE(new.details, '')
|
|
248
|
+
WHERE new.deleted_at IS NULL;
|
|
249
|
+
END
|
|
250
|
+
`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/rumors/init_rumor_tables.ts
|
|
254
|
+
function initRumorTables(db) {
|
|
255
|
+
db.exec(`
|
|
256
|
+
CREATE TABLE IF NOT EXISTS rumors (
|
|
257
|
+
id INTEGER PRIMARY KEY,
|
|
258
|
+
title TEXT NOT NULL,
|
|
259
|
+
details TEXT,
|
|
260
|
+
created_at INTEGER NOT NULL,
|
|
261
|
+
updated_at INTEGER NOT NULL,
|
|
262
|
+
settled_at INTEGER,
|
|
263
|
+
dismissed_at INTEGER,
|
|
264
|
+
deleted_at INTEGER,
|
|
265
|
+
CHECK(length(trim(title)) > 0),
|
|
266
|
+
CHECK(settled_at IS NULL OR dismissed_at IS NULL)
|
|
267
|
+
)
|
|
268
|
+
`);
|
|
269
|
+
db.exec(
|
|
270
|
+
"CREATE INDEX IF NOT EXISTS idx_rumors_open ON rumors(settled_at, dismissed_at, deleted_at)"
|
|
271
|
+
);
|
|
272
|
+
db.exec(
|
|
273
|
+
"CREATE INDEX IF NOT EXISTS idx_rumors_created_at_active ON rumors(created_at DESC, id DESC) WHERE deleted_at IS NULL"
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/tags/init_tag_tables.ts
|
|
278
|
+
function initTagTables(db) {
|
|
279
|
+
db.exec(`
|
|
280
|
+
CREATE TABLE IF NOT EXISTS tags (
|
|
281
|
+
id INTEGER PRIMARY KEY,
|
|
282
|
+
name TEXT NOT NULL,
|
|
283
|
+
normalized_name TEXT NOT NULL,
|
|
284
|
+
created_at INTEGER NOT NULL,
|
|
285
|
+
deleted_at INTEGER,
|
|
286
|
+
CHECK(length(trim(name)) > 0),
|
|
287
|
+
CHECK(length(trim(normalized_name)) > 0)
|
|
288
|
+
)
|
|
289
|
+
`);
|
|
290
|
+
db.exec(
|
|
291
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_tags_normalized_name_unique ON tags(normalized_name) WHERE deleted_at IS NULL"
|
|
292
|
+
);
|
|
293
|
+
db.exec(`
|
|
294
|
+
CREATE TABLE IF NOT EXISTS quest_tags (
|
|
295
|
+
quest_id INTEGER NOT NULL REFERENCES quests(id),
|
|
296
|
+
tag_id INTEGER NOT NULL REFERENCES tags(id),
|
|
297
|
+
created_at INTEGER NOT NULL,
|
|
298
|
+
deleted_at INTEGER,
|
|
299
|
+
PRIMARY KEY (quest_id, tag_id, created_at)
|
|
300
|
+
)
|
|
301
|
+
`);
|
|
302
|
+
db.exec(
|
|
303
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_quest_tags_unique ON quest_tags(quest_id, tag_id) WHERE deleted_at IS NULL"
|
|
304
|
+
);
|
|
305
|
+
db.exec(
|
|
306
|
+
"CREATE INDEX IF NOT EXISTS idx_quest_tags_tag_id ON quest_tags(tag_id) WHERE deleted_at IS NULL"
|
|
307
|
+
);
|
|
308
|
+
db.exec(`
|
|
309
|
+
CREATE TABLE IF NOT EXISTS repeatable_quest_tags (
|
|
310
|
+
repeatable_quest_id INTEGER NOT NULL REFERENCES repeatable_quests(id),
|
|
311
|
+
tag_id INTEGER NOT NULL REFERENCES tags(id),
|
|
312
|
+
created_at INTEGER NOT NULL,
|
|
313
|
+
deleted_at INTEGER,
|
|
314
|
+
PRIMARY KEY (repeatable_quest_id, tag_id, created_at)
|
|
315
|
+
)
|
|
316
|
+
`);
|
|
317
|
+
db.exec(
|
|
318
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_repeatable_quest_tags_unique ON repeatable_quest_tags(repeatable_quest_id, tag_id) WHERE deleted_at IS NULL"
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/unlocks/init_unlock_tables.ts
|
|
323
|
+
function initUnlockTables(db) {
|
|
324
|
+
db.exec(`
|
|
325
|
+
CREATE TABLE IF NOT EXISTS quest_unlocks (
|
|
326
|
+
id INTEGER PRIMARY KEY,
|
|
327
|
+
from_quest_id INTEGER NOT NULL REFERENCES quests(id),
|
|
328
|
+
to_quest_id INTEGER NOT NULL REFERENCES quests(id),
|
|
329
|
+
created_at INTEGER NOT NULL,
|
|
330
|
+
deleted_at INTEGER,
|
|
331
|
+
CHECK(from_quest_id <> to_quest_id)
|
|
332
|
+
)
|
|
333
|
+
`);
|
|
334
|
+
db.exec(
|
|
335
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_quest_unlocks_pair_unique ON quest_unlocks(from_quest_id, to_quest_id) WHERE deleted_at IS NULL"
|
|
336
|
+
);
|
|
337
|
+
db.exec(
|
|
338
|
+
"CREATE INDEX IF NOT EXISTS idx_quest_unlocks_to_quest_id ON quest_unlocks(to_quest_id) WHERE deleted_at IS NULL"
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/init_questlog_tables.ts
|
|
343
|
+
function initQuestlogTables(db) {
|
|
344
|
+
initRumorTables(db);
|
|
345
|
+
initQuestlineTables(db);
|
|
346
|
+
initRepeatableQuestTables(db);
|
|
347
|
+
initQuestTables(db);
|
|
348
|
+
initUnlockTables(db);
|
|
349
|
+
initRewardTables(db);
|
|
350
|
+
initTagTables(db);
|
|
351
|
+
initQuestSearch(db);
|
|
352
|
+
initRumorSearch(db);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/surface/open_questlog_db.ts
|
|
356
|
+
async function openQuestlogDb(path) {
|
|
357
|
+
const original = process.emitWarning;
|
|
358
|
+
process.emitWarning = ((warning, ...args) => {
|
|
359
|
+
const type = typeof args[0] === "string" ? args[0] : typeof args[0] === "object" && args[0] !== null && "type" in args[0] ? args[0].type : warning instanceof Error ? warning.name : void 0;
|
|
360
|
+
const message = warning instanceof Error ? warning.message : warning;
|
|
361
|
+
if (type === "ExperimentalWarning" && message.startsWith("SQLite is an experimental feature"))
|
|
362
|
+
return;
|
|
363
|
+
Reflect.apply(original, process, [warning, ...args]);
|
|
364
|
+
});
|
|
365
|
+
let sqlite;
|
|
366
|
+
try {
|
|
367
|
+
sqlite = await import('node:sqlite');
|
|
368
|
+
} finally {
|
|
369
|
+
process.emitWarning = original;
|
|
370
|
+
}
|
|
371
|
+
if (path !== ":memory:") mkdirSync(dirname(path), { recursive: true });
|
|
372
|
+
const db = new sqlite.DatabaseSync(path);
|
|
373
|
+
try {
|
|
374
|
+
db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;");
|
|
375
|
+
const context = db;
|
|
376
|
+
initQuestlogTables(context);
|
|
377
|
+
return context;
|
|
378
|
+
} catch (error) {
|
|
379
|
+
db.close();
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export { openQuestlogDb };
|