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,17 @@
1
+ import { AssemblyAI } from 'assemblyai';
2
+ import { getConfig } from '../config.js';
3
+
4
+ function isAssemblyAIEnabled() {
5
+ return Boolean(getConfig('ASSEMBLYAI_API_KEY'));
6
+ }
7
+
8
+ async function transcribeAudio(audioBuffer) {
9
+ const client = new AssemblyAI({ apiKey: getConfig('ASSEMBLYAI_API_KEY') });
10
+ const transcript = await client.transcripts.transcribe({ audio: audioBuffer });
11
+ if (transcript.status === 'error') {
12
+ throw new Error(`AssemblyAI error: ${transcript.error}`);
13
+ }
14
+ return transcript.text;
15
+ }
16
+
17
+ export { isAssemblyAIEnabled, transcribeAudio };
@@ -1,22 +1,23 @@
1
1
  import { v4 as uuidv4 } from 'uuid';
2
2
  import { z } from 'zod';
3
3
  import { githubApi } from './github.js';
4
- import { createModel } from '../ai/model.js';
4
+ import { callHelperLlmStructured } from '../ai/helper-llm.js';
5
5
  import { getConfig } from '../config.js';
6
6
  /**
7
- * Generate a short descriptive title for an agent job using the LLM.
7
+ * Generate a short descriptive title for an agent job using the helper LLM.
8
8
  * Uses structured output to avoid thinking-token leaks with extended-thinking models.
9
9
  * @param {string} agentJobDescription - The full job description
10
10
  * @returns {Promise<string>} ~10 word title
11
11
  */
12
12
  async function generateAgentJobTitle(agentJobDescription) {
13
13
  try {
14
- const model = await createModel({ maxTokens: 100 });
15
- const response = await model.withStructuredOutput(z.object({ title: z.string() })).invoke([
16
- ['system', 'Generate a descriptive ~10 word title for this agent job. The title should clearly describe what the job will do.'],
17
- ['human', agentJobDescription],
18
- ]);
19
- return response.title.trim() || agentJobDescription.slice(0, 80);
14
+ const result = await callHelperLlmStructured({
15
+ system: 'Generate a descriptive ~10 word title for this agent job. The title should clearly describe what the job will do.',
16
+ user: agentJobDescription,
17
+ schema: z.object({ title: z.string() }),
18
+ maxTokens: 100,
19
+ });
20
+ return result?.title?.trim() || agentJobDescription.slice(0, 80);
20
21
  } catch {
21
22
  // Fallback: first line, truncated
22
23
  const firstLine = agentJobDescription.split('\n').find(l => l.trim()) || agentJobDescription;
@@ -51,6 +52,8 @@ async function createAgentJob(agentJobDescription, options = {}) {
51
52
  const config = { title, job: agentJobDescription };
52
53
  if (options.llmModel) config.llm_model = options.llmModel;
53
54
  if (options.agentBackend) config.agent_backend = options.agentBackend;
55
+ if (options.scope) config.scope = options.scope;
56
+ if (options.systemPrompt) config.system_prompt = options.systemPrompt;
54
57
 
55
58
  const treeEntries = [
56
59
  {
@@ -99,6 +102,7 @@ async function createAgentJob(agentJobDescription, options = {}) {
99
102
  description: agentJobDescription,
100
103
  codingAgent: options.agentBackend,
101
104
  llmModel: options.llmModel,
105
+ scope: options.scope,
102
106
  }).catch(err => {
103
107
  console.error(`[agent-job] Failed to launch container for ${agentJobId}:`, err.message);
104
108
  });
@@ -196,9 +196,11 @@ async function runContainer({ containerName, image, env = [], workingDir, hostCo
196
196
  * @param {string} [options.featureBranch] - Feature branch to create after cloning
197
197
  * @param {string} [options.workspaceId] - Workspace ID (for bind mount)
198
198
  * @param {boolean} [options.injectSecrets] - Inject agent job secrets into container env
199
+ * @param {string} [options.systemPrompt] - Pre-rendered system prompt (written to volume as SYSTEM.md)
200
+ * @param {string} [options.scope] - Subdirectory scope within the repo (e.g., 'agents/gary-v')
199
201
  * @returns {Promise<{containerId: string, containerName: string}>}
200
202
  */
201
- async function runInteractiveContainer({ containerName, repo, branch, codingAgent, featureBranch, workspaceId, injectSecrets, continueSession = true }) {
203
+ async function runInteractiveContainer({ containerName, repo, branch, codingAgent, featureBranch, workspaceId, injectSecrets, systemPrompt, continueSession = true, scope }) {
202
204
  const agent = codingAgent || getConfig('CODING_AGENT') || 'claude-code';
203
205
  const version = process.env.THEPOPEBOT_VERSION;
204
206
  const image = `stephengpope/thepopebot:coding-agent-${agent}-${version}`;
@@ -223,6 +225,9 @@ async function runInteractiveContainer({ containerName, repo, branch, codingAgen
223
225
  if (continueSession) {
224
226
  env.push(`CONTINUE_SESSION=1`);
225
227
  }
228
+ if (scope) {
229
+ env.push(`SCOPE=${scope}`);
230
+ }
226
231
 
227
232
  // Inject agent job secrets when running in agent chat mode
228
233
  if (injectSecrets) {
@@ -246,8 +251,16 @@ async function runInteractiveContainer({ containerName, repo, branch, codingAgen
246
251
  const dir = workspaceDir(workspaceId);
247
252
  const wsDir = path.join(dir, 'workspace');
248
253
  fs.mkdirSync(wsDir, { recursive: true });
249
- fs.chownSync(dir, CODING_AGENT_UID, CODING_AGENT_UID);
250
- fs.chownSync(wsDir, CODING_AGENT_UID, CODING_AGENT_UID);
254
+
255
+ // Write pre-rendered system prompt to volume (read by container setup scripts)
256
+ const systemPromptPath = path.join(dir, 'SYSTEM.md');
257
+ if (systemPrompt) {
258
+ fs.writeFileSync(systemPromptPath, systemPrompt, 'utf8');
259
+ } else {
260
+ // Remove stale prompt if no system prompt for this mode
261
+ try { fs.unlinkSync(systemPromptPath); } catch {}
262
+ }
263
+
251
264
  const hostDir = await resolveHostPath(dir);
252
265
  hostConfig.Binds = [`${hostDir}:/home/coding-agent`];
253
266
  }
@@ -397,9 +410,10 @@ function buildAgentAuthEnv(agent) {
397
410
  * @param {string} [options.systemPrompt] - Optional system prompt
398
411
  * @param {boolean} [options.continueSession] - Continue most recent session
399
412
  * @param {boolean} [options.injectSecrets] - Inject agent job secrets into container env
413
+ * @param {string} [options.scope] - Subdirectory scope within the repo (e.g., 'agents/gary-v')
400
414
  * @returns {Promise<{containerId: string, containerName: string}>}
401
415
  */
402
- async function runHeadlessContainer({ containerName, repo, branch, featureBranch, workspaceId, taskPrompt, mode = 'plan', codingAgent, systemPrompt, continueSession = true, injectSecrets }) {
416
+ async function runHeadlessContainer({ containerName, repo, branch, featureBranch, workspaceId, taskPrompt, mode = 'plan', codingAgent, systemPrompt, continueSession = true, injectSecrets, scope }) {
403
417
  const agent = codingAgent || getConfig('CODING_AGENT') || 'claude-code';
404
418
  const version = process.env.THEPOPEBOT_VERSION;
405
419
  const image = `stephengpope/thepopebot:coding-agent-${agent}-${version}`;
@@ -418,12 +432,12 @@ async function runHeadlessContainer({ containerName, repo, branch, featureBranch
418
432
  const permission = mode === 'dangerous' ? 'code' : mode;
419
433
  env.push(`PERMISSION=${permission}`);
420
434
  }
421
- if (systemPrompt) {
422
- env.push(`SYSTEM_PROMPT=${systemPrompt}`);
423
- }
424
435
  if (continueSession) {
425
436
  env.push(`CONTINUE_SESSION=1`);
426
437
  }
438
+ if (scope) {
439
+ env.push(`SCOPE=${scope}`);
440
+ }
427
441
 
428
442
  // Auth env vars based on agent type
429
443
  const { env: authEnv, backendApi } = buildAgentAuthEnv(agent);
@@ -456,8 +470,15 @@ async function runHeadlessContainer({ containerName, repo, branch, featureBranch
456
470
  const dir = workspaceDir(workspaceId);
457
471
  const wsDir = path.join(dir, 'workspace');
458
472
  fs.mkdirSync(wsDir, { recursive: true });
459
- fs.chownSync(dir, CODING_AGENT_UID, CODING_AGENT_UID);
460
- fs.chownSync(wsDir, CODING_AGENT_UID, CODING_AGENT_UID);
473
+
474
+ // Write pre-rendered system prompt to volume (read by container setup scripts)
475
+ const systemPromptPath = path.join(dir, 'SYSTEM.md');
476
+ if (systemPrompt) {
477
+ fs.writeFileSync(systemPromptPath, systemPrompt, 'utf8');
478
+ } else {
479
+ try { fs.unlinkSync(systemPromptPath); } catch {}
480
+ }
481
+
461
482
  const hostDir = await resolveHostPath(dir);
462
483
  hostConfig.Binds = [`${hostDir}:/home/coding-agent`];
463
484
  }
@@ -859,7 +880,7 @@ async function removeVolume(name) {
859
880
  * @param {string} [options.llmModel] - Model override
860
881
  * @returns {Promise<{containerId: string, containerName: string, volumeName: string}>}
861
882
  */
862
- async function runAgentJobContainer({ agentJobId, repo, branch, title, description, codingAgent, llmModel }) {
883
+ async function runAgentJobContainer({ agentJobId, repo, branch, title, description, codingAgent, llmModel, scope }) {
863
884
  const agent = codingAgent || getConfig('CODING_AGENT') || 'claude-code';
864
885
  const version = process.env.THEPOPEBOT_VERSION;
865
886
  const image = `stephengpope/thepopebot:coding-agent-${agent}-${version}`;
@@ -883,6 +904,9 @@ async function runAgentJobContainer({ agentJobId, repo, branch, title, descripti
883
904
  if (llmModel) {
884
905
  env.push(`LLM_MODEL=${llmModel}`);
885
906
  }
907
+ if (scope) {
908
+ env.push(`SCOPE=${scope}`);
909
+ }
886
910
 
887
911
  // Auth env vars based on agent type
888
912
  const { env: authEnv, backendApi } = buildAgentAuthEnv(agent);
@@ -251,6 +251,38 @@ async function listBranches(repoFullName) {
251
251
  }));
252
252
  }
253
253
 
254
+ /**
255
+ * Get the default branch name for a repository.
256
+ * @param {string} repoFullName - e.g. "owner/repo"
257
+ * @returns {Promise<string|null>}
258
+ */
259
+ async function getDefaultBranch(repoFullName) {
260
+ if (!repoFullName) return null;
261
+ const [owner, repo] = repoFullName.split('/');
262
+ if (!owner || !repo) return null;
263
+ try {
264
+ const info = await githubApi(`/repos/${owner}/${repo}`);
265
+ return info?.default_branch || null;
266
+ } catch {
267
+ return null;
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Create a new repository for the authenticated user.
273
+ * Uses auto_init to create an initial README commit (which creates the default branch).
274
+ * @param {string} name - Repository name
275
+ * @returns {Promise<{full_name: string, default_branch: string}>}
276
+ */
277
+ async function createRepository(name) {
278
+ const data = await githubApi('/user/repos', {
279
+ method: 'POST',
280
+ headers: { 'Content-Type': 'application/json' },
281
+ body: JSON.stringify({ name, auto_init: true, private: true }),
282
+ });
283
+ return { full_name: data.full_name, default_branch: data.default_branch };
284
+ }
285
+
254
286
  export {
255
287
  githubApi,
256
288
  getWorkflowRuns,
@@ -262,4 +294,6 @@ export {
262
294
  getOpenPullRequests,
263
295
  listRepositories,
264
296
  listBranches,
297
+ getDefaultBranch,
298
+ createRepository,
265
299
  };
@@ -1,9 +1,75 @@
1
1
  import { Bot } from 'grammy';
2
2
  import parseModePlugin from '@grammyjs/parse-mode';
3
+ import { randomBytes } from 'crypto';
3
4
  const { hydrateReply } = parseModePlugin;
4
5
 
5
6
  const MAX_LENGTH = 4096;
6
7
 
8
+ /**
9
+ * Validate a Telegram bot token by calling getMe.
10
+ * @param {string} botToken
11
+ * @returns {Promise<{valid: boolean, botInfo?: object, error?: string}>}
12
+ */
13
+ async function validateBotToken(botToken) {
14
+ try {
15
+ const response = await fetch(`https://api.telegram.org/bot${botToken}/getMe`);
16
+ const result = await response.json();
17
+ if (result.ok) return { valid: true, botInfo: result.result };
18
+ return { valid: false, error: result.description };
19
+ } catch (err) {
20
+ return { valid: false, error: err.message };
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Register a Telegram webhook (deletes any existing webhook first).
26
+ * @param {string} botToken
27
+ * @param {string} webhookUrl
28
+ * @param {string} [secretToken]
29
+ * @returns {Promise<object>} Telegram API response
30
+ */
31
+ async function setTelegramWebhook(botToken, webhookUrl, secretToken = null) {
32
+ // Delete first — Telegram ignores secret_token changes if the URL is unchanged
33
+ await deleteTelegramWebhook(botToken);
34
+ const body = { url: webhookUrl };
35
+ if (secretToken) body.secret_token = secretToken;
36
+ const response = await fetch(`https://api.telegram.org/bot${botToken}/setWebhook`, {
37
+ method: 'POST',
38
+ headers: { 'Content-Type': 'application/json' },
39
+ body: JSON.stringify(body),
40
+ });
41
+ return response.json();
42
+ }
43
+
44
+ /**
45
+ * Get current webhook info from Telegram.
46
+ * @param {string} botToken
47
+ * @returns {Promise<object>}
48
+ */
49
+ async function getTelegramWebhookInfo(botToken) {
50
+ const response = await fetch(`https://api.telegram.org/bot${botToken}/getWebhookInfo`);
51
+ return response.json();
52
+ }
53
+
54
+ /**
55
+ * Delete the current webhook.
56
+ * @param {string} botToken
57
+ * @returns {Promise<object>}
58
+ */
59
+ async function deleteTelegramWebhook(botToken) {
60
+ const response = await fetch(`https://api.telegram.org/bot${botToken}/deleteWebhook`, {
61
+ method: 'POST',
62
+ });
63
+ return response.json();
64
+ }
65
+
66
+ /**
67
+ * Generate a random webhook secret (64 hex chars).
68
+ */
69
+ function generateWebhookSecret() {
70
+ return randomBytes(32).toString('hex');
71
+ }
72
+
7
73
  /**
8
74
  * Convert markdown to Telegram-compatible HTML.
9
75
  * Handles: code blocks, inline code, links, bold, italic, strikethrough, headings, lists.
@@ -278,6 +344,40 @@ function startTypingIndicator(botToken, chatId) {
278
344
  };
279
345
  }
280
346
 
347
+ /**
348
+ * Format a tool call as a compact one-liner for Telegram.
349
+ * @param {string} toolName - e.g. 'Read', 'Edit', 'Bash', 'Grep'
350
+ * @param {object} args - Tool arguments
351
+ * @returns {string} Formatted HTML string
352
+ */
353
+ function formatToolCall(toolName, args) {
354
+ let detail = '';
355
+
356
+ if (args.file_path) {
357
+ // Read, Edit, Write — show the file path (basename + parent)
358
+ const parts = args.file_path.split('/');
359
+ detail = parts.length > 2 ? parts.slice(-2).join('/') : parts.pop();
360
+ } else if (args.command) {
361
+ // Bash — show the command, truncated
362
+ detail = args.command.length > 60 ? args.command.slice(0, 57) + '...' : args.command;
363
+ } else if (args.pattern) {
364
+ // Grep, Glob — show the pattern
365
+ detail = args.pattern;
366
+ if (args.path) {
367
+ const parts = args.path.split('/');
368
+ detail += ` in ${parts.length > 2 ? parts.slice(-2).join('/') : parts.pop()}`;
369
+ }
370
+ } else if (args.prompt) {
371
+ // Agent — show truncated prompt
372
+ detail = args.prompt.length > 50 ? args.prompt.slice(0, 47) + '...' : args.prompt;
373
+ } else if (args.description) {
374
+ detail = args.description;
375
+ }
376
+
377
+ const escaped = escapeHtml(detail);
378
+ return `⚙️ <code>${escapeHtml(toolName)}</code> ${escaped}`;
379
+ }
380
+
281
381
  export {
282
382
  getBot,
283
383
  setWebhook,
@@ -285,8 +385,14 @@ export {
285
385
  smartSplit,
286
386
  escapeHtml,
287
387
  markdownToTelegramHtml,
388
+ formatToolCall,
288
389
  formatJobNotification,
289
390
  downloadFile,
290
391
  reactToMessage,
291
392
  startTypingIndicator,
393
+ validateBotToken,
394
+ setTelegramWebhook,
395
+ getTelegramWebhookInfo,
396
+ deleteTelegramWebhook,
397
+ generateWebhookSecret,
292
398
  };
@@ -2,27 +2,43 @@ import fs from 'fs';
2
2
  import path from 'path';
3
3
  import { PROJECT_ROOT } from '../paths.js';
4
4
 
5
- const skillsDir = path.join(PROJECT_ROOT, 'skills');
6
-
7
5
  const INCLUDE_PATTERN = /\{\{([^}]+\.md)\}\}/g;
8
6
  const VARIABLE_PATTERN = /\{\{(datetime|skills)\}\}/gi;
9
7
 
10
- // Scan skill directories under skills/active/ for SKILL.md files and extract
11
- // description from YAML frontmatter. Returns a bullet list of descriptions.
12
- function loadSkillDescriptions() {
13
- const activeDir = path.join(skillsDir, 'active');
8
+ // Default skills directory (used when no explicit skillsDir is provided)
9
+ const defaultSkillsDir = path.join(PROJECT_ROOT, 'skills');
10
+
11
+ /**
12
+ * Scan a skills directory for SKILL.md files and extract descriptions
13
+ * from YAML frontmatter. Returns a bullet list of descriptions.
14
+ *
15
+ * @param {string|null} [skillsDir] - Absolute path to skills directory.
16
+ * Falls back to PROJECT_ROOT/skills if not provided.
17
+ * @returns {string}
18
+ */
19
+ function loadSkillDescriptions(skillsDir) {
20
+ const dir = skillsDir || defaultSkillsDir;
14
21
  try {
15
- if (!fs.existsSync(activeDir)) {
22
+ if (!fs.existsSync(dir)) {
16
23
  return 'No additional abilities configured.';
17
24
  }
18
25
 
19
- const entries = fs.readdirSync(activeDir, { withFileTypes: true });
26
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
20
27
  const descriptions = [];
21
28
 
22
29
  for (const entry of entries) {
23
- if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
30
+ // Follow symlinks — resolve to check if target is a directory
31
+ const entryPath = path.join(dir, entry.name);
32
+ let isDir = entry.isDirectory() || entry.isSymbolicLink();
33
+ if (entry.isSymbolicLink()) {
34
+ try {
35
+ const stat = fs.statSync(entryPath);
36
+ isDir = stat.isDirectory();
37
+ } catch { continue; } // broken symlink
38
+ }
39
+ if (!isDir) continue;
24
40
 
25
- const skillMdPath = path.join(activeDir, entry.name, 'SKILL.md');
41
+ const skillMdPath = path.join(entryPath, 'SKILL.md');
26
42
  if (!fs.existsSync(skillMdPath)) continue;
27
43
 
28
44
  const content = fs.readFileSync(skillMdPath, 'utf8');
@@ -32,7 +48,7 @@ function loadSkillDescriptions() {
32
48
  const frontmatter = frontmatterMatch[1];
33
49
  const descMatch = frontmatter.match(/^description:\s*(.+)$/m);
34
50
  if (descMatch) {
35
- descriptions.push(`- ${descMatch[1].trim()}`);
51
+ descriptions.push(`- **${entry.name}**: ${descMatch[1].trim()}`);
36
52
  }
37
53
  }
38
54
 
@@ -49,15 +65,17 @@ function loadSkillDescriptions() {
49
65
  /**
50
66
  * Resolve built-in variables like {{datetime}} and {{skills}}.
51
67
  * @param {string} content - Content with possible variable placeholders
68
+ * @param {object} [vars] - Variable overrides
69
+ * @param {string|null} [vars.skillsDir] - Skills directory for {{skills}} resolution
52
70
  * @returns {string} Content with variables resolved
53
71
  */
54
- function resolveVariables(content) {
72
+ function resolveVariables(content, vars = {}) {
55
73
  return content.replace(VARIABLE_PATTERN, (match, variable) => {
56
74
  switch (variable.toLowerCase()) {
57
75
  case 'datetime':
58
76
  return new Date().toISOString();
59
77
  case 'skills':
60
- return loadSkillDescriptions();
78
+ return loadSkillDescriptions(vars.skillsDir || null);
61
79
  default:
62
80
  return match;
63
81
  }
@@ -69,10 +87,18 @@ function resolveVariables(content) {
69
87
  * and {{datetime}}, {{skills}} built-in variables.
70
88
  * Referenced file paths resolve relative to the project root.
71
89
  * @param {string} filePath - Absolute path to the markdown file
72
- * @param {string[]} [chain=[]] - Already-resolved file paths (for circular detection)
90
+ * @param {object} [options] - Render options
91
+ * @param {string|null} [options.skillsDir] - Skills directory for {{skills}} resolution
92
+ * @param {string[]} [options._chain] - Internal: already-resolved file paths (circular detection)
73
93
  * @returns {string} Rendered markdown content
74
94
  */
75
- function render_md(filePath, chain = []) {
95
+ function render_md(filePath, options = {}) {
96
+ // Support legacy positional arg: render_md(path, chain)
97
+ if (Array.isArray(options)) {
98
+ options = { _chain: options };
99
+ }
100
+
101
+ const chain = options._chain || [];
76
102
  const resolved = path.resolve(filePath);
77
103
 
78
104
  if (chain.includes(resolved)) {
@@ -93,10 +119,10 @@ function render_md(filePath, chain = []) {
93
119
  if (!fs.existsSync(includeResolved)) {
94
120
  return match;
95
121
  }
96
- return render_md(includeResolved, currentChain);
122
+ return render_md(includeResolved, { ...options, _chain: currentChain });
97
123
  });
98
124
 
99
- return resolveVariables(withIncludes);
125
+ return resolveVariables(withIncludes, { skillsDir: options.skillsDir || null });
100
126
  }
101
127
 
102
- export { render_md };
128
+ export { render_md, loadSkillDescriptions };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thepopebot",
3
- "version": "1.2.76-beta.2",
3
+ "version": "1.2.76-beta.21",
4
4
  "type": "module",
5
5
  "description": "Create autonomous AI agents with a two-layer architecture: Next.js Event Handler + Docker Agent.",
6
6
  "bin": {
@@ -19,6 +19,7 @@
19
19
  "./db/oauth-tokens": "./lib/db/oauth-tokens.js",
20
20
  "./chat": "./lib/chat/components/index.js",
21
21
  "./chat/actions": "./lib/chat/actions.js",
22
+ "./chat/telegram-profile": "./lib/chat/telegram-profile.js",
22
23
  "./code": "./lib/code/index.js",
23
24
  "./code/actions": "./lib/code/actions.js",
24
25
  "./code/ws-proxy": "./lib/code/ws-proxy.js",
@@ -67,19 +68,17 @@
67
68
  "author": "Stephen Pope",
68
69
  "license": "MIT",
69
70
  "dependencies": {
71
+ "@ai-sdk/anthropic": "^2.0.0",
72
+ "@ai-sdk/google": "^2.0.0",
73
+ "@ai-sdk/openai": "^2.0.0",
74
+ "@ai-sdk/openai-compatible": "^1.0.0",
70
75
  "@ai-sdk/react": "^2.0.0",
71
- "@anthropic-ai/claude-agent-sdk": "^0.2.92",
76
+ "@anthropic-ai/claude-agent-sdk": "^0.2.101",
72
77
  "@clack/prompts": "^0.10.0",
73
78
  "@dnd-kit/core": "^6.3.1",
74
79
  "@dnd-kit/modifiers": "^9.0.0",
75
80
  "@dnd-kit/sortable": "^10.0.0",
76
81
  "@grammyjs/parse-mode": "^2.2.0",
77
- "@langchain/anthropic": "^1.3.17",
78
- "@langchain/core": "^1.1.24",
79
- "@langchain/google-genai": "^2.1.18",
80
- "@langchain/langgraph": "^1.1.4",
81
- "@langchain/langgraph-checkpoint-sqlite": "^1.0.1",
82
- "@langchain/openai": "^1.2.7",
83
82
  "@monaco-editor/react": "^4.7.0",
84
83
  "@xterm/addon-fit": "^0.10.0",
85
84
  "@xterm/addon-search": "^0.15.0",
@@ -87,6 +86,7 @@
87
86
  "@xterm/addon-web-links": "^0.11.0",
88
87
  "@xterm/xterm": "^5.5.0",
89
88
  "ai": "^5.0.0",
89
+ "assemblyai": "^4.30.0",
90
90
  "bcrypt-ts": "^6.0.0",
91
91
  "better-sqlite3": "^12.6.2",
92
92
  "chalk": "^5.3.0",
package/setup/CLAUDE.md CHANGED
@@ -4,13 +4,19 @@ Entry point: `setup.mjs` (invoked via `thepopebot setup`).
4
4
 
5
5
  ## Wizard Steps
6
6
 
7
- 1. **Prerequisites**Checks Node.js (>=18), git, gh CLI (authenticated), Docker. Initializes git repo and GitHub remote if needed.
8
- 2. **GitHub PAT** — Validates fine-grained token with required scopes (Actions, Admin, Contents, PRs, Secrets, Workflows).
9
- 3. **App URL** — Prompts for public HTTPS URL (ngrok, VPS, PaaS). Generates webhook secret.
10
- 4. **Sync Config** — Writes secrets/variables to GitHub and local DB via `syncConfig()`.
11
- 5. **Build** — Runs `npm run build` with retry.
7
+ 1. **Load `.env`** `dotenv.config()` runs first so existing values are available to subsequent steps.
8
+ 2. **Prerequisites** — Checks Node.js (>=18), git, gh CLI (authenticated), Docker. Initializes git repo and GitHub remote if needed.
9
+ 3. **GitHub PAT** — Validates fine-grained token with required scopes (Actions, Admin, Contents, PRs, Secrets, Workflows).
10
+ 4. **App URL** — Prompts for public HTTPS URL (ngrok, VPS, PaaS). Generates webhook secret.
11
+ 5. **Sync Config** — Writes secrets/variables to GitHub and local DB via `syncConfig()`.
12
12
  6. **Start Server** — Starts Docker containers, polls `/api/ping` to confirm.
13
13
 
14
+ The setup wizard does NOT run `npm run build` — `.next` is baked into the event-handler Docker image at publish time.
15
+
16
+ ## Database
17
+
18
+ Settings DB defaults to `data/db/thepopebot.sqlite` (relative to project root). Override via `DATABASE_PATH` in `.env`. Schema migrations run automatically on server start (`lib/db/index.js`).
19
+
14
20
  ## Sync Target Types
15
21
 
16
22
  Config values are synced to different targets via `lib/sync.mjs`:
@@ -14,7 +14,8 @@ export const PROVIDERS = {
14
14
  builtin: true,
15
15
  oauthSupported: true,
16
16
  models: [
17
- { id: 'claude-opus-4-6', name: 'Claude Opus 4.6', default: true },
17
+ { id: 'claude-opus-4-7', name: 'Claude Opus 4.7', default: true },
18
+ { id: 'claude-opus-4-6', name: 'Claude Opus 4.6' },
18
19
  { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6' },
19
20
  { id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5' },
20
21
  ],
@@ -15,24 +15,22 @@
15
15
  */
16
16
  export const CONFIG_TARGETS = {
17
17
  // Secrets → DB encrypted (never .env)
18
- GH_TOKEN: { env: true, dbSecret: true, secret: 'AGENT_GH_TOKEN' },
19
- ANTHROPIC_API_KEY: { dbSecret: true, secret: 'AGENT_ANTHROPIC_API_KEY' },
20
- OPENAI_API_KEY: { dbSecret: true, secret: 'AGENT_OPENAI_API_KEY' },
21
- GOOGLE_API_KEY: { dbSecret: true, secret: 'AGENT_GOOGLE_API_KEY' },
22
- CUSTOM_API_KEY: { dbSecret: true, secret: 'AGENT_CUSTOM_API_KEY' },
23
- MOONSHOT_API_KEY: { dbSecret: true, secret: 'AGENT_MOONSHOT_API_KEY' },
24
- CLAUDE_CODE_OAUTH_TOKEN: { dbSecret: true, secret: 'AGENT_CLAUDE_CODE_OAUTH_TOKEN' },
18
+ GH_TOKEN: { env: true, dbSecret: true },
19
+ ANTHROPIC_API_KEY: { dbSecret: true },
20
+ OPENAI_API_KEY: { dbSecret: true },
21
+ GOOGLE_API_KEY: { dbSecret: true },
22
+ CUSTOM_API_KEY: { dbSecret: true },
23
+ MOONSHOT_API_KEY: { dbSecret: true },
24
+ CLAUDE_CODE_OAUTH_TOKEN: { dbSecret: true },
25
25
  GH_WEBHOOK_SECRET: { dbSecret: true, secret: true },
26
26
  TELEGRAM_BOT_TOKEN: { dbSecret: true },
27
27
  TELEGRAM_WEBHOOK_SECRET: { dbSecret: true },
28
28
 
29
29
  // Plain config → DB (not .env)
30
- LLM_PROVIDER: { db: true, variable: true },
31
- LLM_MODEL: { db: true, variable: true },
32
- CUSTOM_OPENAI_BASE_URL: { db: true, variable: true },
33
- AGENT_BACKEND: { db: true, variable: true },
34
- TELEGRAM_CHAT_ID: { db: true },
35
- TELEGRAM_VERIFICATION: { db: true },
30
+ LLM_PROVIDER: { db: true },
31
+ LLM_MODEL: { db: true },
32
+ CUSTOM_OPENAI_BASE_URL: { db: true },
33
+ AGENT_BACKEND: { db: true },
36
34
 
37
35
  // Infrastructure → .env only (needed before DB is available)
38
36
  GH_OWNER: { env: true },
@@ -40,9 +38,8 @@ export const CONFIG_TARGETS = {
40
38
  APP_URL: { env: true, variable: true },
41
39
  APP_HOSTNAME: { env: true },
42
40
 
43
- // GitHub-only
44
- BRAVE_API_KEY: { secret: 'AGENT_LLM_BRAVE_API_KEY' },
41
+ // GitHub variables consumed by scaffolded workflows
45
42
  AUTO_MERGE: { variable: true, default: 'true', firstRunOnly: true },
46
- ALLOWED_PATHS: { variable: true, default: '/logs', firstRunOnly: true },
43
+ ALLOWED_PATHS: { variable: true, default: '/logs,/agents', firstRunOnly: true },
47
44
  RUNS_ON: { variable: true },
48
45
  };