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 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. Only this key type is allowed to call `/api/get-agent-job-secret` (the route rejects other types).
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()` session |
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
- | 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). |
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/telegram/register` | `x-api-key` | Register bot token + webhook URL |
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, can be overridden by /telegram/register
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 `scope` 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.
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
- * Ensure workspace directory exists and contains the git repo
19
- * on the correct branch. Idempotent safe to call on every message.
18
+ * Set up the workspace once. If `.git` already exists, returns immediately
19
+ * and never touches gitgit is the source of truth from then on.
20
20
  *
21
- * Replaces Docker entrypoint scripts: setup-git.sh, clone.sh, feature-branch.sh.
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/checkout
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
- // 3. Clone if not already a git repo
47
- const hasGit = existsSync(path.join(workspaceDir, '.git'));
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
- // 3. Git identity (only if not already configured)
57
- try {
58
- await run('git', ['config', 'user.name'], execOpts);
59
- } catch {
60
- // Not configured derive from GitHub token
61
- if (ghToken) {
62
- try {
63
- const userJson = await run('gh', ['api', 'user', '-q', '{name: .name, login: .login, email: .email, id: .id}'], execOpts);
64
- const user = JSON.parse(userJson);
65
- const name = user.name || user.login;
66
- const email = user.email || `${user.id}+${user.login}@users.noreply.github.com`;
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
- try {
96
- const remoteCheck = await run('git', ['ls-remote', '--heads', 'origin', featureBranch], execOpts);
97
- if (remoteCheck) {
98
- // Remote branch exists — checkout tracking it
99
- await run('git', ['checkout', '-B', featureBranch, `origin/${featureBranch}`], execOpts);
100
- log.push(`Checked out ${featureBranch} (tracking origin)`);
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');
@@ -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>`.
@@ -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 (commit 740c734 / d9bf19a) inserts intermediate "→ used X" lines as tool calls land.
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
 
@@ -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`, `/code/branches`, `/code/default-repo` — GitHub repo/branch listing
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` — notification + PR badge 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/[workspaceId]/chat-data` — chat data by workspace
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
- - `/admin/app-version` — version + update check
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 first user message streams, the client calls `/chat/finalize-chat` to generate the auto-title (helper LLM with truncated-description fallback).
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
- Admin pages live under `/admin/` with two top-level sections:
8
-
9
- - **`/admin/event-handler/`** — Event handler config with pill-style sub-tabs via `SubTabLayout` (`settings-secrets-layout.jsx`):
10
- - `/admin/event-handler/llms` — LLM provider credentials
11
- - `/admin/event-handler/coding-agents` — Multi-agent config (5 backends)
12
- - `/admin/event-handler/helper-llm` — Helper LLM settings (provider/model used for chat titles, agent-job titles + summaries)
13
- - `/admin/event-handler/jobs` — Agent job custom secrets
14
- - `/admin/event-handler/telegram` — Telegram integration
15
- - `/admin/event-handler/voice` — Voice input (AssemblyAI)
16
- - `/admin/event-handler/webhooks` — Webhook secret
17
- - **Other admin pages**: `/admin/api-keys`, `/admin/github`, `/admin/users`, `/admin/crons`, `/admin/triggers`, `/admin/general`
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 — 5 agent cards with enable/disable, auth mode, model selection |
24
- | `JobsPage` | `settings-jobs-page.jsx` | Custom env vars for agent job containers |
25
- | `ContainersPage` | `containers-page.jsx` | Live Docker container monitoring via SSE (`/stream/containers`) |
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: branches.map((b) => ({ value: b.name, label: b.name })),
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={branches.map((b) => ({ value: b.name, label: b.name }))}
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));
@@ -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`. 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.
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.
@@ -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` | Admin accounts (email, bcrypt password hash, role) |
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` | Chat messages (chat_id, role, content) |
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
- | `notifications` | Job completion notifications (notification text, payload, read status) |
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 and OAuth tokens via type/key/value) |
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
 
@@ -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
- **Structured output for titles**: Uses `model.withStructuredOutput(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.
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
 
@@ -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 [branches, repoInfo] = await Promise.all([
244
- githubApi(`/repos/${owner}/${repo}/branches?per_page=100`),
245
- githubApi(`/repos/${owner}/${repo}`),
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 branches.map(b => ({
255
+ return all.map(b => ({
249
256
  name: b.name,
250
257
  isDefault: b.name === defaultBranch,
251
258
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thepopebot",
3
- "version": "1.2.76-beta.36",
3
+ "version": "1.2.76-beta.38",
4
4
  "type": "module",
5
5
  "description": "Create autonomous AI agents with a two-layer architecture: Next.js Event Handler + Docker Agent.",
6
6
  "bin": {
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
- | Target | Storage | Example |
25
- |--------|---------|---------|
26
- | `env` | `.env` file | `APP_URL`, `GH_OWNER` |
27
- | `db` | `settings` table (plaintext) | Non-secret config |
28
- | `db_secret` | `settings` table (encrypted) | `GH_TOKEN` |
29
- | `github_secret` | GitHub repo secret | `GH_TOKEN`, `WEBHOOK_SECRET` |
30
- | `github_variable` | GitHub repo variable | `LLM_PROVIDER`, `LLM_MODEL` |
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 config field can sync to multiple targets (e.g., `GH_TOKEN` → `db_secret` + `github_secret` + `env`).
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 definition to the sync config map in `lib/sync.mjs` with its target(s)
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.
@@ -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
- }