@ghostpaw/questlog 0.9.1 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env node
2
+ // src/surfaces/schema_actions.ts
3
+ var requirements = /* @__PURE__ */ new WeakMap();
4
+ function registerSchemaActions(schema, actions) {
5
+ requirements.set(schema, actions);
6
+ }
7
+ function requiredForAction(schema, action) {
8
+ return requirements.get(schema)?.[String(action)] ?? [];
9
+ }
10
+
11
+ // src/surfaces/schema_examples.ts
12
+ function minimalInput(schema, action, name = "") {
13
+ const constant = "const" in schema ? schema.const : void 0;
14
+ if (constant !== void 0) return constant;
15
+ if (schema.enum)
16
+ return action !== void 0 && schema.enum.includes(action) ? action : schema.enum[0];
17
+ const branch = schema.oneOf?.[0];
18
+ if (schema.type === "object" || schema.properties) {
19
+ action ??= schema.properties?.action?.enum?.[0];
20
+ const properties = { ...schema.properties, ...branch?.properties };
21
+ return Object.fromEntries(
22
+ [
23
+ .../* @__PURE__ */ new Set([
24
+ ...schema.required ?? [],
25
+ ...requiredForAction(schema, action),
26
+ ...schema.properties?.outcome && ["finish", "abandon", "abandon_and_spawn_followups"].includes(String(action)) ? ["outcome"] : [],
27
+ ...branch?.required ?? []
28
+ ])
29
+ ].map((key) => [
30
+ key,
31
+ minimalInput(properties[key] ?? {}, key === "action" ? action : void 0, key)
32
+ ])
33
+ );
34
+ }
35
+ if (branch) return minimalInput(branch, action, name);
36
+ if (schema.type === "array") return [minimalInput(schema.items ?? {}, void 0, name)];
37
+ if (schema.type === "integer" || schema.type === "number")
38
+ return name === "anchorAt" ? 18e11 : name === "toQuestId" ? 2 : 1;
39
+ if (schema.type === "boolean") return false;
40
+ if (schema.type === "null") return null;
41
+ if (name === "rrule") return "FREQ=WEEKLY";
42
+ if (name === "title") return "Review release notes";
43
+ if (name === "objective") return "Check changes against the release";
44
+ return "release";
45
+ }
46
+
47
+ export { minimalInput, registerSchemaActions, requiredForAction };
@@ -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-ELAKW6KW.js');
16
+ await import('./cli_main-7AIEZV75.js');
17
17
  }
@@ -1,111 +1,36 @@
1
1
  #!/usr/bin/env node
2
- import { validateInput, isErrorResult, questlogVersion, resolveQuestlogDbPath } from './chunk-KRQDV4KF.js';
3
- import { reviewQuestlogTool, getQuestDetail, questlogSurfaceTools } from './chunk-3Y5F3SMB.js';
4
- import { readFileSync, existsSync } from 'node:fs';
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';
7
+ import { minimalInput } from './chunk-QYSIJ2YN.js';
8
+ import { isSea } from 'node:sea';
9
+ import { existsSync } from 'node:fs';
5
10
  import { resolve } from 'node:path';
6
11
 
7
- var CliUsageError = class extends Error {
8
- exitCode = 2;
9
- };
10
- var keyOf = (name) => name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
11
- function parseCliArgs(argv, spec) {
12
- const values = {};
13
- const positions = [];
14
- const flags = spec.flags ?? [];
15
- const convert = (raw, arg) => {
16
- let value = raw;
17
- if (raw.startsWith("@")) {
18
- try {
19
- value = readFileSync(raw.slice(1), "utf8");
20
- } catch {
21
- throw new CliUsageError(`Cannot read value file: ${raw.slice(1)}`);
22
- }
23
- }
24
- let result = value;
25
- if (arg.type === "number" || arg.type === "integer") {
26
- result = Number(value);
27
- if (!value.trim() || !Number.isFinite(result) || arg.type === "integer" && !Number.isInteger(result))
28
- throw new CliUsageError(`Expected ${arg.type} for ${arg.name}: ${value}`);
29
- } else if (arg.type === "boolean") {
30
- if (!["true", "false"].includes(value))
31
- throw new CliUsageError(`Expected boolean for ${arg.name}`);
32
- result = value === "true";
33
- } else if (arg.type === "json") {
34
- try {
35
- result = JSON.parse(value);
36
- } catch {
37
- throw new CliUsageError(`Invalid JSON for ${arg.name}`);
38
- }
39
- }
40
- if (arg.enum && !arg.enum.includes(result))
41
- throw new CliUsageError(`${arg.name} must be one of: ${arg.enum.join(", ")}`);
42
- return result;
43
- };
44
- let ended = false;
45
- for (let i = 0; i < argv.length; i++) {
46
- const token = argv[i];
47
- if (!ended && token === "--") {
48
- ended = true;
49
- continue;
50
- }
51
- if (ended || token === "-" || !token.startsWith("-")) {
52
- positions.push(token);
53
- continue;
54
- }
55
- const [flag, inline] = token.split(/=(.*)/s);
56
- const negative = flag?.startsWith("--no-");
57
- const name = flag?.replace(/^--?/, "").replace(/^no-/, "");
58
- const arg = flags.find(
59
- (entry) => entry.name === name || keyOf(entry.name) === name || entry.short === name
60
- );
61
- if (!arg) {
62
- const prefix = (name ?? "").slice(0, 3);
63
- const suggestion = prefix ? flags.find((entry) => entry.name.startsWith(prefix)) : void 0;
64
- throw new CliUsageError(
65
- `Unknown flag: ${flag}${suggestion ? `; did you mean --${suggestion.name}?` : ""}`
66
- );
67
- }
68
- if (negative && arg.type !== "boolean")
69
- throw new CliUsageError(`--no-${arg.name} requires a boolean flag`);
70
- let raw = inline;
71
- if (arg.type === "boolean" && raw === void 0) raw = negative ? "false" : "true";
72
- if (raw === void 0) {
73
- const next = argv[i + 1];
74
- if (next === void 0 || next.startsWith("--"))
75
- throw new CliUsageError(`Missing value: --${arg.name}`);
76
- raw = next;
77
- i++;
78
- }
79
- const key = keyOf(arg.name);
80
- if (arg.type === "array") {
81
- const item = convert(raw, { ...arg, type: arg.itemType ?? "string" });
82
- values[key] = [...values[key] ?? [], item];
83
- } else values[key] = convert(raw, arg);
84
- }
85
- let index = 0;
86
- for (const arg of spec.positionals ?? []) {
87
- if (index >= positions.length) continue;
88
- values[keyOf(arg.name)] = arg.variadic ? positions.slice(index).map((value) => convert(value, { ...arg, type: arg.itemType ?? arg.type })) : convert(positions[index], arg);
89
- index = arg.variadic ? positions.length : index + 1;
90
- }
91
- if (index < positions.length)
92
- throw new CliUsageError(`Unexpected positional: ${positions[index]}`);
93
- for (const arg of [...flags, ...spec.positionals ?? []]) {
94
- if (arg.required && values[keyOf(arg.name)] === void 0)
95
- throw new CliUsageError(`Missing required argument: ${arg.name}`);
96
- }
97
- return values;
12
+ var npmEntry = false;
13
+ function markNpmEntry() {
14
+ npmEntry = true;
98
15
  }
99
- function renderCliHelp(spec) {
100
- const describe = (arg) => `${arg.type}${arg.enum ? ` (${arg.enum.join("|")})` : ""}${arg.required ? " required" : ""} ${arg.description ?? ""}`;
101
- return `Usage: ${spec.command} ${(spec.positionals ?? []).map((arg) => `<${arg.name}${arg.variadic ? "..." : ""}>`).join(" ")} [flags]
102
- ${spec.description ?? ""}
103
-
104
- Positionals:
105
- ${(spec.positionals ?? []).map((arg) => ` ${arg.name} ${describe(arg)}`).join("\n")}
106
- Flags:
107
- ${(spec.flags ?? []).map((arg) => ` --${arg.name}${arg.short ? `, -${arg.short}` : ""} ${describe(arg)}`).join("\n")}
108
- `;
16
+ function cliVersion(version, {
17
+ sea = isSea,
18
+ npm = npmEntry,
19
+ platform = process.platform,
20
+ arch = process.arch,
21
+ args = []
22
+ } = {}) {
23
+ let binary = false;
24
+ try {
25
+ binary = sea();
26
+ } catch {
27
+ }
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;
33
+ return `questlog ${version} (${binary ? `binary, ${target}` : npm ? "npm" : "library"})`;
109
34
  }
110
35
 
111
36
  // src/surface/plain_dates.ts
@@ -189,6 +114,8 @@ function renderQuestlogPlainResult(result, db, values) {
189
114
  Missing: ${r.clarification.missing.map((name) => `--${name}`).join(", ")}
190
115
  ` : `${r.summary}
191
116
  ${r.error.message}
117
+ `;
118
+ if (r.data.deliveries && !r.data.item) return `${JSON.stringify(r.data.deliveries, null, 2)}
192
119
  `;
193
120
  if (r.data.items) {
194
121
  const items = values.limit === void 0 ? r.data.items : r.data.items.slice(0, Math.max(0, Number(values.limit)));
@@ -214,6 +141,11 @@ ${r.error.message}
214
141
  ]);
215
142
  if (detail.outcome != null) fields.push(["outcome", detail.outcome]);
216
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
+ ]);
217
149
  const width = Math.max(...fields.map(([label]) => label.length));
218
150
  return `${fields.map(([label, value]) => `${label.padEnd(width)}: ${String(value).replace(/[\r\n]+/g, " ")}`).join("\n")}
219
151
  `;
@@ -237,12 +169,41 @@ function questlogPlainCommands() {
237
169
  const id = { name: "id", type: "integer", required: true };
238
170
  const views = reviewQuestlogTool.inputSchema.properties?.view?.enum ?? [];
239
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
+ },
240
200
  {
241
201
  spec: {
242
202
  command: "add",
243
- description: "Create a quest. Objective defaults to the title.",
203
+ description: 'Create a quest: add "My title" or add --title "My title". Objective defaults to the title.',
204
+ positionals: [{ name: "positionalTitle", type: "string" }],
244
205
  flags: [
245
- { name: "title", type: "string", required: true },
206
+ { name: "title", type: "string" },
246
207
  { name: "tag", type: "array" },
247
208
  { name: "objective", type: "string" },
248
209
  {
@@ -255,9 +216,15 @@ function questlogPlainCommands() {
255
216
  },
256
217
  tool: "shape_work",
257
218
  input: (v) => {
219
+ if (v.title !== void 0 && v.positionalTitle !== void 0)
220
+ throw new CliUsageError(
221
+ "give the title once: either as the first argument or with --title"
222
+ );
223
+ const title = v.positionalTitle ?? v.title;
224
+ if (title === void 0) throw new CliUsageError("Missing required argument: title");
258
225
  const quest = {
259
- title: v.title,
260
- objective: v.objective ?? v.title
226
+ title,
227
+ objective: v.objective ?? title
261
228
  };
262
229
  if (v.tag !== void 0)
263
230
  quest.tags = v.tag.flatMap(
@@ -342,12 +309,13 @@ function selectedDbPath(options) {
342
309
  var questlogCliOptions = {
343
310
  bin: "ghostpaw-questlog",
344
311
  version: questlogVersion,
312
+ versionLine: (args) => cliVersion(questlogVersion, { args }),
345
313
  description: "Local task and calendar library",
346
314
  tools: questlogSurfaceTools,
347
315
  describeDb: (options) => `Database: ${selectedDbPath(options)}
348
316
  Use --db <path> or GHOSTPAW_QUESTLOG_DB to override the default ~/.ghostpaw/questlog.db.`,
349
317
  openContext: async (options) => {
350
- const { openQuestlogDb } = await import('./open_questlog_db-SH3DAU4C.js');
318
+ const { openQuestlogDb } = await import('./open_questlog_db-LAEH7UDM.js');
351
319
  const path = selectedDbPath(options);
352
320
  const created = path !== ":memory:" && !existsSync(path);
353
321
  const db = await openQuestlogDb(path);
@@ -357,14 +325,24 @@ Use --db <path> or GHOSTPAW_QUESTLOG_DB to override the default ~/.ghostpaw/ques
357
325
  },
358
326
  closeContext: (db) => db.close(),
359
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
+ },
360
337
  plain: questlogPlainCommands(),
338
+ guides: questlogSkills,
361
339
  mcp: async (options) => {
362
- const { serveQuestlogMcp } = await import('./serve_questlog_mcp-QSHMNNFT.js');
340
+ const { serveQuestlogMcp } = await import('./serve_questlog_mcp-W3R63HW4.js');
363
341
  await serveQuestlogMcp(options.db === void 0 ? {} : { dbPath: options.db });
364
342
  },
365
343
  skill: async () => {
366
- const { renderSkillMarkdown } = await import('./skill_markdown-7HCMSJYR.js');
367
- const { questlogSkillSpec } = await import('./questlog_skill_spec-LOQ2FNOF.js');
344
+ const { renderSkillMarkdown } = await import('./skill_markdown-HXYQ3K6U.js');
345
+ const { questlogSkillSpec } = await import('./questlog_skill_spec-HEW2B6EB.js');
368
346
  return renderSkillMarkdown(questlogSkillSpec);
369
347
  }
370
348
  }
@@ -433,6 +411,12 @@ async function runToolCli(argv, options) {
433
411
  i--;
434
412
  }
435
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
+ }
436
420
  const ctxOptions = db === void 0 ? {} : { db };
437
421
  const command = args.shift() ?? "help";
438
422
  const normalize = (name2) => name2.replace(/[-_]/g, "").toLowerCase();
@@ -442,8 +426,14 @@ async function runToolCli(argv, options) {
442
426
  ${options.description}
443
427
 
444
428
  Usage: ${options.bin} [--db path] <command>
445
- 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
446
435
  Options: --help, --version, --compact, --db <path>
436
+ Version formats: --version --short, --version --json
447
437
  ${options.describeDb ? `${options.describeDb(ctxOptions)}
448
438
  ` : ""}
449
439
  ${options.tools.map((tool2) => `${tool2.name} ${tool2.description}`).join("\n")}
@@ -451,11 +441,6 @@ ${options.tools.map((tool2) => `${tool2.name} ${tool2.description}`).join("\n")
451
441
  );
452
442
  return 0;
453
443
  }
454
- if (["--version", "-v"].includes(command)) {
455
- stdout.write(`${options.version}
456
- `);
457
- return 0;
458
- }
459
444
  if (command === "tools") {
460
445
  stdout.write(
461
446
  args.includes("--json") ? `${JSON.stringify(
@@ -470,7 +455,26 @@ ${options.tools.map((tool2) => `${tool2.name} ${tool2.description}`).join("\n")
470
455
  }
471
456
  if (command === "skill") {
472
457
  if (!options.commands?.skill) return fail("skill command unavailable");
473
- stdout.write(await options.commands.skill());
458
+ if (args.length > 1) return fail(`Usage: ${options.bin} skill [--list | <guide> | --help]`);
459
+ const guide = args[0];
460
+ if (guide === "--help" || guide === "-h") {
461
+ stdout.write(
462
+ `Usage:
463
+ ${options.bin} skill
464
+ ${options.bin} skill --list
465
+ ${options.bin} skill <guide>
466
+ `
467
+ );
468
+ } else if (guide === "--list") {
469
+ stdout.write(
470
+ `${(options.commands.guides ?? []).map((entry) => `${entry.name} ${entry.description.replace(/\s+/g, " ")}`).join("\n")}
471
+ `
472
+ );
473
+ } else if (guide) {
474
+ const entry = options.commands.guides?.find((entry2) => entry2.name === guide);
475
+ if (!entry) return fail(`unknown guide ${guide}; run ${options.bin} skill --list`);
476
+ stdout.write(entry.content);
477
+ } else stdout.write(await options.commands.skill());
474
478
  return 0;
475
479
  }
476
480
  if (command === "mcp") {
@@ -478,6 +482,12 @@ ${options.tools.map((tool2) => `${tool2.name} ${tool2.description}`).join("\n")
478
482
  await options.commands.mcp(ctxOptions);
479
483
  return 0;
480
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;
481
491
  const plain = options.commands?.plain?.find((entry) => entry.spec.command === command);
482
492
  let plainValues = {};
483
493
  if (plain) {
@@ -507,6 +517,7 @@ ${options.tools.map((tool2) => `${tool2.name} ${tool2.description}`).join("\n")
507
517
  if (command === "call" || command === "describe") return fail(`Unknown tool: ${name ?? ""}`);
508
518
  const suggestion = suggestCommand(command, [
509
519
  ...options.commands?.plain?.map((entry) => entry.spec.command) ?? [],
520
+ ...options.commands?.names ?? [],
510
521
  "help",
511
522
  "tools",
512
523
  "describe",
@@ -599,7 +610,11 @@ ${JSON.stringify(tool.inputSchema, null, 2)}
599
610
  return fail(`Invalid input: ${cause instanceof Error ? cause.message : String(cause)}`, 1);
600
611
  }
601
612
  const errors = validateInput(tool.inputSchema, input);
602
- if (errors.length) return fail(errors.join("\n"), 1);
613
+ if (errors.length)
614
+ return fail(
615
+ `${errors.join("\n")}; pass ${JSON.stringify(minimalInput(tool.inputSchema, input?.action))}`,
616
+ 1
617
+ );
603
618
  let context;
604
619
  try {
605
620
  context = await options.openContext(ctxOptions);
@@ -623,4 +638,5 @@ ${JSON.stringify(tool.inputSchema, null, 2)}
623
638
  }
624
639
 
625
640
  // src/cli_main.ts
641
+ markNpmEntry();
626
642
  process.exitCode = await runToolCli(process.argv.slice(2), questlogCliOptions);