thepopebot 1.2.76-beta.2 → 1.2.76-beta.21

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 (128) hide show
  1. package/README.md +3 -3
  2. package/api/CLAUDE.md +11 -4
  3. package/api/index.js +56 -18
  4. package/bin/CLAUDE.md +7 -4
  5. package/bin/cli.js +25 -45
  6. package/config/CLAUDE.md +23 -4
  7. package/drizzle/0021_coding_agent_workspace.sql +1 -0
  8. package/drizzle/0022_organic_apocalypse.sql +16 -0
  9. package/drizzle/0023_needy_ender_wiggin.sql +1 -0
  10. package/drizzle/meta/0021_snapshot.json +639 -0
  11. package/drizzle/meta/0022_snapshot.json +743 -0
  12. package/drizzle/meta/0023_snapshot.json +750 -0
  13. package/drizzle/meta/_journal.json +21 -0
  14. package/lib/CLAUDE.md +2 -2
  15. package/lib/actions.js +9 -1
  16. package/lib/ai/CLAUDE.md +72 -57
  17. package/lib/ai/helper-llm.js +108 -0
  18. package/lib/ai/index.js +308 -438
  19. package/lib/ai/line-mappers.js +42 -24
  20. package/lib/ai/scope.js +26 -0
  21. package/lib/ai/sdk-adapters/CLAUDE.md +114 -0
  22. package/lib/ai/sdk-adapters/claude-code.js +120 -8
  23. package/lib/ai/system-prompt.js +34 -0
  24. package/lib/ai/workspace-setup.js +19 -35
  25. package/lib/channels/CLAUDE.md +14 -4
  26. package/lib/channels/base.js +6 -2
  27. package/lib/channels/commands/index.js +42 -0
  28. package/lib/channels/commands/session.js +53 -0
  29. package/lib/channels/commands/verify.js +18 -0
  30. package/lib/channels/telegram.js +79 -28
  31. package/lib/chat/CLAUDE.md +4 -4
  32. package/lib/chat/actions.js +270 -49
  33. package/lib/chat/api.js +185 -31
  34. package/lib/chat/components/CLAUDE.md +6 -2
  35. package/lib/chat/components/chat-input.js +77 -47
  36. package/lib/chat/components/chat-input.jsx +77 -40
  37. package/lib/chat/components/chat-page.js +2 -0
  38. package/lib/chat/components/chat-page.jsx +3 -0
  39. package/lib/chat/components/chat.js +62 -14
  40. package/lib/chat/components/chat.jsx +68 -10
  41. package/lib/chat/components/code-mode-toggle.js +141 -22
  42. package/lib/chat/components/code-mode-toggle.jsx +129 -20
  43. package/lib/chat/components/containers-page.js +58 -40
  44. package/lib/chat/components/containers-page.jsx +64 -25
  45. package/lib/chat/components/crons-page.js +17 -3
  46. package/lib/chat/components/crons-page.jsx +34 -6
  47. package/lib/chat/components/index.js +2 -2
  48. package/lib/chat/components/message.js +18 -3
  49. package/lib/chat/components/message.jsx +18 -3
  50. package/lib/chat/components/profile-page.js +182 -4
  51. package/lib/chat/components/profile-page.jsx +196 -1
  52. package/lib/chat/components/scope-picker.js +21 -0
  53. package/lib/chat/components/scope-picker.jsx +27 -0
  54. package/lib/chat/components/settings-chat-page.js +11 -11
  55. package/lib/chat/components/settings-chat-page.jsx +14 -18
  56. package/lib/chat/components/settings-coding-agents-page.js +110 -16
  57. package/lib/chat/components/settings-coding-agents-page.jsx +87 -3
  58. package/lib/chat/components/settings-github-page.js +5 -0
  59. package/lib/chat/components/settings-github-page.jsx +5 -0
  60. package/lib/chat/components/settings-layout.js +3 -3
  61. package/lib/chat/components/settings-layout.jsx +3 -3
  62. package/lib/chat/components/settings-secrets-layout.js +1 -2
  63. package/lib/chat/components/settings-secrets-layout.jsx +1 -2
  64. package/lib/chat/components/settings-secrets-page.js +180 -75
  65. package/lib/chat/components/settings-secrets-page.jsx +212 -66
  66. package/lib/chat/components/triggers-page.js +17 -3
  67. package/lib/chat/components/triggers-page.jsx +34 -6
  68. package/lib/chat/components/ui/combobox.js +18 -2
  69. package/lib/chat/components/ui/combobox.jsx +17 -1
  70. package/lib/chat/components/ui/dropdown-menu.js +23 -2
  71. package/lib/chat/components/ui/dropdown-menu.jsx +27 -2
  72. package/lib/chat/telegram-profile.js +33 -0
  73. package/lib/cluster/CLAUDE.md +9 -3
  74. package/lib/code/CLAUDE.md +11 -3
  75. package/lib/code/actions.js +47 -8
  76. package/lib/code/terminal-view.js +31 -21
  77. package/lib/code/terminal-view.jsx +32 -23
  78. package/lib/config.js +15 -4
  79. package/lib/containers/CLAUDE.md +16 -6
  80. package/lib/db/CLAUDE.md +5 -2
  81. package/lib/db/chats.js +9 -17
  82. package/lib/db/code-workspaces.js +8 -3
  83. package/lib/db/config.js +0 -1
  84. package/lib/db/index.js +12 -0
  85. package/lib/db/schema.js +24 -1
  86. package/lib/db/user-channels.js +129 -0
  87. package/lib/llm-providers.js +8 -0
  88. package/lib/maintenance.js +31 -21
  89. package/lib/tools/CLAUDE.md +12 -3
  90. package/lib/tools/assemblyai.js +17 -0
  91. package/lib/tools/create-agent-job.js +12 -8
  92. package/lib/tools/docker.js +34 -10
  93. package/lib/tools/github.js +34 -0
  94. package/lib/tools/telegram.js +106 -0
  95. package/lib/utils/render-md.js +44 -18
  96. package/package.json +8 -8
  97. package/setup/CLAUDE.md +11 -5
  98. package/setup/lib/providers.mjs +2 -1
  99. package/setup/lib/targets.mjs +13 -16
  100. package/setup/lib/telegram.mjs +8 -69
  101. package/templates/.env.example +0 -7
  102. package/templates/.github/workflows/rebuild-event-handler.yml +1 -1
  103. package/templates/.gitignore.template +1 -3
  104. package/templates/CLAUDE.md +1 -1
  105. package/templates/CLAUDE.md.template +29 -7
  106. package/templates/agent-job/CLAUDE.md.template +5 -3
  107. package/templates/agent-job/CRONS.json +16 -0
  108. package/templates/agent-job/SYSTEM.md +16 -11
  109. package/templates/agents/CLAUDE.md.template +17 -17
  110. package/templates/coding-workspace/CLAUDE.md.template +7 -0
  111. package/templates/data/CLAUDE.md.template +1 -1
  112. package/templates/docker-compose.custom.yml +1 -0
  113. package/templates/docker-compose.yml +1 -0
  114. package/templates/event-handler/CLAUDE.md.template +79 -0
  115. package/templates/event-handler/TRIGGERS.json +18 -2
  116. package/templates/skills/CLAUDE.md.template +20 -22
  117. package/templates/skills/{library/agent-job-secrets → agent-job-secrets}/SKILL.md +2 -2
  118. package/lib/ai/agent.js +0 -65
  119. package/lib/ai/async-channel.js +0 -51
  120. package/lib/ai/model.js +0 -130
  121. package/lib/ai/tools.js +0 -164
  122. package/lib/tools/openai.js +0 -37
  123. package/setup/lib/telegram-verify.mjs +0 -63
  124. package/setup/setup-telegram.mjs +0 -260
  125. package/templates/agent-job/SOUL.md +0 -17
  126. /package/templates/{skills/active/.gitkeep → coding-workspace/SYSTEM.md} +0 -0
  127. /package/templates/skills/{library/agent-job-secrets → agent-job-secrets}/agent-job-secrets.js +0 -0
  128. /package/templates/skills/{library/playwright-cli → playwright-cli}/SKILL.md +0 -0
@@ -0,0 +1,42 @@
1
+ import { verifyCommand } from './verify.js';
2
+ import { sessionCommand } from './session.js';
3
+
4
+ const PRE_AUTH = {
5
+ verify: verifyCommand,
6
+ };
7
+
8
+ const POST_AUTH = {
9
+ session: sessionCommand,
10
+ };
11
+
12
+ function parse(text) {
13
+ const trimmed = (text || '').trim();
14
+ if (!trimmed.startsWith('/')) return null;
15
+ const [head, ...rest] = trimmed.slice(1).split(/\s+/);
16
+ return { name: head.toLowerCase(), args: rest };
17
+ }
18
+
19
+ /**
20
+ * Dispatch a pre-auth command. Returns { handled, reply, userId? } or null.
21
+ * Called when the incoming channel chat is not yet bound to a user.
22
+ * Only /verify runs here.
23
+ */
24
+ export async function dispatchPreAuthCommand(normalized, ctx) {
25
+ const parsed = parse(normalized.text);
26
+ if (!parsed) return null;
27
+ const handler = PRE_AUTH[parsed.name];
28
+ if (!handler) return null;
29
+ return handler({ args: parsed.args, normalized, ctx });
30
+ }
31
+
32
+ /**
33
+ * Dispatch a post-auth command. Returns { handled, reply } or null.
34
+ * ctx must include { userId, channel, channelChatId }.
35
+ */
36
+ export async function dispatchCommand(normalized, ctx) {
37
+ const parsed = parse(normalized.text);
38
+ if (!parsed) return null;
39
+ const handler = POST_AUTH[parsed.name];
40
+ if (!handler) return null;
41
+ return handler({ args: parsed.args, normalized, ctx });
42
+ }
@@ -0,0 +1,53 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { setActiveThread } from '../../db/user-channels.js';
3
+ import { getChatById, getChatsByUser } from '../../db/chats.js';
4
+
5
+ function shortId(id) {
6
+ return id.replace(/-/g, '').slice(0, 8);
7
+ }
8
+
9
+ /**
10
+ * /session → mint a new session UUID, mark it active
11
+ * /session list → list the user's 10 most-recent chats
12
+ * /session <id|short> → switch active to an existing chat (full uuid or 8-char short id)
13
+ */
14
+ export async function sessionCommand({ args, ctx }) {
15
+ const sub = args[0];
16
+
17
+ if (!sub) {
18
+ const threadId = randomUUID();
19
+ setActiveThread(ctx.userId, ctx.channel, threadId);
20
+ return {
21
+ handled: true,
22
+ reply: `New session: ${shortId(threadId)}\nSend a message to begin.`,
23
+ };
24
+ }
25
+
26
+ if (sub.toLowerCase() === 'list') {
27
+ const chats = getChatsByUser(ctx.userId).slice(0, 10);
28
+ if (!chats.length) {
29
+ return { handled: true, reply: 'No sessions yet. Send a message or use /session to start one.' };
30
+ }
31
+ const lines = chats.map((c) => `${shortId(c.id)} ${c.title}`);
32
+ return { handled: true, reply: `Recent sessions:\n${lines.join('\n')}` };
33
+ }
34
+
35
+ const target = resolveChatId(sub, ctx.userId);
36
+ if (!target) {
37
+ return { handled: true, reply: `Session not found: ${sub}` };
38
+ }
39
+ setActiveThread(ctx.userId, ctx.channel, target.id);
40
+ return { handled: true, reply: `Switched to session: ${shortId(target.id)}\n${target.title}` };
41
+ }
42
+
43
+ function resolveChatId(input, userId) {
44
+ // Full UUID match first
45
+ const direct = getChatById(input);
46
+ if (direct && direct.userId === userId) return direct;
47
+ // Short-id match (8-char prefix of UUID minus dashes)
48
+ if (input.length === 8) {
49
+ const chats = getChatsByUser(userId);
50
+ return chats.find((c) => shortId(c.id) === input.toLowerCase()) || null;
51
+ }
52
+ return null;
53
+ }
@@ -0,0 +1,18 @@
1
+ import { redeemCode } from '../../db/user-channels.js';
2
+
3
+ /**
4
+ * /verify <code>
5
+ * Pre-auth: binds the incoming channel chat to the user who issued the code.
6
+ */
7
+ export async function verifyCommand({ args, ctx }) {
8
+ const [code] = args;
9
+ if (!code) {
10
+ return { handled: true, reply: 'Usage: /verify <code>' };
11
+ }
12
+ try {
13
+ const { userId } = redeemCode(ctx.channel, code, ctx.channelChatId);
14
+ return { handled: true, reply: 'Linked. Send a message to start chatting.', userId };
15
+ } catch (err) {
16
+ return { handled: true, reply: `Verification failed: ${err.message}` };
17
+ }
18
+ }
@@ -4,8 +4,10 @@ import {
4
4
  downloadFile,
5
5
  reactToMessage,
6
6
  startTypingIndicator,
7
+ formatToolCall,
7
8
  } from '../tools/telegram.js';
8
- import { isWhisperEnabled, transcribeAudio } from '../tools/openai.js';
9
+ import { isAssemblyAIEnabled, transcribeAudio } from '../tools/assemblyai.js';
10
+ import { getConfig } from '../config.js';
9
11
 
10
12
  class TelegramAdapter extends ChannelAdapter {
11
13
  constructor(botToken) {
@@ -17,17 +19,21 @@ class TelegramAdapter extends ChannelAdapter {
17
19
  * Parse a Telegram webhook update into normalized message data.
18
20
  * Handles: text, voice/audio (transcribed), photos, documents.
19
21
  * Returns null if the update should be ignored.
22
+ *
23
+ * Does NOT authorize the chat — auth is handled downstream by resolving
24
+ * `channelChatId` against `user_channels`. This adapter only validates
25
+ * that the request came from Telegram (webhook secret) and extracts the
26
+ * payload.
20
27
  */
21
28
  async receive(request) {
22
- const { TELEGRAM_WEBHOOK_SECRET, TELEGRAM_CHAT_ID, TELEGRAM_VERIFICATION } = process.env;
29
+ const webhookSecret = getConfig('TELEGRAM_WEBHOOK_SECRET');
23
30
 
24
- // Validate secret token (required)
25
- if (!TELEGRAM_WEBHOOK_SECRET) {
31
+ if (!webhookSecret) {
26
32
  console.error('[telegram] TELEGRAM_WEBHOOK_SECRET not configured — rejecting webhook');
27
33
  return null;
28
34
  }
29
35
  const headerSecret = request.headers.get('x-telegram-bot-api-secret-token');
30
- if (headerSecret !== TELEGRAM_WEBHOOK_SECRET) {
36
+ if (headerSecret !== webhookSecret) {
31
37
  return null;
32
38
  }
33
39
 
@@ -40,31 +46,19 @@ class TelegramAdapter extends ChannelAdapter {
40
46
  let text = message.text || null;
41
47
  const attachments = [];
42
48
 
43
- // Check for verification code — works even before TELEGRAM_CHAT_ID is set
44
- if (TELEGRAM_VERIFICATION && text === TELEGRAM_VERIFICATION) {
45
- await sendMessage(this.botToken, chatId, `Your chat ID:\n<code>${chatId}</code>`);
46
- return null;
47
- }
48
-
49
- // Security: if no TELEGRAM_CHAT_ID configured, ignore all messages
50
- if (!TELEGRAM_CHAT_ID) return null;
51
-
52
- // Security: only accept messages from configured chat
53
- if (chatId !== TELEGRAM_CHAT_ID) return null;
54
-
55
49
  // Voice messages → transcribe to text
56
50
  if (message.voice) {
57
- if (!isWhisperEnabled()) {
51
+ if (!isAssemblyAIEnabled()) {
58
52
  await sendMessage(
59
53
  this.botToken,
60
54
  chatId,
61
- 'Voice messages are not supported. Please set OPENAI_API_KEY to enable transcription.'
55
+ 'Voice messages are not supported. Please set ASSEMBLYAI_API_KEY to enable transcription.'
62
56
  );
63
57
  return null;
64
58
  }
65
59
  try {
66
- const { buffer, filename } = await downloadFile(this.botToken, message.voice.file_id);
67
- text = await transcribeAudio(buffer, filename);
60
+ const { buffer } = await downloadFile(this.botToken, message.voice.file_id);
61
+ text = await transcribeAudio(buffer);
68
62
  } catch (err) {
69
63
  console.error('Failed to transcribe voice:', err);
70
64
  await sendMessage(this.botToken, chatId, 'Sorry, I could not transcribe your voice message.');
@@ -74,17 +68,17 @@ class TelegramAdapter extends ChannelAdapter {
74
68
 
75
69
  // Audio messages → transcribe to text
76
70
  if (message.audio && !text) {
77
- if (!isWhisperEnabled()) {
71
+ if (!isAssemblyAIEnabled()) {
78
72
  await sendMessage(
79
73
  this.botToken,
80
74
  chatId,
81
- 'Audio messages are not supported. Please set OPENAI_API_KEY to enable transcription.'
75
+ 'Audio messages are not supported. Please set ASSEMBLYAI_API_KEY to enable transcription.'
82
76
  );
83
77
  return null;
84
78
  }
85
79
  try {
86
- const { buffer, filename } = await downloadFile(this.botToken, message.audio.file_id);
87
- text = await transcribeAudio(buffer, filename);
80
+ const { buffer } = await downloadFile(this.botToken, message.audio.file_id);
81
+ text = await transcribeAudio(buffer);
88
82
  } catch (err) {
89
83
  console.error('Failed to transcribe audio:', err);
90
84
  await sendMessage(this.botToken, chatId, 'Sorry, I could not transcribe your audio message.');
@@ -121,7 +115,8 @@ class TelegramAdapter extends ChannelAdapter {
121
115
  if (!text && attachments.length === 0) return null;
122
116
 
123
117
  return {
124
- threadId: chatId,
118
+ channel: 'telegram',
119
+ channelChatId: chatId,
125
120
  text: text || '',
126
121
  attachments,
127
122
  metadata: { messageId: message.message_id, chatId },
@@ -136,8 +131,64 @@ class TelegramAdapter extends ChannelAdapter {
136
131
  return startTypingIndicator(this.botToken, metadata.chatId);
137
132
  }
138
133
 
139
- async sendResponse(threadId, text, metadata) {
140
- await sendMessage(this.botToken, threadId, text);
134
+ async sendResponse(channelChatId, text, metadata) {
135
+ await sendMessage(this.botToken, channelChatId, text);
136
+ }
137
+
138
+ /**
139
+ * Consume a chatStream() async iterable and send progressive messages.
140
+ * - Text chunks accumulate and flush when a tool-call arrives or stream ends.
141
+ * - Each tool-call sends immediately as its own message.
142
+ * - Tool-results react to the tool-call message with ✅ (or ❌ on error).
143
+ *
144
+ * @param {string} chatId - Telegram chat ID
145
+ * @param {AsyncIterable} chunks - chatStream() output
146
+ */
147
+ async streamChatResponse(chatId, chunks) {
148
+ let textBuffer = '';
149
+ // Map toolCallId → { telegramMessageId, hasCompleteArgs }
150
+ const toolMessages = new Map();
151
+
152
+ for await (const chunk of chunks) {
153
+ if (chunk.type === 'text') {
154
+ textBuffer += chunk.text;
155
+ } else if (chunk.type === 'tool-call') {
156
+ // Skip the first empty-args emission — wait for complete args
157
+ if (!chunk.args || Object.keys(chunk.args).length === 0) {
158
+ continue;
159
+ }
160
+
161
+ // Flush accumulated text before tool call
162
+ if (textBuffer.trim()) {
163
+ await sendMessage(this.botToken, chatId, textBuffer.trim());
164
+ textBuffer = '';
165
+ }
166
+
167
+ // Send tool call as its own message
168
+ const text = formatToolCall(chunk.toolName, chunk.args);
169
+ try {
170
+ const msg = await sendMessage(this.botToken, chatId, text);
171
+ toolMessages.set(chunk.toolCallId, msg.message_id);
172
+ } catch (err) {
173
+ console.error('[telegram] Failed to send tool call:', err.message);
174
+ }
175
+ } else if (chunk.type === 'tool-result') {
176
+ const messageId = toolMessages.get(chunk.toolCallId);
177
+ if (messageId) {
178
+ const emoji = chunk.result?.includes?.('error') || chunk.result?.includes?.('Error')
179
+ ? '👎'
180
+ : '👍';
181
+ reactToMessage(this.botToken, chatId, messageId, emoji).catch(() => {});
182
+ toolMessages.delete(chunk.toolCallId);
183
+ }
184
+ }
185
+ // Skip: meta, result, thinking-*, unknown
186
+ }
187
+
188
+ // Flush remaining text
189
+ if (textBuffer.trim()) {
190
+ await sendMessage(this.botToken, chatId, textBuffer.trim());
191
+ }
141
192
  }
142
193
 
143
194
  get supportsStreaming() {
@@ -38,10 +38,10 @@ export { getRepositoriesHandler as GET } from 'thepopebot/chat/api';
38
38
  ## Chat Streaming Flow
39
39
 
40
40
  1. Client sends message via AI SDK `DefaultChatTransport` → `POST /stream/chat`
41
- 2. Handler validates session, extracts text + file attachments from message parts
42
- 3. Calls `chatStream()` from `lib/ai/` which handles DB persistence and LLM invocation
43
- 4. Streams response chunks (text deltas, tool calls, tool results) via `createUIMessageStream`
44
- 5. After first message, client calls `/chat/finalize-chat` to generate auto-title
41
+ 2. Handler validates session, extracts text + file attachments from message parts. Images and PDFs pass through as vision content; text files are inlined into the prompt.
42
+ 3. Calls `chatStream()` from `lib/ai/` which handles DB persistence and LLM invocation. Two paths: SDK adapter (in-process, e.g. Claude Agent SDK) or direct headless container (other agents).
43
+ 4. Streams response chunks (text deltas, tool calls, tool results, thinking blocks) via `createUIMessageStream`. Tool call/tool result pairs and `{ type: 'error' }` chunks are persisted as JSON message parts.
44
+ 5. After the first user message streams, the client calls `/chat/finalize-chat` to generate the auto-title (helper LLM with truncated-description fallback).
45
45
 
46
46
  ## Server Actions (actions.js)
47
47