@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.
@@ -2,6 +2,29 @@
2
2
  import { mkdirSync } from 'node:fs';
3
3
  import { dirname } from 'node:path';
4
4
 
5
+ // src/hooks/init_hook_tables.ts
6
+ function initHookTables(db) {
7
+ db.exec(`
8
+ CREATE TABLE IF NOT EXISTS hook_targets (
9
+ id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL, secret TEXT NOT NULL,
10
+ tag TEXT, events TEXT NOT NULL, min_gap INTEGER NOT NULL, created_at INTEGER NOT NULL,
11
+ last_wake INTEGER, lease_until INTEGER, claim TEXT);
12
+ CREATE TABLE IF NOT EXISTS hook_events (
13
+ id TEXT PRIMARY KEY, occurred_at INTEGER NOT NULL, event TEXT NOT NULL);
14
+ CREATE TABLE IF NOT EXISTS hook_assignments (
15
+ target_id INTEGER NOT NULL REFERENCES hook_targets(id) ON DELETE CASCADE,
16
+ event_id TEXT NOT NULL REFERENCES hook_events(id), delivery_id TEXT,
17
+ PRIMARY KEY(target_id, event_id));
18
+ CREATE TABLE IF NOT EXISTS hook_deliveries (
19
+ id TEXT PRIMARY KEY, target_id INTEGER NOT NULL REFERENCES hook_targets(id) ON DELETE CASCADE,
20
+ events TEXT NOT NULL, status TEXT NOT NULL, attempts INTEGER NOT NULL DEFAULT 0,
21
+ last_attempt_at INTEGER, http_status INTEGER, error_class TEXT,
22
+ next_attempt_at INTEGER NOT NULL);
23
+ CREATE INDEX IF NOT EXISTS idx_hook_deliveries_target ON hook_deliveries(target_id, status, next_attempt_at);
24
+ CREATE TABLE IF NOT EXISTS hook_settings (id INTEGER PRIMARY KEY CHECK(id = 1), label TEXT NOT NULL);
25
+ `);
26
+ }
27
+
5
28
  // src/questlines/init_questline_tables.ts
6
29
  function initQuestlineTables(db) {
7
30
  db.exec(`
@@ -350,6 +373,7 @@ function initQuestlogTables(db) {
350
373
  initTagTables(db);
351
374
  initQuestSearch(db);
352
375
  initRumorSearch(db);
376
+ initHookTables(db);
353
377
  }
354
378
 
355
379
  // src/surface/open_questlog_db.ts
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-XNZP4TY2.js');
16
+ await import('./cli_main-7AIEZV75.js');
17
17
  }
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { isErrorResult, questlogVersion, resolveQuestlogDbPath } from './chunk-DKH623HU.js';
3
- import { questlogSkills } from './chunk-6KTQK3ZE.js';
4
- import { reviewQuestlogTool, validateInput, getQuestDetail, questlogSurfaceTools } from './chunk-UTUQPSGO.js';
2
+ import { CliUsageError, renderCliHelp, parseCliArgs } from './chunk-GVD6A3RR.js';
3
+ import { isErrorResult, resolveQuestlogDbPath } from './chunk-IL2QRKZL.js';
4
+ import { questlogSkills } from './chunk-D2W5MYNO.js';
5
+ import { reviewQuestlogTool, validateInput, getQuestDetail, questlogSurfaceTools } from './chunk-55JAQORX.js';
6
+ import { listDeliveries, questlogVersion } from './chunk-N7V7VUI5.js';
5
7
  import { minimalInput } from './chunk-QYSIJ2YN.js';
6
8
  import { isSea } from 'node:sea';
7
- import { readFileSync, existsSync } from 'node:fs';
9
+ import { existsSync } from 'node:fs';
8
10
  import { resolve } from 'node:path';
9
11
 
10
12
  var npmEntry = false;
@@ -15,7 +17,8 @@ function cliVersion(version, {
15
17
  sea = isSea,
16
18
  npm = npmEntry,
17
19
  platform = process.platform,
18
- arch = process.arch
20
+ arch = process.arch,
21
+ args = []
19
22
  } = {}) {
20
23
  let binary = false;
21
24
  try {
@@ -23,111 +26,12 @@ function cliVersion(version, {
23
26
  } catch {
24
27
  }
25
28
  const target = `${platform === "win32" ? "windows" : platform}-${arch}`;
29
+ const variant = binary ? "binary" : npm ? "npm" : "library";
30
+ if (args.includes("--json"))
31
+ return JSON.stringify({ name: "questlog", version, variant, target });
32
+ if (args.includes("--short")) return version;
26
33
  return `questlog ${version} (${binary ? `binary, ${target}` : npm ? "npm" : "library"})`;
27
34
  }
28
- var CliUsageError = class extends Error {
29
- exitCode = 2;
30
- };
31
- var keyOf = (name) => name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
32
- function parseCliArgs(argv, spec) {
33
- const values = {};
34
- const positions = [];
35
- const flags = spec.flags ?? [];
36
- const convert = (raw, arg) => {
37
- let value = raw;
38
- if (raw.startsWith("@")) {
39
- try {
40
- value = readFileSync(raw.slice(1), "utf8");
41
- } catch {
42
- throw new CliUsageError(`Cannot read value file: ${raw.slice(1)}`);
43
- }
44
- }
45
- let result = value;
46
- if (arg.type === "number" || arg.type === "integer") {
47
- result = Number(value);
48
- if (!value.trim() || !Number.isFinite(result) || arg.type === "integer" && !Number.isInteger(result))
49
- throw new CliUsageError(`Expected ${arg.type} for ${arg.name}: ${value}`);
50
- } else if (arg.type === "boolean") {
51
- if (!["true", "false"].includes(value))
52
- throw new CliUsageError(`Expected boolean for ${arg.name}`);
53
- result = value === "true";
54
- } else if (arg.type === "json") {
55
- try {
56
- result = JSON.parse(value);
57
- } catch {
58
- throw new CliUsageError(`Invalid JSON for ${arg.name}`);
59
- }
60
- }
61
- if (arg.enum && !arg.enum.includes(result))
62
- throw new CliUsageError(`${arg.name} must be one of: ${arg.enum.join(", ")}`);
63
- return result;
64
- };
65
- let ended = false;
66
- for (let i = 0; i < argv.length; i++) {
67
- const token = argv[i];
68
- if (!ended && token === "--") {
69
- ended = true;
70
- continue;
71
- }
72
- if (ended || token === "-" || !token.startsWith("-")) {
73
- positions.push(token);
74
- continue;
75
- }
76
- const [flag, inline] = token.split(/=(.*)/s);
77
- const negative = flag?.startsWith("--no-");
78
- const name = flag?.replace(/^--?/, "").replace(/^no-/, "");
79
- const arg = flags.find(
80
- (entry) => entry.name === name || keyOf(entry.name) === name || entry.short === name
81
- );
82
- if (!arg) {
83
- const prefix = (name ?? "").slice(0, 3);
84
- const suggestion = prefix ? flags.find((entry) => entry.name.startsWith(prefix)) : void 0;
85
- throw new CliUsageError(
86
- `Unknown flag: ${flag}${suggestion ? `; did you mean --${suggestion.name}?` : ""}`
87
- );
88
- }
89
- if (negative && arg.type !== "boolean")
90
- throw new CliUsageError(`--no-${arg.name} requires a boolean flag`);
91
- let raw = inline;
92
- if (arg.type === "boolean" && raw === void 0) raw = negative ? "false" : "true";
93
- if (raw === void 0) {
94
- const next = argv[i + 1];
95
- if (next === void 0 || /^--[a-z]/i.test(next))
96
- throw new CliUsageError(`Missing value: --${arg.name}`);
97
- raw = next;
98
- i++;
99
- }
100
- const key = keyOf(arg.name);
101
- if (arg.type === "array") {
102
- const item = convert(raw, { ...arg, type: arg.itemType ?? "string" });
103
- values[key] = [...values[key] ?? [], item];
104
- } else values[key] = convert(raw, arg);
105
- }
106
- let index = 0;
107
- for (const arg of spec.positionals ?? []) {
108
- if (index >= positions.length) continue;
109
- values[keyOf(arg.name)] = arg.variadic ? positions.slice(index).map((value) => convert(value, { ...arg, type: arg.itemType ?? arg.type })) : convert(positions[index], arg);
110
- index = arg.variadic ? positions.length : index + 1;
111
- }
112
- if (index < positions.length)
113
- throw new CliUsageError(`Unexpected positional: ${positions[index]}`);
114
- for (const arg of [...flags, ...spec.positionals ?? []]) {
115
- if (arg.required && values[keyOf(arg.name)] === void 0)
116
- throw new CliUsageError(`Missing required argument: ${arg.name}`);
117
- }
118
- return values;
119
- }
120
- function renderCliHelp(spec) {
121
- const describe = (arg) => `${arg.type}${arg.enum ? ` (${arg.enum.join("|")})` : ""}${arg.required ? " required" : ""} ${arg.description ?? ""}`;
122
- return `Usage: ${spec.command} ${(spec.positionals ?? []).map((arg) => `<${arg.name}${arg.variadic ? "..." : ""}>`).join(" ")} [flags]
123
- ${spec.description ?? ""}
124
-
125
- Positionals:
126
- ${(spec.positionals ?? []).map((arg) => ` ${arg.name} ${describe(arg)}`).join("\n")}
127
- Flags:
128
- ${(spec.flags ?? []).map((arg) => ` --${arg.name}${arg.short ? `, -${arg.short}` : ""} ${describe(arg)}`).join("\n")}
129
- `;
130
- }
131
35
 
132
36
  // src/surface/plain_dates.ts
133
37
  function parsePlainDue(raw, now = Date.now()) {
@@ -210,6 +114,8 @@ function renderQuestlogPlainResult(result, db, values) {
210
114
  Missing: ${r.clarification.missing.map((name) => `--${name}`).join(", ")}
211
115
  ` : `${r.summary}
212
116
  ${r.error.message}
117
+ `;
118
+ if (r.data.deliveries && !r.data.item) return `${JSON.stringify(r.data.deliveries, null, 2)}
213
119
  `;
214
120
  if (r.data.items) {
215
121
  const items = values.limit === void 0 ? r.data.items : r.data.items.slice(0, Math.max(0, Number(values.limit)));
@@ -235,6 +141,11 @@ ${r.error.message}
235
141
  ]);
236
142
  if (detail.outcome != null) fields.push(["outcome", detail.outcome]);
237
143
  if (detail.questlineId != null) fields.push(["questline", detail.questlineId]);
144
+ for (const delivery of listDeliveries(db, { questId: detail.id }))
145
+ fields.push([
146
+ "delivery",
147
+ `${delivery.id} target ${delivery.targetId} ${delivery.status} attempts ${delivery.attempts}`
148
+ ]);
238
149
  const width = Math.max(...fields.map(([label]) => label.length));
239
150
  return `${fields.map(([label, value]) => `${label.padEnd(width)}: ${String(value).replace(/[\r\n]+/g, " ")}`).join("\n")}
240
151
  `;
@@ -258,6 +169,34 @@ function questlogPlainCommands() {
258
169
  const id = { name: "id", type: "integer", required: true };
259
170
  const views = reviewQuestlogTool.inputSchema.properties?.view?.enum ?? [];
260
171
  const commands = [
172
+ {
173
+ spec: {
174
+ command: "tick",
175
+ description: "Detect events and deliver outbound webhooks once.",
176
+ flags: [{ name: "dry-run", type: "boolean" }]
177
+ },
178
+ tool: "manage_hooks",
179
+ input: (v) => ({ action: "tick", dry_run: v.dryRun ?? false }),
180
+ render: (r, _db, values) => {
181
+ const result = r;
182
+ if (!result.ok || !result.data) return `${result.summary}
183
+ `;
184
+ const d = result.data;
185
+ return `${d.events.length} events found; ${d.sent} deliveries sent, ${d.queued} queued, ${d.retrying} retrying, ${d.failed} failed.
186
+ ${values.dryRun ? `${JSON.stringify({ events: d.events, batches: d.batches }, null, 2)}
187
+ ` : ""}`;
188
+ }
189
+ },
190
+ {
191
+ spec: {
192
+ command: "review",
193
+ description: "Review an operational view.",
194
+ positionals: [{ name: "view", type: "string", required: true, enum: views }]
195
+ },
196
+ tool: "review_questlog",
197
+ input: (v) => ({ view: v.view }),
198
+ render: renderQuestlogPlainResult
199
+ },
261
200
  {
262
201
  spec: {
263
202
  command: "add",
@@ -370,13 +309,13 @@ function selectedDbPath(options) {
370
309
  var questlogCliOptions = {
371
310
  bin: "ghostpaw-questlog",
372
311
  version: questlogVersion,
373
- versionLine: () => cliVersion(questlogVersion),
312
+ versionLine: (args) => cliVersion(questlogVersion, { args }),
374
313
  description: "Local task and calendar library",
375
314
  tools: questlogSurfaceTools,
376
315
  describeDb: (options) => `Database: ${selectedDbPath(options)}
377
316
  Use --db <path> or GHOSTPAW_QUESTLOG_DB to override the default ~/.ghostpaw/questlog.db.`,
378
317
  openContext: async (options) => {
379
- const { openQuestlogDb } = await import('./open_questlog_db-SH3DAU4C.js');
318
+ const { openQuestlogDb } = await import('./open_questlog_db-LAEH7UDM.js');
380
319
  const path = selectedDbPath(options);
381
320
  const created = path !== ":memory:" && !existsSync(path);
382
321
  const db = await openQuestlogDb(path);
@@ -386,15 +325,24 @@ Use --db <path> or GHOSTPAW_QUESTLOG_DB to override the default ~/.ghostpaw/ques
386
325
  },
387
326
  closeContext: (db) => db.close(),
388
327
  commands: {
328
+ names: ["hook", "watch"],
329
+ run: async (command, args, options, io) => {
330
+ if (command !== "hook" && command !== "watch") return void 0;
331
+ const { runHookCommand } = await import('./run_hook_command-C42BQC36.js');
332
+ return runHookCommand(command, args, io, async () => {
333
+ const { openQuestlogDb } = await import('./open_questlog_db-LAEH7UDM.js');
334
+ return openQuestlogDb(selectedDbPath(options));
335
+ });
336
+ },
389
337
  plain: questlogPlainCommands(),
390
338
  guides: questlogSkills,
391
339
  mcp: async (options) => {
392
- const { serveQuestlogMcp } = await import('./serve_questlog_mcp-KFUYJB2P.js');
340
+ const { serveQuestlogMcp } = await import('./serve_questlog_mcp-W3R63HW4.js');
393
341
  await serveQuestlogMcp(options.db === void 0 ? {} : { dbPath: options.db });
394
342
  },
395
343
  skill: async () => {
396
344
  const { renderSkillMarkdown } = await import('./skill_markdown-HXYQ3K6U.js');
397
- const { questlogSkillSpec } = await import('./questlog_skill_spec-EM6DGGVN.js');
345
+ const { questlogSkillSpec } = await import('./questlog_skill_spec-HEW2B6EB.js');
398
346
  return renderSkillMarkdown(questlogSkillSpec);
399
347
  }
400
348
  }
@@ -463,6 +411,12 @@ async function runToolCli(argv, options) {
463
411
  i--;
464
412
  }
465
413
  }
414
+ const versionArgs = args.filter((arg) => arg !== "--short" && arg !== "--json");
415
+ if (versionArgs[0] === "--version" || versionArgs[0] === "-v") {
416
+ stdout.write(`${options.versionLine?.(args) ?? options.version}
417
+ `);
418
+ return 0;
419
+ }
466
420
  const ctxOptions = db === void 0 ? {} : { db };
467
421
  const command = args.shift() ?? "help";
468
422
  const normalize = (name2) => name2.replace(/[-_]/g, "").toLowerCase();
@@ -472,8 +426,14 @@ async function runToolCli(argv, options) {
472
426
  ${options.description}
473
427
 
474
428
  Usage: ${options.bin} [--db path] <command>
475
- Commands: ${options.commands?.plain?.map((entry) => entry.spec.command).join(", ") ?? ""}${options.commands?.plain?.length ? ", " : ""}tools [--json], describe <tool>, call <tool> [JSON|-], <tool> --parameter value, mcp, skill
429
+ Commands: ${[
430
+ ...options.commands?.plain?.map((entry) => entry.spec.command) ?? [],
431
+ ...options.commands?.names ?? []
432
+ ].map((name2) => `${name2}, `).join(
433
+ ""
434
+ )}tools [--json], describe <tool>, call <tool> [JSON|-], <tool> --parameter value, mcp, skill
476
435
  Options: --help, --version, --compact, --db <path>
436
+ Version formats: --version --short, --version --json
477
437
  ${options.describeDb ? `${options.describeDb(ctxOptions)}
478
438
  ` : ""}
479
439
  ${options.tools.map((tool2) => `${tool2.name} ${tool2.description}`).join("\n")}
@@ -481,11 +441,6 @@ ${options.tools.map((tool2) => `${tool2.name} ${tool2.description}`).join("\n")
481
441
  );
482
442
  return 0;
483
443
  }
484
- if (["--version", "-v"].includes(command)) {
485
- stdout.write(`${options.versionLine?.() ?? options.version}
486
- `);
487
- return 0;
488
- }
489
444
  if (command === "tools") {
490
445
  stdout.write(
491
446
  args.includes("--json") ? `${JSON.stringify(
@@ -527,6 +482,12 @@ ${options.tools.map((tool2) => `${tool2.name} ${tool2.description}`).join("\n")
527
482
  await options.commands.mcp(ctxOptions);
528
483
  return 0;
529
484
  }
485
+ const custom = await options.commands?.run?.(command, args, ctxOptions, {
486
+ stdin,
487
+ stdout,
488
+ stderr
489
+ });
490
+ if (custom !== void 0) return custom;
530
491
  const plain = options.commands?.plain?.find((entry) => entry.spec.command === command);
531
492
  let plainValues = {};
532
493
  if (plain) {
@@ -556,6 +517,7 @@ ${options.tools.map((tool2) => `${tool2.name} ${tool2.description}`).join("\n")
556
517
  if (command === "call" || command === "describe") return fail(`Unknown tool: ${name ?? ""}`);
557
518
  const suggestion = suggestCommand(command, [
558
519
  ...options.commands?.plain?.map((entry) => entry.spec.command) ?? [],
520
+ ...options.commands?.names ?? [],
559
521
  "help",
560
522
  "tools",
561
523
  "describe",