@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.
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import { tickQuestlog, removeHook, listDeliveries, listHooks, listDueRepeatableQuestAnchors, getRepeatableQuestOrThrow, spawnDueRepeatableQuests, deriveQuestState, deriveQuestMarkerId, queryQuestDetailRecords, resolveNow, withTransaction, getQuestOrThrow, parseRRule, mapQuestRewardRow, normalizeTagName } from './chunk-N7V7VUI5.js';
2
3
  import { registerSchemaActions, minimalInput, requiredForAction } from './chunk-QYSIJ2YN.js';
3
4
 
4
5
  // src/questlines/map_questline_row.ts
@@ -118,51 +119,6 @@ function assertNonEmpty(value, label) {
118
119
  return trimmed;
119
120
  }
120
121
 
121
- // src/resolve_now.ts
122
- function resolveNow(now) {
123
- return now ?? Date.now();
124
- }
125
-
126
- // src/quests/map_quest_row.ts
127
- function asBoolean(value) {
128
- return Number(value) === 1;
129
- }
130
- function mapQuestRow(row) {
131
- return {
132
- id: Number(row.id),
133
- questlineId: row.questline_id == null ? null : Number(row.questline_id),
134
- sourceRumorId: row.source_rumor_id == null ? null : Number(row.source_rumor_id),
135
- spawnedByQuestId: row.spawned_by_quest_id == null ? null : Number(row.spawned_by_quest_id),
136
- spawnedFromRepeatableId: row.spawned_from_repeatable_id == null ? null : Number(row.spawned_from_repeatable_id),
137
- spawnedForAt: row.spawned_for_at == null ? null : Number(row.spawned_for_at),
138
- title: String(row.title),
139
- objective: String(row.objective),
140
- outcome: row.outcome == null ? null : String(row.outcome),
141
- createdAt: Number(row.created_at),
142
- updatedAt: Number(row.updated_at),
143
- startedAt: row.started_at == null ? null : Number(row.started_at),
144
- resolvedAt: row.resolved_at == null ? null : Number(row.resolved_at),
145
- success: row.success == null ? null : asBoolean(row.success),
146
- effortSeconds: Number(row.effort_seconds),
147
- estimateSeconds: row.estimate_seconds == null ? null : Number(row.estimate_seconds),
148
- notBeforeAt: row.not_before_at == null ? null : Number(row.not_before_at),
149
- dueAt: row.due_at == null ? null : Number(row.due_at),
150
- scheduledStartAt: row.scheduled_start_at == null ? null : Number(row.scheduled_start_at),
151
- scheduledEndAt: row.scheduled_end_at == null ? null : Number(row.scheduled_end_at),
152
- allDay: asBoolean(row.all_day),
153
- deletedAt: row.deleted_at == null ? null : Number(row.deleted_at)
154
- };
155
- }
156
-
157
- // src/quests/get_quest_or_throw.ts
158
- function getQuestOrThrow(db, questId) {
159
- const row = db.prepare("SELECT * FROM quests WHERE id = ? AND deleted_at IS NULL").get(questId);
160
- if (!row) {
161
- throw new Error(`Quest ${questId} was not found.`);
162
- }
163
- return mapQuestRow(row);
164
- }
165
-
166
122
  // src/quests/resolve_quest.ts
167
123
  function resolveQuest(db, questId, success, outcome, resolvedAt) {
168
124
  const current = getQuestOrThrow(db, questId);
@@ -235,41 +191,6 @@ function addUnlock(db, fromQuestId, toQuestId, now) {
235
191
  ).run(fromQuestId, toQuestId, resolveNow(now));
236
192
  }
237
193
 
238
- // src/with_transaction.ts
239
- var transactionDepth = /* @__PURE__ */ new WeakMap();
240
- function withTransaction(db, fn) {
241
- const depth = transactionDepth.get(db) ?? 0;
242
- const savepoint = `questlog_${depth}`;
243
- if (depth === 0) {
244
- db.exec("BEGIN IMMEDIATE");
245
- } else {
246
- db.exec(`SAVEPOINT ${savepoint}`);
247
- }
248
- transactionDepth.set(db, depth + 1);
249
- try {
250
- const result = fn();
251
- if (depth === 0) {
252
- db.exec("COMMIT");
253
- } else {
254
- db.exec(`RELEASE SAVEPOINT ${savepoint}`);
255
- }
256
- transactionDepth.set(db, depth);
257
- return result;
258
- } catch (error) {
259
- try {
260
- if (depth === 0) {
261
- db.exec("ROLLBACK");
262
- } else {
263
- db.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`);
264
- db.exec(`RELEASE SAVEPOINT ${savepoint}`);
265
- }
266
- } catch {
267
- }
268
- transactionDepth.set(db, depth);
269
- throw error;
270
- }
271
- }
272
-
273
194
  // src/assert_non_negative_seconds.ts
274
195
  function assertNonNegativeSeconds(value, label) {
275
196
  if (value != null && value < 0) {
@@ -307,11 +228,6 @@ function replaceActiveQuestRewards(db, questId, rewards, now) {
307
228
  }
308
229
  }
309
230
 
310
- // src/tags/normalize_tag_name.ts
311
- function normalizeTagName(name) {
312
- return name.trim().toLowerCase().replace(/\s+/g, " ");
313
- }
314
-
315
231
  // src/tags/replace_active_quest_tags.ts
316
232
  function getOrCreateTagId(db, name, now) {
317
233
  const normalizedName = normalizeTagName(name);
@@ -444,209 +360,6 @@ function finishQuest(db, questId, outcome, resolvedAt) {
444
360
  return resolveQuest(db, questId, true, outcome, resolvedAt);
445
361
  }
446
362
 
447
- // src/markers/derive_quest_marker_id.ts
448
- function deriveQuestMarkerId({
449
- state,
450
- available,
451
- hasUnclaimedReward,
452
- notBeforeAt,
453
- blockerCount,
454
- now
455
- }) {
456
- if (state === "done") {
457
- return hasUnclaimedReward ? "progress.complete" : null;
458
- }
459
- if (state === "abandoned") {
460
- return null;
461
- }
462
- if (state === "in_progress") {
463
- return "progress.incomplete";
464
- }
465
- if (available) {
466
- return "attention.available";
467
- }
468
- if (notBeforeAt != null && notBeforeAt > now) {
469
- return "attention.available.future";
470
- }
471
- if (blockerCount > 0) {
472
- return "progress.incomplete";
473
- }
474
- return null;
475
- }
476
-
477
- // src/rewards/map_quest_reward_row.ts
478
- function mapQuestRewardRow(row) {
479
- return {
480
- id: Number(row.id),
481
- questId: Number(row.quest_id),
482
- kind: String(row.kind),
483
- name: String(row.name),
484
- description: row.description == null ? null : String(row.description),
485
- quantity: row.quantity == null ? null : Number(row.quantity),
486
- createdAt: Number(row.created_at),
487
- claimedAt: row.claimed_at == null ? null : Number(row.claimed_at),
488
- deletedAt: row.deleted_at == null ? null : Number(row.deleted_at)
489
- };
490
- }
491
-
492
- // src/quests/derive_quest_state.ts
493
- function deriveQuestState(quest) {
494
- if (quest.resolvedAt != null && quest.success === true) {
495
- return "done";
496
- }
497
- if (quest.resolvedAt != null && quest.success === false) {
498
- return "abandoned";
499
- }
500
- if (quest.startedAt != null) {
501
- return "in_progress";
502
- }
503
- return "open";
504
- }
505
-
506
- // src/quests/query_quest_detail_records.ts
507
- function queryQuestDetailRecords(db, filters = {}, now = Date.now(), questIds) {
508
- if (questIds && questIds.length === 0) {
509
- return [];
510
- }
511
- const clauses = [];
512
- const params = [];
513
- if (!filters.includeDeleted) {
514
- clauses.push("q.deleted_at IS NULL");
515
- }
516
- if (questIds && questIds.length > 0) {
517
- clauses.push(`q.id IN (${questIds.map(() => "?").join(", ")})`);
518
- params.push(...questIds);
519
- }
520
- if (filters.questlineId != null) {
521
- clauses.push("q.questline_id = ?");
522
- params.push(filters.questlineId);
523
- }
524
- if (filters.tagNames && filters.tagNames.length > 0) {
525
- const normalized = [...new Set(filters.tagNames.map(normalizeTagName).filter(Boolean))];
526
- for (const tag of normalized) {
527
- clauses.push(
528
- `EXISTS (
529
- SELECT 1
530
- FROM quest_tags qt
531
- JOIN tags t ON t.id = qt.tag_id
532
- WHERE qt.quest_id = q.id
533
- AND qt.deleted_at IS NULL
534
- AND t.deleted_at IS NULL
535
- AND t.normalized_name = ?
536
- )`
537
- );
538
- params.push(tag);
539
- }
540
- }
541
- const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
542
- const rows = db.prepare(
543
- `SELECT q.*,
544
- COALESCE(q.due_at, ql.due_at) AS effective_due_at,
545
- (
546
- SELECT COUNT(*)
547
- FROM quest_unlocks qu
548
- JOIN quests blocker ON blocker.id = qu.from_quest_id
549
- WHERE qu.to_quest_id = q.id
550
- AND qu.deleted_at IS NULL
551
- AND blocker.deleted_at IS NULL
552
- AND NOT (blocker.resolved_at IS NOT NULL AND blocker.success = 1)
553
- ) AS blocker_count
554
- FROM quests q
555
- LEFT JOIN questlines ql ON ql.id = q.questline_id AND ql.deleted_at IS NULL
556
- ${where}
557
- ORDER BY q.created_at DESC, q.id DESC`
558
- ).all(...params);
559
- if (rows.length === 0) {
560
- return [];
561
- }
562
- const ids = rows.map((row) => Number(row.id));
563
- const placeholders = ids.map(() => "?").join(", ");
564
- const tagNamesByQuestId = /* @__PURE__ */ new Map();
565
- const rewardsByQuestId = /* @__PURE__ */ new Map();
566
- const unlocksByQuestId = /* @__PURE__ */ new Map();
567
- const unlockedByQuestId = /* @__PURE__ */ new Map();
568
- for (const row of db.prepare(
569
- `SELECT qt.quest_id, t.name
570
- FROM quest_tags qt
571
- JOIN tags t ON t.id = qt.tag_id
572
- WHERE qt.quest_id IN (${placeholders})
573
- AND qt.deleted_at IS NULL
574
- AND t.deleted_at IS NULL
575
- ORDER BY qt.quest_id, t.name`
576
- ).all(...ids)) {
577
- const questId = Number(row.quest_id);
578
- const tagNames = tagNamesByQuestId.get(questId) ?? [];
579
- tagNames.push(String(row.name));
580
- tagNamesByQuestId.set(questId, tagNames);
581
- }
582
- for (const row of db.prepare(
583
- `SELECT *
584
- FROM quest_rewards
585
- WHERE quest_id IN (${placeholders})
586
- AND deleted_at IS NULL
587
- ORDER BY quest_id, created_at, id`
588
- ).all(...ids)) {
589
- const reward = mapQuestRewardRow(row);
590
- const rewards = rewardsByQuestId.get(reward.questId) ?? [];
591
- rewards.push(reward);
592
- rewardsByQuestId.set(reward.questId, rewards);
593
- }
594
- for (const row of db.prepare(
595
- `SELECT from_quest_id, to_quest_id
596
- FROM quest_unlocks
597
- WHERE from_quest_id IN (${placeholders})
598
- AND deleted_at IS NULL
599
- ORDER BY from_quest_id, to_quest_id`
600
- ).all(...ids)) {
601
- const questId = Number(row.from_quest_id);
602
- const unlocks = unlocksByQuestId.get(questId) ?? [];
603
- unlocks.push(Number(row.to_quest_id));
604
- unlocksByQuestId.set(questId, unlocks);
605
- }
606
- for (const row of db.prepare(
607
- `SELECT from_quest_id, to_quest_id
608
- FROM quest_unlocks
609
- WHERE to_quest_id IN (${placeholders})
610
- AND deleted_at IS NULL
611
- ORDER BY to_quest_id, from_quest_id`
612
- ).all(...ids)) {
613
- const questId = Number(row.to_quest_id);
614
- const unlockedBy = unlockedByQuestId.get(questId) ?? [];
615
- unlockedBy.push(Number(row.from_quest_id));
616
- unlockedByQuestId.set(questId, unlockedBy);
617
- }
618
- return rows.map((row) => {
619
- const quest = mapQuestRow(row);
620
- const blockerCount = Number(row.blocker_count ?? 0);
621
- const effectiveDueAt = row.effective_due_at == null ? null : Number(row.effective_due_at);
622
- const rewards = rewardsByQuestId.get(quest.id) ?? [];
623
- const state = deriveQuestState(quest);
624
- const available = quest.deletedAt == null && quest.resolvedAt == null && (quest.notBeforeAt == null || quest.notBeforeAt <= now) && blockerCount === 0;
625
- const markerId = deriveQuestMarkerId({
626
- state,
627
- available,
628
- hasUnclaimedReward: rewards.some((reward) => reward.claimedAt == null),
629
- notBeforeAt: quest.notBeforeAt,
630
- blockerCount,
631
- now
632
- });
633
- return {
634
- detail: {
635
- ...quest,
636
- state,
637
- markerId,
638
- effectiveDueAt,
639
- available,
640
- tagNames: tagNamesByQuestId.get(quest.id) ?? [],
641
- rewards,
642
- unlocksQuestIds: unlocksByQuestId.get(quest.id) ?? [],
643
- unlockedByQuestIds: unlockedByQuestId.get(quest.id) ?? []
644
- },
645
- blockerCount
646
- };
647
- });
648
- }
649
-
650
363
  // src/quests/get_quest_detail.ts
651
364
  function getQuestDetail(db, questId, now = Date.now()) {
652
365
  const [record] = queryQuestDetailRecords(db, {}, now, [questId]);
@@ -830,215 +543,6 @@ function startQuest(db, questId, startedAt) {
830
543
  return getQuestOrThrow(db, questId);
831
544
  }
832
545
 
833
- // src/repeatable_quests/map_repeatable_quest_row.ts
834
- function asBoolean2(value) {
835
- return Number(value) === 1;
836
- }
837
- function mapRepeatableQuestRow(row) {
838
- return {
839
- id: Number(row.id),
840
- questlineId: row.questline_id == null ? null : Number(row.questline_id),
841
- title: String(row.title),
842
- objective: String(row.objective),
843
- rrule: String(row.rrule),
844
- anchorAt: Number(row.anchor_at),
845
- notBeforeOffsetSeconds: row.not_before_offset_seconds == null ? null : Number(row.not_before_offset_seconds),
846
- dueOffsetSeconds: row.due_offset_seconds == null ? null : Number(row.due_offset_seconds),
847
- scheduledStartOffsetSeconds: row.scheduled_start_offset_seconds == null ? null : Number(row.scheduled_start_offset_seconds),
848
- scheduledEndOffsetSeconds: row.scheduled_end_offset_seconds == null ? null : Number(row.scheduled_end_offset_seconds),
849
- allDay: asBoolean2(row.all_day),
850
- estimateSeconds: row.estimate_seconds == null ? null : Number(row.estimate_seconds),
851
- createdAt: Number(row.created_at),
852
- updatedAt: Number(row.updated_at),
853
- archivedAt: row.archived_at == null ? null : Number(row.archived_at),
854
- deletedAt: row.deleted_at == null ? null : Number(row.deleted_at)
855
- };
856
- }
857
-
858
- // src/repeatable_quests/get_repeatable_quest_or_throw.ts
859
- function getRepeatableQuestOrThrow(db, repeatableQuestId) {
860
- const row = db.prepare("SELECT * FROM repeatable_quests WHERE id = ? AND deleted_at IS NULL").get(repeatableQuestId);
861
- if (!row) {
862
- throw new Error(`Repeatable quest ${repeatableQuestId} was not found.`);
863
- }
864
- return mapRepeatableQuestRow(row);
865
- }
866
-
867
- // src/repeatable_quests/advance_anchor.ts
868
- var WEEK_MS = 7 * 864e5;
869
- function advanceAnchor(anchorAt, freq, interval) {
870
- const date = new Date(anchorAt);
871
- if (freq === "MONTHLY") {
872
- date.setUTCMonth(date.getUTCMonth() + interval);
873
- return date.getTime();
874
- }
875
- if (freq === "YEARLY") {
876
- date.setUTCFullYear(date.getUTCFullYear() + interval);
877
- return date.getTime();
878
- }
879
- return anchorAt + interval * WEEK_MS;
880
- }
881
-
882
- // src/repeatable_quests/parse_rrule.ts
883
- var WEEKDAY = {
884
- SU: 0,
885
- MO: 1,
886
- TU: 2,
887
- WE: 3,
888
- TH: 4,
889
- FR: 5,
890
- SA: 6
891
- };
892
- function parseRRule(rrule) {
893
- const parts = Object.fromEntries(
894
- rrule.split(";").map((part) => part.trim()).filter(Boolean).map((part) => {
895
- const [key, value] = part.split("=");
896
- return [key, value];
897
- })
898
- );
899
- const freq = parts.FREQ;
900
- if (freq !== "DAILY" && freq !== "WEEKLY" && freq !== "MONTHLY" && freq !== "YEARLY") {
901
- throw new Error(`Unsupported RRULE FREQ: ${freq ?? "missing"}`);
902
- }
903
- const interval = parts.INTERVAL == null ? 1 : Number(parts.INTERVAL);
904
- if (!Number.isFinite(interval) || interval < 1) {
905
- throw new Error("RRULE INTERVAL must be a positive integer.");
906
- }
907
- const count = parts.COUNT == null ? null : Number(parts.COUNT);
908
- if (count != null && (!Number.isFinite(count) || count < 1)) {
909
- throw new Error("RRULE COUNT must be a positive integer.");
910
- }
911
- const until = parts.UNTIL == null ? null : Date.parse(parts.UNTIL);
912
- if (parts.UNTIL != null && Number.isNaN(until)) {
913
- throw new Error("RRULE UNTIL must be parseable as a date.");
914
- }
915
- const byDay = parts.BYDAY == null ? null : parts.BYDAY.split(",").map((token) => {
916
- const value = WEEKDAY[token];
917
- if (value == null) {
918
- throw new Error(`Unsupported RRULE BYDAY token: ${token}`);
919
- }
920
- return value;
921
- });
922
- return { freq, interval, count, until, byDay };
923
- }
924
-
925
- // src/repeatable_quests/start_of_utc_day.ts
926
- function startOfUtcDay(timestamp) {
927
- const date = new Date(timestamp);
928
- return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
929
- }
930
-
931
- // src/repeatable_quests/start_of_utc_week.ts
932
- var DAY_MS = 864e5;
933
- function startOfUtcWeek(dayStart) {
934
- const date = new Date(dayStart);
935
- return dayStart - date.getUTCDay() * DAY_MS;
936
- }
937
-
938
- // src/repeatable_quests/generate_anchors_until.ts
939
- var DAY_MS2 = 864e5;
940
- var WEEK_MS2 = 7 * DAY_MS2;
941
- function generateAnchorsUntil(repeatable, now) {
942
- const rule = parseRRule(repeatable.rrule);
943
- const anchors = [];
944
- const until = rule.until == null ? now : Math.min(now, rule.until);
945
- if (repeatable.anchorAt > until) {
946
- return anchors;
947
- }
948
- if (rule.freq === "DAILY") {
949
- let occurrence2 = 0;
950
- for (let current2 = repeatable.anchorAt; current2 <= until; current2 += rule.interval * DAY_MS2) {
951
- occurrence2 += 1;
952
- if (rule.count != null && occurrence2 > rule.count) {
953
- break;
954
- }
955
- anchors.push(current2);
956
- }
957
- return anchors;
958
- }
959
- if (rule.freq === "WEEKLY" && rule.byDay && rule.byDay.length > 0) {
960
- const anchorDayStart = startOfUtcDay(repeatable.anchorAt);
961
- const anchorWeekStart = startOfUtcWeek(anchorDayStart);
962
- const timeOffset = repeatable.anchorAt - anchorDayStart;
963
- let occurrence2 = 0;
964
- for (let day = anchorDayStart; day <= until; day += DAY_MS2) {
965
- const weeksSince = Math.floor((startOfUtcWeek(day) - anchorWeekStart) / WEEK_MS2);
966
- if (weeksSince < 0 || weeksSince % rule.interval !== 0) {
967
- continue;
968
- }
969
- if (!rule.byDay.includes(new Date(day).getUTCDay())) {
970
- continue;
971
- }
972
- const anchor = day + timeOffset;
973
- if (anchor < repeatable.anchorAt || anchor > until) {
974
- continue;
975
- }
976
- occurrence2 += 1;
977
- if (rule.count != null && occurrence2 > rule.count) {
978
- break;
979
- }
980
- anchors.push(anchor);
981
- }
982
- return anchors;
983
- }
984
- let occurrence = 0;
985
- let current = repeatable.anchorAt;
986
- while (current <= until) {
987
- occurrence += 1;
988
- if (rule.count != null && occurrence > rule.count) {
989
- break;
990
- }
991
- anchors.push(current);
992
- current = advanceAnchor(current, rule.freq, rule.interval);
993
- }
994
- return anchors;
995
- }
996
-
997
- // src/repeatable_quests/list_due_repeatable_quest_anchors.ts
998
- function listDueRepeatableQuestAnchors(db, now) {
999
- const repeatables = db.prepare(
1000
- `SELECT *
1001
- FROM repeatable_quests
1002
- WHERE deleted_at IS NULL
1003
- AND archived_at IS NULL`
1004
- ).all().map(mapRepeatableQuestRow);
1005
- const anchors = [];
1006
- if (repeatables.length === 0) {
1007
- return anchors;
1008
- }
1009
- const repeatableIds = repeatables.map((repeatable) => repeatable.id);
1010
- const maxAnchorAt = Math.max(
1011
- ...repeatables.map((repeatable) => Math.max(repeatable.anchorAt, now))
1012
- );
1013
- const existingRows = db.prepare(
1014
- `SELECT spawned_from_repeatable_id, spawned_for_at
1015
- FROM quests
1016
- WHERE spawned_from_repeatable_id IN (${repeatableIds.map(() => "?").join(", ")})
1017
- AND spawned_for_at IS NOT NULL
1018
- AND spawned_for_at <= ?`
1019
- ).all(...repeatableIds, maxAnchorAt);
1020
- const existingAnchors = new Set(
1021
- existingRows.map(
1022
- (row) => `${Number(row.spawned_from_repeatable_id)}:${Number(row.spawned_for_at)}`
1023
- )
1024
- );
1025
- for (const repeatable of repeatables) {
1026
- const dueAnchors = generateAnchorsUntil(repeatable, now);
1027
- for (const anchorAt of dueAnchors) {
1028
- if (!existingAnchors.has(`${repeatable.id}:${anchorAt}`)) {
1029
- anchors.push({
1030
- repeatableQuestId: repeatable.id,
1031
- anchorAt,
1032
- markerId: "attention.available.repeatable"
1033
- });
1034
- }
1035
- }
1036
- }
1037
- return anchors.sort(
1038
- (a, b) => a.anchorAt - b.anchorAt || a.repeatableQuestId - b.repeatableQuestId
1039
- );
1040
- }
1041
-
1042
546
  // src/markers/derive_rumor_marker_id.ts
1043
547
  function deriveRumorMarkerId({ state }) {
1044
548
  if (state === "open") {
@@ -1136,6 +640,7 @@ function validateInput(schema, input, path = "$") {
1136
640
 
1137
641
  // src/tools/action_requirements.ts
1138
642
  var actionRequirements = {
643
+ manage_hooks: { remove: ["id"] },
1139
644
  shape_work: {
1140
645
  create_quest: ["quest"],
1141
646
  create_questline: ["questline"],
@@ -1448,7 +953,7 @@ function validateReviewInput(input) {
1448
953
  'Use `filters.dayAt` only with `view: "quests.scheduled_for_day"`.'
1449
954
  );
1450
955
  }
1451
- if ((input.view === "rumors" || input.view === "questlines") && hasQuestFilters(input.filters)) {
956
+ if ((input.view === "rumors" || input.view === "questlines" || input.view === "deliveries") && hasQuestFilters(input.filters)) {
1452
957
  return toolFailure(
1453
958
  "protocol",
1454
959
  "invalid_input",
@@ -1516,6 +1021,15 @@ function reviewQuestlogToolHandler(db, input) {
1516
1021
  if (invalid) {
1517
1022
  return invalid;
1518
1023
  }
1024
+ if (input.view === "deliveries")
1025
+ return toolSuccess("Pending and failed deliveries.", {
1026
+ view: input.view,
1027
+ items: [],
1028
+ deliveries: listDeliveries(db),
1029
+ count: listDeliveries(db).length,
1030
+ appliedFilters: input.filters,
1031
+ evaluatedAt: input.now ?? Date.now()
1032
+ });
1519
1033
  const now = input.now ?? Date.now();
1520
1034
  let items = [];
1521
1035
  switch (input.view) {
@@ -1575,6 +1089,7 @@ var reviewQuestlogTool = defineQuestlogTool({
1575
1089
  inputSchema: objectSchema(
1576
1090
  {
1577
1091
  view: enumSchema("Which operational review surface to return.", [
1092
+ "deliveries",
1578
1093
  "questlines",
1579
1094
  "quests.active",
1580
1095
  "quests.available",
@@ -1933,7 +1448,8 @@ function inspectQuestlogItemToolHandler(db, input) {
1933
1448
  detail: detailLevel === "full" ? detail : toQuestCompactDetail(detail)
1934
1449
  },
1935
1450
  related,
1936
- detailLevel
1451
+ detailLevel,
1452
+ deliveries: listDeliveries(db, { questId: detail.id })
1937
1453
  },
1938
1454
  { entities }
1939
1455
  );
@@ -2039,6 +1555,56 @@ var inspectQuestlogItemTool = defineQuestlogTool({
2039
1555
  handler: inspectQuestlogItemToolHandler
2040
1556
  });
2041
1557
 
1558
+ // src/tools/manage_hooks_tool.ts
1559
+ var manageHooksTool = defineQuestlogTool({
1560
+ name: "manage_hooks",
1561
+ description: "List or remove outbound webhook targets, run a tick, or inspect pending and failed deliveries.",
1562
+ whenToUse: "Use to operate configured outbound webhooks. Add and test targets with the CLI.",
1563
+ whenNotToUse: "Use quest tools to change work itself.",
1564
+ sideEffects: "writes_state",
1565
+ readOnly: false,
1566
+ supportsClarification: false,
1567
+ targetKinds: ["hook"],
1568
+ inputDescriptions: {
1569
+ action: "Operation to perform.",
1570
+ id: "Target id for removal.",
1571
+ dry_run: "Preview a tick without recording or sending."
1572
+ },
1573
+ outputDescription: "Returns redacted targets, delivery metadata, or a tick summary. Never returns signing secrets.",
1574
+ inputSchema: objectSchema(
1575
+ {
1576
+ action: enumSchema("Operation.", ["list", "remove", "tick", "deliveries"]),
1577
+ id: integerSchema("Target id."),
1578
+ dry_run: booleanSchema("Preview only.")
1579
+ },
1580
+ ["action"]
1581
+ ),
1582
+ handler: async (db, input) => {
1583
+ switch (input.action) {
1584
+ case "list":
1585
+ return toolSuccess("Webhook targets.", listHooks(db));
1586
+ case "deliveries":
1587
+ return toolSuccess("Pending and failed deliveries.", listDeliveries(db));
1588
+ case "remove":
1589
+ if (!Number.isSafeInteger(input.id) || Number(input.id) < 1)
1590
+ return toolFailure(
1591
+ "protocol",
1592
+ "invalid_input",
1593
+ "Target id required.",
1594
+ "Provide a positive target id.",
1595
+ { recovery: 'Pass {"action":"remove","id":1}' }
1596
+ );
1597
+ removeHook(db, Number(input.id));
1598
+ return toolSuccess("Webhook target removed.", {});
1599
+ case "tick":
1600
+ return toolSuccess(
1601
+ "Webhook tick completed.",
1602
+ await tickQuestlog(db, { dryRun: input.dry_run ?? false })
1603
+ );
1604
+ }
1605
+ }
1606
+ });
1607
+
2042
1608
  // src/repeatable_quests/update_repeatable_quest.ts
2043
1609
  function updateRepeatableQuest(db, repeatableQuestId, input) {
2044
1610
  return withTransaction(db, () => {
@@ -2208,95 +1774,6 @@ function softDeleteRepeatableQuest(db, repeatableQuestId, deletedAt) {
2208
1774
  ).run(now, now, repeatableQuestId);
2209
1775
  }
2210
1776
 
2211
- // src/rewards/copy_repeatable_quest_rewards_to_quest.ts
2212
- function copyRepeatableQuestRewardsToQuest(db, repeatableQuestId, questId, now) {
2213
- const effectiveNow = resolveNow(now);
2214
- const rows = db.prepare(
2215
- `SELECT kind, name, description, quantity
2216
- FROM repeatable_quest_rewards
2217
- WHERE repeatable_quest_id = ? AND deleted_at IS NULL`
2218
- ).all(repeatableQuestId);
2219
- for (const row of rows) {
2220
- db.prepare(
2221
- `INSERT INTO quest_rewards (quest_id, kind, name, description, quantity, created_at)
2222
- VALUES (?, ?, ?, ?, ?, ?)`
2223
- ).run(
2224
- questId,
2225
- String(row.kind),
2226
- String(row.name),
2227
- row.description == null ? null : String(row.description),
2228
- row.quantity == null ? null : Number(row.quantity),
2229
- effectiveNow
2230
- );
2231
- }
2232
- }
2233
-
2234
- // src/tags/copy_repeatable_quest_tags_to_quest.ts
2235
- function copyRepeatableQuestTagsToQuest(db, repeatableQuestId, questId, now) {
2236
- const effectiveNow = resolveNow(now);
2237
- const rows = db.prepare(
2238
- `SELECT tag_id
2239
- FROM repeatable_quest_tags
2240
- WHERE repeatable_quest_id = ? AND deleted_at IS NULL`
2241
- ).all(repeatableQuestId);
2242
- for (const row of rows) {
2243
- db.prepare(
2244
- `INSERT INTO quest_tags (quest_id, tag_id, created_at)
2245
- VALUES (?, ?, ?)`
2246
- ).run(questId, Number(row.tag_id), effectiveNow);
2247
- }
2248
- }
2249
-
2250
- // src/repeatable_quests/spawn_due_repeatable_quests.ts
2251
- function spawnDueRepeatableQuests(db, now) {
2252
- return withTransaction(db, () => {
2253
- const anchors = listDueRepeatableQuestAnchors(db, now);
2254
- const quests = [];
2255
- for (const anchor of anchors) {
2256
- const repeatable = getRepeatableQuestOrThrow(db, anchor.repeatableQuestId);
2257
- const created = db.prepare(
2258
- `INSERT INTO quests (
2259
- questline_id,
2260
- source_rumor_id,
2261
- spawned_by_quest_id,
2262
- spawned_from_repeatable_id,
2263
- spawned_for_at,
2264
- title,
2265
- objective,
2266
- created_at,
2267
- updated_at,
2268
- effort_seconds,
2269
- estimate_seconds,
2270
- not_before_at,
2271
- due_at,
2272
- scheduled_start_at,
2273
- scheduled_end_at,
2274
- all_day
2275
- ) VALUES (?, NULL, NULL, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?)`
2276
- ).run(
2277
- repeatable.questlineId ?? null,
2278
- repeatable.id,
2279
- anchor.anchorAt,
2280
- repeatable.title,
2281
- repeatable.objective,
2282
- now,
2283
- now,
2284
- repeatable.estimateSeconds ?? null,
2285
- repeatable.notBeforeOffsetSeconds == null ? null : anchor.anchorAt + repeatable.notBeforeOffsetSeconds * 1e3,
2286
- repeatable.dueOffsetSeconds == null ? null : anchor.anchorAt + repeatable.dueOffsetSeconds * 1e3,
2287
- repeatable.scheduledStartOffsetSeconds == null ? null : anchor.anchorAt + repeatable.scheduledStartOffsetSeconds * 1e3,
2288
- repeatable.scheduledEndOffsetSeconds == null ? null : anchor.anchorAt + repeatable.scheduledEndOffsetSeconds * 1e3,
2289
- repeatable.allDay ? 1 : 0
2290
- );
2291
- const questId = Number(created.lastInsertRowid);
2292
- copyRepeatableQuestTagsToQuest(db, repeatable.id, questId, now);
2293
- copyRepeatableQuestRewardsToQuest(db, repeatable.id, questId, now);
2294
- quests.push(getQuestOrThrow(db, questId));
2295
- }
2296
- return quests;
2297
- });
2298
- }
2299
-
2300
1777
  // src/tools/manage_repeatable_tool.ts
2301
1778
  function manageRepeatableToolHandler(db, input) {
2302
1779
  try {
@@ -4379,6 +3856,7 @@ var tagWorkTool = defineQuestlogTool({
4379
3856
 
4380
3857
  // src/tools/tool_registry.ts
4381
3858
  var questlogTools = [
3859
+ manageHooksTool,
4382
3860
  searchQuestlogTool,
4383
3861
  reviewQuestlogTool,
4384
3862
  inspectQuestlogItemTool,