thepopebot 1.2.73 → 1.2.74-beta.10
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/index.js +5 -4
- package/bin/cli.js +6 -30
- package/bin/docker-build.js +11 -11
- package/bin/managed-paths.js +1 -2
- package/bin/sync.js +23 -23
- package/config/instrumentation.js +8 -0
- package/lib/ai/CLAUDE.md +3 -1
- package/lib/ai/model.js +42 -29
- package/lib/ai/web-search.js +4 -2
- package/lib/auth/actions.js +173 -1
- package/lib/auth/middleware.js +5 -0
- package/lib/chat/actions.js +427 -13
- package/lib/chat/components/icons.js +21 -0
- package/lib/chat/components/icons.jsx +19 -0
- package/lib/chat/components/index.js +7 -1
- package/lib/chat/components/profile-page.js +141 -0
- package/lib/chat/components/profile-page.jsx +168 -0
- package/lib/chat/components/settings-chat-page.js +709 -0
- package/lib/chat/components/settings-chat-page.jsx +757 -0
- package/lib/chat/components/settings-general-page.js +56 -0
- package/lib/chat/components/settings-general-page.jsx +69 -0
- package/lib/chat/components/settings-github-page.js +760 -0
- package/lib/chat/components/settings-github-page.jsx +793 -0
- package/lib/chat/components/settings-layout.js +9 -5
- package/lib/chat/components/settings-layout.jsx +9 -5
- package/lib/chat/components/settings-secrets-layout.js +56 -0
- package/lib/chat/components/settings-secrets-layout.jsx +77 -0
- package/lib/chat/components/settings-secrets-page.js +420 -114
- package/lib/chat/components/settings-secrets-page.jsx +438 -132
- package/lib/chat/components/settings-users-page.js +409 -0
- package/lib/chat/components/settings-users-page.jsx +406 -0
- package/lib/chat/components/sidebar-user-nav.js +8 -4
- package/lib/chat/components/sidebar-user-nav.jsx +13 -5
- package/lib/chat/components/ui/dropdown-menu.js +1 -1
- package/lib/chat/components/ui/dropdown-menu.jsx +1 -1
- package/lib/chat/components/ui/sidebar.js +1 -1
- package/lib/chat/components/ui/sidebar.jsx +1 -1
- package/lib/cluster/actions.js +6 -7
- package/lib/cluster/components/cluster-console-page.js +53 -34
- package/lib/cluster/components/cluster-console-page.jsx +53 -41
- package/lib/cluster/execute.js +43 -18
- package/lib/cluster/runtime.js +15 -19
- package/lib/code/actions.js +16 -7
- package/lib/code/code-page.js +206 -57
- package/lib/code/code-page.jsx +228 -60
- package/lib/code/ws-proxy.js +12 -4
- package/lib/config.js +105 -0
- package/lib/cron.js +56 -20
- package/lib/db/api-keys.js +73 -50
- package/lib/db/config.js +306 -0
- package/lib/db/crypto.js +59 -0
- package/lib/db/notifications.js +2 -1
- package/lib/db/users.js +88 -0
- package/lib/github-api.js +169 -0
- package/lib/llm-providers.js +73 -0
- package/lib/tools/docker.js +13 -8
- package/lib/tools/github.js +6 -4
- package/lib/tools/openai.js +4 -2
- package/lib/voice/actions.js +2 -1
- package/package.json +2 -1
- package/setup/lib/sync.mjs +41 -4
- package/setup/lib/targets.mjs +24 -22
- package/setup/setup.mjs +86 -23
- package/templates/.github/workflows/rebuild-event-handler.yml +20 -63
- package/templates/.github/workflows/upgrade-event-handler.yml +1 -1
- package/templates/.gitignore.template +4 -6
- package/templates/CLAUDE.md +5 -6
- package/templates/CLAUDE.md.template +12 -14
- package/templates/docker-compose.custom.yml +119 -0
- package/templates/docker-compose.yml +10 -3
- package/templates/traefik-dynamic.yml.example +7 -0
- package/templates/app/api/[...thepopebot]/route.js +0 -1
- package/templates/app/api/auth/[...nextauth]/route.js +0 -1
- package/templates/app/chat/[chatId]/page.js +0 -8
- package/templates/app/chat/finalize-chat/route.js +0 -1
- package/templates/app/chats/page.js +0 -7
- package/templates/app/cluster/[clusterId]/console/page.js +0 -8
- package/templates/app/cluster/[clusterId]/logs/page.js +0 -8
- package/templates/app/cluster/[clusterId]/page.js +0 -8
- package/templates/app/cluster/[clusterId]/role/[roleId]/page.js +0 -8
- package/templates/app/clusters/layout.js +0 -7
- package/templates/app/clusters/list/page.js +0 -5
- package/templates/app/clusters/page.js +0 -5
- package/templates/app/code/[codeWorkspaceId]/page.js +0 -8
- package/templates/app/crons/page.js +0 -5
- package/templates/app/globals.css +0 -114
- package/templates/app/icon.svg +0 -12
- package/templates/app/layout.js +0 -34
- package/templates/app/login/page.js +0 -13
- package/templates/app/notifications/page.js +0 -7
- package/templates/app/page.js +0 -7
- package/templates/app/pull-requests/page.js +0 -7
- package/templates/app/runners/page.js +0 -7
- package/templates/app/settings/crons/page.js +0 -5
- package/templates/app/settings/layout.js +0 -7
- package/templates/app/settings/page.js +0 -5
- package/templates/app/settings/secrets/page.js +0 -5
- package/templates/app/settings/triggers/page.js +0 -5
- package/templates/app/stream/chat/route.js +0 -1
- package/templates/app/stream/cluster/[clusterId]/logs/route.js +0 -1
- package/templates/app/triggers/page.js +0 -5
- package/templates/docker/CLAUDE.md +0 -25
- package/templates/docker/claude-code-cluster-worker/Dockerfile +0 -49
- package/templates/docker/claude-code-cluster-worker/entrypoint.sh +0 -77
- package/templates/docker/claude-code-headless/Dockerfile +0 -55
- package/templates/docker/claude-code-headless/commands/ai-merge-back.md +0 -103
- package/templates/docker/claude-code-headless/commands/commit-changes.md +0 -14
- package/templates/docker/claude-code-headless/entrypoint.sh +0 -91
- package/templates/docker/claude-code-job/Dockerfile +0 -34
- package/templates/docker/claude-code-job/entrypoint.sh +0 -149
- package/templates/docker/claude-code-workspace/.tmux.conf +0 -5
- package/templates/docker/claude-code-workspace/Dockerfile +0 -64
- package/templates/docker/claude-code-workspace/commands/ai-merge-back.md +0 -103
- package/templates/docker/claude-code-workspace/commands/commit-changes.md +0 -14
- package/templates/docker/claude-code-workspace/entrypoint.sh +0 -101
- package/templates/docker/event-handler/Dockerfile +0 -37
- package/templates/docker/event-handler/ecosystem.config.cjs +0 -7
- package/templates/docker/pi-coding-agent-job/Dockerfile +0 -51
- package/templates/docker/pi-coding-agent-job/entrypoint.sh +0 -164
- package/templates/instrumentation.js +0 -6
- package/templates/middleware.js +0 -1
- package/templates/next.config.mjs +0 -3
- package/templates/postcss.config.mjs +0 -5
- package/templates/server.js +0 -25
- package/templates/theme.css +0 -5
package/api/index.js
CHANGED
|
@@ -7,8 +7,9 @@ import { chat, summarizeJob } from '../lib/ai/index.js';
|
|
|
7
7
|
import { createNotification } from '../lib/db/notifications.js';
|
|
8
8
|
import { loadTriggers } from '../lib/triggers.js';
|
|
9
9
|
import { verifyApiKey } from '../lib/db/api-keys.js';
|
|
10
|
+
import { getConfig } from '../lib/config.js';
|
|
10
11
|
|
|
11
|
-
// Bot token from env, can be overridden by /telegram/register
|
|
12
|
+
// Bot token — resolved from DB/env, can be overridden by /telegram/register
|
|
12
13
|
let telegramBotToken = null;
|
|
13
14
|
|
|
14
15
|
// Cached trigger firing function (initialized on first request)
|
|
@@ -16,7 +17,7 @@ let _fireTriggers = null;
|
|
|
16
17
|
|
|
17
18
|
function getTelegramBotToken() {
|
|
18
19
|
if (!telegramBotToken) {
|
|
19
|
-
telegramBotToken =
|
|
20
|
+
telegramBotToken = getConfig('TELEGRAM_BOT_TOKEN') || null;
|
|
20
21
|
}
|
|
21
22
|
return telegramBotToken;
|
|
22
23
|
}
|
|
@@ -103,7 +104,7 @@ async function handleTelegramRegister(request) {
|
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
try {
|
|
106
|
-
const result = await setWebhook(bot_token, webhook_url,
|
|
107
|
+
const result = await setWebhook(bot_token, webhook_url, getConfig('TELEGRAM_WEBHOOK_SECRET'));
|
|
107
108
|
telegramBotToken = bot_token;
|
|
108
109
|
return Response.json({ success: true, result });
|
|
109
110
|
} catch (err) {
|
|
@@ -159,7 +160,7 @@ async function processChannelMessage(adapter, normalized) {
|
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
async function handleGithubWebhook(request) {
|
|
162
|
-
const
|
|
163
|
+
const GH_WEBHOOK_SECRET = getConfig('GH_WEBHOOK_SECRET');
|
|
163
164
|
|
|
164
165
|
// Validate webhook secret (timing-safe, required)
|
|
165
166
|
if (!GH_WEBHOOK_SECRET || !safeCompare(request.headers.get('x-github-webhook-secret-token'), GH_WEBHOOK_SECRET)) {
|
package/bin/cli.js
CHANGED
|
@@ -248,22 +248,12 @@ async function init() {
|
|
|
248
248
|
name: dirName,
|
|
249
249
|
private: true,
|
|
250
250
|
scripts: {
|
|
251
|
-
dev: 'next dev --turbopack',
|
|
252
|
-
build: 'next build',
|
|
253
|
-
start: 'next start',
|
|
254
251
|
setup: 'thepopebot setup',
|
|
255
252
|
'setup-telegram': 'thepopebot setup-telegram',
|
|
256
253
|
'reset-auth': 'thepopebot reset-auth',
|
|
257
254
|
},
|
|
258
255
|
dependencies: {
|
|
259
256
|
thepopebot: thepopebotDep,
|
|
260
|
-
next: '^15.5.12',
|
|
261
|
-
'next-auth': '5.0.0-beta.30',
|
|
262
|
-
'next-themes': '^0.4.0',
|
|
263
|
-
react: '^19.0.0',
|
|
264
|
-
'react-dom': '^19.0.0',
|
|
265
|
-
tailwindcss: '^4.0.0',
|
|
266
|
-
'@tailwindcss/postcss': '^4.0.0',
|
|
267
257
|
},
|
|
268
258
|
};
|
|
269
259
|
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
@@ -354,6 +344,10 @@ async function init() {
|
|
|
354
344
|
AUTH_SECRET=${authSecret}
|
|
355
345
|
AUTH_TRUST_HOST=true
|
|
356
346
|
THEPOPEBOT_VERSION=${version}
|
|
347
|
+
|
|
348
|
+
# Uncomment to use a custom docker-compose file that won't be overwritten by upgrades.
|
|
349
|
+
# Edit docker-compose.custom.yml with your changes, then uncomment:
|
|
350
|
+
# COMPOSE_FILE=docker-compose.custom.yml
|
|
357
351
|
`;
|
|
358
352
|
fs.writeFileSync(envPath, seedEnv);
|
|
359
353
|
console.log(` Created .env (AUTH_SECRET, THEPOPEBOT_VERSION=${version})`);
|
|
@@ -618,24 +612,6 @@ async function upgrade() {
|
|
|
618
612
|
process.exit(1);
|
|
619
613
|
}
|
|
620
614
|
|
|
621
|
-
// --- Clear .next ---
|
|
622
|
-
try {
|
|
623
|
-
fs.rmSync(path.join(cwd, '.next'), { recursive: true, force: true });
|
|
624
|
-
} catch {}
|
|
625
|
-
|
|
626
|
-
// --- Build ---
|
|
627
|
-
console.log('\n Building...\n');
|
|
628
|
-
try {
|
|
629
|
-
execSync('npm run build', { stdio: 'inherit', cwd });
|
|
630
|
-
} catch {
|
|
631
|
-
console.error('\n Build failed. The upgrade has been applied but the project does not build.');
|
|
632
|
-
console.error(' Fix the build errors, then run:\n');
|
|
633
|
-
console.error(` npm run build`);
|
|
634
|
-
console.error(` git add -A && git commit -m "upgrade thepopebot to ${targetVersion}"`);
|
|
635
|
-
console.error(' git push\n');
|
|
636
|
-
process.exit(1);
|
|
637
|
-
}
|
|
638
|
-
|
|
639
615
|
// --- Commit upgrade ---
|
|
640
616
|
const changes = execSync('git status --porcelain', { encoding: 'utf8', cwd }).trim();
|
|
641
617
|
if (changes) {
|
|
@@ -664,8 +640,8 @@ async function upgrade() {
|
|
|
664
640
|
try {
|
|
665
641
|
const running = execSync('docker compose ps --status running -q', { encoding: 'utf8', cwd }).trim();
|
|
666
642
|
if (running) {
|
|
667
|
-
console.log('
|
|
668
|
-
execSync('docker compose
|
|
643
|
+
console.log(' Pulling new image and restarting Docker containers...\n');
|
|
644
|
+
execSync('docker compose pull event-handler && docker compose up -d --force-recreate event-handler', { stdio: 'inherit', cwd });
|
|
669
645
|
}
|
|
670
646
|
} catch {
|
|
671
647
|
// Docker not available or not running — skip
|
package/bin/docker-build.js
CHANGED
|
@@ -26,33 +26,33 @@ const REPO = 'stephengpope/thepopebot';
|
|
|
26
26
|
const IMAGES = [
|
|
27
27
|
{
|
|
28
28
|
name: 'pi-coding-agent-job',
|
|
29
|
-
context: '
|
|
30
|
-
dockerfile: '
|
|
29
|
+
context: 'docker/pi-coding-agent-job',
|
|
30
|
+
dockerfile: 'docker/pi-coding-agent-job/Dockerfile',
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
name: 'claude-code-job',
|
|
34
|
-
context: '
|
|
35
|
-
dockerfile: '
|
|
34
|
+
context: 'docker/claude-code-job',
|
|
35
|
+
dockerfile: 'docker/claude-code-job/Dockerfile',
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
name: 'claude-code-workspace',
|
|
39
|
-
context: '
|
|
40
|
-
dockerfile: '
|
|
39
|
+
context: 'docker/claude-code-workspace',
|
|
40
|
+
dockerfile: 'docker/claude-code-workspace/Dockerfile',
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
name: 'claude-code-headless',
|
|
44
|
-
context: '
|
|
45
|
-
dockerfile: '
|
|
44
|
+
context: 'docker/claude-code-headless',
|
|
45
|
+
dockerfile: 'docker/claude-code-headless/Dockerfile',
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
name: 'claude-code-cluster-worker',
|
|
49
|
-
context: '
|
|
50
|
-
dockerfile: '
|
|
49
|
+
context: 'docker/claude-code-cluster-worker',
|
|
50
|
+
dockerfile: 'docker/claude-code-cluster-worker/Dockerfile',
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
53
|
name: 'event-handler',
|
|
54
54
|
context: '.',
|
|
55
|
-
dockerfile: '
|
|
55
|
+
dockerfile: 'docker/event-handler/Dockerfile',
|
|
56
56
|
},
|
|
57
57
|
];
|
|
58
58
|
|
package/bin/managed-paths.js
CHANGED
|
@@ -4,11 +4,10 @@
|
|
|
4
4
|
// Paths ending with '/' are directories (all contents are managed).
|
|
5
5
|
export const MANAGED_PATHS = [
|
|
6
6
|
'.github/workflows/',
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
'docker-compose.yml',
|
|
9
9
|
'.dockerignore',
|
|
10
10
|
'CLAUDE.md',
|
|
11
|
-
'app/',
|
|
12
11
|
];
|
|
13
12
|
|
|
14
13
|
export function isManaged(relPath) {
|
package/bin/sync.js
CHANGED
|
@@ -29,10 +29,9 @@
|
|
|
29
29
|
* 2. npm pack → copy tarball to project
|
|
30
30
|
* 3. mirrorTemplates() — overwrite + delete stale
|
|
31
31
|
* 4. npm install tarball on host (--no-save)
|
|
32
|
-
* 5. Next.js build
|
|
33
|
-
* 6.
|
|
34
|
-
* 7.
|
|
35
|
-
* 8. Cleanup tarball
|
|
32
|
+
* 5. Docker image build (patches Dockerfile for local tarball, includes Next.js build)
|
|
33
|
+
* 6. docker compose up -d -V event-handler
|
|
34
|
+
* 7. Cleanup tarball
|
|
36
35
|
*/
|
|
37
36
|
import { execSync } from 'child_process';
|
|
38
37
|
import fs from 'fs';
|
|
@@ -158,7 +157,7 @@ function mirrorTemplates(projectPath) {
|
|
|
158
157
|
function buildDockerImage(projectPath) {
|
|
159
158
|
console.log('\n Building Docker event handler image...');
|
|
160
159
|
|
|
161
|
-
const dockerfilePath = path.join(
|
|
160
|
+
const dockerfilePath = path.join(PACKAGE_DIR, 'docker', 'event-handler', 'Dockerfile');
|
|
162
161
|
let dockerfile = fs.readFileSync(dockerfilePath, 'utf8');
|
|
163
162
|
|
|
164
163
|
// Add COPY for tarball after the package.json COPY line in builder stage
|
|
@@ -169,24 +168,30 @@ function buildDockerImage(projectPath) {
|
|
|
169
168
|
|
|
170
169
|
// Replace npm install from registry with local tarball install
|
|
171
170
|
dockerfile = dockerfile.replace(
|
|
172
|
-
/RUN
|
|
173
|
-
'RUN
|
|
171
|
+
/RUN TPB_VERSION=.*\n\s+echo.*\n\s+npm install --no-save "thepopebot@\$\{TPB_VERSION\}" tailwindcss @tailwindcss\/postcss/,
|
|
172
|
+
'RUN echo \'{"private":true}\' > package.json && \\\n npm install --no-save /tmp/thepopebot.tgz tailwindcss @tailwindcss/postcss && \\\n rm /tmp/thepopebot.tgz'
|
|
174
173
|
);
|
|
175
174
|
|
|
176
|
-
// Fix template paths for project context (templates/docker/... → docker/...)
|
|
177
|
-
dockerfile = dockerfile.replace(/COPY templates\//g, 'COPY ');
|
|
178
|
-
|
|
179
175
|
// Read version from package.json
|
|
180
176
|
const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_DIR, 'package.json'), 'utf8'));
|
|
181
177
|
const version = pkg.version;
|
|
182
178
|
const imageTag = `stephengpope/thepopebot:event-handler-${version}`;
|
|
183
179
|
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
180
|
+
// Copy web/ to project for Docker build context
|
|
181
|
+
const webSrc = path.join(PACKAGE_DIR, 'web');
|
|
182
|
+
const webDest = path.join(projectPath, 'web');
|
|
183
|
+
fs.cpSync(webSrc, webDest, { recursive: true });
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
// Build using stdin Dockerfile with project dir as context (no cache to ensure fresh package)
|
|
187
|
+
execSync(`docker build --no-cache -f - -t ${imageTag} .`, {
|
|
188
|
+
input: dockerfile,
|
|
189
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
190
|
+
cwd: projectPath,
|
|
191
|
+
});
|
|
192
|
+
} finally {
|
|
193
|
+
fs.rmSync(webDest, { recursive: true, force: true });
|
|
194
|
+
}
|
|
190
195
|
|
|
191
196
|
// Update THEPOPEBOT_VERSION in .env
|
|
192
197
|
const envPath = path.join(projectPath, '.env');
|
|
@@ -239,15 +244,10 @@ export async function sync(projectPath) {
|
|
|
239
244
|
console.log('\n Installing package on host...');
|
|
240
245
|
execSync(`npm install --no-save ${tarballDest}`, { stdio: 'inherit', cwd: projectPath });
|
|
241
246
|
|
|
242
|
-
// 5. Build
|
|
243
|
-
console.log('\n Building Next.js...');
|
|
244
|
-
fs.rmSync(path.join(projectPath, '.next'), { recursive: true, force: true });
|
|
245
|
-
execSync('npm run build', { stdio: 'inherit', cwd: projectPath });
|
|
246
|
-
|
|
247
|
-
// 6. Build Docker image with patched Dockerfile
|
|
247
|
+
// 5. Build Docker image with patched Dockerfile (includes Next.js build)
|
|
248
248
|
buildDockerImage(projectPath);
|
|
249
249
|
|
|
250
|
-
//
|
|
250
|
+
// 6. Restart container with new image
|
|
251
251
|
console.log('\n Restarting event handler...');
|
|
252
252
|
execSync('docker compose up -d -V event-handler', { stdio: 'inherit', cwd: projectPath });
|
|
253
253
|
|
|
@@ -43,6 +43,14 @@ export async function register() {
|
|
|
43
43
|
const { initDatabase } = await import('../lib/db/index.js');
|
|
44
44
|
initDatabase();
|
|
45
45
|
|
|
46
|
+
// Migrate env vars to DB on first run (idempotent)
|
|
47
|
+
try {
|
|
48
|
+
const { migrateEnvToDb } = await import('../lib/db/config.js');
|
|
49
|
+
migrateEnvToDb();
|
|
50
|
+
} catch (err) {
|
|
51
|
+
console.warn('Config migration:', err.message);
|
|
52
|
+
}
|
|
53
|
+
|
|
46
54
|
// Start cron scheduler
|
|
47
55
|
const { loadCrons } = await import('../lib/cron.js');
|
|
48
56
|
loadCrons();
|
package/lib/ai/CLAUDE.md
CHANGED
|
@@ -30,11 +30,13 @@ Two agent types, both using `createReactAgent` from `@langchain/langgraph/prebui
|
|
|
30
30
|
|----------|----------------|---------------|-------------|
|
|
31
31
|
| Anthropic | `anthropic` (default) | `claude-sonnet-4-20250514` | `ANTHROPIC_API_KEY` |
|
|
32
32
|
| OpenAI | `openai` | `gpt-4o` | `OPENAI_API_KEY` |
|
|
33
|
-
| Google | `google` | `gemini-2.5-
|
|
33
|
+
| Google | `google` | `gemini-2.5-flash` | `GOOGLE_API_KEY` |
|
|
34
34
|
| Custom | `custom` | — | `OPENAI_BASE_URL`, `CUSTOM_API_KEY` (optional) |
|
|
35
35
|
|
|
36
36
|
`LLM_MAX_TOKENS` defaults to 4096. Web search available for `anthropic` and `openai` providers only (disable with `WEB_SEARCH=false`).
|
|
37
37
|
|
|
38
|
+
> **Google model compatibility note:** `gemini-2.5-pro` and all `gemini-3.*` models require `thought_signature` round-tripping that `@langchain/google-genai` does not yet support. Setting `LLM_MODEL` to one of these will automatically fall back to `gemini-2.5-flash` at runtime with a warning. Supported Gemini models: `gemini-2.5-flash` (default), `gemini-2.5-flash-lite`. Full support for thinking models is tracked in issue #201.
|
|
39
|
+
|
|
38
40
|
## Chat Streaming
|
|
39
41
|
|
|
40
42
|
`chatStream()` in `index.js` yields chunks: `{ type: 'text', content }`, `{ type: 'tool-call', name, args }`, `{ type: 'tool-result', name, result }`. Called by `lib/chat/api.js` (the `/stream/chat` endpoint).
|
package/lib/ai/model.js
CHANGED
|
@@ -1,36 +1,43 @@
|
|
|
1
1
|
import { ChatAnthropic } from '@langchain/anthropic';
|
|
2
|
+
import { getConfig } from '../config.js';
|
|
3
|
+
import { BUILTIN_PROVIDERS } from '../llm-providers.js';
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
};
|
|
5
|
+
// These models require thought_signature round-tripping which @langchain/google-genai doesn't support.
|
|
6
|
+
// Auto-replace with gemini-2.5-flash until we migrate to @langchain/google (see issue #201).
|
|
7
|
+
const GEMINI_UNSUPPORTED_MODELS = ['gemini-2.5-pro', 'gemini-3'];
|
|
8
|
+
const GEMINI_FALLBACK = 'gemini-2.5-flash';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
|
-
* Create a LangChain chat model based on
|
|
11
|
-
*
|
|
12
|
-
* Config env vars:
|
|
13
|
-
* LLM_PROVIDER — "anthropic" (default), "openai", "google"
|
|
14
|
-
* LLM_MODEL — Model name override (e.g. "claude-sonnet-4-20250514")
|
|
15
|
-
* ANTHROPIC_API_KEY — Required for anthropic provider
|
|
16
|
-
* OPENAI_API_KEY — Required for openai provider (optional with OPENAI_BASE_URL)
|
|
17
|
-
* OPENAI_BASE_URL — Custom OpenAI-compatible base URL (e.g. http://localhost:11434/v1 for Ollama)
|
|
18
|
-
* GOOGLE_API_KEY — Required for google provider
|
|
11
|
+
* Create a LangChain chat model based on DB/env configuration.
|
|
19
12
|
*
|
|
20
13
|
* @param {object} [options]
|
|
21
|
-
* @param {number} [options.maxTokens
|
|
14
|
+
* @param {number} [options.maxTokens] - Max tokens for the response
|
|
22
15
|
* @returns {import('@langchain/core/language_models/chat_models').BaseChatModel}
|
|
23
16
|
*/
|
|
24
17
|
export async function createModel(options = {}) {
|
|
25
|
-
const provider =
|
|
26
|
-
const modelName =
|
|
27
|
-
const maxTokens = options.maxTokens || Number(
|
|
18
|
+
const provider = getConfig('LLM_PROVIDER');
|
|
19
|
+
const modelName = getConfig('LLM_MODEL');
|
|
20
|
+
const maxTokens = options.maxTokens || Number(getConfig('LLM_MAX_TOKENS')) || 4096;
|
|
21
|
+
|
|
22
|
+
// Custom provider (not in BUILTIN_PROVIDERS) → OpenAI-compatible
|
|
23
|
+
if (!BUILTIN_PROVIDERS[provider]) {
|
|
24
|
+
const { ChatOpenAI } = await import('@langchain/openai');
|
|
25
|
+
const { getCustomProvider } = await import('../db/config.js');
|
|
26
|
+
const custom = getCustomProvider(provider);
|
|
27
|
+
if (!custom) throw new Error(`Unknown LLM provider: ${provider}`);
|
|
28
|
+
const config = { modelName: custom.model || modelName, maxTokens };
|
|
29
|
+
config.apiKey = custom.apiKey || 'not-needed';
|
|
30
|
+
if (custom.baseUrl) {
|
|
31
|
+
config.configuration = { baseURL: custom.baseUrl };
|
|
32
|
+
}
|
|
33
|
+
return new ChatOpenAI(config);
|
|
34
|
+
}
|
|
28
35
|
|
|
29
36
|
switch (provider) {
|
|
30
37
|
case 'anthropic': {
|
|
31
|
-
const apiKey =
|
|
38
|
+
const apiKey = getConfig('ANTHROPIC_API_KEY');
|
|
32
39
|
if (!apiKey) {
|
|
33
|
-
throw new Error('ANTHROPIC_API_KEY
|
|
40
|
+
throw new Error('ANTHROPIC_API_KEY is required — set it on the Settings > Chat page');
|
|
34
41
|
}
|
|
35
42
|
return new ChatAnthropic({
|
|
36
43
|
modelName,
|
|
@@ -38,15 +45,12 @@ export async function createModel(options = {}) {
|
|
|
38
45
|
anthropicApiKey: apiKey,
|
|
39
46
|
});
|
|
40
47
|
}
|
|
41
|
-
case 'custom':
|
|
42
48
|
case 'openai': {
|
|
43
49
|
const { ChatOpenAI } = await import('@langchain/openai');
|
|
44
|
-
const apiKey =
|
|
45
|
-
|
|
46
|
-
: process.env.OPENAI_API_KEY;
|
|
47
|
-
const baseURL = process.env.OPENAI_BASE_URL;
|
|
50
|
+
const apiKey = getConfig('OPENAI_API_KEY');
|
|
51
|
+
const baseURL = getConfig('OPENAI_BASE_URL');
|
|
48
52
|
if (!apiKey && !baseURL) {
|
|
49
|
-
throw new Error('OPENAI_API_KEY
|
|
53
|
+
throw new Error('OPENAI_API_KEY is required — set it on the Settings > Chat page');
|
|
50
54
|
}
|
|
51
55
|
const config = { modelName, maxTokens };
|
|
52
56
|
config.apiKey = apiKey || 'not-needed';
|
|
@@ -57,12 +61,21 @@ export async function createModel(options = {}) {
|
|
|
57
61
|
}
|
|
58
62
|
case 'google': {
|
|
59
63
|
const { ChatGoogleGenerativeAI } = await import('@langchain/google-genai');
|
|
60
|
-
const apiKey =
|
|
64
|
+
const apiKey = getConfig('GOOGLE_API_KEY');
|
|
61
65
|
if (!apiKey) {
|
|
62
|
-
throw new Error('GOOGLE_API_KEY
|
|
66
|
+
throw new Error('GOOGLE_API_KEY is required — set it on the Settings > Chat page');
|
|
67
|
+
}
|
|
68
|
+
let resolvedModel = modelName;
|
|
69
|
+
const isUnsupported = GEMINI_UNSUPPORTED_MODELS.some(m => resolvedModel.startsWith(m));
|
|
70
|
+
if (isUnsupported) {
|
|
71
|
+
console.warn(
|
|
72
|
+
`[model] ${resolvedModel} requires thought_signature support not yet available in @langchain/google-genai. ` +
|
|
73
|
+
`Falling back to ${GEMINI_FALLBACK}. See https://github.com/stephengpope/thepopebot/issues/201.`
|
|
74
|
+
);
|
|
75
|
+
resolvedModel = GEMINI_FALLBACK;
|
|
63
76
|
}
|
|
64
77
|
return new ChatGoogleGenerativeAI({
|
|
65
|
-
model:
|
|
78
|
+
model: resolvedModel,
|
|
66
79
|
maxOutputTokens: maxTokens,
|
|
67
80
|
apiKey,
|
|
68
81
|
});
|
package/lib/ai/web-search.js
CHANGED
|
@@ -4,12 +4,14 @@
|
|
|
4
4
|
* runs the search and the model's synthesized answer streams normally.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { getConfig } from '../config.js';
|
|
8
|
+
|
|
7
9
|
export function getProvider() {
|
|
8
|
-
return
|
|
10
|
+
return getConfig('LLM_PROVIDER');
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export function isWebSearchAvailable() {
|
|
12
|
-
if (
|
|
14
|
+
if (getConfig('WEB_SEARCH') === 'false') return false;
|
|
13
15
|
const provider = getProvider();
|
|
14
16
|
return provider === 'anthropic' || provider === 'openai';
|
|
15
17
|
}
|
package/lib/auth/actions.js
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
'use server';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { auth } from './index.js';
|
|
4
|
+
import {
|
|
5
|
+
createFirstUser,
|
|
6
|
+
createUser,
|
|
7
|
+
getAllUsers,
|
|
8
|
+
deleteUser,
|
|
9
|
+
getUserByEmail,
|
|
10
|
+
updateUserEmail,
|
|
11
|
+
updateUserRole,
|
|
12
|
+
updateUserPasswordById,
|
|
13
|
+
verifyPassword,
|
|
14
|
+
} from '../db/users.js';
|
|
15
|
+
|
|
16
|
+
async function requireAuth() {
|
|
17
|
+
const session = await auth();
|
|
18
|
+
if (!session?.user?.id) throw new Error('Unauthorized');
|
|
19
|
+
return session.user;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function requireAdmin() {
|
|
23
|
+
const user = await requireAuth();
|
|
24
|
+
if (user.role !== 'admin') throw new Error('Forbidden');
|
|
25
|
+
return user;
|
|
26
|
+
}
|
|
4
27
|
|
|
5
28
|
/**
|
|
6
29
|
* Create the first admin user (setup action).
|
|
@@ -26,3 +49,152 @@ export async function setupAdmin(email, password) {
|
|
|
26
49
|
|
|
27
50
|
return { success: true };
|
|
28
51
|
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get all users.
|
|
55
|
+
*/
|
|
56
|
+
export async function getUsers() {
|
|
57
|
+
await requireAdmin();
|
|
58
|
+
return getAllUsers();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Add a new user.
|
|
63
|
+
*/
|
|
64
|
+
export async function addUser(email, password, role) {
|
|
65
|
+
await requireAdmin();
|
|
66
|
+
|
|
67
|
+
if (!email || !password) {
|
|
68
|
+
return { error: 'Email and password are required.' };
|
|
69
|
+
}
|
|
70
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
71
|
+
return { error: 'Invalid email format.' };
|
|
72
|
+
}
|
|
73
|
+
if (password.length < 8) {
|
|
74
|
+
return { error: 'Password must be at least 8 characters.' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await createUser(email, password);
|
|
79
|
+
if (role && role !== 'admin') {
|
|
80
|
+
// createUser defaults to admin; update if different role requested
|
|
81
|
+
const users = getAllUsers();
|
|
82
|
+
const created = users.find((u) => u.email === email.toLowerCase());
|
|
83
|
+
if (created) updateUserRole(created.id, role);
|
|
84
|
+
}
|
|
85
|
+
return { success: true };
|
|
86
|
+
} catch (err) {
|
|
87
|
+
if (err.message?.includes('UNIQUE constraint')) {
|
|
88
|
+
return { error: 'A user with this email already exists.' };
|
|
89
|
+
}
|
|
90
|
+
return { error: 'Failed to create user.' };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Edit a user's email and/or role.
|
|
96
|
+
*/
|
|
97
|
+
export async function editUser(id, { email, role }) {
|
|
98
|
+
const user = await requireAdmin();
|
|
99
|
+
|
|
100
|
+
if (role !== undefined && id === user.id) {
|
|
101
|
+
return { error: 'Cannot change your own role.' };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
if (email !== undefined) {
|
|
106
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
107
|
+
return { error: 'Invalid email format.' };
|
|
108
|
+
}
|
|
109
|
+
updateUserEmail(id, email);
|
|
110
|
+
}
|
|
111
|
+
if (role !== undefined) {
|
|
112
|
+
updateUserRole(id, role);
|
|
113
|
+
}
|
|
114
|
+
return { success: true };
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (err.message?.includes('UNIQUE constraint')) {
|
|
117
|
+
return { error: 'A user with this email already exists.' };
|
|
118
|
+
}
|
|
119
|
+
return { error: 'Failed to update user.' };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Remove a user.
|
|
125
|
+
*/
|
|
126
|
+
export async function removeUser(id) {
|
|
127
|
+
const user = await requireAdmin();
|
|
128
|
+
|
|
129
|
+
if (id === user.id) {
|
|
130
|
+
return { error: 'Cannot delete yourself.' };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const deleted = deleteUser(id);
|
|
134
|
+
if (!deleted) {
|
|
135
|
+
return { error: 'User not found.' };
|
|
136
|
+
}
|
|
137
|
+
return { success: true };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Reset a user's password.
|
|
142
|
+
*/
|
|
143
|
+
export async function resetPassword(id, newPassword) {
|
|
144
|
+
await requireAdmin();
|
|
145
|
+
|
|
146
|
+
if (!newPassword || newPassword.length < 8) {
|
|
147
|
+
return { error: 'Password must be at least 8 characters.' };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const updated = updateUserPasswordById(id, newPassword);
|
|
151
|
+
if (!updated) {
|
|
152
|
+
return { error: 'User not found.' };
|
|
153
|
+
}
|
|
154
|
+
return { success: true };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Update the current user's own email and/or password.
|
|
159
|
+
* Requires current password for verification.
|
|
160
|
+
*/
|
|
161
|
+
export async function updateProfile({ email, currentPassword, newPassword }) {
|
|
162
|
+
const sessionUser = await requireAuth();
|
|
163
|
+
|
|
164
|
+
if (!currentPassword) {
|
|
165
|
+
return { error: 'Current password is required.' };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const user = getUserByEmail(sessionUser.email);
|
|
169
|
+
if (!user) {
|
|
170
|
+
return { error: 'User not found.' };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const valid = await verifyPassword(user, currentPassword);
|
|
174
|
+
if (!valid) {
|
|
175
|
+
return { error: 'Current password is incorrect.' };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
if (email !== undefined && email !== sessionUser.email) {
|
|
180
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
181
|
+
return { error: 'Invalid email format.' };
|
|
182
|
+
}
|
|
183
|
+
updateUserEmail(user.id, email);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (newPassword) {
|
|
187
|
+
if (newPassword.length < 8) {
|
|
188
|
+
return { error: 'New password must be at least 8 characters.' };
|
|
189
|
+
}
|
|
190
|
+
updateUserPasswordById(user.id, newPassword);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { success: true };
|
|
194
|
+
} catch (err) {
|
|
195
|
+
if (err.message?.includes('UNIQUE constraint')) {
|
|
196
|
+
return { error: 'A user with this email already exists.' };
|
|
197
|
+
}
|
|
198
|
+
return { error: 'Failed to update profile.' };
|
|
199
|
+
}
|
|
200
|
+
}
|
package/lib/auth/middleware.js
CHANGED
|
@@ -45,6 +45,11 @@ export const middleware = auth((req) => {
|
|
|
45
45
|
|
|
46
46
|
return response;
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
// Admin panel requires admin role (after auth check above)
|
|
50
|
+
if (pathname.startsWith('/admin') && req.auth?.user?.role !== 'admin') {
|
|
51
|
+
return NextResponse.redirect(new URL('/forbidden', req.url));
|
|
52
|
+
}
|
|
48
53
|
});
|
|
49
54
|
|
|
50
55
|
export const config = {
|