@ghostpaw/questlog 0.11.0 → 0.11.2

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 CHANGED
@@ -81,6 +81,9 @@ questlog list --view quests.resolved --json
81
81
  `--questline <id>`, and `--due` as YYYY-MM-DD, DD.MM.YYYY, `today`, `tomorrow`,
82
82
  `+3d`, an ISO date-time with timezone, or milliseconds since epoch. Date-only
83
83
  values use local end of day.
84
+ `--objective` is the one flag that reads a file: `@path`, `@-` for stdin, and
85
+ `@@text` for a literal `@text` (help marks it `(@file, @- stdin, @@ literal @)`). Every
86
+ other value, including titles, `--outcome` and per-tool flags, is literal.
84
87
  `list` defaults to available quests; `list --help` lists all supported views.
85
88
  Its `--limit` caps text output; JSON preserves the full review tool result.
86
89
  `search --limit` caps matches in both formats. `show` prints full quest detail.
@@ -91,8 +94,8 @@ adds ISO UTC siblings for numeric timestamp fields ending in `At`.
91
94
  Commands print text by default, including hooks, delivery views and each watch tick; `call` and direct tool commands return the raw tool result as JSON. Use `--json` for the full tool result
92
95
  and `--compact` for compact JSON. `--db path` works before or after the command.
93
96
  Each command accepts `--help`. Exit codes are 0 for success, 1 for invalid input
94
- or a tool error, 2 for usage errors, and 3 when clarification is needed or `hook remove`/`hook test` addresses a missing target.
95
- Missing quests and deliveries exit 1. `hook label` names the database, not an individual target.
97
+ or a tool error, 2 for usage errors, and 3 when clarification is needed or a quest, delivery, or hook target is not found.
98
+ An empty `hook list` prints `No hooks` (`[]` with `--json`). Delivery text timestamps use local time; JSON retains UTC/epoch timestamps. `hook label` names the database, not an individual target.
96
99
 
97
100
  `questlog skill --short` prints the compact cheat sheet; `questlog skill --list` lists guides.
98
101
  ISO date-times without a zone (for example `2026-10-29T09:00`) use the local time zone.
@@ -163,7 +166,7 @@ irm https://raw.githubusercontent.com/GhostPawJS/questlog/main/install.ps1 | iex
163
166
  Pinned to a release:
164
167
 
165
168
  ```sh
166
- curl -fsSL https://raw.githubusercontent.com/GhostPawJS/questlog/main/install.sh | sh -s -- --target linux-x64 --version 0.11.0 && export PATH="$HOME/.local/bin:$PATH"
169
+ curl -fsSL https://raw.githubusercontent.com/GhostPawJS/questlog/main/install.sh | sh -s -- --target linux-x64 --version 0.11.1 && export PATH="$HOME/.local/bin:$PATH"
167
170
  ```
168
171
 
169
172
  Also adding ~/.local/bin to ~/.profile (edits ~/.profile once; skipped if it already mentions ~/.local/bin; zsh login shells read ~/.zprofile instead, so use that file name there):
@@ -180,7 +183,7 @@ $env:GHOSTPAW_NO_MODIFY_PATH=1; irm https://raw.githubusercontent.com/GhostPawJS
180
183
 
181
184
  ```text
182
185
  $ ghostpaw-questlog --version
183
- questlog 0.11.0 (binary, linux-x64)
186
+ questlog 0.11.1 (binary, linux-x64)
184
187
  ```
185
188
 
186
189
  Scripts that compare versions should use `--version --short` or `--version --json`.
@@ -550,9 +553,11 @@ Secrets and custom header values are stored in the database. On POSIX, new datab
550
553
  and their WAL/SHM siblings use mode 0600, and directories created for them use 0700.
551
554
  Existing database permissions are preserved; Windows uses its existing access controls.
552
555
 
553
- Add repeatable custom headers with `--header "Authorization: Bearer value"` or
554
- `--header-stdin` (one `Name: value` line per header, keeping values out of shell history).
555
- For example: `questlog hook add https://example.org/wake --header "X-Key: value"`.
556
+ Prefer `--header-stdin` for real keys (one `Name: value` line per header), keeping
557
+ values out of shell history and process arguments:
558
+ `questlog hook add https://example.org/wake --header-stdin < /secure/headers.txt`.
559
+ Keep that file private (0600 on POSIX). Repeatable `--header "Name: value"` remains
560
+ available, but exposes values in process arguments.
556
561
  Headers accompany every delivery and test. Header names are shown with values replaced by
557
562
  `***`; values are never included in list, show, test, tick, watch, or tool output.
558
563
  Names must be HTTP tokens; `host`, `content-length`, `content-type`, and `x-questlog-*`
package/SKILL.md CHANGED
@@ -35,7 +35,7 @@ Run tools as `ghostpaw-questlog ...`; use `~/.local/bin/ghostpaw-questlog` if it
35
35
  - `questlog show 12`
36
36
  - `questlog list --view quests.available`
37
37
  - Use `--json` on any plain command.
38
- - Exit codes: 0 ok / 1 error / 2 usage / 3 needs input or hook target not found.
38
+ - Exit codes: 0 ok / 1 error / 2 usage / 3 needs input or item not found.
39
39
  - Database: `~/.ghostpaw/questlog.db`; override with `--db` or `GHOSTPAW_QUESTLOG_DB`.
40
40
  - Use one database per project.
41
41
  - `questlog --version` prints the installed version.
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // src/surfaces/text_output.ts
3
- var isTime = (key, value) => typeof value === "number" && value > 1e12 && /(At|Wake)$/.test(key);
3
+ var isTime = (key, value) => /(At|Wake)$/.test(key) && (typeof value === "number" || typeof value === "string" && /^\d{4}-\d{2}-\d{2}T/.test(value)) && Number.isFinite(new Date(value).getTime());
4
4
  function scalar(key, value) {
5
5
  if (value == null || value === "") return "-";
6
- if (isTime(key, value)) return new Date(value).toISOString();
6
+ if (isTime(key, value)) return new Date(value).toLocaleString("en-GB", { timeZoneName: "short" });
7
7
  return String(value).replace(/[\r\n]+/g, " ");
8
8
  }
9
9
  function lines(value, indent, key = "") {
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { tickQuestlog, removeHook, listDueRepeatableQuestAnchors, getRepeatableQuestOrThrow, spawnDueRepeatableQuests, deriveQuestState, deriveQuestMarkerId, queryQuestDetailRecords, resolveNow, withTransaction, getQuestOrThrow, parseRRule, mapQuestRewardRow, normalizeTagName } from './chunk-RBGQK5WP.js';
2
+ import { tickQuestlog, removeHook, listDueRepeatableQuestAnchors, getRepeatableQuestOrThrow, spawnDueRepeatableQuests, deriveQuestState, deriveQuestMarkerId, queryQuestDetailRecords, resolveNow, withTransaction, getQuestOrThrow, parseRRule, mapQuestRewardRow, normalizeTagName } from './chunk-S5EP3A3L.js';
3
3
  import { listDeliveries } from './chunk-D6EGZCT7.js';
4
4
  import { listHooks } from './chunk-NBCCA6AX.js';
5
5
  import { registerSchemaActions, minimalInput, requiredForAction } from './chunk-QYSIJ2YN.js';
@@ -591,6 +591,7 @@ function listRumors(db) {
591
591
  // src/surfaces/validate_input.ts
592
592
  function validateInput(schema, input, path = "$") {
593
593
  const errors = [];
594
+ const received = `; received ${JSON.stringify(input)}`;
594
595
  const matchesType = (type) => {
595
596
  switch (type) {
596
597
  case "object":
@@ -608,13 +609,13 @@ function validateInput(schema, input, path = "$") {
608
609
  }
609
610
  };
610
611
  if (schema.type && !(Array.isArray(schema.type) ? schema.type : [schema.type]).some(matchesType)) {
611
- return [`${path}: expected ${String(schema.type)}`];
612
+ return [`${path}: expected ${schema.type}${received}`];
612
613
  }
613
614
  if (schema.enum && !schema.enum.some((value) => JSON.stringify(value) === JSON.stringify(input)))
614
- errors.push(`${path}: expected one of ${JSON.stringify(schema.enum)}`);
615
+ errors.push(`${path}: expected one of ${JSON.stringify(schema.enum)}${received}`);
615
616
  const constant = "const" in schema ? schema.const : void 0;
616
617
  if (constant !== void 0 && constant !== input)
617
- errors.push(`${path}: expected ${JSON.stringify(constant)}`);
618
+ errors.push(`${path}: expected ${JSON.stringify(constant)}${received}`);
618
619
  if (schema.oneOf) {
619
620
  const branches = schema.oneOf.map((branch) => validateInput(branch, input, path));
620
621
  if (!branches.some((errors2) => errors2.length === 0)) {
@@ -625,7 +626,7 @@ function validateInput(schema, input, path = "$") {
625
626
  );
626
627
  }
627
628
  }
628
- if (typeof input === "object" && input !== null && !Array.isArray(input)) {
629
+ if (matchesType("object")) {
629
630
  const record = input;
630
631
  for (const name of [...schema.required ?? [], ...requiredForAction(schema, record.action)])
631
632
  if (!Object.hasOwn(record, name)) errors.push(`${path}.${name}: required`);
@@ -4,20 +4,26 @@ import { readFileSync } from 'node:fs';
4
4
  var CliUsageError = class extends Error {
5
5
  exitCode = 2;
6
6
  };
7
+ function readFileValue(raw, name) {
8
+ if (!raw.startsWith("@")) return raw;
9
+ if (raw.startsWith("@@")) return raw.slice(1);
10
+ const path = raw.slice(1);
11
+ if (!path) throw new CliUsageError(`Empty file path for ${name}; use @@ for a literal leading @`);
12
+ try {
13
+ return readFileSync(path === "-" ? 0 : path, "utf8");
14
+ } catch {
15
+ throw new CliUsageError(
16
+ `Cannot read value file for ${name}: ${path} (use @@ for a literal leading @)`
17
+ );
18
+ }
19
+ }
7
20
  var keyOf = (name) => name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
8
21
  function parseCliArgs(argv, spec) {
9
22
  const values = {};
10
23
  const positions = [];
11
24
  const flags = spec.flags ?? [];
12
25
  const convert = (raw, arg) => {
13
- let value = raw;
14
- if (raw.startsWith("@")) {
15
- try {
16
- value = readFileSync(raw.slice(1), "utf8");
17
- } catch {
18
- throw new CliUsageError(`Cannot read value file: ${raw.slice(1)}`);
19
- }
20
- }
26
+ const value = arg.file ? readFileValue(raw, arg.name) : raw;
21
27
  let result = value;
22
28
  if (arg.type === "number" || arg.type === "integer") {
23
29
  result = Number(value);
@@ -25,17 +31,19 @@ function parseCliArgs(argv, spec) {
25
31
  throw new CliUsageError(`Expected ${arg.type} for ${arg.name}: ${value}`);
26
32
  } else if (arg.type === "boolean") {
27
33
  if (!["true", "false"].includes(value))
28
- throw new CliUsageError(`Expected boolean for ${arg.name}`);
34
+ throw new CliUsageError(`Expected boolean for ${arg.name}: ${value}`);
29
35
  result = value === "true";
30
36
  } else if (arg.type === "json") {
31
37
  try {
32
38
  result = JSON.parse(value);
33
39
  } catch {
34
- throw new CliUsageError(`Invalid JSON for ${arg.name}`);
40
+ throw new CliUsageError(`Invalid JSON for ${arg.name}: ${value}`);
35
41
  }
36
42
  }
37
43
  if (arg.enum && !arg.enum.includes(result))
38
- throw new CliUsageError(`${arg.name} must be one of: ${arg.enum.join(", ")}`);
44
+ throw new CliUsageError(
45
+ `${arg.name} ${JSON.stringify(value)} must be one of: ${arg.enum.join(", ")}`
46
+ );
39
47
  return result;
40
48
  };
41
49
  let ended = false;
@@ -94,7 +102,7 @@ function parseCliArgs(argv, spec) {
94
102
  return values;
95
103
  }
96
104
  function renderCliHelp(spec) {
97
- const describe = (arg) => `${arg.type}${arg.enum ? ` (${arg.enum.join("|")})` : ""}${arg.required ? " required" : ""} ${arg.description ?? ""}`;
105
+ const describe = (arg) => `${arg.type}${arg.enum ? ` (${arg.enum.join("|")})` : ""}${arg.required ? " required" : ""}${arg.file ? " (@file, @- stdin, @@ literal @)" : ""} ${arg.description ?? ""}`;
98
106
  return `Usage: ${spec.command} ${(spec.positionals ?? []).map((arg) => `<${arg.name}${arg.variadic ? "..." : ""}>`).join(" ")} [flags]
99
107
  ${spec.description ?? ""}
100
108
 
@@ -728,7 +728,7 @@ Failure handling:
728
728
  var webhooksSkill = defineQuestlogSkill({
729
729
  name: "webhooks",
730
730
  description: "Configure signed outbound event webhooks and verify deliveries.",
731
- content: '# Webhooks\n\nUse `questlog` to configure opt-in outbound automation. No server or scheduler is added.\nNo targets means no network calls. `manage_hooks` supports list, remove, tick with\n`dry_run`, and deliveries; add/test are CLI-only. `review_questlog` view `deliveries`\nand `questlog show <id>` expose pending/failed deliveries.\n\n```sh\nquestlog hook add https://example.org/wake --min-gap 60\nquestlog hook add https://example.org/team --tag work --events quest.due,quest.overdue --secret-stdin\nquestlog hook label work\nquestlog hook list\nquestlog tick --dry-run\nquestlog tick\nquestlog watch --interval 60\nquestlog hook test 1\nquestlog review deliveries\nquestlog show 12\nquestlog hook remove 1\n```\n\n`hook add` prints its random 32-byte signing secret once, also as `secret` in `--json`.\nSave it securely. `--secret-stdin` supplies one via stdin; there is no secret flag.\nSecrets and custom header values are stored in the DB. New POSIX DB/WAL/SHM files use 0600 and created directories use 0700; existing DB modes are preserved. Windows uses existing access controls.\nUse repeatable `--header "Name: value"` or `--header-stdin` (one line per header, avoiding shell history). Headers are sent on every delivery and test; output shows names with `***` values. HTTP token names are required; host, content-length, content-type, and x-questlog-* are reserved. Change headers by removing and adding the hook again. Use --header-stdin and --secret-stdin separately. URL userinfo is rejected; use a custom header or query key.\n`questlog show d_\u2026` displays delivery status, attempts, HTTP status, next attempt, redacted target URL and event ids. Text is the default; --json prints JSON. Missing hook remove/test targets exit 3; missing quests/deliveries exit 1. hook label names the database.\nList output redacts userinfo/query strings. Removal deletes the target and deliveries.\nDefault scope is the database; `--tag` matches quest tags, never rumors.\n`--events` defaults to all types. The DB label defaults to the filename without its\npath/extension (`questlog` in memory); set it to a name suitable for disclosure.\n\n```cron\n* * * * * questlog tick\n```\n\nCron\'s PATH may need `$HOME/.local/bin/questlog tick --db /path/to/quests.db`.\nWatch defaults to 60 seconds (minimum 5); SIGINT/SIGTERM finishes the current tick.\nTick exits 0 even when deliveries need retries. Dry-run records nothing and sends nothing.\n\nEvents are detected from the model at tick time:\n\n- `quest.due`: an unresolved, undeleted quest reaches its effective due time (own `due_at`, otherwise its undeleted questline\'s due time).\n- `quest.overdue`: that effective deadline is strictly earlier than now and the quest remains unresolved.\n- `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.\n- `repeatable.spawned`: a concrete repeatable instance was created; occurrence time is its creation time. Tick first materializes due anchors through the existing spawn operation.\n- `rumor.captured`: an undeleted rumor was created; occurrence time is its creation time, even if subsequently settled or dismissed.\n\nPersistent event keys contain type, entity ids, and occurrence time. Restarts and repeated\nticks do not repeat occurrences. Removed transitions between ticks cannot be reconstructed.\nOnly occurrences at/after target creation are assigned; tags are evaluated at assignment.\nTick materializes due repeatables using the existing spawn path, including during preview\nwith rollback. Batches contain only ids, event types, timestamps, and the DB label:\n\n```json\n{"type":"questlog.events","db":"work","deliveryId":"d_\u2026","sentAt":"2026-10-28T10:00:00.000Z","events":[{"id":"e_\u2026","type":"quest.due","questId":12,"occurredAt":"2026-10-28T09:59:00.000Z"}]}\n```\n\nHeaders are `content-type: application/json`, `user-agent: questlog/<version>`,\n`x-questlog-delivery: <deliveryId>`, `x-questlog-timestamp: <unix seconds>`, and\n`x-questlog-signature: sha256=<hex HMAC-SHA256(secret, timestamp + "." + rawBody)>`.\nNo titles, objectives, details, or tag text are sent. `hook test` sends a logged `hook.test`\nevent immediately, ignoring the min gap. Add and test are CLI-only in the tool facade;\n`manage_hooks` supports list, remove, tick (with `dry_run`), and deliveries.\n\n2xx delivers. Timeout is 5 seconds. Failures retry after 30 s, 1 m, 2 m, 4 m, doubling\nto 1 h plus <1 s jitter, subject to the target\'s min gap. Eight failures mark failed.\nRetries keep the delivery id/events; new events queue behind them. Keep the last 500\ndelivered log entries per target and all pending/failed entries. Transactional claims\nlease work for two minutes. A crash after receipt may retry: dedupe at the receiver.\n\n### Verifying a delivery\n\nCompute HMAC-SHA256 over `timestamp + "." + rawBody` with the secret and compare in constant\ntime. Reject timestamps older than 5 minutes (and timestamps too far in the future), then\ndedupe on `x-questlog-delivery`. Use durable deduplication storage in a production receiver.\nThis 15-line Node example uses an in-memory set:\n\n```js\nimport { createHmac, timingSafeEqual } from \'node:crypto\';\nimport { createServer } from \'node:http\';\nconst secret = process.env.WEBHOOK_SECRET, seen = new Set();\ncreateServer(async (req, res) => {\n const chunks = []; for await (const chunk of req) chunks.push(chunk);\n const rawBody = Buffer.concat(chunks), h = req.headers;\n const timestamp = h[\'x-questlog-timestamp\'], id = h[\'x-questlog-delivery\'];\n const age = Date.now() / 1000 - Number(timestamp);\n const expected = createHmac(\'sha256\', secret).update(timestamp + \'.\').update(rawBody).digest();\n const signature = String(h[\'x-questlog-signature\'] || \'\');\n const actual = Buffer.from(signature.slice(7), \'hex\');\n 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; }\n if (!seen.has(id)) { seen.add(id); console.log(JSON.parse(rawBody)); }\n res.writeHead(204).end();\n}).listen(8080, \'127.0.0.1\');\n```\n'
731
+ content: '# Webhooks\n\nUse `questlog` to configure opt-in outbound automation. No targets means no network calls. `manage_hooks` supports list, remove, tick with\n`dry_run`, and deliveries; add/test are CLI-only. `review_questlog` view `deliveries`\nand `questlog show <id>` expose pending/failed deliveries.\n\n```sh\nquestlog hook add https://example.org/wake --min-gap 60\nquestlog hook add https://example.org/team --tag work --events quest.due,quest.overdue --secret-stdin\nquestlog hook label work\nquestlog hook list\nquestlog tick --dry-run\nquestlog tick\nquestlog watch --interval 60\nquestlog hook test 1\nquestlog review deliveries\nquestlog show 12\nquestlog hook remove 1\n```\n\n`hook add` prints its random 32-byte signing secret once, also as `secret` in `--json`.\nSave it securely. `--secret-stdin` supplies one via stdin; there is no secret flag.\nSecrets/headers are stored in the DB. New POSIX DB/WAL/SHM files use 0600; new directories use 0700. Existing modes and Windows access controls are preserved.\nPrefer `--header-stdin` for real keys (one Name: value per line); use a private 0600 file. Repeatable `--header` exposes values in process arguments and shell history. Output redacts values as `***`. host, content-length, content-type and x-questlog-* are reserved. To change headers, remove/add the hook. Use --header-stdin and --secret-stdin separately. URL userinfo is rejected.\n`show d_\u2026` displays delivery attempts, HTTP status, next attempt, redacted target and event ids. Text timestamps are local; --json keeps UTC/epoch times. Missing hook remove/test targets exit 3; missing quests/deliveries also exit 3. Empty hook lists print `No hooks`. hook label names the database.\nList output redacts userinfo/query strings. Removal deletes the target and deliveries.\nDefault scope is the DB; `--tag` matches quest tags only.\n`--events` defaults to all types. The DB label defaults to the filename without its\npath/extension (`questlog` in memory); set it to a name suitable for disclosure.\n\n```cron\n* * * * * questlog tick\n```\n\nCron may need `$HOME/.local/bin/questlog tick --db /path/to/quests.db`.\nWatch defaults to 60 s (minimum 5); SIGINT/SIGTERM finishes the tick.\nTick exits 0 even when deliveries need retries. Dry-run records nothing and sends nothing.\n\nEvents are detected from the model at tick time:\n\n- `quest.due`: an unresolved, undeleted quest reaches its effective due time (own `due_at`, otherwise its undeleted questline\'s due time).\n- `quest.overdue`: that effective deadline is strictly earlier than now and the quest remains unresolved.\n- `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.\n- `repeatable.spawned`: a concrete repeatable instance was created; occurrence time is its creation time. Tick first materializes due anchors through the existing spawn operation.\n- `rumor.captured`: an undeleted rumor was created; occurrence time is its creation time, even if subsequently settled or dismissed.\n\nEvent keys include type, entity ids and occurrence time, preventing repeats across restarts. Transitions removed between ticks cannot be reconstructed.\nOnly events at/after target creation are assigned; tags are checked then.\nTick materializes due repeatables using the existing spawn path, including during preview\nwith rollback. Batches contain only ids, event types, timestamps, and the DB label:\n\n```json\n{"type":"questlog.events","db":"work","deliveryId":"d_\u2026","sentAt":"2026-10-28T10:00:00.000Z","events":[{"id":"e_\u2026","type":"quest.due","questId":12,"occurredAt":"2026-10-28T09:59:00.000Z"}]}\n```\n\nHeaders are `content-type: application/json`, `user-agent: questlog/<version>`,\n`x-questlog-delivery: <deliveryId>`, `x-questlog-timestamp: <unix seconds>`, and\n`x-questlog-signature: sha256=<hex HMAC-SHA256(secret, timestamp + "." + rawBody)>`.\nNo titles, objectives, details, or tag text are sent. `hook test` sends a logged `hook.test`\nevent immediately, ignoring the min gap. Add and test are CLI-only in the tool facade;\n`manage_hooks` supports list, remove, tick (with `dry_run`), and deliveries.\n\n2xx delivers. Timeout is 5 seconds. Failures retry after 30 s, 1 m, 2 m, 4 m, doubling\nto 1 h plus <1 s jitter, subject to the target\'s min gap. Eight failures mark failed.\nRetries keep the delivery id/events; new events queue behind them. Keep the last 500\ndelivered log entries per target and all pending/failed entries. Claims lease work for two minutes. Dedupe retries at the receiver.\n\n### Verifying a delivery\n\nCompute HMAC-SHA256 over `timestamp + "." + rawBody` with the secret and compare in constant\ntime. Reject timestamps older than 5 minutes (and timestamps too far in the future), then\ndedupe on `x-questlog-delivery`. Use durable deduplication in production; this example uses an in-memory set:\n\n```js\nimport { createHmac, timingSafeEqual } from \'node:crypto\';\nimport { createServer } from \'node:http\';\nconst secret = process.env.WEBHOOK_SECRET, seen = new Set();\ncreateServer(async (req, res) => {\n const chunks = []; for await (const chunk of req) chunks.push(chunk);\n const rawBody = Buffer.concat(chunks), h = req.headers;\n const timestamp = h[\'x-questlog-timestamp\'], id = h[\'x-questlog-delivery\'];\n const age = Date.now() / 1000 - Number(timestamp);\n const expected = createHmac(\'sha256\', secret).update(timestamp + \'.\').update(rawBody).digest();\n const signature = String(h[\'x-questlog-signature\'] || \'\');\n const actual = Buffer.from(signature.slice(7), \'hex\');\n 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; }\n if (!seen.has(id)) { seen.add(id); console.log(JSON.parse(rawBody)); }\n res.writeHead(204).end();\n}).listen(8080, \'127.0.0.1\');\n```\n'
732
732
  });
733
733
 
734
734
  // src/skills/skill_registry.ts
@@ -69,14 +69,14 @@ function addHook(db, input) {
69
69
  );
70
70
  const minGap = input.minGap ?? 60;
71
71
  if (!Number.isSafeInteger(minGap) || minGap < 0)
72
- throw new Error("Min gap must be a nonnegative integer");
72
+ throw new Error(`Min gap ${minGap} must be a nonnegative integer`);
73
73
  const events = input.events ?? [...hookEventTypes];
74
74
  if (!events.length || events.some((e) => !hookEventTypes.includes(e)))
75
75
  throw new Error(
76
76
  `unknown event ${events.find((e) => !hookEventTypes.includes(e)) ?? "(empty)"}; valid: ${hookEventTypes.join(", ")}`
77
77
  );
78
78
  if (input.tag !== void 0 && !normalizeTagName(input.tag))
79
- throw new Error("Expected a tag name");
79
+ throw new Error(`Expected a tag name: ${JSON.stringify(input.tag)}`);
80
80
  const secret = input.secret ?? randomBytes(32).toString("hex");
81
81
  if (!secret.trim()) throw new Error("Expected a nonempty signing secret");
82
82
  const result = db.prepare(
@@ -116,7 +116,7 @@ function removeHook(db, id) {
116
116
  }
117
117
 
118
118
  // src/version.ts
119
- var questlogVersion = "0.11.0";
119
+ var questlogVersion = "0.11.2";
120
120
 
121
121
  // src/hooks/send_hook_delivery.ts
122
122
  async function sendHookDelivery(db, delivery, target, options) {
package/dist/cli.js CHANGED
@@ -13,5 +13,5 @@ function checkNodeVersion(version, stderr) {
13
13
  if (!checkNodeVersion(process.versions.node, process.stderr)) {
14
14
  process.exitCode = 2;
15
15
  } else {
16
- await import('./cli_main-TEG3QTDP.js');
16
+ await import('./cli_main-KHRJVLLC.js');
17
17
  }
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { questlogSkills } from './chunk-I2AMIXGP.js';
3
- import { CliUsageError, renderCliHelp, parseCliArgs } from './chunk-GVD6A3RR.js';
2
+ import { questlogSkills } from './chunk-PQAQZUCH.js';
3
+ import { CliUsageError, renderCliHelp, parseCliArgs } from './chunk-M3WAPFGB.js';
4
4
  import { isErrorResult, resolveQuestlogDbPath } from './chunk-IL2QRKZL.js';
5
- import { reviewQuestlogTool, validateInput, getQuestDetail, questlogSurfaceTools } from './chunk-KOWIVBG6.js';
6
- import { questlogVersion } from './chunk-RBGQK5WP.js';
5
+ import { reviewQuestlogTool, validateInput, getQuestDetail, questlogSurfaceTools } from './chunk-FCHLDYAR.js';
6
+ import { questlogVersion } from './chunk-S5EP3A3L.js';
7
7
  import { listDeliveries } from './chunk-D6EGZCT7.js';
8
8
  import './chunk-NBCCA6AX.js';
9
9
  import { schemaRows } from './chunk-7KTRNXTU.js';
@@ -119,7 +119,7 @@ Missing: ${r.clarification.missing.map((name) => `--${name}`).join(", ")}
119
119
  ${r.error.message}
120
120
  `;
121
121
  if (r.data.deliveries && !r.data.item)
122
- return `${r.data.deliveries.map((d) => `${d.id} target ${d.targetId} ${d.status} attempts ${d.attempts} HTTP ${d.httpStatus ?? "-"} next ${new Date(d.nextAttemptAt).toISOString()} events ${d.events.map((e) => e.id).join(", ")}`).join("\n") || "No deliveries"}
122
+ return `${r.data.deliveries.map((d) => `${d.id} target ${d.targetId} ${d.status} attempts ${d.attempts} HTTP ${d.httpStatus ?? "-"} next ${formatPlainDate(d.nextAttemptAt, true)} events ${d.events.map((e) => e.id).join(", ")}`).join("\n") || "No deliveries"}
123
123
  `;
124
124
  if (r.data.items) {
125
125
  const items = values.limit === void 0 ? r.data.items : r.data.items.slice(0, Math.max(0, Number(values.limit)));
@@ -209,7 +209,12 @@ ${values.dryRun ? `${JSON.stringify({ events: d.events, batches: d.batches }, nu
209
209
  flags: [
210
210
  { name: "title", type: "string" },
211
211
  { name: "tag", type: "array" },
212
- { name: "objective", type: "string" },
212
+ {
213
+ name: "objective",
214
+ type: "string",
215
+ file: true,
216
+ description: "Quest objective text; defaults to the title."
217
+ },
213
218
  {
214
219
  name: "due",
215
220
  type: "string",
@@ -302,7 +307,7 @@ ${values.dryRun ? `${JSON.stringify({ events: d.events, batches: d.batches }, nu
302
307
  spec: { ...command.spec, flags: [...command.spec.flags ?? [], json] },
303
308
  input: (values) => {
304
309
  if (values.limit !== void 0 && Number(values.limit) < 1)
305
- throw new Error("Limit must be at least 1");
310
+ throw new Error(`Limit ${values.limit} must be at least 1`);
306
311
  return command.input(values);
307
312
  }
308
313
  }));
@@ -337,7 +342,7 @@ Use --db <path> or GHOSTPAW_QUESTLOG_DB to override the default ~/.ghostpaw/ques
337
342
  if (command === "show" && args[0]?.startsWith("d_")) {
338
343
  const { listDeliveries: listDeliveries2 } = await import('./list_deliveries-WJEJUZIQ.js');
339
344
  const { listHooks } = await import('./list_hooks-C5CAA2C7.js');
340
- const { textOutput } = await import('./text_output-X7F3K23Q.js');
345
+ const { textOutput } = await import('./text_output-U55ZUZX4.js');
341
346
  if (args.includes("--help") || args.includes("-h")) {
342
347
  io.stdout.write("questlog show <delivery-id> [--json]\n");
343
348
  return 0;
@@ -351,7 +356,7 @@ Use --db <path> or GHOSTPAW_QUESTLOG_DB to override the default ~/.ghostpaw/ques
351
356
  if (!delivery) {
352
357
  io.stderr.write(`Delivery ${args[0]} not found
353
358
  `);
354
- return 1;
359
+ return 3;
355
360
  }
356
361
  const target = listHooks(db).find((h) => h.id === delivery.targetId);
357
362
  const result = {
@@ -369,7 +374,7 @@ Use --db <path> or GHOSTPAW_QUESTLOG_DB to override the default ~/.ghostpaw/ques
369
374
  }
370
375
  }
371
376
  if (command !== "hook" && command !== "watch") return void 0;
372
- const { runHookCommand } = await import('./run_hook_command-BOENYNDT.js');
377
+ const { runHookCommand } = await import('./run_hook_command-H4J6S5DZ.js');
373
378
  return runHookCommand(command, args, io, async () => {
374
379
  const { openQuestlogDb } = await import('./open_questlog_db-6NS6BVBC.js');
375
380
  return openQuestlogDb(selectedDbPath(options));
@@ -378,12 +383,12 @@ Use --db <path> or GHOSTPAW_QUESTLOG_DB to override the default ~/.ghostpaw/ques
378
383
  plain: questlogPlainCommands(),
379
384
  guides: questlogSkills,
380
385
  mcp: async (options) => {
381
- const { serveQuestlogMcp } = await import('./serve_questlog_mcp-X524X34F.js');
386
+ const { serveQuestlogMcp } = await import('./serve_questlog_mcp-3UGYRARA.js');
382
387
  await serveQuestlogMcp(options.db === void 0 ? {} : { dbPath: options.db });
383
388
  },
384
389
  skill: async () => {
385
390
  const { renderSkillMarkdown } = await import('./skill_markdown-EGKYZ3W2.js');
386
- const { questlogSkillSpec } = await import('./questlog_skill_spec-ILTCTRYS.js');
391
+ const { questlogSkillSpec } = await import('./questlog_skill_spec-4U36GV6W.js');
387
392
  return renderSkillMarkdown(questlogSkillSpec);
388
393
  }
389
394
  }
@@ -625,7 +630,8 @@ ${JSON.stringify(tool.inputSchema, null, 2)}
625
630
  const coerce = (value, schema) => {
626
631
  if (schema.type === "number" || schema.type === "integer") return Number(value);
627
632
  if (schema.type === "boolean") {
628
- if (!["true", "false"].includes(value)) throw new Error("Expected true or false");
633
+ if (!["true", "false"].includes(value))
634
+ throw new Error(`Expected true or false: ${value}`);
629
635
  return value === "true";
630
636
  }
631
637
  if (schema.type === "object" || schema.type === "array") return JSON.parse(value);
@@ -686,7 +692,11 @@ ${JSON.stringify(tool.inputSchema, null, 2)}
686
692
  );
687
693
  if (typeof result === "object" && result !== null && "outcome" in result && result.outcome === "needs_clarification")
688
694
  return 3;
689
- return isErrorResult(result) ? 1 : 0;
695
+ if (isErrorResult(result)) {
696
+ const error = result.error;
697
+ return error?.code === "not_found" ? 3 : 1;
698
+ }
699
+ return 0;
690
700
  } catch (cause) {
691
701
  return fail(String(cause), 1);
692
702
  } finally {
package/dist/index.cjs CHANGED
@@ -53,14 +53,14 @@ function addHook(db, input) {
53
53
  );
54
54
  const minGap = input.minGap ?? 60;
55
55
  if (!Number.isSafeInteger(minGap) || minGap < 0)
56
- throw new Error("Min gap must be a nonnegative integer");
56
+ throw new Error(`Min gap ${minGap} must be a nonnegative integer`);
57
57
  const events = input.events ?? [...hookEventTypes];
58
58
  if (!events.length || events.some((e) => !hookEventTypes.includes(e)))
59
59
  throw new Error(
60
60
  `unknown event ${events.find((e) => !hookEventTypes.includes(e)) ?? "(empty)"}; valid: ${hookEventTypes.join(", ")}`
61
61
  );
62
62
  if (input.tag !== void 0 && !normalizeTagName(input.tag))
63
- throw new Error("Expected a tag name");
63
+ throw new Error(`Expected a tag name: ${JSON.stringify(input.tag)}`);
64
64
  const secret = input.secret ?? node_crypto.randomBytes(32).toString("hex");
65
65
  if (!secret.trim()) throw new Error("Expected a nonempty signing secret");
66
66
  const result = db.prepare(
@@ -185,7 +185,7 @@ function removeHook(db, id) {
185
185
  }
186
186
 
187
187
  // src/version.ts
188
- var questlogVersion = "0.11.0";
188
+ var questlogVersion = "0.11.2";
189
189
 
190
190
  // src/hooks/send_hook_delivery.ts
191
191
  async function sendHookDelivery(db, delivery, target, options) {
@@ -3317,7 +3317,7 @@ Failure handling:
3317
3317
  var webhooksSkill = defineQuestlogSkill({
3318
3318
  name: "webhooks",
3319
3319
  description: "Configure signed outbound event webhooks and verify deliveries.",
3320
- content: '# Webhooks\n\nUse `questlog` to configure opt-in outbound automation. No server or scheduler is added.\nNo targets means no network calls. `manage_hooks` supports list, remove, tick with\n`dry_run`, and deliveries; add/test are CLI-only. `review_questlog` view `deliveries`\nand `questlog show <id>` expose pending/failed deliveries.\n\n```sh\nquestlog hook add https://example.org/wake --min-gap 60\nquestlog hook add https://example.org/team --tag work --events quest.due,quest.overdue --secret-stdin\nquestlog hook label work\nquestlog hook list\nquestlog tick --dry-run\nquestlog tick\nquestlog watch --interval 60\nquestlog hook test 1\nquestlog review deliveries\nquestlog show 12\nquestlog hook remove 1\n```\n\n`hook add` prints its random 32-byte signing secret once, also as `secret` in `--json`.\nSave it securely. `--secret-stdin` supplies one via stdin; there is no secret flag.\nSecrets and custom header values are stored in the DB. New POSIX DB/WAL/SHM files use 0600 and created directories use 0700; existing DB modes are preserved. Windows uses existing access controls.\nUse repeatable `--header "Name: value"` or `--header-stdin` (one line per header, avoiding shell history). Headers are sent on every delivery and test; output shows names with `***` values. HTTP token names are required; host, content-length, content-type, and x-questlog-* are reserved. Change headers by removing and adding the hook again. Use --header-stdin and --secret-stdin separately. URL userinfo is rejected; use a custom header or query key.\n`questlog show d_\u2026` displays delivery status, attempts, HTTP status, next attempt, redacted target URL and event ids. Text is the default; --json prints JSON. Missing hook remove/test targets exit 3; missing quests/deliveries exit 1. hook label names the database.\nList output redacts userinfo/query strings. Removal deletes the target and deliveries.\nDefault scope is the database; `--tag` matches quest tags, never rumors.\n`--events` defaults to all types. The DB label defaults to the filename without its\npath/extension (`questlog` in memory); set it to a name suitable for disclosure.\n\n```cron\n* * * * * questlog tick\n```\n\nCron\'s PATH may need `$HOME/.local/bin/questlog tick --db /path/to/quests.db`.\nWatch defaults to 60 seconds (minimum 5); SIGINT/SIGTERM finishes the current tick.\nTick exits 0 even when deliveries need retries. Dry-run records nothing and sends nothing.\n\nEvents are detected from the model at tick time:\n\n- `quest.due`: an unresolved, undeleted quest reaches its effective due time (own `due_at`, otherwise its undeleted questline\'s due time).\n- `quest.overdue`: that effective deadline is strictly earlier than now and the quest remains unresolved.\n- `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.\n- `repeatable.spawned`: a concrete repeatable instance was created; occurrence time is its creation time. Tick first materializes due anchors through the existing spawn operation.\n- `rumor.captured`: an undeleted rumor was created; occurrence time is its creation time, even if subsequently settled or dismissed.\n\nPersistent event keys contain type, entity ids, and occurrence time. Restarts and repeated\nticks do not repeat occurrences. Removed transitions between ticks cannot be reconstructed.\nOnly occurrences at/after target creation are assigned; tags are evaluated at assignment.\nTick materializes due repeatables using the existing spawn path, including during preview\nwith rollback. Batches contain only ids, event types, timestamps, and the DB label:\n\n```json\n{"type":"questlog.events","db":"work","deliveryId":"d_\u2026","sentAt":"2026-10-28T10:00:00.000Z","events":[{"id":"e_\u2026","type":"quest.due","questId":12,"occurredAt":"2026-10-28T09:59:00.000Z"}]}\n```\n\nHeaders are `content-type: application/json`, `user-agent: questlog/<version>`,\n`x-questlog-delivery: <deliveryId>`, `x-questlog-timestamp: <unix seconds>`, and\n`x-questlog-signature: sha256=<hex HMAC-SHA256(secret, timestamp + "." + rawBody)>`.\nNo titles, objectives, details, or tag text are sent. `hook test` sends a logged `hook.test`\nevent immediately, ignoring the min gap. Add and test are CLI-only in the tool facade;\n`manage_hooks` supports list, remove, tick (with `dry_run`), and deliveries.\n\n2xx delivers. Timeout is 5 seconds. Failures retry after 30 s, 1 m, 2 m, 4 m, doubling\nto 1 h plus <1 s jitter, subject to the target\'s min gap. Eight failures mark failed.\nRetries keep the delivery id/events; new events queue behind them. Keep the last 500\ndelivered log entries per target and all pending/failed entries. Transactional claims\nlease work for two minutes. A crash after receipt may retry: dedupe at the receiver.\n\n### Verifying a delivery\n\nCompute HMAC-SHA256 over `timestamp + "." + rawBody` with the secret and compare in constant\ntime. Reject timestamps older than 5 minutes (and timestamps too far in the future), then\ndedupe on `x-questlog-delivery`. Use durable deduplication storage in a production receiver.\nThis 15-line Node example uses an in-memory set:\n\n```js\nimport { createHmac, timingSafeEqual } from \'node:crypto\';\nimport { createServer } from \'node:http\';\nconst secret = process.env.WEBHOOK_SECRET, seen = new Set();\ncreateServer(async (req, res) => {\n const chunks = []; for await (const chunk of req) chunks.push(chunk);\n const rawBody = Buffer.concat(chunks), h = req.headers;\n const timestamp = h[\'x-questlog-timestamp\'], id = h[\'x-questlog-delivery\'];\n const age = Date.now() / 1000 - Number(timestamp);\n const expected = createHmac(\'sha256\', secret).update(timestamp + \'.\').update(rawBody).digest();\n const signature = String(h[\'x-questlog-signature\'] || \'\');\n const actual = Buffer.from(signature.slice(7), \'hex\');\n 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; }\n if (!seen.has(id)) { seen.add(id); console.log(JSON.parse(rawBody)); }\n res.writeHead(204).end();\n}).listen(8080, \'127.0.0.1\');\n```\n'
3320
+ content: '# Webhooks\n\nUse `questlog` to configure opt-in outbound automation. No targets means no network calls. `manage_hooks` supports list, remove, tick with\n`dry_run`, and deliveries; add/test are CLI-only. `review_questlog` view `deliveries`\nand `questlog show <id>` expose pending/failed deliveries.\n\n```sh\nquestlog hook add https://example.org/wake --min-gap 60\nquestlog hook add https://example.org/team --tag work --events quest.due,quest.overdue --secret-stdin\nquestlog hook label work\nquestlog hook list\nquestlog tick --dry-run\nquestlog tick\nquestlog watch --interval 60\nquestlog hook test 1\nquestlog review deliveries\nquestlog show 12\nquestlog hook remove 1\n```\n\n`hook add` prints its random 32-byte signing secret once, also as `secret` in `--json`.\nSave it securely. `--secret-stdin` supplies one via stdin; there is no secret flag.\nSecrets/headers are stored in the DB. New POSIX DB/WAL/SHM files use 0600; new directories use 0700. Existing modes and Windows access controls are preserved.\nPrefer `--header-stdin` for real keys (one Name: value per line); use a private 0600 file. Repeatable `--header` exposes values in process arguments and shell history. Output redacts values as `***`. host, content-length, content-type and x-questlog-* are reserved. To change headers, remove/add the hook. Use --header-stdin and --secret-stdin separately. URL userinfo is rejected.\n`show d_\u2026` displays delivery attempts, HTTP status, next attempt, redacted target and event ids. Text timestamps are local; --json keeps UTC/epoch times. Missing hook remove/test targets exit 3; missing quests/deliveries also exit 3. Empty hook lists print `No hooks`. hook label names the database.\nList output redacts userinfo/query strings. Removal deletes the target and deliveries.\nDefault scope is the DB; `--tag` matches quest tags only.\n`--events` defaults to all types. The DB label defaults to the filename without its\npath/extension (`questlog` in memory); set it to a name suitable for disclosure.\n\n```cron\n* * * * * questlog tick\n```\n\nCron may need `$HOME/.local/bin/questlog tick --db /path/to/quests.db`.\nWatch defaults to 60 s (minimum 5); SIGINT/SIGTERM finishes the tick.\nTick exits 0 even when deliveries need retries. Dry-run records nothing and sends nothing.\n\nEvents are detected from the model at tick time:\n\n- `quest.due`: an unresolved, undeleted quest reaches its effective due time (own `due_at`, otherwise its undeleted questline\'s due time).\n- `quest.overdue`: that effective deadline is strictly earlier than now and the quest remains unresolved.\n- `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.\n- `repeatable.spawned`: a concrete repeatable instance was created; occurrence time is its creation time. Tick first materializes due anchors through the existing spawn operation.\n- `rumor.captured`: an undeleted rumor was created; occurrence time is its creation time, even if subsequently settled or dismissed.\n\nEvent keys include type, entity ids and occurrence time, preventing repeats across restarts. Transitions removed between ticks cannot be reconstructed.\nOnly events at/after target creation are assigned; tags are checked then.\nTick materializes due repeatables using the existing spawn path, including during preview\nwith rollback. Batches contain only ids, event types, timestamps, and the DB label:\n\n```json\n{"type":"questlog.events","db":"work","deliveryId":"d_\u2026","sentAt":"2026-10-28T10:00:00.000Z","events":[{"id":"e_\u2026","type":"quest.due","questId":12,"occurredAt":"2026-10-28T09:59:00.000Z"}]}\n```\n\nHeaders are `content-type: application/json`, `user-agent: questlog/<version>`,\n`x-questlog-delivery: <deliveryId>`, `x-questlog-timestamp: <unix seconds>`, and\n`x-questlog-signature: sha256=<hex HMAC-SHA256(secret, timestamp + "." + rawBody)>`.\nNo titles, objectives, details, or tag text are sent. `hook test` sends a logged `hook.test`\nevent immediately, ignoring the min gap. Add and test are CLI-only in the tool facade;\n`manage_hooks` supports list, remove, tick (with `dry_run`), and deliveries.\n\n2xx delivers. Timeout is 5 seconds. Failures retry after 30 s, 1 m, 2 m, 4 m, doubling\nto 1 h plus <1 s jitter, subject to the target\'s min gap. Eight failures mark failed.\nRetries keep the delivery id/events; new events queue behind them. Keep the last 500\ndelivered log entries per target and all pending/failed entries. Claims lease work for two minutes. Dedupe retries at the receiver.\n\n### Verifying a delivery\n\nCompute HMAC-SHA256 over `timestamp + "." + rawBody` with the secret and compare in constant\ntime. Reject timestamps older than 5 minutes (and timestamps too far in the future), then\ndedupe on `x-questlog-delivery`. Use durable deduplication in production; this example uses an in-memory set:\n\n```js\nimport { createHmac, timingSafeEqual } from \'node:crypto\';\nimport { createServer } from \'node:http\';\nconst secret = process.env.WEBHOOK_SECRET, seen = new Set();\ncreateServer(async (req, res) => {\n const chunks = []; for await (const chunk of req) chunks.push(chunk);\n const rawBody = Buffer.concat(chunks), h = req.headers;\n const timestamp = h[\'x-questlog-timestamp\'], id = h[\'x-questlog-delivery\'];\n const age = Date.now() / 1000 - Number(timestamp);\n const expected = createHmac(\'sha256\', secret).update(timestamp + \'.\').update(rawBody).digest();\n const signature = String(h[\'x-questlog-signature\'] || \'\');\n const actual = Buffer.from(signature.slice(7), \'hex\');\n 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; }\n if (!seen.has(id)) { seen.add(id); console.log(JSON.parse(rawBody)); }\n res.writeHead(204).end();\n}).listen(8080, \'127.0.0.1\');\n```\n'
3321
3321
  });
3322
3322
 
3323
3323
  // src/skills/skill_registry.ts
@@ -3691,6 +3691,7 @@ function minimalInput(schema, action, name = "") {
3691
3691
  // src/surfaces/validate_input.ts
3692
3692
  function validateInput(schema, input, path = "$") {
3693
3693
  const errors = [];
3694
+ const received = `; received ${JSON.stringify(input)}`;
3694
3695
  const matchesType = (type) => {
3695
3696
  switch (type) {
3696
3697
  case "object":
@@ -3708,13 +3709,13 @@ function validateInput(schema, input, path = "$") {
3708
3709
  }
3709
3710
  };
3710
3711
  if (schema.type && !(Array.isArray(schema.type) ? schema.type : [schema.type]).some(matchesType)) {
3711
- return [`${path}: expected ${String(schema.type)}`];
3712
+ return [`${path}: expected ${schema.type}${received}`];
3712
3713
  }
3713
3714
  if (schema.enum && !schema.enum.some((value) => JSON.stringify(value) === JSON.stringify(input)))
3714
- errors.push(`${path}: expected one of ${JSON.stringify(schema.enum)}`);
3715
+ errors.push(`${path}: expected one of ${JSON.stringify(schema.enum)}${received}`);
3715
3716
  const constant = "const" in schema ? schema.const : void 0;
3716
3717
  if (constant !== void 0 && constant !== input)
3717
- errors.push(`${path}: expected ${JSON.stringify(constant)}`);
3718
+ errors.push(`${path}: expected ${JSON.stringify(constant)}${received}`);
3718
3719
  if (schema.oneOf) {
3719
3720
  const branches = schema.oneOf.map((branch) => validateInput(branch, input, path));
3720
3721
  if (!branches.some((errors2) => errors2.length === 0)) {
@@ -3725,7 +3726,7 @@ function validateInput(schema, input, path = "$") {
3725
3726
  );
3726
3727
  }
3727
3728
  }
3728
- if (typeof input === "object" && input !== null && !Array.isArray(input)) {
3729
+ if (matchesType("object")) {
3729
3730
  const record = input;
3730
3731
  for (const name of [...schema.required ?? [], ...requiredForAction(schema, record.action)])
3731
3732
  if (!Object.hasOwn(record, name)) errors.push(`${path}.${name}: required`);
package/dist/index.js CHANGED
@@ -51,14 +51,14 @@ function addHook(db, input) {
51
51
  );
52
52
  const minGap = input.minGap ?? 60;
53
53
  if (!Number.isSafeInteger(minGap) || minGap < 0)
54
- throw new Error("Min gap must be a nonnegative integer");
54
+ throw new Error(`Min gap ${minGap} must be a nonnegative integer`);
55
55
  const events = input.events ?? [...hookEventTypes];
56
56
  if (!events.length || events.some((e) => !hookEventTypes.includes(e)))
57
57
  throw new Error(
58
58
  `unknown event ${events.find((e) => !hookEventTypes.includes(e)) ?? "(empty)"}; valid: ${hookEventTypes.join(", ")}`
59
59
  );
60
60
  if (input.tag !== void 0 && !normalizeTagName(input.tag))
61
- throw new Error("Expected a tag name");
61
+ throw new Error(`Expected a tag name: ${JSON.stringify(input.tag)}`);
62
62
  const secret = input.secret ?? randomBytes(32).toString("hex");
63
63
  if (!secret.trim()) throw new Error("Expected a nonempty signing secret");
64
64
  const result = db.prepare(
@@ -183,7 +183,7 @@ function removeHook(db, id) {
183
183
  }
184
184
 
185
185
  // src/version.ts
186
- var questlogVersion = "0.11.0";
186
+ var questlogVersion = "0.11.2";
187
187
 
188
188
  // src/hooks/send_hook_delivery.ts
189
189
  async function sendHookDelivery(db, delivery, target, options) {
@@ -3315,7 +3315,7 @@ Failure handling:
3315
3315
  var webhooksSkill = defineQuestlogSkill({
3316
3316
  name: "webhooks",
3317
3317
  description: "Configure signed outbound event webhooks and verify deliveries.",
3318
- content: '# Webhooks\n\nUse `questlog` to configure opt-in outbound automation. No server or scheduler is added.\nNo targets means no network calls. `manage_hooks` supports list, remove, tick with\n`dry_run`, and deliveries; add/test are CLI-only. `review_questlog` view `deliveries`\nand `questlog show <id>` expose pending/failed deliveries.\n\n```sh\nquestlog hook add https://example.org/wake --min-gap 60\nquestlog hook add https://example.org/team --tag work --events quest.due,quest.overdue --secret-stdin\nquestlog hook label work\nquestlog hook list\nquestlog tick --dry-run\nquestlog tick\nquestlog watch --interval 60\nquestlog hook test 1\nquestlog review deliveries\nquestlog show 12\nquestlog hook remove 1\n```\n\n`hook add` prints its random 32-byte signing secret once, also as `secret` in `--json`.\nSave it securely. `--secret-stdin` supplies one via stdin; there is no secret flag.\nSecrets and custom header values are stored in the DB. New POSIX DB/WAL/SHM files use 0600 and created directories use 0700; existing DB modes are preserved. Windows uses existing access controls.\nUse repeatable `--header "Name: value"` or `--header-stdin` (one line per header, avoiding shell history). Headers are sent on every delivery and test; output shows names with `***` values. HTTP token names are required; host, content-length, content-type, and x-questlog-* are reserved. Change headers by removing and adding the hook again. Use --header-stdin and --secret-stdin separately. URL userinfo is rejected; use a custom header or query key.\n`questlog show d_\u2026` displays delivery status, attempts, HTTP status, next attempt, redacted target URL and event ids. Text is the default; --json prints JSON. Missing hook remove/test targets exit 3; missing quests/deliveries exit 1. hook label names the database.\nList output redacts userinfo/query strings. Removal deletes the target and deliveries.\nDefault scope is the database; `--tag` matches quest tags, never rumors.\n`--events` defaults to all types. The DB label defaults to the filename without its\npath/extension (`questlog` in memory); set it to a name suitable for disclosure.\n\n```cron\n* * * * * questlog tick\n```\n\nCron\'s PATH may need `$HOME/.local/bin/questlog tick --db /path/to/quests.db`.\nWatch defaults to 60 seconds (minimum 5); SIGINT/SIGTERM finishes the current tick.\nTick exits 0 even when deliveries need retries. Dry-run records nothing and sends nothing.\n\nEvents are detected from the model at tick time:\n\n- `quest.due`: an unresolved, undeleted quest reaches its effective due time (own `due_at`, otherwise its undeleted questline\'s due time).\n- `quest.overdue`: that effective deadline is strictly earlier than now and the quest remains unresolved.\n- `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.\n- `repeatable.spawned`: a concrete repeatable instance was created; occurrence time is its creation time. Tick first materializes due anchors through the existing spawn operation.\n- `rumor.captured`: an undeleted rumor was created; occurrence time is its creation time, even if subsequently settled or dismissed.\n\nPersistent event keys contain type, entity ids, and occurrence time. Restarts and repeated\nticks do not repeat occurrences. Removed transitions between ticks cannot be reconstructed.\nOnly occurrences at/after target creation are assigned; tags are evaluated at assignment.\nTick materializes due repeatables using the existing spawn path, including during preview\nwith rollback. Batches contain only ids, event types, timestamps, and the DB label:\n\n```json\n{"type":"questlog.events","db":"work","deliveryId":"d_\u2026","sentAt":"2026-10-28T10:00:00.000Z","events":[{"id":"e_\u2026","type":"quest.due","questId":12,"occurredAt":"2026-10-28T09:59:00.000Z"}]}\n```\n\nHeaders are `content-type: application/json`, `user-agent: questlog/<version>`,\n`x-questlog-delivery: <deliveryId>`, `x-questlog-timestamp: <unix seconds>`, and\n`x-questlog-signature: sha256=<hex HMAC-SHA256(secret, timestamp + "." + rawBody)>`.\nNo titles, objectives, details, or tag text are sent. `hook test` sends a logged `hook.test`\nevent immediately, ignoring the min gap. Add and test are CLI-only in the tool facade;\n`manage_hooks` supports list, remove, tick (with `dry_run`), and deliveries.\n\n2xx delivers. Timeout is 5 seconds. Failures retry after 30 s, 1 m, 2 m, 4 m, doubling\nto 1 h plus <1 s jitter, subject to the target\'s min gap. Eight failures mark failed.\nRetries keep the delivery id/events; new events queue behind them. Keep the last 500\ndelivered log entries per target and all pending/failed entries. Transactional claims\nlease work for two minutes. A crash after receipt may retry: dedupe at the receiver.\n\n### Verifying a delivery\n\nCompute HMAC-SHA256 over `timestamp + "." + rawBody` with the secret and compare in constant\ntime. Reject timestamps older than 5 minutes (and timestamps too far in the future), then\ndedupe on `x-questlog-delivery`. Use durable deduplication storage in a production receiver.\nThis 15-line Node example uses an in-memory set:\n\n```js\nimport { createHmac, timingSafeEqual } from \'node:crypto\';\nimport { createServer } from \'node:http\';\nconst secret = process.env.WEBHOOK_SECRET, seen = new Set();\ncreateServer(async (req, res) => {\n const chunks = []; for await (const chunk of req) chunks.push(chunk);\n const rawBody = Buffer.concat(chunks), h = req.headers;\n const timestamp = h[\'x-questlog-timestamp\'], id = h[\'x-questlog-delivery\'];\n const age = Date.now() / 1000 - Number(timestamp);\n const expected = createHmac(\'sha256\', secret).update(timestamp + \'.\').update(rawBody).digest();\n const signature = String(h[\'x-questlog-signature\'] || \'\');\n const actual = Buffer.from(signature.slice(7), \'hex\');\n 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; }\n if (!seen.has(id)) { seen.add(id); console.log(JSON.parse(rawBody)); }\n res.writeHead(204).end();\n}).listen(8080, \'127.0.0.1\');\n```\n'
3318
+ content: '# Webhooks\n\nUse `questlog` to configure opt-in outbound automation. No targets means no network calls. `manage_hooks` supports list, remove, tick with\n`dry_run`, and deliveries; add/test are CLI-only. `review_questlog` view `deliveries`\nand `questlog show <id>` expose pending/failed deliveries.\n\n```sh\nquestlog hook add https://example.org/wake --min-gap 60\nquestlog hook add https://example.org/team --tag work --events quest.due,quest.overdue --secret-stdin\nquestlog hook label work\nquestlog hook list\nquestlog tick --dry-run\nquestlog tick\nquestlog watch --interval 60\nquestlog hook test 1\nquestlog review deliveries\nquestlog show 12\nquestlog hook remove 1\n```\n\n`hook add` prints its random 32-byte signing secret once, also as `secret` in `--json`.\nSave it securely. `--secret-stdin` supplies one via stdin; there is no secret flag.\nSecrets/headers are stored in the DB. New POSIX DB/WAL/SHM files use 0600; new directories use 0700. Existing modes and Windows access controls are preserved.\nPrefer `--header-stdin` for real keys (one Name: value per line); use a private 0600 file. Repeatable `--header` exposes values in process arguments and shell history. Output redacts values as `***`. host, content-length, content-type and x-questlog-* are reserved. To change headers, remove/add the hook. Use --header-stdin and --secret-stdin separately. URL userinfo is rejected.\n`show d_\u2026` displays delivery attempts, HTTP status, next attempt, redacted target and event ids. Text timestamps are local; --json keeps UTC/epoch times. Missing hook remove/test targets exit 3; missing quests/deliveries also exit 3. Empty hook lists print `No hooks`. hook label names the database.\nList output redacts userinfo/query strings. Removal deletes the target and deliveries.\nDefault scope is the DB; `--tag` matches quest tags only.\n`--events` defaults to all types. The DB label defaults to the filename without its\npath/extension (`questlog` in memory); set it to a name suitable for disclosure.\n\n```cron\n* * * * * questlog tick\n```\n\nCron may need `$HOME/.local/bin/questlog tick --db /path/to/quests.db`.\nWatch defaults to 60 s (minimum 5); SIGINT/SIGTERM finishes the tick.\nTick exits 0 even when deliveries need retries. Dry-run records nothing and sends nothing.\n\nEvents are detected from the model at tick time:\n\n- `quest.due`: an unresolved, undeleted quest reaches its effective due time (own `due_at`, otherwise its undeleted questline\'s due time).\n- `quest.overdue`: that effective deadline is strictly earlier than now and the quest remains unresolved.\n- `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.\n- `repeatable.spawned`: a concrete repeatable instance was created; occurrence time is its creation time. Tick first materializes due anchors through the existing spawn operation.\n- `rumor.captured`: an undeleted rumor was created; occurrence time is its creation time, even if subsequently settled or dismissed.\n\nEvent keys include type, entity ids and occurrence time, preventing repeats across restarts. Transitions removed between ticks cannot be reconstructed.\nOnly events at/after target creation are assigned; tags are checked then.\nTick materializes due repeatables using the existing spawn path, including during preview\nwith rollback. Batches contain only ids, event types, timestamps, and the DB label:\n\n```json\n{"type":"questlog.events","db":"work","deliveryId":"d_\u2026","sentAt":"2026-10-28T10:00:00.000Z","events":[{"id":"e_\u2026","type":"quest.due","questId":12,"occurredAt":"2026-10-28T09:59:00.000Z"}]}\n```\n\nHeaders are `content-type: application/json`, `user-agent: questlog/<version>`,\n`x-questlog-delivery: <deliveryId>`, `x-questlog-timestamp: <unix seconds>`, and\n`x-questlog-signature: sha256=<hex HMAC-SHA256(secret, timestamp + "." + rawBody)>`.\nNo titles, objectives, details, or tag text are sent. `hook test` sends a logged `hook.test`\nevent immediately, ignoring the min gap. Add and test are CLI-only in the tool facade;\n`manage_hooks` supports list, remove, tick (with `dry_run`), and deliveries.\n\n2xx delivers. Timeout is 5 seconds. Failures retry after 30 s, 1 m, 2 m, 4 m, doubling\nto 1 h plus <1 s jitter, subject to the target\'s min gap. Eight failures mark failed.\nRetries keep the delivery id/events; new events queue behind them. Keep the last 500\ndelivered log entries per target and all pending/failed entries. Claims lease work for two minutes. Dedupe retries at the receiver.\n\n### Verifying a delivery\n\nCompute HMAC-SHA256 over `timestamp + "." + rawBody` with the secret and compare in constant\ntime. Reject timestamps older than 5 minutes (and timestamps too far in the future), then\ndedupe on `x-questlog-delivery`. Use durable deduplication in production; this example uses an in-memory set:\n\n```js\nimport { createHmac, timingSafeEqual } from \'node:crypto\';\nimport { createServer } from \'node:http\';\nconst secret = process.env.WEBHOOK_SECRET, seen = new Set();\ncreateServer(async (req, res) => {\n const chunks = []; for await (const chunk of req) chunks.push(chunk);\n const rawBody = Buffer.concat(chunks), h = req.headers;\n const timestamp = h[\'x-questlog-timestamp\'], id = h[\'x-questlog-delivery\'];\n const age = Date.now() / 1000 - Number(timestamp);\n const expected = createHmac(\'sha256\', secret).update(timestamp + \'.\').update(rawBody).digest();\n const signature = String(h[\'x-questlog-signature\'] || \'\');\n const actual = Buffer.from(signature.slice(7), \'hex\');\n 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; }\n if (!seen.has(id)) { seen.add(id); console.log(JSON.parse(rawBody)); }\n res.writeHead(204).end();\n}).listen(8080, \'127.0.0.1\');\n```\n'
3319
3319
  });
3320
3320
 
3321
3321
  // src/skills/skill_registry.ts
@@ -3689,6 +3689,7 @@ function minimalInput(schema, action, name = "") {
3689
3689
  // src/surfaces/validate_input.ts
3690
3690
  function validateInput(schema, input, path = "$") {
3691
3691
  const errors = [];
3692
+ const received = `; received ${JSON.stringify(input)}`;
3692
3693
  const matchesType = (type) => {
3693
3694
  switch (type) {
3694
3695
  case "object":
@@ -3706,13 +3707,13 @@ function validateInput(schema, input, path = "$") {
3706
3707
  }
3707
3708
  };
3708
3709
  if (schema.type && !(Array.isArray(schema.type) ? schema.type : [schema.type]).some(matchesType)) {
3709
- return [`${path}: expected ${String(schema.type)}`];
3710
+ return [`${path}: expected ${schema.type}${received}`];
3710
3711
  }
3711
3712
  if (schema.enum && !schema.enum.some((value) => JSON.stringify(value) === JSON.stringify(input)))
3712
- errors.push(`${path}: expected one of ${JSON.stringify(schema.enum)}`);
3713
+ errors.push(`${path}: expected one of ${JSON.stringify(schema.enum)}${received}`);
3713
3714
  const constant = "const" in schema ? schema.const : void 0;
3714
3715
  if (constant !== void 0 && constant !== input)
3715
- errors.push(`${path}: expected ${JSON.stringify(constant)}`);
3716
+ errors.push(`${path}: expected ${JSON.stringify(constant)}${received}`);
3716
3717
  if (schema.oneOf) {
3717
3718
  const branches = schema.oneOf.map((branch) => validateInput(branch, input, path));
3718
3719
  if (!branches.some((errors2) => errors2.length === 0)) {
@@ -3723,7 +3724,7 @@ function validateInput(schema, input, path = "$") {
3723
3724
  );
3724
3725
  }
3725
3726
  }
3726
- if (typeof input === "object" && input !== null && !Array.isArray(input)) {
3727
+ if (matchesType("object")) {
3727
3728
  const record = input;
3728
3729
  for (const name of [...schema.required ?? [], ...requiredForAction(schema, record.action)])
3729
3730
  if (!Object.hasOwn(record, name)) errors.push(`${path}.${name}: required`);
@@ -86,6 +86,7 @@ var init_surface_types = __esm({
86
86
  // src/surfaces/validate_input.ts
87
87
  function validateInput(schema, input, path = "$") {
88
88
  const errors = [];
89
+ const received = `; received ${JSON.stringify(input)}`;
89
90
  const matchesType = (type) => {
90
91
  switch (type) {
91
92
  case "object":
@@ -103,13 +104,13 @@ function validateInput(schema, input, path = "$") {
103
104
  }
104
105
  };
105
106
  if (schema.type && !(Array.isArray(schema.type) ? schema.type : [schema.type]).some(matchesType)) {
106
- return [`${path}: expected ${String(schema.type)}`];
107
+ return [`${path}: expected ${schema.type}${received}`];
107
108
  }
108
109
  if (schema.enum && !schema.enum.some((value) => JSON.stringify(value) === JSON.stringify(input)))
109
- errors.push(`${path}: expected one of ${JSON.stringify(schema.enum)}`);
110
+ errors.push(`${path}: expected one of ${JSON.stringify(schema.enum)}${received}`);
110
111
  const constant = "const" in schema ? schema.const : void 0;
111
112
  if (constant !== void 0 && constant !== input)
112
- errors.push(`${path}: expected ${JSON.stringify(constant)}`);
113
+ errors.push(`${path}: expected ${JSON.stringify(constant)}${received}`);
113
114
  if (schema.oneOf) {
114
115
  const branches = schema.oneOf.map((branch) => validateInput(branch, input, path));
115
116
  if (!branches.some((errors2) => errors2.length === 0)) {
@@ -120,7 +121,7 @@ function validateInput(schema, input, path = "$") {
120
121
  );
121
122
  }
122
123
  }
123
- if (typeof input === "object" && input !== null && !Array.isArray(input)) {
124
+ if (matchesType("object")) {
124
125
  const record = input;
125
126
  for (const name of [...schema.required ?? [], ...requiredForAction(schema, record.action)])
126
127
  if (!Object.hasOwn(record, name)) errors.push(`${path}.${name}: required`);
@@ -797,7 +798,7 @@ var init_open_questlog_db = __esm({
797
798
  var questlogVersion;
798
799
  var init_version = __esm({
799
800
  "src/version.ts"() {
800
- questlogVersion = "0.11.0";
801
+ questlogVersion = "0.11.2";
801
802
  }
802
803
  });
803
804
 
package/dist/mcp.cjs CHANGED
@@ -420,7 +420,7 @@ async function openQuestlogDb(path) {
420
420
  }
421
421
 
422
422
  // src/version.ts
423
- var questlogVersion = "0.11.0";
423
+ var questlogVersion = "0.11.2";
424
424
 
425
425
  // src/surface/questlog_mcp_instructions.ts
426
426
  var questlogMcpInstructions = "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.";
@@ -714,6 +714,7 @@ function minimalInput(schema, action, name = "") {
714
714
  // src/surfaces/validate_input.ts
715
715
  function validateInput(schema, input, path = "$") {
716
716
  const errors = [];
717
+ const received = `; received ${JSON.stringify(input)}`;
717
718
  const matchesType = (type) => {
718
719
  switch (type) {
719
720
  case "object":
@@ -731,13 +732,13 @@ function validateInput(schema, input, path = "$") {
731
732
  }
732
733
  };
733
734
  if (schema.type && !(Array.isArray(schema.type) ? schema.type : [schema.type]).some(matchesType)) {
734
- return [`${path}: expected ${String(schema.type)}`];
735
+ return [`${path}: expected ${schema.type}${received}`];
735
736
  }
736
737
  if (schema.enum && !schema.enum.some((value) => JSON.stringify(value) === JSON.stringify(input)))
737
- errors.push(`${path}: expected one of ${JSON.stringify(schema.enum)}`);
738
+ errors.push(`${path}: expected one of ${JSON.stringify(schema.enum)}${received}`);
738
739
  const constant = "const" in schema ? schema.const : void 0;
739
740
  if (constant !== void 0 && constant !== input)
740
- errors.push(`${path}: expected ${JSON.stringify(constant)}`);
741
+ errors.push(`${path}: expected ${JSON.stringify(constant)}${received}`);
741
742
  if (schema.oneOf) {
742
743
  const branches = schema.oneOf.map((branch) => validateInput(branch, input, path));
743
744
  if (!branches.some((errors2) => errors2.length === 0)) {
@@ -748,7 +749,7 @@ function validateInput(schema, input, path = "$") {
748
749
  );
749
750
  }
750
751
  }
751
- if (typeof input === "object" && input !== null && !Array.isArray(input)) {
752
+ if (matchesType("object")) {
752
753
  const record = input;
753
754
  for (const name of [...schema.required ?? [], ...requiredForAction(schema, record.action)])
754
755
  if (!Object.hasOwn(record, name)) errors.push(`${path}.${name}: required`);
package/dist/mcp.js CHANGED
@@ -418,7 +418,7 @@ async function openQuestlogDb(path) {
418
418
  }
419
419
 
420
420
  // src/version.ts
421
- var questlogVersion = "0.11.0";
421
+ var questlogVersion = "0.11.2";
422
422
 
423
423
  // src/surface/questlog_mcp_instructions.ts
424
424
  var questlogMcpInstructions = "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.";
@@ -712,6 +712,7 @@ function minimalInput(schema, action, name = "") {
712
712
  // src/surfaces/validate_input.ts
713
713
  function validateInput(schema, input, path = "$") {
714
714
  const errors = [];
715
+ const received = `; received ${JSON.stringify(input)}`;
715
716
  const matchesType = (type) => {
716
717
  switch (type) {
717
718
  case "object":
@@ -729,13 +730,13 @@ function validateInput(schema, input, path = "$") {
729
730
  }
730
731
  };
731
732
  if (schema.type && !(Array.isArray(schema.type) ? schema.type : [schema.type]).some(matchesType)) {
732
- return [`${path}: expected ${String(schema.type)}`];
733
+ return [`${path}: expected ${schema.type}${received}`];
733
734
  }
734
735
  if (schema.enum && !schema.enum.some((value) => JSON.stringify(value) === JSON.stringify(input)))
735
- errors.push(`${path}: expected one of ${JSON.stringify(schema.enum)}`);
736
+ errors.push(`${path}: expected one of ${JSON.stringify(schema.enum)}${received}`);
736
737
  const constant = "const" in schema ? schema.const : void 0;
737
738
  if (constant !== void 0 && constant !== input)
738
- errors.push(`${path}: expected ${JSON.stringify(constant)}`);
739
+ errors.push(`${path}: expected ${JSON.stringify(constant)}${received}`);
739
740
  if (schema.oneOf) {
740
741
  const branches = schema.oneOf.map((branch) => validateInput(branch, input, path));
741
742
  if (!branches.some((errors2) => errors2.length === 0)) {
@@ -746,7 +747,7 @@ function validateInput(schema, input, path = "$") {
746
747
  );
747
748
  }
748
749
  }
749
- if (typeof input === "object" && input !== null && !Array.isArray(input)) {
750
+ if (matchesType("object")) {
750
751
  const record = input;
751
752
  for (const name of [...schema.required ?? [], ...requiredForAction(schema, record.action)])
752
753
  if (!Object.hasOwn(record, name)) errors.push(`${path}.${name}: required`);
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { questlogSkills } from './chunk-I2AMIXGP.js';
2
+ import { questlogSkills } from './chunk-PQAQZUCH.js';
3
3
  import { questlogMcpInstructions } from './chunk-SVWAEAKC.js';
4
- import { reviewQuestlogTool, questlogSurfaceTools } from './chunk-KOWIVBG6.js';
5
- import './chunk-RBGQK5WP.js';
4
+ import { reviewQuestlogTool, questlogSurfaceTools } from './chunk-FCHLDYAR.js';
5
+ import './chunk-S5EP3A3L.js';
6
6
  import './chunk-D6EGZCT7.js';
7
7
  import './chunk-NBCCA6AX.js';
8
8
  import './chunk-QYSIJ2YN.js';
@@ -59,7 +59,7 @@ var questlogSkillSpec = {
59
59
  "- `questlog show 12`",
60
60
  "- `questlog list --view quests.available`",
61
61
  "- Use `--json` on any plain command.",
62
- "- Exit codes: 0 ok / 1 error / 2 usage / 3 needs input or hook target not found.",
62
+ "- Exit codes: 0 ok / 1 error / 2 usage / 3 needs input or item not found.",
63
63
  "- Database: `~/.ghostpaw/questlog.db`; override with `--db` or `GHOSTPAW_QUESTLOG_DB`.",
64
64
  "- Use one database per project.",
65
65
  "- `questlog --version` prints the installed version.",
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { parseCliArgs } from './chunk-GVD6A3RR.js';
3
- import { textOutput } from './chunk-QK3H2AL5.js';
4
- import { watchQuestlog, testHook, removeHook, hookLabel, withTransaction, addHook } from './chunk-RBGQK5WP.js';
2
+ import { parseCliArgs } from './chunk-M3WAPFGB.js';
3
+ import { textOutput } from './chunk-DDMYHMXY.js';
4
+ import { watchQuestlog, testHook, removeHook, hookLabel, withTransaction, addHook } from './chunk-S5EP3A3L.js';
5
5
  import './chunk-D6EGZCT7.js';
6
6
  import { listHooks } from './chunk-NBCCA6AX.js';
7
7
 
@@ -133,7 +133,7 @@ async function runHookCommand(command, args, io, open) {
133
133
  values.json ? `${JSON.stringify(result, null, 2)}
134
134
  ` : action === "add" ? `Created hook ${result.id}
135
135
  Secret (shown once): ${result.secret}
136
- ` : `${textOutput(result)}
136
+ ` : action === "list" && Array.isArray(result) && !result.length ? "No hooks\n" : `${textOutput(result)}
137
137
  `
138
138
  );
139
139
  return 0;
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { questlogMcpInstructions } from './chunk-SVWAEAKC.js';
3
3
  import { isErrorResult, resolveQuestlogDbPath } from './chunk-IL2QRKZL.js';
4
- import { validateInput, questlogSurfaceTools } from './chunk-KOWIVBG6.js';
4
+ import { validateInput, questlogSurfaceTools } from './chunk-FCHLDYAR.js';
5
5
  import { openQuestlogDb } from './chunk-IA2GVCZS.js';
6
- import { questlogVersion } from './chunk-RBGQK5WP.js';
6
+ import { questlogVersion } from './chunk-S5EP3A3L.js';
7
7
  import './chunk-D6EGZCT7.js';
8
8
  import './chunk-NBCCA6AX.js';
9
9
  import { minimalInput } from './chunk-QYSIJ2YN.js';
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export { textOutput } from './chunk-DDMYHMXY.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghostpaw/questlog",
3
- "version": "0.11.0",
3
+ "version": "0.11.2",
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": {
@@ -35,7 +35,7 @@ Run tools as `ghostpaw-questlog ...`; use `~/.local/bin/ghostpaw-questlog` if it
35
35
  - `questlog show 12`
36
36
  - `questlog list --view quests.available`
37
37
  - Use `--json` on any plain command.
38
- - Exit codes: 0 ok / 1 error / 2 usage / 3 needs input or hook target not found.
38
+ - Exit codes: 0 ok / 1 error / 2 usage / 3 needs input or item not found.
39
39
  - Database: `~/.ghostpaw/questlog.db`; override with `--db` or `GHOSTPAW_QUESTLOG_DB`.
40
40
  - Use one database per project.
41
41
  - `questlog --version` prints the installed version.
@@ -1,7 +1,6 @@
1
1
  # Webhooks
2
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
3
+ Use `questlog` to configure opt-in outbound automation. No targets means no network calls. `manage_hooks` supports list, remove, tick with
5
4
  `dry_run`, and deliveries; add/test are CLI-only. `review_questlog` view `deliveries`
6
5
  and `questlog show <id>` expose pending/failed deliveries.
7
6
 
@@ -21,11 +20,11 @@ questlog hook remove 1
21
20
 
22
21
  `hook add` prints its random 32-byte signing secret once, also as `secret` in `--json`.
23
22
  Save it securely. `--secret-stdin` supplies one via stdin; there is no secret flag.
24
- Secrets and custom header values are stored in the DB. New POSIX DB/WAL/SHM files use 0600 and created directories use 0700; existing DB modes are preserved. Windows uses existing access controls.
25
- Use repeatable `--header "Name: value"` or `--header-stdin` (one line per header, avoiding shell history). Headers are sent on every delivery and test; output shows names with `***` values. HTTP token names are required; host, content-length, content-type, and x-questlog-* are reserved. Change headers by removing and adding the hook again. Use --header-stdin and --secret-stdin separately. URL userinfo is rejected; use a custom header or query key.
26
- `questlog show d_…` displays delivery status, attempts, HTTP status, next attempt, redacted target URL and event ids. Text is the default; --json prints JSON. Missing hook remove/test targets exit 3; missing quests/deliveries exit 1. hook label names the database.
23
+ Secrets/headers are stored in the DB. New POSIX DB/WAL/SHM files use 0600; new directories use 0700. Existing modes and Windows access controls are preserved.
24
+ Prefer `--header-stdin` for real keys (one Name: value per line); use a private 0600 file. Repeatable `--header` exposes values in process arguments and shell history. Output redacts values as `***`. host, content-length, content-type and x-questlog-* are reserved. To change headers, remove/add the hook. Use --header-stdin and --secret-stdin separately. URL userinfo is rejected.
25
+ `show d_…` displays delivery attempts, HTTP status, next attempt, redacted target and event ids. Text timestamps are local; --json keeps UTC/epoch times. Missing hook remove/test targets exit 3; missing quests/deliveries also exit 3. Empty hook lists print `No hooks`. hook label names the database.
27
26
  List output redacts userinfo/query strings. Removal deletes the target and deliveries.
28
- Default scope is the database; `--tag` matches quest tags, never rumors.
27
+ Default scope is the DB; `--tag` matches quest tags only.
29
28
  `--events` defaults to all types. The DB label defaults to the filename without its
30
29
  path/extension (`questlog` in memory); set it to a name suitable for disclosure.
31
30
 
@@ -33,8 +32,8 @@ path/extension (`questlog` in memory); set it to a name suitable for disclosure.
33
32
  * * * * * questlog tick
34
33
  ```
35
34
 
36
- Cron's PATH may need `$HOME/.local/bin/questlog tick --db /path/to/quests.db`.
37
- Watch defaults to 60 seconds (minimum 5); SIGINT/SIGTERM finishes the current tick.
35
+ Cron may need `$HOME/.local/bin/questlog tick --db /path/to/quests.db`.
36
+ Watch defaults to 60 s (minimum 5); SIGINT/SIGTERM finishes the tick.
38
37
  Tick exits 0 even when deliveries need retries. Dry-run records nothing and sends nothing.
39
38
 
40
39
  Events are detected from the model at tick time:
@@ -45,9 +44,8 @@ Events are detected from the model at tick time:
45
44
  - `repeatable.spawned`: a concrete repeatable instance was created; occurrence time is its creation time. Tick first materializes due anchors through the existing spawn operation.
46
45
  - `rumor.captured`: an undeleted rumor was created; occurrence time is its creation time, even if subsequently settled or dismissed.
47
46
 
48
- Persistent event keys contain type, entity ids, and occurrence time. Restarts and repeated
49
- ticks do not repeat occurrences. Removed transitions between ticks cannot be reconstructed.
50
- Only occurrences at/after target creation are assigned; tags are evaluated at assignment.
47
+ Event keys include type, entity ids and occurrence time, preventing repeats across restarts. Transitions removed between ticks cannot be reconstructed.
48
+ Only events at/after target creation are assigned; tags are checked then.
51
49
  Tick materializes due repeatables using the existing spawn path, including during preview
52
50
  with rollback. Batches contain only ids, event types, timestamps, and the DB label:
53
51
 
@@ -65,15 +63,13 @@ event immediately, ignoring the min gap. Add and test are CLI-only in the tool f
65
63
  2xx delivers. Timeout is 5 seconds. Failures retry after 30 s, 1 m, 2 m, 4 m, doubling
66
64
  to 1 h plus <1 s jitter, subject to the target's min gap. Eight failures mark failed.
67
65
  Retries keep the delivery id/events; new events queue behind them. Keep the last 500
68
- delivered log entries per target and all pending/failed entries. Transactional claims
69
- lease work for two minutes. A crash after receipt may retry: dedupe at the receiver.
66
+ delivered log entries per target and all pending/failed entries. Claims lease work for two minutes. Dedupe retries at the receiver.
70
67
 
71
68
  ### Verifying a delivery
72
69
 
73
70
  Compute HMAC-SHA256 over `timestamp + "." + rawBody` with the secret and compare in constant
74
71
  time. Reject timestamps older than 5 minutes (and timestamps too far in the future), then
75
- dedupe on `x-questlog-delivery`. Use durable deduplication storage in a production receiver.
76
- This 15-line Node example uses an in-memory set:
72
+ dedupe on `x-questlog-delivery`. Use durable deduplication in production; this example uses an in-memory set:
77
73
 
78
74
  ```js
79
75
  import { createHmac, timingSafeEqual } from 'node:crypto';
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export { textOutput } from './chunk-QK3H2AL5.js';