@ghostpaw/questlog 0.9.2 → 0.11.0
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 +144 -4
- package/SKILL.md +95 -40
- package/dist/chunk-7KTRNXTU.js +21 -0
- package/dist/chunk-D6EGZCT7.js +23 -0
- package/dist/chunk-GVD6A3RR.js +108 -0
- package/dist/{chunk-6KTQK3ZE.js → chunk-I2AMIXGP.js} +8 -0
- package/dist/{chunk-3B6FMHZU.js → chunk-IA2GVCZS.js} +35 -2
- package/dist/{chunk-DKH623HU.js → chunk-IL2QRKZL.js} +1 -4
- package/dist/{chunk-UTUQPSGO.js → chunk-KOWIVBG6.js} +68 -588
- package/dist/chunk-NBCCA6AX.js +31 -0
- package/dist/chunk-QK3H2AL5.js +36 -0
- package/dist/chunk-RBGQK5WP.js +949 -0
- package/dist/cli.js +1 -1
- package/dist/{cli_main-XNZP4TY2.js → cli_main-TEG3QTDP.js} +147 -128
- package/dist/index.cjs +1152 -627
- package/dist/index.d.cts +142 -11
- package/dist/index.d.ts +142 -11
- package/dist/index.js +1152 -628
- package/dist/list_deliveries-WJEJUZIQ.js +2 -0
- package/dist/list_hooks-C5CAA2C7.js +2 -0
- package/dist/mcp-server.js +840 -389
- package/dist/mcp.cjs +709 -322
- package/dist/mcp.js +711 -324
- package/dist/open_questlog_db-6NS6BVBC.js +2 -0
- package/dist/{questlog_skill_spec-EM6DGGVN.js → questlog_skill_spec-ILTCTRYS.js} +16 -8
- package/dist/run_hook_command-BOENYNDT.js +155 -0
- package/dist/{serve_questlog_mcp-KFUYJB2P.js → serve_questlog_mcp-X524X34F.js} +6 -3
- package/dist/{skill_markdown-HXYQ3K6U.js → skill_markdown-EGKYZ3W2.js} +4 -20
- package/dist/text_output-X7F3K23Q.js +2 -0
- package/package.json +1 -1
- package/skills/questlog/SKILL.md +95 -40
- package/skills/questlog/references/webhooks.md +94 -0
- package/dist/open_questlog_db-SH3DAU4C.js +0 -2
|
@@ -1,4 +1,7 @@
|
|
|
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';
|
|
3
|
+
import { listDeliveries } from './chunk-D6EGZCT7.js';
|
|
4
|
+
import { listHooks } from './chunk-NBCCA6AX.js';
|
|
2
5
|
import { registerSchemaActions, minimalInput, requiredForAction } from './chunk-QYSIJ2YN.js';
|
|
3
6
|
|
|
4
7
|
// src/questlines/map_questline_row.ts
|
|
@@ -118,51 +121,6 @@ function assertNonEmpty(value, label) {
|
|
|
118
121
|
return trimmed;
|
|
119
122
|
}
|
|
120
123
|
|
|
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
124
|
// src/quests/resolve_quest.ts
|
|
167
125
|
function resolveQuest(db, questId, success, outcome, resolvedAt) {
|
|
168
126
|
const current = getQuestOrThrow(db, questId);
|
|
@@ -235,41 +193,6 @@ function addUnlock(db, fromQuestId, toQuestId, now) {
|
|
|
235
193
|
).run(fromQuestId, toQuestId, resolveNow(now));
|
|
236
194
|
}
|
|
237
195
|
|
|
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
196
|
// src/assert_non_negative_seconds.ts
|
|
274
197
|
function assertNonNegativeSeconds(value, label) {
|
|
275
198
|
if (value != null && value < 0) {
|
|
@@ -307,11 +230,6 @@ function replaceActiveQuestRewards(db, questId, rewards, now) {
|
|
|
307
230
|
}
|
|
308
231
|
}
|
|
309
232
|
|
|
310
|
-
// src/tags/normalize_tag_name.ts
|
|
311
|
-
function normalizeTagName(name) {
|
|
312
|
-
return name.trim().toLowerCase().replace(/\s+/g, " ");
|
|
313
|
-
}
|
|
314
|
-
|
|
315
233
|
// src/tags/replace_active_quest_tags.ts
|
|
316
234
|
function getOrCreateTagId(db, name, now) {
|
|
317
235
|
const normalizedName = normalizeTagName(name);
|
|
@@ -444,209 +362,6 @@ function finishQuest(db, questId, outcome, resolvedAt) {
|
|
|
444
362
|
return resolveQuest(db, questId, true, outcome, resolvedAt);
|
|
445
363
|
}
|
|
446
364
|
|
|
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
365
|
// src/quests/get_quest_detail.ts
|
|
651
366
|
function getQuestDetail(db, questId, now = Date.now()) {
|
|
652
367
|
const [record] = queryQuestDetailRecords(db, {}, now, [questId]);
|
|
@@ -830,215 +545,6 @@ function startQuest(db, questId, startedAt) {
|
|
|
830
545
|
return getQuestOrThrow(db, questId);
|
|
831
546
|
}
|
|
832
547
|
|
|
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
548
|
// src/markers/derive_rumor_marker_id.ts
|
|
1043
549
|
function deriveRumorMarkerId({ state }) {
|
|
1044
550
|
if (state === "open") {
|
|
@@ -1136,6 +642,7 @@ function validateInput(schema, input, path = "$") {
|
|
|
1136
642
|
|
|
1137
643
|
// src/tools/action_requirements.ts
|
|
1138
644
|
var actionRequirements = {
|
|
645
|
+
manage_hooks: { remove: ["id"] },
|
|
1139
646
|
shape_work: {
|
|
1140
647
|
create_quest: ["quest"],
|
|
1141
648
|
create_questline: ["questline"],
|
|
@@ -1448,7 +955,7 @@ function validateReviewInput(input) {
|
|
|
1448
955
|
'Use `filters.dayAt` only with `view: "quests.scheduled_for_day"`.'
|
|
1449
956
|
);
|
|
1450
957
|
}
|
|
1451
|
-
if ((input.view === "rumors" || input.view === "questlines") && hasQuestFilters(input.filters)) {
|
|
958
|
+
if ((input.view === "rumors" || input.view === "questlines" || input.view === "deliveries") && hasQuestFilters(input.filters)) {
|
|
1452
959
|
return toolFailure(
|
|
1453
960
|
"protocol",
|
|
1454
961
|
"invalid_input",
|
|
@@ -1516,6 +1023,15 @@ function reviewQuestlogToolHandler(db, input) {
|
|
|
1516
1023
|
if (invalid) {
|
|
1517
1024
|
return invalid;
|
|
1518
1025
|
}
|
|
1026
|
+
if (input.view === "deliveries")
|
|
1027
|
+
return toolSuccess("Pending and failed deliveries.", {
|
|
1028
|
+
view: input.view,
|
|
1029
|
+
items: [],
|
|
1030
|
+
deliveries: listDeliveries(db),
|
|
1031
|
+
count: listDeliveries(db).length,
|
|
1032
|
+
appliedFilters: input.filters,
|
|
1033
|
+
evaluatedAt: input.now ?? Date.now()
|
|
1034
|
+
});
|
|
1519
1035
|
const now = input.now ?? Date.now();
|
|
1520
1036
|
let items = [];
|
|
1521
1037
|
switch (input.view) {
|
|
@@ -1575,6 +1091,7 @@ var reviewQuestlogTool = defineQuestlogTool({
|
|
|
1575
1091
|
inputSchema: objectSchema(
|
|
1576
1092
|
{
|
|
1577
1093
|
view: enumSchema("Which operational review surface to return.", [
|
|
1094
|
+
"deliveries",
|
|
1578
1095
|
"questlines",
|
|
1579
1096
|
"quests.active",
|
|
1580
1097
|
"quests.available",
|
|
@@ -1933,7 +1450,8 @@ function inspectQuestlogItemToolHandler(db, input) {
|
|
|
1933
1450
|
detail: detailLevel === "full" ? detail : toQuestCompactDetail(detail)
|
|
1934
1451
|
},
|
|
1935
1452
|
related,
|
|
1936
|
-
detailLevel
|
|
1453
|
+
detailLevel,
|
|
1454
|
+
deliveries: listDeliveries(db, { questId: detail.id })
|
|
1937
1455
|
},
|
|
1938
1456
|
{ entities }
|
|
1939
1457
|
);
|
|
@@ -2039,6 +1557,56 @@ var inspectQuestlogItemTool = defineQuestlogTool({
|
|
|
2039
1557
|
handler: inspectQuestlogItemToolHandler
|
|
2040
1558
|
});
|
|
2041
1559
|
|
|
1560
|
+
// src/tools/manage_hooks_tool.ts
|
|
1561
|
+
var manageHooksTool = defineQuestlogTool({
|
|
1562
|
+
name: "manage_hooks",
|
|
1563
|
+
description: "List or remove outbound webhook targets, run a tick, or inspect pending and failed deliveries.",
|
|
1564
|
+
whenToUse: "Use to operate configured outbound webhooks. Add and test targets with the CLI.",
|
|
1565
|
+
whenNotToUse: "Use quest tools to change work itself.",
|
|
1566
|
+
sideEffects: "writes_state",
|
|
1567
|
+
readOnly: false,
|
|
1568
|
+
supportsClarification: false,
|
|
1569
|
+
targetKinds: ["hook"],
|
|
1570
|
+
inputDescriptions: {
|
|
1571
|
+
action: "Operation to perform.",
|
|
1572
|
+
id: "Target id for removal.",
|
|
1573
|
+
dry_run: "Preview a tick without recording or sending."
|
|
1574
|
+
},
|
|
1575
|
+
outputDescription: "Returns redacted targets, delivery metadata, or a tick summary. Never returns signing secrets.",
|
|
1576
|
+
inputSchema: objectSchema(
|
|
1577
|
+
{
|
|
1578
|
+
action: enumSchema("Operation.", ["list", "remove", "tick", "deliveries"]),
|
|
1579
|
+
id: integerSchema("Target id."),
|
|
1580
|
+
dry_run: booleanSchema("Preview only.")
|
|
1581
|
+
},
|
|
1582
|
+
["action"]
|
|
1583
|
+
),
|
|
1584
|
+
handler: async (db, input) => {
|
|
1585
|
+
switch (input.action) {
|
|
1586
|
+
case "list":
|
|
1587
|
+
return toolSuccess("Webhook targets.", listHooks(db));
|
|
1588
|
+
case "deliveries":
|
|
1589
|
+
return toolSuccess("Pending and failed deliveries.", listDeliveries(db));
|
|
1590
|
+
case "remove":
|
|
1591
|
+
if (!Number.isSafeInteger(input.id) || Number(input.id) < 1)
|
|
1592
|
+
return toolFailure(
|
|
1593
|
+
"protocol",
|
|
1594
|
+
"invalid_input",
|
|
1595
|
+
"Target id required.",
|
|
1596
|
+
"Provide a positive target id.",
|
|
1597
|
+
{ recovery: 'Pass {"action":"remove","id":1}' }
|
|
1598
|
+
);
|
|
1599
|
+
removeHook(db, Number(input.id));
|
|
1600
|
+
return toolSuccess("Webhook target removed.", {});
|
|
1601
|
+
case "tick":
|
|
1602
|
+
return toolSuccess(
|
|
1603
|
+
"Webhook tick completed.",
|
|
1604
|
+
await tickQuestlog(db, { dryRun: input.dry_run ?? false })
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
});
|
|
1609
|
+
|
|
2042
1610
|
// src/repeatable_quests/update_repeatable_quest.ts
|
|
2043
1611
|
function updateRepeatableQuest(db, repeatableQuestId, input) {
|
|
2044
1612
|
return withTransaction(db, () => {
|
|
@@ -2208,95 +1776,6 @@ function softDeleteRepeatableQuest(db, repeatableQuestId, deletedAt) {
|
|
|
2208
1776
|
).run(now, now, repeatableQuestId);
|
|
2209
1777
|
}
|
|
2210
1778
|
|
|
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
1779
|
// src/tools/manage_repeatable_tool.ts
|
|
2301
1780
|
function manageRepeatableToolHandler(db, input) {
|
|
2302
1781
|
try {
|
|
@@ -4379,6 +3858,7 @@ var tagWorkTool = defineQuestlogTool({
|
|
|
4379
3858
|
|
|
4380
3859
|
// src/tools/tool_registry.ts
|
|
4381
3860
|
var questlogTools = [
|
|
3861
|
+
manageHooksTool,
|
|
4382
3862
|
searchQuestlogTool,
|
|
4383
3863
|
reviewQuestlogTool,
|
|
4384
3864
|
inspectQuestlogItemTool,
|