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.
- package/README.md +3 -3
- package/api/CLAUDE.md +11 -4
- package/api/index.js +56 -18
- package/bin/CLAUDE.md +7 -4
- package/bin/cli.js +25 -45
- package/config/CLAUDE.md +23 -4
- package/drizzle/0021_coding_agent_workspace.sql +1 -0
- package/drizzle/0022_organic_apocalypse.sql +16 -0
- package/drizzle/0023_needy_ender_wiggin.sql +1 -0
- package/drizzle/meta/0021_snapshot.json +639 -0
- package/drizzle/meta/0022_snapshot.json +743 -0
- package/drizzle/meta/0023_snapshot.json +750 -0
- package/drizzle/meta/_journal.json +21 -0
- package/lib/CLAUDE.md +2 -2
- package/lib/actions.js +9 -1
- package/lib/ai/CLAUDE.md +72 -57
- package/lib/ai/helper-llm.js +108 -0
- package/lib/ai/index.js +308 -438
- package/lib/ai/line-mappers.js +42 -24
- package/lib/ai/scope.js +26 -0
- package/lib/ai/sdk-adapters/CLAUDE.md +114 -0
- package/lib/ai/sdk-adapters/claude-code.js +120 -8
- package/lib/ai/system-prompt.js +34 -0
- package/lib/ai/workspace-setup.js +19 -35
- package/lib/channels/CLAUDE.md +14 -4
- package/lib/channels/base.js +6 -2
- package/lib/channels/commands/index.js +42 -0
- package/lib/channels/commands/session.js +53 -0
- package/lib/channels/commands/verify.js +18 -0
- package/lib/channels/telegram.js +79 -28
- package/lib/chat/CLAUDE.md +4 -4
- package/lib/chat/actions.js +270 -49
- package/lib/chat/api.js +185 -31
- package/lib/chat/components/CLAUDE.md +6 -2
- package/lib/chat/components/chat-input.js +77 -47
- package/lib/chat/components/chat-input.jsx +77 -40
- package/lib/chat/components/chat-page.js +2 -0
- package/lib/chat/components/chat-page.jsx +3 -0
- package/lib/chat/components/chat.js +62 -14
- package/lib/chat/components/chat.jsx +68 -10
- package/lib/chat/components/code-mode-toggle.js +141 -22
- package/lib/chat/components/code-mode-toggle.jsx +129 -20
- package/lib/chat/components/containers-page.js +58 -40
- package/lib/chat/components/containers-page.jsx +64 -25
- package/lib/chat/components/crons-page.js +17 -3
- package/lib/chat/components/crons-page.jsx +34 -6
- package/lib/chat/components/index.js +2 -2
- package/lib/chat/components/message.js +18 -3
- package/lib/chat/components/message.jsx +18 -3
- package/lib/chat/components/profile-page.js +182 -4
- package/lib/chat/components/profile-page.jsx +196 -1
- package/lib/chat/components/scope-picker.js +21 -0
- package/lib/chat/components/scope-picker.jsx +27 -0
- package/lib/chat/components/settings-chat-page.js +11 -11
- package/lib/chat/components/settings-chat-page.jsx +14 -18
- package/lib/chat/components/settings-coding-agents-page.js +110 -16
- package/lib/chat/components/settings-coding-agents-page.jsx +87 -3
- package/lib/chat/components/settings-github-page.js +5 -0
- package/lib/chat/components/settings-github-page.jsx +5 -0
- package/lib/chat/components/settings-layout.js +3 -3
- package/lib/chat/components/settings-layout.jsx +3 -3
- package/lib/chat/components/settings-secrets-layout.js +1 -2
- package/lib/chat/components/settings-secrets-layout.jsx +1 -2
- package/lib/chat/components/settings-secrets-page.js +180 -75
- package/lib/chat/components/settings-secrets-page.jsx +212 -66
- package/lib/chat/components/triggers-page.js +17 -3
- package/lib/chat/components/triggers-page.jsx +34 -6
- package/lib/chat/components/ui/combobox.js +18 -2
- package/lib/chat/components/ui/combobox.jsx +17 -1
- package/lib/chat/components/ui/dropdown-menu.js +23 -2
- package/lib/chat/components/ui/dropdown-menu.jsx +27 -2
- package/lib/chat/telegram-profile.js +33 -0
- package/lib/cluster/CLAUDE.md +9 -3
- package/lib/code/CLAUDE.md +11 -3
- package/lib/code/actions.js +47 -8
- package/lib/code/terminal-view.js +31 -21
- package/lib/code/terminal-view.jsx +32 -23
- package/lib/config.js +15 -4
- package/lib/containers/CLAUDE.md +16 -6
- package/lib/db/CLAUDE.md +5 -2
- package/lib/db/chats.js +9 -17
- package/lib/db/code-workspaces.js +8 -3
- package/lib/db/config.js +0 -1
- package/lib/db/index.js +12 -0
- package/lib/db/schema.js +24 -1
- package/lib/db/user-channels.js +129 -0
- package/lib/llm-providers.js +8 -0
- package/lib/maintenance.js +31 -21
- package/lib/tools/CLAUDE.md +12 -3
- package/lib/tools/assemblyai.js +17 -0
- package/lib/tools/create-agent-job.js +12 -8
- package/lib/tools/docker.js +34 -10
- package/lib/tools/github.js +34 -0
- package/lib/tools/telegram.js +106 -0
- package/lib/utils/render-md.js +44 -18
- package/package.json +8 -8
- package/setup/CLAUDE.md +11 -5
- package/setup/lib/providers.mjs +2 -1
- package/setup/lib/targets.mjs +13 -16
- package/setup/lib/telegram.mjs +8 -69
- package/templates/.env.example +0 -7
- package/templates/.github/workflows/rebuild-event-handler.yml +1 -1
- package/templates/.gitignore.template +1 -3
- package/templates/CLAUDE.md +1 -1
- package/templates/CLAUDE.md.template +29 -7
- package/templates/agent-job/CLAUDE.md.template +5 -3
- package/templates/agent-job/CRONS.json +16 -0
- package/templates/agent-job/SYSTEM.md +16 -11
- package/templates/agents/CLAUDE.md.template +17 -17
- package/templates/coding-workspace/CLAUDE.md.template +7 -0
- package/templates/data/CLAUDE.md.template +1 -1
- package/templates/docker-compose.custom.yml +1 -0
- package/templates/docker-compose.yml +1 -0
- package/templates/event-handler/CLAUDE.md.template +79 -0
- package/templates/event-handler/TRIGGERS.json +18 -2
- package/templates/skills/CLAUDE.md.template +20 -22
- package/templates/skills/{library/agent-job-secrets → agent-job-secrets}/SKILL.md +2 -2
- package/lib/ai/agent.js +0 -65
- package/lib/ai/async-channel.js +0 -51
- package/lib/ai/model.js +0 -130
- package/lib/ai/tools.js +0 -164
- package/lib/tools/openai.js +0 -37
- package/setup/lib/telegram-verify.mjs +0 -63
- package/setup/setup-telegram.mjs +0 -260
- package/templates/agent-job/SOUL.md +0 -17
- /package/templates/{skills/active/.gitkeep → coding-workspace/SYSTEM.md} +0 -0
- /package/templates/skills/{library/agent-job-secrets → agent-job-secrets}/agent-job-secrets.js +0 -0
- /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 {
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
});
|
package/lib/tools/docker.js
CHANGED
|
@@ -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
|
-
|
|
250
|
-
|
|
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
|
-
|
|
460
|
-
|
|
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);
|
package/lib/tools/github.js
CHANGED
|
@@ -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
|
};
|
package/lib/tools/telegram.js
CHANGED
|
@@ -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
|
};
|
package/lib/utils/render-md.js
CHANGED
|
@@ -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
|
-
//
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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(
|
|
22
|
+
if (!fs.existsSync(dir)) {
|
|
16
23
|
return 'No additional abilities configured.';
|
|
17
24
|
}
|
|
18
25
|
|
|
19
|
-
const entries = fs.readdirSync(
|
|
26
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
20
27
|
const descriptions = [];
|
|
21
28
|
|
|
22
29
|
for (const entry of entries) {
|
|
23
|
-
if
|
|
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(
|
|
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 {
|
|
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,
|
|
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.
|
|
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.
|
|
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. **
|
|
8
|
-
2. **
|
|
9
|
-
3. **
|
|
10
|
-
4. **
|
|
11
|
-
5. **
|
|
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`:
|
package/setup/lib/providers.mjs
CHANGED
|
@@ -14,7 +14,8 @@ export const PROVIDERS = {
|
|
|
14
14
|
builtin: true,
|
|
15
15
|
oauthSupported: true,
|
|
16
16
|
models: [
|
|
17
|
-
{ id: 'claude-opus-4-
|
|
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
|
],
|
package/setup/lib/targets.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
31
|
-
LLM_MODEL: { db: true
|
|
32
|
-
CUSTOM_OPENAI_BASE_URL: { db: true
|
|
33
|
-
AGENT_BACKEND: { db: 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
|
|
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
|
};
|