thepopebot 1.2.76-beta.33 → 1.2.76-beta.35

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 (36) hide show
  1. package/api/index.js +58 -39
  2. package/drizzle/0025_common_psylocke.sql +51 -0
  3. package/drizzle/meta/0025_snapshot.json +734 -0
  4. package/drizzle/meta/_journal.json +7 -0
  5. package/lib/actions.js +1 -0
  6. package/lib/ai/index.js +26 -36
  7. package/lib/ai/sdk-adapters/claude-code.js +3 -1
  8. package/lib/auth/actions.js +2 -20
  9. package/lib/auth/index.js +20 -0
  10. package/lib/chat/actions.js +61 -60
  11. package/lib/chat/api.js +8 -8
  12. package/lib/chat/components/app-sidebar.js +6 -6
  13. package/lib/chat/components/app-sidebar.jsx +9 -9
  14. package/lib/chat/components/index.js +1 -1
  15. package/lib/chat/components/messages-page.js +138 -0
  16. package/lib/chat/components/messages-page.jsx +180 -0
  17. package/lib/code/actions.js +3 -0
  18. package/lib/db/chats.js +15 -7
  19. package/lib/db/messages.js +117 -0
  20. package/lib/db/schema.js +19 -23
  21. package/lib/db/user-channels.js +2 -1
  22. package/lib/db/users.js +3 -2
  23. package/lib/tools/create-agent-job.js +5 -0
  24. package/lib/tools/docker.js +8 -3
  25. package/package.json +1 -1
  26. package/setup/setup.mjs +2 -1
  27. package/templates/.github/workflows/notify-pr-complete.yml +5 -1
  28. package/templates/agent-job/CLAUDE.md.template +1 -0
  29. package/templates/event-handler/CLAUDE.md.template +4 -2
  30. package/templates/skills/agent-job-background/SKILL.md +2 -1
  31. package/templates/skills/agent-job-background/agent-job-background.js +1 -0
  32. package/templates/skills/agent-job-dm/SKILL.md +39 -17
  33. package/templates/skills/agent-job-dm/agent-job-dm.js +22 -10
  34. package/lib/chat/components/notifications-page.js +0 -89
  35. package/lib/chat/components/notifications-page.jsx +0 -126
  36. package/lib/db/notifications.js +0 -104
package/api/index.js CHANGED
@@ -7,7 +7,7 @@ import { dispatchCommand, dispatchPreAuthCommand } from '../lib/channels/command
7
7
  import { getByChannelChatId, getVerifiedChannels, setActiveThread } from '../lib/db/user-channels.js';
8
8
  import { getAllUsers, getUserById } from '../lib/db/users.js';
9
9
  import { chat, chatStream, summarizeAgentJob } from '../lib/ai/index.js';
10
- import { createNotification } from '../lib/db/notifications.js';
10
+ import { createSystemMessage, getSubscribedAdminIds, markDelivered } from '../lib/db/messages.js';
11
11
  import { getFireTriggers } from '../lib/triggers.js';
12
12
  import { verifyApiKey } from '../lib/db/api-keys.js';
13
13
  import { getConfig } from '../lib/config.js';
@@ -94,6 +94,7 @@ async function handleCreateAgentJob(request) {
94
94
  llmModel: body.llm_model,
95
95
  agentBackend: body.agent_backend,
96
96
  scope: body.scope,
97
+ userId: body.user_id,
97
98
  });
98
99
  return Response.json(result);
99
100
  } catch (err) {
@@ -179,6 +180,48 @@ async function handleListUsers() {
179
180
  return Response.json({ users: enriched });
180
181
  }
181
182
 
183
+ /**
184
+ * Push a stored message to the recipient's default channel.
185
+ * On success: stamp `deliveredAt`. On failure: log; row stays undelivered.
186
+ * Best-effort, async — caller does not await.
187
+ */
188
+ async function pushToDefaultChannel(row) {
189
+ try {
190
+ const verified = getVerifiedChannels(row.userId);
191
+ if (verified.length === 0) return; // No channel — row remains in inbox only
192
+ const target = verified[0]; // Ordered by verifiedAt ASC; first verified is default
193
+
194
+ if (target.channel === 'telegram') {
195
+ const botToken = getTelegramBotToken();
196
+ if (!botToken) {
197
+ console.error(`[pushToDefaultChannel] Telegram token not configured for user ${row.userId}`);
198
+ return;
199
+ }
200
+ const adapter = getTelegramAdapter(botToken);
201
+ await adapter.sendResponse(target.channelChatId, row.content, { chatId: target.channelChatId });
202
+ markDelivered(row.id);
203
+ }
204
+ } catch (err) {
205
+ console.error(`[pushToDefaultChannel] failed for user ${row.userId}:`, err.message);
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Dispatch a system message: store + deliver.
211
+ * - userId set → 1 row to that user, pushed to their default channel.
212
+ * - userId absent → fan-out: 1 row per admin where subscribedToSystemMessages=1, each pushed.
213
+ * Returns the count of rows written.
214
+ */
215
+ function dispatchSystemMessage({ userId, content, payload }) {
216
+ const recipients = userId ? [userId] : getSubscribedAdminIds();
217
+ const rows = recipients.map((uid) => createSystemMessage(uid, content, payload));
218
+ // Fire-and-forget delivery; deliveredAt updated per row when each push lands.
219
+ for (const row of rows) {
220
+ pushToDefaultChannel(row);
221
+ }
222
+ return rows.length;
223
+ }
224
+
182
225
  async function handleSendDm(request) {
183
226
  let body;
184
227
  try {
@@ -187,47 +230,18 @@ async function handleSendDm(request) {
187
230
  return Response.json({ error: 'Invalid JSON body' }, { status: 400 });
188
231
  }
189
232
 
190
- const { user_id, message, channel } = body;
191
- if (!user_id) return Response.json({ error: 'Missing user_id' }, { status: 400 });
233
+ const { user_id, message, payload } = body;
192
234
  if (!message || typeof message !== 'string') {
193
235
  return Response.json({ error: 'Missing message' }, { status: 400 });
194
236
  }
195
237
 
196
- const user = getUserById(user_id);
197
- if (!user) return Response.json({ error: 'User not found' }, { status: 404 });
198
-
199
- const verified = getVerifiedChannels(user_id);
200
- if (verified.length === 0) {
201
- return Response.json({ error: 'User has no verified DM channels' }, { status: 409 });
202
- }
203
-
204
- // Pick channel: explicit name, or 'default' / omitted = first verified (Telegram preferred while it's the only impl).
205
- const wantDefault = !channel || channel === 'default';
206
- const target = wantDefault
207
- ? (verified.find((c) => c.channel === 'telegram') || verified[0])
208
- : verified.find((c) => c.channel === channel);
209
-
210
- if (!target) {
211
- return Response.json(
212
- { error: `User has no verified ${channel} channel`, available: verified.map((c) => c.channel) },
213
- { status: 409 }
214
- );
238
+ if (user_id) {
239
+ const user = getUserById(user_id);
240
+ if (!user) return Response.json({ error: 'User not found' }, { status: 404 });
215
241
  }
216
242
 
217
- if (target.channel === 'telegram') {
218
- const botToken = getTelegramBotToken();
219
- if (!botToken) return Response.json({ error: 'Telegram bot token not configured' }, { status: 500 });
220
- const adapter = getTelegramAdapter(botToken);
221
- try {
222
- await adapter.sendResponse(target.channelChatId, message, { chatId: target.channelChatId });
223
- return Response.json({ ok: true, channel: 'telegram', user_id });
224
- } catch (err) {
225
- console.error('Failed to send DM:', err);
226
- return Response.json({ error: 'Failed to send DM' }, { status: 502 });
227
- }
228
- }
229
-
230
- return Response.json({ error: `Channel "${target.channel}" not implemented` }, { status: 501 });
243
+ const count = dispatchSystemMessage({ userId: user_id || null, content: message, payload });
244
+ return Response.json({ ok: true, recipients: count });
231
245
  }
232
246
 
233
247
  async function handleListAgentSecrets(request) {
@@ -369,11 +383,16 @@ async function handleGithubWebhook(request) {
369
383
  };
370
384
 
371
385
  const message = await summarizeAgentJob(results);
372
- await createNotification(message, payload);
386
+ const recipientUserId = payload.user_id || null;
387
+ const count = dispatchSystemMessage({
388
+ userId: recipientUserId,
389
+ content: message,
390
+ payload,
391
+ });
373
392
 
374
- console.log(`Notification saved for agent-job ${agentJobId.slice(0, 8)}`);
393
+ console.log(`Notified ${count} recipient(s) for agent-job ${agentJobId.slice(0, 8)}${recipientUserId ? ` (user ${recipientUserId.slice(0, 8)})` : ' (broadcast)'}`);
375
394
 
376
- return Response.json({ ok: true, notified: true });
395
+ return Response.json({ ok: true, notified: true, recipients: count });
377
396
  } catch (err) {
378
397
  console.error('Failed to process GitHub webhook:', err);
379
398
  return Response.json({ error: 'Failed to process webhook' }, { status: 500 });
@@ -0,0 +1,51 @@
1
+ -- Drop orphan chats (legacy 'telegram'/'unknown' userId literals) and their messages.
2
+ -- Forward-only: never user-owned data.
3
+ DELETE FROM `messages` WHERE `chat_id` IN (SELECT `id` FROM `chats` WHERE `user_id` IN ('telegram','unknown'));--> statement-breakpoint
4
+ DELETE FROM `chats` WHERE `user_id` IN ('telegram','unknown');--> statement-breakpoint
5
+
6
+ -- Drop legacy notification tables (folded into messages).
7
+ DROP TABLE `notifications`;--> statement-breakpoint
8
+ DROP TABLE `subscriptions`;--> statement-breakpoint
9
+
10
+ -- Recreate messages with new shape, backfilling user_id from chats join.
11
+ -- Orphan messages (chat row missing) are dropped by the INNER JOIN.
12
+ PRAGMA foreign_keys=OFF;--> statement-breakpoint
13
+ CREATE TABLE `__new_messages` (
14
+ `id` text PRIMARY KEY NOT NULL,
15
+ `chat_id` text,
16
+ `user_id` text NOT NULL,
17
+ `role` text NOT NULL,
18
+ `content` text NOT NULL,
19
+ `payload` text,
20
+ `read` integer DEFAULT 0 NOT NULL,
21
+ `delivered_at` integer,
22
+ `created_at` integer NOT NULL
23
+ );
24
+ --> statement-breakpoint
25
+ INSERT INTO `__new_messages` (`id`, `chat_id`, `user_id`, `role`, `content`, `payload`, `read`, `delivered_at`, `created_at`)
26
+ SELECT `m`.`id`, `m`.`chat_id`, `c`.`user_id`, `m`.`role`, `m`.`content`, NULL, 0, NULL, `m`.`created_at`
27
+ FROM `messages` `m` INNER JOIN `chats` `c` ON `c`.`id` = `m`.`chat_id`;--> statement-breakpoint
28
+ DROP TABLE `messages`;--> statement-breakpoint
29
+ ALTER TABLE `__new_messages` RENAME TO `messages`;--> statement-breakpoint
30
+ PRAGMA foreign_keys=ON;--> statement-breakpoint
31
+ CREATE INDEX `messages_inbox_lookup` ON `messages` (`user_id`,`read`,`created_at`);--> statement-breakpoint
32
+
33
+ -- Recreate users with subscribed_to_system_messages column (defaults to 1 for existing rows).
34
+ CREATE TABLE `__new_users` (
35
+ `id` text PRIMARY KEY NOT NULL,
36
+ `email` text NOT NULL,
37
+ `password_hash` text NOT NULL,
38
+ `role` text DEFAULT 'user' NOT NULL,
39
+ `first_name` text,
40
+ `last_name` text,
41
+ `nickname` text,
42
+ `subscribed_to_system_messages` integer DEFAULT 1 NOT NULL,
43
+ `created_at` integer NOT NULL,
44
+ `updated_at` integer NOT NULL
45
+ );
46
+ --> statement-breakpoint
47
+ INSERT INTO `__new_users` (`id`, `email`, `password_hash`, `role`, `first_name`, `last_name`, `nickname`, `subscribed_to_system_messages`, `created_at`, `updated_at`)
48
+ SELECT `id`, `email`, `password_hash`, `role`, `first_name`, `last_name`, `nickname`, 1, `created_at`, `updated_at` FROM `users`;--> statement-breakpoint
49
+ DROP TABLE `users`;--> statement-breakpoint
50
+ ALTER TABLE `__new_users` RENAME TO `users`;--> statement-breakpoint
51
+ CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);