@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
package/dist/mcp.js
CHANGED
|
@@ -1,9 +1,35 @@
|
|
|
1
|
-
import { mkdirSync } from 'node:fs';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
1
|
+
import { mkdirSync, closeSync, openSync } from 'node:fs';
|
|
2
|
+
import { basename, extname, dirname, join } from 'node:path';
|
|
3
|
+
import { randomBytes, createHmac } from 'node:crypto';
|
|
3
4
|
import { homedir } from 'node:os';
|
|
4
5
|
|
|
5
6
|
// src/surface/open_questlog_db.ts
|
|
6
7
|
|
|
8
|
+
// src/hooks/init_hook_tables.ts
|
|
9
|
+
function initHookTables(db) {
|
|
10
|
+
db.exec(`
|
|
11
|
+
CREATE TABLE IF NOT EXISTS hook_targets (
|
|
12
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL, secret TEXT NOT NULL,
|
|
13
|
+
tag TEXT, events TEXT NOT NULL, min_gap INTEGER NOT NULL, created_at INTEGER NOT NULL,
|
|
14
|
+
last_wake INTEGER, lease_until INTEGER, claim TEXT);
|
|
15
|
+
CREATE TABLE IF NOT EXISTS hook_events (
|
|
16
|
+
id TEXT PRIMARY KEY, occurred_at INTEGER NOT NULL, event TEXT NOT NULL);
|
|
17
|
+
CREATE TABLE IF NOT EXISTS hook_assignments (
|
|
18
|
+
target_id INTEGER NOT NULL REFERENCES hook_targets(id) ON DELETE CASCADE,
|
|
19
|
+
event_id TEXT NOT NULL REFERENCES hook_events(id), delivery_id TEXT,
|
|
20
|
+
PRIMARY KEY(target_id, event_id));
|
|
21
|
+
CREATE TABLE IF NOT EXISTS hook_deliveries (
|
|
22
|
+
id TEXT PRIMARY KEY, target_id INTEGER NOT NULL REFERENCES hook_targets(id) ON DELETE CASCADE,
|
|
23
|
+
events TEXT NOT NULL, status TEXT NOT NULL, attempts INTEGER NOT NULL DEFAULT 0,
|
|
24
|
+
last_attempt_at INTEGER, http_status INTEGER, error_class TEXT,
|
|
25
|
+
next_attempt_at INTEGER NOT NULL);
|
|
26
|
+
CREATE INDEX IF NOT EXISTS idx_hook_deliveries_target ON hook_deliveries(target_id, status, next_attempt_at);
|
|
27
|
+
CREATE TABLE IF NOT EXISTS hook_settings (id INTEGER PRIMARY KEY CHECK(id = 1), label TEXT NOT NULL);
|
|
28
|
+
`);
|
|
29
|
+
if (!db.prepare("PRAGMA table_info(hook_targets)").all().some((row) => row.name === "headers"))
|
|
30
|
+
db.exec("ALTER TABLE hook_targets ADD COLUMN headers TEXT NOT NULL DEFAULT '{}'");
|
|
31
|
+
}
|
|
32
|
+
|
|
7
33
|
// src/questlines/init_questline_tables.ts
|
|
8
34
|
function initQuestlineTables(db) {
|
|
9
35
|
db.exec(`
|
|
@@ -352,6 +378,7 @@ function initQuestlogTables(db) {
|
|
|
352
378
|
initTagTables(db);
|
|
353
379
|
initQuestSearch(db);
|
|
354
380
|
initRumorSearch(db);
|
|
381
|
+
initHookTables(db);
|
|
355
382
|
}
|
|
356
383
|
|
|
357
384
|
// src/surface/open_questlog_db.ts
|
|
@@ -370,7 +397,14 @@ async function openQuestlogDb(path) {
|
|
|
370
397
|
} finally {
|
|
371
398
|
process.emitWarning = original;
|
|
372
399
|
}
|
|
373
|
-
if (path !== ":memory:")
|
|
400
|
+
if (path !== ":memory:") {
|
|
401
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
402
|
+
try {
|
|
403
|
+
closeSync(openSync(path, "wx", 384));
|
|
404
|
+
} catch (error) {
|
|
405
|
+
if (error.code !== "EEXIST") throw error;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
374
408
|
const db = new sqlite.DatabaseSync(path);
|
|
375
409
|
try {
|
|
376
410
|
db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;");
|
|
@@ -384,7 +418,7 @@ async function openQuestlogDb(path) {
|
|
|
384
418
|
}
|
|
385
419
|
|
|
386
420
|
// src/version.ts
|
|
387
|
-
var questlogVersion = "0.
|
|
421
|
+
var questlogVersion = "0.11.0";
|
|
388
422
|
|
|
389
423
|
// src/surface/questlog_mcp_instructions.ts
|
|
390
424
|
var questlogMcpInstructions = "Questlog manages local tasks, calendar commitments, and recurring work. Search before creating work, inspect before changing it, and keep uncertain work as rumors. Use unlock prerequisites to sequence work. run_quest refuses blocked starts unless force is true. Review commitments and resolve work with an honest outcome.";
|
|
@@ -729,6 +763,7 @@ function validateInput(schema, input, path = "$") {
|
|
|
729
763
|
|
|
730
764
|
// src/tools/action_requirements.ts
|
|
731
765
|
var actionRequirements = {
|
|
766
|
+
manage_hooks: { remove: ["id"] },
|
|
732
767
|
shape_work: {
|
|
733
768
|
create_quest: ["quest"],
|
|
734
769
|
create_questline: ["questline"],
|
|
@@ -1020,6 +1055,27 @@ var captureRumorTool = defineQuestlogTool({
|
|
|
1020
1055
|
handler: captureRumorToolHandler
|
|
1021
1056
|
});
|
|
1022
1057
|
|
|
1058
|
+
// src/hooks/list_deliveries.ts
|
|
1059
|
+
function listDeliveries(db, options = {}) {
|
|
1060
|
+
return db.prepare(
|
|
1061
|
+
`SELECT * FROM hook_deliveries ${options.includeDelivered ? "" : "WHERE status <> 'delivered'"} ORDER BY target_id, next_attempt_at, id`
|
|
1062
|
+
).all().map(
|
|
1063
|
+
(row) => ({
|
|
1064
|
+
id: String(row.id),
|
|
1065
|
+
targetId: Number(row.target_id),
|
|
1066
|
+
events: JSON.parse(String(row.events)),
|
|
1067
|
+
status: row.status,
|
|
1068
|
+
attempts: Number(row.attempts),
|
|
1069
|
+
lastAttemptAt: row.last_attempt_at == null ? null : Number(row.last_attempt_at),
|
|
1070
|
+
httpStatus: row.http_status == null ? null : Number(row.http_status),
|
|
1071
|
+
errorClass: row.error_class == null ? null : String(row.error_class),
|
|
1072
|
+
nextAttemptAt: Number(row.next_attempt_at)
|
|
1073
|
+
})
|
|
1074
|
+
).filter(
|
|
1075
|
+
(d) => options.questId === void 0 || d.events.some((e) => e.questId === options.questId)
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1023
1079
|
// src/questlines/map_questline_row.ts
|
|
1024
1080
|
function mapQuestlineRow(row) {
|
|
1025
1081
|
return {
|
|
@@ -1497,7 +1553,8 @@ function inspectQuestlogItemToolHandler(db, input) {
|
|
|
1497
1553
|
detail: detailLevel === "full" ? detail : toQuestCompactDetail(detail)
|
|
1498
1554
|
},
|
|
1499
1555
|
related,
|
|
1500
|
-
detailLevel
|
|
1556
|
+
detailLevel,
|
|
1557
|
+
deliveries: listDeliveries(db, { questId: detail.id })
|
|
1501
1558
|
},
|
|
1502
1559
|
{ entities }
|
|
1503
1560
|
);
|
|
@@ -1602,13 +1659,42 @@ var inspectQuestlogItemTool = defineQuestlogTool({
|
|
|
1602
1659
|
),
|
|
1603
1660
|
handler: inspectQuestlogItemToolHandler
|
|
1604
1661
|
});
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
return
|
|
1662
|
+
function hookLabel(db, name) {
|
|
1663
|
+
const saved = db.prepare("SELECT label FROM hook_settings WHERE id = 1").get();
|
|
1664
|
+
if (saved) return String(saved.label);
|
|
1665
|
+
const path = String(
|
|
1666
|
+
db.prepare("PRAGMA database_list").all().find((row) => row.name === "main")?.file ?? ""
|
|
1667
|
+
);
|
|
1668
|
+
return path ? basename(path, extname(path)) : "questlog";
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// src/hooks/list_hooks.ts
|
|
1672
|
+
function listHooks(db) {
|
|
1673
|
+
return db.prepare(`SELECT id, url, headers, tag, events, min_gap, created_at, last_wake,
|
|
1674
|
+
(SELECT COUNT(*) FROM hook_assignments a LEFT JOIN hook_deliveries d ON d.id = a.delivery_id WHERE a.target_id = t.id AND (a.delivery_id IS NULL OR d.status = 'pending')) AS pending,
|
|
1675
|
+
(SELECT COUNT(*) FROM hook_deliveries d WHERE d.target_id = t.id AND d.status = 'failed') AS failed
|
|
1676
|
+
FROM hook_targets t ORDER BY id`).all().map((row) => {
|
|
1677
|
+
const url = new URL(String(row.url));
|
|
1678
|
+
url.username = "";
|
|
1679
|
+
url.password = "";
|
|
1680
|
+
url.search = "";
|
|
1681
|
+
url.hash = "";
|
|
1682
|
+
return {
|
|
1683
|
+
id: Number(row.id),
|
|
1684
|
+
headers: Object.fromEntries(
|
|
1685
|
+
Object.keys(JSON.parse(String(row.headers))).map((name) => [name, "***"])
|
|
1686
|
+
),
|
|
1687
|
+
scope: row.tag == null ? "database" : "tag",
|
|
1688
|
+
url: url.href,
|
|
1689
|
+
tag: row.tag == null ? null : String(row.tag),
|
|
1690
|
+
events: JSON.parse(String(row.events)),
|
|
1691
|
+
minGap: Number(row.min_gap),
|
|
1692
|
+
createdAt: Number(row.created_at),
|
|
1693
|
+
lastWake: row.last_wake == null ? null : Number(row.last_wake),
|
|
1694
|
+
pending: Number(row.pending),
|
|
1695
|
+
failed: Number(row.failed)
|
|
1696
|
+
};
|
|
1697
|
+
});
|
|
1612
1698
|
}
|
|
1613
1699
|
|
|
1614
1700
|
// src/with_transaction.ts
|
|
@@ -1646,6 +1732,140 @@ function withTransaction(db, fn) {
|
|
|
1646
1732
|
}
|
|
1647
1733
|
}
|
|
1648
1734
|
|
|
1735
|
+
// src/hooks/remove_hook.ts
|
|
1736
|
+
function removeHook(db, id) {
|
|
1737
|
+
withTransaction(db, () => {
|
|
1738
|
+
db.prepare("DELETE FROM hook_assignments WHERE target_id = ?").run(id);
|
|
1739
|
+
db.prepare("DELETE FROM hook_deliveries WHERE target_id = ?").run(id);
|
|
1740
|
+
db.prepare("DELETE FROM hook_targets WHERE id = ?").run(id);
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
async function sendHookDelivery(db, delivery, target, options) {
|
|
1744
|
+
const now = options.now ?? options.clock?.() ?? Date.now();
|
|
1745
|
+
const timestamp = String(Math.floor(now / 1e3));
|
|
1746
|
+
const body = JSON.stringify({
|
|
1747
|
+
type: "questlog.events",
|
|
1748
|
+
db: hookLabel(db),
|
|
1749
|
+
deliveryId: delivery.id,
|
|
1750
|
+
sentAt: new Date(now).toISOString(),
|
|
1751
|
+
events: delivery.events
|
|
1752
|
+
});
|
|
1753
|
+
const signature = createHmac("sha256", target.secret).update(`${timestamp}.${body}`).digest("hex");
|
|
1754
|
+
let status = null;
|
|
1755
|
+
let errorClass = null;
|
|
1756
|
+
try {
|
|
1757
|
+
const response = await (options.fetch ?? globalThis.fetch)(target.url, {
|
|
1758
|
+
method: "POST",
|
|
1759
|
+
redirect: "error",
|
|
1760
|
+
headers: {
|
|
1761
|
+
"user-agent": `questlog/${questlogVersion}`,
|
|
1762
|
+
...JSON.parse(
|
|
1763
|
+
String(
|
|
1764
|
+
db.prepare("SELECT headers FROM hook_targets WHERE id = ?").get(target.id)?.headers ?? "{}"
|
|
1765
|
+
)
|
|
1766
|
+
),
|
|
1767
|
+
"content-type": "application/json",
|
|
1768
|
+
"x-questlog-delivery": delivery.id,
|
|
1769
|
+
"x-questlog-timestamp": timestamp,
|
|
1770
|
+
"x-questlog-signature": `sha256=${signature}`
|
|
1771
|
+
},
|
|
1772
|
+
body,
|
|
1773
|
+
signal: AbortSignal.timeout(Math.max(1, Math.min(options.timeoutMs ?? 5e3, 5e3)))
|
|
1774
|
+
});
|
|
1775
|
+
status = response.status;
|
|
1776
|
+
await response.body?.cancel();
|
|
1777
|
+
} catch (error) {
|
|
1778
|
+
errorClass = error instanceof Error && ["TimeoutError", "AbortError"].includes(error.name) ? error.name : "NetworkError";
|
|
1779
|
+
}
|
|
1780
|
+
const delivered = status != null && status >= 200 && status < 300;
|
|
1781
|
+
const completedAt = options.now ?? options.clock?.() ?? Date.now();
|
|
1782
|
+
withTransaction(db, () => {
|
|
1783
|
+
if (!db.prepare("SELECT id FROM hook_targets WHERE id = ? AND claim = ?").get(target.id, target.claim))
|
|
1784
|
+
return;
|
|
1785
|
+
const delay = Math.min(36e5, 3e4 * 2 ** (delivery.attempts - 1));
|
|
1786
|
+
const jitter = Number.parseInt(randomBytes(2).toString("hex"), 16) % 1e3;
|
|
1787
|
+
db.prepare(
|
|
1788
|
+
"UPDATE hook_deliveries SET status = ?, http_status = ?, error_class = ?, next_attempt_at = ? WHERE id = ?"
|
|
1789
|
+
).run(
|
|
1790
|
+
delivered ? "delivered" : delivery.attempts >= 8 ? "failed" : "pending",
|
|
1791
|
+
status,
|
|
1792
|
+
errorClass,
|
|
1793
|
+
completedAt + delay + jitter,
|
|
1794
|
+
delivery.id
|
|
1795
|
+
);
|
|
1796
|
+
db.prepare(
|
|
1797
|
+
"UPDATE hook_targets SET claim = NULL, lease_until = NULL WHERE id = ? AND claim = ?"
|
|
1798
|
+
).run(target.id, target.claim);
|
|
1799
|
+
db.prepare(
|
|
1800
|
+
"DELETE FROM hook_deliveries WHERE target_id = ? AND status = 'delivered' AND id NOT IN (SELECT id FROM hook_deliveries WHERE target_id = ? AND status = 'delivered' ORDER BY last_attempt_at DESC, id DESC LIMIT 500)"
|
|
1801
|
+
).run(target.id, target.id);
|
|
1802
|
+
});
|
|
1803
|
+
return delivered;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// src/quests/get_quest_or_throw.ts
|
|
1807
|
+
function getQuestOrThrow(db, questId) {
|
|
1808
|
+
const row = db.prepare("SELECT * FROM quests WHERE id = ? AND deleted_at IS NULL").get(questId);
|
|
1809
|
+
if (!row) {
|
|
1810
|
+
throw new Error(`Quest ${questId} was not found.`);
|
|
1811
|
+
}
|
|
1812
|
+
return mapQuestRow(row);
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// src/rewards/copy_repeatable_quest_rewards_to_quest.ts
|
|
1816
|
+
function copyRepeatableQuestRewardsToQuest(db, repeatableQuestId, questId, now) {
|
|
1817
|
+
const effectiveNow = resolveNow(now);
|
|
1818
|
+
const rows = db.prepare(
|
|
1819
|
+
`SELECT kind, name, description, quantity
|
|
1820
|
+
FROM repeatable_quest_rewards
|
|
1821
|
+
WHERE repeatable_quest_id = ? AND deleted_at IS NULL`
|
|
1822
|
+
).all(repeatableQuestId);
|
|
1823
|
+
for (const row of rows) {
|
|
1824
|
+
db.prepare(
|
|
1825
|
+
`INSERT INTO quest_rewards (quest_id, kind, name, description, quantity, created_at)
|
|
1826
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
1827
|
+
).run(
|
|
1828
|
+
questId,
|
|
1829
|
+
String(row.kind),
|
|
1830
|
+
String(row.name),
|
|
1831
|
+
row.description == null ? null : String(row.description),
|
|
1832
|
+
row.quantity == null ? null : Number(row.quantity),
|
|
1833
|
+
effectiveNow
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
// src/tags/copy_repeatable_quest_tags_to_quest.ts
|
|
1839
|
+
function copyRepeatableQuestTagsToQuest(db, repeatableQuestId, questId, now) {
|
|
1840
|
+
const effectiveNow = resolveNow(now);
|
|
1841
|
+
const rows = db.prepare(
|
|
1842
|
+
`SELECT tag_id
|
|
1843
|
+
FROM repeatable_quest_tags
|
|
1844
|
+
WHERE repeatable_quest_id = ? AND deleted_at IS NULL`
|
|
1845
|
+
).all(repeatableQuestId);
|
|
1846
|
+
for (const row of rows) {
|
|
1847
|
+
db.prepare(
|
|
1848
|
+
`INSERT INTO quest_tags (quest_id, tag_id, created_at)
|
|
1849
|
+
VALUES (?, ?, ?)`
|
|
1850
|
+
).run(questId, Number(row.tag_id), effectiveNow);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
// src/repeatable_quests/advance_anchor.ts
|
|
1855
|
+
var WEEK_MS = 7 * 864e5;
|
|
1856
|
+
function advanceAnchor(anchorAt, freq, interval) {
|
|
1857
|
+
const date = new Date(anchorAt);
|
|
1858
|
+
if (freq === "MONTHLY") {
|
|
1859
|
+
date.setUTCMonth(date.getUTCMonth() + interval);
|
|
1860
|
+
return date.getTime();
|
|
1861
|
+
}
|
|
1862
|
+
if (freq === "YEARLY") {
|
|
1863
|
+
date.setUTCFullYear(date.getUTCFullYear() + interval);
|
|
1864
|
+
return date.getTime();
|
|
1865
|
+
}
|
|
1866
|
+
return anchorAt + interval * WEEK_MS;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1649
1869
|
// src/repeatable_quests/parse_rrule.ts
|
|
1650
1870
|
var WEEKDAY = {
|
|
1651
1871
|
SU: 0,
|
|
@@ -1689,265 +1909,76 @@ function parseRRule(rrule) {
|
|
|
1689
1909
|
return { freq, interval, count, until, byDay };
|
|
1690
1910
|
}
|
|
1691
1911
|
|
|
1692
|
-
// src/repeatable_quests/
|
|
1693
|
-
function
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
const now = resolveNow(input.now);
|
|
1697
|
-
const rrule = input.rrule ?? current.rrule;
|
|
1698
|
-
parseRRule(rrule);
|
|
1699
|
-
db.prepare(
|
|
1700
|
-
`UPDATE repeatable_quests
|
|
1701
|
-
SET questline_id = ?,
|
|
1702
|
-
title = ?,
|
|
1703
|
-
objective = ?,
|
|
1704
|
-
rrule = ?,
|
|
1705
|
-
anchor_at = ?,
|
|
1706
|
-
not_before_offset_seconds = ?,
|
|
1707
|
-
due_offset_seconds = ?,
|
|
1708
|
-
scheduled_start_offset_seconds = ?,
|
|
1709
|
-
scheduled_end_offset_seconds = ?,
|
|
1710
|
-
all_day = ?,
|
|
1711
|
-
estimate_seconds = ?,
|
|
1712
|
-
archived_at = ?,
|
|
1713
|
-
updated_at = ?
|
|
1714
|
-
WHERE id = ? AND deleted_at IS NULL`
|
|
1715
|
-
).run(
|
|
1716
|
-
input.questlineId === void 0 ? current.questlineId : input.questlineId,
|
|
1717
|
-
input.title == null ? current.title : assertNonEmpty(input.title, "Repeatable quest title"),
|
|
1718
|
-
input.objective == null ? current.objective : assertNonEmpty(input.objective, "Repeatable quest objective"),
|
|
1719
|
-
rrule,
|
|
1720
|
-
input.anchorAt ?? current.anchorAt,
|
|
1721
|
-
input.notBeforeOffsetSeconds === void 0 ? current.notBeforeOffsetSeconds : assertNonNegativeSeconds(input.notBeforeOffsetSeconds, "Repeatable not-before offset") ?? null,
|
|
1722
|
-
input.dueOffsetSeconds === void 0 ? current.dueOffsetSeconds : assertNonNegativeSeconds(input.dueOffsetSeconds, "Repeatable due offset") ?? null,
|
|
1723
|
-
input.scheduledStartOffsetSeconds === void 0 ? current.scheduledStartOffsetSeconds : input.scheduledStartOffsetSeconds,
|
|
1724
|
-
input.scheduledEndOffsetSeconds === void 0 ? current.scheduledEndOffsetSeconds : input.scheduledEndOffsetSeconds,
|
|
1725
|
-
input.allDay === void 0 ? current.allDay ? 1 : 0 : input.allDay ? 1 : 0,
|
|
1726
|
-
input.estimateSeconds === void 0 ? current.estimateSeconds : assertNonNegativeSeconds(input.estimateSeconds, "Repeatable estimate") ?? null,
|
|
1727
|
-
input.archivedAt === void 0 ? current.archivedAt : input.archivedAt,
|
|
1728
|
-
now,
|
|
1729
|
-
repeatableQuestId
|
|
1730
|
-
);
|
|
1731
|
-
return getRepeatableQuestOrThrow(db, repeatableQuestId);
|
|
1732
|
-
});
|
|
1912
|
+
// src/repeatable_quests/start_of_utc_day.ts
|
|
1913
|
+
function startOfUtcDay(timestamp) {
|
|
1914
|
+
const date = new Date(timestamp);
|
|
1915
|
+
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
|
1733
1916
|
}
|
|
1734
1917
|
|
|
1735
|
-
// src/repeatable_quests/
|
|
1736
|
-
|
|
1737
|
-
|
|
1918
|
+
// src/repeatable_quests/start_of_utc_week.ts
|
|
1919
|
+
var DAY_MS = 864e5;
|
|
1920
|
+
function startOfUtcWeek(dayStart) {
|
|
1921
|
+
const date = new Date(dayStart);
|
|
1922
|
+
return dayStart - date.getUTCDay() * DAY_MS;
|
|
1738
1923
|
}
|
|
1739
1924
|
|
|
1740
|
-
// src/
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1925
|
+
// src/repeatable_quests/generate_anchors_until.ts
|
|
1926
|
+
var DAY_MS2 = 864e5;
|
|
1927
|
+
var WEEK_MS2 = 7 * DAY_MS2;
|
|
1928
|
+
function generateAnchorsUntil(repeatable, now) {
|
|
1929
|
+
const rule = parseRRule(repeatable.rrule);
|
|
1930
|
+
const anchors = [];
|
|
1931
|
+
const until = rule.until == null ? now : Math.min(now, rule.until);
|
|
1932
|
+
if (repeatable.anchorAt > until) {
|
|
1933
|
+
return anchors;
|
|
1745
1934
|
}
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1935
|
+
if (rule.freq === "DAILY") {
|
|
1936
|
+
let occurrence2 = 0;
|
|
1937
|
+
for (let current2 = repeatable.anchorAt; current2 <= until; current2 += rule.interval * DAY_MS2) {
|
|
1938
|
+
occurrence2 += 1;
|
|
1939
|
+
if (rule.count != null && occurrence2 > rule.count) {
|
|
1940
|
+
break;
|
|
1941
|
+
}
|
|
1942
|
+
anchors.push(current2);
|
|
1943
|
+
}
|
|
1944
|
+
return anchors;
|
|
1752
1945
|
}
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1946
|
+
if (rule.freq === "WEEKLY" && rule.byDay && rule.byDay.length > 0) {
|
|
1947
|
+
const anchorDayStart = startOfUtcDay(repeatable.anchorAt);
|
|
1948
|
+
const anchorWeekStart = startOfUtcWeek(anchorDayStart);
|
|
1949
|
+
const timeOffset = repeatable.anchorAt - anchorDayStart;
|
|
1950
|
+
let occurrence2 = 0;
|
|
1951
|
+
for (let day = anchorDayStart; day <= until; day += DAY_MS2) {
|
|
1952
|
+
const weeksSince = Math.floor((startOfUtcWeek(day) - anchorWeekStart) / WEEK_MS2);
|
|
1953
|
+
if (weeksSince < 0 || weeksSince % rule.interval !== 0) {
|
|
1954
|
+
continue;
|
|
1955
|
+
}
|
|
1956
|
+
if (!rule.byDay.includes(new Date(day).getUTCDay())) {
|
|
1957
|
+
continue;
|
|
1958
|
+
}
|
|
1959
|
+
const anchor = day + timeOffset;
|
|
1960
|
+
if (anchor < repeatable.anchorAt || anchor > until) {
|
|
1961
|
+
continue;
|
|
1962
|
+
}
|
|
1963
|
+
occurrence2 += 1;
|
|
1964
|
+
if (rule.count != null && occurrence2 > rule.count) {
|
|
1965
|
+
break;
|
|
1966
|
+
}
|
|
1967
|
+
anchors.push(anchor);
|
|
1968
|
+
}
|
|
1969
|
+
return anchors;
|
|
1970
|
+
}
|
|
1971
|
+
let occurrence = 0;
|
|
1972
|
+
let current = repeatable.anchorAt;
|
|
1973
|
+
while (current <= until) {
|
|
1974
|
+
occurrence += 1;
|
|
1975
|
+
if (rule.count != null && occurrence > rule.count) {
|
|
1976
|
+
break;
|
|
1977
|
+
}
|
|
1978
|
+
anchors.push(current);
|
|
1979
|
+
current = advanceAnchor(current, rule.freq, rule.interval);
|
|
1780
1980
|
}
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
// src/tags/replace_active_repeatable_quest_tags.ts
|
|
1784
|
-
function getOrCreateTagId(db, name, now) {
|
|
1785
|
-
const normalizedName = normalizeTagName(name);
|
|
1786
|
-
if (!normalizedName) {
|
|
1787
|
-
throw new Error("Tag name must not be empty.");
|
|
1788
|
-
}
|
|
1789
|
-
const existing = db.prepare("SELECT id FROM tags WHERE normalized_name = ? AND deleted_at IS NULL").get(normalizedName);
|
|
1790
|
-
if (existing) {
|
|
1791
|
-
return Number(existing.id);
|
|
1792
|
-
}
|
|
1793
|
-
const created = db.prepare(
|
|
1794
|
-
`INSERT INTO tags (name, normalized_name, created_at)
|
|
1795
|
-
VALUES (?, ?, ?)`
|
|
1796
|
-
).run(name.trim(), normalizedName, now);
|
|
1797
|
-
return Number(created.lastInsertRowid);
|
|
1798
|
-
}
|
|
1799
|
-
function replaceActiveRepeatableQuestTags(db, repeatableQuestId, tagNames, now) {
|
|
1800
|
-
const effectiveNow = resolveNow(now);
|
|
1801
|
-
db.prepare(
|
|
1802
|
-
"UPDATE repeatable_quest_tags SET deleted_at = ? WHERE repeatable_quest_id = ? AND deleted_at IS NULL"
|
|
1803
|
-
).run(effectiveNow, repeatableQuestId);
|
|
1804
|
-
const uniqueNames = [...new Set(tagNames.map((tag) => tag.trim()).filter(Boolean))];
|
|
1805
|
-
for (const name of uniqueNames) {
|
|
1806
|
-
const tagId = getOrCreateTagId(db, name, effectiveNow);
|
|
1807
|
-
db.prepare(
|
|
1808
|
-
`INSERT INTO repeatable_quest_tags (repeatable_quest_id, tag_id, created_at)
|
|
1809
|
-
VALUES (?, ?, ?)`
|
|
1810
|
-
).run(repeatableQuestId, tagId, effectiveNow);
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
// src/repeatable_quests/create_repeatable_quest.ts
|
|
1815
|
-
function createRepeatableQuest(db, input) {
|
|
1816
|
-
return withTransaction(db, () => {
|
|
1817
|
-
const now = resolveNow(input.now);
|
|
1818
|
-
const title = assertNonEmpty(input.title, "Repeatable quest title");
|
|
1819
|
-
const objective = assertNonEmpty(input.objective, "Repeatable quest objective");
|
|
1820
|
-
parseRRule(input.rrule);
|
|
1821
|
-
if (input.questlineId != null) {
|
|
1822
|
-
assertActiveRowExists(db, "questlines", input.questlineId, "Questline");
|
|
1823
|
-
}
|
|
1824
|
-
const created = db.prepare(
|
|
1825
|
-
`INSERT INTO repeatable_quests (
|
|
1826
|
-
questline_id,
|
|
1827
|
-
title,
|
|
1828
|
-
objective,
|
|
1829
|
-
rrule,
|
|
1830
|
-
anchor_at,
|
|
1831
|
-
not_before_offset_seconds,
|
|
1832
|
-
due_offset_seconds,
|
|
1833
|
-
scheduled_start_offset_seconds,
|
|
1834
|
-
scheduled_end_offset_seconds,
|
|
1835
|
-
all_day,
|
|
1836
|
-
estimate_seconds,
|
|
1837
|
-
created_at,
|
|
1838
|
-
updated_at
|
|
1839
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
1840
|
-
).run(
|
|
1841
|
-
input.questlineId ?? null,
|
|
1842
|
-
title,
|
|
1843
|
-
objective,
|
|
1844
|
-
input.rrule,
|
|
1845
|
-
input.anchorAt,
|
|
1846
|
-
assertNonNegativeSeconds(input.notBeforeOffsetSeconds, "Repeatable not-before offset") ?? null,
|
|
1847
|
-
assertNonNegativeSeconds(input.dueOffsetSeconds, "Repeatable due offset") ?? null,
|
|
1848
|
-
input.scheduledStartOffsetSeconds ?? null,
|
|
1849
|
-
input.scheduledEndOffsetSeconds ?? null,
|
|
1850
|
-
input.allDay ? 1 : 0,
|
|
1851
|
-
assertNonNegativeSeconds(input.estimateSeconds, "Repeatable estimate") ?? null,
|
|
1852
|
-
now,
|
|
1853
|
-
now
|
|
1854
|
-
);
|
|
1855
|
-
const repeatableQuestId = Number(created.lastInsertRowid);
|
|
1856
|
-
if (input.tags && input.tags.length > 0) {
|
|
1857
|
-
replaceActiveRepeatableQuestTags(db, repeatableQuestId, input.tags, now);
|
|
1858
|
-
}
|
|
1859
|
-
if (input.rewards && input.rewards.length > 0) {
|
|
1860
|
-
replaceActiveRepeatableQuestRewards(db, repeatableQuestId, input.rewards, now);
|
|
1861
|
-
}
|
|
1862
|
-
return getRepeatableQuestOrThrow(db, repeatableQuestId);
|
|
1863
|
-
});
|
|
1864
|
-
}
|
|
1865
|
-
|
|
1866
|
-
// src/repeatable_quests/advance_anchor.ts
|
|
1867
|
-
var WEEK_MS = 7 * 864e5;
|
|
1868
|
-
function advanceAnchor(anchorAt, freq, interval) {
|
|
1869
|
-
const date = new Date(anchorAt);
|
|
1870
|
-
if (freq === "MONTHLY") {
|
|
1871
|
-
date.setUTCMonth(date.getUTCMonth() + interval);
|
|
1872
|
-
return date.getTime();
|
|
1873
|
-
}
|
|
1874
|
-
if (freq === "YEARLY") {
|
|
1875
|
-
date.setUTCFullYear(date.getUTCFullYear() + interval);
|
|
1876
|
-
return date.getTime();
|
|
1877
|
-
}
|
|
1878
|
-
return anchorAt + interval * WEEK_MS;
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
// src/repeatable_quests/start_of_utc_day.ts
|
|
1882
|
-
function startOfUtcDay(timestamp) {
|
|
1883
|
-
const date = new Date(timestamp);
|
|
1884
|
-
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
// src/repeatable_quests/start_of_utc_week.ts
|
|
1888
|
-
var DAY_MS = 864e5;
|
|
1889
|
-
function startOfUtcWeek(dayStart) {
|
|
1890
|
-
const date = new Date(dayStart);
|
|
1891
|
-
return dayStart - date.getUTCDay() * DAY_MS;
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
|
-
// src/repeatable_quests/generate_anchors_until.ts
|
|
1895
|
-
var DAY_MS2 = 864e5;
|
|
1896
|
-
var WEEK_MS2 = 7 * DAY_MS2;
|
|
1897
|
-
function generateAnchorsUntil(repeatable, now) {
|
|
1898
|
-
const rule = parseRRule(repeatable.rrule);
|
|
1899
|
-
const anchors = [];
|
|
1900
|
-
const until = rule.until == null ? now : Math.min(now, rule.until);
|
|
1901
|
-
if (repeatable.anchorAt > until) {
|
|
1902
|
-
return anchors;
|
|
1903
|
-
}
|
|
1904
|
-
if (rule.freq === "DAILY") {
|
|
1905
|
-
let occurrence2 = 0;
|
|
1906
|
-
for (let current2 = repeatable.anchorAt; current2 <= until; current2 += rule.interval * DAY_MS2) {
|
|
1907
|
-
occurrence2 += 1;
|
|
1908
|
-
if (rule.count != null && occurrence2 > rule.count) {
|
|
1909
|
-
break;
|
|
1910
|
-
}
|
|
1911
|
-
anchors.push(current2);
|
|
1912
|
-
}
|
|
1913
|
-
return anchors;
|
|
1914
|
-
}
|
|
1915
|
-
if (rule.freq === "WEEKLY" && rule.byDay && rule.byDay.length > 0) {
|
|
1916
|
-
const anchorDayStart = startOfUtcDay(repeatable.anchorAt);
|
|
1917
|
-
const anchorWeekStart = startOfUtcWeek(anchorDayStart);
|
|
1918
|
-
const timeOffset = repeatable.anchorAt - anchorDayStart;
|
|
1919
|
-
let occurrence2 = 0;
|
|
1920
|
-
for (let day = anchorDayStart; day <= until; day += DAY_MS2) {
|
|
1921
|
-
const weeksSince = Math.floor((startOfUtcWeek(day) - anchorWeekStart) / WEEK_MS2);
|
|
1922
|
-
if (weeksSince < 0 || weeksSince % rule.interval !== 0) {
|
|
1923
|
-
continue;
|
|
1924
|
-
}
|
|
1925
|
-
if (!rule.byDay.includes(new Date(day).getUTCDay())) {
|
|
1926
|
-
continue;
|
|
1927
|
-
}
|
|
1928
|
-
const anchor = day + timeOffset;
|
|
1929
|
-
if (anchor < repeatable.anchorAt || anchor > until) {
|
|
1930
|
-
continue;
|
|
1931
|
-
}
|
|
1932
|
-
occurrence2 += 1;
|
|
1933
|
-
if (rule.count != null && occurrence2 > rule.count) {
|
|
1934
|
-
break;
|
|
1935
|
-
}
|
|
1936
|
-
anchors.push(anchor);
|
|
1937
|
-
}
|
|
1938
|
-
return anchors;
|
|
1939
|
-
}
|
|
1940
|
-
let occurrence = 0;
|
|
1941
|
-
let current = repeatable.anchorAt;
|
|
1942
|
-
while (current <= until) {
|
|
1943
|
-
occurrence += 1;
|
|
1944
|
-
if (rule.count != null && occurrence > rule.count) {
|
|
1945
|
-
break;
|
|
1946
|
-
}
|
|
1947
|
-
anchors.push(current);
|
|
1948
|
-
current = advanceAnchor(current, rule.freq, rule.interval);
|
|
1949
|
-
}
|
|
1950
|
-
return anchors;
|
|
1981
|
+
return anchors;
|
|
1951
1982
|
}
|
|
1952
1983
|
|
|
1953
1984
|
// src/repeatable_quests/list_due_repeatable_quest_anchors.ts
|
|
@@ -1995,65 +2026,6 @@ function listDueRepeatableQuestAnchors(db, now) {
|
|
|
1995
2026
|
);
|
|
1996
2027
|
}
|
|
1997
2028
|
|
|
1998
|
-
// src/repeatable_quests/soft_delete_repeatable_quest.ts
|
|
1999
|
-
function softDeleteRepeatableQuest(db, repeatableQuestId, deletedAt) {
|
|
2000
|
-
const now = resolveNow(deletedAt);
|
|
2001
|
-
getRepeatableQuestOrThrow(db, repeatableQuestId);
|
|
2002
|
-
db.prepare(
|
|
2003
|
-
`UPDATE repeatable_quests
|
|
2004
|
-
SET deleted_at = ?, updated_at = ?
|
|
2005
|
-
WHERE id = ? AND deleted_at IS NULL`
|
|
2006
|
-
).run(now, now, repeatableQuestId);
|
|
2007
|
-
}
|
|
2008
|
-
|
|
2009
|
-
// src/quests/get_quest_or_throw.ts
|
|
2010
|
-
function getQuestOrThrow(db, questId) {
|
|
2011
|
-
const row = db.prepare("SELECT * FROM quests WHERE id = ? AND deleted_at IS NULL").get(questId);
|
|
2012
|
-
if (!row) {
|
|
2013
|
-
throw new Error(`Quest ${questId} was not found.`);
|
|
2014
|
-
}
|
|
2015
|
-
return mapQuestRow(row);
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
// src/rewards/copy_repeatable_quest_rewards_to_quest.ts
|
|
2019
|
-
function copyRepeatableQuestRewardsToQuest(db, repeatableQuestId, questId, now) {
|
|
2020
|
-
const effectiveNow = resolveNow(now);
|
|
2021
|
-
const rows = db.prepare(
|
|
2022
|
-
`SELECT kind, name, description, quantity
|
|
2023
|
-
FROM repeatable_quest_rewards
|
|
2024
|
-
WHERE repeatable_quest_id = ? AND deleted_at IS NULL`
|
|
2025
|
-
).all(repeatableQuestId);
|
|
2026
|
-
for (const row of rows) {
|
|
2027
|
-
db.prepare(
|
|
2028
|
-
`INSERT INTO quest_rewards (quest_id, kind, name, description, quantity, created_at)
|
|
2029
|
-
VALUES (?, ?, ?, ?, ?, ?)`
|
|
2030
|
-
).run(
|
|
2031
|
-
questId,
|
|
2032
|
-
String(row.kind),
|
|
2033
|
-
String(row.name),
|
|
2034
|
-
row.description == null ? null : String(row.description),
|
|
2035
|
-
row.quantity == null ? null : Number(row.quantity),
|
|
2036
|
-
effectiveNow
|
|
2037
|
-
);
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
|
|
2041
|
-
// src/tags/copy_repeatable_quest_tags_to_quest.ts
|
|
2042
|
-
function copyRepeatableQuestTagsToQuest(db, repeatableQuestId, questId, now) {
|
|
2043
|
-
const effectiveNow = resolveNow(now);
|
|
2044
|
-
const rows = db.prepare(
|
|
2045
|
-
`SELECT tag_id
|
|
2046
|
-
FROM repeatable_quest_tags
|
|
2047
|
-
WHERE repeatable_quest_id = ? AND deleted_at IS NULL`
|
|
2048
|
-
).all(repeatableQuestId);
|
|
2049
|
-
for (const row of rows) {
|
|
2050
|
-
db.prepare(
|
|
2051
|
-
`INSERT INTO quest_tags (quest_id, tag_id, created_at)
|
|
2052
|
-
VALUES (?, ?, ?)`
|
|
2053
|
-
).run(questId, Number(row.tag_id), effectiveNow);
|
|
2054
|
-
}
|
|
2055
|
-
}
|
|
2056
|
-
|
|
2057
2029
|
// src/repeatable_quests/spawn_due_repeatable_quests.ts
|
|
2058
2030
|
function spawnDueRepeatableQuests(db, now) {
|
|
2059
2031
|
return withTransaction(db, () => {
|
|
@@ -2104,6 +2076,410 @@ function spawnDueRepeatableQuests(db, now) {
|
|
|
2104
2076
|
});
|
|
2105
2077
|
}
|
|
2106
2078
|
|
|
2079
|
+
// src/hooks/detect_hook_events.ts
|
|
2080
|
+
function detectHookEvents(db, now) {
|
|
2081
|
+
const events = [];
|
|
2082
|
+
const add = (type, at, ids) => {
|
|
2083
|
+
events.push({
|
|
2084
|
+
id: `e_${type}_${ids.questId ?? ids.rumorId}_${ids.repeatableId ?? ""}_${at}`,
|
|
2085
|
+
type,
|
|
2086
|
+
...ids,
|
|
2087
|
+
occurredAt: new Date(at).toISOString()
|
|
2088
|
+
});
|
|
2089
|
+
};
|
|
2090
|
+
for (const { detail: q } of queryQuestDetailRecords(db, {}, now)) {
|
|
2091
|
+
if (q.resolvedAt == null && q.effectiveDueAt != null && q.effectiveDueAt <= now) {
|
|
2092
|
+
add("quest.due", q.effectiveDueAt, { questId: q.id });
|
|
2093
|
+
if (q.effectiveDueAt < now) add("quest.overdue", q.effectiveDueAt, { questId: q.id });
|
|
2094
|
+
}
|
|
2095
|
+
if (q.available) {
|
|
2096
|
+
const prerequisites = db.prepare(
|
|
2097
|
+
`SELECT p.id, p.resolved_at, u.created_at FROM quest_unlocks u JOIN quests p ON p.id = u.from_quest_id WHERE u.to_quest_id = ? AND u.deleted_at IS NULL AND p.deleted_at IS NULL AND p.success = 1 AND p.resolved_at IS NOT NULL`
|
|
2098
|
+
).all(q.id);
|
|
2099
|
+
if (prerequisites.some((p) => Number(p.resolved_at) >= Number(p.created_at))) {
|
|
2100
|
+
const at = Math.max(q.notBeforeAt ?? 0, ...prerequisites.map((p) => Number(p.resolved_at)));
|
|
2101
|
+
if (at <= now) add("quest.unblocked", at, { questId: q.id });
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
if (q.spawnedFromRepeatableId != null && q.createdAt <= now)
|
|
2105
|
+
add("repeatable.spawned", q.createdAt, {
|
|
2106
|
+
questId: q.id,
|
|
2107
|
+
repeatableId: q.spawnedFromRepeatableId
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
for (const row of db.prepare("SELECT id, created_at FROM rumors WHERE deleted_at IS NULL AND created_at <= ?").all(now))
|
|
2111
|
+
add("rumor.captured", Number(row.created_at), { rumorId: Number(row.id) });
|
|
2112
|
+
return events.sort(
|
|
2113
|
+
(a, b) => a.occurredAt.localeCompare(b.occurredAt) || a.id.localeCompare(b.id)
|
|
2114
|
+
);
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// src/hooks/tick_questlog.ts
|
|
2118
|
+
async function tickQuestlog(db, options = {}) {
|
|
2119
|
+
const now = options.now ?? options.clock?.() ?? Date.now();
|
|
2120
|
+
const result = {
|
|
2121
|
+
events: [],
|
|
2122
|
+
batches: [],
|
|
2123
|
+
sent: 0,
|
|
2124
|
+
queued: 0,
|
|
2125
|
+
retrying: 0,
|
|
2126
|
+
failed: 0
|
|
2127
|
+
};
|
|
2128
|
+
const claims = [];
|
|
2129
|
+
const collect = () => {
|
|
2130
|
+
spawnDueRepeatableQuests(db, now);
|
|
2131
|
+
const detected = detectHookEvents(db, now);
|
|
2132
|
+
result.events = detected.filter(
|
|
2133
|
+
(e) => !db.prepare("SELECT id FROM hook_events WHERE id = ?").get(e.id)
|
|
2134
|
+
);
|
|
2135
|
+
for (const event of result.events)
|
|
2136
|
+
db.prepare("INSERT INTO hook_events(id, occurred_at, event) VALUES (?, ?, ?)").run(
|
|
2137
|
+
event.id,
|
|
2138
|
+
Date.parse(event.occurredAt),
|
|
2139
|
+
JSON.stringify(event)
|
|
2140
|
+
);
|
|
2141
|
+
for (const target of listHooks(db)) {
|
|
2142
|
+
const events = db.prepare(
|
|
2143
|
+
"SELECT e.event FROM hook_events e WHERE occurred_at >= ? AND NOT EXISTS (SELECT 1 FROM hook_assignments a WHERE a.target_id = ? AND a.event_id = e.id) ORDER BY occurred_at, id"
|
|
2144
|
+
).all(target.createdAt, target.id).map((row2) => JSON.parse(String(row2.event))).filter((event) => {
|
|
2145
|
+
if (!target.events.includes(event.type)) return false;
|
|
2146
|
+
if (target.tag == null) return true;
|
|
2147
|
+
return event.questId !== void 0 && db.prepare(
|
|
2148
|
+
"SELECT 1 FROM quest_tags qt JOIN tags t ON t.id = qt.tag_id WHERE qt.quest_id = ? AND qt.deleted_at IS NULL AND t.deleted_at IS NULL AND t.normalized_name = ?"
|
|
2149
|
+
).get(event.questId, normalizeTagName(target.tag)) !== void 0;
|
|
2150
|
+
});
|
|
2151
|
+
for (const event of events)
|
|
2152
|
+
db.prepare("INSERT INTO hook_assignments(target_id, event_id) VALUES (?, ?)").run(
|
|
2153
|
+
target.id,
|
|
2154
|
+
event.id
|
|
2155
|
+
);
|
|
2156
|
+
const row = db.prepare("SELECT url, secret, lease_until FROM hook_targets WHERE id = ?").get(target.id);
|
|
2157
|
+
if (!row || row.lease_until != null && Number(row.lease_until) > now) continue;
|
|
2158
|
+
let pending = listDeliveries(db).find(
|
|
2159
|
+
(d) => d.targetId === target.id && d.status === "pending"
|
|
2160
|
+
);
|
|
2161
|
+
const queued = db.prepare(
|
|
2162
|
+
"SELECT e.event FROM hook_assignments a JOIN hook_events e ON e.id = a.event_id WHERE a.target_id = ? AND a.delivery_id IS NULL ORDER BY e.occurred_at, e.id"
|
|
2163
|
+
).all(target.id).map((r) => JSON.parse(String(r.event)));
|
|
2164
|
+
if (!pending && queued.length) {
|
|
2165
|
+
const id = `d_${randomBytes(16).toString("hex")}`;
|
|
2166
|
+
db.prepare(
|
|
2167
|
+
"INSERT INTO hook_deliveries(id, target_id, events, status, next_attempt_at) VALUES (?, ?, ?, 'pending', ?)"
|
|
2168
|
+
).run(
|
|
2169
|
+
id,
|
|
2170
|
+
target.id,
|
|
2171
|
+
JSON.stringify(queued),
|
|
2172
|
+
Math.max(now, (target.lastWake ?? -Infinity) + target.minGap * 1e3)
|
|
2173
|
+
);
|
|
2174
|
+
db.prepare(
|
|
2175
|
+
"UPDATE hook_assignments SET delivery_id = ? WHERE target_id = ? AND delivery_id IS NULL"
|
|
2176
|
+
).run(id, target.id);
|
|
2177
|
+
pending = listDeliveries(db).find((d) => d.id === id);
|
|
2178
|
+
}
|
|
2179
|
+
if (!pending) continue;
|
|
2180
|
+
if (pending.attempts === 0 && queued.length && !queued.every((e) => pending?.events.some((p) => p.id === e.id))) {
|
|
2181
|
+
pending.events.push(...queued);
|
|
2182
|
+
db.prepare("UPDATE hook_deliveries SET events = ? WHERE id = ?").run(
|
|
2183
|
+
JSON.stringify(pending.events),
|
|
2184
|
+
pending.id
|
|
2185
|
+
);
|
|
2186
|
+
db.prepare(
|
|
2187
|
+
"UPDATE hook_assignments SET delivery_id = ? WHERE target_id = ? AND delivery_id IS NULL"
|
|
2188
|
+
).run(pending.id, target.id);
|
|
2189
|
+
}
|
|
2190
|
+
if (pending.nextAttemptAt > now || target.lastWake != null && target.lastWake + target.minGap * 1e3 > now)
|
|
2191
|
+
continue;
|
|
2192
|
+
if (pending.attempts >= 8) {
|
|
2193
|
+
db.prepare("UPDATE hook_deliveries SET status = 'failed' WHERE id = ?").run(pending.id);
|
|
2194
|
+
continue;
|
|
2195
|
+
}
|
|
2196
|
+
const claim = randomBytes(16).toString("hex");
|
|
2197
|
+
db.prepare(
|
|
2198
|
+
"UPDATE hook_targets SET claim = ?, lease_until = ?, last_wake = ? WHERE id = ?"
|
|
2199
|
+
).run(claim, now + 12e4, now, target.id);
|
|
2200
|
+
db.prepare(
|
|
2201
|
+
"UPDATE hook_deliveries SET attempts = attempts + 1, last_attempt_at = ? WHERE id = ?"
|
|
2202
|
+
).run(now, pending.id);
|
|
2203
|
+
pending.attempts++;
|
|
2204
|
+
pending.lastAttemptAt = now;
|
|
2205
|
+
result.batches.push({ targetId: target.id, events: pending.events });
|
|
2206
|
+
claims.push({
|
|
2207
|
+
delivery: pending,
|
|
2208
|
+
target: { id: target.id, url: String(row.url), secret: String(row.secret), claim }
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
};
|
|
2212
|
+
if (options.dryRun) {
|
|
2213
|
+
const rollback = new Error("preview rollback");
|
|
2214
|
+
try {
|
|
2215
|
+
withTransaction(db, () => {
|
|
2216
|
+
collect();
|
|
2217
|
+
throw rollback;
|
|
2218
|
+
});
|
|
2219
|
+
} catch (error) {
|
|
2220
|
+
if (error !== rollback) throw error;
|
|
2221
|
+
}
|
|
2222
|
+
} else withTransaction(db, collect);
|
|
2223
|
+
if (!options.dryRun) {
|
|
2224
|
+
const sent = await Promise.all(
|
|
2225
|
+
claims.map((claim) => sendHookDelivery(db, claim.delivery, claim.target, options))
|
|
2226
|
+
);
|
|
2227
|
+
result.sent = sent.filter(Boolean).length;
|
|
2228
|
+
}
|
|
2229
|
+
const deliveries = listDeliveries(db);
|
|
2230
|
+
result.queued = deliveries.filter((d) => d.status === "pending" && d.attempts === 0).length + Number(
|
|
2231
|
+
db.prepare(
|
|
2232
|
+
"SELECT COUNT(DISTINCT target_id) AS n FROM hook_assignments WHERE delivery_id IS NULL"
|
|
2233
|
+
).get()?.n ?? 0
|
|
2234
|
+
);
|
|
2235
|
+
result.retrying = deliveries.filter((d) => d.status === "pending" && d.attempts > 0).length;
|
|
2236
|
+
result.failed = deliveries.filter((d) => d.status === "failed").length;
|
|
2237
|
+
return result;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
// src/tools/manage_hooks_tool.ts
|
|
2241
|
+
var manageHooksTool = defineQuestlogTool({
|
|
2242
|
+
name: "manage_hooks",
|
|
2243
|
+
description: "List or remove outbound webhook targets, run a tick, or inspect pending and failed deliveries.",
|
|
2244
|
+
whenToUse: "Use to operate configured outbound webhooks. Add and test targets with the CLI.",
|
|
2245
|
+
whenNotToUse: "Use quest tools to change work itself.",
|
|
2246
|
+
sideEffects: "writes_state",
|
|
2247
|
+
readOnly: false,
|
|
2248
|
+
supportsClarification: false,
|
|
2249
|
+
targetKinds: ["hook"],
|
|
2250
|
+
inputDescriptions: {
|
|
2251
|
+
action: "Operation to perform.",
|
|
2252
|
+
id: "Target id for removal.",
|
|
2253
|
+
dry_run: "Preview a tick without recording or sending."
|
|
2254
|
+
},
|
|
2255
|
+
outputDescription: "Returns redacted targets, delivery metadata, or a tick summary. Never returns signing secrets.",
|
|
2256
|
+
inputSchema: objectSchema(
|
|
2257
|
+
{
|
|
2258
|
+
action: enumSchema("Operation.", ["list", "remove", "tick", "deliveries"]),
|
|
2259
|
+
id: integerSchema("Target id."),
|
|
2260
|
+
dry_run: booleanSchema("Preview only.")
|
|
2261
|
+
},
|
|
2262
|
+
["action"]
|
|
2263
|
+
),
|
|
2264
|
+
handler: async (db, input) => {
|
|
2265
|
+
switch (input.action) {
|
|
2266
|
+
case "list":
|
|
2267
|
+
return toolSuccess("Webhook targets.", listHooks(db));
|
|
2268
|
+
case "deliveries":
|
|
2269
|
+
return toolSuccess("Pending and failed deliveries.", listDeliveries(db));
|
|
2270
|
+
case "remove":
|
|
2271
|
+
if (!Number.isSafeInteger(input.id) || Number(input.id) < 1)
|
|
2272
|
+
return toolFailure(
|
|
2273
|
+
"protocol",
|
|
2274
|
+
"invalid_input",
|
|
2275
|
+
"Target id required.",
|
|
2276
|
+
"Provide a positive target id.",
|
|
2277
|
+
{ recovery: 'Pass {"action":"remove","id":1}' }
|
|
2278
|
+
);
|
|
2279
|
+
removeHook(db, Number(input.id));
|
|
2280
|
+
return toolSuccess("Webhook target removed.", {});
|
|
2281
|
+
case "tick":
|
|
2282
|
+
return toolSuccess(
|
|
2283
|
+
"Webhook tick completed.",
|
|
2284
|
+
await tickQuestlog(db, { dryRun: input.dry_run ?? false })
|
|
2285
|
+
);
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
});
|
|
2289
|
+
|
|
2290
|
+
// src/assert_non_negative_seconds.ts
|
|
2291
|
+
function assertNonNegativeSeconds(value, label) {
|
|
2292
|
+
if (value != null && value < 0) {
|
|
2293
|
+
throw new Error(`${label} must not be negative.`);
|
|
2294
|
+
}
|
|
2295
|
+
return value;
|
|
2296
|
+
}
|
|
2297
|
+
|
|
2298
|
+
// src/repeatable_quests/update_repeatable_quest.ts
|
|
2299
|
+
function updateRepeatableQuest(db, repeatableQuestId, input) {
|
|
2300
|
+
return withTransaction(db, () => {
|
|
2301
|
+
const current = getRepeatableQuestOrThrow(db, repeatableQuestId);
|
|
2302
|
+
const now = resolveNow(input.now);
|
|
2303
|
+
const rrule = input.rrule ?? current.rrule;
|
|
2304
|
+
parseRRule(rrule);
|
|
2305
|
+
db.prepare(
|
|
2306
|
+
`UPDATE repeatable_quests
|
|
2307
|
+
SET questline_id = ?,
|
|
2308
|
+
title = ?,
|
|
2309
|
+
objective = ?,
|
|
2310
|
+
rrule = ?,
|
|
2311
|
+
anchor_at = ?,
|
|
2312
|
+
not_before_offset_seconds = ?,
|
|
2313
|
+
due_offset_seconds = ?,
|
|
2314
|
+
scheduled_start_offset_seconds = ?,
|
|
2315
|
+
scheduled_end_offset_seconds = ?,
|
|
2316
|
+
all_day = ?,
|
|
2317
|
+
estimate_seconds = ?,
|
|
2318
|
+
archived_at = ?,
|
|
2319
|
+
updated_at = ?
|
|
2320
|
+
WHERE id = ? AND deleted_at IS NULL`
|
|
2321
|
+
).run(
|
|
2322
|
+
input.questlineId === void 0 ? current.questlineId : input.questlineId,
|
|
2323
|
+
input.title == null ? current.title : assertNonEmpty(input.title, "Repeatable quest title"),
|
|
2324
|
+
input.objective == null ? current.objective : assertNonEmpty(input.objective, "Repeatable quest objective"),
|
|
2325
|
+
rrule,
|
|
2326
|
+
input.anchorAt ?? current.anchorAt,
|
|
2327
|
+
input.notBeforeOffsetSeconds === void 0 ? current.notBeforeOffsetSeconds : assertNonNegativeSeconds(input.notBeforeOffsetSeconds, "Repeatable not-before offset") ?? null,
|
|
2328
|
+
input.dueOffsetSeconds === void 0 ? current.dueOffsetSeconds : assertNonNegativeSeconds(input.dueOffsetSeconds, "Repeatable due offset") ?? null,
|
|
2329
|
+
input.scheduledStartOffsetSeconds === void 0 ? current.scheduledStartOffsetSeconds : input.scheduledStartOffsetSeconds,
|
|
2330
|
+
input.scheduledEndOffsetSeconds === void 0 ? current.scheduledEndOffsetSeconds : input.scheduledEndOffsetSeconds,
|
|
2331
|
+
input.allDay === void 0 ? current.allDay ? 1 : 0 : input.allDay ? 1 : 0,
|
|
2332
|
+
input.estimateSeconds === void 0 ? current.estimateSeconds : assertNonNegativeSeconds(input.estimateSeconds, "Repeatable estimate") ?? null,
|
|
2333
|
+
input.archivedAt === void 0 ? current.archivedAt : input.archivedAt,
|
|
2334
|
+
now,
|
|
2335
|
+
repeatableQuestId
|
|
2336
|
+
);
|
|
2337
|
+
return getRepeatableQuestOrThrow(db, repeatableQuestId);
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
// src/repeatable_quests/archive_repeatable_quest.ts
|
|
2342
|
+
function archiveRepeatableQuest(db, repeatableQuestId, archivedAt) {
|
|
2343
|
+
return updateRepeatableQuest(db, repeatableQuestId, { archivedAt: resolveNow(archivedAt) });
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
// src/assert_active_row_exists.ts
|
|
2347
|
+
function assertActiveRowExists(db, table, id, label) {
|
|
2348
|
+
const row = db.prepare(`SELECT id FROM ${table} WHERE id = ? AND deleted_at IS NULL`).get(id);
|
|
2349
|
+
if (!row) {
|
|
2350
|
+
throw new Error(`${label} ${id} was not found.`);
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
// src/assert_non_negative_number.ts
|
|
2355
|
+
function assertNonNegativeNumber(value, label) {
|
|
2356
|
+
if (value != null && value < 0) {
|
|
2357
|
+
throw new Error(`${label} must not be negative.`);
|
|
2358
|
+
}
|
|
2359
|
+
return value;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
// src/rewards/replace_active_repeatable_quest_rewards.ts
|
|
2363
|
+
function replaceActiveRepeatableQuestRewards(db, repeatableQuestId, rewards, now) {
|
|
2364
|
+
const effectiveNow = resolveNow(now);
|
|
2365
|
+
db.prepare(
|
|
2366
|
+
"UPDATE repeatable_quest_rewards SET deleted_at = ? WHERE repeatable_quest_id = ? AND deleted_at IS NULL"
|
|
2367
|
+
).run(effectiveNow, repeatableQuestId);
|
|
2368
|
+
for (const reward of rewards) {
|
|
2369
|
+
db.prepare(
|
|
2370
|
+
`INSERT INTO repeatable_quest_rewards (
|
|
2371
|
+
repeatable_quest_id,
|
|
2372
|
+
kind,
|
|
2373
|
+
name,
|
|
2374
|
+
description,
|
|
2375
|
+
quantity,
|
|
2376
|
+
created_at
|
|
2377
|
+
) VALUES (?, ?, ?, ?, ?, ?)`
|
|
2378
|
+
).run(
|
|
2379
|
+
repeatableQuestId,
|
|
2380
|
+
assertNonEmpty(reward.kind, "Reward kind"),
|
|
2381
|
+
assertNonEmpty(reward.name, "Reward name"),
|
|
2382
|
+
reward.description?.trim() || null,
|
|
2383
|
+
assertNonNegativeNumber(reward.quantity, "Reward quantity") ?? null,
|
|
2384
|
+
effectiveNow
|
|
2385
|
+
);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
// src/tags/replace_active_repeatable_quest_tags.ts
|
|
2390
|
+
function getOrCreateTagId(db, name, now) {
|
|
2391
|
+
const normalizedName = normalizeTagName(name);
|
|
2392
|
+
if (!normalizedName) {
|
|
2393
|
+
throw new Error("Tag name must not be empty.");
|
|
2394
|
+
}
|
|
2395
|
+
const existing = db.prepare("SELECT id FROM tags WHERE normalized_name = ? AND deleted_at IS NULL").get(normalizedName);
|
|
2396
|
+
if (existing) {
|
|
2397
|
+
return Number(existing.id);
|
|
2398
|
+
}
|
|
2399
|
+
const created = db.prepare(
|
|
2400
|
+
`INSERT INTO tags (name, normalized_name, created_at)
|
|
2401
|
+
VALUES (?, ?, ?)`
|
|
2402
|
+
).run(name.trim(), normalizedName, now);
|
|
2403
|
+
return Number(created.lastInsertRowid);
|
|
2404
|
+
}
|
|
2405
|
+
function replaceActiveRepeatableQuestTags(db, repeatableQuestId, tagNames, now) {
|
|
2406
|
+
const effectiveNow = resolveNow(now);
|
|
2407
|
+
db.prepare(
|
|
2408
|
+
"UPDATE repeatable_quest_tags SET deleted_at = ? WHERE repeatable_quest_id = ? AND deleted_at IS NULL"
|
|
2409
|
+
).run(effectiveNow, repeatableQuestId);
|
|
2410
|
+
const uniqueNames = [...new Set(tagNames.map((tag) => tag.trim()).filter(Boolean))];
|
|
2411
|
+
for (const name of uniqueNames) {
|
|
2412
|
+
const tagId = getOrCreateTagId(db, name, effectiveNow);
|
|
2413
|
+
db.prepare(
|
|
2414
|
+
`INSERT INTO repeatable_quest_tags (repeatable_quest_id, tag_id, created_at)
|
|
2415
|
+
VALUES (?, ?, ?)`
|
|
2416
|
+
).run(repeatableQuestId, tagId, effectiveNow);
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
// src/repeatable_quests/create_repeatable_quest.ts
|
|
2421
|
+
function createRepeatableQuest(db, input) {
|
|
2422
|
+
return withTransaction(db, () => {
|
|
2423
|
+
const now = resolveNow(input.now);
|
|
2424
|
+
const title = assertNonEmpty(input.title, "Repeatable quest title");
|
|
2425
|
+
const objective = assertNonEmpty(input.objective, "Repeatable quest objective");
|
|
2426
|
+
parseRRule(input.rrule);
|
|
2427
|
+
if (input.questlineId != null) {
|
|
2428
|
+
assertActiveRowExists(db, "questlines", input.questlineId, "Questline");
|
|
2429
|
+
}
|
|
2430
|
+
const created = db.prepare(
|
|
2431
|
+
`INSERT INTO repeatable_quests (
|
|
2432
|
+
questline_id,
|
|
2433
|
+
title,
|
|
2434
|
+
objective,
|
|
2435
|
+
rrule,
|
|
2436
|
+
anchor_at,
|
|
2437
|
+
not_before_offset_seconds,
|
|
2438
|
+
due_offset_seconds,
|
|
2439
|
+
scheduled_start_offset_seconds,
|
|
2440
|
+
scheduled_end_offset_seconds,
|
|
2441
|
+
all_day,
|
|
2442
|
+
estimate_seconds,
|
|
2443
|
+
created_at,
|
|
2444
|
+
updated_at
|
|
2445
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
2446
|
+
).run(
|
|
2447
|
+
input.questlineId ?? null,
|
|
2448
|
+
title,
|
|
2449
|
+
objective,
|
|
2450
|
+
input.rrule,
|
|
2451
|
+
input.anchorAt,
|
|
2452
|
+
assertNonNegativeSeconds(input.notBeforeOffsetSeconds, "Repeatable not-before offset") ?? null,
|
|
2453
|
+
assertNonNegativeSeconds(input.dueOffsetSeconds, "Repeatable due offset") ?? null,
|
|
2454
|
+
input.scheduledStartOffsetSeconds ?? null,
|
|
2455
|
+
input.scheduledEndOffsetSeconds ?? null,
|
|
2456
|
+
input.allDay ? 1 : 0,
|
|
2457
|
+
assertNonNegativeSeconds(input.estimateSeconds, "Repeatable estimate") ?? null,
|
|
2458
|
+
now,
|
|
2459
|
+
now
|
|
2460
|
+
);
|
|
2461
|
+
const repeatableQuestId = Number(created.lastInsertRowid);
|
|
2462
|
+
if (input.tags && input.tags.length > 0) {
|
|
2463
|
+
replaceActiveRepeatableQuestTags(db, repeatableQuestId, input.tags, now);
|
|
2464
|
+
}
|
|
2465
|
+
if (input.rewards && input.rewards.length > 0) {
|
|
2466
|
+
replaceActiveRepeatableQuestRewards(db, repeatableQuestId, input.rewards, now);
|
|
2467
|
+
}
|
|
2468
|
+
return getRepeatableQuestOrThrow(db, repeatableQuestId);
|
|
2469
|
+
});
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
// src/repeatable_quests/soft_delete_repeatable_quest.ts
|
|
2473
|
+
function softDeleteRepeatableQuest(db, repeatableQuestId, deletedAt) {
|
|
2474
|
+
const now = resolveNow(deletedAt);
|
|
2475
|
+
getRepeatableQuestOrThrow(db, repeatableQuestId);
|
|
2476
|
+
db.prepare(
|
|
2477
|
+
`UPDATE repeatable_quests
|
|
2478
|
+
SET deleted_at = ?, updated_at = ?
|
|
2479
|
+
WHERE id = ? AND deleted_at IS NULL`
|
|
2480
|
+
).run(now, now, repeatableQuestId);
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2107
2483
|
// src/tools/manage_repeatable_tool.ts
|
|
2108
2484
|
function manageRepeatableToolHandler(db, input) {
|
|
2109
2485
|
try {
|
|
@@ -3323,7 +3699,7 @@ function validateReviewInput(input) {
|
|
|
3323
3699
|
'Use `filters.dayAt` only with `view: "quests.scheduled_for_day"`.'
|
|
3324
3700
|
);
|
|
3325
3701
|
}
|
|
3326
|
-
if ((input.view === "rumors" || input.view === "questlines") && hasQuestFilters(input.filters)) {
|
|
3702
|
+
if ((input.view === "rumors" || input.view === "questlines" || input.view === "deliveries") && hasQuestFilters(input.filters)) {
|
|
3327
3703
|
return toolFailure(
|
|
3328
3704
|
"protocol",
|
|
3329
3705
|
"invalid_input",
|
|
@@ -3391,6 +3767,15 @@ function reviewQuestlogToolHandler(db, input) {
|
|
|
3391
3767
|
if (invalid) {
|
|
3392
3768
|
return invalid;
|
|
3393
3769
|
}
|
|
3770
|
+
if (input.view === "deliveries")
|
|
3771
|
+
return toolSuccess("Pending and failed deliveries.", {
|
|
3772
|
+
view: input.view,
|
|
3773
|
+
items: [],
|
|
3774
|
+
deliveries: listDeliveries(db),
|
|
3775
|
+
count: listDeliveries(db).length,
|
|
3776
|
+
appliedFilters: input.filters,
|
|
3777
|
+
evaluatedAt: input.now ?? Date.now()
|
|
3778
|
+
});
|
|
3394
3779
|
const now = input.now ?? Date.now();
|
|
3395
3780
|
let items = [];
|
|
3396
3781
|
switch (input.view) {
|
|
@@ -3450,6 +3835,7 @@ var reviewQuestlogTool = defineQuestlogTool({
|
|
|
3450
3835
|
inputSchema: objectSchema(
|
|
3451
3836
|
{
|
|
3452
3837
|
view: enumSchema("Which operational review surface to return.", [
|
|
3838
|
+
"deliveries",
|
|
3453
3839
|
"questlines",
|
|
3454
3840
|
"quests.active",
|
|
3455
3841
|
"quests.available",
|
|
@@ -4812,6 +5198,7 @@ var tagWorkTool = defineQuestlogTool({
|
|
|
4812
5198
|
|
|
4813
5199
|
// src/tools/tool_registry.ts
|
|
4814
5200
|
var questlogTools = [
|
|
5201
|
+
manageHooksTool,
|
|
4815
5202
|
searchQuestlogTool,
|
|
4816
5203
|
reviewQuestlogTool,
|
|
4817
5204
|
inspectQuestlogItemTool,
|