thepopebot 1.2.76-beta.19 → 1.2.76-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/CLAUDE.md +11 -4
- package/bin/CLAUDE.md +7 -3
- package/config/CLAUDE.md +23 -4
- package/lib/channels/CLAUDE.md +12 -2
- package/lib/chat/CLAUDE.md +4 -4
- package/lib/chat/components/containers-page.js +58 -40
- package/lib/chat/components/containers-page.jsx +64 -25
- package/lib/cluster/CLAUDE.md +9 -3
- package/lib/code/CLAUDE.md +11 -3
- package/lib/containers/CLAUDE.md +16 -6
- package/lib/db/CLAUDE.md +5 -2
- package/lib/tools/CLAUDE.md +10 -1
- package/package.json +1 -1
- package/setup/CLAUDE.md +11 -5
- package/templates/CLAUDE.md.template +3 -5
- package/templates/agents/CLAUDE.md.template +8 -3
- package/templates/data/CLAUDE.md.template +1 -1
- package/templates/event-handler/CLAUDE.md.template +79 -0
- package/templates/skills/CLAUDE.md.template +8 -3
package/api/CLAUDE.md
CHANGED
|
@@ -4,9 +4,14 @@ This directory contains the route handlers for all `/api/*` endpoints. These rou
|
|
|
4
4
|
|
|
5
5
|
## Auth
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Most routes require a valid API key passed via the `x-api-key` header. API keys are stored in the SQLite database and managed through the admin UI — they are NOT environment variables.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
**Public routes** (no API key needed): `/ping`, `/telegram/webhook` (Telegram webhook secret), `/github/webhook` (GitHub webhook secret), `/oauth/callback` (validated via short-lived `state` token).
|
|
10
|
+
|
|
11
|
+
Auth flow: `x-api-key` header → `verifyApiKey()` → DB lookup (hashed, timing-safe comparison). Two key types exist:
|
|
12
|
+
|
|
13
|
+
- **User-owned API keys** — long-lived, created via the admin UI, used by external callers (cURL, GitHub Actions, Telegram register).
|
|
14
|
+
- **Per-job agent API keys** (`agent_job_api_key`) — short-lived, auto-created when an agent-job container launches (`createAgentJobApiKey()` in `lib/db/api-keys.js`), tied to the container name, and cleaned up by the maintenance cron after expiry. Only this key type is allowed to call `/api/get-agent-job-secret` (the route rejects other types).
|
|
10
15
|
|
|
11
16
|
## Do NOT use these routes for browser UI
|
|
12
17
|
|
|
@@ -25,10 +30,12 @@ Browser-facing data fetching uses **fetch route handlers** colocated with pages
|
|
|
25
30
|
|--------|------|------|---------|
|
|
26
31
|
| GET | `/api/ping` | None | Health check |
|
|
27
32
|
| POST | `/api/create-agent-job` | `x-api-key` | Create agent job |
|
|
28
|
-
| GET | `/api/get-agent-job-secret` | `
|
|
33
|
+
| GET | `/api/get-agent-job-secret` | `agent_job_api_key` only | Get an agent job secret. `oauth2` credentials return only the access_token (auto-refreshed under a lock; rotated refresh tokens are persisted back). Other secret types return the raw value. |
|
|
34
|
+
| POST | `/api/set-agent-job-secret` | `agent_job_api_key` only | Create or update an agent-job secret from inside the container (used by the `set-secret` skill). |
|
|
29
35
|
| GET | `/api/agent-job-list-secrets` | `x-api-key` | List agent job secret keys (no values); returns `{secrets: [{key, isSet, updatedAt, secretType}]}` |
|
|
30
36
|
| GET | `/api/agent-jobs/status` | `x-api-key` | Agent job status (query: `?agent_job_id=`) |
|
|
31
|
-
| POST | `/api/telegram/webhook` | Telegram webhook secret | Telegram message handler |
|
|
37
|
+
| POST | `/api/telegram/webhook` | Telegram webhook secret | Telegram message handler (per-user routing via `user_channels`; verifies via `/verify <code>`, dispatches `/session` commands) |
|
|
32
38
|
| POST | `/api/telegram/register` | `x-api-key` | Register bot token + webhook URL |
|
|
33
39
|
| POST | `/api/github/webhook` | GitHub webhook secret | GitHub event handler |
|
|
34
40
|
| POST | `/api/cluster/:clusterId/role/:roleId/webhook` | `x-api-key` | Trigger cluster role execution |
|
|
41
|
+
| GET/POST | `/api/oauth/callback` | `state` token | OAuth provider redirect target. Exchanges `code` for tokens, persists via `setAgentJobSecret(name, stored, 'oauth')`. |
|
package/bin/CLAUDE.md
CHANGED
|
@@ -9,19 +9,23 @@ Entry point: `cli.js` (invoked via `npx thepopebot <command>`).
|
|
|
9
9
|
| `init [--no-managed] [--no-install]` | Scaffold project from templates, sync managed files, create `.env`, install deps |
|
|
10
10
|
| `setup` | Run interactive setup wizard (see `setup/CLAUDE.md`) |
|
|
11
11
|
| `setup-telegram` | Reconfigure Telegram webhook |
|
|
12
|
+
| `setup-ssl` | Configure SSL with Let's Encrypt wildcard cert |
|
|
12
13
|
| `upgrade [@beta\|version]` | Upgrade package, run init, rebuild, commit, push, restart Docker |
|
|
13
14
|
| `reset [file]` | Restore a template file to defaults |
|
|
15
|
+
| `reset-all` | Nuclear reset — restore entire project to fresh init state |
|
|
16
|
+
| `audit` | Show project state vs. package templates (modified / missing / unknown) |
|
|
14
17
|
| `diff [file]` | Show diff between user file and package template |
|
|
15
18
|
| `reset-auth` | Regenerate `AUTH_SECRET` (invalidates all sessions) |
|
|
16
|
-
| `set-var <KEY> [VALUE]` | Set GitHub repository variable |
|
|
19
|
+
| `set-var <KEY> [VALUE]` | Set GitHub repository variable (also reads piped stdin) |
|
|
17
20
|
| `user:password <email>` | Change user password |
|
|
18
|
-
| `sync <path>` | Dev helper —
|
|
21
|
+
| `sync <path>` | Dev helper — pack, build, upload local package to test install |
|
|
22
|
+
| `sync --fast <path>` | Fast variant — copy source into the running container and rebuild `.next` only |
|
|
19
23
|
|
|
20
24
|
## Managed Paths System
|
|
21
25
|
|
|
22
26
|
`managed-paths.js` defines files auto-synced by `init`. These are overwritten on every init/upgrade — users should not edit them.
|
|
23
27
|
|
|
24
|
-
**Managed paths**: `.github/workflows/`, `docker-compose.yml`, `.dockerignore`, `.gitignore
|
|
28
|
+
**Managed paths**: `.github/workflows/`, `docker-compose.yml`, `.dockerignore`, `.gitignore`.
|
|
25
29
|
|
|
26
30
|
`isManaged(relPath)` — returns true if a path is managed (exact match or directory prefix).
|
|
27
31
|
|
package/config/CLAUDE.md
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
|
-
# config/ — Next.js Config
|
|
1
|
+
# config/ — Next.js Config + Server Bootstrap
|
|
2
2
|
|
|
3
3
|
## Next.js Config Wrapper (index.js)
|
|
4
4
|
|
|
5
|
-
`withThepopebot()` wraps user's `next.config.mjs`. Adds `transpilePackages` and `serverExternalPackages` for
|
|
5
|
+
`withThepopebot()` wraps the user's `next.config.mjs`. Adds `transpilePackages` and `serverExternalPackages` for npm-package dependencies that need special bundling (Drizzle, better-sqlite3, etc.).
|
|
6
6
|
|
|
7
|
-
## Instrumentation (instrumentation.js)
|
|
7
|
+
## Instrumentation Hook (instrumentation.js)
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Loaded by Next.js once on server start. The user's project re-exports it from their own `instrumentation.js`:
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
export { register } from 'thepopebot/instrumentation';
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Boot sequence
|
|
16
|
+
|
|
17
|
+
1. **Skip during `next build`** — checks `process.argv` for `'build'` to avoid keeping the event loop alive during build output.
|
|
18
|
+
2. **Load `.env`** — `dotenv.config()` from project root.
|
|
19
|
+
3. **Default `AUTH_URL` from `APP_URL`** — so NextAuth redirects to the correct host on sign-out.
|
|
20
|
+
4. **Validate `AUTH_SECRET`** — throws if unset (required for session encryption).
|
|
21
|
+
5. **`initDatabase()`** — `lib/db/index.js`. Opens SQLite, runs Drizzle migrations.
|
|
22
|
+
6. **`migrateEnvToDb()`** — `lib/db/config.js`. Idempotent first-run migration of `.env` values into the settings table.
|
|
23
|
+
7. **`loadCrons()`** — `lib/cron.js`. Reads `agent-job/CRONS.json`, schedules user-defined crons.
|
|
24
|
+
8. **`startBuiltinCrons()`** — `lib/cron.js`. Starts internal crons (e.g., npm version check). Then warms the in-memory update flag from `lib/db/update-check.js`.
|
|
25
|
+
9. **`startClusterRuntime()`** — `lib/cluster/runtime.js`. Registers cluster role triggers (cron + file watch + webhook).
|
|
26
|
+
10. **`startMaintenanceCron()`** — `lib/maintenance.js`. Hourly cleanup of expired agent-job API keys and other housekeeping.
|
|
27
|
+
|
|
28
|
+
`initialized` is module-scoped so the sequence runs exactly once even if `register()` is called more than once.
|
package/lib/channels/CLAUDE.md
CHANGED
|
@@ -27,6 +27,16 @@ Abstract interface for platform integrations. Methods:
|
|
|
27
27
|
## Telegram Adapter (telegram.js)
|
|
28
28
|
|
|
29
29
|
- **Authorization**: per-user via the `user_channels` table. Unverified chats only accept `/verify <code>`; all other messages are dropped. See `lib/db/user-channels.js` and `lib/channels/commands/verify.js`.
|
|
30
|
-
- **Session commands**: post-auth messages may be slash commands (`/session`, `/session list`, `/session <id>`) dispatched from `lib/channels/commands/`. Resolution chat.id → userId → activeThreadId lives in `api/index.js` `processChannelMessage`.
|
|
31
30
|
- **Webhook auth**: Validates `x-telegram-bot-api-secret-token` header against `TELEGRAM_WEBHOOK_SECRET`.
|
|
32
|
-
- **Streaming**: `supportsStreaming` returns `false` —
|
|
31
|
+
- **Streaming**: `supportsStreaming` returns `false` — text + tool calls accumulate during streaming and are sent as complete messages once the turn ends. Progressive tool-call rendering (commit 740c734 / d9bf19a) inserts intermediate "→ used X" lines as tool calls land.
|
|
32
|
+
|
|
33
|
+
## Slash Commands (`lib/channels/commands/`)
|
|
34
|
+
|
|
35
|
+
Post-auth messages starting with `/` are dispatched here before reaching the LLM. Resolution chat.id → userId → activeThreadId happens in `api/index.js` `processChannelMessage`.
|
|
36
|
+
|
|
37
|
+
| Command | Purpose | Source |
|
|
38
|
+
|---------|---------|--------|
|
|
39
|
+
| `/verify <code>` | Verify a Telegram account against a one-time code generated in the web UI (`/profile/telegram`). Code expires in 10 minutes. Sets `verifiedAt`. | `commands/verify.js` |
|
|
40
|
+
| `/session` | List the user's recent chat threads (active thread marked) | `commands/session.js` |
|
|
41
|
+
| `/session list` | Same as `/session` | `commands/session.js` |
|
|
42
|
+
| `/session <id>` | Switch the user's `activeThreadId` so subsequent messages route to that chat | `commands/session.js` |
|
package/lib/chat/CLAUDE.md
CHANGED
|
@@ -38,10 +38,10 @@ export { getRepositoriesHandler as GET } from 'thepopebot/chat/api';
|
|
|
38
38
|
## Chat Streaming Flow
|
|
39
39
|
|
|
40
40
|
1. Client sends message via AI SDK `DefaultChatTransport` → `POST /stream/chat`
|
|
41
|
-
2. Handler validates session, extracts text + file attachments from message parts
|
|
42
|
-
3. Calls `chatStream()` from `lib/ai/` which handles DB persistence and LLM invocation
|
|
43
|
-
4. Streams response chunks (text deltas, tool calls, tool results) via `createUIMessageStream`
|
|
44
|
-
5. After first message, client calls `/chat/finalize-chat` to generate auto-title
|
|
41
|
+
2. Handler validates session, extracts text + file attachments from message parts. Images and PDFs pass through as vision content; text files are inlined into the prompt.
|
|
42
|
+
3. Calls `chatStream()` from `lib/ai/` which handles DB persistence and LLM invocation. Two paths: SDK adapter (in-process, e.g. Claude Agent SDK) or direct headless container (other agents).
|
|
43
|
+
4. Streams response chunks (text deltas, tool calls, tool results, thinking blocks) via `createUIMessageStream`. Tool call/tool result pairs and `{ type: 'error' }` chunks are persisted as JSON message parts.
|
|
44
|
+
5. After the first user message streams, the client calls `/chat/finalize-chat` to generate the auto-title (helper LLM with truncated-description fallback).
|
|
45
45
|
|
|
46
46
|
## Server Actions (actions.js)
|
|
47
47
|
|
|
@@ -3,7 +3,7 @@ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useState, useEffect, useCallback, useRef } from "react";
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
5
|
import { PageLayout } from "./page-layout.js";
|
|
6
|
-
import { SpinnerIcon, RefreshIcon, StopIcon, PlayIcon, TrashIcon, XIcon } from "./icons.js";
|
|
6
|
+
import { SpinnerIcon, RefreshIcon, StopIcon, PlayIcon, TrashIcon, XIcon, FileTextIcon } from "./icons.js";
|
|
7
7
|
import { ConfirmDialog } from "./ui/confirm-dialog.js";
|
|
8
8
|
import { CodeLogView } from "./code-log-view.js";
|
|
9
9
|
import {
|
|
@@ -195,81 +195,99 @@ function ContainerRow({ container, onRequestStop, onShowLogs, isStopping, isStar
|
|
|
195
195
|
/* @__PURE__ */ jsx("td", { className: "py-2.5 pr-3 text-xs text-right hidden md:table-cell whitespace-nowrap", children: isRunning && container.stats ? /* @__PURE__ */ jsx("span", { children: formatBytes(container.stats.memUsage) }) : /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "\u2014" }) }),
|
|
196
196
|
/* @__PURE__ */ jsx("td", { className: "py-2.5 pr-3 text-xs text-muted-foreground hidden lg:table-cell whitespace-nowrap", children: container.status }),
|
|
197
197
|
/* @__PURE__ */ jsx("td", { className: "py-2.5 text-right whitespace-nowrap", children: /* @__PURE__ */ jsxs("div", { className: "inline-flex items-center gap-1.5", children: [
|
|
198
|
-
|
|
198
|
+
/* @__PURE__ */ jsx(
|
|
199
|
+
"button",
|
|
200
|
+
{
|
|
201
|
+
onClick: () => onShowLogs(container.name),
|
|
202
|
+
title: "Logs",
|
|
203
|
+
"aria-label": "Logs",
|
|
204
|
+
className: "inline-flex items-center justify-center rounded-md p-1.5 border border-border text-muted-foreground hover:bg-accent hover:text-foreground transition-colors",
|
|
205
|
+
children: /* @__PURE__ */ jsx(FileTextIcon, { size: 14 })
|
|
206
|
+
}
|
|
207
|
+
),
|
|
208
|
+
isRunning && (isStopping ? /* @__PURE__ */ jsx(
|
|
199
209
|
"button",
|
|
200
210
|
{
|
|
201
211
|
disabled: true,
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
]
|
|
212
|
+
title: "Stopping...",
|
|
213
|
+
"aria-label": "Stopping",
|
|
214
|
+
className: "inline-flex items-center justify-center rounded-md p-1.5 border border-border text-muted-foreground disabled:opacity-50 disabled:pointer-events-none transition-colors",
|
|
215
|
+
children: /* @__PURE__ */ jsx(SpinnerIcon, { size: 14 })
|
|
207
216
|
}
|
|
208
|
-
) : /* @__PURE__ */
|
|
217
|
+
) : /* @__PURE__ */ jsx(
|
|
209
218
|
"button",
|
|
210
219
|
{
|
|
211
220
|
onClick: () => onRequestStop(container.name),
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
]
|
|
221
|
+
title: "Stop",
|
|
222
|
+
"aria-label": "Stop",
|
|
223
|
+
className: "inline-flex items-center justify-center rounded-md p-1.5 border border-border text-muted-foreground hover:bg-accent hover:text-foreground transition-colors",
|
|
224
|
+
children: /* @__PURE__ */ jsx(StopIcon, { size: 14 })
|
|
217
225
|
}
|
|
218
226
|
)),
|
|
219
|
-
isStopped && (isStarting ? /* @__PURE__ */
|
|
227
|
+
isStopped && (isStarting ? /* @__PURE__ */ jsx(
|
|
220
228
|
"button",
|
|
221
229
|
{
|
|
222
230
|
disabled: true,
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
]
|
|
231
|
+
title: "Starting...",
|
|
232
|
+
"aria-label": "Starting",
|
|
233
|
+
className: "inline-flex items-center justify-center rounded-md p-1.5 border border-border text-muted-foreground disabled:opacity-50 disabled:pointer-events-none transition-colors",
|
|
234
|
+
children: /* @__PURE__ */ jsx(SpinnerIcon, { size: 14 })
|
|
228
235
|
}
|
|
229
|
-
) : /* @__PURE__ */
|
|
236
|
+
) : /* @__PURE__ */ jsx(
|
|
230
237
|
"button",
|
|
231
238
|
{
|
|
232
239
|
onClick: () => onRequestStop(container.name, "start"),
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
]
|
|
240
|
+
title: "Start",
|
|
241
|
+
"aria-label": "Start",
|
|
242
|
+
className: "inline-flex items-center justify-center rounded-md p-1.5 border border-border text-muted-foreground hover:bg-accent hover:text-foreground transition-colors",
|
|
243
|
+
children: /* @__PURE__ */ jsx(PlayIcon, { size: 14 })
|
|
238
244
|
}
|
|
239
245
|
)),
|
|
240
246
|
/* @__PURE__ */ jsx(
|
|
241
|
-
"button",
|
|
242
|
-
{
|
|
243
|
-
onClick: () => onShowLogs(container.name),
|
|
244
|
-
className: "inline-flex items-center gap-1 rounded-md px-2.5 py-1.5 text-xs font-medium border border-border text-muted-foreground hover:bg-accent hover:text-foreground transition-colors",
|
|
245
|
-
children: "Logs"
|
|
246
|
-
}
|
|
247
|
-
),
|
|
248
|
-
/* @__PURE__ */ jsxs(
|
|
249
247
|
"button",
|
|
250
248
|
{
|
|
251
249
|
onClick: () => handleAction("remove"),
|
|
252
250
|
disabled: removingContainer,
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
]
|
|
251
|
+
title: confirmingRemove ? "Confirm remove" : "Remove",
|
|
252
|
+
"aria-label": confirmingRemove ? "Confirm remove" : "Remove",
|
|
253
|
+
className: `inline-flex items-center justify-center rounded-md p-1.5 border transition-colors disabled:opacity-50 disabled:pointer-events-none ${confirmingRemove ? "border-destructive text-destructive hover:bg-destructive/10" : "border-border text-muted-foreground hover:bg-accent hover:text-foreground"}`,
|
|
254
|
+
children: removingContainer ? /* @__PURE__ */ jsx(SpinnerIcon, { size: 14 }) : /* @__PURE__ */ jsx(TrashIcon, { size: 14 })
|
|
258
255
|
}
|
|
259
256
|
)
|
|
260
257
|
] }) })
|
|
261
258
|
] });
|
|
262
259
|
}
|
|
263
260
|
function DockerContainersSection({ containers, loading, onRequestStop, onShowLogs, pendingStop, pendingStart }) {
|
|
261
|
+
const [activeTab, setActiveTab] = useState("running");
|
|
264
262
|
if (loading) {
|
|
265
263
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
266
264
|
/* @__PURE__ */ jsx("h2", { className: "text-base font-medium", children: "Docker Containers" }),
|
|
267
265
|
/* @__PURE__ */ jsx("div", { className: "flex flex-col gap-3", children: [...Array(3)].map((_, i) => /* @__PURE__ */ jsx("div", { className: "h-14 animate-pulse rounded-md bg-border/50" }, i)) })
|
|
268
266
|
] });
|
|
269
267
|
}
|
|
268
|
+
const runningContainers = containers.filter((c) => c.state === "running");
|
|
269
|
+
const exitedContainers = containers.filter((c) => c.state !== "running");
|
|
270
|
+
const visibleContainers = activeTab === "running" ? runningContainers : exitedContainers;
|
|
271
|
+
const tabs = [
|
|
272
|
+
{ id: "running", label: `Running (${runningContainers.length})` },
|
|
273
|
+
{ id: "exited", label: `Exited (${exitedContainers.length})` }
|
|
274
|
+
];
|
|
270
275
|
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
271
276
|
/* @__PURE__ */ jsx("h2", { className: "text-base font-medium", children: "Docker Containers" }),
|
|
272
|
-
|
|
277
|
+
/* @__PURE__ */ jsx("div", { className: "flex gap-1.5 overflow-x-auto scrollbar-hide max-w-full", children: tabs.map((tab) => {
|
|
278
|
+
const isActive = activeTab === tab.id;
|
|
279
|
+
return /* @__PURE__ */ jsx(
|
|
280
|
+
"button",
|
|
281
|
+
{
|
|
282
|
+
type: "button",
|
|
283
|
+
onClick: () => setActiveTab(tab.id),
|
|
284
|
+
className: `rounded-full px-3 py-1.5 min-h-[36px] inline-flex items-center text-xs font-medium transition-colors shrink-0 whitespace-nowrap ${isActive ? "bg-foreground text-background" : "text-muted-foreground hover:text-foreground hover:bg-accent"}`,
|
|
285
|
+
children: tab.label
|
|
286
|
+
},
|
|
287
|
+
tab.id
|
|
288
|
+
);
|
|
289
|
+
}) }),
|
|
290
|
+
visibleContainers.length === 0 ? /* @__PURE__ */ jsx("div", { className: "rounded-lg border border-dashed border-border p-8 text-center text-sm text-muted-foreground", children: activeTab === "running" ? "No running containers." : "No exited containers." }) : /* @__PURE__ */ jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-left", children: [
|
|
273
291
|
/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "text-xs text-muted-foreground", children: [
|
|
274
292
|
/* @__PURE__ */ jsx("th", { className: "pb-2 pr-3 font-medium", children: "State" }),
|
|
275
293
|
/* @__PURE__ */ jsx("th", { className: "pb-2 pr-3 font-medium", children: "Container" }),
|
|
@@ -278,7 +296,7 @@ function DockerContainersSection({ containers, loading, onRequestStop, onShowLog
|
|
|
278
296
|
/* @__PURE__ */ jsx("th", { className: "pb-2 pr-3 font-medium hidden lg:table-cell", children: "Status" }),
|
|
279
297
|
/* @__PURE__ */ jsx("th", { className: "pb-2 font-medium text-right", children: "Actions" })
|
|
280
298
|
] }) }),
|
|
281
|
-
/* @__PURE__ */ jsx("tbody", { children:
|
|
299
|
+
/* @__PURE__ */ jsx("tbody", { children: visibleContainers.map((c) => /* @__PURE__ */ jsx(
|
|
282
300
|
ContainerRow,
|
|
283
301
|
{
|
|
284
302
|
container: c,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
4
4
|
import { createPortal } from 'react-dom';
|
|
5
5
|
import { PageLayout } from './page-layout.js';
|
|
6
|
-
import { SpinnerIcon, RefreshIcon, StopIcon, PlayIcon, TrashIcon, XIcon } from './icons.js';
|
|
6
|
+
import { SpinnerIcon, RefreshIcon, StopIcon, PlayIcon, TrashIcon, XIcon, FileTextIcon } from './icons.js';
|
|
7
7
|
import { ConfirmDialog } from './ui/confirm-dialog.js';
|
|
8
8
|
import { CodeLogView } from './code-log-view.js';
|
|
9
9
|
import {
|
|
@@ -263,22 +263,32 @@ function ContainerRow({ container, onRequestStop, onShowLogs, isStopping, isStar
|
|
|
263
263
|
</td>
|
|
264
264
|
<td className="py-2.5 text-right whitespace-nowrap">
|
|
265
265
|
<div className="inline-flex items-center gap-1.5">
|
|
266
|
+
<button
|
|
267
|
+
onClick={() => onShowLogs(container.name)}
|
|
268
|
+
title="Logs"
|
|
269
|
+
aria-label="Logs"
|
|
270
|
+
className="inline-flex items-center justify-center rounded-md p-1.5 border border-border text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
|
|
271
|
+
>
|
|
272
|
+
<FileTextIcon size={14} />
|
|
273
|
+
</button>
|
|
266
274
|
{isRunning && (
|
|
267
275
|
isStopping ? (
|
|
268
276
|
<button
|
|
269
277
|
disabled
|
|
270
|
-
|
|
278
|
+
title="Stopping..."
|
|
279
|
+
aria-label="Stopping"
|
|
280
|
+
className="inline-flex items-center justify-center rounded-md p-1.5 border border-border text-muted-foreground disabled:opacity-50 disabled:pointer-events-none transition-colors"
|
|
271
281
|
>
|
|
272
|
-
<SpinnerIcon size={
|
|
273
|
-
Stopping...
|
|
282
|
+
<SpinnerIcon size={14} />
|
|
274
283
|
</button>
|
|
275
284
|
) : (
|
|
276
285
|
<button
|
|
277
286
|
onClick={() => onRequestStop(container.name)}
|
|
278
|
-
|
|
287
|
+
title="Stop"
|
|
288
|
+
aria-label="Stop"
|
|
289
|
+
className="inline-flex items-center justify-center rounded-md p-1.5 border border-border text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
|
|
279
290
|
>
|
|
280
|
-
<StopIcon size={
|
|
281
|
-
Stop
|
|
291
|
+
<StopIcon size={14} />
|
|
282
292
|
</button>
|
|
283
293
|
)
|
|
284
294
|
)}
|
|
@@ -286,38 +296,35 @@ function ContainerRow({ container, onRequestStop, onShowLogs, isStopping, isStar
|
|
|
286
296
|
isStarting ? (
|
|
287
297
|
<button
|
|
288
298
|
disabled
|
|
289
|
-
|
|
299
|
+
title="Starting..."
|
|
300
|
+
aria-label="Starting"
|
|
301
|
+
className="inline-flex items-center justify-center rounded-md p-1.5 border border-border text-muted-foreground disabled:opacity-50 disabled:pointer-events-none transition-colors"
|
|
290
302
|
>
|
|
291
|
-
<SpinnerIcon size={
|
|
292
|
-
Starting...
|
|
303
|
+
<SpinnerIcon size={14} />
|
|
293
304
|
</button>
|
|
294
305
|
) : (
|
|
295
306
|
<button
|
|
296
307
|
onClick={() => onRequestStop(container.name, 'start')}
|
|
297
|
-
|
|
308
|
+
title="Start"
|
|
309
|
+
aria-label="Start"
|
|
310
|
+
className="inline-flex items-center justify-center rounded-md p-1.5 border border-border text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
|
|
298
311
|
>
|
|
299
|
-
<PlayIcon size={
|
|
300
|
-
Start
|
|
312
|
+
<PlayIcon size={14} />
|
|
301
313
|
</button>
|
|
302
314
|
)
|
|
303
315
|
)}
|
|
304
|
-
<button
|
|
305
|
-
onClick={() => onShowLogs(container.name)}
|
|
306
|
-
className="inline-flex items-center gap-1 rounded-md px-2.5 py-1.5 text-xs font-medium border border-border text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
|
|
307
|
-
>
|
|
308
|
-
Logs
|
|
309
|
-
</button>
|
|
310
316
|
<button
|
|
311
317
|
onClick={() => handleAction('remove')}
|
|
312
318
|
disabled={removingContainer}
|
|
313
|
-
|
|
319
|
+
title={confirmingRemove ? 'Confirm remove' : 'Remove'}
|
|
320
|
+
aria-label={confirmingRemove ? 'Confirm remove' : 'Remove'}
|
|
321
|
+
className={`inline-flex items-center justify-center rounded-md p-1.5 border transition-colors disabled:opacity-50 disabled:pointer-events-none ${
|
|
314
322
|
confirmingRemove
|
|
315
323
|
? 'border-destructive text-destructive hover:bg-destructive/10'
|
|
316
324
|
: 'border-border text-muted-foreground hover:bg-accent hover:text-foreground'
|
|
317
325
|
}`}
|
|
318
326
|
>
|
|
319
|
-
{removingContainer ? <SpinnerIcon size={
|
|
320
|
-
{confirmingRemove ? 'Confirm' : 'Remove'}
|
|
327
|
+
{removingContainer ? <SpinnerIcon size={14} /> : <TrashIcon size={14} />}
|
|
321
328
|
</button>
|
|
322
329
|
</div>
|
|
323
330
|
</td>
|
|
@@ -330,6 +337,8 @@ function ContainerRow({ container, onRequestStop, onShowLogs, isStopping, isStar
|
|
|
330
337
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
331
338
|
|
|
332
339
|
function DockerContainersSection({ containers, loading, onRequestStop, onShowLogs, pendingStop, pendingStart }) {
|
|
340
|
+
const [activeTab, setActiveTab] = useState('running');
|
|
341
|
+
|
|
333
342
|
if (loading) {
|
|
334
343
|
return (
|
|
335
344
|
<div className="space-y-4">
|
|
@@ -343,12 +352,42 @@ function DockerContainersSection({ containers, loading, onRequestStop, onShowLog
|
|
|
343
352
|
);
|
|
344
353
|
}
|
|
345
354
|
|
|
355
|
+
const runningContainers = containers.filter((c) => c.state === 'running');
|
|
356
|
+
const exitedContainers = containers.filter((c) => c.state !== 'running');
|
|
357
|
+
const visibleContainers = activeTab === 'running' ? runningContainers : exitedContainers;
|
|
358
|
+
|
|
359
|
+
const tabs = [
|
|
360
|
+
{ id: 'running', label: `Running (${runningContainers.length})` },
|
|
361
|
+
{ id: 'exited', label: `Exited (${exitedContainers.length})` },
|
|
362
|
+
];
|
|
363
|
+
|
|
346
364
|
return (
|
|
347
365
|
<div className="space-y-4">
|
|
348
366
|
<h2 className="text-base font-medium">Docker Containers</h2>
|
|
349
|
-
|
|
367
|
+
|
|
368
|
+
<div className="flex gap-1.5 overflow-x-auto scrollbar-hide max-w-full">
|
|
369
|
+
{tabs.map((tab) => {
|
|
370
|
+
const isActive = activeTab === tab.id;
|
|
371
|
+
return (
|
|
372
|
+
<button
|
|
373
|
+
key={tab.id}
|
|
374
|
+
type="button"
|
|
375
|
+
onClick={() => setActiveTab(tab.id)}
|
|
376
|
+
className={`rounded-full px-3 py-1.5 min-h-[36px] inline-flex items-center text-xs font-medium transition-colors shrink-0 whitespace-nowrap ${
|
|
377
|
+
isActive
|
|
378
|
+
? 'bg-foreground text-background'
|
|
379
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-accent'
|
|
380
|
+
}`}
|
|
381
|
+
>
|
|
382
|
+
{tab.label}
|
|
383
|
+
</button>
|
|
384
|
+
);
|
|
385
|
+
})}
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
{visibleContainers.length === 0 ? (
|
|
350
389
|
<div className="rounded-lg border border-dashed border-border p-8 text-center text-sm text-muted-foreground">
|
|
351
|
-
No containers
|
|
390
|
+
{activeTab === 'running' ? 'No running containers.' : 'No exited containers.'}
|
|
352
391
|
</div>
|
|
353
392
|
) : (
|
|
354
393
|
<div className="overflow-x-auto">
|
|
@@ -364,7 +403,7 @@ function DockerContainersSection({ containers, loading, onRequestStop, onShowLog
|
|
|
364
403
|
</tr>
|
|
365
404
|
</thead>
|
|
366
405
|
<tbody>
|
|
367
|
-
{
|
|
406
|
+
{visibleContainers.map((c) => (
|
|
368
407
|
<ContainerRow
|
|
369
408
|
key={c.id}
|
|
370
409
|
container={c}
|
package/lib/cluster/CLAUDE.md
CHANGED
|
@@ -41,9 +41,15 @@ Roles support multiple concurrent triggers. All paths use `canRunRole()` as a sh
|
|
|
41
41
|
|
|
42
42
|
## Concurrency & Validation
|
|
43
43
|
|
|
44
|
-
`canRunRole(roleIdOrData)` is the
|
|
44
|
+
`canRunRole(roleIdOrData)` is the synchronous validation function. It checks cluster enabled status and concurrency limits. Returns `{ allowed, reason?, roleData? }`. Reasons: `disabled` (cluster off), `concurrency` (at max), `not_found`.
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
`acquireAndRunRole(roleId, payload?)` is the **atomic gate** that all trigger paths actually use. It calls `canRunRole()` and `runClusterRole()` together so two simultaneous triggers can't both pass the concurrency check before either container is observable to `listContainers()`. Manual UI triggers, webhooks, cron, and file-watch all funnel through this.
|
|
47
|
+
|
|
48
|
+
Each role has `maxConcurrency` (default 1). `canRunRole()` counts running instances via `listContainers()`.
|
|
49
|
+
|
|
50
|
+
## Plan Mode (Roles)
|
|
51
|
+
|
|
52
|
+
`cluster_roles.planMode` (default `0`) gates the worker into Claude's plan-mode (read-only). When set, the worker is launched with `PERMISSION=plan` so it cannot execute mutating tools. Useful for review/analysis roles.
|
|
47
53
|
|
|
48
54
|
## Prompt Architecture
|
|
49
55
|
|
|
@@ -75,6 +81,6 @@ Built by `buildTemplateVars()` → `buildWorkerSystemPrompt()` + `resolveCluster
|
|
|
75
81
|
## DB Tables
|
|
76
82
|
|
|
77
83
|
- `clusters` — cluster metadata (name, system_prompt, folders, enabled)
|
|
78
|
-
- `cluster_roles` — role definitions scoped to a cluster (role_name, role, prompt, trigger_config, max_concurrency, cleanup_worker_dir, folders)
|
|
84
|
+
- `cluster_roles` — role definitions scoped to a cluster (role_name, role, prompt, trigger_config, max_concurrency, plan_mode, cleanup_worker_dir, folders)
|
|
79
85
|
|
|
80
86
|
Workers are ephemeral containers, not database entities.
|
package/lib/code/CLAUDE.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## Data Flow
|
|
4
4
|
|
|
5
|
-
Chat agent's `coding_agent` tool → `runInteractiveContainer()` in `lib/tools/docker.js` → Docker container runs `coding-agent-
|
|
5
|
+
Chat agent's synthetic `coding_agent` tool wrapper → `runInteractiveContainer()` in `lib/tools/docker.js` → Docker container runs `coding-agent-{agent}` image (interactive runtime) → ttyd on port 7681 fronts a tmux session that runs the agent CLI → browser navigates to `/code/{id}` → `TerminalView` (xterm.js) opens WebSocket → `ws-proxy.js` authenticates and proxies to container.
|
|
6
|
+
|
|
7
|
+
The interactive container's entrypoint launches a tmux session via the per-agent `docker/coding-agent/scripts/agents/<agent>/start-coding-session.sh` script. Multiple terminal tabs can attach to the same tmux session and survive WebSocket reconnects. Tab close + reopen reattaches; container restart re-creates the session with the prior agent session resumed (see "Session Continuity" below).
|
|
6
8
|
|
|
7
9
|
## WebSocket Auth
|
|
8
10
|
|
|
@@ -23,12 +25,18 @@ All actions use `requireAuth()` with ownership checks: `getCodeWorkspaces()`, `c
|
|
|
23
25
|
|
|
24
26
|
## Multi-Agent Backends
|
|
25
27
|
|
|
26
|
-
Code workspaces support multiple coding agent backends.
|
|
28
|
+
Code workspaces support multiple coding agent backends. Selection is **per-workspace** via the `codingAgent` column on `code_workspaces`, falling back to the global `CODING_AGENT` config key, then to `claude-code`. See `lib/code/actions.js:410`: `const agent = workspace.codingAgent || getConfig('CODING_AGENT') || 'claude-code';`. The same fallback chain is used by `lib/ai/index.js` for chat-mode streaming.
|
|
27
29
|
|
|
28
|
-
**Supported agents**: `claude-code`, `pi`, `gemini-cli`, `codex-cli`, `opencode`. Each uses a different Docker image variant (`docker/coding-agent/Dockerfile.*`) and agent-specific setup/auth scripts in `docker/coding-agent/scripts/`.
|
|
30
|
+
**Supported agents**: `claude-code`, `pi-coding-agent`, `gemini-cli`, `codex-cli`, `opencode`, `kimi-cli`. Each uses a different Docker image variant (`docker/coding-agent/Dockerfile.*`) and agent-specific setup/auth scripts in `docker/coding-agent/scripts/`.
|
|
29
31
|
|
|
30
32
|
**Configuration**: Users configure agents via `/admin/event-handler/coding-agents` — enable/disable agents, set per-agent auth mode (OAuth vs API key), provider, and model. `setCodingAgentDefault()` sets the global default. `buildAgentAuthEnv()` in `lib/tools/docker.js` resolves credentials from the settings DB at container launch time.
|
|
31
33
|
|
|
32
34
|
**Container streaming**: `lib/containers/stream.js` provides an SSE endpoint (`/stream/containers`) that polls Docker for container stats every 3 seconds. Used by the Containers admin page for live monitoring.
|
|
33
35
|
|
|
34
36
|
**Backend API in messages**: When an agent produces output, the `backendApi` field in message chunks identifies which agent backend generated the response.
|
|
37
|
+
|
|
38
|
+
## Session Continuity
|
|
39
|
+
|
|
40
|
+
Code workspaces, chat-mode (SDK adapter), and headless agent jobs share session continuity through `lib/ai/session-manager.js`. Session IDs are written to per-port files inside the workspace volume — `~/.{agent}-ttyd-sessions/${PORT}` (or scope-prefixed when `SCOPE` is set). The agent CLI captures its session ID via per-agent hooks (see `docker/coding-agent/CLAUDE.md` § Session Tracking for the 5 patterns). On the next launch the entrypoint reads the saved ID and passes the agent's resume flag (`--continue`, `--resume`, `--session`, depending on agent).
|
|
41
|
+
|
|
42
|
+
Headless `run.sh` always reads from port `7681` — so SDK chat, manual chat tools, and code workspaces all converge on the same conversation when they share a workspace.
|
package/lib/containers/CLAUDE.md
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
# lib/containers/ — Container Streaming
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Two SSE endpoints — both authenticated via `auth()` session.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
**
|
|
8
|
-
**
|
|
9
|
-
**
|
|
5
|
+
## `stream.js` — Container Status
|
|
6
|
+
|
|
7
|
+
- **Endpoint**: `/stream/containers`
|
|
8
|
+
- **Events**: `containers` (every 3s with full container list + CPU/memory stats), `ping` (keepalive every 15s)
|
|
9
|
+
- **Data source**: `listNetworkContainers()` + `getContainerStats()` from `lib/tools/docker.js`. Filtered to containers on the event-handler's Docker network (auto-detected at boot).
|
|
10
|
+
- **Client**: `ContainersPage` connects via `new EventSource('/stream/containers')`
|
|
11
|
+
|
|
12
|
+
## `logs.js` — Container Log Tail
|
|
13
|
+
|
|
14
|
+
- **Endpoint**: `/stream/containers/logs?name=<containerName>`
|
|
15
|
+
- **Events**: raw log lines as SSE `data:` frames
|
|
16
|
+
- **Data source**: Docker `containers/{id}/logs?follow=true&stdout=1&stderr=1` via the multiplexed-stream parser in `lib/tools/docker.js`
|
|
17
|
+
- **Client**: `ContainerLogsView` (used from the Containers admin page when a row's logs are expanded)
|
|
18
|
+
|
|
19
|
+
Both endpoints share the same network filter and frame-decoding logic so all SSE consumers see the same view.
|
package/lib/db/CLAUDE.md
CHANGED
|
@@ -25,11 +25,12 @@ Key files: `schema.js` (source of truth), `drizzle/` (generated migrations), `dr
|
|
|
25
25
|
| `users` | Admin accounts (email, bcrypt password hash, role) |
|
|
26
26
|
| `chats` | Chat sessions (user_id, title, starred, chat_mode, code_workspace_id, timestamps) |
|
|
27
27
|
| `messages` | Chat messages (chat_id, role, content) |
|
|
28
|
-
| `code_workspaces` | Code workspace containers (user_id, container_name, repo, branch, feature_branch, title, last_interactive_commit, starred, has_changes) |
|
|
28
|
+
| `code_workspaces` | Code workspace containers (user_id, container_name, repo, branch, feature_branch, title, last_interactive_commit, coding_agent, scope, starred, has_changes) |
|
|
29
29
|
| `notifications` | Job completion notifications (notification text, payload, read status) |
|
|
30
30
|
| `subscriptions` | Channel subscriptions (platform, channel_id) |
|
|
31
|
+
| `user_channels` | Per-user channel linking (user_id, channel, channel_chat_id, code, code_expires_at, verified_at, active_thread_id) — Telegram verification + active thread |
|
|
31
32
|
| `clusters` | Worker clusters (user_id, name, system_prompt, folders, enabled, starred) |
|
|
32
|
-
| `cluster_roles` | Role definitions scoped to a cluster (cluster_id, role_name, role, trigger_config, max_concurrency, cleanup_worker_dir, folders) |
|
|
33
|
+
| `cluster_roles` | Role definitions scoped to a cluster (cluster_id, role_name, role, trigger_config, max_concurrency, plan_mode, cleanup_worker_dir, folders) |
|
|
33
34
|
| `settings` | Key-value configuration store (also stores API keys and OAuth tokens via type/key/value) |
|
|
34
35
|
|
|
35
36
|
## OAuth Token Storage
|
|
@@ -64,3 +65,5 @@ OAuth tokens for coding agent backends are stored as `config_secret` with LRU ro
|
|
|
64
65
|
- `chats.chatMode` — `'agent'` (default) or `'code'`. Determines which agent singleton and tools are used.
|
|
65
66
|
- `codeWorkspaces.featureBranch` — tracks the git feature branch for the workspace session.
|
|
66
67
|
- `codeWorkspaces.hasChanges` — flag set when workspace has uncommitted changes.
|
|
68
|
+
- `codeWorkspaces.codingAgent` — per-workspace coding-agent override. Falls back to global `CODING_AGENT` config, then `claude-code` (`lib/code/actions.js:410`).
|
|
69
|
+
- `codeWorkspaces.scope` — subdirectory scope within the repo (e.g., `agents/gary-vee`). Resolves the agent's working directory and skills (`lib/ai/scope.js`).
|
package/lib/tools/CLAUDE.md
CHANGED
|
@@ -12,7 +12,16 @@ Calls Docker Engine API directly through `/var/run/docker.sock` using Node's `ht
|
|
|
12
12
|
|
|
13
13
|
**Image pull on demand**: Checks if image exists locally before pulling. Avoids pre-pulling at startup.
|
|
14
14
|
|
|
15
|
-
**`buildAgentAuthEnv(agent)`**: Resolves coding agent type → auth environment variables from the settings DB.
|
|
15
|
+
**`buildAgentAuthEnv(agent)`**: Resolves coding agent type → auth environment variables from the settings DB. All credentials come from the DB (`getConfig`, `getCustomProvider`), never `.env` or GitHub secrets. Returns `{ env: string[], backendApi: string }`.
|
|
16
|
+
|
|
17
|
+
Per-agent resolution paths:
|
|
18
|
+
|
|
19
|
+
- **`claude-code`** — `CODING_AGENT_CLAUDE_CODE_BACKEND` selects backend. If `anthropic`, picks OAuth (`CLAUDE_CODE_OAUTH_TOKEN` via LRU rotation) or API key (`ANTHROPIC_API_KEY`). For Anthropic-compatible third parties (DeepSeek, MiniMax, Kimi, OpenRouter) sets `ANTHROPIC_AUTH_TOKEN` + `ANTHROPIC_BASE_URL` to the provider's `anthropicEndpoint`. For OpenAI-only builtins or custom providers, routes through the LiteLLM sidecar at `http://litellm:4000` and prefixes the model with the provider key.
|
|
20
|
+
- **`pi-coding-agent`, `opencode`, `kimi-cli`** — share a multi-provider pattern. `CODING_AGENT_{AGENT}_PROVIDER` picks anthropic/openai/google/deepseek/minimax/mistral/xai/openrouter/nvidia or a custom provider. Sets the matching `*_API_KEY` (or `CUSTOM_OPENAI_BASE_URL` + `CUSTOM_API_KEY` for custom).
|
|
21
|
+
- **`gemini-cli`** — `GOOGLE_API_KEY` only. Backend is always `google`.
|
|
22
|
+
- **`codex-cli`** — OAuth (`CODEX_OAUTH_TOKEN`) or API key (`OPENAI_API_KEY`). Backend always `openai`.
|
|
23
|
+
|
|
24
|
+
OAuth tokens use LRU rotation via `getNextOAuthToken()` (in `lib/db/oauth-tokens.js`) — distributes load across multiple stored tokens and updates `lastUsedAt` on each pick. Refresh-token rotation is handled at retrieval time in `/api/get-agent-job-secret` (under a per-token lock).
|
|
16
25
|
|
|
17
26
|
## create-agent-job.js — Agent Job Creation
|
|
18
27
|
|
package/package.json
CHANGED
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`:
|
|
@@ -20,16 +20,14 @@ This is a [thepopebot](https://github.com/stephengpope/thepopebot) project.
|
|
|
20
20
|
|
|
21
21
|
## Managed Files
|
|
22
22
|
|
|
23
|
-
Some files are auto-synced by `npx thepopebot init` and will be overwritten on upgrade. Do not edit these:
|
|
23
|
+
Some files are auto-synced by `npx thepopebot init` and will be overwritten on every init/upgrade. Do not edit these:
|
|
24
24
|
|
|
25
25
|
- `.github/workflows/` — CI/CD workflows
|
|
26
26
|
- `docker-compose.yml`
|
|
27
27
|
- `.dockerignore`
|
|
28
28
|
- `.gitignore`
|
|
29
|
-
|
|
30
|
-
- `agents/CLAUDE.md`
|
|
31
|
-
- `event-handler/CLAUDE.md`
|
|
32
|
-
- `skills/CLAUDE.md`
|
|
29
|
+
|
|
30
|
+
The `CLAUDE.md` files scattered through the project tree (e.g. `agent-job/CLAUDE.md`, `agents/CLAUDE.md`, `event-handler/CLAUDE.md`, `skills/CLAUDE.md`) are scaffolded by `init` from `*.template` sources but are **not** in the managed-paths list — they will not be overwritten if you have edited them. Run `npx thepopebot reset <path>` to restore one to its template default, or `npx thepopebot diff <path>` to see your local changes.
|
|
33
31
|
|
|
34
32
|
## Agent Scoping
|
|
35
33
|
|
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
## Adding an Agent
|
|
4
4
|
|
|
5
|
-
Each subdirectory defines an agent.
|
|
5
|
+
Each subdirectory defines an agent. At minimum create a folder with a `SYSTEM.md` file:
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
agents/
|
|
9
9
|
└── my-agent/
|
|
10
|
-
|
|
10
|
+
├── SYSTEM.md # System prompt — identity, instructions, constraints (required)
|
|
11
|
+
├── CLAUDE.md # Optional — guidance for AI assistants editing this agent's files
|
|
12
|
+
├── skills/ # Optional — agent-specific skills (overrides root skills/ for this scope)
|
|
13
|
+
└── jobs/ # Optional — reusable task prompts referenced from CRONS.json
|
|
11
14
|
```
|
|
12
15
|
|
|
13
|
-
`SYSTEM.md` is the agent's system prompt. Write it in markdown addressed to the agent (e.g. "You are a code reviewer...").
|
|
16
|
+
`SYSTEM.md` is the agent's system prompt. Write it in markdown addressed to the agent (e.g. "You are a code reviewer..."). `CLAUDE.md` (if present) is read by AI assistants working in this directory — use it for non-obvious context only.
|
|
17
|
+
|
|
18
|
+
**Skills resolution**: when a job runs with `scope: "agents/my-agent"`, the runtime checks `agents/my-agent/skills/` first; missing skills fall back to root `skills/`. To override a built-in skill for one agent, drop a same-named SKILL.md under that agent's `skills/`.
|
|
14
19
|
|
|
15
20
|
> **Important:** `agents/<name>/SYSTEM.md` **replaces** `agent-job/SYSTEM.md` when the agent is scoped — it doesn't extend it. Use `agent-job/SYSTEM.md` as a starting template and adapt it: keep the runtime environment notes, the `/tmp` scratch directive, and add the `{{skills}}` token if you want skill descriptions injected. Then add the agent's identity-specific instructions on top.
|
|
16
21
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# Data Directory
|
|
2
2
|
|
|
3
|
-
Runtime data including the SQLite database (`thepopebot.sqlite`) and cluster state. Created automatically on server start.
|
|
3
|
+
Runtime data including the SQLite database (`db/thepopebot.sqlite`) and cluster state. Created automatically on server start. Override the DB path with `DATABASE_PATH` in `.env`.
|
|
4
4
|
|
|
5
5
|
Not checked into git.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# event-handler/ — Event Handler Configuration
|
|
2
|
+
|
|
3
|
+
This directory holds configuration for the event handler: webhook triggers, chat system prompts, cluster role definitions, LiteLLM proxy routing, and the job-summary prompt. Edit these to shape how your handler reacts to events.
|
|
4
|
+
|
|
5
|
+
## Files
|
|
6
|
+
|
|
7
|
+
| File | Purpose |
|
|
8
|
+
|------|---------|
|
|
9
|
+
| `TRIGGERS.json` | Webhook trigger definitions — fires actions when matching paths receive HTTP requests |
|
|
10
|
+
| `agent-chat/SYSTEM.md` | System prompt for the chat agent (default chat mode) |
|
|
11
|
+
| `code-chat/SYSTEM.md` | System prompt for code mode chat (workspace-aware) |
|
|
12
|
+
| `clusters/SYSTEM.md` | System prompt prepended to every cluster worker |
|
|
13
|
+
| `clusters/ROLE.md` | Role-template snippet referenced by cluster role definitions |
|
|
14
|
+
| `litellm/main.yaml` | LiteLLM proxy config — routes Claude Code through other providers |
|
|
15
|
+
| `SUMMARY.md` | System prompt for the auto-summary that runs after agent jobs complete |
|
|
16
|
+
|
|
17
|
+
## TRIGGERS.json
|
|
18
|
+
|
|
19
|
+
JSON array of webhook trigger definitions. Loaded at server boot by `lib/triggers.js`.
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"name": "review-github-event",
|
|
24
|
+
"watch_path": "/github/webhook",
|
|
25
|
+
"actions": [
|
|
26
|
+
{ "type": "agent", "job": "Summarize this GitHub event:\n{{body}}" }
|
|
27
|
+
],
|
|
28
|
+
"enabled": true
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
| Field | Required | Notes |
|
|
33
|
+
|-------|----------|-------|
|
|
34
|
+
| `name` | Yes | Unique within the file |
|
|
35
|
+
| `watch_path` | Yes | Path to match (`/github/webhook`, `/webhook`, custom paths) |
|
|
36
|
+
| `actions` | Yes | Array — runs in order, fire-and-forget after auth |
|
|
37
|
+
| `enabled` | No | Defaults `true`. Set `false` to keep the entry but skip it |
|
|
38
|
+
|
|
39
|
+
### Action types
|
|
40
|
+
|
|
41
|
+
Three types — `agent`, `command`, `webhook`. `agent` accepts optional `scope`, `agent_backend`, `llm_model` overrides:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{ "type": "agent", "job": "Process: {{body}}", "scope": "agents/triage", "agent_backend": "claude-code", "llm_model": "claude-sonnet-4-5-20250929" }
|
|
45
|
+
{ "type": "command", "command": "echo 'webhook received: {{body}}' >> logs/webhook.log" }
|
|
46
|
+
{ "type": "webhook", "url": "https://example.com/hook", "method": "POST", "headers": {}, "vars": { "source": "github" } }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`scope` resolves to a directory under `agents/` (see `agents/CLAUDE.md`). The agent runs with that directory as its working dir, picks up its `SYSTEM.md`, and uses the skills under `agents/<scope>/skills/` falling back to root `skills/`.
|
|
50
|
+
|
|
51
|
+
### Template tokens (in `job` and `command` strings)
|
|
52
|
+
|
|
53
|
+
`{{body}}`, `{{body.field}}`, `{{query}}`, `{{query.field}}`, `{{headers}}`, `{{headers.field}}`. Tokens are expanded when the trigger fires.
|
|
54
|
+
|
|
55
|
+
## Chat System Prompts
|
|
56
|
+
|
|
57
|
+
`agent-chat/SYSTEM.md` and `code-chat/SYSTEM.md` are the system prompts injected into chat sessions. They support `{{include path/to/file.md}}`, `{{datetime}}`, and `{{skills}}` variables (resolved by `lib/utils/render-md.js`).
|
|
58
|
+
|
|
59
|
+
- **Agent chat** (`agent-chat/`) — default chat mode. Workspace-agnostic.
|
|
60
|
+
- **Code chat** (`code-chat/`) — code-mode chat. Knows about the active code workspace, repo, branch, and feature branch.
|
|
61
|
+
|
|
62
|
+
Both can be scoped: a chat tied to a workspace with `scope: "agents/foo"` will load `agents/foo/SYSTEM.md` instead of these defaults.
|
|
63
|
+
|
|
64
|
+
## Cluster Configuration
|
|
65
|
+
|
|
66
|
+
`clusters/SYSTEM.md` is prepended to every cluster worker's system prompt. `clusters/ROLE.md` is a reusable role-template snippet that individual cluster role definitions can reference. Cluster behavior is configured in the Admin UI; these files supply the prompt context.
|
|
67
|
+
|
|
68
|
+
## LiteLLM Proxy
|
|
69
|
+
|
|
70
|
+
`litellm/main.yaml` is read by the optional LiteLLM sidecar (`docker-compose.litellm.yml`). It routes Claude Code's Anthropic-protocol calls to other providers (OpenAI, Gemini, custom OpenAI-compatible endpoints). Edit when you want to use Claude Code with a non-Anthropic backend. Refer to [LiteLLM docs](https://docs.litellm.ai/) for the schema.
|
|
71
|
+
|
|
72
|
+
## Summary Prompt
|
|
73
|
+
|
|
74
|
+
`SUMMARY.md` is the prompt for the helper LLM call that auto-summarizes agent job results (PR link, status, files changed). Tweak its tone or formatting here.
|
|
75
|
+
|
|
76
|
+
## Notes
|
|
77
|
+
|
|
78
|
+
- These files are user-owned — edits are preserved across `thepopebot upgrade`.
|
|
79
|
+
- The Admin UI (`/admin/event-handler/`) configures runtime defaults (LLM provider, coding agent backend, OAuth tokens). Trigger-level `agent_backend` / `llm_model` overrides those defaults.
|
|
@@ -69,9 +69,14 @@ If a skill needs an API key, add it via the admin UI (Settings > Agent Jobs > Se
|
|
|
69
69
|
|
|
70
70
|
Skills are active when present in the `skills/` directory. To deactivate, remove or move the skill directory out.
|
|
71
71
|
|
|
72
|
-
All coding agents discover skills from the same `skills/` directory via symlink bridges
|
|
73
|
-
|
|
74
|
-
- `.
|
|
72
|
+
All coding agents discover skills from the same `skills/` directory via symlink bridges created by `npx thepopebot init`:
|
|
73
|
+
|
|
74
|
+
- `.claude/skills → ../skills` (Claude Code)
|
|
75
|
+
- `.pi/skills → ../skills` (Pi)
|
|
76
|
+
- `.codex/skills → ../skills` (Codex CLI)
|
|
77
|
+
- `.gemini/skills → ../skills` (Gemini CLI)
|
|
78
|
+
- `.kimi/skills → ../skills` (Kimi CLI)
|
|
79
|
+
- `.agents/skills → ../skills` (OpenCode and other plugin-style agents)
|
|
75
80
|
|
|
76
81
|
## Creating a Skill
|
|
77
82
|
|