opencode-gateway 0.1.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.
Files changed (137) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +70 -0
  3. package/dist/binding/execution.d.ts +24 -0
  4. package/dist/binding/execution.js +1 -0
  5. package/dist/binding/gateway.d.ts +71 -0
  6. package/dist/binding/gateway.js +1 -0
  7. package/dist/binding/index.d.ts +15 -0
  8. package/dist/binding/index.js +4 -0
  9. package/dist/binding/opencode.d.ts +123 -0
  10. package/dist/binding/opencode.js +1 -0
  11. package/dist/cli/args.d.ts +9 -0
  12. package/dist/cli/args.js +53 -0
  13. package/dist/cli/doctor.d.ts +6 -0
  14. package/dist/cli/doctor.js +59 -0
  15. package/dist/cli/init.d.ts +6 -0
  16. package/dist/cli/init.js +35 -0
  17. package/dist/cli/opencode-config.d.ts +10 -0
  18. package/dist/cli/opencode-config.js +62 -0
  19. package/dist/cli/paths.d.ts +7 -0
  20. package/dist/cli/paths.js +22 -0
  21. package/dist/cli/templates.d.ts +1 -0
  22. package/dist/cli/templates.js +26 -0
  23. package/dist/cli.d.ts +2 -0
  24. package/dist/cli.js +314 -0
  25. package/dist/config/cron.d.ts +7 -0
  26. package/dist/config/cron.js +52 -0
  27. package/dist/config/gateway.d.ts +26 -0
  28. package/dist/config/gateway.js +142 -0
  29. package/dist/config/paths.d.ts +10 -0
  30. package/dist/config/paths.js +33 -0
  31. package/dist/config/telegram.d.ts +13 -0
  32. package/dist/config/telegram.js +91 -0
  33. package/dist/cron/runtime.d.ts +40 -0
  34. package/dist/cron/runtime.js +237 -0
  35. package/dist/delivery/telegram.d.ts +16 -0
  36. package/dist/delivery/telegram.js +75 -0
  37. package/dist/delivery/text.d.ts +21 -0
  38. package/dist/delivery/text.js +175 -0
  39. package/dist/gateway.d.ts +33 -0
  40. package/dist/gateway.js +105 -0
  41. package/dist/host/file-sender.d.ts +16 -0
  42. package/dist/host/file-sender.js +59 -0
  43. package/dist/host/noop.d.ts +4 -0
  44. package/dist/host/noop.js +14 -0
  45. package/dist/host/transport.d.ts +9 -0
  46. package/dist/host/transport.js +35 -0
  47. package/dist/index.d.ts +7 -0
  48. package/dist/index.js +52 -0
  49. package/dist/mailbox/router.d.ts +7 -0
  50. package/dist/mailbox/router.js +16 -0
  51. package/dist/media/mime.d.ts +2 -0
  52. package/dist/media/mime.js +45 -0
  53. package/dist/opencode/adapter.d.ts +19 -0
  54. package/dist/opencode/adapter.js +291 -0
  55. package/dist/opencode/driver-hub.d.ts +15 -0
  56. package/dist/opencode/driver-hub.js +82 -0
  57. package/dist/opencode/event-normalize.d.ts +48 -0
  58. package/dist/opencode/event-normalize.js +48 -0
  59. package/dist/opencode/event-stream.d.ts +23 -0
  60. package/dist/opencode/event-stream.js +65 -0
  61. package/dist/opencode/events.d.ts +2 -0
  62. package/dist/opencode/events.js +1 -0
  63. package/dist/questions/client.d.ts +5 -0
  64. package/dist/questions/client.js +36 -0
  65. package/dist/questions/format.d.ts +3 -0
  66. package/dist/questions/format.js +36 -0
  67. package/dist/questions/normalize.d.ts +10 -0
  68. package/dist/questions/normalize.js +45 -0
  69. package/dist/questions/parser.d.ts +11 -0
  70. package/dist/questions/parser.js +96 -0
  71. package/dist/questions/runtime.d.ts +53 -0
  72. package/dist/questions/runtime.js +195 -0
  73. package/dist/questions/types.d.ts +22 -0
  74. package/dist/questions/types.js +1 -0
  75. package/dist/runtime/attachments.d.ts +3 -0
  76. package/dist/runtime/attachments.js +12 -0
  77. package/dist/runtime/executor.d.ts +24 -0
  78. package/dist/runtime/executor.js +188 -0
  79. package/dist/runtime/mailbox.d.ts +25 -0
  80. package/dist/runtime/mailbox.js +112 -0
  81. package/dist/runtime/opencode-runner.d.ts +26 -0
  82. package/dist/runtime/opencode-runner.js +79 -0
  83. package/dist/session/context.d.ts +10 -0
  84. package/dist/session/context.js +44 -0
  85. package/dist/session/conversation-key.d.ts +3 -0
  86. package/dist/session/conversation-key.js +3 -0
  87. package/dist/session/switcher.d.ts +25 -0
  88. package/dist/session/switcher.js +59 -0
  89. package/dist/store/migrations.d.ts +2 -0
  90. package/dist/store/migrations.js +183 -0
  91. package/dist/store/sqlite.d.ts +127 -0
  92. package/dist/store/sqlite.js +678 -0
  93. package/dist/telegram/client.d.ts +35 -0
  94. package/dist/telegram/client.js +179 -0
  95. package/dist/telegram/media.d.ts +13 -0
  96. package/dist/telegram/media.js +65 -0
  97. package/dist/telegram/normalize.d.ts +47 -0
  98. package/dist/telegram/normalize.js +119 -0
  99. package/dist/telegram/poller.d.ts +29 -0
  100. package/dist/telegram/poller.js +97 -0
  101. package/dist/telegram/runtime.d.ts +51 -0
  102. package/dist/telegram/runtime.js +133 -0
  103. package/dist/telegram/state.d.ts +36 -0
  104. package/dist/telegram/state.js +128 -0
  105. package/dist/telegram/types.d.ts +80 -0
  106. package/dist/telegram/types.js +1 -0
  107. package/dist/tools/channel-new-session.d.ts +4 -0
  108. package/dist/tools/channel-new-session.js +27 -0
  109. package/dist/tools/channel-send-file.d.ts +9 -0
  110. package/dist/tools/channel-send-file.js +27 -0
  111. package/dist/tools/channel-target.d.ts +7 -0
  112. package/dist/tools/channel-target.js +28 -0
  113. package/dist/tools/cron-list.d.ts +3 -0
  114. package/dist/tools/cron-list.js +34 -0
  115. package/dist/tools/cron-remove.d.ts +3 -0
  116. package/dist/tools/cron-remove.js +12 -0
  117. package/dist/tools/cron-run.d.ts +3 -0
  118. package/dist/tools/cron-run.js +20 -0
  119. package/dist/tools/cron-upsert.d.ts +3 -0
  120. package/dist/tools/cron-upsert.js +37 -0
  121. package/dist/tools/gateway-dispatch-cron.d.ts +3 -0
  122. package/dist/tools/gateway-dispatch-cron.js +33 -0
  123. package/dist/tools/gateway-status.d.ts +3 -0
  124. package/dist/tools/gateway-status.js +25 -0
  125. package/dist/tools/telegram-send-test.d.ts +3 -0
  126. package/dist/tools/telegram-send-test.js +26 -0
  127. package/dist/tools/telegram-status.d.ts +3 -0
  128. package/dist/tools/telegram-status.js +49 -0
  129. package/dist/tools/time.d.ts +3 -0
  130. package/dist/tools/time.js +25 -0
  131. package/dist/utils/error.d.ts +1 -0
  132. package/dist/utils/error.js +57 -0
  133. package/generated/wasm/pkg/opencode_gateway_ffi.d.ts +23 -0
  134. package/generated/wasm/pkg/opencode_gateway_ffi.js +574 -0
  135. package/generated/wasm/pkg/opencode_gateway_ffi_bg.wasm +0 -0
  136. package/generated/wasm/pkg/opencode_gateway_ffi_bg.wasm.d.ts +22 -0
  137. package/package.json +61 -0
@@ -0,0 +1,678 @@
1
+ import { Database } from "bun:sqlite";
2
+ import { mkdir } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+ import { migrateGatewayDatabase } from "./migrations";
5
+ export class SqliteStore {
6
+ db;
7
+ constructor(db) {
8
+ this.db = db;
9
+ }
10
+ getSessionBinding(conversationKey) {
11
+ const row = this.db
12
+ .query("SELECT session_id FROM session_bindings WHERE conversation_key = ?1;")
13
+ .get(conversationKey);
14
+ return row?.session_id ?? null;
15
+ }
16
+ putSessionBinding(conversationKey, sessionId, recordedAtMs) {
17
+ this.db
18
+ .query(`
19
+ INSERT INTO session_bindings (conversation_key, session_id, updated_at_ms)
20
+ VALUES (?1, ?2, ?3)
21
+ ON CONFLICT(conversation_key) DO UPDATE SET
22
+ session_id = excluded.session_id,
23
+ updated_at_ms = excluded.updated_at_ms;
24
+ `)
25
+ .run(conversationKey, sessionId, recordedAtMs);
26
+ }
27
+ putSessionBindingIfUnchanged(conversationKey, expectedSessionId, nextSessionId, recordedAtMs) {
28
+ const result = expectedSessionId === null
29
+ ? this.db
30
+ .query(`
31
+ INSERT INTO session_bindings (conversation_key, session_id, updated_at_ms)
32
+ VALUES (?1, ?2, ?3)
33
+ ON CONFLICT(conversation_key) DO NOTHING;
34
+ `)
35
+ .run(conversationKey, nextSessionId, recordedAtMs)
36
+ : this.db
37
+ .query(`
38
+ UPDATE session_bindings
39
+ SET session_id = ?2,
40
+ updated_at_ms = ?3
41
+ WHERE conversation_key = ?1
42
+ AND session_id = ?4;
43
+ `)
44
+ .run(conversationKey, nextSessionId, recordedAtMs, expectedSessionId);
45
+ return result.changes > 0;
46
+ }
47
+ deleteSessionBinding(conversationKey) {
48
+ this.db.query("DELETE FROM session_bindings WHERE conversation_key = ?1;").run(conversationKey);
49
+ }
50
+ clearSessionReplyTargets(sessionId) {
51
+ this.db.query("DELETE FROM session_reply_targets WHERE session_id = ?1;").run(sessionId);
52
+ }
53
+ replaceSessionReplyTargets(input) {
54
+ assertSafeInteger(input.recordedAtMs, "session reply-target recordedAtMs");
55
+ const deleteTargets = this.db.query("DELETE FROM session_reply_targets WHERE session_id = ?1;");
56
+ const insertTarget = this.db.query(`
57
+ INSERT INTO session_reply_targets (
58
+ session_id,
59
+ ordinal,
60
+ conversation_key,
61
+ delivery_channel,
62
+ delivery_target,
63
+ delivery_topic,
64
+ updated_at_ms
65
+ )
66
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);
67
+ `);
68
+ this.db.transaction((payload) => {
69
+ deleteTargets.run(payload.sessionId);
70
+ for (const [ordinal, target] of payload.targets.entries()) {
71
+ insertTarget.run(payload.sessionId, ordinal, payload.conversationKey, target.channel, target.target, normalizeKeyField(target.topic), payload.recordedAtMs);
72
+ }
73
+ })(input);
74
+ }
75
+ listSessionReplyTargets(sessionId) {
76
+ const rows = this.db
77
+ .query(`
78
+ SELECT
79
+ session_id,
80
+ ordinal,
81
+ conversation_key,
82
+ delivery_channel,
83
+ delivery_target,
84
+ delivery_topic,
85
+ updated_at_ms
86
+ FROM session_reply_targets
87
+ WHERE session_id = ?1
88
+ ORDER BY ordinal ASC;
89
+ `)
90
+ .all(sessionId);
91
+ return rows.map(mapSessionReplyTargetRow);
92
+ }
93
+ getDefaultSessionReplyTarget(sessionId) {
94
+ const row = this.db
95
+ .query(`
96
+ SELECT
97
+ session_id,
98
+ ordinal,
99
+ conversation_key,
100
+ delivery_channel,
101
+ delivery_target,
102
+ delivery_topic,
103
+ updated_at_ms
104
+ FROM session_reply_targets
105
+ WHERE session_id = ?1
106
+ ORDER BY ordinal ASC
107
+ LIMIT 1;
108
+ `)
109
+ .get(sessionId);
110
+ return row ? mapSessionReplyTargetRow(row) : null;
111
+ }
112
+ appendJournal(entry) {
113
+ this.db
114
+ .query(`
115
+ INSERT INTO runtime_journal (kind, recorded_at_ms, conversation_key, payload_json)
116
+ VALUES (?1, ?2, ?3, ?4);
117
+ `)
118
+ .run(entry.kind, entry.recordedAtMs, entry.conversationKey, JSON.stringify(entry.payload));
119
+ }
120
+ replacePendingQuestion(input) {
121
+ assertSafeInteger(input.recordedAtMs, "pending question recordedAtMs");
122
+ const deleteQuestion = this.db.query("DELETE FROM pending_questions WHERE request_id = ?1;");
123
+ const insertQuestion = this.db.query(`
124
+ INSERT INTO pending_questions (
125
+ request_id,
126
+ session_id,
127
+ delivery_channel,
128
+ delivery_target,
129
+ delivery_topic,
130
+ question_json,
131
+ telegram_message_id,
132
+ created_at_ms
133
+ )
134
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8);
135
+ `);
136
+ this.db.transaction((payload) => {
137
+ deleteQuestion.run(payload.requestId);
138
+ for (const target of payload.targets) {
139
+ insertQuestion.run(payload.requestId, payload.sessionId, target.deliveryTarget.channel, target.deliveryTarget.target, normalizeKeyField(target.deliveryTarget.topic), JSON.stringify(payload.questions), target.telegramMessageId, payload.recordedAtMs);
140
+ }
141
+ })(input);
142
+ }
143
+ deletePendingQuestion(requestId) {
144
+ this.db.query("DELETE FROM pending_questions WHERE request_id = ?1;").run(requestId);
145
+ }
146
+ deletePendingQuestionsForSession(sessionId) {
147
+ this.db.query("DELETE FROM pending_questions WHERE session_id = ?1;").run(sessionId);
148
+ }
149
+ getPendingQuestionForTarget(target) {
150
+ const row = this.db
151
+ .query(`
152
+ SELECT
153
+ id,
154
+ request_id,
155
+ session_id,
156
+ delivery_channel,
157
+ delivery_target,
158
+ delivery_topic,
159
+ question_json,
160
+ telegram_message_id,
161
+ created_at_ms
162
+ FROM pending_questions
163
+ WHERE delivery_channel = ?1
164
+ AND delivery_target = ?2
165
+ AND delivery_topic = ?3
166
+ ORDER BY created_at_ms ASC, id ASC
167
+ LIMIT 1;
168
+ `)
169
+ .get(target.channel, target.target, normalizeKeyField(target.topic));
170
+ return row ? mapPendingQuestionRow(row) : null;
171
+ }
172
+ getPendingQuestionForTelegramMessage(target, telegramMessageId) {
173
+ assertSafeInteger(telegramMessageId, "pending question telegramMessageId");
174
+ const row = this.db
175
+ .query(`
176
+ SELECT
177
+ id,
178
+ request_id,
179
+ session_id,
180
+ delivery_channel,
181
+ delivery_target,
182
+ delivery_topic,
183
+ question_json,
184
+ telegram_message_id,
185
+ created_at_ms
186
+ FROM pending_questions
187
+ WHERE delivery_channel = ?1
188
+ AND delivery_target = ?2
189
+ AND delivery_topic = ?3
190
+ AND telegram_message_id = ?4
191
+ ORDER BY created_at_ms ASC, id ASC
192
+ LIMIT 1;
193
+ `)
194
+ .get(target.channel, target.target, normalizeKeyField(target.topic), telegramMessageId);
195
+ return row ? mapPendingQuestionRow(row) : null;
196
+ }
197
+ hasMailboxEntry(sourceKind, externalId) {
198
+ const row = this.db
199
+ .query(`
200
+ SELECT 1 AS present
201
+ FROM mailbox_entries
202
+ WHERE source_kind = ?1 AND external_id = ?2
203
+ LIMIT 1;
204
+ `)
205
+ .get(sourceKind, externalId);
206
+ return row?.present === 1;
207
+ }
208
+ enqueueMailboxEntry(input) {
209
+ assertSafeInteger(input.recordedAtMs, "mailbox recordedAtMs");
210
+ const insertEntry = this.db.query(`
211
+ INSERT INTO mailbox_entries (
212
+ mailbox_key,
213
+ source_kind,
214
+ external_id,
215
+ sender,
216
+ body,
217
+ reply_channel,
218
+ reply_target,
219
+ reply_topic,
220
+ created_at_ms
221
+ )
222
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
223
+ ON CONFLICT(source_kind, external_id) DO NOTHING;
224
+ `);
225
+ const insertAttachment = this.db.query(`
226
+ INSERT INTO mailbox_entry_attachments (
227
+ mailbox_entry_id,
228
+ ordinal,
229
+ kind,
230
+ mime_type,
231
+ file_name,
232
+ local_path
233
+ )
234
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6);
235
+ `);
236
+ this.db.transaction((payload) => {
237
+ const result = insertEntry.run(payload.mailboxKey, payload.sourceKind, payload.externalId, payload.sender, payload.text ?? "", payload.replyChannel, payload.replyTarget, payload.replyTopic, payload.recordedAtMs);
238
+ if (result.changes === 0) {
239
+ return;
240
+ }
241
+ const entryId = Number(result.lastInsertRowid);
242
+ for (const [index, attachment] of payload.attachments.entries()) {
243
+ insertAttachment.run(entryId, index, attachment.kind, attachment.mimeType, attachment.fileName, attachment.localPath);
244
+ }
245
+ })(input);
246
+ }
247
+ listPendingMailboxKeys() {
248
+ const rows = this.db
249
+ .query(`
250
+ SELECT DISTINCT mailbox_key
251
+ FROM mailbox_entries
252
+ ORDER BY mailbox_key ASC;
253
+ `)
254
+ .all();
255
+ return rows.map((row) => row.mailbox_key);
256
+ }
257
+ listMailboxEntries(mailboxKey) {
258
+ const rows = this.db
259
+ .query(`
260
+ SELECT
261
+ id,
262
+ mailbox_key,
263
+ source_kind,
264
+ external_id,
265
+ sender,
266
+ body,
267
+ reply_channel,
268
+ reply_target,
269
+ reply_topic,
270
+ created_at_ms
271
+ FROM mailbox_entries
272
+ WHERE mailbox_key = ?1
273
+ ORDER BY id ASC;
274
+ `)
275
+ .all(mailboxKey);
276
+ const attachments = listMailboxAttachments(this.db, rows.map((row) => row.id));
277
+ return rows.map((row) => mapMailboxEntryRow(row, attachments.get(row.id) ?? []));
278
+ }
279
+ deleteMailboxEntries(ids) {
280
+ if (ids.length === 0) {
281
+ return;
282
+ }
283
+ for (const id of ids) {
284
+ assertSafeInteger(id, "mailbox entry id");
285
+ }
286
+ const placeholders = ids.map((_, index) => `?${index + 1}`).join(", ");
287
+ this.db.query(`DELETE FROM mailbox_entries WHERE id IN (${placeholders});`).run(...ids);
288
+ }
289
+ getTelegramUpdateOffset() {
290
+ const value = this.getStateValue("telegram.update_offset");
291
+ if (value === null) {
292
+ return null;
293
+ }
294
+ return parseStoredInteger(value, "stored telegram.update_offset");
295
+ }
296
+ putTelegramUpdateOffset(offset, recordedAtMs) {
297
+ assertSafeInteger(offset, "telegram update offset");
298
+ this.putStateValue("telegram.update_offset", String(offset), recordedAtMs);
299
+ }
300
+ getStateValue(key) {
301
+ const row = this.db.query("SELECT value FROM kv_state WHERE key = ?1;").get(key);
302
+ return row?.value ?? null;
303
+ }
304
+ putStateValue(key, value, recordedAtMs) {
305
+ this.db
306
+ .query(`
307
+ INSERT INTO kv_state (key, value, updated_at_ms)
308
+ VALUES (?1, ?2, ?3)
309
+ ON CONFLICT(key) DO UPDATE SET
310
+ value = excluded.value,
311
+ updated_at_ms = excluded.updated_at_ms;
312
+ `)
313
+ .run(key, value, recordedAtMs);
314
+ }
315
+ upsertCronJob(input) {
316
+ assertSafeInteger(input.nextRunAtMs, "cron next_run_at_ms");
317
+ assertSafeInteger(input.recordedAtMs, "cron recordedAtMs");
318
+ this.db
319
+ .query(`
320
+ INSERT INTO cron_jobs (
321
+ id,
322
+ schedule,
323
+ prompt,
324
+ delivery_channel,
325
+ delivery_target,
326
+ delivery_topic,
327
+ enabled,
328
+ next_run_at_ms,
329
+ created_at_ms,
330
+ updated_at_ms
331
+ )
332
+ VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?9)
333
+ ON CONFLICT(id) DO UPDATE SET
334
+ schedule = excluded.schedule,
335
+ prompt = excluded.prompt,
336
+ delivery_channel = excluded.delivery_channel,
337
+ delivery_target = excluded.delivery_target,
338
+ delivery_topic = excluded.delivery_topic,
339
+ enabled = excluded.enabled,
340
+ next_run_at_ms = excluded.next_run_at_ms,
341
+ updated_at_ms = excluded.updated_at_ms;
342
+ `)
343
+ .run(input.id, input.schedule, input.prompt, input.deliveryChannel, input.deliveryTarget, input.deliveryTopic, input.enabled ? 1 : 0, input.nextRunAtMs, input.recordedAtMs);
344
+ }
345
+ getCronJob(id) {
346
+ const row = this.db
347
+ .query(`
348
+ SELECT
349
+ id,
350
+ schedule,
351
+ prompt,
352
+ delivery_channel,
353
+ delivery_target,
354
+ delivery_topic,
355
+ enabled,
356
+ next_run_at_ms,
357
+ created_at_ms,
358
+ updated_at_ms
359
+ FROM cron_jobs
360
+ WHERE id = ?1;
361
+ `)
362
+ .get(id);
363
+ return row ? mapCronJobRow(row) : null;
364
+ }
365
+ listCronJobs() {
366
+ const rows = this.db
367
+ .query(`
368
+ SELECT
369
+ id,
370
+ schedule,
371
+ prompt,
372
+ delivery_channel,
373
+ delivery_target,
374
+ delivery_topic,
375
+ enabled,
376
+ next_run_at_ms,
377
+ created_at_ms,
378
+ updated_at_ms
379
+ FROM cron_jobs
380
+ ORDER BY id ASC;
381
+ `)
382
+ .all();
383
+ return rows.map(mapCronJobRow);
384
+ }
385
+ listOverdueCronJobs(nowMs) {
386
+ assertSafeInteger(nowMs, "cron nowMs");
387
+ const rows = this.db
388
+ .query(`
389
+ SELECT
390
+ id,
391
+ schedule,
392
+ prompt,
393
+ delivery_channel,
394
+ delivery_target,
395
+ delivery_topic,
396
+ enabled,
397
+ next_run_at_ms,
398
+ created_at_ms,
399
+ updated_at_ms
400
+ FROM cron_jobs
401
+ WHERE enabled = 1 AND next_run_at_ms <= ?1
402
+ ORDER BY next_run_at_ms ASC, id ASC;
403
+ `)
404
+ .all(nowMs);
405
+ return rows.map(mapCronJobRow);
406
+ }
407
+ listDueCronJobs(nowMs, limit) {
408
+ assertSafeInteger(nowMs, "cron nowMs");
409
+ assertSafeInteger(limit, "cron due-job limit");
410
+ const rows = this.db
411
+ .query(`
412
+ SELECT
413
+ id,
414
+ schedule,
415
+ prompt,
416
+ delivery_channel,
417
+ delivery_target,
418
+ delivery_topic,
419
+ enabled,
420
+ next_run_at_ms,
421
+ created_at_ms,
422
+ updated_at_ms
423
+ FROM cron_jobs
424
+ WHERE enabled = 1 AND next_run_at_ms <= ?1
425
+ ORDER BY next_run_at_ms ASC, id ASC
426
+ LIMIT ?2;
427
+ `)
428
+ .all(nowMs, limit);
429
+ return rows.map(mapCronJobRow);
430
+ }
431
+ removeCronJob(id) {
432
+ const result = this.db.query("DELETE FROM cron_jobs WHERE id = ?1;").run(id);
433
+ return result.changes > 0;
434
+ }
435
+ updateCronJobNextRun(id, nextRunAtMs, recordedAtMs) {
436
+ assertSafeInteger(nextRunAtMs, "cron next_run_at_ms");
437
+ assertSafeInteger(recordedAtMs, "cron recordedAtMs");
438
+ this.db
439
+ .query(`
440
+ UPDATE cron_jobs
441
+ SET next_run_at_ms = ?2, updated_at_ms = ?3
442
+ WHERE id = ?1;
443
+ `)
444
+ .run(id, nextRunAtMs, recordedAtMs);
445
+ }
446
+ insertCronRun(jobId, scheduledForMs, startedAtMs) {
447
+ assertSafeInteger(scheduledForMs, "cron scheduled_for_ms");
448
+ assertSafeInteger(startedAtMs, "cron started_at_ms");
449
+ const result = this.db
450
+ .query(`
451
+ INSERT INTO cron_runs (
452
+ job_id,
453
+ scheduled_for_ms,
454
+ started_at_ms,
455
+ finished_at_ms,
456
+ status,
457
+ response_text,
458
+ error_message
459
+ )
460
+ VALUES (?1, ?2, ?3, NULL, 'running', NULL, NULL);
461
+ `)
462
+ .run(jobId, scheduledForMs, startedAtMs);
463
+ return Number(result.lastInsertRowid);
464
+ }
465
+ finishCronRun(runId, status, finishedAtMs, responseText, errorMessage) {
466
+ assertSafeInteger(runId, "cron run id");
467
+ assertSafeInteger(finishedAtMs, "cron finished_at_ms");
468
+ this.db
469
+ .query(`
470
+ UPDATE cron_runs
471
+ SET
472
+ finished_at_ms = ?2,
473
+ status = ?3,
474
+ response_text = ?4,
475
+ error_message = ?5
476
+ WHERE id = ?1;
477
+ `)
478
+ .run(runId, finishedAtMs, status, responseText, errorMessage);
479
+ }
480
+ abandonRunningCronRuns(finishedAtMs) {
481
+ assertSafeInteger(finishedAtMs, "cron abandoned finished_at_ms");
482
+ const result = this.db
483
+ .query(`
484
+ UPDATE cron_runs
485
+ SET
486
+ finished_at_ms = ?1,
487
+ status = 'abandoned',
488
+ error_message = 'gateway process stopped before this run completed'
489
+ WHERE status = 'running';
490
+ `)
491
+ .run(finishedAtMs);
492
+ return result.changes;
493
+ }
494
+ close() {
495
+ this.db.close();
496
+ }
497
+ }
498
+ function mapCronJobRow(row) {
499
+ return {
500
+ id: row.id,
501
+ schedule: row.schedule,
502
+ prompt: row.prompt,
503
+ deliveryChannel: row.delivery_channel,
504
+ deliveryTarget: row.delivery_target,
505
+ deliveryTopic: row.delivery_topic,
506
+ enabled: row.enabled === 1,
507
+ nextRunAtMs: row.next_run_at_ms,
508
+ createdAtMs: row.created_at_ms,
509
+ updatedAtMs: row.updated_at_ms,
510
+ };
511
+ }
512
+ function mapMailboxEntryRow(row, attachments) {
513
+ return {
514
+ id: row.id,
515
+ mailboxKey: row.mailbox_key,
516
+ sourceKind: row.source_kind,
517
+ externalId: row.external_id,
518
+ sender: row.sender,
519
+ text: normalizeStoredMailboxText(row.body),
520
+ attachments,
521
+ replyChannel: row.reply_channel,
522
+ replyTarget: row.reply_target,
523
+ replyTopic: row.reply_topic,
524
+ createdAtMs: row.created_at_ms,
525
+ };
526
+ }
527
+ function listMailboxAttachments(db, entryIds) {
528
+ if (entryIds.length === 0) {
529
+ return new Map();
530
+ }
531
+ for (const entryId of entryIds) {
532
+ assertSafeInteger(entryId, "mailbox entry id");
533
+ }
534
+ const placeholders = entryIds.map((_, index) => `?${index + 1}`).join(", ");
535
+ const rows = db
536
+ .query(`
537
+ SELECT
538
+ mailbox_entry_id,
539
+ ordinal,
540
+ kind,
541
+ mime_type,
542
+ file_name,
543
+ local_path
544
+ FROM mailbox_entry_attachments
545
+ WHERE mailbox_entry_id IN (${placeholders})
546
+ ORDER BY mailbox_entry_id ASC, ordinal ASC;
547
+ `)
548
+ .all(...entryIds);
549
+ const attachments = new Map();
550
+ for (const row of rows) {
551
+ const records = attachments.get(row.mailbox_entry_id) ?? [];
552
+ records.push(mapMailboxEntryAttachmentRow(row));
553
+ attachments.set(row.mailbox_entry_id, records);
554
+ }
555
+ return attachments;
556
+ }
557
+ function mapMailboxEntryAttachmentRow(row) {
558
+ switch (row.kind) {
559
+ case "image":
560
+ return {
561
+ kind: "image",
562
+ ordinal: row.ordinal,
563
+ mimeType: row.mime_type,
564
+ fileName: row.file_name,
565
+ localPath: row.local_path,
566
+ };
567
+ default:
568
+ throw new Error(`unsupported mailbox attachment kind: ${row.kind}`);
569
+ }
570
+ }
571
+ function mapSessionReplyTargetRow(row) {
572
+ return {
573
+ channel: row.delivery_channel,
574
+ target: row.delivery_target,
575
+ topic: normalizeStoredKeyField(row.delivery_topic),
576
+ };
577
+ }
578
+ function mapPendingQuestionRow(row) {
579
+ return {
580
+ requestId: row.request_id,
581
+ sessionId: row.session_id,
582
+ questions: parsePendingQuestions(row.question_json),
583
+ deliveryTarget: {
584
+ channel: row.delivery_channel,
585
+ target: row.delivery_target,
586
+ topic: normalizeStoredKeyField(row.delivery_topic),
587
+ },
588
+ telegramMessageId: row.telegram_message_id,
589
+ createdAtMs: row.created_at_ms,
590
+ };
591
+ }
592
+ function parsePendingQuestions(value) {
593
+ const parsed = JSON.parse(value);
594
+ if (!Array.isArray(parsed)) {
595
+ throw new Error("stored pending question payload is invalid");
596
+ }
597
+ return parsed.map((question, index) => {
598
+ if (typeof question !== "object" || question === null) {
599
+ throw new Error(`stored pending question ${index} is invalid`);
600
+ }
601
+ const header = readRequiredStringField(question, "header");
602
+ const prompt = readRequiredStringField(question, "question");
603
+ const rawOptions = readArrayField(question, "options");
604
+ const options = rawOptions.map((option, optionIndex) => {
605
+ if (typeof option !== "object" || option === null) {
606
+ throw new Error(`stored pending question option ${index}:${optionIndex} is invalid`);
607
+ }
608
+ return {
609
+ label: readRequiredStringField(option, "label"),
610
+ description: readRequiredStringField(option, "description"),
611
+ };
612
+ });
613
+ return {
614
+ header,
615
+ question: prompt,
616
+ options,
617
+ multiple: readBooleanField(question, "multiple", false),
618
+ custom: readBooleanField(question, "custom", true),
619
+ };
620
+ });
621
+ }
622
+ function assertSafeInteger(value, field) {
623
+ if (!Number.isSafeInteger(value) || value < 0) {
624
+ throw new Error(`${field} is out of range: ${value}`);
625
+ }
626
+ }
627
+ function parseStoredInteger(value, field) {
628
+ const parsed = Number.parseInt(value, 10);
629
+ if (!Number.isSafeInteger(parsed) || parsed < 0) {
630
+ throw new Error(`${field} is invalid: ${value}`);
631
+ }
632
+ return parsed;
633
+ }
634
+ function normalizeStoredMailboxText(value) {
635
+ const trimmed = value.trim();
636
+ return trimmed.length === 0 ? null : trimmed;
637
+ }
638
+ function normalizeKeyField(value) {
639
+ if (value === null) {
640
+ return "";
641
+ }
642
+ const trimmed = value.trim();
643
+ return trimmed.length === 0 ? "" : trimmed;
644
+ }
645
+ function normalizeStoredKeyField(value) {
646
+ const trimmed = value.trim();
647
+ return trimmed.length === 0 ? null : trimmed;
648
+ }
649
+ function readRequiredStringField(value, field) {
650
+ const raw = value[field];
651
+ if (typeof raw !== "string" || raw.trim().length === 0) {
652
+ throw new Error(`stored field ${field} is invalid`);
653
+ }
654
+ return raw;
655
+ }
656
+ function readArrayField(value, field) {
657
+ const raw = value[field];
658
+ if (!Array.isArray(raw)) {
659
+ throw new Error(`stored field ${field} is invalid`);
660
+ }
661
+ return raw;
662
+ }
663
+ function readBooleanField(value, field, fallback) {
664
+ const raw = value[field];
665
+ if (raw === undefined) {
666
+ return fallback;
667
+ }
668
+ if (typeof raw !== "boolean") {
669
+ throw new Error(`stored field ${field} is invalid`);
670
+ }
671
+ return raw;
672
+ }
673
+ export async function openSqliteStore(path) {
674
+ await mkdir(dirname(path), { recursive: true });
675
+ const db = new Database(path);
676
+ migrateGatewayDatabase(db);
677
+ return new SqliteStore(db);
678
+ }