@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-server.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { mkdirSync } from 'node:fs';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
2
|
+
import { mkdirSync, closeSync, openSync } from 'node:fs';
|
|
3
|
+
import { basename, extname, dirname, join } from 'node:path';
|
|
4
|
+
import { randomBytes, createHmac } from 'node:crypto';
|
|
4
5
|
import { homedir } from 'node:os';
|
|
5
6
|
|
|
6
7
|
var __defProp = Object.defineProperty;
|
|
@@ -323,6 +324,35 @@ var init_mcp_stdio = __esm({
|
|
|
323
324
|
}
|
|
324
325
|
});
|
|
325
326
|
|
|
327
|
+
// src/hooks/init_hook_tables.ts
|
|
328
|
+
function initHookTables(db) {
|
|
329
|
+
db.exec(`
|
|
330
|
+
CREATE TABLE IF NOT EXISTS hook_targets (
|
|
331
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, url TEXT NOT NULL, secret TEXT NOT NULL,
|
|
332
|
+
tag TEXT, events TEXT NOT NULL, min_gap INTEGER NOT NULL, created_at INTEGER NOT NULL,
|
|
333
|
+
last_wake INTEGER, lease_until INTEGER, claim TEXT);
|
|
334
|
+
CREATE TABLE IF NOT EXISTS hook_events (
|
|
335
|
+
id TEXT PRIMARY KEY, occurred_at INTEGER NOT NULL, event TEXT NOT NULL);
|
|
336
|
+
CREATE TABLE IF NOT EXISTS hook_assignments (
|
|
337
|
+
target_id INTEGER NOT NULL REFERENCES hook_targets(id) ON DELETE CASCADE,
|
|
338
|
+
event_id TEXT NOT NULL REFERENCES hook_events(id), delivery_id TEXT,
|
|
339
|
+
PRIMARY KEY(target_id, event_id));
|
|
340
|
+
CREATE TABLE IF NOT EXISTS hook_deliveries (
|
|
341
|
+
id TEXT PRIMARY KEY, target_id INTEGER NOT NULL REFERENCES hook_targets(id) ON DELETE CASCADE,
|
|
342
|
+
events TEXT NOT NULL, status TEXT NOT NULL, attempts INTEGER NOT NULL DEFAULT 0,
|
|
343
|
+
last_attempt_at INTEGER, http_status INTEGER, error_class TEXT,
|
|
344
|
+
next_attempt_at INTEGER NOT NULL);
|
|
345
|
+
CREATE INDEX IF NOT EXISTS idx_hook_deliveries_target ON hook_deliveries(target_id, status, next_attempt_at);
|
|
346
|
+
CREATE TABLE IF NOT EXISTS hook_settings (id INTEGER PRIMARY KEY CHECK(id = 1), label TEXT NOT NULL);
|
|
347
|
+
`);
|
|
348
|
+
if (!db.prepare("PRAGMA table_info(hook_targets)").all().some((row) => row.name === "headers"))
|
|
349
|
+
db.exec("ALTER TABLE hook_targets ADD COLUMN headers TEXT NOT NULL DEFAULT '{}'");
|
|
350
|
+
}
|
|
351
|
+
var init_init_hook_tables = __esm({
|
|
352
|
+
"src/hooks/init_hook_tables.ts"() {
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
326
356
|
// src/questlines/init_questline_tables.ts
|
|
327
357
|
function initQuestlineTables(db) {
|
|
328
358
|
db.exec(`
|
|
@@ -707,9 +737,11 @@ function initQuestlogTables(db) {
|
|
|
707
737
|
initTagTables(db);
|
|
708
738
|
initQuestSearch(db);
|
|
709
739
|
initRumorSearch(db);
|
|
740
|
+
initHookTables(db);
|
|
710
741
|
}
|
|
711
742
|
var init_init_questlog_tables = __esm({
|
|
712
743
|
"src/init_questlog_tables.ts"() {
|
|
744
|
+
init_init_hook_tables();
|
|
713
745
|
init_init_questline_tables();
|
|
714
746
|
init_init_quest_search();
|
|
715
747
|
init_init_quest_tables();
|
|
@@ -736,7 +768,14 @@ async function openQuestlogDb(path) {
|
|
|
736
768
|
} finally {
|
|
737
769
|
process.emitWarning = original;
|
|
738
770
|
}
|
|
739
|
-
if (path !== ":memory:")
|
|
771
|
+
if (path !== ":memory:") {
|
|
772
|
+
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
773
|
+
try {
|
|
774
|
+
closeSync(openSync(path, "wx", 384));
|
|
775
|
+
} catch (error) {
|
|
776
|
+
if (error.code !== "EEXIST") throw error;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
740
779
|
const db = new sqlite.DatabaseSync(path);
|
|
741
780
|
try {
|
|
742
781
|
db.exec("PRAGMA journal_mode=WAL; PRAGMA busy_timeout=5000;");
|
|
@@ -758,7 +797,7 @@ var init_open_questlog_db = __esm({
|
|
|
758
797
|
var questlogVersion;
|
|
759
798
|
var init_version = __esm({
|
|
760
799
|
"src/version.ts"() {
|
|
761
|
-
questlogVersion = "0.
|
|
800
|
+
questlogVersion = "0.11.0";
|
|
762
801
|
}
|
|
763
802
|
});
|
|
764
803
|
|
|
@@ -1065,6 +1104,7 @@ var actionRequirements;
|
|
|
1065
1104
|
var init_action_requirements = __esm({
|
|
1066
1105
|
"src/tools/action_requirements.ts"() {
|
|
1067
1106
|
actionRequirements = {
|
|
1107
|
+
manage_hooks: { remove: ["id"] },
|
|
1068
1108
|
shape_work: {
|
|
1069
1109
|
create_quest: ["quest"],
|
|
1070
1110
|
create_questline: ["questline"],
|
|
@@ -1398,6 +1438,31 @@ var init_capture_rumor_tool = __esm({
|
|
|
1398
1438
|
}
|
|
1399
1439
|
});
|
|
1400
1440
|
|
|
1441
|
+
// src/hooks/list_deliveries.ts
|
|
1442
|
+
function listDeliveries(db, options = {}) {
|
|
1443
|
+
return db.prepare(
|
|
1444
|
+
`SELECT * FROM hook_deliveries ${options.includeDelivered ? "" : "WHERE status <> 'delivered'"} ORDER BY target_id, next_attempt_at, id`
|
|
1445
|
+
).all().map(
|
|
1446
|
+
(row) => ({
|
|
1447
|
+
id: String(row.id),
|
|
1448
|
+
targetId: Number(row.target_id),
|
|
1449
|
+
events: JSON.parse(String(row.events)),
|
|
1450
|
+
status: row.status,
|
|
1451
|
+
attempts: Number(row.attempts),
|
|
1452
|
+
lastAttemptAt: row.last_attempt_at == null ? null : Number(row.last_attempt_at),
|
|
1453
|
+
httpStatus: row.http_status == null ? null : Number(row.http_status),
|
|
1454
|
+
errorClass: row.error_class == null ? null : String(row.error_class),
|
|
1455
|
+
nextAttemptAt: Number(row.next_attempt_at)
|
|
1456
|
+
})
|
|
1457
|
+
).filter(
|
|
1458
|
+
(d) => options.questId === void 0 || d.events.some((e) => e.questId === options.questId)
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
var init_list_deliveries = __esm({
|
|
1462
|
+
"src/hooks/list_deliveries.ts"() {
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
|
|
1401
1466
|
// src/questlines/map_questline_row.ts
|
|
1402
1467
|
function mapQuestlineRow(row) {
|
|
1403
1468
|
return {
|
|
@@ -1944,7 +2009,8 @@ function inspectQuestlogItemToolHandler(db, input) {
|
|
|
1944
2009
|
detail: detailLevel === "full" ? detail : toQuestCompactDetail(detail)
|
|
1945
2010
|
},
|
|
1946
2011
|
related,
|
|
1947
|
-
detailLevel
|
|
2012
|
+
detailLevel,
|
|
2013
|
+
deliveries: listDeliveries(db, { questId: detail.id })
|
|
1948
2014
|
},
|
|
1949
2015
|
{ entities }
|
|
1950
2016
|
);
|
|
@@ -2014,6 +2080,7 @@ function inspectQuestlogItemToolHandler(db, input) {
|
|
|
2014
2080
|
var inspectQuestlogItemTool;
|
|
2015
2081
|
var init_inspect_questlog_item_tool = __esm({
|
|
2016
2082
|
"src/tools/inspect_questlog_item_tool.ts"() {
|
|
2083
|
+
init_list_deliveries();
|
|
2017
2084
|
init_get_questline_detail();
|
|
2018
2085
|
init_get_quest_detail();
|
|
2019
2086
|
init_get_repeatable_quest_or_throw();
|
|
@@ -2064,16 +2131,49 @@ var init_inspect_questlog_item_tool = __esm({
|
|
|
2064
2131
|
});
|
|
2065
2132
|
}
|
|
2066
2133
|
});
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2134
|
+
function hookLabel(db, name) {
|
|
2135
|
+
const saved = db.prepare("SELECT label FROM hook_settings WHERE id = 1").get();
|
|
2136
|
+
if (saved) return String(saved.label);
|
|
2137
|
+
const path = String(
|
|
2138
|
+
db.prepare("PRAGMA database_list").all().find((row) => row.name === "main")?.file ?? ""
|
|
2139
|
+
);
|
|
2140
|
+
return path ? basename(path, extname(path)) : "questlog";
|
|
2141
|
+
}
|
|
2142
|
+
var init_hook_label = __esm({
|
|
2143
|
+
"src/hooks/hook_label.ts"() {
|
|
2072
2144
|
}
|
|
2073
|
-
|
|
2145
|
+
});
|
|
2146
|
+
|
|
2147
|
+
// src/hooks/list_hooks.ts
|
|
2148
|
+
function listHooks(db) {
|
|
2149
|
+
return db.prepare(`SELECT id, url, headers, tag, events, min_gap, created_at, last_wake,
|
|
2150
|
+
(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,
|
|
2151
|
+
(SELECT COUNT(*) FROM hook_deliveries d WHERE d.target_id = t.id AND d.status = 'failed') AS failed
|
|
2152
|
+
FROM hook_targets t ORDER BY id`).all().map((row) => {
|
|
2153
|
+
const url = new URL(String(row.url));
|
|
2154
|
+
url.username = "";
|
|
2155
|
+
url.password = "";
|
|
2156
|
+
url.search = "";
|
|
2157
|
+
url.hash = "";
|
|
2158
|
+
return {
|
|
2159
|
+
id: Number(row.id),
|
|
2160
|
+
headers: Object.fromEntries(
|
|
2161
|
+
Object.keys(JSON.parse(String(row.headers))).map((name) => [name, "***"])
|
|
2162
|
+
),
|
|
2163
|
+
scope: row.tag == null ? "database" : "tag",
|
|
2164
|
+
url: url.href,
|
|
2165
|
+
tag: row.tag == null ? null : String(row.tag),
|
|
2166
|
+
events: JSON.parse(String(row.events)),
|
|
2167
|
+
minGap: Number(row.min_gap),
|
|
2168
|
+
createdAt: Number(row.created_at),
|
|
2169
|
+
lastWake: row.last_wake == null ? null : Number(row.last_wake),
|
|
2170
|
+
pending: Number(row.pending),
|
|
2171
|
+
failed: Number(row.failed)
|
|
2172
|
+
};
|
|
2173
|
+
});
|
|
2074
2174
|
}
|
|
2075
|
-
var
|
|
2076
|
-
"src/
|
|
2175
|
+
var init_list_hooks = __esm({
|
|
2176
|
+
"src/hooks/list_hooks.ts"() {
|
|
2077
2177
|
}
|
|
2078
2178
|
});
|
|
2079
2179
|
|
|
@@ -2117,6 +2217,172 @@ var init_with_transaction = __esm({
|
|
|
2117
2217
|
}
|
|
2118
2218
|
});
|
|
2119
2219
|
|
|
2220
|
+
// src/hooks/remove_hook.ts
|
|
2221
|
+
function removeHook(db, id) {
|
|
2222
|
+
withTransaction(db, () => {
|
|
2223
|
+
db.prepare("DELETE FROM hook_assignments WHERE target_id = ?").run(id);
|
|
2224
|
+
db.prepare("DELETE FROM hook_deliveries WHERE target_id = ?").run(id);
|
|
2225
|
+
db.prepare("DELETE FROM hook_targets WHERE id = ?").run(id);
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
var init_remove_hook = __esm({
|
|
2229
|
+
"src/hooks/remove_hook.ts"() {
|
|
2230
|
+
init_with_transaction();
|
|
2231
|
+
}
|
|
2232
|
+
});
|
|
2233
|
+
async function sendHookDelivery(db, delivery, target, options) {
|
|
2234
|
+
const now = options.now ?? options.clock?.() ?? Date.now();
|
|
2235
|
+
const timestamp = String(Math.floor(now / 1e3));
|
|
2236
|
+
const body = JSON.stringify({
|
|
2237
|
+
type: "questlog.events",
|
|
2238
|
+
db: hookLabel(db),
|
|
2239
|
+
deliveryId: delivery.id,
|
|
2240
|
+
sentAt: new Date(now).toISOString(),
|
|
2241
|
+
events: delivery.events
|
|
2242
|
+
});
|
|
2243
|
+
const signature = createHmac("sha256", target.secret).update(`${timestamp}.${body}`).digest("hex");
|
|
2244
|
+
let status = null;
|
|
2245
|
+
let errorClass = null;
|
|
2246
|
+
try {
|
|
2247
|
+
const response = await (options.fetch ?? globalThis.fetch)(target.url, {
|
|
2248
|
+
method: "POST",
|
|
2249
|
+
redirect: "error",
|
|
2250
|
+
headers: {
|
|
2251
|
+
"user-agent": `questlog/${questlogVersion}`,
|
|
2252
|
+
...JSON.parse(
|
|
2253
|
+
String(
|
|
2254
|
+
db.prepare("SELECT headers FROM hook_targets WHERE id = ?").get(target.id)?.headers ?? "{}"
|
|
2255
|
+
)
|
|
2256
|
+
),
|
|
2257
|
+
"content-type": "application/json",
|
|
2258
|
+
"x-questlog-delivery": delivery.id,
|
|
2259
|
+
"x-questlog-timestamp": timestamp,
|
|
2260
|
+
"x-questlog-signature": `sha256=${signature}`
|
|
2261
|
+
},
|
|
2262
|
+
body,
|
|
2263
|
+
signal: AbortSignal.timeout(Math.max(1, Math.min(options.timeoutMs ?? 5e3, 5e3)))
|
|
2264
|
+
});
|
|
2265
|
+
status = response.status;
|
|
2266
|
+
await response.body?.cancel();
|
|
2267
|
+
} catch (error) {
|
|
2268
|
+
errorClass = error instanceof Error && ["TimeoutError", "AbortError"].includes(error.name) ? error.name : "NetworkError";
|
|
2269
|
+
}
|
|
2270
|
+
const delivered = status != null && status >= 200 && status < 300;
|
|
2271
|
+
const completedAt = options.now ?? options.clock?.() ?? Date.now();
|
|
2272
|
+
withTransaction(db, () => {
|
|
2273
|
+
if (!db.prepare("SELECT id FROM hook_targets WHERE id = ? AND claim = ?").get(target.id, target.claim))
|
|
2274
|
+
return;
|
|
2275
|
+
const delay = Math.min(36e5, 3e4 * 2 ** (delivery.attempts - 1));
|
|
2276
|
+
const jitter = Number.parseInt(randomBytes(2).toString("hex"), 16) % 1e3;
|
|
2277
|
+
db.prepare(
|
|
2278
|
+
"UPDATE hook_deliveries SET status = ?, http_status = ?, error_class = ?, next_attempt_at = ? WHERE id = ?"
|
|
2279
|
+
).run(
|
|
2280
|
+
delivered ? "delivered" : delivery.attempts >= 8 ? "failed" : "pending",
|
|
2281
|
+
status,
|
|
2282
|
+
errorClass,
|
|
2283
|
+
completedAt + delay + jitter,
|
|
2284
|
+
delivery.id
|
|
2285
|
+
);
|
|
2286
|
+
db.prepare(
|
|
2287
|
+
"UPDATE hook_targets SET claim = NULL, lease_until = NULL WHERE id = ? AND claim = ?"
|
|
2288
|
+
).run(target.id, target.claim);
|
|
2289
|
+
db.prepare(
|
|
2290
|
+
"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)"
|
|
2291
|
+
).run(target.id, target.id);
|
|
2292
|
+
});
|
|
2293
|
+
return delivered;
|
|
2294
|
+
}
|
|
2295
|
+
var init_send_hook_delivery = __esm({
|
|
2296
|
+
"src/hooks/send_hook_delivery.ts"() {
|
|
2297
|
+
init_version();
|
|
2298
|
+
init_with_transaction();
|
|
2299
|
+
init_hook_label();
|
|
2300
|
+
}
|
|
2301
|
+
});
|
|
2302
|
+
|
|
2303
|
+
// src/quests/get_quest_or_throw.ts
|
|
2304
|
+
function getQuestOrThrow(db, questId) {
|
|
2305
|
+
const row = db.prepare("SELECT * FROM quests WHERE id = ? AND deleted_at IS NULL").get(questId);
|
|
2306
|
+
if (!row) {
|
|
2307
|
+
throw new Error(`Quest ${questId} was not found.`);
|
|
2308
|
+
}
|
|
2309
|
+
return mapQuestRow(row);
|
|
2310
|
+
}
|
|
2311
|
+
var init_get_quest_or_throw = __esm({
|
|
2312
|
+
"src/quests/get_quest_or_throw.ts"() {
|
|
2313
|
+
init_map_quest_row();
|
|
2314
|
+
}
|
|
2315
|
+
});
|
|
2316
|
+
|
|
2317
|
+
// src/rewards/copy_repeatable_quest_rewards_to_quest.ts
|
|
2318
|
+
function copyRepeatableQuestRewardsToQuest(db, repeatableQuestId, questId, now) {
|
|
2319
|
+
const effectiveNow = resolveNow(now);
|
|
2320
|
+
const rows = db.prepare(
|
|
2321
|
+
`SELECT kind, name, description, quantity
|
|
2322
|
+
FROM repeatable_quest_rewards
|
|
2323
|
+
WHERE repeatable_quest_id = ? AND deleted_at IS NULL`
|
|
2324
|
+
).all(repeatableQuestId);
|
|
2325
|
+
for (const row of rows) {
|
|
2326
|
+
db.prepare(
|
|
2327
|
+
`INSERT INTO quest_rewards (quest_id, kind, name, description, quantity, created_at)
|
|
2328
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
2329
|
+
).run(
|
|
2330
|
+
questId,
|
|
2331
|
+
String(row.kind),
|
|
2332
|
+
String(row.name),
|
|
2333
|
+
row.description == null ? null : String(row.description),
|
|
2334
|
+
row.quantity == null ? null : Number(row.quantity),
|
|
2335
|
+
effectiveNow
|
|
2336
|
+
);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
var init_copy_repeatable_quest_rewards_to_quest = __esm({
|
|
2340
|
+
"src/rewards/copy_repeatable_quest_rewards_to_quest.ts"() {
|
|
2341
|
+
init_resolve_now();
|
|
2342
|
+
}
|
|
2343
|
+
});
|
|
2344
|
+
|
|
2345
|
+
// src/tags/copy_repeatable_quest_tags_to_quest.ts
|
|
2346
|
+
function copyRepeatableQuestTagsToQuest(db, repeatableQuestId, questId, now) {
|
|
2347
|
+
const effectiveNow = resolveNow(now);
|
|
2348
|
+
const rows = db.prepare(
|
|
2349
|
+
`SELECT tag_id
|
|
2350
|
+
FROM repeatable_quest_tags
|
|
2351
|
+
WHERE repeatable_quest_id = ? AND deleted_at IS NULL`
|
|
2352
|
+
).all(repeatableQuestId);
|
|
2353
|
+
for (const row of rows) {
|
|
2354
|
+
db.prepare(
|
|
2355
|
+
`INSERT INTO quest_tags (quest_id, tag_id, created_at)
|
|
2356
|
+
VALUES (?, ?, ?)`
|
|
2357
|
+
).run(questId, Number(row.tag_id), effectiveNow);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
var init_copy_repeatable_quest_tags_to_quest = __esm({
|
|
2361
|
+
"src/tags/copy_repeatable_quest_tags_to_quest.ts"() {
|
|
2362
|
+
init_resolve_now();
|
|
2363
|
+
}
|
|
2364
|
+
});
|
|
2365
|
+
|
|
2366
|
+
// src/repeatable_quests/advance_anchor.ts
|
|
2367
|
+
function advanceAnchor(anchorAt, freq, interval) {
|
|
2368
|
+
const date = new Date(anchorAt);
|
|
2369
|
+
if (freq === "MONTHLY") {
|
|
2370
|
+
date.setUTCMonth(date.getUTCMonth() + interval);
|
|
2371
|
+
return date.getTime();
|
|
2372
|
+
}
|
|
2373
|
+
if (freq === "YEARLY") {
|
|
2374
|
+
date.setUTCFullYear(date.getUTCFullYear() + interval);
|
|
2375
|
+
return date.getTime();
|
|
2376
|
+
}
|
|
2377
|
+
return anchorAt + interval * WEEK_MS;
|
|
2378
|
+
}
|
|
2379
|
+
var WEEK_MS;
|
|
2380
|
+
var init_advance_anchor = __esm({
|
|
2381
|
+
"src/repeatable_quests/advance_anchor.ts"() {
|
|
2382
|
+
WEEK_MS = 7 * 864e5;
|
|
2383
|
+
}
|
|
2384
|
+
});
|
|
2385
|
+
|
|
2120
2386
|
// src/repeatable_quests/parse_rrule.ts
|
|
2121
2387
|
function parseRRule(rrule) {
|
|
2122
2388
|
const parts = Object.fromEntries(
|
|
@@ -2165,290 +2431,46 @@ var init_parse_rrule = __esm({
|
|
|
2165
2431
|
}
|
|
2166
2432
|
});
|
|
2167
2433
|
|
|
2168
|
-
// src/repeatable_quests/
|
|
2169
|
-
function
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
const now = resolveNow(input.now);
|
|
2173
|
-
const rrule = input.rrule ?? current.rrule;
|
|
2174
|
-
parseRRule(rrule);
|
|
2175
|
-
db.prepare(
|
|
2176
|
-
`UPDATE repeatable_quests
|
|
2177
|
-
SET questline_id = ?,
|
|
2178
|
-
title = ?,
|
|
2179
|
-
objective = ?,
|
|
2180
|
-
rrule = ?,
|
|
2181
|
-
anchor_at = ?,
|
|
2182
|
-
not_before_offset_seconds = ?,
|
|
2183
|
-
due_offset_seconds = ?,
|
|
2184
|
-
scheduled_start_offset_seconds = ?,
|
|
2185
|
-
scheduled_end_offset_seconds = ?,
|
|
2186
|
-
all_day = ?,
|
|
2187
|
-
estimate_seconds = ?,
|
|
2188
|
-
archived_at = ?,
|
|
2189
|
-
updated_at = ?
|
|
2190
|
-
WHERE id = ? AND deleted_at IS NULL`
|
|
2191
|
-
).run(
|
|
2192
|
-
input.questlineId === void 0 ? current.questlineId : input.questlineId,
|
|
2193
|
-
input.title == null ? current.title : assertNonEmpty(input.title, "Repeatable quest title"),
|
|
2194
|
-
input.objective == null ? current.objective : assertNonEmpty(input.objective, "Repeatable quest objective"),
|
|
2195
|
-
rrule,
|
|
2196
|
-
input.anchorAt ?? current.anchorAt,
|
|
2197
|
-
input.notBeforeOffsetSeconds === void 0 ? current.notBeforeOffsetSeconds : assertNonNegativeSeconds(input.notBeforeOffsetSeconds, "Repeatable not-before offset") ?? null,
|
|
2198
|
-
input.dueOffsetSeconds === void 0 ? current.dueOffsetSeconds : assertNonNegativeSeconds(input.dueOffsetSeconds, "Repeatable due offset") ?? null,
|
|
2199
|
-
input.scheduledStartOffsetSeconds === void 0 ? current.scheduledStartOffsetSeconds : input.scheduledStartOffsetSeconds,
|
|
2200
|
-
input.scheduledEndOffsetSeconds === void 0 ? current.scheduledEndOffsetSeconds : input.scheduledEndOffsetSeconds,
|
|
2201
|
-
input.allDay === void 0 ? current.allDay ? 1 : 0 : input.allDay ? 1 : 0,
|
|
2202
|
-
input.estimateSeconds === void 0 ? current.estimateSeconds : assertNonNegativeSeconds(input.estimateSeconds, "Repeatable estimate") ?? null,
|
|
2203
|
-
input.archivedAt === void 0 ? current.archivedAt : input.archivedAt,
|
|
2204
|
-
now,
|
|
2205
|
-
repeatableQuestId
|
|
2206
|
-
);
|
|
2207
|
-
return getRepeatableQuestOrThrow(db, repeatableQuestId);
|
|
2208
|
-
});
|
|
2209
|
-
}
|
|
2210
|
-
var init_update_repeatable_quest = __esm({
|
|
2211
|
-
"src/repeatable_quests/update_repeatable_quest.ts"() {
|
|
2212
|
-
init_assert_non_empty();
|
|
2213
|
-
init_assert_non_negative_seconds();
|
|
2214
|
-
init_resolve_now();
|
|
2215
|
-
init_with_transaction();
|
|
2216
|
-
init_get_repeatable_quest_or_throw();
|
|
2217
|
-
init_parse_rrule();
|
|
2218
|
-
}
|
|
2219
|
-
});
|
|
2220
|
-
|
|
2221
|
-
// src/repeatable_quests/archive_repeatable_quest.ts
|
|
2222
|
-
function archiveRepeatableQuest(db, repeatableQuestId, archivedAt) {
|
|
2223
|
-
return updateRepeatableQuest(db, repeatableQuestId, { archivedAt: resolveNow(archivedAt) });
|
|
2434
|
+
// src/repeatable_quests/start_of_utc_day.ts
|
|
2435
|
+
function startOfUtcDay(timestamp) {
|
|
2436
|
+
const date = new Date(timestamp);
|
|
2437
|
+
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
|
2224
2438
|
}
|
|
2225
|
-
var
|
|
2226
|
-
"src/repeatable_quests/
|
|
2227
|
-
init_resolve_now();
|
|
2228
|
-
init_update_repeatable_quest();
|
|
2439
|
+
var init_start_of_utc_day = __esm({
|
|
2440
|
+
"src/repeatable_quests/start_of_utc_day.ts"() {
|
|
2229
2441
|
}
|
|
2230
2442
|
});
|
|
2231
2443
|
|
|
2232
|
-
// src/
|
|
2233
|
-
function
|
|
2234
|
-
const
|
|
2235
|
-
|
|
2236
|
-
throw new Error(`${label} ${id} was not found.`);
|
|
2237
|
-
}
|
|
2444
|
+
// src/repeatable_quests/start_of_utc_week.ts
|
|
2445
|
+
function startOfUtcWeek(dayStart) {
|
|
2446
|
+
const date = new Date(dayStart);
|
|
2447
|
+
return dayStart - date.getUTCDay() * DAY_MS;
|
|
2238
2448
|
}
|
|
2239
|
-
var
|
|
2240
|
-
|
|
2449
|
+
var DAY_MS;
|
|
2450
|
+
var init_start_of_utc_week = __esm({
|
|
2451
|
+
"src/repeatable_quests/start_of_utc_week.ts"() {
|
|
2452
|
+
DAY_MS = 864e5;
|
|
2241
2453
|
}
|
|
2242
2454
|
});
|
|
2243
2455
|
|
|
2244
|
-
// src/
|
|
2245
|
-
function
|
|
2246
|
-
|
|
2247
|
-
|
|
2456
|
+
// src/repeatable_quests/generate_anchors_until.ts
|
|
2457
|
+
function generateAnchorsUntil(repeatable, now) {
|
|
2458
|
+
const rule = parseRRule(repeatable.rrule);
|
|
2459
|
+
const anchors = [];
|
|
2460
|
+
const until = rule.until == null ? now : Math.min(now, rule.until);
|
|
2461
|
+
if (repeatable.anchorAt > until) {
|
|
2462
|
+
return anchors;
|
|
2248
2463
|
}
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
db.prepare(
|
|
2260
|
-
"UPDATE repeatable_quest_rewards SET deleted_at = ? WHERE repeatable_quest_id = ? AND deleted_at IS NULL"
|
|
2261
|
-
).run(effectiveNow, repeatableQuestId);
|
|
2262
|
-
for (const reward of rewards) {
|
|
2263
|
-
db.prepare(
|
|
2264
|
-
`INSERT INTO repeatable_quest_rewards (
|
|
2265
|
-
repeatable_quest_id,
|
|
2266
|
-
kind,
|
|
2267
|
-
name,
|
|
2268
|
-
description,
|
|
2269
|
-
quantity,
|
|
2270
|
-
created_at
|
|
2271
|
-
) VALUES (?, ?, ?, ?, ?, ?)`
|
|
2272
|
-
).run(
|
|
2273
|
-
repeatableQuestId,
|
|
2274
|
-
assertNonEmpty(reward.kind, "Reward kind"),
|
|
2275
|
-
assertNonEmpty(reward.name, "Reward name"),
|
|
2276
|
-
reward.description?.trim() || null,
|
|
2277
|
-
assertNonNegativeNumber(reward.quantity, "Reward quantity") ?? null,
|
|
2278
|
-
effectiveNow
|
|
2279
|
-
);
|
|
2280
|
-
}
|
|
2281
|
-
}
|
|
2282
|
-
var init_replace_active_repeatable_quest_rewards = __esm({
|
|
2283
|
-
"src/rewards/replace_active_repeatable_quest_rewards.ts"() {
|
|
2284
|
-
init_assert_non_empty();
|
|
2285
|
-
init_assert_non_negative_number();
|
|
2286
|
-
init_resolve_now();
|
|
2287
|
-
}
|
|
2288
|
-
});
|
|
2289
|
-
|
|
2290
|
-
// src/tags/replace_active_repeatable_quest_tags.ts
|
|
2291
|
-
function getOrCreateTagId(db, name, now) {
|
|
2292
|
-
const normalizedName = normalizeTagName(name);
|
|
2293
|
-
if (!normalizedName) {
|
|
2294
|
-
throw new Error("Tag name must not be empty.");
|
|
2295
|
-
}
|
|
2296
|
-
const existing = db.prepare("SELECT id FROM tags WHERE normalized_name = ? AND deleted_at IS NULL").get(normalizedName);
|
|
2297
|
-
if (existing) {
|
|
2298
|
-
return Number(existing.id);
|
|
2299
|
-
}
|
|
2300
|
-
const created = db.prepare(
|
|
2301
|
-
`INSERT INTO tags (name, normalized_name, created_at)
|
|
2302
|
-
VALUES (?, ?, ?)`
|
|
2303
|
-
).run(name.trim(), normalizedName, now);
|
|
2304
|
-
return Number(created.lastInsertRowid);
|
|
2305
|
-
}
|
|
2306
|
-
function replaceActiveRepeatableQuestTags(db, repeatableQuestId, tagNames, now) {
|
|
2307
|
-
const effectiveNow = resolveNow(now);
|
|
2308
|
-
db.prepare(
|
|
2309
|
-
"UPDATE repeatable_quest_tags SET deleted_at = ? WHERE repeatable_quest_id = ? AND deleted_at IS NULL"
|
|
2310
|
-
).run(effectiveNow, repeatableQuestId);
|
|
2311
|
-
const uniqueNames = [...new Set(tagNames.map((tag) => tag.trim()).filter(Boolean))];
|
|
2312
|
-
for (const name of uniqueNames) {
|
|
2313
|
-
const tagId = getOrCreateTagId(db, name, effectiveNow);
|
|
2314
|
-
db.prepare(
|
|
2315
|
-
`INSERT INTO repeatable_quest_tags (repeatable_quest_id, tag_id, created_at)
|
|
2316
|
-
VALUES (?, ?, ?)`
|
|
2317
|
-
).run(repeatableQuestId, tagId, effectiveNow);
|
|
2318
|
-
}
|
|
2319
|
-
}
|
|
2320
|
-
var init_replace_active_repeatable_quest_tags = __esm({
|
|
2321
|
-
"src/tags/replace_active_repeatable_quest_tags.ts"() {
|
|
2322
|
-
init_resolve_now();
|
|
2323
|
-
init_normalize_tag_name();
|
|
2324
|
-
}
|
|
2325
|
-
});
|
|
2326
|
-
|
|
2327
|
-
// src/repeatable_quests/create_repeatable_quest.ts
|
|
2328
|
-
function createRepeatableQuest(db, input) {
|
|
2329
|
-
return withTransaction(db, () => {
|
|
2330
|
-
const now = resolveNow(input.now);
|
|
2331
|
-
const title = assertNonEmpty(input.title, "Repeatable quest title");
|
|
2332
|
-
const objective = assertNonEmpty(input.objective, "Repeatable quest objective");
|
|
2333
|
-
parseRRule(input.rrule);
|
|
2334
|
-
if (input.questlineId != null) {
|
|
2335
|
-
assertActiveRowExists(db, "questlines", input.questlineId, "Questline");
|
|
2336
|
-
}
|
|
2337
|
-
const created = db.prepare(
|
|
2338
|
-
`INSERT INTO repeatable_quests (
|
|
2339
|
-
questline_id,
|
|
2340
|
-
title,
|
|
2341
|
-
objective,
|
|
2342
|
-
rrule,
|
|
2343
|
-
anchor_at,
|
|
2344
|
-
not_before_offset_seconds,
|
|
2345
|
-
due_offset_seconds,
|
|
2346
|
-
scheduled_start_offset_seconds,
|
|
2347
|
-
scheduled_end_offset_seconds,
|
|
2348
|
-
all_day,
|
|
2349
|
-
estimate_seconds,
|
|
2350
|
-
created_at,
|
|
2351
|
-
updated_at
|
|
2352
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
2353
|
-
).run(
|
|
2354
|
-
input.questlineId ?? null,
|
|
2355
|
-
title,
|
|
2356
|
-
objective,
|
|
2357
|
-
input.rrule,
|
|
2358
|
-
input.anchorAt,
|
|
2359
|
-
assertNonNegativeSeconds(input.notBeforeOffsetSeconds, "Repeatable not-before offset") ?? null,
|
|
2360
|
-
assertNonNegativeSeconds(input.dueOffsetSeconds, "Repeatable due offset") ?? null,
|
|
2361
|
-
input.scheduledStartOffsetSeconds ?? null,
|
|
2362
|
-
input.scheduledEndOffsetSeconds ?? null,
|
|
2363
|
-
input.allDay ? 1 : 0,
|
|
2364
|
-
assertNonNegativeSeconds(input.estimateSeconds, "Repeatable estimate") ?? null,
|
|
2365
|
-
now,
|
|
2366
|
-
now
|
|
2367
|
-
);
|
|
2368
|
-
const repeatableQuestId = Number(created.lastInsertRowid);
|
|
2369
|
-
if (input.tags && input.tags.length > 0) {
|
|
2370
|
-
replaceActiveRepeatableQuestTags(db, repeatableQuestId, input.tags, now);
|
|
2371
|
-
}
|
|
2372
|
-
if (input.rewards && input.rewards.length > 0) {
|
|
2373
|
-
replaceActiveRepeatableQuestRewards(db, repeatableQuestId, input.rewards, now);
|
|
2374
|
-
}
|
|
2375
|
-
return getRepeatableQuestOrThrow(db, repeatableQuestId);
|
|
2376
|
-
});
|
|
2377
|
-
}
|
|
2378
|
-
var init_create_repeatable_quest = __esm({
|
|
2379
|
-
"src/repeatable_quests/create_repeatable_quest.ts"() {
|
|
2380
|
-
init_assert_active_row_exists();
|
|
2381
|
-
init_assert_non_empty();
|
|
2382
|
-
init_assert_non_negative_seconds();
|
|
2383
|
-
init_resolve_now();
|
|
2384
|
-
init_replace_active_repeatable_quest_rewards();
|
|
2385
|
-
init_replace_active_repeatable_quest_tags();
|
|
2386
|
-
init_with_transaction();
|
|
2387
|
-
init_get_repeatable_quest_or_throw();
|
|
2388
|
-
init_parse_rrule();
|
|
2389
|
-
}
|
|
2390
|
-
});
|
|
2391
|
-
|
|
2392
|
-
// src/repeatable_quests/advance_anchor.ts
|
|
2393
|
-
function advanceAnchor(anchorAt, freq, interval) {
|
|
2394
|
-
const date = new Date(anchorAt);
|
|
2395
|
-
if (freq === "MONTHLY") {
|
|
2396
|
-
date.setUTCMonth(date.getUTCMonth() + interval);
|
|
2397
|
-
return date.getTime();
|
|
2398
|
-
}
|
|
2399
|
-
if (freq === "YEARLY") {
|
|
2400
|
-
date.setUTCFullYear(date.getUTCFullYear() + interval);
|
|
2401
|
-
return date.getTime();
|
|
2402
|
-
}
|
|
2403
|
-
return anchorAt + interval * WEEK_MS;
|
|
2404
|
-
}
|
|
2405
|
-
var WEEK_MS;
|
|
2406
|
-
var init_advance_anchor = __esm({
|
|
2407
|
-
"src/repeatable_quests/advance_anchor.ts"() {
|
|
2408
|
-
WEEK_MS = 7 * 864e5;
|
|
2409
|
-
}
|
|
2410
|
-
});
|
|
2411
|
-
|
|
2412
|
-
// src/repeatable_quests/start_of_utc_day.ts
|
|
2413
|
-
function startOfUtcDay(timestamp) {
|
|
2414
|
-
const date = new Date(timestamp);
|
|
2415
|
-
return Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate());
|
|
2416
|
-
}
|
|
2417
|
-
var init_start_of_utc_day = __esm({
|
|
2418
|
-
"src/repeatable_quests/start_of_utc_day.ts"() {
|
|
2419
|
-
}
|
|
2420
|
-
});
|
|
2421
|
-
|
|
2422
|
-
// src/repeatable_quests/start_of_utc_week.ts
|
|
2423
|
-
function startOfUtcWeek(dayStart) {
|
|
2424
|
-
const date = new Date(dayStart);
|
|
2425
|
-
return dayStart - date.getUTCDay() * DAY_MS;
|
|
2426
|
-
}
|
|
2427
|
-
var DAY_MS;
|
|
2428
|
-
var init_start_of_utc_week = __esm({
|
|
2429
|
-
"src/repeatable_quests/start_of_utc_week.ts"() {
|
|
2430
|
-
DAY_MS = 864e5;
|
|
2431
|
-
}
|
|
2432
|
-
});
|
|
2433
|
-
|
|
2434
|
-
// src/repeatable_quests/generate_anchors_until.ts
|
|
2435
|
-
function generateAnchorsUntil(repeatable, now) {
|
|
2436
|
-
const rule = parseRRule(repeatable.rrule);
|
|
2437
|
-
const anchors = [];
|
|
2438
|
-
const until = rule.until == null ? now : Math.min(now, rule.until);
|
|
2439
|
-
if (repeatable.anchorAt > until) {
|
|
2440
|
-
return anchors;
|
|
2441
|
-
}
|
|
2442
|
-
if (rule.freq === "DAILY") {
|
|
2443
|
-
let occurrence2 = 0;
|
|
2444
|
-
for (let current2 = repeatable.anchorAt; current2 <= until; current2 += rule.interval * DAY_MS2) {
|
|
2445
|
-
occurrence2 += 1;
|
|
2446
|
-
if (rule.count != null && occurrence2 > rule.count) {
|
|
2447
|
-
break;
|
|
2448
|
-
}
|
|
2449
|
-
anchors.push(current2);
|
|
2450
|
-
}
|
|
2451
|
-
return anchors;
|
|
2464
|
+
if (rule.freq === "DAILY") {
|
|
2465
|
+
let occurrence2 = 0;
|
|
2466
|
+
for (let current2 = repeatable.anchorAt; current2 <= until; current2 += rule.interval * DAY_MS2) {
|
|
2467
|
+
occurrence2 += 1;
|
|
2468
|
+
if (rule.count != null && occurrence2 > rule.count) {
|
|
2469
|
+
break;
|
|
2470
|
+
}
|
|
2471
|
+
anchors.push(current2);
|
|
2472
|
+
}
|
|
2473
|
+
return anchors;
|
|
2452
2474
|
}
|
|
2453
2475
|
if (rule.freq === "WEEKLY" && rule.byDay && rule.byDay.length > 0) {
|
|
2454
2476
|
const anchorDayStart = startOfUtcDay(repeatable.anchorAt);
|
|
@@ -2550,143 +2572,559 @@ var init_list_due_repeatable_quest_anchors = __esm({
|
|
|
2550
2572
|
}
|
|
2551
2573
|
});
|
|
2552
2574
|
|
|
2553
|
-
// src/repeatable_quests/
|
|
2554
|
-
function
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2575
|
+
// src/repeatable_quests/spawn_due_repeatable_quests.ts
|
|
2576
|
+
function spawnDueRepeatableQuests(db, now) {
|
|
2577
|
+
return withTransaction(db, () => {
|
|
2578
|
+
const anchors = listDueRepeatableQuestAnchors(db, now);
|
|
2579
|
+
const quests = [];
|
|
2580
|
+
for (const anchor of anchors) {
|
|
2581
|
+
const repeatable = getRepeatableQuestOrThrow(db, anchor.repeatableQuestId);
|
|
2582
|
+
const created = db.prepare(
|
|
2583
|
+
`INSERT INTO quests (
|
|
2584
|
+
questline_id,
|
|
2585
|
+
source_rumor_id,
|
|
2586
|
+
spawned_by_quest_id,
|
|
2587
|
+
spawned_from_repeatable_id,
|
|
2588
|
+
spawned_for_at,
|
|
2589
|
+
title,
|
|
2590
|
+
objective,
|
|
2591
|
+
created_at,
|
|
2592
|
+
updated_at,
|
|
2593
|
+
effort_seconds,
|
|
2594
|
+
estimate_seconds,
|
|
2595
|
+
not_before_at,
|
|
2596
|
+
due_at,
|
|
2597
|
+
scheduled_start_at,
|
|
2598
|
+
scheduled_end_at,
|
|
2599
|
+
all_day
|
|
2600
|
+
) VALUES (?, NULL, NULL, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?)`
|
|
2601
|
+
).run(
|
|
2602
|
+
repeatable.questlineId ?? null,
|
|
2603
|
+
repeatable.id,
|
|
2604
|
+
anchor.anchorAt,
|
|
2605
|
+
repeatable.title,
|
|
2606
|
+
repeatable.objective,
|
|
2607
|
+
now,
|
|
2608
|
+
now,
|
|
2609
|
+
repeatable.estimateSeconds ?? null,
|
|
2610
|
+
repeatable.notBeforeOffsetSeconds == null ? null : anchor.anchorAt + repeatable.notBeforeOffsetSeconds * 1e3,
|
|
2611
|
+
repeatable.dueOffsetSeconds == null ? null : anchor.anchorAt + repeatable.dueOffsetSeconds * 1e3,
|
|
2612
|
+
repeatable.scheduledStartOffsetSeconds == null ? null : anchor.anchorAt + repeatable.scheduledStartOffsetSeconds * 1e3,
|
|
2613
|
+
repeatable.scheduledEndOffsetSeconds == null ? null : anchor.anchorAt + repeatable.scheduledEndOffsetSeconds * 1e3,
|
|
2614
|
+
repeatable.allDay ? 1 : 0
|
|
2615
|
+
);
|
|
2616
|
+
const questId = Number(created.lastInsertRowid);
|
|
2617
|
+
copyRepeatableQuestTagsToQuest(db, repeatable.id, questId, now);
|
|
2618
|
+
copyRepeatableQuestRewardsToQuest(db, repeatable.id, questId, now);
|
|
2619
|
+
quests.push(getQuestOrThrow(db, questId));
|
|
2620
|
+
}
|
|
2621
|
+
return quests;
|
|
2622
|
+
});
|
|
2623
|
+
}
|
|
2624
|
+
var init_spawn_due_repeatable_quests = __esm({
|
|
2625
|
+
"src/repeatable_quests/spawn_due_repeatable_quests.ts"() {
|
|
2626
|
+
init_get_quest_or_throw();
|
|
2627
|
+
init_copy_repeatable_quest_rewards_to_quest();
|
|
2628
|
+
init_copy_repeatable_quest_tags_to_quest();
|
|
2629
|
+
init_with_transaction();
|
|
2630
|
+
init_get_repeatable_quest_or_throw();
|
|
2631
|
+
init_list_due_repeatable_quest_anchors();
|
|
2632
|
+
}
|
|
2633
|
+
});
|
|
2634
|
+
|
|
2635
|
+
// src/hooks/detect_hook_events.ts
|
|
2636
|
+
function detectHookEvents(db, now) {
|
|
2637
|
+
const events = [];
|
|
2638
|
+
const add = (type, at, ids) => {
|
|
2639
|
+
events.push({
|
|
2640
|
+
id: `e_${type}_${ids.questId ?? ids.rumorId}_${ids.repeatableId ?? ""}_${at}`,
|
|
2641
|
+
type,
|
|
2642
|
+
...ids,
|
|
2643
|
+
occurredAt: new Date(at).toISOString()
|
|
2644
|
+
});
|
|
2645
|
+
};
|
|
2646
|
+
for (const { detail: q } of queryQuestDetailRecords(db, {}, now)) {
|
|
2647
|
+
if (q.resolvedAt == null && q.effectiveDueAt != null && q.effectiveDueAt <= now) {
|
|
2648
|
+
add("quest.due", q.effectiveDueAt, { questId: q.id });
|
|
2649
|
+
if (q.effectiveDueAt < now) add("quest.overdue", q.effectiveDueAt, { questId: q.id });
|
|
2650
|
+
}
|
|
2651
|
+
if (q.available) {
|
|
2652
|
+
const prerequisites = db.prepare(
|
|
2653
|
+
`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`
|
|
2654
|
+
).all(q.id);
|
|
2655
|
+
if (prerequisites.some((p) => Number(p.resolved_at) >= Number(p.created_at))) {
|
|
2656
|
+
const at = Math.max(q.notBeforeAt ?? 0, ...prerequisites.map((p) => Number(p.resolved_at)));
|
|
2657
|
+
if (at <= now) add("quest.unblocked", at, { questId: q.id });
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
if (q.spawnedFromRepeatableId != null && q.createdAt <= now)
|
|
2661
|
+
add("repeatable.spawned", q.createdAt, {
|
|
2662
|
+
questId: q.id,
|
|
2663
|
+
repeatableId: q.spawnedFromRepeatableId
|
|
2664
|
+
});
|
|
2665
|
+
}
|
|
2666
|
+
for (const row of db.prepare("SELECT id, created_at FROM rumors WHERE deleted_at IS NULL AND created_at <= ?").all(now))
|
|
2667
|
+
add("rumor.captured", Number(row.created_at), { rumorId: Number(row.id) });
|
|
2668
|
+
return events.sort(
|
|
2669
|
+
(a, b) => a.occurredAt.localeCompare(b.occurredAt) || a.id.localeCompare(b.id)
|
|
2670
|
+
);
|
|
2671
|
+
}
|
|
2672
|
+
var init_detect_hook_events = __esm({
|
|
2673
|
+
"src/hooks/detect_hook_events.ts"() {
|
|
2674
|
+
init_query_quest_detail_records();
|
|
2675
|
+
}
|
|
2676
|
+
});
|
|
2677
|
+
async function tickQuestlog(db, options = {}) {
|
|
2678
|
+
const now = options.now ?? options.clock?.() ?? Date.now();
|
|
2679
|
+
const result = {
|
|
2680
|
+
events: [],
|
|
2681
|
+
batches: [],
|
|
2682
|
+
sent: 0,
|
|
2683
|
+
queued: 0,
|
|
2684
|
+
retrying: 0,
|
|
2685
|
+
failed: 0
|
|
2686
|
+
};
|
|
2687
|
+
const claims = [];
|
|
2688
|
+
const collect = () => {
|
|
2689
|
+
spawnDueRepeatableQuests(db, now);
|
|
2690
|
+
const detected = detectHookEvents(db, now);
|
|
2691
|
+
result.events = detected.filter(
|
|
2692
|
+
(e) => !db.prepare("SELECT id FROM hook_events WHERE id = ?").get(e.id)
|
|
2693
|
+
);
|
|
2694
|
+
for (const event of result.events)
|
|
2695
|
+
db.prepare("INSERT INTO hook_events(id, occurred_at, event) VALUES (?, ?, ?)").run(
|
|
2696
|
+
event.id,
|
|
2697
|
+
Date.parse(event.occurredAt),
|
|
2698
|
+
JSON.stringify(event)
|
|
2699
|
+
);
|
|
2700
|
+
for (const target of listHooks(db)) {
|
|
2701
|
+
const events = db.prepare(
|
|
2702
|
+
"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"
|
|
2703
|
+
).all(target.createdAt, target.id).map((row2) => JSON.parse(String(row2.event))).filter((event) => {
|
|
2704
|
+
if (!target.events.includes(event.type)) return false;
|
|
2705
|
+
if (target.tag == null) return true;
|
|
2706
|
+
return event.questId !== void 0 && db.prepare(
|
|
2707
|
+
"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 = ?"
|
|
2708
|
+
).get(event.questId, normalizeTagName(target.tag)) !== void 0;
|
|
2709
|
+
});
|
|
2710
|
+
for (const event of events)
|
|
2711
|
+
db.prepare("INSERT INTO hook_assignments(target_id, event_id) VALUES (?, ?)").run(
|
|
2712
|
+
target.id,
|
|
2713
|
+
event.id
|
|
2714
|
+
);
|
|
2715
|
+
const row = db.prepare("SELECT url, secret, lease_until FROM hook_targets WHERE id = ?").get(target.id);
|
|
2716
|
+
if (!row || row.lease_until != null && Number(row.lease_until) > now) continue;
|
|
2717
|
+
let pending = listDeliveries(db).find(
|
|
2718
|
+
(d) => d.targetId === target.id && d.status === "pending"
|
|
2719
|
+
);
|
|
2720
|
+
const queued = db.prepare(
|
|
2721
|
+
"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"
|
|
2722
|
+
).all(target.id).map((r) => JSON.parse(String(r.event)));
|
|
2723
|
+
if (!pending && queued.length) {
|
|
2724
|
+
const id = `d_${randomBytes(16).toString("hex")}`;
|
|
2725
|
+
db.prepare(
|
|
2726
|
+
"INSERT INTO hook_deliveries(id, target_id, events, status, next_attempt_at) VALUES (?, ?, ?, 'pending', ?)"
|
|
2727
|
+
).run(
|
|
2728
|
+
id,
|
|
2729
|
+
target.id,
|
|
2730
|
+
JSON.stringify(queued),
|
|
2731
|
+
Math.max(now, (target.lastWake ?? -Infinity) + target.minGap * 1e3)
|
|
2732
|
+
);
|
|
2733
|
+
db.prepare(
|
|
2734
|
+
"UPDATE hook_assignments SET delivery_id = ? WHERE target_id = ? AND delivery_id IS NULL"
|
|
2735
|
+
).run(id, target.id);
|
|
2736
|
+
pending = listDeliveries(db).find((d) => d.id === id);
|
|
2737
|
+
}
|
|
2738
|
+
if (!pending) continue;
|
|
2739
|
+
if (pending.attempts === 0 && queued.length && !queued.every((e) => pending?.events.some((p) => p.id === e.id))) {
|
|
2740
|
+
pending.events.push(...queued);
|
|
2741
|
+
db.prepare("UPDATE hook_deliveries SET events = ? WHERE id = ?").run(
|
|
2742
|
+
JSON.stringify(pending.events),
|
|
2743
|
+
pending.id
|
|
2744
|
+
);
|
|
2745
|
+
db.prepare(
|
|
2746
|
+
"UPDATE hook_assignments SET delivery_id = ? WHERE target_id = ? AND delivery_id IS NULL"
|
|
2747
|
+
).run(pending.id, target.id);
|
|
2748
|
+
}
|
|
2749
|
+
if (pending.nextAttemptAt > now || target.lastWake != null && target.lastWake + target.minGap * 1e3 > now)
|
|
2750
|
+
continue;
|
|
2751
|
+
if (pending.attempts >= 8) {
|
|
2752
|
+
db.prepare("UPDATE hook_deliveries SET status = 'failed' WHERE id = ?").run(pending.id);
|
|
2753
|
+
continue;
|
|
2754
|
+
}
|
|
2755
|
+
const claim = randomBytes(16).toString("hex");
|
|
2756
|
+
db.prepare(
|
|
2757
|
+
"UPDATE hook_targets SET claim = ?, lease_until = ?, last_wake = ? WHERE id = ?"
|
|
2758
|
+
).run(claim, now + 12e4, now, target.id);
|
|
2759
|
+
db.prepare(
|
|
2760
|
+
"UPDATE hook_deliveries SET attempts = attempts + 1, last_attempt_at = ? WHERE id = ?"
|
|
2761
|
+
).run(now, pending.id);
|
|
2762
|
+
pending.attempts++;
|
|
2763
|
+
pending.lastAttemptAt = now;
|
|
2764
|
+
result.batches.push({ targetId: target.id, events: pending.events });
|
|
2765
|
+
claims.push({
|
|
2766
|
+
delivery: pending,
|
|
2767
|
+
target: { id: target.id, url: String(row.url), secret: String(row.secret), claim }
|
|
2768
|
+
});
|
|
2769
|
+
}
|
|
2770
|
+
};
|
|
2771
|
+
if (options.dryRun) {
|
|
2772
|
+
const rollback = new Error("preview rollback");
|
|
2773
|
+
try {
|
|
2774
|
+
withTransaction(db, () => {
|
|
2775
|
+
collect();
|
|
2776
|
+
throw rollback;
|
|
2777
|
+
});
|
|
2778
|
+
} catch (error) {
|
|
2779
|
+
if (error !== rollback) throw error;
|
|
2780
|
+
}
|
|
2781
|
+
} else withTransaction(db, collect);
|
|
2782
|
+
if (!options.dryRun) {
|
|
2783
|
+
const sent = await Promise.all(
|
|
2784
|
+
claims.map((claim) => sendHookDelivery(db, claim.delivery, claim.target, options))
|
|
2785
|
+
);
|
|
2786
|
+
result.sent = sent.filter(Boolean).length;
|
|
2787
|
+
}
|
|
2788
|
+
const deliveries = listDeliveries(db);
|
|
2789
|
+
result.queued = deliveries.filter((d) => d.status === "pending" && d.attempts === 0).length + Number(
|
|
2790
|
+
db.prepare(
|
|
2791
|
+
"SELECT COUNT(DISTINCT target_id) AS n FROM hook_assignments WHERE delivery_id IS NULL"
|
|
2792
|
+
).get()?.n ?? 0
|
|
2793
|
+
);
|
|
2794
|
+
result.retrying = deliveries.filter((d) => d.status === "pending" && d.attempts > 0).length;
|
|
2795
|
+
result.failed = deliveries.filter((d) => d.status === "failed").length;
|
|
2796
|
+
return result;
|
|
2797
|
+
}
|
|
2798
|
+
var init_tick_questlog = __esm({
|
|
2799
|
+
"src/hooks/tick_questlog.ts"() {
|
|
2800
|
+
init_spawn_due_repeatable_quests();
|
|
2801
|
+
init_normalize_tag_name();
|
|
2802
|
+
init_with_transaction();
|
|
2803
|
+
init_detect_hook_events();
|
|
2804
|
+
init_list_deliveries();
|
|
2805
|
+
init_list_hooks();
|
|
2806
|
+
init_send_hook_delivery();
|
|
2807
|
+
}
|
|
2808
|
+
});
|
|
2809
|
+
|
|
2810
|
+
// src/hooks/index.ts
|
|
2811
|
+
var init_hooks = __esm({
|
|
2812
|
+
"src/hooks/index.ts"() {
|
|
2813
|
+
init_list_deliveries();
|
|
2814
|
+
init_list_hooks();
|
|
2815
|
+
init_remove_hook();
|
|
2816
|
+
init_tick_questlog();
|
|
2817
|
+
}
|
|
2818
|
+
});
|
|
2819
|
+
|
|
2820
|
+
// src/tools/manage_hooks_tool.ts
|
|
2821
|
+
var manageHooksTool;
|
|
2822
|
+
var init_manage_hooks_tool = __esm({
|
|
2823
|
+
"src/tools/manage_hooks_tool.ts"() {
|
|
2824
|
+
init_hooks();
|
|
2825
|
+
init_tool_metadata();
|
|
2826
|
+
init_tool_types();
|
|
2827
|
+
manageHooksTool = defineQuestlogTool({
|
|
2828
|
+
name: "manage_hooks",
|
|
2829
|
+
description: "List or remove outbound webhook targets, run a tick, or inspect pending and failed deliveries.",
|
|
2830
|
+
whenToUse: "Use to operate configured outbound webhooks. Add and test targets with the CLI.",
|
|
2831
|
+
whenNotToUse: "Use quest tools to change work itself.",
|
|
2832
|
+
sideEffects: "writes_state",
|
|
2833
|
+
readOnly: false,
|
|
2834
|
+
supportsClarification: false,
|
|
2835
|
+
targetKinds: ["hook"],
|
|
2836
|
+
inputDescriptions: {
|
|
2837
|
+
action: "Operation to perform.",
|
|
2838
|
+
id: "Target id for removal.",
|
|
2839
|
+
dry_run: "Preview a tick without recording or sending."
|
|
2840
|
+
},
|
|
2841
|
+
outputDescription: "Returns redacted targets, delivery metadata, or a tick summary. Never returns signing secrets.",
|
|
2842
|
+
inputSchema: objectSchema(
|
|
2843
|
+
{
|
|
2844
|
+
action: enumSchema("Operation.", ["list", "remove", "tick", "deliveries"]),
|
|
2845
|
+
id: integerSchema("Target id."),
|
|
2846
|
+
dry_run: booleanSchema("Preview only.")
|
|
2847
|
+
},
|
|
2848
|
+
["action"]
|
|
2849
|
+
),
|
|
2850
|
+
handler: async (db, input) => {
|
|
2851
|
+
switch (input.action) {
|
|
2852
|
+
case "list":
|
|
2853
|
+
return toolSuccess("Webhook targets.", listHooks(db));
|
|
2854
|
+
case "deliveries":
|
|
2855
|
+
return toolSuccess("Pending and failed deliveries.", listDeliveries(db));
|
|
2856
|
+
case "remove":
|
|
2857
|
+
if (!Number.isSafeInteger(input.id) || Number(input.id) < 1)
|
|
2858
|
+
return toolFailure(
|
|
2859
|
+
"protocol",
|
|
2860
|
+
"invalid_input",
|
|
2861
|
+
"Target id required.",
|
|
2862
|
+
"Provide a positive target id.",
|
|
2863
|
+
{ recovery: 'Pass {"action":"remove","id":1}' }
|
|
2864
|
+
);
|
|
2865
|
+
removeHook(db, Number(input.id));
|
|
2866
|
+
return toolSuccess("Webhook target removed.", {});
|
|
2867
|
+
case "tick":
|
|
2868
|
+
return toolSuccess(
|
|
2869
|
+
"Webhook tick completed.",
|
|
2870
|
+
await tickQuestlog(db, { dryRun: input.dry_run ?? false })
|
|
2871
|
+
);
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
});
|
|
2875
|
+
}
|
|
2876
|
+
});
|
|
2877
|
+
|
|
2878
|
+
// src/assert_non_negative_seconds.ts
|
|
2879
|
+
function assertNonNegativeSeconds(value, label) {
|
|
2880
|
+
if (value != null && value < 0) {
|
|
2881
|
+
throw new Error(`${label} must not be negative.`);
|
|
2882
|
+
}
|
|
2883
|
+
return value;
|
|
2884
|
+
}
|
|
2885
|
+
var init_assert_non_negative_seconds = __esm({
|
|
2886
|
+
"src/assert_non_negative_seconds.ts"() {
|
|
2887
|
+
}
|
|
2888
|
+
});
|
|
2889
|
+
|
|
2890
|
+
// src/repeatable_quests/update_repeatable_quest.ts
|
|
2891
|
+
function updateRepeatableQuest(db, repeatableQuestId, input) {
|
|
2892
|
+
return withTransaction(db, () => {
|
|
2893
|
+
const current = getRepeatableQuestOrThrow(db, repeatableQuestId);
|
|
2894
|
+
const now = resolveNow(input.now);
|
|
2895
|
+
const rrule = input.rrule ?? current.rrule;
|
|
2896
|
+
parseRRule(rrule);
|
|
2897
|
+
db.prepare(
|
|
2898
|
+
`UPDATE repeatable_quests
|
|
2899
|
+
SET questline_id = ?,
|
|
2900
|
+
title = ?,
|
|
2901
|
+
objective = ?,
|
|
2902
|
+
rrule = ?,
|
|
2903
|
+
anchor_at = ?,
|
|
2904
|
+
not_before_offset_seconds = ?,
|
|
2905
|
+
due_offset_seconds = ?,
|
|
2906
|
+
scheduled_start_offset_seconds = ?,
|
|
2907
|
+
scheduled_end_offset_seconds = ?,
|
|
2908
|
+
all_day = ?,
|
|
2909
|
+
estimate_seconds = ?,
|
|
2910
|
+
archived_at = ?,
|
|
2911
|
+
updated_at = ?
|
|
2912
|
+
WHERE id = ? AND deleted_at IS NULL`
|
|
2913
|
+
).run(
|
|
2914
|
+
input.questlineId === void 0 ? current.questlineId : input.questlineId,
|
|
2915
|
+
input.title == null ? current.title : assertNonEmpty(input.title, "Repeatable quest title"),
|
|
2916
|
+
input.objective == null ? current.objective : assertNonEmpty(input.objective, "Repeatable quest objective"),
|
|
2917
|
+
rrule,
|
|
2918
|
+
input.anchorAt ?? current.anchorAt,
|
|
2919
|
+
input.notBeforeOffsetSeconds === void 0 ? current.notBeforeOffsetSeconds : assertNonNegativeSeconds(input.notBeforeOffsetSeconds, "Repeatable not-before offset") ?? null,
|
|
2920
|
+
input.dueOffsetSeconds === void 0 ? current.dueOffsetSeconds : assertNonNegativeSeconds(input.dueOffsetSeconds, "Repeatable due offset") ?? null,
|
|
2921
|
+
input.scheduledStartOffsetSeconds === void 0 ? current.scheduledStartOffsetSeconds : input.scheduledStartOffsetSeconds,
|
|
2922
|
+
input.scheduledEndOffsetSeconds === void 0 ? current.scheduledEndOffsetSeconds : input.scheduledEndOffsetSeconds,
|
|
2923
|
+
input.allDay === void 0 ? current.allDay ? 1 : 0 : input.allDay ? 1 : 0,
|
|
2924
|
+
input.estimateSeconds === void 0 ? current.estimateSeconds : assertNonNegativeSeconds(input.estimateSeconds, "Repeatable estimate") ?? null,
|
|
2925
|
+
input.archivedAt === void 0 ? current.archivedAt : input.archivedAt,
|
|
2926
|
+
now,
|
|
2927
|
+
repeatableQuestId
|
|
2928
|
+
);
|
|
2929
|
+
return getRepeatableQuestOrThrow(db, repeatableQuestId);
|
|
2930
|
+
});
|
|
2931
|
+
}
|
|
2932
|
+
var init_update_repeatable_quest = __esm({
|
|
2933
|
+
"src/repeatable_quests/update_repeatable_quest.ts"() {
|
|
2934
|
+
init_assert_non_empty();
|
|
2935
|
+
init_assert_non_negative_seconds();
|
|
2936
|
+
init_resolve_now();
|
|
2937
|
+
init_with_transaction();
|
|
2938
|
+
init_get_repeatable_quest_or_throw();
|
|
2939
|
+
init_parse_rrule();
|
|
2940
|
+
}
|
|
2941
|
+
});
|
|
2942
|
+
|
|
2943
|
+
// src/repeatable_quests/archive_repeatable_quest.ts
|
|
2944
|
+
function archiveRepeatableQuest(db, repeatableQuestId, archivedAt) {
|
|
2945
|
+
return updateRepeatableQuest(db, repeatableQuestId, { archivedAt: resolveNow(archivedAt) });
|
|
2562
2946
|
}
|
|
2563
|
-
var
|
|
2564
|
-
"src/repeatable_quests/
|
|
2947
|
+
var init_archive_repeatable_quest = __esm({
|
|
2948
|
+
"src/repeatable_quests/archive_repeatable_quest.ts"() {
|
|
2565
2949
|
init_resolve_now();
|
|
2566
|
-
|
|
2950
|
+
init_update_repeatable_quest();
|
|
2567
2951
|
}
|
|
2568
2952
|
});
|
|
2569
2953
|
|
|
2570
|
-
// src/
|
|
2571
|
-
function
|
|
2572
|
-
const row = db.prepare(
|
|
2954
|
+
// src/assert_active_row_exists.ts
|
|
2955
|
+
function assertActiveRowExists(db, table, id, label) {
|
|
2956
|
+
const row = db.prepare(`SELECT id FROM ${table} WHERE id = ? AND deleted_at IS NULL`).get(id);
|
|
2573
2957
|
if (!row) {
|
|
2574
|
-
throw new Error(
|
|
2958
|
+
throw new Error(`${label} ${id} was not found.`);
|
|
2575
2959
|
}
|
|
2576
|
-
return mapQuestRow(row);
|
|
2577
2960
|
}
|
|
2578
|
-
var
|
|
2579
|
-
"src/
|
|
2580
|
-
init_map_quest_row();
|
|
2961
|
+
var init_assert_active_row_exists = __esm({
|
|
2962
|
+
"src/assert_active_row_exists.ts"() {
|
|
2581
2963
|
}
|
|
2582
2964
|
});
|
|
2583
2965
|
|
|
2584
|
-
// src/
|
|
2585
|
-
function
|
|
2966
|
+
// src/assert_non_negative_number.ts
|
|
2967
|
+
function assertNonNegativeNumber(value, label) {
|
|
2968
|
+
if (value != null && value < 0) {
|
|
2969
|
+
throw new Error(`${label} must not be negative.`);
|
|
2970
|
+
}
|
|
2971
|
+
return value;
|
|
2972
|
+
}
|
|
2973
|
+
var init_assert_non_negative_number = __esm({
|
|
2974
|
+
"src/assert_non_negative_number.ts"() {
|
|
2975
|
+
}
|
|
2976
|
+
});
|
|
2977
|
+
|
|
2978
|
+
// src/rewards/replace_active_repeatable_quest_rewards.ts
|
|
2979
|
+
function replaceActiveRepeatableQuestRewards(db, repeatableQuestId, rewards, now) {
|
|
2586
2980
|
const effectiveNow = resolveNow(now);
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
).all(repeatableQuestId);
|
|
2592
|
-
for (const row of rows) {
|
|
2981
|
+
db.prepare(
|
|
2982
|
+
"UPDATE repeatable_quest_rewards SET deleted_at = ? WHERE repeatable_quest_id = ? AND deleted_at IS NULL"
|
|
2983
|
+
).run(effectiveNow, repeatableQuestId);
|
|
2984
|
+
for (const reward of rewards) {
|
|
2593
2985
|
db.prepare(
|
|
2594
|
-
`INSERT INTO
|
|
2595
|
-
|
|
2986
|
+
`INSERT INTO repeatable_quest_rewards (
|
|
2987
|
+
repeatable_quest_id,
|
|
2988
|
+
kind,
|
|
2989
|
+
name,
|
|
2990
|
+
description,
|
|
2991
|
+
quantity,
|
|
2992
|
+
created_at
|
|
2993
|
+
) VALUES (?, ?, ?, ?, ?, ?)`
|
|
2596
2994
|
).run(
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2995
|
+
repeatableQuestId,
|
|
2996
|
+
assertNonEmpty(reward.kind, "Reward kind"),
|
|
2997
|
+
assertNonEmpty(reward.name, "Reward name"),
|
|
2998
|
+
reward.description?.trim() || null,
|
|
2999
|
+
assertNonNegativeNumber(reward.quantity, "Reward quantity") ?? null,
|
|
2602
3000
|
effectiveNow
|
|
2603
3001
|
);
|
|
2604
3002
|
}
|
|
2605
3003
|
}
|
|
2606
|
-
var
|
|
2607
|
-
"src/rewards/
|
|
3004
|
+
var init_replace_active_repeatable_quest_rewards = __esm({
|
|
3005
|
+
"src/rewards/replace_active_repeatable_quest_rewards.ts"() {
|
|
3006
|
+
init_assert_non_empty();
|
|
3007
|
+
init_assert_non_negative_number();
|
|
2608
3008
|
init_resolve_now();
|
|
2609
3009
|
}
|
|
2610
3010
|
});
|
|
2611
3011
|
|
|
2612
|
-
// src/tags/
|
|
2613
|
-
function
|
|
3012
|
+
// src/tags/replace_active_repeatable_quest_tags.ts
|
|
3013
|
+
function getOrCreateTagId(db, name, now) {
|
|
3014
|
+
const normalizedName = normalizeTagName(name);
|
|
3015
|
+
if (!normalizedName) {
|
|
3016
|
+
throw new Error("Tag name must not be empty.");
|
|
3017
|
+
}
|
|
3018
|
+
const existing = db.prepare("SELECT id FROM tags WHERE normalized_name = ? AND deleted_at IS NULL").get(normalizedName);
|
|
3019
|
+
if (existing) {
|
|
3020
|
+
return Number(existing.id);
|
|
3021
|
+
}
|
|
3022
|
+
const created = db.prepare(
|
|
3023
|
+
`INSERT INTO tags (name, normalized_name, created_at)
|
|
3024
|
+
VALUES (?, ?, ?)`
|
|
3025
|
+
).run(name.trim(), normalizedName, now);
|
|
3026
|
+
return Number(created.lastInsertRowid);
|
|
3027
|
+
}
|
|
3028
|
+
function replaceActiveRepeatableQuestTags(db, repeatableQuestId, tagNames, now) {
|
|
2614
3029
|
const effectiveNow = resolveNow(now);
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
3030
|
+
db.prepare(
|
|
3031
|
+
"UPDATE repeatable_quest_tags SET deleted_at = ? WHERE repeatable_quest_id = ? AND deleted_at IS NULL"
|
|
3032
|
+
).run(effectiveNow, repeatableQuestId);
|
|
3033
|
+
const uniqueNames = [...new Set(tagNames.map((tag) => tag.trim()).filter(Boolean))];
|
|
3034
|
+
for (const name of uniqueNames) {
|
|
3035
|
+
const tagId = getOrCreateTagId(db, name, effectiveNow);
|
|
2621
3036
|
db.prepare(
|
|
2622
|
-
`INSERT INTO
|
|
3037
|
+
`INSERT INTO repeatable_quest_tags (repeatable_quest_id, tag_id, created_at)
|
|
2623
3038
|
VALUES (?, ?, ?)`
|
|
2624
|
-
).run(
|
|
3039
|
+
).run(repeatableQuestId, tagId, effectiveNow);
|
|
2625
3040
|
}
|
|
2626
3041
|
}
|
|
2627
|
-
var
|
|
2628
|
-
"src/tags/
|
|
3042
|
+
var init_replace_active_repeatable_quest_tags = __esm({
|
|
3043
|
+
"src/tags/replace_active_repeatable_quest_tags.ts"() {
|
|
2629
3044
|
init_resolve_now();
|
|
3045
|
+
init_normalize_tag_name();
|
|
2630
3046
|
}
|
|
2631
3047
|
});
|
|
2632
3048
|
|
|
2633
|
-
// src/repeatable_quests/
|
|
2634
|
-
function
|
|
3049
|
+
// src/repeatable_quests/create_repeatable_quest.ts
|
|
3050
|
+
function createRepeatableQuest(db, input) {
|
|
2635
3051
|
return withTransaction(db, () => {
|
|
2636
|
-
const
|
|
2637
|
-
const
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
questline_id,
|
|
2643
|
-
source_rumor_id,
|
|
2644
|
-
spawned_by_quest_id,
|
|
2645
|
-
spawned_from_repeatable_id,
|
|
2646
|
-
spawned_for_at,
|
|
2647
|
-
title,
|
|
2648
|
-
objective,
|
|
2649
|
-
created_at,
|
|
2650
|
-
updated_at,
|
|
2651
|
-
effort_seconds,
|
|
2652
|
-
estimate_seconds,
|
|
2653
|
-
not_before_at,
|
|
2654
|
-
due_at,
|
|
2655
|
-
scheduled_start_at,
|
|
2656
|
-
scheduled_end_at,
|
|
2657
|
-
all_day
|
|
2658
|
-
) VALUES (?, NULL, NULL, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?)`
|
|
2659
|
-
).run(
|
|
2660
|
-
repeatable.questlineId ?? null,
|
|
2661
|
-
repeatable.id,
|
|
2662
|
-
anchor.anchorAt,
|
|
2663
|
-
repeatable.title,
|
|
2664
|
-
repeatable.objective,
|
|
2665
|
-
now,
|
|
2666
|
-
now,
|
|
2667
|
-
repeatable.estimateSeconds ?? null,
|
|
2668
|
-
repeatable.notBeforeOffsetSeconds == null ? null : anchor.anchorAt + repeatable.notBeforeOffsetSeconds * 1e3,
|
|
2669
|
-
repeatable.dueOffsetSeconds == null ? null : anchor.anchorAt + repeatable.dueOffsetSeconds * 1e3,
|
|
2670
|
-
repeatable.scheduledStartOffsetSeconds == null ? null : anchor.anchorAt + repeatable.scheduledStartOffsetSeconds * 1e3,
|
|
2671
|
-
repeatable.scheduledEndOffsetSeconds == null ? null : anchor.anchorAt + repeatable.scheduledEndOffsetSeconds * 1e3,
|
|
2672
|
-
repeatable.allDay ? 1 : 0
|
|
2673
|
-
);
|
|
2674
|
-
const questId = Number(created.lastInsertRowid);
|
|
2675
|
-
copyRepeatableQuestTagsToQuest(db, repeatable.id, questId, now);
|
|
2676
|
-
copyRepeatableQuestRewardsToQuest(db, repeatable.id, questId, now);
|
|
2677
|
-
quests.push(getQuestOrThrow(db, questId));
|
|
3052
|
+
const now = resolveNow(input.now);
|
|
3053
|
+
const title = assertNonEmpty(input.title, "Repeatable quest title");
|
|
3054
|
+
const objective = assertNonEmpty(input.objective, "Repeatable quest objective");
|
|
3055
|
+
parseRRule(input.rrule);
|
|
3056
|
+
if (input.questlineId != null) {
|
|
3057
|
+
assertActiveRowExists(db, "questlines", input.questlineId, "Questline");
|
|
2678
3058
|
}
|
|
2679
|
-
|
|
3059
|
+
const created = db.prepare(
|
|
3060
|
+
`INSERT INTO repeatable_quests (
|
|
3061
|
+
questline_id,
|
|
3062
|
+
title,
|
|
3063
|
+
objective,
|
|
3064
|
+
rrule,
|
|
3065
|
+
anchor_at,
|
|
3066
|
+
not_before_offset_seconds,
|
|
3067
|
+
due_offset_seconds,
|
|
3068
|
+
scheduled_start_offset_seconds,
|
|
3069
|
+
scheduled_end_offset_seconds,
|
|
3070
|
+
all_day,
|
|
3071
|
+
estimate_seconds,
|
|
3072
|
+
created_at,
|
|
3073
|
+
updated_at
|
|
3074
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
3075
|
+
).run(
|
|
3076
|
+
input.questlineId ?? null,
|
|
3077
|
+
title,
|
|
3078
|
+
objective,
|
|
3079
|
+
input.rrule,
|
|
3080
|
+
input.anchorAt,
|
|
3081
|
+
assertNonNegativeSeconds(input.notBeforeOffsetSeconds, "Repeatable not-before offset") ?? null,
|
|
3082
|
+
assertNonNegativeSeconds(input.dueOffsetSeconds, "Repeatable due offset") ?? null,
|
|
3083
|
+
input.scheduledStartOffsetSeconds ?? null,
|
|
3084
|
+
input.scheduledEndOffsetSeconds ?? null,
|
|
3085
|
+
input.allDay ? 1 : 0,
|
|
3086
|
+
assertNonNegativeSeconds(input.estimateSeconds, "Repeatable estimate") ?? null,
|
|
3087
|
+
now,
|
|
3088
|
+
now
|
|
3089
|
+
);
|
|
3090
|
+
const repeatableQuestId = Number(created.lastInsertRowid);
|
|
3091
|
+
if (input.tags && input.tags.length > 0) {
|
|
3092
|
+
replaceActiveRepeatableQuestTags(db, repeatableQuestId, input.tags, now);
|
|
3093
|
+
}
|
|
3094
|
+
if (input.rewards && input.rewards.length > 0) {
|
|
3095
|
+
replaceActiveRepeatableQuestRewards(db, repeatableQuestId, input.rewards, now);
|
|
3096
|
+
}
|
|
3097
|
+
return getRepeatableQuestOrThrow(db, repeatableQuestId);
|
|
2680
3098
|
});
|
|
2681
3099
|
}
|
|
2682
|
-
var
|
|
2683
|
-
"src/repeatable_quests/
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
3100
|
+
var init_create_repeatable_quest = __esm({
|
|
3101
|
+
"src/repeatable_quests/create_repeatable_quest.ts"() {
|
|
3102
|
+
init_assert_active_row_exists();
|
|
3103
|
+
init_assert_non_empty();
|
|
3104
|
+
init_assert_non_negative_seconds();
|
|
3105
|
+
init_resolve_now();
|
|
3106
|
+
init_replace_active_repeatable_quest_rewards();
|
|
3107
|
+
init_replace_active_repeatable_quest_tags();
|
|
2687
3108
|
init_with_transaction();
|
|
2688
3109
|
init_get_repeatable_quest_or_throw();
|
|
2689
|
-
|
|
3110
|
+
init_parse_rrule();
|
|
3111
|
+
}
|
|
3112
|
+
});
|
|
3113
|
+
|
|
3114
|
+
// src/repeatable_quests/soft_delete_repeatable_quest.ts
|
|
3115
|
+
function softDeleteRepeatableQuest(db, repeatableQuestId, deletedAt) {
|
|
3116
|
+
const now = resolveNow(deletedAt);
|
|
3117
|
+
getRepeatableQuestOrThrow(db, repeatableQuestId);
|
|
3118
|
+
db.prepare(
|
|
3119
|
+
`UPDATE repeatable_quests
|
|
3120
|
+
SET deleted_at = ?, updated_at = ?
|
|
3121
|
+
WHERE id = ? AND deleted_at IS NULL`
|
|
3122
|
+
).run(now, now, repeatableQuestId);
|
|
3123
|
+
}
|
|
3124
|
+
var init_soft_delete_repeatable_quest = __esm({
|
|
3125
|
+
"src/repeatable_quests/soft_delete_repeatable_quest.ts"() {
|
|
3126
|
+
init_resolve_now();
|
|
3127
|
+
init_get_repeatable_quest_or_throw();
|
|
2690
3128
|
}
|
|
2691
3129
|
});
|
|
2692
3130
|
|
|
@@ -4256,7 +4694,7 @@ function validateReviewInput(input) {
|
|
|
4256
4694
|
'Use `filters.dayAt` only with `view: "quests.scheduled_for_day"`.'
|
|
4257
4695
|
);
|
|
4258
4696
|
}
|
|
4259
|
-
if ((input.view === "rumors" || input.view === "questlines") && hasQuestFilters(input.filters)) {
|
|
4697
|
+
if ((input.view === "rumors" || input.view === "questlines" || input.view === "deliveries") && hasQuestFilters(input.filters)) {
|
|
4260
4698
|
return toolFailure(
|
|
4261
4699
|
"protocol",
|
|
4262
4700
|
"invalid_input",
|
|
@@ -4324,6 +4762,15 @@ function reviewQuestlogToolHandler(db, input) {
|
|
|
4324
4762
|
if (invalid) {
|
|
4325
4763
|
return invalid;
|
|
4326
4764
|
}
|
|
4765
|
+
if (input.view === "deliveries")
|
|
4766
|
+
return toolSuccess("Pending and failed deliveries.", {
|
|
4767
|
+
view: input.view,
|
|
4768
|
+
items: [],
|
|
4769
|
+
deliveries: listDeliveries(db),
|
|
4770
|
+
count: listDeliveries(db).length,
|
|
4771
|
+
appliedFilters: input.filters,
|
|
4772
|
+
evaluatedAt: input.now ?? Date.now()
|
|
4773
|
+
});
|
|
4327
4774
|
const now = input.now ?? Date.now();
|
|
4328
4775
|
let items = [];
|
|
4329
4776
|
switch (input.view) {
|
|
@@ -4368,6 +4815,7 @@ function reviewQuestlogToolHandler(db, input) {
|
|
|
4368
4815
|
var reviewQuestlogTool;
|
|
4369
4816
|
var init_review_questlog_tool = __esm({
|
|
4370
4817
|
"src/tools/review_questlog_tool.ts"() {
|
|
4818
|
+
init_list_deliveries();
|
|
4371
4819
|
init_list_questlines();
|
|
4372
4820
|
init_quests();
|
|
4373
4821
|
init_get_repeatable_quest_or_throw();
|
|
@@ -4396,6 +4844,7 @@ var init_review_questlog_tool = __esm({
|
|
|
4396
4844
|
inputSchema: objectSchema(
|
|
4397
4845
|
{
|
|
4398
4846
|
view: enumSchema("Which operational review surface to return.", [
|
|
4847
|
+
"deliveries",
|
|
4399
4848
|
"questlines",
|
|
4400
4849
|
"quests.active",
|
|
4401
4850
|
"quests.available",
|
|
@@ -5966,6 +6415,7 @@ var init_tool_registry = __esm({
|
|
|
5966
6415
|
"src/tools/tool_registry.ts"() {
|
|
5967
6416
|
init_capture_rumor_tool();
|
|
5968
6417
|
init_inspect_questlog_item_tool();
|
|
6418
|
+
init_manage_hooks_tool();
|
|
5969
6419
|
init_manage_repeatable_tool();
|
|
5970
6420
|
init_organize_work_tool();
|
|
5971
6421
|
init_plan_quest_tool();
|
|
@@ -5977,6 +6427,7 @@ var init_tool_registry = __esm({
|
|
|
5977
6427
|
init_shape_work_tool();
|
|
5978
6428
|
init_tag_work_tool();
|
|
5979
6429
|
questlogTools = [
|
|
6430
|
+
manageHooksTool,
|
|
5980
6431
|
searchQuestlogTool,
|
|
5981
6432
|
reviewQuestlogTool,
|
|
5982
6433
|
inspectQuestlogItemTool,
|