@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.
- package/README.md +180 -18
- package/SKILL.md +161 -32
- package/dist/{chunk-3Y5F3SMB.js → chunk-55JAQORX.js} +245 -665
- package/dist/{questlog_skill_spec-LOQ2FNOF.js → chunk-D2W5MYNO.js} +9 -54
- package/dist/chunk-GVD6A3RR.js +108 -0
- package/dist/chunk-IL2QRKZL.js +14 -0
- package/dist/chunk-N7V7VUI5.js +982 -0
- package/dist/chunk-QYSIJ2YN.js +47 -0
- package/dist/{chunk-3B6FMHZU.js → chunk-ZHTX64KD.js} +24 -0
- package/dist/cli.js +1 -1
- package/dist/{cli_main-ELAKW6KW.js → cli_main-7AIEZV75.js} +136 -120
- package/dist/index.cjs +2046 -1393
- package/dist/index.d.cts +142 -11
- package/dist/index.d.ts +142 -11
- package/dist/index.js +2046 -1394
- package/dist/mcp-server.js +959 -386
- package/dist/mcp.cjs +810 -328
- package/dist/mcp.js +811 -329
- package/dist/open_questlog_db-LAEH7UDM.js +2 -0
- package/dist/questlog_skill_spec-HEW2B6EB.js +76 -0
- package/dist/run_hook_command-C42BQC36.js +95 -0
- package/dist/{serve_questlog_mcp-QSHMNNFT.js → serve_questlog_mcp-W3R63HW4.js} +14 -4
- package/dist/{skill_markdown-7HCMSJYR.js → skill_markdown-HXYQ3K6U.js} +34 -6
- package/package.json +6 -3
- package/skills/questlog/SKILL.md +161 -32
- package/skills/questlog/references/webhooks.md +91 -0
- package/dist/chunk-KRQDV4KF.js +0 -58
- package/dist/open_questlog_db-SH3DAU4C.js +0 -2
|
@@ -1,4 +1,7 @@
|
|
|
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';
|
|
3
|
+
import { registerSchemaActions, minimalInput, requiredForAction } from './chunk-QYSIJ2YN.js';
|
|
4
|
+
|
|
2
5
|
// src/questlines/map_questline_row.ts
|
|
3
6
|
function mapQuestlineRow(row) {
|
|
4
7
|
return {
|
|
@@ -116,51 +119,6 @@ function assertNonEmpty(value, label) {
|
|
|
116
119
|
return trimmed;
|
|
117
120
|
}
|
|
118
121
|
|
|
119
|
-
// src/resolve_now.ts
|
|
120
|
-
function resolveNow(now) {
|
|
121
|
-
return now ?? Date.now();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// src/quests/map_quest_row.ts
|
|
125
|
-
function asBoolean(value) {
|
|
126
|
-
return Number(value) === 1;
|
|
127
|
-
}
|
|
128
|
-
function mapQuestRow(row) {
|
|
129
|
-
return {
|
|
130
|
-
id: Number(row.id),
|
|
131
|
-
questlineId: row.questline_id == null ? null : Number(row.questline_id),
|
|
132
|
-
sourceRumorId: row.source_rumor_id == null ? null : Number(row.source_rumor_id),
|
|
133
|
-
spawnedByQuestId: row.spawned_by_quest_id == null ? null : Number(row.spawned_by_quest_id),
|
|
134
|
-
spawnedFromRepeatableId: row.spawned_from_repeatable_id == null ? null : Number(row.spawned_from_repeatable_id),
|
|
135
|
-
spawnedForAt: row.spawned_for_at == null ? null : Number(row.spawned_for_at),
|
|
136
|
-
title: String(row.title),
|
|
137
|
-
objective: String(row.objective),
|
|
138
|
-
outcome: row.outcome == null ? null : String(row.outcome),
|
|
139
|
-
createdAt: Number(row.created_at),
|
|
140
|
-
updatedAt: Number(row.updated_at),
|
|
141
|
-
startedAt: row.started_at == null ? null : Number(row.started_at),
|
|
142
|
-
resolvedAt: row.resolved_at == null ? null : Number(row.resolved_at),
|
|
143
|
-
success: row.success == null ? null : asBoolean(row.success),
|
|
144
|
-
effortSeconds: Number(row.effort_seconds),
|
|
145
|
-
estimateSeconds: row.estimate_seconds == null ? null : Number(row.estimate_seconds),
|
|
146
|
-
notBeforeAt: row.not_before_at == null ? null : Number(row.not_before_at),
|
|
147
|
-
dueAt: row.due_at == null ? null : Number(row.due_at),
|
|
148
|
-
scheduledStartAt: row.scheduled_start_at == null ? null : Number(row.scheduled_start_at),
|
|
149
|
-
scheduledEndAt: row.scheduled_end_at == null ? null : Number(row.scheduled_end_at),
|
|
150
|
-
allDay: asBoolean(row.all_day),
|
|
151
|
-
deletedAt: row.deleted_at == null ? null : Number(row.deleted_at)
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// src/quests/get_quest_or_throw.ts
|
|
156
|
-
function getQuestOrThrow(db, questId) {
|
|
157
|
-
const row = db.prepare("SELECT * FROM quests WHERE id = ? AND deleted_at IS NULL").get(questId);
|
|
158
|
-
if (!row) {
|
|
159
|
-
throw new Error(`Quest ${questId} was not found.`);
|
|
160
|
-
}
|
|
161
|
-
return mapQuestRow(row);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
122
|
// src/quests/resolve_quest.ts
|
|
165
123
|
function resolveQuest(db, questId, success, outcome, resolvedAt) {
|
|
166
124
|
const current = getQuestOrThrow(db, questId);
|
|
@@ -233,41 +191,6 @@ function addUnlock(db, fromQuestId, toQuestId, now) {
|
|
|
233
191
|
).run(fromQuestId, toQuestId, resolveNow(now));
|
|
234
192
|
}
|
|
235
193
|
|
|
236
|
-
// src/with_transaction.ts
|
|
237
|
-
var transactionDepth = /* @__PURE__ */ new WeakMap();
|
|
238
|
-
function withTransaction(db, fn) {
|
|
239
|
-
const depth = transactionDepth.get(db) ?? 0;
|
|
240
|
-
const savepoint = `questlog_${depth}`;
|
|
241
|
-
if (depth === 0) {
|
|
242
|
-
db.exec("BEGIN IMMEDIATE");
|
|
243
|
-
} else {
|
|
244
|
-
db.exec(`SAVEPOINT ${savepoint}`);
|
|
245
|
-
}
|
|
246
|
-
transactionDepth.set(db, depth + 1);
|
|
247
|
-
try {
|
|
248
|
-
const result = fn();
|
|
249
|
-
if (depth === 0) {
|
|
250
|
-
db.exec("COMMIT");
|
|
251
|
-
} else {
|
|
252
|
-
db.exec(`RELEASE SAVEPOINT ${savepoint}`);
|
|
253
|
-
}
|
|
254
|
-
transactionDepth.set(db, depth);
|
|
255
|
-
return result;
|
|
256
|
-
} catch (error) {
|
|
257
|
-
try {
|
|
258
|
-
if (depth === 0) {
|
|
259
|
-
db.exec("ROLLBACK");
|
|
260
|
-
} else {
|
|
261
|
-
db.exec(`ROLLBACK TO SAVEPOINT ${savepoint}`);
|
|
262
|
-
db.exec(`RELEASE SAVEPOINT ${savepoint}`);
|
|
263
|
-
}
|
|
264
|
-
} catch {
|
|
265
|
-
}
|
|
266
|
-
transactionDepth.set(db, depth);
|
|
267
|
-
throw error;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
194
|
// src/assert_non_negative_seconds.ts
|
|
272
195
|
function assertNonNegativeSeconds(value, label) {
|
|
273
196
|
if (value != null && value < 0) {
|
|
@@ -305,11 +228,6 @@ function replaceActiveQuestRewards(db, questId, rewards, now) {
|
|
|
305
228
|
}
|
|
306
229
|
}
|
|
307
230
|
|
|
308
|
-
// src/tags/normalize_tag_name.ts
|
|
309
|
-
function normalizeTagName(name) {
|
|
310
|
-
return name.trim().toLowerCase().replace(/\s+/g, " ");
|
|
311
|
-
}
|
|
312
|
-
|
|
313
231
|
// src/tags/replace_active_quest_tags.ts
|
|
314
232
|
function getOrCreateTagId(db, name, now) {
|
|
315
233
|
const normalizedName = normalizeTagName(name);
|
|
@@ -442,209 +360,6 @@ function finishQuest(db, questId, outcome, resolvedAt) {
|
|
|
442
360
|
return resolveQuest(db, questId, true, outcome, resolvedAt);
|
|
443
361
|
}
|
|
444
362
|
|
|
445
|
-
// src/markers/derive_quest_marker_id.ts
|
|
446
|
-
function deriveQuestMarkerId({
|
|
447
|
-
state,
|
|
448
|
-
available,
|
|
449
|
-
hasUnclaimedReward,
|
|
450
|
-
notBeforeAt,
|
|
451
|
-
blockerCount,
|
|
452
|
-
now
|
|
453
|
-
}) {
|
|
454
|
-
if (state === "done") {
|
|
455
|
-
return hasUnclaimedReward ? "progress.complete" : null;
|
|
456
|
-
}
|
|
457
|
-
if (state === "abandoned") {
|
|
458
|
-
return null;
|
|
459
|
-
}
|
|
460
|
-
if (state === "in_progress") {
|
|
461
|
-
return "progress.incomplete";
|
|
462
|
-
}
|
|
463
|
-
if (available) {
|
|
464
|
-
return "attention.available";
|
|
465
|
-
}
|
|
466
|
-
if (notBeforeAt != null && notBeforeAt > now) {
|
|
467
|
-
return "attention.available.future";
|
|
468
|
-
}
|
|
469
|
-
if (blockerCount > 0) {
|
|
470
|
-
return "progress.incomplete";
|
|
471
|
-
}
|
|
472
|
-
return null;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
// src/rewards/map_quest_reward_row.ts
|
|
476
|
-
function mapQuestRewardRow(row) {
|
|
477
|
-
return {
|
|
478
|
-
id: Number(row.id),
|
|
479
|
-
questId: Number(row.quest_id),
|
|
480
|
-
kind: String(row.kind),
|
|
481
|
-
name: String(row.name),
|
|
482
|
-
description: row.description == null ? null : String(row.description),
|
|
483
|
-
quantity: row.quantity == null ? null : Number(row.quantity),
|
|
484
|
-
createdAt: Number(row.created_at),
|
|
485
|
-
claimedAt: row.claimed_at == null ? null : Number(row.claimed_at),
|
|
486
|
-
deletedAt: row.deleted_at == null ? null : Number(row.deleted_at)
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// src/quests/derive_quest_state.ts
|
|
491
|
-
function deriveQuestState(quest) {
|
|
492
|
-
if (quest.resolvedAt != null && quest.success === true) {
|
|
493
|
-
return "done";
|
|
494
|
-
}
|
|
495
|
-
if (quest.resolvedAt != null && quest.success === false) {
|
|
496
|
-
return "abandoned";
|
|
497
|
-
}
|
|
498
|
-
if (quest.startedAt != null) {
|
|
499
|
-
return "in_progress";
|
|
500
|
-
}
|
|
501
|
-
return "open";
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// src/quests/query_quest_detail_records.ts
|
|
505
|
-
function queryQuestDetailRecords(db, filters = {}, now = Date.now(), questIds) {
|
|
506
|
-
if (questIds && questIds.length === 0) {
|
|
507
|
-
return [];
|
|
508
|
-
}
|
|
509
|
-
const clauses = [];
|
|
510
|
-
const params = [];
|
|
511
|
-
if (!filters.includeDeleted) {
|
|
512
|
-
clauses.push("q.deleted_at IS NULL");
|
|
513
|
-
}
|
|
514
|
-
if (questIds && questIds.length > 0) {
|
|
515
|
-
clauses.push(`q.id IN (${questIds.map(() => "?").join(", ")})`);
|
|
516
|
-
params.push(...questIds);
|
|
517
|
-
}
|
|
518
|
-
if (filters.questlineId != null) {
|
|
519
|
-
clauses.push("q.questline_id = ?");
|
|
520
|
-
params.push(filters.questlineId);
|
|
521
|
-
}
|
|
522
|
-
if (filters.tagNames && filters.tagNames.length > 0) {
|
|
523
|
-
const normalized = [...new Set(filters.tagNames.map(normalizeTagName).filter(Boolean))];
|
|
524
|
-
for (const tag of normalized) {
|
|
525
|
-
clauses.push(
|
|
526
|
-
`EXISTS (
|
|
527
|
-
SELECT 1
|
|
528
|
-
FROM quest_tags qt
|
|
529
|
-
JOIN tags t ON t.id = qt.tag_id
|
|
530
|
-
WHERE qt.quest_id = q.id
|
|
531
|
-
AND qt.deleted_at IS NULL
|
|
532
|
-
AND t.deleted_at IS NULL
|
|
533
|
-
AND t.normalized_name = ?
|
|
534
|
-
)`
|
|
535
|
-
);
|
|
536
|
-
params.push(tag);
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
540
|
-
const rows = db.prepare(
|
|
541
|
-
`SELECT q.*,
|
|
542
|
-
COALESCE(q.due_at, ql.due_at) AS effective_due_at,
|
|
543
|
-
(
|
|
544
|
-
SELECT COUNT(*)
|
|
545
|
-
FROM quest_unlocks qu
|
|
546
|
-
JOIN quests blocker ON blocker.id = qu.from_quest_id
|
|
547
|
-
WHERE qu.to_quest_id = q.id
|
|
548
|
-
AND qu.deleted_at IS NULL
|
|
549
|
-
AND blocker.deleted_at IS NULL
|
|
550
|
-
AND NOT (blocker.resolved_at IS NOT NULL AND blocker.success = 1)
|
|
551
|
-
) AS blocker_count
|
|
552
|
-
FROM quests q
|
|
553
|
-
LEFT JOIN questlines ql ON ql.id = q.questline_id AND ql.deleted_at IS NULL
|
|
554
|
-
${where}
|
|
555
|
-
ORDER BY q.created_at DESC, q.id DESC`
|
|
556
|
-
).all(...params);
|
|
557
|
-
if (rows.length === 0) {
|
|
558
|
-
return [];
|
|
559
|
-
}
|
|
560
|
-
const ids = rows.map((row) => Number(row.id));
|
|
561
|
-
const placeholders = ids.map(() => "?").join(", ");
|
|
562
|
-
const tagNamesByQuestId = /* @__PURE__ */ new Map();
|
|
563
|
-
const rewardsByQuestId = /* @__PURE__ */ new Map();
|
|
564
|
-
const unlocksByQuestId = /* @__PURE__ */ new Map();
|
|
565
|
-
const unlockedByQuestId = /* @__PURE__ */ new Map();
|
|
566
|
-
for (const row of db.prepare(
|
|
567
|
-
`SELECT qt.quest_id, t.name
|
|
568
|
-
FROM quest_tags qt
|
|
569
|
-
JOIN tags t ON t.id = qt.tag_id
|
|
570
|
-
WHERE qt.quest_id IN (${placeholders})
|
|
571
|
-
AND qt.deleted_at IS NULL
|
|
572
|
-
AND t.deleted_at IS NULL
|
|
573
|
-
ORDER BY qt.quest_id, t.name`
|
|
574
|
-
).all(...ids)) {
|
|
575
|
-
const questId = Number(row.quest_id);
|
|
576
|
-
const tagNames = tagNamesByQuestId.get(questId) ?? [];
|
|
577
|
-
tagNames.push(String(row.name));
|
|
578
|
-
tagNamesByQuestId.set(questId, tagNames);
|
|
579
|
-
}
|
|
580
|
-
for (const row of db.prepare(
|
|
581
|
-
`SELECT *
|
|
582
|
-
FROM quest_rewards
|
|
583
|
-
WHERE quest_id IN (${placeholders})
|
|
584
|
-
AND deleted_at IS NULL
|
|
585
|
-
ORDER BY quest_id, created_at, id`
|
|
586
|
-
).all(...ids)) {
|
|
587
|
-
const reward = mapQuestRewardRow(row);
|
|
588
|
-
const rewards = rewardsByQuestId.get(reward.questId) ?? [];
|
|
589
|
-
rewards.push(reward);
|
|
590
|
-
rewardsByQuestId.set(reward.questId, rewards);
|
|
591
|
-
}
|
|
592
|
-
for (const row of db.prepare(
|
|
593
|
-
`SELECT from_quest_id, to_quest_id
|
|
594
|
-
FROM quest_unlocks
|
|
595
|
-
WHERE from_quest_id IN (${placeholders})
|
|
596
|
-
AND deleted_at IS NULL
|
|
597
|
-
ORDER BY from_quest_id, to_quest_id`
|
|
598
|
-
).all(...ids)) {
|
|
599
|
-
const questId = Number(row.from_quest_id);
|
|
600
|
-
const unlocks = unlocksByQuestId.get(questId) ?? [];
|
|
601
|
-
unlocks.push(Number(row.to_quest_id));
|
|
602
|
-
unlocksByQuestId.set(questId, unlocks);
|
|
603
|
-
}
|
|
604
|
-
for (const row of db.prepare(
|
|
605
|
-
`SELECT from_quest_id, to_quest_id
|
|
606
|
-
FROM quest_unlocks
|
|
607
|
-
WHERE to_quest_id IN (${placeholders})
|
|
608
|
-
AND deleted_at IS NULL
|
|
609
|
-
ORDER BY to_quest_id, from_quest_id`
|
|
610
|
-
).all(...ids)) {
|
|
611
|
-
const questId = Number(row.to_quest_id);
|
|
612
|
-
const unlockedBy = unlockedByQuestId.get(questId) ?? [];
|
|
613
|
-
unlockedBy.push(Number(row.from_quest_id));
|
|
614
|
-
unlockedByQuestId.set(questId, unlockedBy);
|
|
615
|
-
}
|
|
616
|
-
return rows.map((row) => {
|
|
617
|
-
const quest = mapQuestRow(row);
|
|
618
|
-
const blockerCount = Number(row.blocker_count ?? 0);
|
|
619
|
-
const effectiveDueAt = row.effective_due_at == null ? null : Number(row.effective_due_at);
|
|
620
|
-
const rewards = rewardsByQuestId.get(quest.id) ?? [];
|
|
621
|
-
const state = deriveQuestState(quest);
|
|
622
|
-
const available = quest.deletedAt == null && quest.resolvedAt == null && (quest.notBeforeAt == null || quest.notBeforeAt <= now) && blockerCount === 0;
|
|
623
|
-
const markerId = deriveQuestMarkerId({
|
|
624
|
-
state,
|
|
625
|
-
available,
|
|
626
|
-
hasUnclaimedReward: rewards.some((reward) => reward.claimedAt == null),
|
|
627
|
-
notBeforeAt: quest.notBeforeAt,
|
|
628
|
-
blockerCount,
|
|
629
|
-
now
|
|
630
|
-
});
|
|
631
|
-
return {
|
|
632
|
-
detail: {
|
|
633
|
-
...quest,
|
|
634
|
-
state,
|
|
635
|
-
markerId,
|
|
636
|
-
effectiveDueAt,
|
|
637
|
-
available,
|
|
638
|
-
tagNames: tagNamesByQuestId.get(quest.id) ?? [],
|
|
639
|
-
rewards,
|
|
640
|
-
unlocksQuestIds: unlocksByQuestId.get(quest.id) ?? [],
|
|
641
|
-
unlockedByQuestIds: unlockedByQuestId.get(quest.id) ?? []
|
|
642
|
-
},
|
|
643
|
-
blockerCount
|
|
644
|
-
};
|
|
645
|
-
});
|
|
646
|
-
}
|
|
647
|
-
|
|
648
363
|
// src/quests/get_quest_detail.ts
|
|
649
364
|
function getQuestDetail(db, questId, now = Date.now()) {
|
|
650
365
|
const [record] = queryQuestDetailRecords(db, {}, now, [questId]);
|
|
@@ -828,215 +543,6 @@ function startQuest(db, questId, startedAt) {
|
|
|
828
543
|
return getQuestOrThrow(db, questId);
|
|
829
544
|
}
|
|
830
545
|
|
|
831
|
-
// src/repeatable_quests/map_repeatable_quest_row.ts
|
|
832
|
-
function asBoolean2(value) {
|
|
833
|
-
return Number(value) === 1;
|
|
834
|
-
}
|
|
835
|
-
function mapRepeatableQuestRow(row) {
|
|
836
|
-
return {
|
|
837
|
-
id: Number(row.id),
|
|
838
|
-
questlineId: row.questline_id == null ? null : Number(row.questline_id),
|
|
839
|
-
title: String(row.title),
|
|
840
|
-
objective: String(row.objective),
|
|
841
|
-
rrule: String(row.rrule),
|
|
842
|
-
anchorAt: Number(row.anchor_at),
|
|
843
|
-
notBeforeOffsetSeconds: row.not_before_offset_seconds == null ? null : Number(row.not_before_offset_seconds),
|
|
844
|
-
dueOffsetSeconds: row.due_offset_seconds == null ? null : Number(row.due_offset_seconds),
|
|
845
|
-
scheduledStartOffsetSeconds: row.scheduled_start_offset_seconds == null ? null : Number(row.scheduled_start_offset_seconds),
|
|
846
|
-
scheduledEndOffsetSeconds: row.scheduled_end_offset_seconds == null ? null : Number(row.scheduled_end_offset_seconds),
|
|
847
|
-
allDay: asBoolean2(row.all_day),
|
|
848
|
-
estimateSeconds: row.estimate_seconds == null ? null : Number(row.estimate_seconds),
|
|
849
|
-
createdAt: Number(row.created_at),
|
|
850
|
-
updatedAt: Number(row.updated_at),
|
|
851
|
-
archivedAt: row.archived_at == null ? null : Number(row.archived_at),
|
|
852
|
-
deletedAt: row.deleted_at == null ? null : Number(row.deleted_at)
|
|
853
|
-
};
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
// src/repeatable_quests/get_repeatable_quest_or_throw.ts
|
|
857
|
-
function getRepeatableQuestOrThrow(db, repeatableQuestId) {
|
|
858
|
-
const row = db.prepare("SELECT * FROM repeatable_quests WHERE id = ? AND deleted_at IS NULL").get(repeatableQuestId);
|
|
859
|
-
if (!row) {
|
|
860
|
-
throw new Error(`Repeatable quest ${repeatableQuestId} was not found.`);
|
|
861
|
-
}
|
|
862
|
-
return mapRepeatableQuestRow(row);
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
// src/repeatable_quests/advance_anchor.ts
|
|
866
|
-
var WEEK_MS = 7 * 864e5;
|
|
867
|
-
function advanceAnchor(anchorAt, freq, interval) {
|
|
868
|
-
const date = new Date(anchorAt);
|
|
869
|
-
if (freq === "MONTHLY") {
|
|
870
|
-
date.setUTCMonth(date.getUTCMonth() + interval);
|
|
871
|
-
return date.getTime();
|
|
872
|
-
}
|
|
873
|
-
if (freq === "YEARLY") {
|
|
874
|
-
date.setUTCFullYear(date.getUTCFullYear() + interval);
|
|
875
|
-
return date.getTime();
|
|
876
|
-
}
|
|
877
|
-
return anchorAt + interval * WEEK_MS;
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
// src/repeatable_quests/parse_rrule.ts
|
|
881
|
-
var WEEKDAY = {
|
|
882
|
-
SU: 0,
|
|
883
|
-
MO: 1,
|
|
884
|
-
TU: 2,
|
|
885
|
-
WE: 3,
|
|
886
|
-
TH: 4,
|
|
887
|
-
FR: 5,
|
|
888
|
-
SA: 6
|
|
889
|
-
};
|
|
890
|
-
function parseRRule(rrule) {
|
|
891
|
-
const parts = Object.fromEntries(
|
|
892
|
-
rrule.split(";").map((part) => part.trim()).filter(Boolean).map((part) => {
|
|
893
|
-
const [key, value] = part.split("=");
|
|
894
|
-
return [key, value];
|
|
895
|
-
})
|
|
896
|
-
);
|
|
897
|
-
const freq = parts.FREQ;
|
|
898
|
-
if (freq !== "DAILY" && freq !== "WEEKLY" && freq !== "MONTHLY" && freq !== "YEARLY") {
|
|
899
|
-
throw new Error(`Unsupported RRULE FREQ: ${freq ?? "missing"}`);
|
|
900
|
-
}
|
|
901
|
-
const interval = parts.INTERVAL == null ? 1 : Number(parts.INTERVAL);
|
|
902
|
-
if (!Number.isFinite(interval) || interval < 1) {
|
|
903
|
-
throw new Error("RRULE INTERVAL must be a positive integer.");
|
|
904
|
-
}
|
|
905
|
-
const count = parts.COUNT == null ? null : Number(parts.COUNT);
|
|
906
|
-
if (count != null && (!Number.isFinite(count) || count < 1)) {
|
|
907
|
-
throw new Error("RRULE COUNT must be a positive integer.");
|
|
908
|
-
}
|
|
909
|
-
const until = parts.UNTIL == null ? null : Date.parse(parts.UNTIL);
|
|
910
|
-
if (parts.UNTIL != null && Number.isNaN(until)) {
|
|
911
|
-
throw new Error("RRULE UNTIL must be parseable as a date.");
|
|
912
|
-
}
|
|
913
|
-
const byDay = parts.BYDAY == null ? null : parts.BYDAY.split(",").map((token) => {
|
|
914
|
-
const value = WEEKDAY[token];
|
|
915
|
-
if (value == null) {
|
|
916
|
-
throw new Error(`Unsupported RRULE BYDAY token: ${token}`);
|
|
917
|
-
}
|
|
918
|
-
return value;
|
|
919
|
-
});
|
|
920
|
-
return { freq, interval, count, until, byDay };
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
// src/repeatable_quests/start_of_utc_day.ts
|
|
924
|
-
function startOfUtcDay(timestamp) {
|
|
925
|
-
const date = new Date(timestamp);
|
|
926
|
-
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
// src/repeatable_quests/start_of_utc_week.ts
|
|
930
|
-
var DAY_MS = 864e5;
|
|
931
|
-
function startOfUtcWeek(dayStart) {
|
|
932
|
-
const date = new Date(dayStart);
|
|
933
|
-
return dayStart - date.getUTCDay() * DAY_MS;
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
// src/repeatable_quests/generate_anchors_until.ts
|
|
937
|
-
var DAY_MS2 = 864e5;
|
|
938
|
-
var WEEK_MS2 = 7 * DAY_MS2;
|
|
939
|
-
function generateAnchorsUntil(repeatable, now) {
|
|
940
|
-
const rule = parseRRule(repeatable.rrule);
|
|
941
|
-
const anchors = [];
|
|
942
|
-
const until = rule.until == null ? now : Math.min(now, rule.until);
|
|
943
|
-
if (repeatable.anchorAt > until) {
|
|
944
|
-
return anchors;
|
|
945
|
-
}
|
|
946
|
-
if (rule.freq === "DAILY") {
|
|
947
|
-
let occurrence2 = 0;
|
|
948
|
-
for (let current2 = repeatable.anchorAt; current2 <= until; current2 += rule.interval * DAY_MS2) {
|
|
949
|
-
occurrence2 += 1;
|
|
950
|
-
if (rule.count != null && occurrence2 > rule.count) {
|
|
951
|
-
break;
|
|
952
|
-
}
|
|
953
|
-
anchors.push(current2);
|
|
954
|
-
}
|
|
955
|
-
return anchors;
|
|
956
|
-
}
|
|
957
|
-
if (rule.freq === "WEEKLY" && rule.byDay && rule.byDay.length > 0) {
|
|
958
|
-
const anchorDayStart = startOfUtcDay(repeatable.anchorAt);
|
|
959
|
-
const anchorWeekStart = startOfUtcWeek(anchorDayStart);
|
|
960
|
-
const timeOffset = repeatable.anchorAt - anchorDayStart;
|
|
961
|
-
let occurrence2 = 0;
|
|
962
|
-
for (let day = anchorDayStart; day <= until; day += DAY_MS2) {
|
|
963
|
-
const weeksSince = Math.floor((startOfUtcWeek(day) - anchorWeekStart) / WEEK_MS2);
|
|
964
|
-
if (weeksSince < 0 || weeksSince % rule.interval !== 0) {
|
|
965
|
-
continue;
|
|
966
|
-
}
|
|
967
|
-
if (!rule.byDay.includes(new Date(day).getUTCDay())) {
|
|
968
|
-
continue;
|
|
969
|
-
}
|
|
970
|
-
const anchor = day + timeOffset;
|
|
971
|
-
if (anchor < repeatable.anchorAt || anchor > until) {
|
|
972
|
-
continue;
|
|
973
|
-
}
|
|
974
|
-
occurrence2 += 1;
|
|
975
|
-
if (rule.count != null && occurrence2 > rule.count) {
|
|
976
|
-
break;
|
|
977
|
-
}
|
|
978
|
-
anchors.push(anchor);
|
|
979
|
-
}
|
|
980
|
-
return anchors;
|
|
981
|
-
}
|
|
982
|
-
let occurrence = 0;
|
|
983
|
-
let current = repeatable.anchorAt;
|
|
984
|
-
while (current <= until) {
|
|
985
|
-
occurrence += 1;
|
|
986
|
-
if (rule.count != null && occurrence > rule.count) {
|
|
987
|
-
break;
|
|
988
|
-
}
|
|
989
|
-
anchors.push(current);
|
|
990
|
-
current = advanceAnchor(current, rule.freq, rule.interval);
|
|
991
|
-
}
|
|
992
|
-
return anchors;
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
// src/repeatable_quests/list_due_repeatable_quest_anchors.ts
|
|
996
|
-
function listDueRepeatableQuestAnchors(db, now) {
|
|
997
|
-
const repeatables = db.prepare(
|
|
998
|
-
`SELECT *
|
|
999
|
-
FROM repeatable_quests
|
|
1000
|
-
WHERE deleted_at IS NULL
|
|
1001
|
-
AND archived_at IS NULL`
|
|
1002
|
-
).all().map(mapRepeatableQuestRow);
|
|
1003
|
-
const anchors = [];
|
|
1004
|
-
if (repeatables.length === 0) {
|
|
1005
|
-
return anchors;
|
|
1006
|
-
}
|
|
1007
|
-
const repeatableIds = repeatables.map((repeatable) => repeatable.id);
|
|
1008
|
-
const maxAnchorAt = Math.max(
|
|
1009
|
-
...repeatables.map((repeatable) => Math.max(repeatable.anchorAt, now))
|
|
1010
|
-
);
|
|
1011
|
-
const existingRows = db.prepare(
|
|
1012
|
-
`SELECT spawned_from_repeatable_id, spawned_for_at
|
|
1013
|
-
FROM quests
|
|
1014
|
-
WHERE spawned_from_repeatable_id IN (${repeatableIds.map(() => "?").join(", ")})
|
|
1015
|
-
AND spawned_for_at IS NOT NULL
|
|
1016
|
-
AND spawned_for_at <= ?`
|
|
1017
|
-
).all(...repeatableIds, maxAnchorAt);
|
|
1018
|
-
const existingAnchors = new Set(
|
|
1019
|
-
existingRows.map(
|
|
1020
|
-
(row) => `${Number(row.spawned_from_repeatable_id)}:${Number(row.spawned_for_at)}`
|
|
1021
|
-
)
|
|
1022
|
-
);
|
|
1023
|
-
for (const repeatable of repeatables) {
|
|
1024
|
-
const dueAnchors = generateAnchorsUntil(repeatable, now);
|
|
1025
|
-
for (const anchorAt of dueAnchors) {
|
|
1026
|
-
if (!existingAnchors.has(`${repeatable.id}:${anchorAt}`)) {
|
|
1027
|
-
anchors.push({
|
|
1028
|
-
repeatableQuestId: repeatable.id,
|
|
1029
|
-
anchorAt,
|
|
1030
|
-
markerId: "attention.available.repeatable"
|
|
1031
|
-
});
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
return anchors.sort(
|
|
1036
|
-
(a, b) => a.anchorAt - b.anchorAt || a.repeatableQuestId - b.repeatableQuestId
|
|
1037
|
-
);
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
546
|
// src/markers/derive_rumor_marker_id.ts
|
|
1041
547
|
function deriveRumorMarkerId({ state }) {
|
|
1042
548
|
if (state === "open") {
|
|
@@ -1080,8 +586,182 @@ function listRumors(db) {
|
|
|
1080
586
|
return db.prepare("SELECT * FROM rumors WHERE deleted_at IS NULL ORDER BY created_at DESC, id DESC").all().map(mapRumorRow);
|
|
1081
587
|
}
|
|
1082
588
|
|
|
589
|
+
// src/surfaces/validate_input.ts
|
|
590
|
+
function validateInput(schema, input, path = "$") {
|
|
591
|
+
const errors = [];
|
|
592
|
+
const matchesType = (type) => {
|
|
593
|
+
switch (type) {
|
|
594
|
+
case "object":
|
|
595
|
+
return typeof input === "object" && input !== null && !Array.isArray(input);
|
|
596
|
+
case "array":
|
|
597
|
+
return Array.isArray(input);
|
|
598
|
+
case "integer":
|
|
599
|
+
return typeof input === "number" && Number.isInteger(input);
|
|
600
|
+
case "number":
|
|
601
|
+
return typeof input === "number" && Number.isFinite(input);
|
|
602
|
+
case "null":
|
|
603
|
+
return input === null;
|
|
604
|
+
default:
|
|
605
|
+
return typeof input === type;
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
if (schema.type && !(Array.isArray(schema.type) ? schema.type : [schema.type]).some(matchesType)) {
|
|
609
|
+
return [`${path}: expected ${String(schema.type)}`];
|
|
610
|
+
}
|
|
611
|
+
if (schema.enum && !schema.enum.some((value) => JSON.stringify(value) === JSON.stringify(input)))
|
|
612
|
+
errors.push(`${path}: expected one of ${JSON.stringify(schema.enum)}`);
|
|
613
|
+
const constant = "const" in schema ? schema.const : void 0;
|
|
614
|
+
if (constant !== void 0 && constant !== input)
|
|
615
|
+
errors.push(`${path}: expected ${JSON.stringify(constant)}`);
|
|
616
|
+
if (schema.oneOf) {
|
|
617
|
+
const branches = schema.oneOf.map((branch) => validateInput(branch, input, path));
|
|
618
|
+
if (!branches.some((errors2) => errors2.length === 0)) {
|
|
619
|
+
errors.push(
|
|
620
|
+
...branches.sort((a, b) => a.length - b.length)[0] ?? [
|
|
621
|
+
`${path}: must match at least one schema branch`
|
|
622
|
+
]
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (typeof input === "object" && input !== null && !Array.isArray(input)) {
|
|
627
|
+
const record = input;
|
|
628
|
+
for (const name of [...schema.required ?? [], ...requiredForAction(schema, record.action)])
|
|
629
|
+
if (!Object.hasOwn(record, name)) errors.push(`${path}.${name}: required`);
|
|
630
|
+
for (const [name, property] of Object.entries(schema.properties ?? {}))
|
|
631
|
+
if (Object.hasOwn(record, name))
|
|
632
|
+
errors.push(...validateInput(property, record[name], `${path}.${name}`));
|
|
633
|
+
}
|
|
634
|
+
if (Array.isArray(input) && schema.items)
|
|
635
|
+
input.forEach((value, index) => {
|
|
636
|
+
errors.push(...validateInput(schema.items, value, `${path}[${index}]`));
|
|
637
|
+
});
|
|
638
|
+
return errors;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// src/tools/action_requirements.ts
|
|
642
|
+
var actionRequirements = {
|
|
643
|
+
manage_hooks: { remove: ["id"] },
|
|
644
|
+
shape_work: {
|
|
645
|
+
create_quest: ["quest"],
|
|
646
|
+
create_questline: ["questline"],
|
|
647
|
+
attach_quest_to_questline: ["questId", "questlineId"],
|
|
648
|
+
detach_quest_from_questline: ["questId"],
|
|
649
|
+
dismiss_rumor: ["rumorId"],
|
|
650
|
+
reopen_rumor: ["rumorId"],
|
|
651
|
+
settle_rumor: ["rumorId"],
|
|
652
|
+
update_rumor: ["rumorId"]
|
|
653
|
+
},
|
|
654
|
+
manage_repeatable: {
|
|
655
|
+
create: ["repeatableQuest"],
|
|
656
|
+
archive: ["repeatableQuestId"],
|
|
657
|
+
update: ["repeatableQuestId"],
|
|
658
|
+
spawn_due: ["now"]
|
|
659
|
+
},
|
|
660
|
+
organize_work: {
|
|
661
|
+
add_unlock: ["fromQuestId", "toQuestId"],
|
|
662
|
+
remove_unlock: ["fromQuestId", "toQuestId"],
|
|
663
|
+
replace_unlocks: ["fromQuestIds", "toQuestId"],
|
|
664
|
+
archive_questline: ["questlineId"],
|
|
665
|
+
set_questline_fields: ["questlineId"]
|
|
666
|
+
},
|
|
667
|
+
plan_quest: { revise_objective: ["objective"] },
|
|
668
|
+
run_quest: {
|
|
669
|
+
abandon_and_spawn_followups: ["followups"],
|
|
670
|
+
log_effort: ["effortSeconds"]
|
|
671
|
+
},
|
|
672
|
+
reward_work: { add: ["reward"], update: ["reward"], replace_repeatable_template: ["rewards"] }
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
// src/tools/tool_types.ts
|
|
676
|
+
function withOptionalFields(result, options) {
|
|
677
|
+
if (options.next && options.next.length > 0) {
|
|
678
|
+
result.next = options.next;
|
|
679
|
+
}
|
|
680
|
+
if (options.warnings && options.warnings.length > 0) {
|
|
681
|
+
result.warnings = options.warnings;
|
|
682
|
+
}
|
|
683
|
+
return result;
|
|
684
|
+
}
|
|
685
|
+
function toolWarning(code, message) {
|
|
686
|
+
return { code, message };
|
|
687
|
+
}
|
|
688
|
+
function toolSuccess(summary, data, options = {}) {
|
|
689
|
+
return withOptionalFields(
|
|
690
|
+
{
|
|
691
|
+
ok: true,
|
|
692
|
+
outcome: "success",
|
|
693
|
+
summary,
|
|
694
|
+
entities: options.entities ?? [],
|
|
695
|
+
data
|
|
696
|
+
},
|
|
697
|
+
options
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
function toolNoOp(summary, data, options = {}) {
|
|
701
|
+
return withOptionalFields(
|
|
702
|
+
{
|
|
703
|
+
ok: true,
|
|
704
|
+
outcome: "no_op",
|
|
705
|
+
summary,
|
|
706
|
+
entities: options.entities ?? [],
|
|
707
|
+
data
|
|
708
|
+
},
|
|
709
|
+
options
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
function toolNeedsClarification(code, question, missing, options = {}) {
|
|
713
|
+
return withOptionalFields(
|
|
714
|
+
{
|
|
715
|
+
ok: false,
|
|
716
|
+
outcome: "needs_clarification",
|
|
717
|
+
summary: question,
|
|
718
|
+
entities: options.entities ?? [],
|
|
719
|
+
clarification: {
|
|
720
|
+
code,
|
|
721
|
+
question,
|
|
722
|
+
missing,
|
|
723
|
+
options: options.options
|
|
724
|
+
}
|
|
725
|
+
},
|
|
726
|
+
options
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
function toolFailure(kind, code, summary, message, options = {}) {
|
|
730
|
+
return withOptionalFields(
|
|
731
|
+
{
|
|
732
|
+
ok: false,
|
|
733
|
+
outcome: "error",
|
|
734
|
+
summary,
|
|
735
|
+
entities: options.entities ?? [],
|
|
736
|
+
error: {
|
|
737
|
+
kind,
|
|
738
|
+
code,
|
|
739
|
+
message,
|
|
740
|
+
recovery: options.recovery,
|
|
741
|
+
details: options.details
|
|
742
|
+
}
|
|
743
|
+
},
|
|
744
|
+
options
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
|
|
1083
748
|
// src/tools/tool_metadata.ts
|
|
1084
749
|
function defineQuestlogTool(definition) {
|
|
750
|
+
const requirements = actionRequirements[definition.name];
|
|
751
|
+
if (requirements) registerSchemaActions(definition.inputSchema, requirements);
|
|
752
|
+
const handler = definition.handler;
|
|
753
|
+
definition.handler = (db, input) => {
|
|
754
|
+
const errors = validateInput(definition.inputSchema, input);
|
|
755
|
+
if (errors.length) {
|
|
756
|
+
const action = input?.action;
|
|
757
|
+
const example = minimalInput(definition.inputSchema, action);
|
|
758
|
+
const message = errors.join("; ");
|
|
759
|
+
return toolFailure("protocol", "invalid_input", message, message, {
|
|
760
|
+
recovery: `${message}; pass ${JSON.stringify(example)}`
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
return handler(db, input);
|
|
764
|
+
};
|
|
1085
765
|
return definition;
|
|
1086
766
|
}
|
|
1087
767
|
function stringSchema(description, options = {}) {
|
|
@@ -1243,79 +923,6 @@ function summarizeCount(count, singular, plural = `${singular}s`) {
|
|
|
1243
923
|
return `Found ${count} ${plural}.`;
|
|
1244
924
|
}
|
|
1245
925
|
|
|
1246
|
-
// src/tools/tool_types.ts
|
|
1247
|
-
function withOptionalFields(result, options) {
|
|
1248
|
-
if (options.next && options.next.length > 0) {
|
|
1249
|
-
result.next = options.next;
|
|
1250
|
-
}
|
|
1251
|
-
if (options.warnings && options.warnings.length > 0) {
|
|
1252
|
-
result.warnings = options.warnings;
|
|
1253
|
-
}
|
|
1254
|
-
return result;
|
|
1255
|
-
}
|
|
1256
|
-
function toolWarning(code, message) {
|
|
1257
|
-
return { code, message };
|
|
1258
|
-
}
|
|
1259
|
-
function toolSuccess(summary, data, options = {}) {
|
|
1260
|
-
return withOptionalFields(
|
|
1261
|
-
{
|
|
1262
|
-
ok: true,
|
|
1263
|
-
outcome: "success",
|
|
1264
|
-
summary,
|
|
1265
|
-
entities: options.entities ?? [],
|
|
1266
|
-
data
|
|
1267
|
-
},
|
|
1268
|
-
options
|
|
1269
|
-
);
|
|
1270
|
-
}
|
|
1271
|
-
function toolNoOp(summary, data, options = {}) {
|
|
1272
|
-
return withOptionalFields(
|
|
1273
|
-
{
|
|
1274
|
-
ok: true,
|
|
1275
|
-
outcome: "no_op",
|
|
1276
|
-
summary,
|
|
1277
|
-
entities: options.entities ?? [],
|
|
1278
|
-
data
|
|
1279
|
-
},
|
|
1280
|
-
options
|
|
1281
|
-
);
|
|
1282
|
-
}
|
|
1283
|
-
function toolNeedsClarification(code, question, missing, options = {}) {
|
|
1284
|
-
return withOptionalFields(
|
|
1285
|
-
{
|
|
1286
|
-
ok: false,
|
|
1287
|
-
outcome: "needs_clarification",
|
|
1288
|
-
summary: question,
|
|
1289
|
-
entities: options.entities ?? [],
|
|
1290
|
-
clarification: {
|
|
1291
|
-
code,
|
|
1292
|
-
question,
|
|
1293
|
-
missing,
|
|
1294
|
-
options: options.options
|
|
1295
|
-
}
|
|
1296
|
-
},
|
|
1297
|
-
options
|
|
1298
|
-
);
|
|
1299
|
-
}
|
|
1300
|
-
function toolFailure(kind, code, summary, message, options = {}) {
|
|
1301
|
-
return withOptionalFields(
|
|
1302
|
-
{
|
|
1303
|
-
ok: false,
|
|
1304
|
-
outcome: "error",
|
|
1305
|
-
summary,
|
|
1306
|
-
entities: options.entities ?? [],
|
|
1307
|
-
error: {
|
|
1308
|
-
kind,
|
|
1309
|
-
code,
|
|
1310
|
-
message,
|
|
1311
|
-
recovery: options.recovery,
|
|
1312
|
-
details: options.details
|
|
1313
|
-
}
|
|
1314
|
-
},
|
|
1315
|
-
options
|
|
1316
|
-
);
|
|
1317
|
-
}
|
|
1318
|
-
|
|
1319
926
|
// src/tools/review_questlog_tool.ts
|
|
1320
927
|
function hasQuestFilters(filters) {
|
|
1321
928
|
return Boolean(
|
|
@@ -1346,7 +953,7 @@ function validateReviewInput(input) {
|
|
|
1346
953
|
'Use `filters.dayAt` only with `view: "quests.scheduled_for_day"`.'
|
|
1347
954
|
);
|
|
1348
955
|
}
|
|
1349
|
-
if ((input.view === "rumors" || input.view === "questlines") && hasQuestFilters(input.filters)) {
|
|
956
|
+
if ((input.view === "rumors" || input.view === "questlines" || input.view === "deliveries") && hasQuestFilters(input.filters)) {
|
|
1350
957
|
return toolFailure(
|
|
1351
958
|
"protocol",
|
|
1352
959
|
"invalid_input",
|
|
@@ -1414,6 +1021,15 @@ function reviewQuestlogToolHandler(db, input) {
|
|
|
1414
1021
|
if (invalid) {
|
|
1415
1022
|
return invalid;
|
|
1416
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
|
+
});
|
|
1417
1033
|
const now = input.now ?? Date.now();
|
|
1418
1034
|
let items = [];
|
|
1419
1035
|
switch (input.view) {
|
|
@@ -1473,6 +1089,7 @@ var reviewQuestlogTool = defineQuestlogTool({
|
|
|
1473
1089
|
inputSchema: objectSchema(
|
|
1474
1090
|
{
|
|
1475
1091
|
view: enumSchema("Which operational review surface to return.", [
|
|
1092
|
+
"deliveries",
|
|
1476
1093
|
"questlines",
|
|
1477
1094
|
"quests.active",
|
|
1478
1095
|
"quests.available",
|
|
@@ -1831,7 +1448,8 @@ function inspectQuestlogItemToolHandler(db, input) {
|
|
|
1831
1448
|
detail: detailLevel === "full" ? detail : toQuestCompactDetail(detail)
|
|
1832
1449
|
},
|
|
1833
1450
|
related,
|
|
1834
|
-
detailLevel
|
|
1451
|
+
detailLevel,
|
|
1452
|
+
deliveries: listDeliveries(db, { questId: detail.id })
|
|
1835
1453
|
},
|
|
1836
1454
|
{ entities }
|
|
1837
1455
|
);
|
|
@@ -1937,6 +1555,56 @@ var inspectQuestlogItemTool = defineQuestlogTool({
|
|
|
1937
1555
|
handler: inspectQuestlogItemToolHandler
|
|
1938
1556
|
});
|
|
1939
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
|
+
|
|
1940
1608
|
// src/repeatable_quests/update_repeatable_quest.ts
|
|
1941
1609
|
function updateRepeatableQuest(db, repeatableQuestId, input) {
|
|
1942
1610
|
return withTransaction(db, () => {
|
|
@@ -2106,95 +1774,6 @@ function softDeleteRepeatableQuest(db, repeatableQuestId, deletedAt) {
|
|
|
2106
1774
|
).run(now, now, repeatableQuestId);
|
|
2107
1775
|
}
|
|
2108
1776
|
|
|
2109
|
-
// src/rewards/copy_repeatable_quest_rewards_to_quest.ts
|
|
2110
|
-
function copyRepeatableQuestRewardsToQuest(db, repeatableQuestId, questId, now) {
|
|
2111
|
-
const effectiveNow = resolveNow(now);
|
|
2112
|
-
const rows = db.prepare(
|
|
2113
|
-
`SELECT kind, name, description, quantity
|
|
2114
|
-
FROM repeatable_quest_rewards
|
|
2115
|
-
WHERE repeatable_quest_id = ? AND deleted_at IS NULL`
|
|
2116
|
-
).all(repeatableQuestId);
|
|
2117
|
-
for (const row of rows) {
|
|
2118
|
-
db.prepare(
|
|
2119
|
-
`INSERT INTO quest_rewards (quest_id, kind, name, description, quantity, created_at)
|
|
2120
|
-
VALUES (?, ?, ?, ?, ?, ?)`
|
|
2121
|
-
).run(
|
|
2122
|
-
questId,
|
|
2123
|
-
String(row.kind),
|
|
2124
|
-
String(row.name),
|
|
2125
|
-
row.description == null ? null : String(row.description),
|
|
2126
|
-
row.quantity == null ? null : Number(row.quantity),
|
|
2127
|
-
effectiveNow
|
|
2128
|
-
);
|
|
2129
|
-
}
|
|
2130
|
-
}
|
|
2131
|
-
|
|
2132
|
-
// src/tags/copy_repeatable_quest_tags_to_quest.ts
|
|
2133
|
-
function copyRepeatableQuestTagsToQuest(db, repeatableQuestId, questId, now) {
|
|
2134
|
-
const effectiveNow = resolveNow(now);
|
|
2135
|
-
const rows = db.prepare(
|
|
2136
|
-
`SELECT tag_id
|
|
2137
|
-
FROM repeatable_quest_tags
|
|
2138
|
-
WHERE repeatable_quest_id = ? AND deleted_at IS NULL`
|
|
2139
|
-
).all(repeatableQuestId);
|
|
2140
|
-
for (const row of rows) {
|
|
2141
|
-
db.prepare(
|
|
2142
|
-
`INSERT INTO quest_tags (quest_id, tag_id, created_at)
|
|
2143
|
-
VALUES (?, ?, ?)`
|
|
2144
|
-
).run(questId, Number(row.tag_id), effectiveNow);
|
|
2145
|
-
}
|
|
2146
|
-
}
|
|
2147
|
-
|
|
2148
|
-
// src/repeatable_quests/spawn_due_repeatable_quests.ts
|
|
2149
|
-
function spawnDueRepeatableQuests(db, now) {
|
|
2150
|
-
return withTransaction(db, () => {
|
|
2151
|
-
const anchors = listDueRepeatableQuestAnchors(db, now);
|
|
2152
|
-
const quests = [];
|
|
2153
|
-
for (const anchor of anchors) {
|
|
2154
|
-
const repeatable = getRepeatableQuestOrThrow(db, anchor.repeatableQuestId);
|
|
2155
|
-
const created = db.prepare(
|
|
2156
|
-
`INSERT INTO quests (
|
|
2157
|
-
questline_id,
|
|
2158
|
-
source_rumor_id,
|
|
2159
|
-
spawned_by_quest_id,
|
|
2160
|
-
spawned_from_repeatable_id,
|
|
2161
|
-
spawned_for_at,
|
|
2162
|
-
title,
|
|
2163
|
-
objective,
|
|
2164
|
-
created_at,
|
|
2165
|
-
updated_at,
|
|
2166
|
-
effort_seconds,
|
|
2167
|
-
estimate_seconds,
|
|
2168
|
-
not_before_at,
|
|
2169
|
-
due_at,
|
|
2170
|
-
scheduled_start_at,
|
|
2171
|
-
scheduled_end_at,
|
|
2172
|
-
all_day
|
|
2173
|
-
) VALUES (?, NULL, NULL, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?)`
|
|
2174
|
-
).run(
|
|
2175
|
-
repeatable.questlineId ?? null,
|
|
2176
|
-
repeatable.id,
|
|
2177
|
-
anchor.anchorAt,
|
|
2178
|
-
repeatable.title,
|
|
2179
|
-
repeatable.objective,
|
|
2180
|
-
now,
|
|
2181
|
-
now,
|
|
2182
|
-
repeatable.estimateSeconds ?? null,
|
|
2183
|
-
repeatable.notBeforeOffsetSeconds == null ? null : anchor.anchorAt + repeatable.notBeforeOffsetSeconds * 1e3,
|
|
2184
|
-
repeatable.dueOffsetSeconds == null ? null : anchor.anchorAt + repeatable.dueOffsetSeconds * 1e3,
|
|
2185
|
-
repeatable.scheduledStartOffsetSeconds == null ? null : anchor.anchorAt + repeatable.scheduledStartOffsetSeconds * 1e3,
|
|
2186
|
-
repeatable.scheduledEndOffsetSeconds == null ? null : anchor.anchorAt + repeatable.scheduledEndOffsetSeconds * 1e3,
|
|
2187
|
-
repeatable.allDay ? 1 : 0
|
|
2188
|
-
);
|
|
2189
|
-
const questId = Number(created.lastInsertRowid);
|
|
2190
|
-
copyRepeatableQuestTagsToQuest(db, repeatable.id, questId, now);
|
|
2191
|
-
copyRepeatableQuestRewardsToQuest(db, repeatable.id, questId, now);
|
|
2192
|
-
quests.push(getQuestOrThrow(db, questId));
|
|
2193
|
-
}
|
|
2194
|
-
return quests;
|
|
2195
|
-
});
|
|
2196
|
-
}
|
|
2197
|
-
|
|
2198
1777
|
// src/tools/manage_repeatable_tool.ts
|
|
2199
1778
|
function manageRepeatableToolHandler(db, input) {
|
|
2200
1779
|
try {
|
|
@@ -3191,7 +2770,7 @@ var rewardWorkTool = defineQuestlogTool({
|
|
|
3191
2770
|
name: stringSchema("Reward name."),
|
|
3192
2771
|
now: integerSchema("Optional update timestamp for the update action.")
|
|
3193
2772
|
},
|
|
3194
|
-
[]
|
|
2773
|
+
["kind", "name"]
|
|
3195
2774
|
),
|
|
3196
2775
|
rewards: arraySchema(
|
|
3197
2776
|
objectSchema(
|
|
@@ -4018,7 +3597,7 @@ var shapeWorkTool = defineQuestlogTool({
|
|
|
4018
3597
|
title: stringSchema("Questline title."),
|
|
4019
3598
|
description: stringSchema("Optional questline description.")
|
|
4020
3599
|
},
|
|
4021
|
-
[]
|
|
3600
|
+
["title"]
|
|
4022
3601
|
),
|
|
4023
3602
|
rumorId: integerSchema("Rumor to settle, dismiss, or reopen."),
|
|
4024
3603
|
dismissedAt: integerSchema("Optional dismissal timestamp."),
|
|
@@ -4277,6 +3856,7 @@ var tagWorkTool = defineQuestlogTool({
|
|
|
4277
3856
|
|
|
4278
3857
|
// src/tools/tool_registry.ts
|
|
4279
3858
|
var questlogTools = [
|
|
3859
|
+
manageHooksTool,
|
|
4280
3860
|
searchQuestlogTool,
|
|
4281
3861
|
reviewQuestlogTool,
|
|
4282
3862
|
inspectQuestlogItemTool,
|
|
@@ -4300,9 +3880,9 @@ var questlogSurfaceTools = questlogTools.map(
|
|
|
4300
3880
|
whenNotToUse: tool.whenNotToUse ?? "",
|
|
4301
3881
|
readOnly: tool.readOnly,
|
|
4302
3882
|
sideEffects: tool.sideEffects,
|
|
4303
|
-
inputSchema:
|
|
3883
|
+
inputSchema: tool.inputSchema,
|
|
4304
3884
|
handler: tool.handler
|
|
4305
3885
|
})
|
|
4306
3886
|
);
|
|
4307
3887
|
|
|
4308
|
-
export { getQuestDetail, questlogSurfaceTools, reviewQuestlogTool };
|
|
3888
|
+
export { getQuestDetail, questlogSurfaceTools, reviewQuestlogTool, validateInput };
|