@ghostpaw/questlog 0.9.2 → 0.10.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.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export { openQuestlogDb } from './chunk-ZHTX64KD.js';
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { questlogMcpInstructions } from './chunk-SVWAEAKC.js';
3
- import { questlogSkills } from './chunk-6KTQK3ZE.js';
4
- import { reviewQuestlogTool, questlogSurfaceTools } from './chunk-UTUQPSGO.js';
3
+ import { questlogSkills } from './chunk-D2W5MYNO.js';
4
+ import { reviewQuestlogTool, questlogSurfaceTools } from './chunk-55JAQORX.js';
5
+ import './chunk-N7V7VUI5.js';
5
6
  import './chunk-QYSIJ2YN.js';
6
7
 
7
8
  // src/soul.ts
@@ -57,7 +58,8 @@ var questlogSkillSpec = {
57
58
  "- Use `--json` on any plain command.",
58
59
  "- Exit codes: 0 ok / 1 error / 2 usage / 3 needs input.",
59
60
  "- Database: `~/.ghostpaw/questlog.db`; override with `--db` or `GHOSTPAW_QUESTLOG_DB`.",
60
- "- Use one database per agent or project."
61
+ "- Use one database per agent or project.",
62
+ "- Webhooks: `questlog hook add <url>`, `questlog tick --dry-run`, `questlog watch`. Read `questlog skill webhooks` for events, signing, verification, and cron setup."
61
63
  ].join("\n"),
62
64
  description: "Use the release binary ghostpaw-questlog to manage local tasks, calendar commitments, and recurring work. Use when planning, executing, reviewing, or organizing work.",
63
65
  license: "MIT",
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ import { parseCliArgs } from './chunk-GVD6A3RR.js';
3
+ import { watchQuestlog, testHook, removeHook, hookLabel, listHooks, addHook } from './chunk-N7V7VUI5.js';
4
+
5
+ // src/surface/run_hook_command.ts
6
+ async function runHookCommand(command, args, io, open) {
7
+ let db;
8
+ try {
9
+ if (args.includes("--help") || args.includes("-h")) {
10
+ io.stdout.write(
11
+ "questlog hook add <url> [--tag name] [--events types] [--min-gap seconds] [--secret-stdin] [--json]\nquestlog hook list|remove <id>|test <id>|label [name]\nquestlog watch [--interval seconds] [--json]\n"
12
+ );
13
+ return 0;
14
+ }
15
+ const action = command === "watch" ? "watch" : args.shift();
16
+ if (!["add", "list", "remove", "test", "label", "watch"].includes(action ?? ""))
17
+ throw new Error("Expected hook add, list, remove, test, or label");
18
+ const values = parseCliArgs(args, {
19
+ command,
20
+ flags: [
21
+ { name: "json", type: "boolean" },
22
+ ...action === "add" ? [
23
+ { name: "tag", type: "string" },
24
+ { name: "events", type: "string" },
25
+ { name: "min-gap", type: "integer" },
26
+ { name: "secret-stdin", type: "boolean" }
27
+ ] : action === "watch" ? [{ name: "interval", type: "number" }] : []
28
+ ],
29
+ positionals: action === "add" ? [{ name: "url", type: "string", required: true }] : ["remove", "test"].includes(action ?? "") ? [{ name: "id", type: "integer", required: true }] : action === "label" ? [{ name: "name", type: "string" }] : []
30
+ });
31
+ if (action === "watch" && values.interval !== void 0 && Number(values.interval) < 5)
32
+ throw new Error("Interval must be at least 5 seconds");
33
+ db = await open();
34
+ let result;
35
+ switch (action) {
36
+ case "add": {
37
+ let secret;
38
+ if (values.secretStdin) {
39
+ secret = "";
40
+ for await (const chunk of io.stdin) secret += String(chunk);
41
+ secret = secret.replace(/\r?\n$/, "");
42
+ }
43
+ result = addHook(db, {
44
+ url: String(values.url),
45
+ ...values.tag === void 0 ? {} : { tag: String(values.tag) },
46
+ ...values.events === void 0 ? {} : { events: String(values.events).split(",") },
47
+ ...values.minGap === void 0 ? {} : { minGap: Number(values.minGap) },
48
+ ...secret === void 0 ? {} : { secret }
49
+ });
50
+ break;
51
+ }
52
+ case "list":
53
+ result = listHooks(db);
54
+ break;
55
+ case "label":
56
+ result = { label: hookLabel(db, values.name) };
57
+ break;
58
+ case "remove":
59
+ removeHook(db, Number(values.id));
60
+ result = { removed: Number(values.id) };
61
+ break;
62
+ case "test":
63
+ result = await testHook(db, Number(values.id));
64
+ break;
65
+ case "watch": {
66
+ const controller = new AbortController();
67
+ const stop = () => controller.abort();
68
+ process.on("SIGINT", stop);
69
+ process.on("SIGTERM", stop);
70
+ try {
71
+ await watchQuestlog(db, {
72
+ signal: controller.signal,
73
+ interval: Number(values.interval ?? 60),
74
+ onTick: (tick) => io.stdout.write(`${JSON.stringify(tick)}
75
+ `)
76
+ });
77
+ } finally {
78
+ process.off("SIGINT", stop);
79
+ process.off("SIGTERM", stop);
80
+ }
81
+ return 0;
82
+ }
83
+ }
84
+ io.stdout.write(`${JSON.stringify(result, null, 2)}
85
+ `);
86
+ return 0;
87
+ } catch {
88
+ io.stderr.write("Invalid webhook command or unavailable target. Run questlog hook --help.\n");
89
+ return 2;
90
+ } finally {
91
+ db?.close();
92
+ }
93
+ }
94
+
95
+ export { runHookCommand };
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { openQuestlogDb } from './chunk-3B6FMHZU.js';
2
+ import { openQuestlogDb } from './chunk-ZHTX64KD.js';
3
3
  import { questlogMcpInstructions } from './chunk-SVWAEAKC.js';
4
- import { isErrorResult, questlogVersion, resolveQuestlogDbPath } from './chunk-DKH623HU.js';
5
- import { validateInput, questlogSurfaceTools } from './chunk-UTUQPSGO.js';
4
+ import { isErrorResult, resolveQuestlogDbPath } from './chunk-IL2QRKZL.js';
5
+ import { validateInput, questlogSurfaceTools } from './chunk-55JAQORX.js';
6
+ import { questlogVersion } from './chunk-N7V7VUI5.js';
6
7
  import { minimalInput } from './chunk-QYSIJ2YN.js';
7
8
 
8
9
  // src/surfaces/mcp_server.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostpaw/questlog",
3
- "version": "0.9.2",
3
+ "version": "0.10.1",
4
4
  "description": "Unified task and calendar model for Node.js — SQLite, zero dependencies, with a built-in LLM tool facade",
5
5
  "type": "module",
6
6
  "exports": {
@@ -29,7 +29,7 @@ Run tools as `ghostpaw-questlog ...`; use `~/.local/bin/ghostpaw-questlog` if it
29
29
 
30
30
  - `questlog search <words>` before `add` to avoid duplicates.
31
31
  - `ghostpaw-questlog add "..." --tag a,b --due tomorrow`
32
- - `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.
32
+ - `questlog list [--view <view>]`: deliveries, 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.
33
33
  - `questlog done <id> --outcome "..."`
34
34
  - `questlog search <words>`
35
35
  - `questlog show <id>`
@@ -37,6 +37,7 @@ Run tools as `ghostpaw-questlog ...`; use `~/.local/bin/ghostpaw-questlog` if it
37
37
  - Exit codes: 0 ok / 1 error / 2 usage / 3 needs input.
38
38
  - Database: `~/.ghostpaw/questlog.db`; override with `--db` or `GHOSTPAW_QUESTLOG_DB`.
39
39
  - Use one database per agent or project.
40
+ - Webhooks: `questlog hook add <url>`, `questlog tick --dry-run`, `questlog watch`. Read `questlog skill webhooks` for events, signing, verification, and cron setup.
40
41
 
41
42
  ## Appendix: MCP and tool calls
42
43
 
@@ -50,6 +51,28 @@ CLI: `ghostpaw-questlog call <tool> '<JSON object>'` or `ghostpaw-questlog <tool
50
51
 
51
52
  Inspect schemas with `ghostpaw-questlog describe <tool>`. Use `--db <path>` to select a database.
52
53
 
54
+ ### manage_hooks
55
+
56
+ List or remove outbound webhook targets, run a tick, or inspect pending and failed deliveries.
57
+
58
+ Use when: Use to operate configured outbound webhooks. Add and test targets with the CLI.
59
+
60
+ Not for: Use quest tools to change work itself.
61
+
62
+ Writes: writes_state.
63
+
64
+ | Parameter | Type | Required | Values / description |
65
+ | --- | --- | --- | --- |
66
+ | action | string | yes | list, remove, tick, deliveries. Operation. |
67
+ | id | integer | no | Target id. |
68
+ | dry_run | boolean | no | Preview only. |
69
+
70
+ ```sh
71
+ ghostpaw-questlog call manage_hooks '{"action":"list"}'
72
+ ghostpaw-questlog call manage_hooks '{"action":"remove","id":1}'
73
+ ghostpaw-questlog call manage_hooks '{"action":"tick"}'
74
+ ```
75
+
53
76
  ### search_questlog
54
77
 
55
78
  Search quests and rumors by text when you need to find an existing item before inspecting or changing it.
@@ -83,7 +106,7 @@ Read-only.
83
106
 
84
107
  | Parameter | Type | Required | Values / description |
85
108
  | --- | --- | --- | --- |
86
- | view | string | yes | 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. Which operational review surface to return. |
109
+ | view | string | yes | deliveries, 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. Which operational review surface to return. |
87
110
  | filters | object | no | Optional filters for supported review surfaces. |
88
111
  | filters.questlineId | integer | no | Optional questline id used by quest views. |
89
112
  | filters.tagNames | array | no | Optional tag names used by quest views. |
@@ -92,7 +115,7 @@ Read-only.
92
115
  | now | integer | no | Optional evaluation timestamp for time-sensitive views. |
93
116
 
94
117
  ```sh
95
- ghostpaw-questlog call review_questlog '{"view":"questlines"}'
118
+ ghostpaw-questlog call review_questlog '{"view":"deliveries"}'
96
119
  ```
97
120
 
98
121
  ### inspect_questlog_item
@@ -376,6 +399,7 @@ ghostpaw-questlog call retire_work '{"action":"hide","target":{"kind":"quest","i
376
399
 
377
400
  ### Workflow guides
378
401
 
402
+ - `ghostpaw-questlog skill webhooks` ([webhooks](references/webhooks.md)): Configure signed outbound event webhooks and verify deliveries.
379
403
  - `ghostpaw-questlog skill archive-delete-and-prune` ([archive-delete-and-prune](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.
380
404
  - `ghostpaw-questlog skill choose-work-shape` ([choose-work-shape](references/choose-work-shape.md)): Choose the right structure for new work by separating uncertainty, one-shot execution, multi-step arcs, and recurring patterns.
381
405
  - `ghostpaw-questlog skill clarify-ambiguous-requests` ([clarify-ambiguous-requests](references/clarify-ambiguous-requests.md)): Treat ambiguity as a real state, capture uncertainty honestly, and only commit work structure once the next executable shape is clear.
@@ -0,0 +1,91 @@
1
+ # Webhooks
2
+
3
+ Use `questlog` to configure opt-in outbound automation. No server or scheduler is added.
4
+ No targets means no network calls. `manage_hooks` supports list, remove, tick with
5
+ `dry_run`, and deliveries; add/test are CLI-only. `review_questlog` view `deliveries`
6
+ and `questlog show <id>` expose pending/failed deliveries.
7
+
8
+ ```sh
9
+ questlog hook add https://example.org/wake --min-gap 60
10
+ questlog hook add https://example.org/team --tag work --events quest.due,quest.overdue --secret-stdin
11
+ questlog hook label work
12
+ questlog hook list
13
+ questlog tick --dry-run
14
+ questlog tick
15
+ questlog watch --interval 60
16
+ questlog hook test 1
17
+ questlog review deliveries
18
+ questlog show 12
19
+ questlog hook remove 1
20
+ ```
21
+
22
+ `hook add` prints its random 32-byte signing secret once, also as `secret` in `--json`.
23
+ Save it securely. `--secret-stdin` supplies one via stdin; there is no secret flag.
24
+ List output redacts userinfo/query strings. Removal deletes the target and deliveries.
25
+ Default scope is the database; `--tag` matches quest tags, never rumors.
26
+ `--events` defaults to all types. The DB label defaults to the filename without its
27
+ path/extension (`questlog` in memory); set it to a name suitable for disclosure.
28
+
29
+ ```cron
30
+ * * * * * questlog tick
31
+ ```
32
+
33
+ Cron's PATH may need `$HOME/.local/bin/questlog tick --db /path/to/quests.db`.
34
+ Watch defaults to 60 seconds (minimum 5); SIGINT/SIGTERM finishes the current tick.
35
+ Tick exits 0 even when deliveries need retries. Dry-run records nothing and sends nothing.
36
+
37
+ Events are detected from the model at tick time:
38
+
39
+ - `quest.due`: an unresolved, undeleted quest reaches its effective due time (own `due_at`, otherwise its undeleted questline's due time).
40
+ - `quest.overdue`: that effective deadline is strictly earlier than now and the quest remains unresolved.
41
+ - `quest.unblocked`: an unresolved quest is available, all active prerequisites have succeeded or been deleted, and at least one prerequisite succeeded after its unlock was installed. Occurrence time is the latest active prerequisite success or not-before time, whichever is later.
42
+ - `repeatable.spawned`: a concrete repeatable instance was created; occurrence time is its creation time. Tick first materializes due anchors through the existing spawn operation.
43
+ - `rumor.captured`: an undeleted rumor was created; occurrence time is its creation time, even if subsequently settled or dismissed.
44
+
45
+ Persistent event keys contain type, entity ids, and occurrence time. Restarts and repeated
46
+ ticks do not repeat occurrences. Removed transitions between ticks cannot be reconstructed.
47
+ Only occurrences at/after target creation are assigned; tags are evaluated at assignment.
48
+ Tick materializes due repeatables using the existing spawn path, including during preview
49
+ with rollback. Batches contain only ids, event types, timestamps, and the DB label:
50
+
51
+ ```json
52
+ {"type":"questlog.events","db":"work","deliveryId":"d_…","sentAt":"2026-10-28T10:00:00.000Z","events":[{"id":"e_…","type":"quest.due","questId":12,"occurredAt":"2026-10-28T09:59:00.000Z"}]}
53
+ ```
54
+
55
+ Headers are `content-type: application/json`, `user-agent: questlog/<version>`,
56
+ `x-questlog-delivery: <deliveryId>`, `x-questlog-timestamp: <unix seconds>`, and
57
+ `x-questlog-signature: sha256=<hex HMAC-SHA256(secret, timestamp + "." + rawBody)>`.
58
+ No titles, objectives, details, or tag text are sent. `hook test` sends a logged `hook.test`
59
+ event immediately, ignoring the min gap. Add and test are CLI-only in the tool facade;
60
+ `manage_hooks` supports list, remove, tick (with `dry_run`), and deliveries.
61
+
62
+ 2xx delivers. Timeout is 5 seconds. Failures retry after 30 s, 1 m, 2 m, 4 m, doubling
63
+ to 1 h plus <1 s jitter, subject to the target's min gap. Eight failures mark failed.
64
+ Retries keep the delivery id/events; new events queue behind them. Keep the last 500
65
+ delivered log entries per target and all pending/failed entries. Transactional claims
66
+ lease work for two minutes. A crash after receipt may retry: dedupe at the receiver.
67
+
68
+ ### Verifying a delivery
69
+
70
+ Compute HMAC-SHA256 over `timestamp + "." + rawBody` with the secret and compare in constant
71
+ time. Reject timestamps older than 5 minutes (and timestamps too far in the future), then
72
+ dedupe on `x-questlog-delivery`. Use durable deduplication storage in a production receiver.
73
+ This 15-line Node example uses an in-memory set:
74
+
75
+ ```js
76
+ import { createHmac, timingSafeEqual } from 'node:crypto';
77
+ import { createServer } from 'node:http';
78
+ const secret = process.env.WEBHOOK_SECRET, seen = new Set();
79
+ createServer(async (req, res) => {
80
+ const chunks = []; for await (const chunk of req) chunks.push(chunk);
81
+ const rawBody = Buffer.concat(chunks), h = req.headers;
82
+ const timestamp = h['x-questlog-timestamp'], id = h['x-questlog-delivery'];
83
+ const age = Date.now() / 1000 - Number(timestamp);
84
+ const expected = createHmac('sha256', secret).update(timestamp + '.').update(rawBody).digest();
85
+ const signature = String(h['x-questlog-signature'] || '');
86
+ const actual = Buffer.from(signature.slice(7), 'hex');
87
+ if (!id || !/^\d+$/.test(timestamp) || age > 300 || age < -300 || !/^sha256=[a-f0-9]{64}$/.test(signature) || actual.length !== expected.length || !timingSafeEqual(actual, expected)) { res.writeHead(401).end(); return; }
88
+ if (!seen.has(id)) { seen.add(id); console.log(JSON.parse(rawBody)); }
89
+ res.writeHead(204).end();
90
+ }).listen(8080, '127.0.0.1');
91
+ ```
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export { openQuestlogDb } from './chunk-3B6FMHZU.js';