thepopebot 1.2.70 → 1.2.71-beta.1
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 +14 -5
- package/api/index.js +8 -2
- package/lib/actions.js +4 -1
- package/lib/chat/actions.js +5 -1
- package/lib/chat/components/crons-page.js +5 -1
- package/lib/chat/components/crons-page.jsx +8 -0
- package/lib/chat/components/triggers-page.js +7 -1
- package/lib/chat/components/triggers-page.jsx +13 -3
- package/lib/cron.js +96 -19
- package/lib/tools/create-job.js +19 -1
- package/lib/tools/github.js +28 -0
- package/package.json +1 -1
- package/setup/lib/github.mjs +4 -2
- package/setup/lib/prerequisites.mjs +22 -2
- package/setup/lib/prompts.mjs +3 -3
- package/setup/setup.mjs +32 -12
- package/templates/.github/workflows/auto-merge.yml +1 -1
- package/templates/.github/workflows/notify-job-failed.yml +3 -5
- package/templates/.github/workflows/notify-pr-complete.yml +4 -11
- package/templates/.github/workflows/rebuild-event-handler.yml +5 -1
- package/templates/.github/workflows/run-job.yml +16 -3
- package/templates/.github/workflows/upgrade-event-handler.yml +15 -1
- package/templates/config/CRONS.json +9 -0
- package/templates/config/TRIGGERS.json +8 -0
package/README.md
CHANGED
|
@@ -64,10 +64,10 @@ You interact with your bot via the web chat interface or Telegram (optional). Th
|
|
|
64
64
|
| **npm** | Included with Node.js |
|
|
65
65
|
| **Git** | [git-scm.com](https://git-scm.com) |
|
|
66
66
|
| **GitHub CLI** | [cli.github.com](https://cli.github.com) |
|
|
67
|
-
| **Docker + Docker Compose** | [docker.com](https://docs.docker.com/get-docker/) |
|
|
68
|
-
| **ngrok*** | [ngrok.com](https://ngrok.com/download) |
|
|
67
|
+
| **Docker + Docker Compose** | [docker.com](https://docs.docker.com/get-docker/) (installer requires admin password) |
|
|
68
|
+
| **ngrok*** | [ngrok.com](https://ngrok.com/download) (free account + authtoken required) |
|
|
69
69
|
|
|
70
|
-
*\*ngrok is only required for local installs without port forwarding. VPS/cloud deployments don't need it.*
|
|
70
|
+
*\*ngrok is only required for local installs without port forwarding. VPS/cloud deployments don't need it. [Sign up](https://dashboard.ngrok.com/signup) for a free ngrok account, then run `ngrok config add-authtoken <YOUR_TOKEN>` before starting setup.*
|
|
71
71
|
|
|
72
72
|
### Three steps
|
|
73
73
|
|
|
@@ -106,7 +106,16 @@ docker compose up -d
|
|
|
106
106
|
- **Webhook**: Send a POST to `/api/create-job` with your API key to create jobs programmatically
|
|
107
107
|
- **Cron**: Edit `config/CRONS.json` to schedule recurring jobs
|
|
108
108
|
|
|
109
|
-
> **Local installs**: Your server needs to be reachable from the internet for GitHub webhooks and Telegram. On a VPS/cloud server, your APP_URL is just your domain. For local development, use [ngrok](https://ngrok.com) (`ngrok http 80`) or port forwarding to expose your machine.
|
|
109
|
+
> **Local installs**: Your server needs to be reachable from the internet for GitHub webhooks and Telegram. On a VPS/cloud server, your APP_URL is just your domain. For local development, use [ngrok](https://ngrok.com) (`ngrok http 80`) or port forwarding to expose your machine.
|
|
110
|
+
>
|
|
111
|
+
> **If your ngrok URL changes** (it changes every time you restart ngrok on the free plan), you must update APP_URL everywhere:
|
|
112
|
+
>
|
|
113
|
+
> ```bash
|
|
114
|
+
> # Update .env and GitHub variable in one command:
|
|
115
|
+
> npx thepopebot set-var APP_URL https://your-new-url.ngrok.io
|
|
116
|
+
> # If Telegram is configured, re-register the webhook:
|
|
117
|
+
> npm run setup-telegram
|
|
118
|
+
> ```
|
|
110
119
|
|
|
111
120
|
---
|
|
112
121
|
|
|
@@ -272,7 +281,7 @@ See [docs/SECURITY.md](docs/SECURITY.md) for full details on what's exposed, the
|
|
|
272
281
|
| [Auto-Merge](docs/AUTO_MERGE.md) | Auto-merge controls, ALLOWED_PATHS configuration |
|
|
273
282
|
| [Deployment](docs/DEPLOYMENT.md) | VPS setup, Docker Compose, HTTPS with Let's Encrypt |
|
|
274
283
|
| [How to Use Pi](docs/HOW_TO_USE_PI.md) | Guide to the Pi coding agent |
|
|
275
|
-
| [Pre-Release](docs/PRE_RELEASE.md) | Installing beta/alpha builds
|
|
284
|
+
| [Pre-Release](docs/PRE_RELEASE.md) | Installing beta/alpha builds |
|
|
276
285
|
| [Security](docs/SECURITY.md) | Security disclaimer, local development risks |
|
|
277
286
|
| [Upgrading](docs/UPGRADE.md) | Automated upgrades, recovering from failed upgrades |
|
|
278
287
|
|
package/api/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash, timingSafeEqual } from 'crypto';
|
|
2
2
|
import { createJob } from '../lib/tools/create-job.js';
|
|
3
3
|
import { setWebhook } from '../lib/tools/telegram.js';
|
|
4
|
-
import { getJobStatus } from '../lib/tools/github.js';
|
|
4
|
+
import { getJobStatus, fetchJobLog } from '../lib/tools/github.js';
|
|
5
5
|
import { getTelegramAdapter } from '../lib/channels/index.js';
|
|
6
6
|
import { chat, summarizeJob } from '../lib/ai/index.js';
|
|
7
7
|
import { createNotification } from '../lib/db/notifications.js';
|
|
@@ -171,13 +171,19 @@ async function handleGithubWebhook(request) {
|
|
|
171
171
|
if (!jobId) return Response.json({ ok: true, skipped: true, reason: 'not a job' });
|
|
172
172
|
|
|
173
173
|
try {
|
|
174
|
+
// Fetch log from repo via API (no longer sent in payload)
|
|
175
|
+
let log = payload.log || '';
|
|
176
|
+
if (!log) {
|
|
177
|
+
log = await fetchJobLog(jobId, payload.commit_sha);
|
|
178
|
+
}
|
|
179
|
+
|
|
174
180
|
const results = {
|
|
175
181
|
job: payload.job || '',
|
|
176
182
|
pr_url: payload.pr_url || payload.run_url || '',
|
|
177
183
|
run_url: payload.run_url || '',
|
|
178
184
|
status: payload.status || '',
|
|
179
185
|
merge_result: payload.merge_result || '',
|
|
180
|
-
log
|
|
186
|
+
log,
|
|
181
187
|
changed_files: payload.changed_files || [],
|
|
182
188
|
commit_message: payload.commit_message || '',
|
|
183
189
|
};
|
package/lib/actions.js
CHANGED
|
@@ -34,7 +34,10 @@ async function executeAction(action, opts = {}) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
// Default: agent
|
|
37
|
-
const
|
|
37
|
+
const options = {};
|
|
38
|
+
if (action.llm_provider) options.llmProvider = action.llm_provider;
|
|
39
|
+
if (action.llm_model) options.llmModel = action.llm_model;
|
|
40
|
+
const result = await createJob(action.job, options);
|
|
38
41
|
return `job ${result.job_id}`;
|
|
39
42
|
}
|
|
40
43
|
|
package/lib/chat/actions.js
CHANGED
|
@@ -176,7 +176,11 @@ export async function getAppVersion() {
|
|
|
176
176
|
export async function triggerUpgrade() {
|
|
177
177
|
await requireAuth();
|
|
178
178
|
const { triggerWorkflowDispatch } = await import('../tools/github.js');
|
|
179
|
-
await
|
|
179
|
+
const { getAvailableVersion } = await import('../db/update-check.js');
|
|
180
|
+
const targetVersion = getAvailableVersion();
|
|
181
|
+
await triggerWorkflowDispatch('upgrade-event-handler.yml', 'main', {
|
|
182
|
+
target_version: targetVersion || '',
|
|
183
|
+
});
|
|
180
184
|
return { success: true };
|
|
181
185
|
}
|
|
182
186
|
|
|
@@ -99,7 +99,11 @@ function CronCard({ cron }) {
|
|
|
99
99
|
expanded && /* @__PURE__ */ jsxs("div", { className: "border-t px-4 py-3", children: [
|
|
100
100
|
type === "agent" && cron.job && /* @__PURE__ */ jsxs("div", { children: [
|
|
101
101
|
/* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground mb-1.5", children: "Job prompt" }),
|
|
102
|
-
/* @__PURE__ */ jsx("pre", { className: "text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-48", children: cron.job })
|
|
102
|
+
/* @__PURE__ */ jsx("pre", { className: "text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-48", children: cron.job }),
|
|
103
|
+
(cron.llm_provider || cron.llm_model) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mt-2", children: [
|
|
104
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "LLM:" }),
|
|
105
|
+
/* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-full bg-purple-500/10 text-purple-500 px-2 py-0.5 text-[10px] font-medium", children: [cron.llm_provider, cron.llm_model].filter(Boolean).join(" / ") })
|
|
106
|
+
] })
|
|
103
107
|
] }),
|
|
104
108
|
type === "command" && cron.command && /* @__PURE__ */ jsxs("div", { children: [
|
|
105
109
|
/* @__PURE__ */ jsx("p", { className: "text-xs font-medium text-muted-foreground mb-1.5", children: "Command" }),
|
|
@@ -133,6 +133,14 @@ function CronCard({ cron }) {
|
|
|
133
133
|
<pre className="text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-48">
|
|
134
134
|
{cron.job}
|
|
135
135
|
</pre>
|
|
136
|
+
{(cron.llm_provider || cron.llm_model) && (
|
|
137
|
+
<div className="flex items-center gap-2 mt-2">
|
|
138
|
+
<span className="text-xs font-medium text-muted-foreground">LLM:</span>
|
|
139
|
+
<span className="inline-flex items-center rounded-full bg-purple-500/10 text-purple-500 px-2 py-0.5 text-[10px] font-medium">
|
|
140
|
+
{[cron.llm_provider, cron.llm_model].filter(Boolean).join(' / ')}
|
|
141
|
+
</span>
|
|
142
|
+
</div>
|
|
143
|
+
)}
|
|
136
144
|
</div>
|
|
137
145
|
)}
|
|
138
146
|
{type === 'command' && cron.command && (
|
|
@@ -38,7 +38,13 @@ function ActionCard({ action, index }) {
|
|
|
38
38
|
] }),
|
|
39
39
|
/* @__PURE__ */ jsx("span", { className: `inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ${typeBadgeStyles[type] || typeBadgeStyles.agent}`, children: type })
|
|
40
40
|
] }),
|
|
41
|
-
type === "agent" && action.job && /* @__PURE__ */
|
|
41
|
+
type === "agent" && action.job && /* @__PURE__ */ jsxs("div", { children: [
|
|
42
|
+
/* @__PURE__ */ jsx("pre", { className: "text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-48", children: action.job }),
|
|
43
|
+
(action.llm_provider || action.llm_model) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 mt-2", children: [
|
|
44
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs font-medium text-muted-foreground", children: "LLM:" }),
|
|
45
|
+
/* @__PURE__ */ jsx("span", { className: "inline-flex items-center rounded-full bg-purple-500/10 text-purple-500 px-2 py-0.5 text-[10px] font-medium", children: [action.llm_provider, action.llm_model].filter(Boolean).join(" / ") })
|
|
46
|
+
] })
|
|
47
|
+
] }),
|
|
42
48
|
type === "command" && action.command && /* @__PURE__ */ jsx("pre", { className: "text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-48", children: action.command }),
|
|
43
49
|
type === "webhook" && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
|
|
44
50
|
/* @__PURE__ */ jsxs("pre", { className: "text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto", children: [
|
|
@@ -51,9 +51,19 @@ function ActionCard({ action, index }) {
|
|
|
51
51
|
</span>
|
|
52
52
|
</div>
|
|
53
53
|
{type === 'agent' && action.job && (
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
<div>
|
|
55
|
+
<pre className="text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-48">
|
|
56
|
+
{action.job}
|
|
57
|
+
</pre>
|
|
58
|
+
{(action.llm_provider || action.llm_model) && (
|
|
59
|
+
<div className="flex items-center gap-2 mt-2">
|
|
60
|
+
<span className="text-xs font-medium text-muted-foreground">LLM:</span>
|
|
61
|
+
<span className="inline-flex items-center rounded-full bg-purple-500/10 text-purple-500 px-2 py-0.5 text-[10px] font-medium">
|
|
62
|
+
{[action.llm_provider, action.llm_model].filter(Boolean).join(' / ')}
|
|
63
|
+
</span>
|
|
64
|
+
</div>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
57
67
|
)}
|
|
58
68
|
{type === 'command' && action.command && (
|
|
59
69
|
<pre className="text-xs bg-muted rounded-md p-3 whitespace-pre-wrap break-words font-mono overflow-auto max-h-48">
|
package/lib/cron.js
CHANGED
|
@@ -49,31 +49,108 @@ function isVersionNewer(candidate, baseline) {
|
|
|
49
49
|
return false;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Check if a version string is a pre-release (contains '-').
|
|
54
|
+
* @param {string} v
|
|
55
|
+
* @returns {boolean}
|
|
56
|
+
*/
|
|
57
|
+
function isPrerelease(v) {
|
|
58
|
+
return v.includes('-');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Compare two semver strings (including pre-release).
|
|
63
|
+
* Returns positive if a > b, negative if a < b, 0 if equal.
|
|
64
|
+
* Ordering: 1.2.71-beta.0 < 1.2.71-beta.1 < 1.2.71 (stable) < 1.2.72-beta.0
|
|
65
|
+
* @param {string} a
|
|
66
|
+
* @param {string} b
|
|
67
|
+
* @returns {number}
|
|
68
|
+
*/
|
|
69
|
+
function compareVersions(a, b) {
|
|
70
|
+
const [aCore, aPre] = a.split('-');
|
|
71
|
+
const [bCore, bPre] = b.split('-');
|
|
72
|
+
|
|
73
|
+
const aParts = aCore.split('.').map(Number);
|
|
74
|
+
const bParts = bCore.split('.').map(Number);
|
|
75
|
+
|
|
76
|
+
// Compare major.minor.patch
|
|
77
|
+
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
78
|
+
const av = aParts[i] || 0;
|
|
79
|
+
const bv = bParts[i] || 0;
|
|
80
|
+
if (av !== bv) return av - bv;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Same core version: stable beats pre-release
|
|
84
|
+
if (!aPre && bPre) return 1; // a is stable, b is pre-release
|
|
85
|
+
if (aPre && !bPre) return -1; // a is pre-release, b is stable
|
|
86
|
+
if (!aPre && !bPre) return 0; // both stable, same core
|
|
87
|
+
|
|
88
|
+
// Both pre-release with same core: compare pre-release number
|
|
89
|
+
const aNum = parseInt(aPre.split('.').pop(), 10) || 0;
|
|
90
|
+
const bNum = parseInt(bPre.split('.').pop(), 10) || 0;
|
|
91
|
+
return aNum - bNum;
|
|
92
|
+
}
|
|
93
|
+
|
|
52
94
|
/**
|
|
53
95
|
* Check npm registry for a newer version of thepopebot.
|
|
54
96
|
*/
|
|
55
97
|
async function runVersionCheck() {
|
|
56
98
|
try {
|
|
57
|
-
const res = await fetch('https://registry.npmjs.org/thepopebot/latest');
|
|
58
|
-
if (!res.ok) {
|
|
59
|
-
console.warn(`[version check] npm registry returned ${res.status}`);
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
const data = await res.json();
|
|
63
|
-
const latest = data.version;
|
|
64
|
-
|
|
65
99
|
const installed = getInstalledVersion();
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
100
|
+
|
|
101
|
+
if (isPrerelease(installed)) {
|
|
102
|
+
// Beta path: check both stable and beta dist-tags
|
|
103
|
+
const results = await Promise.allSettled([
|
|
104
|
+
fetch('https://registry.npmjs.org/thepopebot/latest'),
|
|
105
|
+
fetch('https://registry.npmjs.org/thepopebot/beta'),
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
const candidates = [];
|
|
109
|
+
for (const result of results) {
|
|
110
|
+
if (result.status !== 'fulfilled') continue;
|
|
111
|
+
const res = result.value;
|
|
112
|
+
if (!res.ok) continue;
|
|
113
|
+
const data = await res.json();
|
|
114
|
+
if (data.version && compareVersions(data.version, installed) > 0) {
|
|
115
|
+
candidates.push(data.version);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (candidates.length > 0) {
|
|
120
|
+
// Pick the best candidate (highest version)
|
|
121
|
+
candidates.sort(compareVersions);
|
|
122
|
+
const best = candidates[candidates.length - 1];
|
|
123
|
+
console.log(`[version check] update available: ${installed} → ${best}`);
|
|
124
|
+
setUpdateAvailable(best);
|
|
125
|
+
const { setAvailableVersion } = await import('./db/update-check.js');
|
|
126
|
+
setAvailableVersion(best);
|
|
127
|
+
} else {
|
|
128
|
+
setUpdateAvailable(null);
|
|
129
|
+
const { clearAvailableVersion } = await import('./db/update-check.js');
|
|
130
|
+
clearAvailableVersion();
|
|
131
|
+
}
|
|
72
132
|
} else {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
133
|
+
// Stable path: existing logic, untouched
|
|
134
|
+
const res = await fetch('https://registry.npmjs.org/thepopebot/latest');
|
|
135
|
+
if (!res.ok) {
|
|
136
|
+
console.warn(`[version check] npm registry returned ${res.status}`);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const data = await res.json();
|
|
140
|
+
const latest = data.version;
|
|
141
|
+
|
|
142
|
+
if (isVersionNewer(latest, installed)) {
|
|
143
|
+
console.log(`[version check] update available: ${installed} → ${latest}`);
|
|
144
|
+
setUpdateAvailable(latest);
|
|
145
|
+
// Persist to DB
|
|
146
|
+
const { setAvailableVersion } = await import('./db/update-check.js');
|
|
147
|
+
setAvailableVersion(latest);
|
|
148
|
+
} else {
|
|
149
|
+
setUpdateAvailable(null);
|
|
150
|
+
// Clear DB
|
|
151
|
+
const { clearAvailableVersion } = await import('./db/update-check.js');
|
|
152
|
+
clearAvailableVersion();
|
|
153
|
+
}
|
|
77
154
|
}
|
|
78
155
|
} catch (err) {
|
|
79
156
|
console.warn(`[version check] failed: ${err.message}`);
|
|
@@ -144,4 +221,4 @@ function loadCrons() {
|
|
|
144
221
|
return tasks;
|
|
145
222
|
}
|
|
146
223
|
|
|
147
|
-
export { loadCrons, startBuiltinCrons, getUpdateAvailable, setUpdateAvailable, getInstalledVersion };
|
|
224
|
+
export { loadCrons, startBuiltinCrons, getUpdateAvailable, setUpdateAvailable, getInstalledVersion, isPrerelease };
|
package/lib/tools/create-job.js
CHANGED
|
@@ -4,9 +4,12 @@ import { githubApi } from './github.js';
|
|
|
4
4
|
/**
|
|
5
5
|
* Create a new job branch with updated job.md
|
|
6
6
|
* @param {string} jobDescription - The job description to write to job.md
|
|
7
|
+
* @param {Object} [options] - Optional overrides
|
|
8
|
+
* @param {string} [options.llmProvider] - LLM provider override (e.g. 'openai', 'anthropic')
|
|
9
|
+
* @param {string} [options.llmModel] - LLM model override (e.g. 'gpt-4o', 'claude-sonnet-4-5-20250929')
|
|
7
10
|
* @returns {Promise<{job_id: string, branch: string}>} - Job ID and branch name
|
|
8
11
|
*/
|
|
9
|
-
async function createJob(jobDescription) {
|
|
12
|
+
async function createJob(jobDescription, options = {}) {
|
|
10
13
|
const { GH_OWNER, GH_REPO } = process.env;
|
|
11
14
|
const jobId = uuidv4();
|
|
12
15
|
const branch = `job/${jobId}`;
|
|
@@ -34,6 +37,21 @@ async function createJob(jobDescription) {
|
|
|
34
37
|
}),
|
|
35
38
|
});
|
|
36
39
|
|
|
40
|
+
// Write job.config.json if LLM overrides specified
|
|
41
|
+
const config = {};
|
|
42
|
+
if (options.llmProvider) config.llm_provider = options.llmProvider;
|
|
43
|
+
if (options.llmModel) config.llm_model = options.llmModel;
|
|
44
|
+
if (Object.keys(config).length > 0) {
|
|
45
|
+
await githubApi(`/repos/${GH_OWNER}/${GH_REPO}/contents/logs/${jobId}/job.config.json`, {
|
|
46
|
+
method: 'PUT',
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
message: `job config: ${jobId}`,
|
|
49
|
+
content: Buffer.from(JSON.stringify(config, null, 2)).toString('base64'),
|
|
50
|
+
branch: branch,
|
|
51
|
+
}),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
37
55
|
return { job_id: jobId, branch };
|
|
38
56
|
}
|
|
39
57
|
|
package/lib/tools/github.js
CHANGED
|
@@ -178,6 +178,33 @@ async function triggerWorkflowDispatch(workflowId, ref = 'main', inputs = {}) {
|
|
|
178
178
|
return { success: true };
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Fetch the session log (.jsonl) for a job from the GitHub repo at a specific commit.
|
|
183
|
+
* @param {string} jobId - The job ID (used to locate logs/{jobId}/)
|
|
184
|
+
* @param {string} commitSha - Git commit SHA to read from
|
|
185
|
+
* @returns {Promise<string>} - Log content or empty string if unavailable
|
|
186
|
+
*/
|
|
187
|
+
async function fetchJobLog(jobId, commitSha) {
|
|
188
|
+
if (!commitSha) return '';
|
|
189
|
+
const { GH_OWNER, GH_REPO } = process.env;
|
|
190
|
+
try {
|
|
191
|
+
const files = await githubApi(
|
|
192
|
+
`/repos/${GH_OWNER}/${GH_REPO}/contents/logs/${jobId}?ref=${encodeURIComponent(commitSha)}`
|
|
193
|
+
);
|
|
194
|
+
if (!Array.isArray(files)) return '';
|
|
195
|
+
const logFile = files.find(f => f.name.endsWith('.jsonl'));
|
|
196
|
+
if (!logFile || !logFile.download_url) return '';
|
|
197
|
+
const res = await fetch(logFile.download_url, {
|
|
198
|
+
headers: { 'Authorization': `Bearer ${process.env.GH_TOKEN}` },
|
|
199
|
+
});
|
|
200
|
+
if (!res.ok) return '';
|
|
201
|
+
return await res.text();
|
|
202
|
+
} catch (err) {
|
|
203
|
+
console.error('Failed to fetch job log:', err.message);
|
|
204
|
+
return '';
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
181
208
|
export {
|
|
182
209
|
githubApi,
|
|
183
210
|
getWorkflowRuns,
|
|
@@ -185,4 +212,5 @@ export {
|
|
|
185
212
|
getJobStatus,
|
|
186
213
|
getSwarmStatus,
|
|
187
214
|
triggerWorkflowDispatch,
|
|
215
|
+
fetchJobLog,
|
|
188
216
|
};
|
package/package.json
CHANGED
package/setup/lib/github.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execSync, exec } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import { randomBytes } from 'crypto';
|
|
4
|
+
import { ghEnv } from './prerequisites.mjs';
|
|
4
5
|
|
|
5
6
|
const execAsync = promisify(exec);
|
|
6
7
|
|
|
@@ -69,7 +70,7 @@ export async function setSecret(owner, repo, name, value) {
|
|
|
69
70
|
// Use stdin to pass the secret value securely
|
|
70
71
|
const { stdout, stderr } = await execAsync(
|
|
71
72
|
`echo "${value.replace(/"/g, '\\"')}" | gh secret set ${name} --repo ${owner}/${repo}`,
|
|
72
|
-
{ encoding: 'utf-8' }
|
|
73
|
+
{ encoding: 'utf-8', env: ghEnv() }
|
|
73
74
|
);
|
|
74
75
|
return { success: true };
|
|
75
76
|
} catch (error) {
|
|
@@ -95,6 +96,7 @@ export async function listSecrets(owner, repo) {
|
|
|
95
96
|
try {
|
|
96
97
|
const { stdout } = await execAsync(`gh secret list --repo ${owner}/${repo}`, {
|
|
97
98
|
encoding: 'utf-8',
|
|
99
|
+
env: ghEnv(),
|
|
98
100
|
});
|
|
99
101
|
const secrets = stdout
|
|
100
102
|
.trim()
|
|
@@ -114,7 +116,7 @@ export async function setVariable(owner, repo, name, value) {
|
|
|
114
116
|
try {
|
|
115
117
|
const { stdout, stderr } = await execAsync(
|
|
116
118
|
`echo "${value.replace(/"/g, '\\"')}" | gh variable set ${name} --repo ${owner}/${repo}`,
|
|
117
|
-
{ encoding: 'utf-8' }
|
|
119
|
+
{ encoding: 'utf-8', env: ghEnv() }
|
|
118
120
|
);
|
|
119
121
|
return { success: true };
|
|
120
122
|
} catch (error) {
|
|
@@ -3,6 +3,18 @@ import { promisify } from 'util';
|
|
|
3
3
|
|
|
4
4
|
const execAsync = promisify(exec);
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Return process.env without GITHUB_TOKEN and GH_TOKEN.
|
|
8
|
+
* The gh CLI auto-uses these env vars, which can shadow interactive login
|
|
9
|
+
* and cause secret/variable operations to fail with the wrong identity.
|
|
10
|
+
*/
|
|
11
|
+
export function ghEnv() {
|
|
12
|
+
const env = { ...process.env };
|
|
13
|
+
delete env.GITHUB_TOKEN;
|
|
14
|
+
delete env.GH_TOKEN;
|
|
15
|
+
return env;
|
|
16
|
+
}
|
|
17
|
+
|
|
6
18
|
/**
|
|
7
19
|
* Check if a command exists
|
|
8
20
|
*/
|
|
@@ -11,6 +23,14 @@ function commandExists(cmd) {
|
|
|
11
23
|
execSync(`which ${cmd}`, { stdio: 'ignore' });
|
|
12
24
|
return true;
|
|
13
25
|
} catch {
|
|
26
|
+
if (process.platform === 'win32') {
|
|
27
|
+
try {
|
|
28
|
+
execSync(`where ${cmd}`, { stdio: 'ignore' });
|
|
29
|
+
return true;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
14
34
|
return false;
|
|
15
35
|
}
|
|
16
36
|
}
|
|
@@ -32,7 +52,7 @@ function getNodeVersion() {
|
|
|
32
52
|
*/
|
|
33
53
|
async function isGhAuthenticated() {
|
|
34
54
|
try {
|
|
35
|
-
await execAsync('gh auth status');
|
|
55
|
+
await execAsync('gh auth status', { env: ghEnv() });
|
|
36
56
|
return true;
|
|
37
57
|
} catch {
|
|
38
58
|
return false;
|
|
@@ -136,7 +156,7 @@ export async function installGlobalPackage(packageName) {
|
|
|
136
156
|
*/
|
|
137
157
|
export async function runGhAuth() {
|
|
138
158
|
// This needs to be interactive, so we use execSync
|
|
139
|
-
execSync('gh auth login', { stdio: 'inherit' });
|
|
159
|
+
execSync('gh auth login', { stdio: 'inherit', env: ghEnv() });
|
|
140
160
|
}
|
|
141
161
|
|
|
142
162
|
export { commandExists, getGitRemoteInfo, getPackageManager };
|
package/setup/lib/prompts.mjs
CHANGED
|
@@ -189,18 +189,18 @@ export async function promptForCustomProvider() {
|
|
|
189
189
|
*/
|
|
190
190
|
export async function promptForBraveKey() {
|
|
191
191
|
const addKey = handleCancel(await clack.confirm({
|
|
192
|
-
message: 'Add Brave Search API key? (
|
|
192
|
+
message: 'Add Brave Search API key? (optional, greatly improves agent)',
|
|
193
193
|
initialValue: true,
|
|
194
194
|
}));
|
|
195
195
|
|
|
196
196
|
if (!addKey) return null;
|
|
197
197
|
|
|
198
198
|
clack.log.info(
|
|
199
|
-
'To get a
|
|
199
|
+
'To get a Brave Search API key:\n' +
|
|
200
200
|
' 1. Go to https://api-dashboard.search.brave.com/app/keys\n' +
|
|
201
201
|
' 2. Click "Get Started"\n' +
|
|
202
202
|
' 3. Create an account (or sign in)\n' +
|
|
203
|
-
' 4. Subscribe to
|
|
203
|
+
' 4. Subscribe to a plan (free credits may be available)\n' +
|
|
204
204
|
' 5. Copy your API key'
|
|
205
205
|
);
|
|
206
206
|
|
package/setup/setup.mjs
CHANGED
|
@@ -249,7 +249,11 @@ async function main() {
|
|
|
249
249
|
clack.log.success('ngrok installed');
|
|
250
250
|
} else {
|
|
251
251
|
clack.log.warn('ngrok not installed (needed to expose local server)');
|
|
252
|
-
clack.log.info(
|
|
252
|
+
clack.log.info(
|
|
253
|
+
'Install with: brew install ngrok/ngrok/ngrok\n' +
|
|
254
|
+
' Sign up for a free account at https://dashboard.ngrok.com/signup\n' +
|
|
255
|
+
' Then run: ngrok config add-authtoken <YOUR_TOKEN>'
|
|
256
|
+
);
|
|
253
257
|
}
|
|
254
258
|
|
|
255
259
|
// ─── Step 2: GitHub PAT ──────────────────────────────────────────────
|
|
@@ -559,25 +563,41 @@ async function main() {
|
|
|
559
563
|
// Server not reachable
|
|
560
564
|
}
|
|
561
565
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
clack.log.info('Rebuilding Next.js...');
|
|
566
|
+
// Helper: run build with retry on failure
|
|
567
|
+
async function runBuildWithRetry() {
|
|
568
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
566
569
|
try {
|
|
567
570
|
execSync('npm run build', { stdio: 'inherit' });
|
|
568
571
|
clack.log.success('Build complete');
|
|
572
|
+
return true;
|
|
569
573
|
} catch {
|
|
570
|
-
|
|
574
|
+
if (attempt === 1) {
|
|
575
|
+
clack.log.error('Build failed.');
|
|
576
|
+
const retry = await confirm('Retry build?');
|
|
577
|
+
if (!retry) break;
|
|
578
|
+
} else {
|
|
579
|
+
clack.log.error('Build failed again.');
|
|
580
|
+
}
|
|
571
581
|
}
|
|
572
582
|
}
|
|
583
|
+
clack.log.error(
|
|
584
|
+
'Cannot continue without a successful build.\n' +
|
|
585
|
+
' Fix the error above, then run:\n\n' +
|
|
586
|
+
' npm run build\n' +
|
|
587
|
+
' docker compose up -d'
|
|
588
|
+
);
|
|
589
|
+
process.exit(1);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (serverAlreadyRunning) {
|
|
593
|
+
clack.log.success('Server is already running');
|
|
594
|
+
if (await confirm('Rebuild and restart anyway?', false)) {
|
|
595
|
+
clack.log.info('Rebuilding Next.js...');
|
|
596
|
+
await runBuildWithRetry();
|
|
597
|
+
}
|
|
573
598
|
} else {
|
|
574
599
|
clack.log.info('Building Next.js...');
|
|
575
|
-
|
|
576
|
-
execSync('npm run build', { stdio: 'inherit' });
|
|
577
|
-
clack.log.success('Build complete');
|
|
578
|
-
} catch {
|
|
579
|
-
clack.log.error('Build failed — run npm run build manually');
|
|
580
|
-
}
|
|
600
|
+
await runBuildWithRetry();
|
|
581
601
|
|
|
582
602
|
clack.log.info('Start Docker in a new terminal window:\n\n docker compose up -d');
|
|
583
603
|
|
|
@@ -114,4 +114,4 @@ jobs:
|
|
|
114
114
|
env:
|
|
115
115
|
GH_TOKEN: ${{ github.token }}
|
|
116
116
|
run: |
|
|
117
|
-
gh pr merge ${{ github.event.pull_request.number }} --squash --repo "${{ github.repository }}"
|
|
117
|
+
gh pr merge ${{ github.event.pull_request.number }} --squash --delete-branch --repo "${{ github.repository }}"
|
|
@@ -35,9 +35,7 @@ jobs:
|
|
|
35
35
|
JOB_CONTENT=$(cat "logs/${JOB_ID}/job.md")
|
|
36
36
|
fi
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
RUN_LOG=$(gh run view "$RUN_ID" --repo "${{ github.repository }}" --log 2>/dev/null || echo "")
|
|
40
|
-
|
|
38
|
+
COMMIT_SHA=$(git rev-parse HEAD)
|
|
41
39
|
STATUS="${{ github.event.workflow_run.conclusion }}"
|
|
42
40
|
|
|
43
41
|
jq -n \
|
|
@@ -46,7 +44,7 @@ jobs:
|
|
|
46
44
|
--arg status "$STATUS" \
|
|
47
45
|
--arg job "$JOB_CONTENT" \
|
|
48
46
|
--arg run_url "$RUN_URL" \
|
|
49
|
-
--arg
|
|
47
|
+
--arg commit_sha "$COMMIT_SHA" \
|
|
50
48
|
'{
|
|
51
49
|
job_id: $job_id,
|
|
52
50
|
branch: $branch,
|
|
@@ -56,7 +54,7 @@ jobs:
|
|
|
56
54
|
pr_url: "",
|
|
57
55
|
changed_files: [],
|
|
58
56
|
commit_message: "",
|
|
59
|
-
|
|
57
|
+
commit_sha: $commit_sha,
|
|
60
58
|
merge_result: ""
|
|
61
59
|
}' > /tmp/payload.json
|
|
62
60
|
|
|
@@ -77,15 +77,8 @@ jobs:
|
|
|
77
77
|
# 4. Get PR status
|
|
78
78
|
PR_STATUS=$(gh pr view "$PR_NUMBER" --json state --jq '.state' --repo "${{ github.repository }}" 2>/dev/null || echo "open")
|
|
79
79
|
|
|
80
|
-
# 5.
|
|
81
|
-
|
|
82
|
-
LOG_DIR="logs/${JOB_ID}"
|
|
83
|
-
if [ -d "$LOG_DIR" ]; then
|
|
84
|
-
LOG_FILE=$(find "$LOG_DIR" -name "*.jsonl" -type f | head -1)
|
|
85
|
-
if [ -n "$LOG_FILE" ]; then
|
|
86
|
-
LOG_CONTENT=$(cat "$LOG_FILE")
|
|
87
|
-
fi
|
|
88
|
-
fi
|
|
80
|
+
# 5. Commit SHA for server-side log fetching
|
|
81
|
+
COMMIT_SHA="${{ github.event.workflow_run.head_sha }}"
|
|
89
82
|
|
|
90
83
|
# Determine status based on merge result and PR state
|
|
91
84
|
if [ "$MERGE_RESULT" = "merged" ]; then
|
|
@@ -104,7 +97,7 @@ jobs:
|
|
|
104
97
|
--arg pr_url "${{ steps.pr.outputs.url }}" \
|
|
105
98
|
--arg commit_message "$COMMIT_MSG" \
|
|
106
99
|
--arg changed_files "$CHANGED_FILES" \
|
|
107
|
-
--arg
|
|
100
|
+
--arg commit_sha "$COMMIT_SHA" \
|
|
108
101
|
--arg merge_result "$MERGE_RESULT" \
|
|
109
102
|
'{
|
|
110
103
|
job_id: $job_id,
|
|
@@ -115,7 +108,7 @@ jobs:
|
|
|
115
108
|
pr_url: $pr_url,
|
|
116
109
|
changed_files: ($changed_files | split("\n") | map(select(length > 0))),
|
|
117
110
|
commit_message: $commit_message,
|
|
118
|
-
|
|
111
|
+
commit_sha: $commit_sha,
|
|
119
112
|
merge_result: $merge_result
|
|
120
113
|
}' > /tmp/payload.json
|
|
121
114
|
|
|
@@ -40,7 +40,11 @@ jobs:
|
|
|
40
40
|
|
|
41
41
|
if [ -n "$OLD_TPB" ] && [ "$OLD_TPB" != "$NEW_TPB" ]; then
|
|
42
42
|
# Version changed — run thepopebot init to scaffold new templates
|
|
43
|
-
|
|
43
|
+
if echo "$NEW_TPB" | grep -q '-'; then
|
|
44
|
+
npx --yes "thepopebot@${NEW_TPB}" init
|
|
45
|
+
else
|
|
46
|
+
npx --yes thepopebot@latest init
|
|
47
|
+
fi
|
|
44
48
|
|
|
45
49
|
# Commit any template changes from init
|
|
46
50
|
git add -A
|
|
@@ -16,7 +16,9 @@ jobs:
|
|
|
16
16
|
uses: actions/checkout@v4
|
|
17
17
|
with:
|
|
18
18
|
ref: ${{ github.ref_name }}
|
|
19
|
-
sparse-checkout:
|
|
19
|
+
sparse-checkout: |
|
|
20
|
+
package-lock.json
|
|
21
|
+
logs/*/job.config.json
|
|
20
22
|
sparse-checkout-cone-mode: false
|
|
21
23
|
|
|
22
24
|
- name: Get thepopebot version
|
|
@@ -25,6 +27,17 @@ jobs:
|
|
|
25
27
|
VERSION=$(jq -r '.packages["node_modules/thepopebot"].version // "latest"' package-lock.json)
|
|
26
28
|
echo "tag=$VERSION" >> $GITHUB_OUTPUT
|
|
27
29
|
|
|
30
|
+
- name: Read job config overrides
|
|
31
|
+
id: job-config
|
|
32
|
+
run: |
|
|
33
|
+
JOB_ID="${{ github.ref_name }}"
|
|
34
|
+
JOB_ID="${JOB_ID#job/}"
|
|
35
|
+
CONFIG_FILE="logs/${JOB_ID}/job.config.json"
|
|
36
|
+
if [ -f "$CONFIG_FILE" ]; then
|
|
37
|
+
echo "llm_provider=$(jq -r '.llm_provider // empty' "$CONFIG_FILE")" >> $GITHUB_OUTPUT
|
|
38
|
+
echo "llm_model=$(jq -r '.llm_model // empty' "$CONFIG_FILE")" >> $GITHUB_OUTPUT
|
|
39
|
+
fi
|
|
40
|
+
|
|
28
41
|
- name: Login to GHCR
|
|
29
42
|
if: startsWith(vars.JOB_IMAGE_URL, 'ghcr.io/')
|
|
30
43
|
uses: docker/login-action@v3
|
|
@@ -38,8 +51,8 @@ jobs:
|
|
|
38
51
|
ALL_SECRETS: ${{ toJson(secrets) }}
|
|
39
52
|
JOB_IMAGE_URL: ${{ vars.JOB_IMAGE_URL }}
|
|
40
53
|
THEPOPEBOT_VERSION: ${{ steps.version.outputs.tag }}
|
|
41
|
-
LLM_MODEL: ${{ vars.LLM_MODEL }}
|
|
42
|
-
LLM_PROVIDER: ${{ vars.LLM_PROVIDER }}
|
|
54
|
+
LLM_MODEL: ${{ steps.job-config.outputs.llm_model || vars.LLM_MODEL }}
|
|
55
|
+
LLM_PROVIDER: ${{ steps.job-config.outputs.llm_provider || vars.LLM_PROVIDER }}
|
|
43
56
|
OPENAI_BASE_URL: ${{ vars.OPENAI_BASE_URL }}
|
|
44
57
|
run: |
|
|
45
58
|
if [ -n "$JOB_IMAGE_URL" ]; then
|
|
@@ -2,6 +2,11 @@ name: Upgrade Event Handler
|
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
4
|
workflow_dispatch:
|
|
5
|
+
inputs:
|
|
6
|
+
target_version:
|
|
7
|
+
description: 'Target version to install (empty for latest stable)'
|
|
8
|
+
required: false
|
|
9
|
+
default: ''
|
|
5
10
|
|
|
6
11
|
concurrency:
|
|
7
12
|
group: event-handler-deploy
|
|
@@ -29,7 +34,16 @@ jobs:
|
|
|
29
34
|
|
|
30
35
|
npm install --omit=dev
|
|
31
36
|
OLD_VERSION=$(node -p "require(\"./node_modules/thepopebot/package.json\").version")
|
|
32
|
-
|
|
37
|
+
|
|
38
|
+
TARGET="${{ github.event.inputs.target_version }}"
|
|
39
|
+
if [ -n "$TARGET" ] && echo "$TARGET" | grep -q '-'; then
|
|
40
|
+
# Beta: npm update won't upgrade an exact-pinned dependency, must use install
|
|
41
|
+
npm install "thepopebot@${TARGET}" --prefer-online
|
|
42
|
+
else
|
|
43
|
+
# Stable: npm update resolves to latest within semver range
|
|
44
|
+
npm update thepopebot --prefer-online
|
|
45
|
+
fi
|
|
46
|
+
|
|
33
47
|
NEW_VERSION=$(node -p "require(\"./node_modules/thepopebot/package.json\").version")
|
|
34
48
|
|
|
35
49
|
if [ "$OLD_VERSION" != "$NEW_VERSION" ]; then
|
|
@@ -43,5 +43,14 @@
|
|
|
43
43
|
"url": "https://example.com/health",
|
|
44
44
|
"method": "GET",
|
|
45
45
|
"enabled": false
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "daily-check-openai",
|
|
49
|
+
"schedule": "0 9 * * *",
|
|
50
|
+
"type": "agent",
|
|
51
|
+
"job": "Check for dependency updates in package.json and report any outdated packages.",
|
|
52
|
+
"llm_provider": "openai",
|
|
53
|
+
"llm_model": "gpt-4o",
|
|
54
|
+
"enabled": false
|
|
46
55
|
}
|
|
47
56
|
]
|
|
@@ -46,5 +46,13 @@
|
|
|
46
46
|
{ "type": "webhook", "url": "https://example.com/hook", "method": "POST", "vars": { "source": "webhook" } }
|
|
47
47
|
],
|
|
48
48
|
"enabled": false
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
"name": "review-with-openai",
|
|
52
|
+
"watch_path": "/github/webhook",
|
|
53
|
+
"actions": [
|
|
54
|
+
{ "type": "agent", "job": "Review the GitHub event and summarize what happened:\n{{body}}", "llm_provider": "openai", "llm_model": "gpt-4o" }
|
|
55
|
+
],
|
|
56
|
+
"enabled": false
|
|
49
57
|
}
|
|
50
58
|
]
|