thepopebot 1.0.0
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/LICENSE +21 -0
- package/README.md +127 -0
- package/api/index.js +357 -0
- package/bin/cli.js +278 -0
- package/config/index.js +29 -0
- package/config/instrumentation.js +29 -0
- package/docker/Dockerfile +51 -0
- package/docker/entrypoint.sh +100 -0
- package/lib/actions.js +40 -0
- package/lib/claude/conversation.js +76 -0
- package/lib/claude/index.js +142 -0
- package/lib/claude/tools.js +54 -0
- package/lib/cron.js +60 -0
- package/lib/paths.js +30 -0
- package/lib/tools/create-job.js +40 -0
- package/lib/tools/github.js +122 -0
- package/lib/tools/openai.js +35 -0
- package/lib/tools/telegram.js +222 -0
- package/lib/triggers.js +105 -0
- package/lib/utils/render-md.js +39 -0
- package/package.json +57 -0
- package/pi/extensions/env-sanitizer/index.ts +48 -0
- package/pi/extensions/env-sanitizer/package.json +5 -0
- package/pi/skills/llm-secrets/SKILL.md +34 -0
- package/pi/skills/llm-secrets/llm-secrets.js +34 -0
- package/setup/lib/auth.mjs +160 -0
- package/setup/lib/github.mjs +148 -0
- package/setup/lib/prerequisites.mjs +135 -0
- package/setup/lib/prompts.mjs +268 -0
- package/setup/lib/telegram-verify.mjs +66 -0
- package/setup/lib/telegram.mjs +76 -0
- package/setup/package.json +6 -0
- package/setup/setup-telegram.mjs +236 -0
- package/setup/setup.mjs +540 -0
- package/templates/.env.example +38 -0
- package/templates/.github/workflows/auto-merge.yml +117 -0
- package/templates/.github/workflows/docker-build.yml +34 -0
- package/templates/.github/workflows/run-job.yml +40 -0
- package/templates/.github/workflows/update-event-handler.yml +126 -0
- package/templates/.pi/skills/modify-self/SKILL.md +12 -0
- package/templates/CLAUDE.md +52 -0
- package/templates/app/api/[...thepopebot]/route.js +1 -0
- package/templates/app/layout.js +12 -0
- package/templates/app/page.js +8 -0
- package/templates/instrumentation.js +1 -0
- package/templates/next.config.mjs +3 -0
- package/templates/operating_system/AGENT.md +32 -0
- package/templates/operating_system/CHATBOT.md +74 -0
- package/templates/operating_system/CRONS.json +16 -0
- package/templates/operating_system/HEARTBEAT.md +3 -0
- package/templates/operating_system/JOB_SUMMARY.md +36 -0
- package/templates/operating_system/SOUL.md +17 -0
- package/templates/operating_system/TELEGRAM.md +21 -0
- package/templates/operating_system/TRIGGERS.json +18 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const paths = require('../paths');
|
|
2
|
+
const { render_md } = require('../utils/render-md');
|
|
3
|
+
|
|
4
|
+
const DEFAULT_MODEL = 'claude-sonnet-4-20250514';
|
|
5
|
+
|
|
6
|
+
// Web search tool definition (Anthropic built-in)
|
|
7
|
+
const WEB_SEARCH_TOOL = {
|
|
8
|
+
type: 'web_search_20250305',
|
|
9
|
+
name: 'web_search',
|
|
10
|
+
max_uses: 5,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get Anthropic API key from environment
|
|
15
|
+
* @returns {string} API key
|
|
16
|
+
*/
|
|
17
|
+
function getApiKey() {
|
|
18
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
19
|
+
return process.env.ANTHROPIC_API_KEY;
|
|
20
|
+
}
|
|
21
|
+
throw new Error('ANTHROPIC_API_KEY environment variable is required');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Call Claude API
|
|
26
|
+
* @param {Array} messages - Conversation messages
|
|
27
|
+
* @param {Array} tools - Tool definitions
|
|
28
|
+
* @returns {Promise<Object>} API response
|
|
29
|
+
*/
|
|
30
|
+
async function callClaude(messages, tools) {
|
|
31
|
+
const apiKey = getApiKey();
|
|
32
|
+
const model = process.env.EVENT_HANDLER_MODEL || DEFAULT_MODEL;
|
|
33
|
+
const systemPrompt = render_md(paths.chatbotMd);
|
|
34
|
+
|
|
35
|
+
// Combine user tools with web search
|
|
36
|
+
const allTools = [WEB_SEARCH_TOOL, ...tools];
|
|
37
|
+
|
|
38
|
+
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
39
|
+
method: 'POST',
|
|
40
|
+
headers: {
|
|
41
|
+
'Content-Type': 'application/json',
|
|
42
|
+
'x-api-key': apiKey,
|
|
43
|
+
'anthropic-version': '2023-06-01',
|
|
44
|
+
'anthropic-beta': 'web-search-2025-03-05',
|
|
45
|
+
},
|
|
46
|
+
body: JSON.stringify({
|
|
47
|
+
model,
|
|
48
|
+
max_tokens: 4096,
|
|
49
|
+
system: systemPrompt,
|
|
50
|
+
messages,
|
|
51
|
+
tools: allTools,
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (!response.ok) {
|
|
56
|
+
const error = await response.text();
|
|
57
|
+
throw new Error(`Claude API error: ${response.status} ${error}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return response.json();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Process a conversation turn with Claude, handling tool calls
|
|
65
|
+
* @param {string} userMessage - User's message
|
|
66
|
+
* @param {Array} history - Conversation history
|
|
67
|
+
* @param {Array} toolDefinitions - Available tools
|
|
68
|
+
* @param {Object} toolExecutors - Tool executor functions
|
|
69
|
+
* @returns {Promise<{response: string, history: Array}>}
|
|
70
|
+
*/
|
|
71
|
+
async function chat(userMessage, history, toolDefinitions, toolExecutors) {
|
|
72
|
+
// Add user message to history
|
|
73
|
+
const messages = [...history, { role: 'user', content: userMessage }];
|
|
74
|
+
|
|
75
|
+
let response = await callClaude(messages, toolDefinitions);
|
|
76
|
+
let assistantContent = response.content;
|
|
77
|
+
|
|
78
|
+
// Add assistant response to history
|
|
79
|
+
messages.push({ role: 'assistant', content: assistantContent });
|
|
80
|
+
|
|
81
|
+
// Handle tool use loop
|
|
82
|
+
while (response.stop_reason === 'tool_use') {
|
|
83
|
+
const toolResults = [];
|
|
84
|
+
|
|
85
|
+
for (const block of assistantContent) {
|
|
86
|
+
if (block.type === 'tool_use') {
|
|
87
|
+
// Skip web_search - it's a server-side tool executed by Anthropic
|
|
88
|
+
if (block.name === 'web_search') {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const executor = toolExecutors[block.name];
|
|
93
|
+
let result;
|
|
94
|
+
|
|
95
|
+
if (executor) {
|
|
96
|
+
try {
|
|
97
|
+
result = await executor(block.input);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
result = { error: err.message };
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
result = { error: `Unknown tool: ${block.name}` };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
toolResults.push({
|
|
106
|
+
type: 'tool_result',
|
|
107
|
+
tool_use_id: block.id,
|
|
108
|
+
content: JSON.stringify(result),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// If no client-side tools to execute, we're done
|
|
114
|
+
if (toolResults.length === 0) {
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Add tool results to messages
|
|
119
|
+
messages.push({ role: 'user', content: toolResults });
|
|
120
|
+
|
|
121
|
+
// Get next response from Claude
|
|
122
|
+
response = await callClaude(messages, toolDefinitions);
|
|
123
|
+
assistantContent = response.content;
|
|
124
|
+
|
|
125
|
+
// Add new assistant response to history
|
|
126
|
+
messages.push({ role: 'assistant', content: assistantContent });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Extract text response
|
|
130
|
+
const textBlocks = assistantContent.filter((block) => block.type === 'text');
|
|
131
|
+
const responseText = textBlocks.map((block) => block.text).join('\n');
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
response: responseText,
|
|
135
|
+
history: messages,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
chat,
|
|
141
|
+
getApiKey,
|
|
142
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const { createJob } = require('../tools/create-job');
|
|
2
|
+
const { getJobStatus } = require('../tools/github');
|
|
3
|
+
|
|
4
|
+
const toolDefinitions = [
|
|
5
|
+
{
|
|
6
|
+
name: 'create_job',
|
|
7
|
+
description:
|
|
8
|
+
'Create an autonomous job for thepopebot to execute. Use this tool liberally - if the user asks for ANY task to be done, create a job. Jobs can handle code changes, file updates, research tasks, web scraping, data analysis, or anything requiring autonomous work. When the user explicitly asks for a job, ALWAYS use this tool. Returns the job ID and branch name.',
|
|
9
|
+
input_schema: {
|
|
10
|
+
type: 'object',
|
|
11
|
+
properties: {
|
|
12
|
+
job_description: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
description:
|
|
15
|
+
'Detailed job description including context and requirements. Be specific about what needs to be done.',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
required: ['job_description'],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'get_job_status',
|
|
23
|
+
description:
|
|
24
|
+
'Check status of running jobs. Returns list of active workflow runs with timing and current step. Use when user asks about job progress, running jobs, or job status.',
|
|
25
|
+
input_schema: {
|
|
26
|
+
type: 'object',
|
|
27
|
+
properties: {
|
|
28
|
+
job_id: {
|
|
29
|
+
type: 'string',
|
|
30
|
+
description:
|
|
31
|
+
'Optional: specific job ID to check. If omitted, returns all running jobs.',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required: [],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const toolExecutors = {
|
|
40
|
+
create_job: async (input) => {
|
|
41
|
+
const result = await createJob(input.job_description);
|
|
42
|
+
return {
|
|
43
|
+
success: true,
|
|
44
|
+
job_id: result.job_id,
|
|
45
|
+
branch: result.branch,
|
|
46
|
+
};
|
|
47
|
+
},
|
|
48
|
+
get_job_status: async (input) => {
|
|
49
|
+
const result = await getJobStatus(input.job_id);
|
|
50
|
+
return result;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
module.exports = { toolDefinitions, toolExecutors };
|
package/lib/cron.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const cron = require('node-cron');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const paths = require('./paths');
|
|
4
|
+
|
|
5
|
+
const { executeAction } = require('./actions');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Load and schedule crons from CRONS.json
|
|
9
|
+
* @returns {Array} - Array of scheduled cron tasks
|
|
10
|
+
*/
|
|
11
|
+
function loadCrons() {
|
|
12
|
+
const cronFile = paths.cronsFile;
|
|
13
|
+
|
|
14
|
+
console.log('\n--- Cron Jobs ---');
|
|
15
|
+
|
|
16
|
+
if (!fs.existsSync(cronFile)) {
|
|
17
|
+
console.log('No CRONS.json found');
|
|
18
|
+
console.log('-----------------\n');
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const crons = JSON.parse(fs.readFileSync(cronFile, 'utf8'));
|
|
23
|
+
const tasks = [];
|
|
24
|
+
|
|
25
|
+
for (const cronEntry of crons) {
|
|
26
|
+
const { name, schedule, type = 'agent', enabled } = cronEntry;
|
|
27
|
+
if (enabled === false) continue;
|
|
28
|
+
|
|
29
|
+
if (!cron.validate(schedule)) {
|
|
30
|
+
console.error(`Invalid schedule for "${name}": ${schedule}`);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const task = cron.schedule(schedule, async () => {
|
|
35
|
+
try {
|
|
36
|
+
const result = await executeAction(cronEntry, { cwd: paths.cronDir });
|
|
37
|
+
console.log(`[CRON] ${name}: ${result || 'ran'}`);
|
|
38
|
+
console.log(`[CRON] ${name}: completed!`);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.error(`[CRON] ${name}: error - ${err.message}`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
tasks.push({ name, schedule, type, task });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (tasks.length === 0) {
|
|
48
|
+
console.log('No active cron jobs');
|
|
49
|
+
} else {
|
|
50
|
+
for (const { name, schedule, type } of tasks) {
|
|
51
|
+
console.log(` ${name}: ${schedule} (${type})`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log('-----------------\n');
|
|
56
|
+
|
|
57
|
+
return tasks;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { loadCrons };
|
package/lib/paths.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Central path resolver for thepopebot.
|
|
5
|
+
* All paths resolve from process.cwd() (the user's project root).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const PROJECT_ROOT = process.cwd();
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
PROJECT_ROOT,
|
|
12
|
+
|
|
13
|
+
// operating_system/ files
|
|
14
|
+
operatingSystemDir: path.join(PROJECT_ROOT, 'operating_system'),
|
|
15
|
+
cronsFile: path.join(PROJECT_ROOT, 'operating_system', 'CRONS.json'),
|
|
16
|
+
triggersFile: path.join(PROJECT_ROOT, 'operating_system', 'TRIGGERS.json'),
|
|
17
|
+
chatbotMd: path.join(PROJECT_ROOT, 'operating_system', 'CHATBOT.md'),
|
|
18
|
+
jobSummaryMd: path.join(PROJECT_ROOT, 'operating_system', 'JOB_SUMMARY.md'),
|
|
19
|
+
soulMd: path.join(PROJECT_ROOT, 'operating_system', 'SOUL.md'),
|
|
20
|
+
|
|
21
|
+
// Working directories for command-type actions
|
|
22
|
+
cronDir: path.join(PROJECT_ROOT, 'cron'),
|
|
23
|
+
triggersDir: path.join(PROJECT_ROOT, 'triggers'),
|
|
24
|
+
|
|
25
|
+
// Logs
|
|
26
|
+
logsDir: path.join(PROJECT_ROOT, 'logs'),
|
|
27
|
+
|
|
28
|
+
// .env
|
|
29
|
+
envFile: path.join(PROJECT_ROOT, '.env'),
|
|
30
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const { v4: uuidv4 } = require('uuid');
|
|
2
|
+
const { githubApi } = require('./github');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create a new job branch with updated job.md
|
|
6
|
+
* @param {string} jobDescription - The job description to write to job.md
|
|
7
|
+
* @returns {Promise<{job_id: string, branch: string}>} - Job ID and branch name
|
|
8
|
+
*/
|
|
9
|
+
async function createJob(jobDescription) {
|
|
10
|
+
const { GH_OWNER, GH_REPO } = process.env;
|
|
11
|
+
const jobId = uuidv4();
|
|
12
|
+
const branch = `job/${jobId}`;
|
|
13
|
+
|
|
14
|
+
// 1. Get main branch SHA
|
|
15
|
+
const mainRef = await githubApi(`/repos/${GH_OWNER}/${GH_REPO}/git/ref/heads/main`);
|
|
16
|
+
const mainSha = mainRef.object.sha;
|
|
17
|
+
|
|
18
|
+
// 2. Create new branch
|
|
19
|
+
await githubApi(`/repos/${GH_OWNER}/${GH_REPO}/git/refs`, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
body: JSON.stringify({
|
|
22
|
+
ref: `refs/heads/${branch}`,
|
|
23
|
+
sha: mainSha,
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// 3. Create logs/${jobId}/job.md with job content
|
|
28
|
+
await githubApi(`/repos/${GH_OWNER}/${GH_REPO}/contents/logs/${jobId}/job.md`, {
|
|
29
|
+
method: 'PUT',
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
message: `job: ${jobId}`,
|
|
32
|
+
content: Buffer.from(jobDescription).toString('base64'),
|
|
33
|
+
branch: branch,
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return { job_id: jobId, branch };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { createJob };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub REST API helper with authentication
|
|
3
|
+
* @param {string} endpoint - API endpoint (e.g., '/repos/owner/repo/...')
|
|
4
|
+
* @param {object} options - Fetch options (method, body, headers)
|
|
5
|
+
* @returns {Promise<object>} - Parsed JSON response
|
|
6
|
+
*/
|
|
7
|
+
async function githubApi(endpoint, options = {}) {
|
|
8
|
+
const { GH_TOKEN } = process.env;
|
|
9
|
+
const res = await fetch(`https://api.github.com${endpoint}`, {
|
|
10
|
+
...options,
|
|
11
|
+
headers: {
|
|
12
|
+
'Authorization': `Bearer ${GH_TOKEN}`,
|
|
13
|
+
'Accept': 'application/vnd.github+json',
|
|
14
|
+
'X-GitHub-Api-Version': '2022-11-28',
|
|
15
|
+
...options.headers,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
if (!res.ok) {
|
|
20
|
+
const error = await res.text();
|
|
21
|
+
throw new Error(`GitHub API error: ${res.status} ${error}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return res.json();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get workflow runs with optional status filter
|
|
29
|
+
* @param {string} [status] - Filter by status (in_progress, queued, completed)
|
|
30
|
+
* @returns {Promise<object>} - Workflow runs response
|
|
31
|
+
*/
|
|
32
|
+
async function getWorkflowRuns(status) {
|
|
33
|
+
const { GH_OWNER, GH_REPO } = process.env;
|
|
34
|
+
const params = new URLSearchParams();
|
|
35
|
+
if (status) params.set('status', status);
|
|
36
|
+
params.set('per_page', '20');
|
|
37
|
+
|
|
38
|
+
const query = params.toString();
|
|
39
|
+
return githubApi(`/repos/${GH_OWNER}/${GH_REPO}/actions/runs?${query}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get jobs for a specific workflow run
|
|
44
|
+
* @param {number} runId - Workflow run ID
|
|
45
|
+
* @returns {Promise<object>} - Jobs response with steps
|
|
46
|
+
*/
|
|
47
|
+
async function getWorkflowRunJobs(runId) {
|
|
48
|
+
const { GH_OWNER, GH_REPO } = process.env;
|
|
49
|
+
return githubApi(`/repos/${GH_OWNER}/${GH_REPO}/actions/runs/${runId}/jobs`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get job status for running/recent jobs
|
|
54
|
+
* @param {string} [jobId] - Optional specific job ID to filter by
|
|
55
|
+
* @returns {Promise<object>} - Status summary with jobs array
|
|
56
|
+
*/
|
|
57
|
+
async function getJobStatus(jobId) {
|
|
58
|
+
// Fetch both in_progress and queued runs
|
|
59
|
+
const [inProgress, queued] = await Promise.all([
|
|
60
|
+
getWorkflowRuns('in_progress'),
|
|
61
|
+
getWorkflowRuns('queued'),
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
const allRuns = [...(inProgress.workflow_runs || []), ...(queued.workflow_runs || [])];
|
|
65
|
+
|
|
66
|
+
// Filter to only job/* branches
|
|
67
|
+
const jobRuns = allRuns.filter(run => run.head_branch?.startsWith('job/'));
|
|
68
|
+
|
|
69
|
+
// If specific job requested, filter further
|
|
70
|
+
const filteredRuns = jobId
|
|
71
|
+
? jobRuns.filter(run => run.head_branch === `job/${jobId}`)
|
|
72
|
+
: jobRuns;
|
|
73
|
+
|
|
74
|
+
// Get detailed job info for each run
|
|
75
|
+
const jobs = await Promise.all(
|
|
76
|
+
filteredRuns.map(async (run) => {
|
|
77
|
+
const extractedJobId = run.head_branch.slice(4); // Remove 'job/' prefix
|
|
78
|
+
const startedAt = new Date(run.created_at);
|
|
79
|
+
const durationMinutes = Math.round((Date.now() - startedAt.getTime()) / 60000);
|
|
80
|
+
|
|
81
|
+
let currentStep = null;
|
|
82
|
+
let stepsCompleted = 0;
|
|
83
|
+
let stepsTotal = 0;
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const jobsData = await getWorkflowRunJobs(run.id);
|
|
87
|
+
if (jobsData.jobs?.length > 0) {
|
|
88
|
+
const job = jobsData.jobs[0];
|
|
89
|
+
stepsTotal = job.steps?.length || 0;
|
|
90
|
+
stepsCompleted = job.steps?.filter(s => s.status === 'completed').length || 0;
|
|
91
|
+
currentStep = job.steps?.find(s => s.status === 'in_progress')?.name || null;
|
|
92
|
+
}
|
|
93
|
+
} catch (err) {
|
|
94
|
+
// Jobs endpoint may fail if run hasn't started yet
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
job_id: extractedJobId,
|
|
99
|
+
branch: run.head_branch,
|
|
100
|
+
status: run.status,
|
|
101
|
+
started_at: run.created_at,
|
|
102
|
+
duration_minutes: durationMinutes,
|
|
103
|
+
current_step: currentStep,
|
|
104
|
+
steps_completed: stepsCompleted,
|
|
105
|
+
steps_total: stepsTotal,
|
|
106
|
+
run_id: run.id,
|
|
107
|
+
};
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
// Count only job/* branches, not all workflows
|
|
112
|
+
const runningCount = jobs.filter(j => j.status === 'in_progress').length;
|
|
113
|
+
const queuedCount = jobs.filter(j => j.status === 'queued').length;
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
jobs,
|
|
117
|
+
queued: queuedCount,
|
|
118
|
+
running: runningCount,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { githubApi, getWorkflowRuns, getWorkflowRunJobs, getJobStatus };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if Whisper transcription is enabled
|
|
3
|
+
* @returns {boolean}
|
|
4
|
+
*/
|
|
5
|
+
function isWhisperEnabled() {
|
|
6
|
+
return Boolean(process.env.OPENAI_API_KEY);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Transcribe audio using OpenAI Whisper API
|
|
11
|
+
* @param {Buffer} audioBuffer - Audio file buffer
|
|
12
|
+
* @param {string} filename - Original filename (e.g., "voice.ogg")
|
|
13
|
+
* @returns {Promise<string>} Transcribed text
|
|
14
|
+
*/
|
|
15
|
+
async function transcribeAudio(audioBuffer, filename) {
|
|
16
|
+
const formData = new FormData();
|
|
17
|
+
formData.append('file', new Blob([audioBuffer]), filename);
|
|
18
|
+
formData.append('model', 'whisper-1');
|
|
19
|
+
|
|
20
|
+
const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: { 'Authorization': `Bearer ${process.env.OPENAI_API_KEY}` },
|
|
23
|
+
body: formData,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
const error = await response.text();
|
|
28
|
+
throw new Error(`OpenAI API error: ${response.status} ${error}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const result = await response.json();
|
|
32
|
+
return result.text;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { isWhisperEnabled, transcribeAudio };
|