thepopebot 1.2.76-beta.36 → 1.2.76-beta.38
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 +13 -9
- package/api/index.js +1 -20
- package/config/CLAUDE.md +1 -0
- package/lib/CLAUDE.md +4 -2
- package/lib/ai/CLAUDE.md +3 -0
- package/lib/ai/workspace-setup.js +31 -69
- package/lib/auth/CLAUDE.md +13 -0
- package/lib/channels/CLAUDE.md +1 -1
- package/lib/chat/CLAUDE.md +12 -8
- package/lib/chat/components/CLAUDE.md +17 -14
- package/lib/chat/components/code-mode-toggle.js +18 -2
- package/lib/chat/components/code-mode-toggle.jsx +22 -2
- package/lib/code/CLAUDE.md +12 -2
- package/lib/containers/CLAUDE.md +3 -2
- package/lib/db/CLAUDE.md +17 -6
- package/lib/tools/CLAUDE.md +6 -2
- package/lib/tools/github.js +13 -6
- package/package.json +1 -1
- package/setup/CLAUDE.md +17 -12
- package/setup/lib/telegram.mjs +0 -15
package/api/CLAUDE.md
CHANGED
|
@@ -11,31 +11,35 @@ Most routes require a valid API key passed via the `x-api-key` header. API keys
|
|
|
11
11
|
Auth flow: `x-api-key` header → `verifyApiKey()` → DB lookup (hashed, timing-safe comparison). Two key types exist:
|
|
12
12
|
|
|
13
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.
|
|
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. Routes that read agent-job secrets (`/api/get-agent-job-secret`, `/api/agent-job-list-secrets`) reject any other key type.
|
|
15
15
|
|
|
16
16
|
## Do NOT use these routes for browser UI
|
|
17
17
|
|
|
18
18
|
Browser-facing data fetching uses **fetch route handlers** colocated with pages (`route.js` files in `web/app/`). These check `auth()` session — never use `/api` routes from the browser. Server actions (`'use server'`) are used only for **mutations** (rename, delete, star, config updates) — never for data fetching (causes page refresh issues). Handler implementations live in `lib/chat/api.js`; route files are thin re-exports.
|
|
19
19
|
|
|
20
|
+
Mutation server actions in `lib/chat/actions.js` use `requireAdmin()` for settings/config writes and `requireAuth()` for chat CRUD on user-owned rows. Reads stay open to any logged-in user. Sidebar items like Upgrade are gated on `user.role === 'admin'`.
|
|
21
|
+
|
|
20
22
|
| Caller | Mechanism | Auth |
|
|
21
23
|
|--------|-----------|------|
|
|
22
24
|
| External (cURL, GitHub Actions, Telegram) | `/api` route | `x-api-key` header |
|
|
23
25
|
| Browser UI (data fetching) | Fetch route handler colocated with page | `auth()` session |
|
|
24
|
-
| Browser UI (mutations) | Server action | `requireAuth()`
|
|
25
|
-
| Browser UI (streaming) | `/stream/chat`, `/stream/containers`, `/stream/cluster/*/logs` | `auth()` session |
|
|
26
|
+
| Browser UI (mutations) | Server action | `requireAuth()` / `requireAdmin()` |
|
|
27
|
+
| Browser UI (streaming) | `/stream/chat`, `/stream/containers`, `/stream/containers/logs`, `/stream/cluster/*/logs` | `auth()` session |
|
|
26
28
|
|
|
27
29
|
## Routes
|
|
28
30
|
|
|
29
31
|
| Method | Path | Auth | Handler |
|
|
30
32
|
|--------|------|------|---------|
|
|
31
33
|
| GET | `/api/ping` | None | Health check |
|
|
32
|
-
| POST | `/api/create-agent-job` | `x-api-key` | Create agent job. Body: `{ agent_job, llm_model?, agent_backend?, scope? }` |
|
|
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
|
-
|
|
|
35
|
-
| GET | `/api/agent-job-list-secrets` | `x-api-key` | List agent job secret keys (no values); returns `{secrets: [{key, isSet, updatedAt, secretType}]}` |
|
|
34
|
+
| POST | `/api/create-agent-job` | `x-api-key` | Create agent job. Body: `{ agent_job, llm_model?, agent_backend?, scope?, user_id? }`. `user_id` attributes the job to that user — the PR-merge webhook reads it back to DM the originator instead of broadcasting. |
|
|
35
|
+
| GET | `/api/get-agent-job-secret` | `agent_job_api_key` only | Get an agent job secret. `key` query param is upper-cased before lookup (admin form already saves uppercase). `oauth2` credentials return only the access_token (auto-refreshed under a per-key lock; rotated refresh tokens are persisted back). Other secret types return the raw value. |
|
|
36
|
+
| GET | `/api/agent-job-list-secrets` | `agent_job_api_key` only | List agent job secret keys (no values); returns `{secrets: [{key, isSet, updatedAt, secretType}]}` |
|
|
36
37
|
| GET | `/api/agent-jobs/status` | `x-api-key` | Agent job status (query: `?agent_job_id=`) |
|
|
38
|
+
| GET | `/api/users` | `x-api-key` | List users with verified DM channels: `{users: [{id, email, first_name, last_name, nickname, role, channels: ['telegram', ...]}]}`. Used by the `agent-job-dm` skill. |
|
|
39
|
+
| POST | `/api/send-dm` | `x-api-key` | Dispatch a system message. Body: `{ user_id?, message, payload? }`. With `user_id`: write 1 row + push to that user's default channel. Without: fan out 1 row per admin where `subscribedToSystemMessages=1`, each pushed to their default channel. Returns `{ok, recipients}`. |
|
|
37
40
|
| POST | `/api/telegram/webhook` | Telegram webhook secret | Telegram message handler (per-user routing via `user_channels`; verifies via `/verify <code>`, dispatches `/session` commands) |
|
|
38
|
-
| POST | `/api/
|
|
39
|
-
| POST | `/api/github/webhook` | GitHub webhook secret | GitHub event handler |
|
|
41
|
+
| POST | `/api/github/webhook` | GitHub webhook secret | GitHub event handler. PR-merge events read `user_id` from `agent-job.config.json` and dispatch the completion message via `dispatchSystemMessage` (per-user when set; broadcast to subscribed admins when absent). |
|
|
40
42
|
| POST | `/api/cluster/:clusterId/role/:roleId/webhook` | `x-api-key` | Trigger cluster role execution |
|
|
41
43
|
| GET/POST | `/api/oauth/callback` | `state` token | OAuth provider redirect target. Exchanges `code` for tokens, persists via `setAgentJobSecret(name, stored, 'oauth')`. |
|
|
44
|
+
|
|
45
|
+
Telegram bot configuration (token + webhook registration) is done from the admin UI at `/admin/event-handler/telegram`, not via an API route.
|
package/api/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createHash, timingSafeEqual, randomUUID } from 'crypto';
|
|
2
2
|
import { createAgentJob } from '../lib/tools/create-agent-job.js';
|
|
3
|
-
import { setWebhook } from '../lib/tools/telegram.js';
|
|
4
3
|
import { getAgentJobStatus, fetchAgentJobLog } from '../lib/tools/github.js';
|
|
5
4
|
import { getTelegramAdapter } from '../lib/channels/index.js';
|
|
6
5
|
import { dispatchCommand, dispatchPreAuthCommand } from '../lib/channels/commands/index.js';
|
|
@@ -17,7 +16,7 @@ import { setAgentJobSecret } from '../lib/db/config.js';
|
|
|
17
16
|
// ── Per-key lock for OAuth token refresh ────────────────────────────
|
|
18
17
|
const _refreshLocks = new Map();
|
|
19
18
|
|
|
20
|
-
// Bot token — resolved from DB/env
|
|
19
|
+
// Bot token — resolved from DB/env
|
|
21
20
|
let telegramBotToken = null;
|
|
22
21
|
|
|
23
22
|
|
|
@@ -253,23 +252,6 @@ async function handleListAgentSecrets(request) {
|
|
|
253
252
|
return Response.json({ secrets: listAgentJobSecrets() });
|
|
254
253
|
}
|
|
255
254
|
|
|
256
|
-
async function handleTelegramRegister(request) {
|
|
257
|
-
const body = await request.json();
|
|
258
|
-
const { bot_token, webhook_url } = body;
|
|
259
|
-
if (!bot_token || !webhook_url) {
|
|
260
|
-
return Response.json({ error: 'Missing bot_token or webhook_url' }, { status: 400 });
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
try {
|
|
264
|
-
const result = await setWebhook(bot_token, webhook_url, getConfig('TELEGRAM_WEBHOOK_SECRET'));
|
|
265
|
-
telegramBotToken = bot_token;
|
|
266
|
-
return Response.json({ success: true, result });
|
|
267
|
-
} catch (err) {
|
|
268
|
-
console.error(err);
|
|
269
|
-
return Response.json({ error: 'Failed to register webhook' }, { status: 500 });
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
255
|
async function handleTelegramWebhook(request) {
|
|
274
256
|
const botToken = getTelegramBotToken();
|
|
275
257
|
if (!botToken) return Response.json({ ok: true });
|
|
@@ -519,7 +501,6 @@ async function POST(request) {
|
|
|
519
501
|
case '/create-agent-job': return handleCreateAgentJob(request);
|
|
520
502
|
case '/send-dm': return handleSendDm(request);
|
|
521
503
|
case '/telegram/webhook': return handleTelegramWebhook(request);
|
|
522
|
-
case '/telegram/register': return handleTelegramRegister(request);
|
|
523
504
|
case '/github/webhook': return handleGithubWebhook(request);
|
|
524
505
|
default: return Response.json({ error: 'Not found' }, { status: 404 });
|
|
525
506
|
}
|
package/config/CLAUDE.md
CHANGED
|
@@ -24,5 +24,6 @@ export { register } from 'thepopebot/instrumentation';
|
|
|
24
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
25
|
9. **`startClusterRuntime()`** — `lib/cluster/runtime.js`. Registers cluster role triggers (cron + file watch + webhook).
|
|
26
26
|
10. **`startMaintenanceCron()`** — `lib/maintenance.js`. Hourly cleanup of expired agent-job API keys and other housekeeping.
|
|
27
|
+
11. **Config file watchers** — chokidar watches `agent-job/CRONS.json` and `event-handler/TRIGGERS.json` (with `awaitWriteFinish`). Changes call `reloadCrons()` / `reloadTriggers()` in place — no PM2 restart needed after a `git pull` updates the configs. Invalid JSON is logged and ignored; the previous schedule keeps running.
|
|
27
28
|
|
|
28
29
|
`initialized` is module-scoped so the sequence runs exactly once even if `register()` is called more than once.
|
package/lib/CLAUDE.md
CHANGED
|
@@ -18,10 +18,12 @@ If the task needs to *think*, use `agent`. If it just needs to *do*, use `comman
|
|
|
18
18
|
|
|
19
19
|
## Cron Jobs
|
|
20
20
|
|
|
21
|
-
Defined in `agent-job/CRONS.json`, loaded by `lib/cron.js` at startup via `node-cron`. Each entry has `name`, `schedule` (cron expression), `type` (`agent`/`command`/`webhook`), and the corresponding action fields (`job`, `command`, or `url`/`method`/`headers`/`vars`). Set `enabled: false` to disable. Agent-type entries support optional `agent_backend`, `llm_model`, and `
|
|
21
|
+
Defined in `agent-job/CRONS.json`, loaded by `lib/cron.js` at startup via `node-cron`. Each entry has `name`, `schedule` (cron expression), `type` (`agent`/`command`/`webhook`), and the corresponding action fields (`job`, `command`, or `url`/`method`/`headers`/`vars`). Set `enabled: false` to disable. Agent-type entries support optional `agent_backend`, `llm_model`, `scope`, and `user_id` fields. `agent_backend` picks which coding agent runs the job (e.g. `claude-code`, `codex-cli`); `llm_model` overrides the model within that agent. `scope` sets the agent's working directory to a subdirectory (e.g., `"scope": "agents/gary-vee"`) — the system prompt and skills resolve from that scope. `user_id` attributes the spawned job to a user — its completion DM (after PR merge) routes to that user's inbox; omit to broadcast to subscribed admins.
|
|
22
22
|
|
|
23
23
|
## Webhook Triggers
|
|
24
24
|
|
|
25
|
-
Defined in `event-handler/TRIGGERS.json`, loaded by `lib/triggers.js`. Each trigger watches an endpoint path (`watch_path`) and fires an array of actions (fire-and-forget, after auth, before route handler). Actions use the same `type`/`job`/`command`/`url` fields as cron jobs, including optional `agent_backend`/`llm_model`/`scope` overrides.
|
|
25
|
+
Defined in `event-handler/TRIGGERS.json`, loaded by `lib/triggers.js`. Each trigger watches an endpoint path (`watch_path`) and fires an array of actions (fire-and-forget, after auth, before route handler). Actions use the same `type`/`job`/`command`/`url` fields as cron jobs, including optional `agent_backend`/`llm_model`/`scope`/`user_id` overrides.
|
|
26
|
+
|
|
27
|
+
Both `lib/cron.js` and `lib/triggers.js` expose `reloadCrons()` / `reloadTriggers()`, called by chokidar watchers in `config/instrumentation.js` when the JSON files change — schedules and trigger maps reload in place without a server restart. Invalid JSON is logged and ignored.
|
|
26
28
|
|
|
27
29
|
Template tokens in `job` and `command` strings: `{{body}}`, `{{body.field}}`, `{{query}}`, `{{query.field}}`, `{{headers}}`, `{{headers.field}}`.
|
package/lib/ai/CLAUDE.md
CHANGED
|
@@ -37,9 +37,12 @@ The "job" sub-mode is no longer wired — a skill will replace autonomous job di
|
|
|
37
37
|
- `{ type: 'tool-call', toolCallId, toolName, args }`
|
|
38
38
|
- `{ type: 'tool-result', toolCallId, result }`
|
|
39
39
|
- `{ type: 'error', message }` — surfaced to the UI as a red message and persisted for refresh
|
|
40
|
+
- `{ type: 'auto-run', command }` — emitted at stream end when the active mode's `*_AUTO_RUN` flag is on, the workspace has uncommitted changes, and the configured git command (`pull`/`commit`/`push`/`pull-push`/`create-pr`) launched in a workspace command container. `lib/chat/api.js` surfaces it as a `data-auto-run` UI part; the chat page forwards it to WorkspaceBar so the spinner attaches without opening the dialog.
|
|
40
41
|
- `{ type: 'meta', ... }`, `{ type: 'result', ... }` — internal, not emitted to client
|
|
41
42
|
- `{ type: 'thinking-start' | 'thinking' | 'thinking-end' }` — SDK path only
|
|
42
43
|
|
|
44
|
+
`chatStream()` requires `options.userId` and throws if missing — there are no `'unknown'` / `'telegram'` fallbacks anywhere in chat creation. Both paths set `USER_ID` on the spawned container so skills resolve the originator consistently.
|
|
45
|
+
|
|
43
46
|
## Workspace Setup
|
|
44
47
|
|
|
45
48
|
`ensureWorkspaceRepo()` (workspace-setup.js) is called before either path runs. It clones the repo, sets git identity, and checks out/creates the feature branch on the host — agent-agnostic. The container's `2_clone.sh` is a no-op when `.git` already exists.
|
|
@@ -15,99 +15,61 @@ async function run(cmd, args, opts) {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* Set up the workspace once. If `.git` already exists, returns immediately
|
|
19
|
+
* and never touches git — git is the source of truth from then on.
|
|
20
20
|
*
|
|
21
|
-
*
|
|
21
|
+
* First-setup steps: gh auth setup-git, shallow clone the base branch,
|
|
22
|
+
* set git identity, checkout the feature branch (or stay on base).
|
|
22
23
|
*
|
|
23
24
|
* @param {object} opts
|
|
24
25
|
* @param {string} opts.workspaceDir - Absolute path to workspace directory (the git repo root)
|
|
25
26
|
* @param {string} opts.repo - GitHub owner/repo (e.g. "owner/repo")
|
|
26
27
|
* @param {string} opts.branch - Base branch (e.g. "main")
|
|
27
|
-
* @param {string} [opts.featureBranch] - Feature branch to create
|
|
28
|
+
* @param {string} [opts.featureBranch] - Feature branch to create on first setup
|
|
28
29
|
*/
|
|
29
30
|
export async function ensureWorkspaceRepo({ workspaceDir, repo, branch, featureBranch }) {
|
|
31
|
+
// Already set up — never touch git again.
|
|
32
|
+
if (existsSync(path.join(workspaceDir, '.git'))) return '';
|
|
33
|
+
|
|
34
|
+
if (!repo) throw new Error('ensureWorkspaceRepo: repo is required for initial clone');
|
|
35
|
+
if (!branch) throw new Error(`ensureWorkspaceRepo: branch is required (could not resolve default branch for ${repo})`);
|
|
36
|
+
|
|
30
37
|
const ghToken = getConfig('GH_TOKEN');
|
|
31
38
|
const env = { ...process.env };
|
|
32
39
|
if (ghToken) env.GH_TOKEN = ghToken;
|
|
33
40
|
|
|
41
|
+
mkdirSync(workspaceDir, { recursive: true });
|
|
34
42
|
const execOpts = { cwd: workspaceDir, env };
|
|
35
43
|
const log = [];
|
|
36
44
|
|
|
37
|
-
// 1. Create workspace directory
|
|
38
|
-
mkdirSync(workspaceDir, { recursive: true });
|
|
39
|
-
|
|
40
|
-
// 2. Configure git to use GH_TOKEN for GitHub HTTPS URLs (mirrors setup-git.sh)
|
|
41
45
|
if (ghToken) {
|
|
42
46
|
const out = await run('gh', ['auth', 'setup-git'], execOpts);
|
|
43
47
|
if (out) log.push(out);
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (!hasGit) {
|
|
49
|
-
if (!repo) throw new Error('ensureWorkspaceRepo: repo is required for initial clone');
|
|
50
|
-
if (!branch) throw new Error(`ensureWorkspaceRepo: branch is required (could not resolve default branch for ${repo})`);
|
|
51
|
-
const out = await run('git', ['clone', '--branch', branch, `https://github.com/${repo}`, '.'], execOpts);
|
|
52
|
-
log.push(`Cloned ${repo} (branch: ${branch})`);
|
|
53
|
-
if (out) log.push(out);
|
|
54
|
-
}
|
|
50
|
+
await run('git', ['clone', '--depth', '1', '--branch', branch, `https://github.com/${repo}`, '.'], execOpts);
|
|
51
|
+
log.push(`Cloned ${repo} (branch: ${branch}, depth 1)`);
|
|
55
52
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
await run('git', ['config', 'user.name', name], execOpts);
|
|
68
|
-
await run('git', ['config', 'user.email', email], execOpts);
|
|
69
|
-
log.push(`Git identity: ${name} <${email}>`);
|
|
70
|
-
} catch (err) {
|
|
71
|
-
console.error('[workspace-setup] Failed to set git identity:', err.message);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// 4. Feature branch checkout
|
|
77
|
-
if (!featureBranch) return log.join('\n');
|
|
78
|
-
|
|
79
|
-
// Already on the right branch locally?
|
|
80
|
-
try {
|
|
81
|
-
await run('git', ['rev-parse', '--verify', featureBranch], execOpts);
|
|
82
|
-
// Branch exists locally — make sure we're on it
|
|
83
|
-
const current = await run('git', ['rev-parse', '--abbrev-ref', 'HEAD'], execOpts);
|
|
84
|
-
if (current !== featureBranch) {
|
|
85
|
-
await run('git', ['checkout', featureBranch], execOpts);
|
|
86
|
-
log.push(`Checked out ${featureBranch}`);
|
|
87
|
-
} else {
|
|
88
|
-
log.push(`Already on ${featureBranch}`);
|
|
53
|
+
if (ghToken) {
|
|
54
|
+
try {
|
|
55
|
+
const userJson = await run('gh', ['api', 'user', '-q', '{name: .name, login: .login, email: .email, id: .id}'], execOpts);
|
|
56
|
+
const user = JSON.parse(userJson);
|
|
57
|
+
const name = user.name || user.login;
|
|
58
|
+
const email = user.email || `${user.id}+${user.login}@users.noreply.github.com`;
|
|
59
|
+
await run('git', ['config', 'user.name', name], execOpts);
|
|
60
|
+
await run('git', ['config', 'user.email', email], execOpts);
|
|
61
|
+
log.push(`Git identity: ${name} <${email}>`);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error('[workspace-setup] Failed to set git identity:', err.message);
|
|
89
64
|
}
|
|
90
|
-
return log.join('\n');
|
|
91
|
-
} catch {
|
|
92
|
-
// Branch doesn't exist locally — check remote
|
|
93
65
|
}
|
|
94
66
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
} else {
|
|
102
|
-
// Create new branch and push
|
|
103
|
-
await run('git', ['checkout', '-b', featureBranch], execOpts);
|
|
104
|
-
const pushOut = await run('git', ['push', '-u', 'origin', featureBranch], execOpts);
|
|
105
|
-
log.push(`Created and pushed ${featureBranch}`);
|
|
106
|
-
if (pushOut) log.push(pushOut);
|
|
107
|
-
}
|
|
108
|
-
} catch (err) {
|
|
109
|
-
console.error('[workspace-setup] Feature branch error:', err.message);
|
|
110
|
-
throw err;
|
|
67
|
+
if (featureBranch && featureBranch !== branch) {
|
|
68
|
+
await run('git', ['checkout', '-b', featureBranch], execOpts);
|
|
69
|
+
log.push(`Created and checked out ${featureBranch}`);
|
|
70
|
+
} else {
|
|
71
|
+
await run('git', ['checkout', branch], execOpts);
|
|
72
|
+
log.push(`On ${branch}`);
|
|
111
73
|
}
|
|
112
74
|
|
|
113
75
|
return log.join('\n');
|
package/lib/auth/CLAUDE.md
CHANGED
|
@@ -26,6 +26,19 @@ When `AUTH_SECRET` rotates or the container restarts, old session JWTs can't be
|
|
|
26
26
|
|
|
27
27
|
**No implicit login**: After setup, the user is redirected to `/login?created=1` and must authenticate normally. This ensures proper JWT session establishment.
|
|
28
28
|
|
|
29
|
+
## Role Model + requireAuth / requireAdmin
|
|
30
|
+
|
|
31
|
+
The `users.role` column defaults to `'user'`. Only `createFirstUser()` writes `'admin'` — every subsequent `addUser()` / `createUser()` row is a regular user unless explicitly promoted via the admin UI. The role lives in the JWT and is surfaced as `session.user.role`.
|
|
32
|
+
|
|
33
|
+
`lib/auth/index.js` exports two helpers (regular functions, not `'use server'`):
|
|
34
|
+
|
|
35
|
+
- `requireAuth()` — any logged-in user; throws redirect to `/login` otherwise.
|
|
36
|
+
- `requireAdmin()` — requires `role === 'admin'`; throws redirect to `/forbidden` otherwise.
|
|
37
|
+
|
|
38
|
+
Mutation server actions in `lib/chat/actions.js` use `requireAdmin()` for settings/config writes (LLM keys, coding-agent config, telegram, voice, webhooks, agent secrets, etc.) and `requireAuth()` for chat/workspace CRUD on user-owned rows. Reads stay open to any logged-in user. The Upgrade sidebar item is gated on `user.role === 'admin'`.
|
|
39
|
+
|
|
40
|
+
JWT role staleness on demotion is a known limitation — a demoted user keeps `'admin'` in their token until next sign-in.
|
|
41
|
+
|
|
29
42
|
## getPageAuthState()
|
|
30
43
|
|
|
31
44
|
Combines `auth()` + `getUserCount()` in a single `Promise.all()` call. Returns `{ session, needsSetup }`. The login page uses `needsSetup` to show either `<SetupForm>` or `<LoginForm>`.
|
package/lib/channels/CLAUDE.md
CHANGED
|
@@ -28,7 +28,7 @@ Abstract interface for platform integrations. Methods:
|
|
|
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
30
|
- **Webhook auth**: Validates `x-telegram-bot-api-secret-token` header against `TELEGRAM_WEBHOOK_SECRET`.
|
|
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
|
|
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 inserts intermediate "→ used X" lines as tool calls land.
|
|
32
32
|
|
|
33
33
|
## Slash Commands (`lib/channels/commands/`)
|
|
34
34
|
|
package/lib/chat/CLAUDE.md
CHANGED
|
@@ -22,17 +22,19 @@ export { getRepositoriesHandler as GET } from 'thepopebot/chat/api';
|
|
|
22
22
|
- `POST /stream/chat` — AI SDK streaming via `createUIMessageStream`. Handles file attachments (images/PDFs as visual, text files inlined), workspace context, and code mode settings.
|
|
23
23
|
|
|
24
24
|
**Data fetch routes** (colocated with pages):
|
|
25
|
-
- `/code/repositories
|
|
25
|
+
- `/code/repositories` (GET), `/code/repositories/create` (POST) — list / create GitHub repos
|
|
26
|
+
- `/code/branches`, `/code/default-branch`, `/code/default-repo` — GitHub branch listing + defaults
|
|
26
27
|
- `/code/workspace-branch` (POST) — update workspace branch
|
|
27
28
|
- `/code/workspace-diff/[workspaceId]` — diff stats
|
|
28
29
|
- `/code/workspace-diff/[workspaceId]/full` — full unified diff
|
|
29
|
-
- `/chats` — chat list with workspace join
|
|
30
|
-
- `/chats/counts` —
|
|
30
|
+
- `/chats/list` — chat list with workspace join
|
|
31
|
+
- `/chats/counts` — sidebar badge counts (`messages`, `pull_requests`, etc.) — `messages` is per-user unread count from the `messages` table
|
|
31
32
|
- `/chat/[chatId]/data` — chat + workspace data
|
|
32
33
|
- `/chat/[chatId]/messages` — chat message history
|
|
33
|
-
- `/code/[
|
|
34
|
+
- `/code/[codeWorkspaceId]/chat-data` — chat data + chatMode by workspace (used by terminal-view + code-mode-toggle to pick per-mode storage keys)
|
|
34
35
|
- `/chat/voice-token` — AssemblyAI temporary token
|
|
35
|
-
- `/
|
|
36
|
+
- `/chat/scopes` — list of available agent scopes
|
|
37
|
+
- `/admin/app-version` (GET/POST) — current version + update check
|
|
36
38
|
- `/chat/finalize-chat` (POST) — auto-title after first message
|
|
37
39
|
|
|
38
40
|
## Chat Streaming Flow
|
|
@@ -40,14 +42,16 @@ export { getRepositoriesHandler as GET } from 'thepopebot/chat/api';
|
|
|
40
42
|
1. Client sends message via AI SDK `DefaultChatTransport` → `POST /stream/chat`
|
|
41
43
|
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
44
|
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
|
|
45
|
+
4. Streams response chunks (text deltas, tool calls, tool results, thinking blocks, errors) via `createUIMessageStream`. Tool call/tool result pairs and `{ type: 'error' }` chunks are persisted as JSON message parts.
|
|
46
|
+
5. After the stream ends successfully, if the active mode's `*_AUTO_RUN` flag is on and the workspace has uncommitted changes, `chatStream()` launches the configured workspace command (`pull`/`commit`/`push`/`pull-push`/`create-pr`) in a fresh container and yields a final `{ type: 'auto-run', command }` chunk. `api.js` writes it as a `data-auto-run` part; `chat.jsx` forwards it to WorkspaceBar so the spinner attaches.
|
|
47
|
+
6. After the first user message streams, the client calls `/chat/finalize-chat` to generate the auto-title (helper LLM with truncated-description fallback).
|
|
45
48
|
|
|
46
49
|
## Server Actions (actions.js)
|
|
47
50
|
|
|
48
|
-
Used for mutations that don't need streaming responses. Key groups:
|
|
51
|
+
Used for mutations that don't need streaming responses. Settings/config writes go through `requireAdmin()`; chat/workspace CRUD uses `requireAuth()` plus user-id ownership checks. Key groups:
|
|
49
52
|
|
|
50
53
|
- **Chat CRUD**: `renameChat()`, `deleteChat()`, `starChat()`
|
|
51
54
|
- **Coding agents**: `getCodingAgentSettings()`, `updateCodingAgentConfig()`, `setCodingAgentDefault()`
|
|
55
|
+
- **Mode defaults**: `getModeBranchDefault()`, `getModeGitActionDefault()`, `setModeDefault()` — branch/git-action/auto-run defaults per chat mode (agent vs code). `setModeDefault()` validates against `GIT_COMMAND_SET` from `lib/git-commands.js`.
|
|
52
56
|
- **Agent job secrets**: `getAgentJobSecrets()`, `updateAgentJobSecret()`, `deleteAgentJobSecretAction()`
|
|
53
57
|
- **Container management**: `getRunnersStatus()`, `stopDockerContainer()`, `startDockerContainer()`, `removeDockerContainer()`
|
|
@@ -4,26 +4,29 @@ JSX components for the chat UI. Compiled to `.js` by `npm run build` (esbuild).
|
|
|
4
4
|
|
|
5
5
|
## Admin Navigation
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
Top-level admin tabs (`settings-layout.jsx`), in order: **General → Event Handler → Crons → Triggers → Users → GitHub**. Most-used settings up front; GitHub (rarely touched once configured) is last.
|
|
8
|
+
|
|
9
|
+
Sub-tabs under `/admin/event-handler/` (pill-style nav via `SubTabLayout` in `settings-secrets-layout.jsx`):
|
|
10
|
+
- `/admin/event-handler/coding-agents` — Multi-agent config (default coding agent + per-agent enable/auth/provider/model). Section heading: "Coding Agent"; the dropdown row label is "Default Coding Agent".
|
|
11
|
+
- `/admin/event-handler/helper-llm` — Helper LLM (provider/model used for chat titles, agent-job titles + summaries)
|
|
12
|
+
- `/admin/event-handler/llms` — LLM provider credentials + custom OpenAI-compatible providers
|
|
13
|
+
- `/admin/event-handler/agent-secrets` — Custom env vars/secrets injected into agent containers (not `/jobs` — that path is gone)
|
|
14
|
+
- `/admin/event-handler/telegram` — Bot token + webhook register
|
|
15
|
+
- `/admin/event-handler/voice` — Voice input (AssemblyAI)
|
|
16
|
+
- `/admin/event-handler/webhooks` — Webhook secret
|
|
17
|
+
|
|
18
|
+
The **Clusters** sidebar entry is hidden behind `{false && ...}` for now — markup intact for fast re-enable.
|
|
18
19
|
|
|
19
20
|
## Key Component Pages
|
|
20
21
|
|
|
21
22
|
| Component | File | Purpose |
|
|
22
23
|
|-----------|------|---------|
|
|
23
|
-
| `CodingAgentsPage` | `settings-coding-agents-page.jsx` | Multi-agent config —
|
|
24
|
-
| `JobsPage` | `settings-jobs-page.jsx` | Custom env vars for agent job containers |
|
|
25
|
-
| `
|
|
24
|
+
| `CodingAgentsPage` | `settings-coding-agents-page.jsx` | Multi-agent config — 6 agent cards (claude-code, pi, codex-cli, gemini-cli, opencode, kimi-cli) with enable/disable, auth mode, model selection. Also holds Default Coding Agent + per-mode Branch/Git action/Auto-run defaults. |
|
|
25
|
+
| `JobsPage` / `JobSecretsManager` | `settings-jobs-page.jsx` | Custom env vars for agent job containers. `JobSecretsManager` is also reused as a popup from the agent-mode chat input (KeyIcon left of the Plan/Code dropdown) via `Dialog` with `maxWidth="max-w-2xl"` and `showHeader={false}`. |
|
|
26
|
+
| `MessagesPage` | `messages-page.jsx` | Per-user inbox (Inbox/All tabs, click-to-mark-read, bulk mark-all, no delete). Replaces the old global `notifications-page.jsx`. |
|
|
27
|
+
| `ContainersPage` | `containers-page.jsx` | Live Docker container monitoring via SSE (`/stream/containers`) plus a per-row logs button (FileTextIcon) that opens `/stream/containers/logs?name=...` |
|
|
26
28
|
| `ChatsPage` | `chats-page.jsx` | Full chat history with search, bulk actions |
|
|
29
|
+
| `ProfilePage` | `profile-page.jsx` | Two forms: **Profile** (firstName/lastName/nickname, no password gate) and **Login & security** (email/password, current password required) |
|
|
27
30
|
|
|
28
31
|
## Tool Display Names
|
|
29
32
|
|
|
@@ -192,8 +192,23 @@ function WorkspaceBar({
|
|
|
192
192
|
}) {
|
|
193
193
|
const [branches, setBranches] = useState([]);
|
|
194
194
|
const [loadingBranches, setLoadingBranches] = useState(false);
|
|
195
|
+
const branchesLoadedRef = useRef(false);
|
|
195
196
|
const featureBranch = workspace?.featureBranch;
|
|
196
197
|
const repoName = repo ? repo.split("/").pop() : "";
|
|
198
|
+
const branchOptions = (() => {
|
|
199
|
+
const seen = /* @__PURE__ */ new Set();
|
|
200
|
+
const out = [];
|
|
201
|
+
const push = (name) => {
|
|
202
|
+
if (!name || seen.has(name)) return;
|
|
203
|
+
seen.add(name);
|
|
204
|
+
out.push({ value: name, label: name });
|
|
205
|
+
};
|
|
206
|
+
push(branch);
|
|
207
|
+
const defaultBranch = branches.find((b) => b.isDefault)?.name;
|
|
208
|
+
push(defaultBranch);
|
|
209
|
+
for (const b of branches) push(b.name);
|
|
210
|
+
return out;
|
|
211
|
+
})();
|
|
197
212
|
return /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 text-xs min-w-0 px-1 py-0.5", children: [
|
|
198
213
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 text-muted-foreground min-w-0", children: [
|
|
199
214
|
/* @__PURE__ */ jsx(GitBranchIcon, { size: 12, className: "shrink-0" }),
|
|
@@ -203,16 +218,17 @@ function WorkspaceBar({
|
|
|
203
218
|
/* @__PURE__ */ jsx("div", { className: "min-w-0", children: /* @__PURE__ */ jsx(
|
|
204
219
|
Combobox,
|
|
205
220
|
{
|
|
206
|
-
options:
|
|
221
|
+
options: branchOptions,
|
|
207
222
|
value: branch,
|
|
208
223
|
onChange: onBranchChange,
|
|
209
224
|
loading: loadingBranches,
|
|
210
225
|
side: "top",
|
|
211
226
|
onOpen: () => {
|
|
212
|
-
if (!loadingBranches && repo) {
|
|
227
|
+
if (!loadingBranches && repo && !branchesLoadedRef.current) {
|
|
213
228
|
setLoadingBranches(true);
|
|
214
229
|
getBranches(repo).then((data) => {
|
|
215
230
|
setBranches(data || []);
|
|
231
|
+
branchesLoadedRef.current = true;
|
|
216
232
|
}).catch(() => {
|
|
217
233
|
setBranches([]);
|
|
218
234
|
}).finally(() => setLoadingBranches(false));
|
|
@@ -208,10 +208,29 @@ export function WorkspaceBar({
|
|
|
208
208
|
}) {
|
|
209
209
|
const [branches, setBranches] = useState([]);
|
|
210
210
|
const [loadingBranches, setLoadingBranches] = useState(false);
|
|
211
|
+
const branchesLoadedRef = useRef(false);
|
|
211
212
|
|
|
212
213
|
const featureBranch = workspace?.featureBranch;
|
|
213
214
|
const repoName = repo ? repo.split('/').pop() : '';
|
|
214
215
|
|
|
216
|
+
// Pin the current branch and the repo's default branch to the top of the
|
|
217
|
+
// dropdown so the user can always re-select them, even if the API list
|
|
218
|
+
// omits them (very large repos hit pagination caps; deleted branches; etc.).
|
|
219
|
+
const branchOptions = (() => {
|
|
220
|
+
const seen = new Set();
|
|
221
|
+
const out = [];
|
|
222
|
+
const push = (name) => {
|
|
223
|
+
if (!name || seen.has(name)) return;
|
|
224
|
+
seen.add(name);
|
|
225
|
+
out.push({ value: name, label: name });
|
|
226
|
+
};
|
|
227
|
+
push(branch);
|
|
228
|
+
const defaultBranch = branches.find((b) => b.isDefault)?.name;
|
|
229
|
+
push(defaultBranch);
|
|
230
|
+
for (const b of branches) push(b.name);
|
|
231
|
+
return out;
|
|
232
|
+
})();
|
|
233
|
+
|
|
215
234
|
return (
|
|
216
235
|
<div className="flex items-center gap-2 text-xs min-w-0 px-1 py-0.5">
|
|
217
236
|
<div className="flex items-center gap-1.5 text-muted-foreground min-w-0">
|
|
@@ -222,16 +241,17 @@ export function WorkspaceBar({
|
|
|
222
241
|
<span className="shrink-0 text-muted-foreground/30 hidden md:inline">/</span>
|
|
223
242
|
<div className="min-w-0">
|
|
224
243
|
<Combobox
|
|
225
|
-
options={
|
|
244
|
+
options={branchOptions}
|
|
226
245
|
value={branch}
|
|
227
246
|
onChange={onBranchChange}
|
|
228
247
|
loading={loadingBranches}
|
|
229
248
|
side="top"
|
|
230
249
|
onOpen={() => {
|
|
231
|
-
if (!loadingBranches && repo) {
|
|
250
|
+
if (!loadingBranches && repo && !branchesLoadedRef.current) {
|
|
232
251
|
setLoadingBranches(true);
|
|
233
252
|
getBranches(repo).then((data) => {
|
|
234
253
|
setBranches(data || []);
|
|
254
|
+
branchesLoadedRef.current = true;
|
|
235
255
|
}).catch(() => {
|
|
236
256
|
setBranches([]);
|
|
237
257
|
}).finally(() => setLoadingBranches(false));
|
package/lib/code/CLAUDE.md
CHANGED
|
@@ -25,7 +25,7 @@ All actions use `requireAuth()` with ownership checks: `getCodeWorkspaces()`, `c
|
|
|
25
25
|
|
|
26
26
|
## Multi-Agent Backends
|
|
27
27
|
|
|
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`.
|
|
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`. The same fallback chain is used by `lib/ai/index.js` for chat-mode streaming.
|
|
29
29
|
|
|
30
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/`.
|
|
31
31
|
|
|
@@ -33,10 +33,20 @@ Code workspaces support multiple coding agent backends. Selection is **per-works
|
|
|
33
33
|
|
|
34
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.
|
|
35
35
|
|
|
36
|
+
**USER_ID env**: Interactive containers receive `USER_ID` (originator user id) so skills like `agent-job-dm` and `agent-job-background` resolve attribution without explicit flags.
|
|
37
|
+
|
|
36
38
|
**Backend API in messages**: When an agent produces output, the `backendApi` field in message chunks identifies which agent backend generated the response.
|
|
37
39
|
|
|
40
|
+
## Workspace Commands & Auto-Run
|
|
41
|
+
|
|
42
|
+
`launchWorkspaceCommand(id, command)` and `runWorkspaceCommand(id, command)` (deprecated) spin up an ephemeral `command/<cmd>` runtime container against the workspace volume. Supported commands: `commit`, `push`, `create-pr`, `pull`, `pull-push`. Prompts are sourced from `lib/git-commands.js` (`getCommandPrompt`) — single source of truth shared with the chat dropdown, the workspace toolbar dropup, the admin defaults select, and `maybeAutoRun()` in `lib/ai/index.js`.
|
|
43
|
+
|
|
44
|
+
Per-run container names are uniquely suffixed (`command-<cmd>-<shortId>-<rand8>`) so repeated invocations don't collide. The matching SSE log endpoint (`/stream/containers/logs?name=...&cleanup=true`) removes the container on every terminal path.
|
|
45
|
+
|
|
46
|
+
The interactive workspace toolbar's git command button (`terminal-view.jsx`) reads the workspace's `chatMode` from `/code/{id}/chat-data` and uses a per-mode `localStorage` key (`thepopebot-workspace-command:agent` / `:code`) plus a per-mode `FALLBACK_BY_MODE` (`agent`→`pull-push`, `code`→`create-pr`) so agent-mode workspaces don't inherit code-mode defaults.
|
|
47
|
+
|
|
38
48
|
## Session Continuity
|
|
39
49
|
|
|
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).
|
|
50
|
+
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). Capture is gated on `CONTINUE_SESSION=1` so one-shot command containers (commit/push/create-pr/pull/pull-push) sharing a workspace with a live chat don't overwrite the chat's session file. On the next launch the entrypoint reads the saved ID and passes the agent's resume flag (`--continue`, `--resume`, `--session`, depending on agent).
|
|
41
51
|
|
|
42
52
|
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
|
@@ -11,9 +11,10 @@ Two SSE endpoints — both authenticated via `auth()` session.
|
|
|
11
11
|
|
|
12
12
|
## `logs.js` — Container Log Tail
|
|
13
13
|
|
|
14
|
-
- **Endpoint**: `/stream/containers/logs?name=<containerName>`
|
|
14
|
+
- **Endpoint**: `/stream/containers/logs?name=<containerName>&cleanup=<bool>`
|
|
15
15
|
- **Events**: raw log lines as SSE `data:` frames
|
|
16
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)
|
|
17
|
+
- **Client**: `ContainerLogsView` (used from the Containers admin page when a row's logs are expanded). Workspace command containers connect with `cleanup=true`.
|
|
18
|
+
- **`cleanup=true`**: the endpoint removes the container on every terminal path (stream error, tail-setup error, post-exit success, post-exit error). This is required for the per-run unique-named workspace command containers (`command-<cmd>-<shortId>-<rand8>`) to avoid host accumulation.
|
|
18
19
|
|
|
19
20
|
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
|
@@ -22,16 +22,27 @@ Key files: `schema.js` (source of truth), `drizzle/` (generated migrations), `dr
|
|
|
22
22
|
|
|
23
23
|
| Table | Purpose |
|
|
24
24
|
|-------|---------|
|
|
25
|
-
| `users` |
|
|
25
|
+
| `users` | User accounts (email, bcrypt password hash, role, first_name/last_name/nickname, subscribed_to_system_messages). `role` defaults to `'user'`; only `createFirstUser()` writes `'admin'`. |
|
|
26
26
|
| `chats` | Chat sessions (user_id, title, starred, chat_mode, code_workspace_id, timestamps) |
|
|
27
|
-
| `messages` |
|
|
27
|
+
| `messages` | Unified per-user inbox + chat history. `chat_id` nullable (system DMs have none); `user_id` NOT NULL; `payload`, `read`, `delivered_at` columns. Index `messages_inbox_lookup` on `(user_id, read, created_at)` drives the inbox query. |
|
|
28
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
|
-
| `
|
|
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 |
|
|
29
|
+
| `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. `getVerifiedChannels()` orders by `verified_at ASC`; the first verified row is the user's default channel. |
|
|
32
30
|
| `clusters` | Worker clusters (user_id, name, system_prompt, folders, enabled, starred) |
|
|
33
31
|
| `cluster_roles` | Role definitions scoped to a cluster (cluster_id, role_name, role, trigger_config, max_concurrency, plan_mode, cleanup_worker_dir, folders) |
|
|
34
|
-
| `settings` | Key-value configuration store (also stores API keys
|
|
32
|
+
| `settings` | Key-value configuration store (also stores API keys, OAuth tokens, custom LLM providers, and agent job secrets via type/key/value) |
|
|
33
|
+
|
|
34
|
+
The legacy `notifications` and `subscriptions` tables were dropped in migration 0025. System DMs (job completion, etc.) are now rows in the `messages` table with `chat_id = NULL`.
|
|
35
|
+
|
|
36
|
+
## System Messages (`lib/db/messages.js`)
|
|
37
|
+
|
|
38
|
+
Per-user inbox API:
|
|
39
|
+
|
|
40
|
+
- `createSystemMessage(userId, content, payload)` — write a row with `chat_id = NULL`, `read = 0`, returns the row.
|
|
41
|
+
- `getSubscribedAdminIds()` — admin user_ids where `subscribed_to_system_messages = 1`. Used for broadcast fan-out when no `user_id` is supplied to `/api/send-dm` or the GitHub PR-merge webhook.
|
|
42
|
+
- `markDelivered(id)` — stamp `delivered_at` after the channel push lands.
|
|
43
|
+
- `getMessagesForUser(userId, {scope})`, `getUnreadCountForUser(userId)`, `markMessageRead(userId, id)`, `markAllReadForUser(userId)` — UI inbox helpers.
|
|
44
|
+
|
|
45
|
+
`saveMessage(chatId, userId, role, content)` in `lib/db/chats.js` is the chat-history writer; it requires `userId` and logs+throws on missing rows.
|
|
35
46
|
|
|
36
47
|
## OAuth Token Storage
|
|
37
48
|
|
package/lib/tools/CLAUDE.md
CHANGED
|
@@ -25,11 +25,15 @@ OAuth tokens use LRU rotation via `getNextOAuthToken()` (in `lib/db/oauth-tokens
|
|
|
25
25
|
|
|
26
26
|
## create-agent-job.js — Agent Job Creation
|
|
27
27
|
|
|
28
|
-
**
|
|
28
|
+
**Signature**: `createAgentJob(description, { llmModel?, agentBackend?, scope?, userId? })`. All optional fields land in `agent-job.config.json` as `llm_model` / `agent_backend` / `scope` / `user_id`. `runAgentJobContainer()` reads them back at launch time and threads them into env vars.
|
|
29
|
+
|
|
30
|
+
**Structured output for titles**: Uses `callHelperLlmStructured({schema: z.object({title})})` to force JSON output and avoid thinking-token leaks with extended-thinking models. Two-tier fallback: LLM → truncated description → first non-empty line with markdown heading syntax stripped.
|
|
31
|
+
|
|
32
|
+
**Scope SYSTEM.md pre-render**: When `scope` is set, `createAgentJob` itself resolves the scope's `SYSTEM.md` via `buildCodingAgentSystemPrompt()` (with `{{skills}}` / `{{include}}` template resolution) and stores the rendered text under `system_prompt` in `agent-job.config.json`. Named volumes can't render host-side, so the container reads the pre-rendered prompt from the config file. Both callers (cron/trigger via `lib/actions.js` and HTTP via `api/index.js`) just forward `scope` — they do not pre-render.
|
|
29
33
|
|
|
30
34
|
**Git tree construction**: Uses GitHub's Git Data API (not REST content API) to create commits. Builds a tree with `base_tree` to preserve existing files, adding only `logs/{agentJobId}/agent-job.config.json`. This file is the single source of truth for job metadata.
|
|
31
35
|
|
|
32
|
-
**Local Docker launch**: After pushing the `agent-job/*` branch, launches a Docker container locally (fire-and-forget). Uses a named volume for workspace, cleaned up after container exits.
|
|
36
|
+
**Local Docker launch**: After pushing the `agent-job/*` branch, launches a Docker container locally (fire-and-forget). Uses a named volume for workspace, cleaned up after container exits. `runAgentJobContainer()` sets `USER_ID` from the config when present so the running agent can attribute spawned child jobs / DMs back to the originator.
|
|
33
37
|
|
|
34
38
|
## github.js — GitHub API & PAT Probing
|
|
35
39
|
|
package/lib/tools/github.js
CHANGED
|
@@ -234,18 +234,25 @@ async function listRepositories() {
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
/**
|
|
237
|
-
* List branches for a repository.
|
|
237
|
+
* List branches for a repository. Paginates up to MAX_PAGES (1000 branches)
|
|
238
|
+
* so repos with >100 branches return their full list to the dropdown.
|
|
238
239
|
* @param {string} repoFullName - e.g. "owner/repo"
|
|
239
240
|
* @returns {Promise<{name: string, isDefault: boolean}[]>}
|
|
240
241
|
*/
|
|
241
242
|
async function listBranches(repoFullName) {
|
|
242
243
|
const [owner, repo] = repoFullName.split('/');
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
]
|
|
244
|
+
const PER_PAGE = 100;
|
|
245
|
+
const MAX_PAGES = 10;
|
|
246
|
+
const repoInfoPromise = githubApi(`/repos/${owner}/${repo}`);
|
|
247
|
+
const all = [];
|
|
248
|
+
for (let page = 1; page <= MAX_PAGES; page++) {
|
|
249
|
+
const batch = await githubApi(`/repos/${owner}/${repo}/branches?per_page=${PER_PAGE}&page=${page}`);
|
|
250
|
+
all.push(...batch);
|
|
251
|
+
if (batch.length < PER_PAGE) break;
|
|
252
|
+
}
|
|
253
|
+
const repoInfo = await repoInfoPromise;
|
|
247
254
|
const defaultBranch = repoInfo.default_branch;
|
|
248
|
-
return
|
|
255
|
+
return all.map(b => ({
|
|
249
256
|
name: b.name,
|
|
250
257
|
isDefault: b.name === defaultBranch,
|
|
251
258
|
}));
|
package/package.json
CHANGED
package/setup/CLAUDE.md
CHANGED
|
@@ -19,20 +19,25 @@ Settings DB defaults to `data/db/thepopebot.sqlite` (relative to project root).
|
|
|
19
19
|
|
|
20
20
|
## Sync Target Types
|
|
21
21
|
|
|
22
|
-
Config values are synced to different targets via `lib/sync.mjs
|
|
22
|
+
Config values are synced to different targets via `lib/sync.mjs`. The mapping lives in `lib/targets.mjs` (`CONFIG_TARGETS`):
|
|
23
23
|
|
|
24
|
-
|
|
|
25
|
-
|
|
26
|
-
| `env` | `.env` file | `APP_URL`, `GH_OWNER` |
|
|
27
|
-
| `db` | `settings` table (plaintext) |
|
|
28
|
-
| `
|
|
29
|
-
| `
|
|
30
|
-
| `
|
|
24
|
+
| Flag | Storage | Example |
|
|
25
|
+
|------|---------|---------|
|
|
26
|
+
| `env: true` | `.env` file | `APP_URL`, `GH_OWNER`, `GH_REPO`, `APP_HOSTNAME` |
|
|
27
|
+
| `db: true` | `settings` table (plaintext) | `LLM_PROVIDER`, `LLM_MODEL`, `CUSTOM_OPENAI_BASE_URL`, `AGENT_BACKEND` |
|
|
28
|
+
| `dbSecret: true` | `settings` table (AES-256-GCM encrypted) | All API keys, OAuth tokens, Telegram bot token, `GH_WEBHOOK_SECRET` |
|
|
29
|
+
| `variable: true` | GitHub repo variable | `APP_URL`, `AUTO_MERGE`, `ALLOWED_PATHS`, `RUNS_ON` |
|
|
30
|
+
| `secret: true` / `secret: 'NAME'` | GitHub repo secret | `GH_TOKEN`, `GH_WEBHOOK_SECRET` |
|
|
31
|
+
| `firstRunOnly: true` | Only written on first setup | `AUTO_MERGE`, `ALLOWED_PATHS` |
|
|
31
32
|
|
|
32
|
-
A single
|
|
33
|
+
A single field can carry multiple flags (e.g., `GH_TOKEN` → `env` + `dbSecret`; `GH_WEBHOOK_SECRET` → `dbSecret` + `secret`).
|
|
34
|
+
|
|
35
|
+
GitHub-side state is intentionally minimal: only `GH_TOKEN` and `GH_WEBHOOK_SECRET` are mirrored to GitHub Secrets (consumed by CI workflows). Earlier `AGENT_*` mirrors and the `LLM_PROVIDER` / `LLM_MODEL` / `CUSTOM_OPENAI_BASE_URL` / `AGENT_BACKEND` GitHub variables were removed — agent-job containers run locally and read everything from the DB, so those mirrors had no consumers.
|
|
36
|
+
|
|
37
|
+
The fine-grained PAT must include the **Variables: Read** scope, otherwise `/admin/github/variables` shows every variable as "Not set" and surfaces the underlying API error.
|
|
33
38
|
|
|
34
39
|
## Adding New Config Fields
|
|
35
40
|
|
|
36
|
-
1. Add the field
|
|
37
|
-
2. If it needs user input, add a prompt step in `setup.mjs
|
|
38
|
-
3. Run `syncConfig()` to write to all targets
|
|
41
|
+
1. Add the field to `setup/lib/targets.mjs` `CONFIG_TARGETS` with its flags.
|
|
42
|
+
2. If it needs user input, add a prompt step in `setup.mjs`.
|
|
43
|
+
3. Run `syncConfig()` to write to all targets.
|
package/setup/lib/telegram.mjs
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
// Re-export Telegram API helpers from the package (single source of truth).
|
|
2
|
-
// The CLI setup wizard shares these helpers with the admin UI server actions.
|
|
3
|
-
export {
|
|
4
|
-
validateBotToken,
|
|
5
|
-
setTelegramWebhook,
|
|
6
|
-
getTelegramWebhookInfo,
|
|
7
|
-
deleteTelegramWebhook,
|
|
8
|
-
} from '../../lib/tools/telegram.js';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Get BotFather URL for creating a new bot
|
|
12
|
-
*/
|
|
13
|
-
export function getBotFatherURL() {
|
|
14
|
-
return 'https://t.me/BotFather';
|
|
15
|
-
}
|