thepopebot 1.2.76-beta.8 → 1.2.76-beta.9

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/api/index.js CHANGED
@@ -3,7 +3,7 @@ import { createAgentJob } from '../lib/tools/create-agent-job.js';
3
3
  import { setWebhook } from '../lib/tools/telegram.js';
4
4
  import { getAgentJobStatus, fetchAgentJobLog } from '../lib/tools/github.js';
5
5
  import { getTelegramAdapter } from '../lib/channels/index.js';
6
- import { chat, summarizeAgentJob } from '../lib/ai/index.js';
6
+ import { chat, chatStream, summarizeAgentJob } from '../lib/ai/index.js';
7
7
  import { createNotification } from '../lib/db/notifications.js';
8
8
  import { loadTriggers } from '../lib/triggers.js';
9
9
  import { verifyApiKey } from '../lib/db/api-keys.js';
@@ -211,19 +211,33 @@ async function handleTelegramWebhook(request) {
211
211
  /**
212
212
  * Process a normalized message through the AI layer with channel UX.
213
213
  * Message persistence is handled centrally by the AI layer.
214
+ *
215
+ * Uses chatStream() for progressive tool-call rendering when the adapter
216
+ * supports it (Telegram: sends each tool call as a message, reacts on completion).
217
+ * Falls back to chat() for adapters without streamChatResponse.
214
218
  */
215
219
  async function processChannelMessage(adapter, normalized) {
216
220
  await adapter.acknowledge(normalized.metadata);
217
221
  const stopIndicator = adapter.startProcessingIndicator(normalized.metadata);
218
222
 
219
223
  try {
220
- const response = await chat(
221
- normalized.threadId,
222
- normalized.text,
223
- normalized.attachments,
224
- { userId: 'telegram', chatTitle: 'Telegram' }
225
- );
226
- await adapter.sendResponse(normalized.threadId, response, normalized.metadata);
224
+ if (adapter.streamChatResponse) {
225
+ const chunks = chatStream(
226
+ normalized.threadId,
227
+ normalized.text,
228
+ normalized.attachments,
229
+ { userId: 'telegram', chatTitle: 'Telegram' }
230
+ );
231
+ await adapter.streamChatResponse(normalized.metadata.chatId, chunks);
232
+ } else {
233
+ const response = await chat(
234
+ normalized.threadId,
235
+ normalized.text,
236
+ normalized.attachments,
237
+ { userId: 'telegram', chatTitle: 'Telegram' }
238
+ );
239
+ await adapter.sendResponse(normalized.threadId, response, normalized.metadata);
240
+ }
227
241
  } catch (err) {
228
242
  console.error('Failed to process message with AI:', err);
229
243
  await adapter
package/lib/ai/index.js CHANGED
@@ -178,7 +178,7 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
178
178
  const setupArgs = { repo, branch, featureBranch };
179
179
 
180
180
  if (needsSetup) {
181
- yield { type: 'tool-call', toolCallId: setupToolCallId, toolName: 'set_up_workspace', args: setupArgs };
181
+ yield { type: 'tool-call', toolCallId: setupToolCallId, toolName: 'workspace', args: setupArgs };
182
182
  }
183
183
 
184
184
  try {
@@ -190,7 +190,7 @@ async function* chatStream(threadId, message, attachments = [], options = {}) {
190
190
  persistMessage(threadId, 'assistant', JSON.stringify({
191
191
  type: 'tool-invocation',
192
192
  toolCallId: setupToolCallId,
193
- toolName: 'set_up_workspace',
193
+ toolName: 'workspace',
194
194
  state: 'output-available',
195
195
  input: setupArgs,
196
196
  output: result,
@@ -4,8 +4,10 @@ import {
4
4
  downloadFile,
5
5
  reactToMessage,
6
6
  startTypingIndicator,
7
+ formatToolCall,
7
8
  } from '../tools/telegram.js';
8
9
  import { isWhisperEnabled, transcribeAudio } from '../tools/openai.js';
10
+ import { getConfig } from '../config.js';
9
11
 
10
12
  class TelegramAdapter extends ChannelAdapter {
11
13
  constructor(botToken) {
@@ -19,15 +21,18 @@ class TelegramAdapter extends ChannelAdapter {
19
21
  * Returns null if the update should be ignored.
20
22
  */
21
23
  async receive(request) {
22
- const { TELEGRAM_WEBHOOK_SECRET, TELEGRAM_CHAT_ID, TELEGRAM_VERIFICATION } = process.env;
24
+ // Read config from DB (with .env fallback via getConfig)
25
+ const webhookSecret = getConfig('TELEGRAM_WEBHOOK_SECRET');
26
+ const allowedChatId = getConfig('TELEGRAM_CHAT_ID');
27
+ const verificationCode = getConfig('TELEGRAM_VERIFICATION');
23
28
 
24
29
  // Validate secret token (required)
25
- if (!TELEGRAM_WEBHOOK_SECRET) {
30
+ if (!webhookSecret) {
26
31
  console.error('[telegram] TELEGRAM_WEBHOOK_SECRET not configured — rejecting webhook');
27
32
  return null;
28
33
  }
29
34
  const headerSecret = request.headers.get('x-telegram-bot-api-secret-token');
30
- if (headerSecret !== TELEGRAM_WEBHOOK_SECRET) {
35
+ if (headerSecret !== webhookSecret) {
31
36
  return null;
32
37
  }
33
38
 
@@ -40,17 +45,27 @@ class TelegramAdapter extends ChannelAdapter {
40
45
  let text = message.text || null;
41
46
  const attachments = [];
42
47
 
43
- // Check for verification code — works even before TELEGRAM_CHAT_ID is set
44
- if (TELEGRAM_VERIFICATION && text === TELEGRAM_VERIFICATION) {
48
+ // Check for verification code — works even before TELEGRAM_CHAT_ID is set.
49
+ // On match, save the chat ID directly and clear the (one-time) verification code.
50
+ if (verificationCode && text === verificationCode) {
45
51
  await sendMessage(this.botToken, chatId, `Your chat ID:\n<code>${chatId}</code>`);
52
+ try {
53
+ const { setConfigValue, deleteConfigValue } = await import('../db/config.js');
54
+ const { invalidateConfigCache } = await import('../config.js');
55
+ setConfigValue('TELEGRAM_CHAT_ID', chatId);
56
+ deleteConfigValue('TELEGRAM_VERIFICATION');
57
+ invalidateConfigCache();
58
+ } catch (err) {
59
+ console.error('[telegram] Failed to persist verified chat ID:', err.message);
60
+ }
46
61
  return null;
47
62
  }
48
63
 
49
64
  // Security: if no TELEGRAM_CHAT_ID configured, ignore all messages
50
- if (!TELEGRAM_CHAT_ID) return null;
65
+ if (!allowedChatId) return null;
51
66
 
52
67
  // Security: only accept messages from configured chat
53
- if (chatId !== TELEGRAM_CHAT_ID) return null;
68
+ if (chatId !== allowedChatId) return null;
54
69
 
55
70
  // Voice messages → transcribe to text
56
71
  if (message.voice) {
@@ -140,6 +155,62 @@ class TelegramAdapter extends ChannelAdapter {
140
155
  await sendMessage(this.botToken, threadId, text);
141
156
  }
142
157
 
158
+ /**
159
+ * Consume a chatStream() async iterable and send progressive messages.
160
+ * - Text chunks accumulate and flush when a tool-call arrives or stream ends.
161
+ * - Each tool-call sends immediately as its own message.
162
+ * - Tool-results react to the tool-call message with ✅ (or ❌ on error).
163
+ *
164
+ * @param {string} chatId - Telegram chat ID
165
+ * @param {AsyncIterable} chunks - chatStream() output
166
+ */
167
+ async streamChatResponse(chatId, chunks) {
168
+ let textBuffer = '';
169
+ // Map toolCallId → { telegramMessageId, hasCompleteArgs }
170
+ const toolMessages = new Map();
171
+
172
+ for await (const chunk of chunks) {
173
+ if (chunk.type === 'text') {
174
+ textBuffer += chunk.text;
175
+ } else if (chunk.type === 'tool-call') {
176
+ // Skip the first empty-args emission — wait for complete args
177
+ if (!chunk.args || Object.keys(chunk.args).length === 0) {
178
+ continue;
179
+ }
180
+
181
+ // Flush accumulated text before tool call
182
+ if (textBuffer.trim()) {
183
+ await sendMessage(this.botToken, chatId, textBuffer.trim());
184
+ textBuffer = '';
185
+ }
186
+
187
+ // Send tool call as its own message
188
+ const text = formatToolCall(chunk.toolName, chunk.args);
189
+ try {
190
+ const msg = await sendMessage(this.botToken, chatId, text);
191
+ toolMessages.set(chunk.toolCallId, msg.message_id);
192
+ } catch (err) {
193
+ console.error('[telegram] Failed to send tool call:', err.message);
194
+ }
195
+ } else if (chunk.type === 'tool-result') {
196
+ const messageId = toolMessages.get(chunk.toolCallId);
197
+ if (messageId) {
198
+ const emoji = chunk.result?.includes?.('error') || chunk.result?.includes?.('Error')
199
+ ? '👎'
200
+ : '👍';
201
+ reactToMessage(this.botToken, chatId, messageId, emoji).catch(() => {});
202
+ toolMessages.delete(chunk.toolCallId);
203
+ }
204
+ }
205
+ // Skip: meta, result, thinking-*, unknown
206
+ }
207
+
208
+ // Flush remaining text
209
+ if (textBuffer.trim()) {
210
+ await sendMessage(this.botToken, chatId, textBuffer.trim());
211
+ }
212
+ }
213
+
143
214
  get supportsStreaming() {
144
215
  return false;
145
216
  }
@@ -966,6 +966,151 @@ export async function regenerateWebhookSecret(key) {
966
966
  }
967
967
  }
968
968
 
969
+ // ─────────────────────────────────────────────────────────────────────────────
970
+ // Settings — Telegram setup flow
971
+ // ─────────────────────────────────────────────────────────────────────────────
972
+
973
+ /**
974
+ * Get Telegram status: bot info (if token valid), webhook info, chat ID.
975
+ * Used by the admin UI to render the current state of each setup step.
976
+ */
977
+ export async function getTelegramStatus() {
978
+ await requireAuth();
979
+ try {
980
+ const { getConfigSecret, getConfigValue } = await import('../db/config.js');
981
+ const { validateBotToken, getTelegramWebhookInfo } = await import('../tools/telegram.js');
982
+
983
+ const botToken = getConfigSecret('TELEGRAM_BOT_TOKEN');
984
+ const webhookSecret = getConfigSecret('TELEGRAM_WEBHOOK_SECRET');
985
+ const chatId = getConfigValue('TELEGRAM_CHAT_ID');
986
+ const verificationCode = getConfigValue('TELEGRAM_VERIFICATION');
987
+
988
+ let botInfo = null;
989
+ let webhookInfo = null;
990
+ if (botToken) {
991
+ const v = await validateBotToken(botToken);
992
+ if (v.valid) botInfo = { username: v.botInfo.username, id: v.botInfo.id };
993
+ try {
994
+ const info = await getTelegramWebhookInfo(botToken);
995
+ if (info.ok) {
996
+ webhookInfo = {
997
+ url: info.result.url || null,
998
+ hasCustomCertificate: info.result.has_custom_certificate,
999
+ pendingUpdates: info.result.pending_update_count,
1000
+ lastErrorMessage: info.result.last_error_message || null,
1001
+ };
1002
+ }
1003
+ } catch {
1004
+ // ignore — webhook info is best-effort
1005
+ }
1006
+ }
1007
+
1008
+ return {
1009
+ botInfo,
1010
+ botTokenSet: !!botToken,
1011
+ webhookSecretSet: !!webhookSecret,
1012
+ webhookInfo,
1013
+ chatId: chatId || null,
1014
+ pendingVerification: !!verificationCode,
1015
+ };
1016
+ } catch (err) {
1017
+ console.error('Failed to get Telegram status:', err);
1018
+ return { error: 'Failed to load Telegram status' };
1019
+ }
1020
+ }
1021
+
1022
+ /**
1023
+ * Validate a Telegram bot token without saving it.
1024
+ * Called from the UI as the user pastes a token to show live feedback.
1025
+ */
1026
+ export async function validateTelegramToken(token) {
1027
+ await requireAuth();
1028
+ if (!token) return { valid: false, error: 'Token is required' };
1029
+ try {
1030
+ const { validateBotToken } = await import('../tools/telegram.js');
1031
+ const result = await validateBotToken(token);
1032
+ if (result.valid) {
1033
+ return { valid: true, botInfo: { username: result.botInfo.username, id: result.botInfo.id } };
1034
+ }
1035
+ return { valid: false, error: result.error };
1036
+ } catch (err) {
1037
+ return { valid: false, error: err.message };
1038
+ }
1039
+ }
1040
+
1041
+ /**
1042
+ * Register the Telegram webhook with the currently saved bot token.
1043
+ * Generates a fresh webhook secret, saves it, and calls Telegram's setWebhook.
1044
+ * APP_URL must be configured.
1045
+ */
1046
+ export async function registerTelegramWebhook() {
1047
+ const user = await requireAuth();
1048
+ try {
1049
+ const { getConfigSecret, setConfigSecret } = await import('../db/config.js');
1050
+ const { invalidateConfigCache, getConfig } = await import('../config.js');
1051
+ const { setTelegramWebhook, generateWebhookSecret } = await import('../tools/telegram.js');
1052
+
1053
+ const botToken = getConfigSecret('TELEGRAM_BOT_TOKEN');
1054
+ if (!botToken) return { error: 'Bot token must be set first' };
1055
+
1056
+ const appUrl = getConfig('APP_URL');
1057
+ if (!appUrl) return { error: 'APP_URL must be configured first' };
1058
+
1059
+ const webhookUrl = `${appUrl.replace(/\/$/, '')}/api/telegram/webhook`;
1060
+ const secret = generateWebhookSecret();
1061
+ setConfigSecret('TELEGRAM_WEBHOOK_SECRET', secret, user.id);
1062
+ invalidateConfigCache();
1063
+
1064
+ const result = await setTelegramWebhook(botToken, webhookUrl, secret);
1065
+ if (!result.ok) {
1066
+ return { error: result.description || 'Failed to register webhook' };
1067
+ }
1068
+ return { success: true, webhookUrl };
1069
+ } catch (err) {
1070
+ console.error('Failed to register Telegram webhook:', err);
1071
+ return { error: err.message };
1072
+ }
1073
+ }
1074
+
1075
+ /**
1076
+ * Start verification: generate a code, save it, return it to the UI.
1077
+ * User sends the code to the bot; the webhook handler captures the chat ID,
1078
+ * saves it as TELEGRAM_CHAT_ID, and clears this verification code.
1079
+ * The UI polls getTelegramStatus() to detect completion.
1080
+ */
1081
+ export async function startTelegramVerification() {
1082
+ const user = await requireAuth();
1083
+ try {
1084
+ const { setConfigValue } = await import('../db/config.js');
1085
+ const { invalidateConfigCache } = await import('../config.js');
1086
+ const { generateVerificationCode } = await import('../tools/telegram.js');
1087
+
1088
+ const code = generateVerificationCode();
1089
+ setConfigValue('TELEGRAM_VERIFICATION', code, user.id);
1090
+ invalidateConfigCache();
1091
+ return { success: true, code };
1092
+ } catch (err) {
1093
+ console.error('Failed to start Telegram verification:', err);
1094
+ return { error: err.message };
1095
+ }
1096
+ }
1097
+
1098
+ /**
1099
+ * Cancel pending verification (clear the code).
1100
+ */
1101
+ export async function cancelTelegramVerification() {
1102
+ await requireAuth();
1103
+ try {
1104
+ const { deleteConfigValue } = await import('../db/config.js');
1105
+ const { invalidateConfigCache } = await import('../config.js');
1106
+ deleteConfigValue('TELEGRAM_VERIFICATION');
1107
+ invalidateConfigCache();
1108
+ return { success: true };
1109
+ } catch (err) {
1110
+ return { error: err.message };
1111
+ }
1112
+ }
1113
+
969
1114
  // ─────────────────────────────────────────────────────────────────────────────
970
1115
  // Settings — Chat sub-tab
971
1116
  // ─────────────────────────────────────────────────────────────────────────────
@@ -218,8 +218,8 @@ function WorkspaceBar({
218
218
  }).finally(() => setLoadingBranches(false));
219
219
  }
220
220
  },
221
- triggerClassName: "font-medium text-foreground hover:text-primary hover:bg-accent transition-colors cursor-pointer truncate text-xs rounded px-1 -mx-1",
222
- triggerLabel: /* @__PURE__ */ jsx("span", { className: "truncate", title: branch, children: branch })
221
+ triggerClassName: "block w-full text-left font-medium text-foreground hover:text-primary hover:bg-accent transition-colors cursor-pointer truncate text-xs rounded px-1 -mx-1",
222
+ triggerLabel: /* @__PURE__ */ jsx("span", { className: "block truncate", title: branch, children: branch })
223
223
  }
224
224
  ) })
225
225
  ] }),
@@ -242,8 +242,8 @@ export function WorkspaceBar({
242
242
  }).finally(() => setLoadingBranches(false));
243
243
  }
244
244
  }}
245
- triggerClassName="font-medium text-foreground hover:text-primary hover:bg-accent transition-colors cursor-pointer truncate text-xs rounded px-1 -mx-1"
246
- triggerLabel={<span className="truncate" title={branch}>{branch}</span>}
245
+ triggerClassName="block w-full text-left font-medium text-foreground hover:text-primary hover:bg-accent transition-colors cursor-pointer truncate text-xs rounded px-1 -mx-1"
246
+ triggerLabel={<span className="block truncate" title={branch}>{branch}</span>}
247
247
  />
248
248
  </div>
249
249
  </>
@@ -434,7 +434,7 @@ function PreviewMessage({ message, isLoading, onRetry, onEdit }) {
434
434
  function ThinkingMessage() {
435
435
  return /* @__PURE__ */ jsx("div", { className: "flex gap-4 w-full justify-start", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-4 py-3 text-sm text-muted-foreground", children: [
436
436
  /* @__PURE__ */ jsx(SpinnerIcon, { size: 14 }),
437
- /* @__PURE__ */ jsx("span", { children: "Thinking..." })
437
+ /* @__PURE__ */ jsx("span", { className: "thinking-shimmer", children: "Waiting..." })
438
438
  ] }) });
439
439
  }
440
440
  export {
@@ -523,7 +523,7 @@ export function ThinkingMessage() {
523
523
  <div className="flex gap-4 w-full justify-start">
524
524
  <div className="flex items-center gap-2 px-4 py-3 text-sm text-muted-foreground">
525
525
  <SpinnerIcon size={14} />
526
- <span>Thinking...</span>
526
+ <span className="thinking-shimmer">Waiting...</span>
527
527
  </div>
528
528
  </div>
529
529
  );