heyio 0.19.0 → 0.21.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.
@@ -14,13 +14,25 @@ import { requireAuth } from "./auth.js";
14
14
  import { listSchedules, getSchedule, deleteSchedule, setScheduleEnabled } from "../store/schedules.js";
15
15
  import { listIoSchedules, getIoSchedule, deleteIoSchedule, setIoScheduleEnabled } from "../store/io-schedules.js";
16
16
  import { getScheduleRuns } from "../store/schedule-runs.js";
17
- import { createInboxEntry, listInboxEntries, deleteInboxEntry, countInboxEntries } from "../store/inbox.js";
17
+ import { createFeedEntry, listFeedEntries, countUnreadFeedEntries, markFeedEntryRead, markAllFeedEntriesRead, deleteFeedEntry, markFeedEntriesRead, deleteFeedEntries } from "../store/feed.js";
18
18
  import { listPages, readPage } from "../wiki/fs.js";
19
19
  import { runScheduleNow } from "../copilot/scheduler.js";
20
20
  import { runIoScheduleNow } from "../copilot/io-scheduler.js";
21
- import { listRecentNotifications, listUnreadNotifications, countUnreadNotifications, markNotificationRead, markAllNotificationsRead, } from "../store/notifications.js";
22
21
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
22
  const WEB_DIST = path.resolve(__dirname, "../../web-dist");
23
+ let releasesCache = null;
24
+ function loadReleases() {
25
+ if (releasesCache)
26
+ return releasesCache;
27
+ try {
28
+ const raw = readFileSync(path.join(__dirname, "../releases.json"), "utf-8");
29
+ releasesCache = JSON.parse(raw);
30
+ return releasesCache;
31
+ }
32
+ catch {
33
+ return [];
34
+ }
35
+ }
24
36
  let messageHandler;
25
37
  const sseConnections = new Set();
26
38
  export function setMessageHandler(handler) {
@@ -33,7 +45,7 @@ export function broadcastToSSE(text) {
33
45
  }
34
46
  }
35
47
  export function broadcastNotificationToSSE(payload) {
36
- const data = JSON.stringify({ type: "notification", ...payload });
48
+ const data = JSON.stringify({ type: "feed", ...payload });
37
49
  for (const res of sseConnections) {
38
50
  res.write(`data: ${data}\n\n`);
39
51
  }
@@ -99,24 +111,53 @@ export async function startApiServer() {
99
111
  res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
100
112
  }
101
113
  });
102
- // Inbox read endpoints
103
- api.get("/inbox/count", (_req, res) => {
114
+ // Feed endpoints — unified deliverables + notifications feed
115
+ api.get("/feed/count", (req, res) => {
104
116
  try {
105
- const count = countInboxEntries();
117
+ const rawType = req.query.type;
118
+ const type = rawType === "deliverable" || rawType === "notification"
119
+ ? rawType
120
+ : undefined;
121
+ const count = countUnreadFeedEntries(type);
106
122
  res.json({ count });
107
123
  }
108
124
  catch (e) {
109
- console.error("Error counting inbox entries:", e);
125
+ console.error("Error counting feed entries:", e);
110
126
  res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
111
127
  }
112
128
  });
113
- api.get("/inbox", (_req, res) => {
129
+ api.get("/feed", (req, res) => {
114
130
  try {
115
- const entries = listInboxEntries();
116
- res.json({ entries });
131
+ const rawType = req.query.type;
132
+ const type = rawType === "deliverable" || rawType === "notification"
133
+ ? rawType
134
+ : undefined;
135
+ const unreadOnly = req.query.unread === "true";
136
+ const rawLimit = req.query.limit;
137
+ const parsed = typeof rawLimit === "string" ? Number.parseInt(rawLimit, 10) : NaN;
138
+ const limit = Number.isFinite(parsed) && parsed > 0 ? Math.min(parsed, 200) : 50;
139
+ const rows = listFeedEntries({ type, unreadOnly, limit });
140
+ const unreadCount = countUnreadFeedEntries(type);
141
+ const entries = rows.map(({ id, type: entryType, title, body, created_at, read_at, source_type, source_ref }) => {
142
+ let source = null;
143
+ if (source_type) {
144
+ source = { type: source_type };
145
+ if (source_ref) {
146
+ try {
147
+ const parsedRef = JSON.parse(source_ref);
148
+ source = { type: source_type, ...parsedRef };
149
+ }
150
+ catch {
151
+ // source_ref is not valid JSON — fall back to type-only
152
+ }
153
+ }
154
+ }
155
+ return { id, type: entryType, title, body, created_at, read_at, source };
156
+ });
157
+ res.json({ entries, unreadCount });
117
158
  }
118
159
  catch (e) {
119
- console.error("Error listing inbox entries:", e);
160
+ console.error("Error listing feed entries:", e);
120
161
  res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
121
162
  }
122
163
  });
@@ -124,38 +165,9 @@ export async function startApiServer() {
124
165
  api.get("/status", (_req, res) => {
125
166
  res.json({ version: IO_VERSION, uptime: process.uptime() });
126
167
  });
127
- // Notifications endpoint
128
- api.get("/notifications", (_req, res) => {
129
- try {
130
- const unreadOnly = _req.query.unread === "true";
131
- const rows = unreadOnly
132
- ? listUnreadNotifications()
133
- : (() => {
134
- const rawLimit = _req.query.limit;
135
- const parsed = typeof rawLimit === "string" ? Number.parseInt(rawLimit, 10) : NaN;
136
- const limit = Number.isFinite(parsed) && parsed > 0 ? Math.min(parsed, 200) : 50;
137
- return listRecentNotifications(limit);
138
- })();
139
- const unreadCount = countUnreadNotifications();
140
- const notifications = rows.map(({ id, title, text, created_at, read_at, source_type, source_ref }) => {
141
- let source = { type: source_type };
142
- if (source_ref) {
143
- try {
144
- const parsed = JSON.parse(source_ref);
145
- source = { type: source_type, ...parsed };
146
- }
147
- catch {
148
- // source_ref is not valid JSON — fall back to type-only
149
- }
150
- }
151
- return { id, title, text, created_at, read_at, source };
152
- });
153
- res.json({ notifications, unreadCount });
154
- }
155
- catch (e) {
156
- console.error("Error listing notifications:", e);
157
- res.status(500).json({ error: "Failed to list notifications" });
158
- }
168
+ // Releases endpoint — serves build-time bundled GitHub release notes
169
+ api.get("/releases", (_req, res) => {
170
+ res.json({ releases: loadReleases() });
159
171
  });
160
172
  // SSE events endpoint
161
173
  api.get("/events", (req, res) => {
@@ -238,9 +250,78 @@ export async function startApiServer() {
238
250
  res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
239
251
  }
240
252
  });
241
- // Inbox write endpoints — auth required
242
- api.post("/inbox", (req, res) => {
243
- const { title, body } = req.body;
253
+ // Feed write endpoints
254
+ // Note: POST /feed/read-all must be before POST /feed/:id/read to avoid route shadowing
255
+ api.post("/feed/read-all", (req, res) => {
256
+ try {
257
+ const rawType = req.query.type;
258
+ const type = rawType === "deliverable" || rawType === "notification"
259
+ ? rawType
260
+ : undefined;
261
+ const marked = markAllFeedEntriesRead(type);
262
+ res.json({ marked });
263
+ }
264
+ catch (e) {
265
+ console.error("Error marking feed entries read:", e);
266
+ res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
267
+ }
268
+ });
269
+ api.post("/feed/batch-read", (req, res) => {
270
+ const { ids } = req.body;
271
+ if (!Array.isArray(ids) || ids.length === 0 || !ids.every((x) => typeof x === "number")) {
272
+ res.status(400).json({ error: "ids must be a non-empty array of numbers" });
273
+ return;
274
+ }
275
+ try {
276
+ const marked = markFeedEntriesRead(ids);
277
+ res.json({ marked });
278
+ }
279
+ catch (e) {
280
+ console.error("Error batch-marking feed entries read:", e);
281
+ res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
282
+ }
283
+ });
284
+ api.post("/feed/batch-delete", (req, res) => {
285
+ const { ids } = req.body;
286
+ if (!Array.isArray(ids) || ids.length === 0 || !ids.every((x) => typeof x === "number")) {
287
+ res.status(400).json({ error: "ids must be a non-empty array of numbers" });
288
+ return;
289
+ }
290
+ try {
291
+ const deleted = deleteFeedEntries(ids);
292
+ res.json({ deleted });
293
+ }
294
+ catch (e) {
295
+ console.error("Error batch-deleting feed entries:", e);
296
+ res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
297
+ }
298
+ });
299
+ api.post("/feed/:id/read", (req, res) => {
300
+ const raw = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
301
+ const id = Number.parseInt(raw, 10);
302
+ if (Number.isNaN(id)) {
303
+ res.status(400).json({ error: "Invalid id" });
304
+ return;
305
+ }
306
+ try {
307
+ const found = markFeedEntryRead(id);
308
+ if (!found) {
309
+ res.status(404).json({ error: "Feed entry not found" });
310
+ return;
311
+ }
312
+ res.json({ ok: true });
313
+ }
314
+ catch (e) {
315
+ console.error("Error marking feed entry read:", e);
316
+ res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
317
+ }
318
+ });
319
+ api.post("/feed", (req, res) => {
320
+ const { type, title, body, source_type, source_ref } = req.body;
321
+ if (type !== "deliverable" && type !== "notification") {
322
+ res.status(400).json({ error: "type must be 'deliverable' or 'notification'" });
323
+ return;
324
+ }
244
325
  if (!title || typeof title !== "string" || title.trim() === "") {
245
326
  res.status(400).json({ error: "Missing or empty required field: title" });
246
327
  return;
@@ -250,15 +331,21 @@ export async function startApiServer() {
250
331
  return;
251
332
  }
252
333
  try {
253
- const entry = createInboxEntry(title.trim(), body.trim());
334
+ const entry = createFeedEntry({
335
+ type: type,
336
+ title: title.trim(),
337
+ body: body.trim(),
338
+ source_type: typeof source_type === "string" ? source_type : undefined,
339
+ source_ref: typeof source_ref === "string" ? source_ref : undefined,
340
+ });
254
341
  res.status(201).json({ entry });
255
342
  }
256
343
  catch (e) {
257
- console.error("Error creating inbox entry:", e);
344
+ console.error("Error creating feed entry:", e);
258
345
  res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
259
346
  }
260
347
  });
261
- api.delete("/inbox/:id", (req, res) => {
348
+ api.delete("/feed/:id", (req, res) => {
262
349
  const raw = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
263
350
  const id = Number.parseInt(raw, 10);
264
351
  if (Number.isNaN(id)) {
@@ -266,15 +353,15 @@ export async function startApiServer() {
266
353
  return;
267
354
  }
268
355
  try {
269
- const deleted = deleteInboxEntry(id);
356
+ const deleted = deleteFeedEntry(id);
270
357
  if (!deleted) {
271
- res.status(404).json({ error: "Inbox entry not found" });
358
+ res.status(404).json({ error: "Feed entry not found" });
272
359
  return;
273
360
  }
274
361
  res.json({ deleted: true });
275
362
  }
276
363
  catch (e) {
277
- console.error("Error deleting inbox entry:", e);
364
+ console.error("Error deleting feed entry:", e);
278
365
  res.status(500).json({ error: e instanceof Error ? e.message : String(e) });
279
366
  }
280
367
  });
@@ -651,36 +738,6 @@ export async function startApiServer() {
651
738
  res.status(500).json({ error: (e instanceof Error ? e.message : String(e)) });
652
739
  }
653
740
  });
654
- api.post("/notifications/read-all", (_req, res) => {
655
- try {
656
- const marked = markAllNotificationsRead();
657
- res.json({ marked });
658
- }
659
- catch (e) {
660
- console.error("Error marking all notifications read:", e);
661
- res.status(500).json({ error: "Failed to mark notifications read" });
662
- }
663
- });
664
- api.post("/notifications/:id/read", (req, res) => {
665
- try {
666
- const rawId = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
667
- const id = Number.parseInt(rawId, 10);
668
- if (Number.isNaN(id)) {
669
- res.status(400).json({ error: "invalid id" });
670
- return;
671
- }
672
- const found = markNotificationRead(id);
673
- if (!found) {
674
- res.status(404).json({ error: "notification not found" });
675
- return;
676
- }
677
- res.json({ ok: true });
678
- }
679
- catch (e) {
680
- console.error("Error marking notification read:", e);
681
- res.status(500).json({ error: "Failed to mark notification read" });
682
- }
683
- });
684
741
  // Chat endpoints
685
742
  api.post("/message", async (req, res) => {
686
743
  const { text } = req.body;
@@ -16,7 +16,7 @@ import { delegateToAgent } from "./agents.js";
16
16
  import { nextRun } from "./cron.js";
17
17
  import { notifyBackground } from "../notify.js";
18
18
  import { startScheduleRun, completeScheduleRun, failScheduleRun } from "../store/schedule-runs.js";
19
- import { createInboxEntry } from "../store/inbox.js";
19
+ import { createFeedEntry } from "../store/feed.js";
20
20
  import { shouldRouteToInbox } from "./tools.js";
21
21
  const TICK_MS = 30_000;
22
22
  const AGENDA_BLOCKS = {
@@ -83,7 +83,7 @@ async function fireSchedule(schedule) {
83
83
  try {
84
84
  await delegateToAgent(squad.slug, prompt, (_taskId, result) => {
85
85
  if (shouldRouteToInbox(prompt)) {
86
- createInboxEntry(`[${squad.slug}] ${schedule.name}`, result);
86
+ createFeedEntry({ type: "deliverable", title: `[${squad.slug}] ${schedule.name}`, body: result });
87
87
  console.error(`[io] Schedule ${schedule.id} result routed to inbox`);
88
88
  completeScheduleRun(run.id, 0);
89
89
  }
@@ -5,7 +5,7 @@ import { readFileSync, writeFileSync, readdirSync, statSync, existsSync, mkdirSy
5
5
  import { join, dirname, resolve } from "path";
6
6
  import { homedir } from "os";
7
7
  import { UNIVERSES } from "./universes.js";
8
- import { createInboxEntry } from "../store/inbox.js";
8
+ import { createFeedEntry } from "../store/feed.js";
9
9
  import { validateCron, nextRun } from "./cron.js";
10
10
  import { createIoSchedule, deleteIoSchedule, getIoSchedule, listIoSchedules, setIoScheduleEnabled, updateIoScheduleNextRun, } from "../store/io-schedules.js";
11
11
  import { runIoScheduleNow } from "./io-scheduler.js";
@@ -399,7 +399,7 @@ export function createTools(deps) {
399
399
  const taskId = await deps.delegateToAgent(slug, task, (id, result) => {
400
400
  console.error(`[io] Agent task ${id} completed for squad ${slug}`);
401
401
  if (shouldRouteToInbox(task)) {
402
- createInboxEntry(`[${slug}] Task result`, result);
402
+ createFeedEntry({ type: "deliverable", title: `[${slug}] Task result`, body: result });
403
403
  console.error(`[io] Task ${id} result routed to inbox`);
404
404
  }
405
405
  }, agent);
package/dist/daemon.js CHANGED
@@ -4,7 +4,7 @@ import { startApiServer, setMessageHandler as setApiHandler, broadcastNotificati
4
4
  import { createBot, startBot, stopBot, sendProactiveMessage, sendBackgroundNotification, setMessageHandler as setTelegramHandler } from "./telegram/bot.js";
5
5
  import { setTelegramSender, setTuiSender, setSseBroadcaster } from "./notify.js";
6
6
  import { pruneOldScheduleRuns } from "./store/schedule-runs.js";
7
- import { pruneOldNotifications } from "./store/notifications.js";
7
+ import { pruneOldFeedEntries } from "./store/feed.js";
8
8
  import { printBackgroundNotification } from "./tui/index.js";
9
9
  import { getDb, closeDb } from "./store/db.js";
10
10
  import { clearStaleTasks } from "./store/tasks.js";
@@ -132,9 +132,9 @@ export async function startDaemon() {
132
132
  const pruneTimer = setInterval(() => {
133
133
  try {
134
134
  const runsDeleted = pruneOldScheduleRuns(PRUNE_RETENTION_DAYS);
135
- const notificationsDeleted = pruneOldNotifications(PRUNE_RETENTION_DAYS);
135
+ const notificationsDeleted = pruneOldFeedEntries(PRUNE_RETENTION_DAYS);
136
136
  if (runsDeleted > 0 || notificationsDeleted > 0) {
137
- console.log(`[prune] Cleaned up ${runsDeleted} schedule runs and ${notificationsDeleted} notifications older than ${PRUNE_RETENTION_DAYS} days`);
137
+ console.log(`[prune] Cleaned up ${runsDeleted} schedule runs and ${notificationsDeleted} feed entries older than ${PRUNE_RETENTION_DAYS} days`);
138
138
  }
139
139
  }
140
140
  catch (err) {
@@ -146,7 +146,7 @@ export async function startDaemon() {
146
146
  const pruneStartup = setTimeout(() => {
147
147
  try {
148
148
  pruneOldScheduleRuns(PRUNE_RETENTION_DAYS);
149
- pruneOldNotifications(PRUNE_RETENTION_DAYS);
149
+ pruneOldFeedEntries(PRUNE_RETENTION_DAYS);
150
150
  }
151
151
  catch { /* best effort */ }
152
152
  }, 5000);
package/dist/notify.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { config } from "./config.js";
2
- import { insertNotification, } from "./store/notifications.js";
2
+ import { createFeedEntry } from "./store/feed.js";
3
3
  const HEARTBEAT_PATTERNS = [
4
4
  /^no active tasks?\.?$/i,
5
5
  /^nothing to report\.?$/i,
@@ -51,11 +51,12 @@ export async function notifyBackground(input) {
51
51
  const sourceRefJson = JSON.stringify(stripType(source));
52
52
  let row;
53
53
  try {
54
- row = insertNotification({
54
+ row = createFeedEntry({
55
+ type: "notification",
56
+ title,
57
+ body: text,
55
58
  source_type: source.type,
56
59
  source_ref: sourceRefJson === "{}" ? null : sourceRefJson,
57
- title,
58
- text,
59
60
  });
60
61
  }
61
62
  catch (err) {
package/dist/store/db.js CHANGED
@@ -160,6 +160,18 @@ GROUP BY agent_slug`,
160
160
  body TEXT NOT NULL,
161
161
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
162
162
  )`,
163
+ `CREATE TABLE IF NOT EXISTS unified_feed (
164
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
165
+ type TEXT NOT NULL CHECK(type IN ('deliverable', 'notification')),
166
+ title TEXT NOT NULL,
167
+ body TEXT NOT NULL,
168
+ source_type TEXT,
169
+ source_ref TEXT,
170
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
171
+ read_at DATETIME
172
+ )`,
173
+ `CREATE INDEX IF NOT EXISTS idx_unified_feed_type ON unified_feed(type, created_at)`,
174
+ `CREATE INDEX IF NOT EXISTS idx_unified_feed_unread ON unified_feed(read_at, created_at)`,
163
175
  ];
164
176
  for (const migration of migrations) {
165
177
  try {
@@ -169,6 +181,26 @@ GROUP BY agent_slug`,
169
181
  // Already applied — ignore
170
182
  }
171
183
  }
184
+ // One-time data migration: copy inbox_entries + background_notifications → unified_feed
185
+ try {
186
+ const migrated = db.prepare("SELECT value FROM io_state WHERE key = 'unified_feed_migrated'").get();
187
+ if (!migrated) {
188
+ db.exec(`
189
+ INSERT OR IGNORE INTO unified_feed (type, title, body, source_type, source_ref, created_at, read_at)
190
+ SELECT 'deliverable', title, body, NULL, NULL, created_at, NULL
191
+ FROM inbox_entries
192
+ `);
193
+ db.exec(`
194
+ INSERT OR IGNORE INTO unified_feed (type, title, body, source_type, source_ref, created_at, read_at)
195
+ SELECT 'notification', title, text, source_type, source_ref, created_at, read_at
196
+ FROM background_notifications
197
+ `);
198
+ db.prepare("INSERT OR REPLACE INTO io_state (key, value) VALUES ('unified_feed_migrated', '1')").run();
199
+ }
200
+ }
201
+ catch {
202
+ // Migration failed (e.g. old tables don't exist yet on a fresh install) — safe to ignore
203
+ }
172
204
  return db;
173
205
  }
174
206
  export function closeDb() {
@@ -0,0 +1,94 @@
1
+ import { getDb } from "./db.js";
2
+ export function createFeedEntry(input) {
3
+ const db = getDb();
4
+ const info = db
5
+ .prepare(`INSERT INTO unified_feed (type, title, body, source_type, source_ref)
6
+ VALUES (?, ?, ?, ?, ?)`)
7
+ .run(input.type, input.title, input.body, input.source_type ?? null, input.source_ref ?? null);
8
+ return db
9
+ .prepare("SELECT * FROM unified_feed WHERE id = ?")
10
+ .get(info.lastInsertRowid);
11
+ }
12
+ export function listFeedEntries(opts) {
13
+ const conditions = [];
14
+ const params = [];
15
+ if (opts?.type) {
16
+ conditions.push("type = ?");
17
+ params.push(opts.type);
18
+ }
19
+ if (opts?.unreadOnly) {
20
+ conditions.push("read_at IS NULL");
21
+ }
22
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
23
+ const limit = opts?.limit ?? 50;
24
+ params.push(limit);
25
+ return getDb()
26
+ .prepare(`SELECT * FROM unified_feed ${where} ORDER BY created_at DESC, id DESC LIMIT ?`)
27
+ .all(...params);
28
+ }
29
+ export function countUnreadFeedEntries(type) {
30
+ if (type) {
31
+ const row = getDb()
32
+ .prepare("SELECT COUNT(*) AS n FROM unified_feed WHERE read_at IS NULL AND type = ?")
33
+ .get(type);
34
+ return row.n;
35
+ }
36
+ const row = getDb()
37
+ .prepare("SELECT COUNT(*) AS n FROM unified_feed WHERE read_at IS NULL")
38
+ .get();
39
+ return row.n;
40
+ }
41
+ export function markFeedEntryRead(id) {
42
+ const db = getDb();
43
+ const info = db
44
+ .prepare("UPDATE unified_feed SET read_at = CURRENT_TIMESTAMP WHERE id = ? AND read_at IS NULL")
45
+ .run(id);
46
+ if (info.changes > 0)
47
+ return true;
48
+ // Idempotent: return true if row exists (already read), false if missing
49
+ const exists = db.prepare("SELECT id FROM unified_feed WHERE id = ?").get(id);
50
+ return exists !== undefined;
51
+ }
52
+ export function markAllFeedEntriesRead(type) {
53
+ if (type) {
54
+ const info = getDb()
55
+ .prepare("UPDATE unified_feed SET read_at = CURRENT_TIMESTAMP WHERE read_at IS NULL AND type = ?")
56
+ .run(type);
57
+ return info.changes;
58
+ }
59
+ const info = getDb()
60
+ .prepare("UPDATE unified_feed SET read_at = CURRENT_TIMESTAMP WHERE read_at IS NULL")
61
+ .run();
62
+ return info.changes;
63
+ }
64
+ export function deleteFeedEntry(id) {
65
+ const info = getDb()
66
+ .prepare("DELETE FROM unified_feed WHERE id = ?")
67
+ .run(id);
68
+ return info.changes > 0;
69
+ }
70
+ export function pruneOldFeedEntries(olderThanDays) {
71
+ const info = getDb()
72
+ .prepare(`DELETE FROM unified_feed WHERE created_at < datetime('now', '-' || ? || ' days')`)
73
+ .run(olderThanDays);
74
+ return info.changes;
75
+ }
76
+ export function markFeedEntriesRead(ids) {
77
+ if (ids.length === 0)
78
+ return 0;
79
+ const placeholders = ids.map(() => "?").join(", ");
80
+ const info = getDb()
81
+ .prepare(`UPDATE unified_feed SET read_at = CURRENT_TIMESTAMP WHERE id IN (${placeholders}) AND read_at IS NULL`)
82
+ .run(...ids);
83
+ return info.changes;
84
+ }
85
+ export function deleteFeedEntries(ids) {
86
+ if (ids.length === 0)
87
+ return 0;
88
+ const placeholders = ids.map(() => "?").join(", ");
89
+ const info = getDb()
90
+ .prepare(`DELETE FROM unified_feed WHERE id IN (${placeholders})`)
91
+ .run(...ids);
92
+ return info.changes;
93
+ }
94
+ //# sourceMappingURL=feed.js.map