@harbinger-ai/harbinger 0.1.0
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/LICENSE +21 -0
- package/README.md +406 -0
- package/agents/README.md +76 -0
- package/agents/_template/CONFIG.yaml +7 -0
- package/agents/_template/HEARTBEAT.md +59 -0
- package/agents/_template/IDENTITY.md +4 -0
- package/agents/_template/SKILLS.md +1 -0
- package/agents/_template/SOUL.md +25 -0
- package/agents/_template/TOOLS.md +3 -0
- package/agents/binary-reverser/CONFIG.yaml +21 -0
- package/agents/binary-reverser/HEARTBEAT.md +65 -0
- package/agents/binary-reverser/IDENTITY.md +1 -0
- package/agents/binary-reverser/SKILLS.md +1 -0
- package/agents/binary-reverser/SOUL.md +23 -0
- package/agents/binary-reverser/TOOLS.md +99 -0
- package/agents/browser-agent/CONFIG.yaml +20 -0
- package/agents/browser-agent/HEARTBEAT.md +79 -0
- package/agents/browser-agent/IDENTITY.md +5 -0
- package/agents/browser-agent/SKILLS.md +86 -0
- package/agents/browser-agent/SOUL.md +23 -0
- package/agents/browser-agent/TOOLS.md +186 -0
- package/agents/cloud-infiltrator/CONFIG.yaml +22 -0
- package/agents/cloud-infiltrator/HEARTBEAT.md +78 -0
- package/agents/cloud-infiltrator/IDENTITY.md +1 -0
- package/agents/cloud-infiltrator/SKILLS.md +1 -0
- package/agents/cloud-infiltrator/SOUL.md +23 -0
- package/agents/cloud-infiltrator/TOOLS.md +68 -0
- package/agents/coding-assistant/CONFIG.yaml +22 -0
- package/agents/coding-assistant/HEARTBEAT.md +57 -0
- package/agents/coding-assistant/IDENTITY.md +5 -0
- package/agents/coding-assistant/SKILLS.md +69 -0
- package/agents/coding-assistant/SOUL.md +60 -0
- package/agents/coding-assistant/TOOLS.md +168 -0
- package/agents/learning-agent/CONFIG.yaml +21 -0
- package/agents/learning-agent/HEARTBEAT.md +63 -0
- package/agents/learning-agent/IDENTITY.md +5 -0
- package/agents/learning-agent/SKILLS.md +86 -0
- package/agents/learning-agent/SOUL.md +77 -0
- package/agents/learning-agent/TOOLS.md +145 -0
- package/agents/maintainer/CONFIG.yaml +31 -0
- package/agents/maintainer/HEARTBEAT.md +28 -0
- package/agents/maintainer/IDENTITY.md +33 -0
- package/agents/maintainer/SKILLS.md +24 -0
- package/agents/maintainer/SOUL.md +61 -0
- package/agents/maintainer/TOOLS.md +29 -0
- package/agents/maintainer/lib/engine.js +279 -0
- package/agents/maintainer/lib/safe-fixer.js +183 -0
- package/agents/morning-brief/CONFIG.yaml +22 -0
- package/agents/morning-brief/HEARTBEAT.md +60 -0
- package/agents/morning-brief/IDENTITY.md +5 -0
- package/agents/morning-brief/SKILLS.md +56 -0
- package/agents/morning-brief/SOUL.md +64 -0
- package/agents/morning-brief/TOOLS.md +112 -0
- package/agents/osint-detective/CONFIG.yaml +24 -0
- package/agents/osint-detective/HEARTBEAT.md +66 -0
- package/agents/osint-detective/IDENTITY.md +1 -0
- package/agents/osint-detective/SKILLS.md +1 -0
- package/agents/osint-detective/SOUL.md +23 -0
- package/agents/osint-detective/TOOLS.md +81 -0
- package/agents/recon-scout/CONFIG.yaml +22 -0
- package/agents/recon-scout/HEARTBEAT.md +79 -0
- package/agents/recon-scout/IDENTITY.md +1 -0
- package/agents/recon-scout/SKILLS.md +1 -0
- package/agents/recon-scout/SOUL.md +23 -0
- package/agents/recon-scout/TOOLS.md +93 -0
- package/agents/report-writer/CONFIG.yaml +21 -0
- package/agents/report-writer/HEARTBEAT.md +63 -0
- package/agents/report-writer/IDENTITY.md +1 -0
- package/agents/report-writer/SKILLS.md +1 -0
- package/agents/report-writer/SOUL.md +23 -0
- package/agents/report-writer/TOOLS.md +69 -0
- package/agents/shared/README.md +13 -0
- package/agents/web-hacker/CONFIG.yaml +24 -0
- package/agents/web-hacker/HEARTBEAT.md +78 -0
- package/agents/web-hacker/IDENTITY.md +1 -0
- package/agents/web-hacker/SKILLS.md +1 -0
- package/agents/web-hacker/SOUL.md +23 -0
- package/agents/web-hacker/TOOLS.md +86 -0
- package/api/CLAUDE.md +19 -0
- package/api/index.js +274 -0
- package/bin/cli.js +620 -0
- package/bin/local.sh +31 -0
- package/bin/postinstall.js +63 -0
- package/config/index.js +24 -0
- package/config/instrumentation.js +93 -0
- package/drizzle/0000_initial.sql +52 -0
- package/drizzle/0001_bounty_and_registry.sql +82 -0
- package/drizzle/0002_sync_columns.sql +7 -0
- package/drizzle/0003_graceful_bloodscream.sql +86 -0
- package/drizzle/meta/0000_snapshot.json +321 -0
- package/drizzle/meta/0003_snapshot.json +878 -0
- package/drizzle/meta/_journal.json +34 -0
- package/drizzle/relations.ts +3 -0
- package/drizzle/schema.ts +145 -0
- package/lib/actions.js +47 -0
- package/lib/agents.js +166 -0
- package/lib/ai/agent.js +96 -0
- package/lib/ai/autonomous-engine.js +261 -0
- package/lib/ai/index.js +359 -0
- package/lib/ai/model-router.js +254 -0
- package/lib/ai/model.js +73 -0
- package/lib/ai/tools.js +84 -0
- package/lib/auth/actions.js +28 -0
- package/lib/auth/config.js +27 -0
- package/lib/auth/edge-config.js +27 -0
- package/lib/auth/index.js +27 -0
- package/lib/auth/middleware.js +53 -0
- package/lib/bounty/actions.js +119 -0
- package/lib/bounty/findings.js +64 -0
- package/lib/bounty/programs.js +34 -0
- package/lib/bounty/sync-targets.js +267 -0
- package/lib/bounty/targets.js +33 -0
- package/lib/channels/base.js +56 -0
- package/lib/channels/index.js +15 -0
- package/lib/channels/telegram.js +148 -0
- package/lib/chat/actions.js +288 -0
- package/lib/chat/api.js +135 -0
- package/lib/chat/components/app-sidebar.js +237 -0
- package/lib/chat/components/app-sidebar.jsx +289 -0
- package/lib/chat/components/chat-header.js +27 -0
- package/lib/chat/components/chat-header.jsx +37 -0
- package/lib/chat/components/chat-input.js +230 -0
- package/lib/chat/components/chat-input.jsx +228 -0
- package/lib/chat/components/chat-nav-context.js +11 -0
- package/lib/chat/components/chat-nav-context.jsx +11 -0
- package/lib/chat/components/chat-page.js +81 -0
- package/lib/chat/components/chat-page.jsx +100 -0
- package/lib/chat/components/chat.js +150 -0
- package/lib/chat/components/chat.jsx +182 -0
- package/lib/chat/components/chats-page.js +302 -0
- package/lib/chat/components/chats-page.jsx +330 -0
- package/lib/chat/components/crons-page.js +172 -0
- package/lib/chat/components/crons-page.jsx +244 -0
- package/lib/chat/components/enhanced-tool-call.js +103 -0
- package/lib/chat/components/enhanced-tool-call.jsx +139 -0
- package/lib/chat/components/findings-page.js +175 -0
- package/lib/chat/components/findings-page.jsx +214 -0
- package/lib/chat/components/greeting.js +22 -0
- package/lib/chat/components/greeting.jsx +26 -0
- package/lib/chat/components/icons.js +777 -0
- package/lib/chat/components/icons.jsx +741 -0
- package/lib/chat/components/index.js +26 -0
- package/lib/chat/components/mcp-page.js +260 -0
- package/lib/chat/components/mcp-page.jsx +355 -0
- package/lib/chat/components/message.js +289 -0
- package/lib/chat/components/message.jsx +315 -0
- package/lib/chat/components/messages.js +66 -0
- package/lib/chat/components/messages.jsx +77 -0
- package/lib/chat/components/notifications-page.js +56 -0
- package/lib/chat/components/notifications-page.jsx +87 -0
- package/lib/chat/components/page-layout.js +21 -0
- package/lib/chat/components/page-layout.jsx +28 -0
- package/lib/chat/components/registry-page.js +222 -0
- package/lib/chat/components/registry-page.jsx +255 -0
- package/lib/chat/components/settings-layout.js +40 -0
- package/lib/chat/components/settings-layout.jsx +54 -0
- package/lib/chat/components/settings-secrets-page.js +216 -0
- package/lib/chat/components/settings-secrets-page.jsx +264 -0
- package/lib/chat/components/sidebar-history-item.js +132 -0
- package/lib/chat/components/sidebar-history-item.jsx +113 -0
- package/lib/chat/components/sidebar-history.js +115 -0
- package/lib/chat/components/sidebar-history.jsx +157 -0
- package/lib/chat/components/sidebar-user-nav.js +63 -0
- package/lib/chat/components/sidebar-user-nav.jsx +73 -0
- package/lib/chat/components/status-bar.js +39 -0
- package/lib/chat/components/status-bar.jsx +51 -0
- package/lib/chat/components/swarm-page.js +157 -0
- package/lib/chat/components/swarm-page.jsx +210 -0
- package/lib/chat/components/targets-page.js +376 -0
- package/lib/chat/components/targets-page.jsx +389 -0
- package/lib/chat/components/tool-call.js +86 -0
- package/lib/chat/components/tool-call.jsx +104 -0
- package/lib/chat/components/tool-panel.js +107 -0
- package/lib/chat/components/tool-panel.jsx +145 -0
- package/lib/chat/components/triggers-page.js +153 -0
- package/lib/chat/components/triggers-page.jsx +221 -0
- package/lib/chat/components/ui/confirm-dialog.js +53 -0
- package/lib/chat/components/ui/confirm-dialog.jsx +57 -0
- package/lib/chat/components/ui/dropdown-menu.js +98 -0
- package/lib/chat/components/ui/dropdown-menu.jsx +116 -0
- package/lib/chat/components/ui/rename-dialog.js +74 -0
- package/lib/chat/components/ui/rename-dialog.jsx +72 -0
- package/lib/chat/components/ui/scroll-area.js +13 -0
- package/lib/chat/components/ui/scroll-area.jsx +17 -0
- package/lib/chat/components/ui/separator.js +21 -0
- package/lib/chat/components/ui/separator.jsx +18 -0
- package/lib/chat/components/ui/sheet.js +75 -0
- package/lib/chat/components/ui/sheet.jsx +95 -0
- package/lib/chat/components/ui/sidebar.js +227 -0
- package/lib/chat/components/ui/sidebar.jsx +245 -0
- package/lib/chat/components/ui/tooltip.js +56 -0
- package/lib/chat/components/ui/tooltip.jsx +66 -0
- package/lib/chat/components/upgrade-dialog.js +151 -0
- package/lib/chat/components/upgrade-dialog.jsx +170 -0
- package/lib/chat/utils.js +11 -0
- package/lib/cron.js +246 -0
- package/lib/db/api-keys.js +163 -0
- package/lib/db/chats.js +145 -0
- package/lib/db/index.js +52 -0
- package/lib/db/notifications.js +99 -0
- package/lib/db/schema.js +145 -0
- package/lib/db/update-check.js +96 -0
- package/lib/db/users.js +89 -0
- package/lib/mcp/actions.js +104 -0
- package/lib/mcp/client.js +79 -0
- package/lib/mcp/handler.js +57 -0
- package/lib/mcp/server.js +165 -0
- package/lib/paths.js +46 -0
- package/lib/registry/actions.js +164 -0
- package/lib/registry/catalog.js +137 -0
- package/lib/registry/tools.js +71 -0
- package/lib/tools/create-job.js +99 -0
- package/lib/tools/github.js +217 -0
- package/lib/tools/openai.js +35 -0
- package/lib/tools/telegram.js +292 -0
- package/lib/triggers.js +118 -0
- package/lib/utils/render-md.js +102 -0
- package/package.json +103 -0
- package/setup/lib/auth.mjs +81 -0
- package/setup/lib/env.mjs +21 -0
- package/setup/lib/fs-utils.mjs +20 -0
- package/setup/lib/github.mjs +149 -0
- package/setup/lib/prerequisites.mjs +155 -0
- package/setup/lib/prompts.mjs +267 -0
- package/setup/lib/providers.mjs +48 -0
- package/setup/lib/sync.mjs +125 -0
- package/setup/lib/targets.mjs +45 -0
- package/setup/lib/telegram-verify.mjs +63 -0
- package/setup/lib/telegram.mjs +76 -0
- package/setup/setup-telegram.mjs +264 -0
- package/setup/setup.mjs +842 -0
- package/templates/.dockerignore +5 -0
- package/templates/.env.example +63 -0
- package/templates/.github/workflows/auto-merge.yml +117 -0
- package/templates/.github/workflows/build-image.yml +36 -0
- package/templates/.github/workflows/notify-job-failed.yml +64 -0
- package/templates/.github/workflows/notify-pr-complete.yml +119 -0
- package/templates/.github/workflows/rebuild-event-handler.yml +121 -0
- package/templates/.github/workflows/run-job.yml +89 -0
- package/templates/.github/workflows/upgrade-event-handler.yml +62 -0
- package/templates/.gitignore.template +45 -0
- package/templates/.pi/extensions/env-sanitizer/index.ts +48 -0
- package/templates/.pi/extensions/env-sanitizer/package.json +5 -0
- package/templates/CLAUDE.md +29 -0
- package/templates/CLAUDE.md.template +307 -0
- package/templates/app/api/[...thepopebot]/route.js +1 -0
- package/templates/app/api/auth/[...nextauth]/route.js +1 -0
- package/templates/app/chat/[chatId]/page.js +8 -0
- package/templates/app/chats/page.js +7 -0
- package/templates/app/components/ascii-logo.jsx +10 -0
- package/templates/app/components/login-form.jsx +92 -0
- package/templates/app/components/setup-form.jsx +82 -0
- package/templates/app/components/theme-provider.jsx +11 -0
- package/templates/app/components/theme-toggle.jsx +38 -0
- package/templates/app/components/ui/button.jsx +21 -0
- package/templates/app/components/ui/card.jsx +23 -0
- package/templates/app/components/ui/input.jsx +10 -0
- package/templates/app/components/ui/label.jsx +10 -0
- package/templates/app/crons/page.js +5 -0
- package/templates/app/findings/page.js +7 -0
- package/templates/app/globals.css +90 -0
- package/templates/app/layout.js +19 -0
- package/templates/app/login/page.js +15 -0
- package/templates/app/notifications/page.js +7 -0
- package/templates/app/page.js +7 -0
- package/templates/app/settings/crons/page.js +5 -0
- package/templates/app/settings/layout.js +7 -0
- package/templates/app/settings/mcp/page.js +5 -0
- package/templates/app/settings/page.js +5 -0
- package/templates/app/settings/secrets/page.js +5 -0
- package/templates/app/settings/triggers/page.js +5 -0
- package/templates/app/stream/chat/route.js +1 -0
- package/templates/app/swarm/page.js +7 -0
- package/templates/app/targets/page.js +7 -0
- package/templates/app/toolbox/page.js +7 -0
- package/templates/app/triggers/page.js +5 -0
- package/templates/config/AGENT.md +34 -0
- package/templates/config/CRONS.json +56 -0
- package/templates/config/EVENT_HANDLER.md +224 -0
- package/templates/config/HEARTBEAT.md +3 -0
- package/templates/config/JOB_SUMMARY.md +130 -0
- package/templates/config/MCP_SERVERS.json +1 -0
- package/templates/config/SKILL_BUILDING_GUIDE.md +90 -0
- package/templates/config/SOUL.md +17 -0
- package/templates/config/TRIGGERS.json +58 -0
- package/templates/docker/event-handler/Dockerfile +20 -0
- package/templates/docker/event-handler/ecosystem.config.cjs +8 -0
- package/templates/docker/job-claude-code/Dockerfile +34 -0
- package/templates/docker/job-claude-code/entrypoint.sh +139 -0
- package/templates/docker/job-pi-coding-agent/Dockerfile +44 -0
- package/templates/docker/job-pi-coding-agent/entrypoint.sh +163 -0
- package/templates/docker-compose.yml +63 -0
- package/templates/instrumentation.js +6 -0
- package/templates/middleware.js +1 -0
- package/templates/next.config.mjs +3 -0
- package/templates/postcss.config.mjs +5 -0
- package/templates/skills/LICENSE +21 -0
- package/templates/skills/README.md +119 -0
- package/templates/skills/brave-search/SKILL.md +79 -0
- package/templates/skills/brave-search/content.js +86 -0
- package/templates/skills/brave-search/package-lock.json +621 -0
- package/templates/skills/brave-search/package.json +14 -0
- package/templates/skills/brave-search/search.js +199 -0
- package/templates/skills/browser-tools/SKILL.md +196 -0
- package/templates/skills/browser-tools/browser-content.js +103 -0
- package/templates/skills/browser-tools/browser-cookies.js +35 -0
- package/templates/skills/browser-tools/browser-eval.js +53 -0
- package/templates/skills/browser-tools/browser-hn-scraper.js +108 -0
- package/templates/skills/browser-tools/browser-nav.js +44 -0
- package/templates/skills/browser-tools/browser-pick.js +162 -0
- package/templates/skills/browser-tools/browser-screenshot.js +34 -0
- package/templates/skills/browser-tools/browser-start.js +87 -0
- package/templates/skills/browser-tools/package-lock.json +2556 -0
- package/templates/skills/browser-tools/package.json +19 -0
- package/templates/skills/llm-secrets/SKILL.md +34 -0
- package/templates/skills/llm-secrets/llm-secrets.js +33 -0
- package/templates/skills/modify-self/SKILL.md +12 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SmartModelRouter — Local-first model routing for agents.
|
|
3
|
+
*
|
|
4
|
+
* Picks the cheapest sufficient model for each task. Five complexity tiers,
|
|
5
|
+
* per-agent overrides, provider health checks. Integrates with the bot's
|
|
6
|
+
* createModel() for actual LangChain model instantiation.
|
|
7
|
+
*
|
|
8
|
+
* Adapted from Harbinger's agents/shared/model-router.js.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createModel } from './model.js';
|
|
12
|
+
|
|
13
|
+
export class SmartModelRouter {
|
|
14
|
+
constructor(opts = {}) {
|
|
15
|
+
this.localOnly = opts.localOnly || false;
|
|
16
|
+
|
|
17
|
+
// Provider registry — local providers first
|
|
18
|
+
this.providers = {
|
|
19
|
+
ollama: { type: 'local', baseUrl: 'http://localhost:11434', models: ['llama3', 'codellama', 'mistral', 'phi3'] },
|
|
20
|
+
lmstudio: { type: 'local', baseUrl: 'http://localhost:1234/v1', models: ['local-model'] },
|
|
21
|
+
anthropic: { type: 'cloud', models: ['claude-opus-4-6', 'claude-sonnet-4-6', 'claude-haiku-4-5-20251001'] },
|
|
22
|
+
openai: { type: 'cloud', models: ['gpt-4o', 'gpt-4o-mini', 'o1'] },
|
|
23
|
+
google: { type: 'cloud', models: ['gemini-2.0-flash', 'gemini-pro'] },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Complexity tiers — tokens and time budgets
|
|
27
|
+
this.tiers = {
|
|
28
|
+
trivial: { maxTokens: 500, timeout: 1000, description: 'Greetings, simple lookups' },
|
|
29
|
+
simple: { maxTokens: 2000, timeout: 5000, description: 'Single-step tasks, short answers' },
|
|
30
|
+
moderate: { maxTokens: 4000, timeout: 15000, description: 'Multi-step analysis, code review' },
|
|
31
|
+
complex: { maxTokens: 8000, timeout: 30000, description: 'Deep reasoning, exploit dev' },
|
|
32
|
+
massive: { maxTokens: 32000, timeout: 60000, description: 'Full codebase analysis, report gen' },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Default route table — local-first
|
|
36
|
+
this.routes = {
|
|
37
|
+
trivial: { provider: 'ollama', model: 'llama3', fallback: null },
|
|
38
|
+
simple: { provider: 'ollama', model: 'llama3', fallback: null },
|
|
39
|
+
moderate: { provider: 'ollama', model: 'codellama', fallback: { provider: 'anthropic', model: 'claude-sonnet-4-6' } },
|
|
40
|
+
complex: { provider: 'anthropic', model: 'claude-sonnet-4-6', fallback: { provider: 'anthropic', model: 'claude-opus-4-6' } },
|
|
41
|
+
massive: { provider: 'anthropic', model: 'claude-opus-4-6', fallback: { provider: 'anthropic', model: 'claude-opus-4-6' } },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Per-agent overrides
|
|
45
|
+
this.agentOverrides = opts.agentOverrides || {};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Select the best model for a task.
|
|
50
|
+
*
|
|
51
|
+
* Priority chain:
|
|
52
|
+
* 1. User preference (explicit model request)
|
|
53
|
+
* 2. Agent override (pinned model for this agent)
|
|
54
|
+
* 3. Complexity classification → route table
|
|
55
|
+
* 4. Provider health check → fallback if primary down
|
|
56
|
+
*/
|
|
57
|
+
async selectModel(task, context = {}) {
|
|
58
|
+
// 1. User preference wins
|
|
59
|
+
if (context.preferredModel) {
|
|
60
|
+
return { provider: context.preferredProvider || 'anthropic', model: context.preferredModel, reason: 'user_preference' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 2. Agent override
|
|
64
|
+
if (context.agentId && this.agentOverrides[context.agentId]) {
|
|
65
|
+
const override = this.agentOverrides[context.agentId];
|
|
66
|
+
return { ...override, reason: 'agent_override' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 3. Classify complexity
|
|
70
|
+
const complexity = this.assessComplexity(task);
|
|
71
|
+
const route = this.routes[complexity];
|
|
72
|
+
|
|
73
|
+
if (!route) {
|
|
74
|
+
return { provider: 'ollama', model: 'llama3', complexity, reason: 'default_fallback' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 4. Local mode enforcement
|
|
78
|
+
if (this.localOnly && this.providers[route.provider]?.type === 'cloud') {
|
|
79
|
+
return { provider: 'ollama', model: 'llama3', complexity, reason: 'local_mode_enforced' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 5. Health check primary provider
|
|
83
|
+
const primaryAvailable = await this.isProviderAvailable(route.provider);
|
|
84
|
+
if (primaryAvailable) {
|
|
85
|
+
return { provider: route.provider, model: route.model, complexity, reason: 'route_table' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 6. Fallback
|
|
89
|
+
if (route.fallback) {
|
|
90
|
+
const fallbackAvailable = await this.isProviderAvailable(route.fallback.provider);
|
|
91
|
+
if (fallbackAvailable) {
|
|
92
|
+
return { ...route.fallback, complexity, reason: 'fallback' };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 7. Last resort — local model
|
|
97
|
+
return { provider: 'ollama', model: 'llama3', complexity, reason: 'last_resort' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Assess task complexity based on content analysis.
|
|
102
|
+
* Returns: 'trivial' | 'simple' | 'moderate' | 'complex' | 'massive'
|
|
103
|
+
*/
|
|
104
|
+
assessComplexity(task) {
|
|
105
|
+
if (!task || typeof task !== 'string') return 'simple';
|
|
106
|
+
|
|
107
|
+
let score = 0;
|
|
108
|
+
const text = task.toLowerCase();
|
|
109
|
+
|
|
110
|
+
// Token estimate — rough proxy
|
|
111
|
+
const tokenEstimate = text.split(/\s+/).length * 1.3;
|
|
112
|
+
if (tokenEstimate > 500) score += 2;
|
|
113
|
+
if (tokenEstimate > 2000) score += 2;
|
|
114
|
+
if (tokenEstimate > 5000) score += 2;
|
|
115
|
+
|
|
116
|
+
// Reasoning depth indicators
|
|
117
|
+
if (/\b(analyze|explain|compare|evaluate|synthesize)\b/.test(text)) score += 2;
|
|
118
|
+
if (/\b(why|how|reason|because|therefore)\b/.test(text)) score += 1;
|
|
119
|
+
|
|
120
|
+
// Code task indicators
|
|
121
|
+
if (/\b(code|function|class|implement|refactor|debug)\b/.test(text)) score += 2;
|
|
122
|
+
if (/\b(exploit|vulnerability|payload|injection|bypass)\b/.test(text)) score += 3;
|
|
123
|
+
|
|
124
|
+
// Math/logic indicators
|
|
125
|
+
if (/\b(calculate|equation|algorithm|optimize|crypto)\b/.test(text)) score += 2;
|
|
126
|
+
|
|
127
|
+
// Conversational / trivial indicators
|
|
128
|
+
if (/^(hi|hello|hey|thanks|ok|yes|no)\b/i.test(text)) return 'trivial';
|
|
129
|
+
if (text.length < 20) return 'trivial';
|
|
130
|
+
|
|
131
|
+
// Map score to tier
|
|
132
|
+
if (score <= 1) return 'trivial';
|
|
133
|
+
if (score <= 3) return 'simple';
|
|
134
|
+
if (score <= 6) return 'moderate';
|
|
135
|
+
if (score <= 9) return 'complex';
|
|
136
|
+
return 'massive';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Check if a provider is reachable.
|
|
141
|
+
*/
|
|
142
|
+
async isProviderAvailable(providerName) {
|
|
143
|
+
const provider = this.providers[providerName];
|
|
144
|
+
if (!provider) return false;
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
// Local providers: hit their health endpoint
|
|
148
|
+
if (provider.type === 'local') {
|
|
149
|
+
const controller = new AbortController();
|
|
150
|
+
const timeout = setTimeout(() => controller.abort(), 3000);
|
|
151
|
+
const url = providerName === 'ollama'
|
|
152
|
+
? `${provider.baseUrl}/api/tags`
|
|
153
|
+
: `${provider.baseUrl}/models`;
|
|
154
|
+
const res = await fetch(url, { signal: controller.signal });
|
|
155
|
+
clearTimeout(timeout);
|
|
156
|
+
return res.ok;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Cloud providers: assume available if configured (don't waste API calls)
|
|
160
|
+
return true;
|
|
161
|
+
} catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get the current route table (for Settings UI).
|
|
168
|
+
*/
|
|
169
|
+
getRoutes() {
|
|
170
|
+
return Object.entries(this.routes).map(([tier, route]) => ({
|
|
171
|
+
tier,
|
|
172
|
+
...this.tiers[tier],
|
|
173
|
+
provider: route.provider,
|
|
174
|
+
model: route.model,
|
|
175
|
+
fallbackProvider: route.fallback?.provider || null,
|
|
176
|
+
fallbackModel: route.fallback?.model || null,
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Update a route in the table.
|
|
182
|
+
*/
|
|
183
|
+
updateRoute(tier, update) {
|
|
184
|
+
if (!this.routes[tier]) return false;
|
|
185
|
+
Object.assign(this.routes[tier], update);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let _router = null;
|
|
191
|
+
|
|
192
|
+
/** Get the SmartModelRouter singleton (lazy-initialized). */
|
|
193
|
+
export function getModelRouter() {
|
|
194
|
+
if (!_router) {
|
|
195
|
+
_router = new SmartModelRouter({
|
|
196
|
+
localOnly: process.env.LOCAL_ONLY === 'true',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return _router;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Create a LangChain model routed by task complexity.
|
|
204
|
+
* Uses the SmartModelRouter to pick provider/model, then delegates to createModel().
|
|
205
|
+
*
|
|
206
|
+
* @param {string} task - The task description to route on
|
|
207
|
+
* @param {object} [context={}] - Optional routing context (agentId, preferredModel, etc.)
|
|
208
|
+
* @returns {Promise<import('@langchain/core/language_models/chat_models').BaseChatModel>}
|
|
209
|
+
*/
|
|
210
|
+
export async function createRoutedModel(task, context = {}) {
|
|
211
|
+
const router = getModelRouter();
|
|
212
|
+
const selection = await router.selectModel(task, context);
|
|
213
|
+
|
|
214
|
+
// Temporarily override env vars so createModel() picks the right provider/model
|
|
215
|
+
const origProvider = process.env.LLM_PROVIDER;
|
|
216
|
+
const origModel = process.env.LLM_MODEL;
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
process.env.LLM_PROVIDER = selection.provider === 'ollama' ? 'custom' : selection.provider;
|
|
220
|
+
process.env.LLM_MODEL = selection.model;
|
|
221
|
+
|
|
222
|
+
// For local providers, set the base URL
|
|
223
|
+
if (selection.provider === 'ollama') {
|
|
224
|
+
const origBaseUrl = process.env.OPENAI_BASE_URL;
|
|
225
|
+
process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1';
|
|
226
|
+
try {
|
|
227
|
+
return await createModel();
|
|
228
|
+
} finally {
|
|
229
|
+
if (origBaseUrl !== undefined) process.env.OPENAI_BASE_URL = origBaseUrl;
|
|
230
|
+
else delete process.env.OPENAI_BASE_URL;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (selection.provider === 'lmstudio') {
|
|
235
|
+
const origBaseUrl = process.env.OPENAI_BASE_URL;
|
|
236
|
+
process.env.OPENAI_BASE_URL = 'http://localhost:1234/v1';
|
|
237
|
+
process.env.LLM_PROVIDER = 'custom';
|
|
238
|
+
try {
|
|
239
|
+
return await createModel();
|
|
240
|
+
} finally {
|
|
241
|
+
if (origBaseUrl !== undefined) process.env.OPENAI_BASE_URL = origBaseUrl;
|
|
242
|
+
else delete process.env.OPENAI_BASE_URL;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return await createModel();
|
|
247
|
+
} finally {
|
|
248
|
+
// Restore original env vars
|
|
249
|
+
if (origProvider !== undefined) process.env.LLM_PROVIDER = origProvider;
|
|
250
|
+
else delete process.env.LLM_PROVIDER;
|
|
251
|
+
if (origModel !== undefined) process.env.LLM_MODEL = origModel;
|
|
252
|
+
else delete process.env.LLM_MODEL;
|
|
253
|
+
}
|
|
254
|
+
}
|
package/lib/ai/model.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { ChatAnthropic } from '@langchain/anthropic';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MODELS = {
|
|
4
|
+
anthropic: 'claude-sonnet-4-20250514',
|
|
5
|
+
openai: 'gpt-4o',
|
|
6
|
+
google: 'gemini-2.5-pro',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create a LangChain chat model based on environment configuration.
|
|
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
|
|
19
|
+
*
|
|
20
|
+
* @param {object} [options]
|
|
21
|
+
* @param {number} [options.maxTokens=4096] - Max tokens for the response
|
|
22
|
+
* @returns {import('@langchain/core/language_models/chat_models').BaseChatModel}
|
|
23
|
+
*/
|
|
24
|
+
export async function createModel(options = {}) {
|
|
25
|
+
const provider = process.env.LLM_PROVIDER || 'anthropic';
|
|
26
|
+
const modelName = process.env.LLM_MODEL || DEFAULT_MODELS[provider] || DEFAULT_MODELS.anthropic;
|
|
27
|
+
const maxTokens = options.maxTokens || Number(process.env.LLM_MAX_TOKENS) || 4096;
|
|
28
|
+
|
|
29
|
+
switch (provider) {
|
|
30
|
+
case 'anthropic': {
|
|
31
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
32
|
+
if (!apiKey) {
|
|
33
|
+
throw new Error('ANTHROPIC_API_KEY environment variable is required');
|
|
34
|
+
}
|
|
35
|
+
return new ChatAnthropic({
|
|
36
|
+
modelName,
|
|
37
|
+
maxTokens,
|
|
38
|
+
anthropicApiKey: apiKey,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
case 'custom':
|
|
42
|
+
case 'openai': {
|
|
43
|
+
const { ChatOpenAI } = await import('@langchain/openai');
|
|
44
|
+
const apiKey = provider === 'custom'
|
|
45
|
+
? (process.env.CUSTOM_API_KEY || 'not-needed')
|
|
46
|
+
: process.env.OPENAI_API_KEY;
|
|
47
|
+
const baseURL = process.env.OPENAI_BASE_URL;
|
|
48
|
+
if (!apiKey && !baseURL) {
|
|
49
|
+
throw new Error('OPENAI_API_KEY environment variable is required (or set OPENAI_BASE_URL for local models)');
|
|
50
|
+
}
|
|
51
|
+
const config = { modelName, maxTokens };
|
|
52
|
+
config.apiKey = apiKey || 'not-needed';
|
|
53
|
+
if (baseURL) {
|
|
54
|
+
config.configuration = { baseURL };
|
|
55
|
+
}
|
|
56
|
+
return new ChatOpenAI(config);
|
|
57
|
+
}
|
|
58
|
+
case 'google': {
|
|
59
|
+
const { ChatGoogleGenerativeAI } = await import('@langchain/google-genai');
|
|
60
|
+
const apiKey = process.env.GOOGLE_API_KEY;
|
|
61
|
+
if (!apiKey) {
|
|
62
|
+
throw new Error('GOOGLE_API_KEY environment variable is required');
|
|
63
|
+
}
|
|
64
|
+
return new ChatGoogleGenerativeAI({
|
|
65
|
+
model: modelName,
|
|
66
|
+
maxOutputTokens: maxTokens,
|
|
67
|
+
apiKey,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
default:
|
|
71
|
+
throw new Error(`Unknown LLM provider: ${provider}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
package/lib/ai/tools.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { tool } from '@langchain/core/tools';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { createJob } from '../tools/create-job.js';
|
|
5
|
+
import { getJobStatus } from '../tools/github.js';
|
|
6
|
+
import { claudeMd, skillGuidePath } from '../paths.js';
|
|
7
|
+
|
|
8
|
+
const createJobTool = tool(
|
|
9
|
+
async ({ job_description }) => {
|
|
10
|
+
const result = await createJob(job_description);
|
|
11
|
+
return JSON.stringify({
|
|
12
|
+
success: true,
|
|
13
|
+
job_id: result.job_id,
|
|
14
|
+
branch: result.branch,
|
|
15
|
+
title: result.title,
|
|
16
|
+
});
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: 'create_job',
|
|
20
|
+
description:
|
|
21
|
+
'Create an autonomous job that runs a Docker agent in a container. The Docker agent has full filesystem access, web search, browser automation, and other abilities. The job description you provide becomes the Docker agent\'s task prompt. Returns the job ID and branch name.',
|
|
22
|
+
schema: z.object({
|
|
23
|
+
job_description: z
|
|
24
|
+
.string()
|
|
25
|
+
.describe(
|
|
26
|
+
'Detailed job description including context and requirements. Be specific about what needs to be done.'
|
|
27
|
+
),
|
|
28
|
+
}),
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const getJobStatusTool = tool(
|
|
33
|
+
async ({ job_id }) => {
|
|
34
|
+
const result = await getJobStatus(job_id);
|
|
35
|
+
return JSON.stringify(result);
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'get_job_status',
|
|
39
|
+
description:
|
|
40
|
+
'Check status of running jobs. Returns list of active workflow runs with timing and current step. Use when user asks about job progress, running jobs, or job status.',
|
|
41
|
+
schema: z.object({
|
|
42
|
+
job_id: z
|
|
43
|
+
.string()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe(
|
|
46
|
+
'Optional: specific job ID to check. If omitted, returns all running jobs.'
|
|
47
|
+
),
|
|
48
|
+
}),
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const getSystemTechnicalSpecsTool = tool(
|
|
53
|
+
async () => {
|
|
54
|
+
try {
|
|
55
|
+
return fs.readFileSync(claudeMd, 'utf8');
|
|
56
|
+
} catch {
|
|
57
|
+
return 'No technical documentation found (CLAUDE.md not present in project root).';
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'get_system_technical_specs',
|
|
62
|
+
description:
|
|
63
|
+
'Read the system architecture and technical documentation (CLAUDE.md). You MUST call this before modifying any config file (CRONS.json, TRIGGERS.json, etc.) or system infrastructure — config entries have advanced fields (per-entry LLM overrides, webhook options, etc.) that are only documented here. Also use this when you need to understand how the system works — event handler, Docker agent, API routes, database, GitHub Actions, deployment, or file structure. NOT for skill creation (use get_skill_building_guide for that).',
|
|
64
|
+
schema: z.object({}),
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const getSkillBuildingGuideTool = tool(
|
|
69
|
+
async () => {
|
|
70
|
+
try {
|
|
71
|
+
return fs.readFileSync(skillGuidePath, 'utf8');
|
|
72
|
+
} catch {
|
|
73
|
+
return 'Skill guide not found.';
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'get_skill_building_guide',
|
|
78
|
+
description:
|
|
79
|
+
'Load the guide for creating, modifying, and understanding agent skills. You MUST call this before creating or modifying any skill — the guide contains required file structure, naming conventions, SKILL.md frontmatter format, activation steps, and testing procedures that are only documented there. Skills are lightweight bash/Node.js wrappers in `skills/` that extend what agents can do. NOT for understanding the system architecture (use get_system_technical_specs for that).',
|
|
80
|
+
schema: z.object({}),
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
export { createJobTool, getJobStatusTool, getSystemTechnicalSpecsTool, getSkillBuildingGuideTool };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
|
|
3
|
+
import { createFirstUser } from '../db/users.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create the first admin user (setup action).
|
|
7
|
+
* Uses atomic createFirstUser() to prevent race conditions.
|
|
8
|
+
* No session/token is created — the admin must log in through the normal auth flow.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} email
|
|
11
|
+
* @param {string} password
|
|
12
|
+
* @returns {Promise<{ success?: boolean, error?: string }>}
|
|
13
|
+
*/
|
|
14
|
+
export async function setupAdmin(email, password) {
|
|
15
|
+
if (!email || !password) {
|
|
16
|
+
return { error: 'Email and password are required.' };
|
|
17
|
+
}
|
|
18
|
+
if (password.length < 8) {
|
|
19
|
+
return { error: 'Password must be at least 8 characters.' };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const created = createFirstUser(email, password);
|
|
23
|
+
if (!created) {
|
|
24
|
+
return { error: 'Setup already completed.' };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return { success: true };
|
|
28
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import NextAuth from 'next-auth';
|
|
2
|
+
import Credentials from 'next-auth/providers/credentials';
|
|
3
|
+
import { authConfig } from './edge-config.js';
|
|
4
|
+
|
|
5
|
+
export const { handlers, signIn, signOut, auth } = NextAuth({
|
|
6
|
+
...authConfig,
|
|
7
|
+
providers: [
|
|
8
|
+
Credentials({
|
|
9
|
+
credentials: {
|
|
10
|
+
email: { label: 'Email', type: 'email' },
|
|
11
|
+
password: { label: 'Password', type: 'password' },
|
|
12
|
+
},
|
|
13
|
+
async authorize(credentials) {
|
|
14
|
+
if (!credentials?.email || !credentials?.password) return null;
|
|
15
|
+
|
|
16
|
+
const { getUserByEmail, verifyPassword } = await import('../db/users.js');
|
|
17
|
+
const user = getUserByEmail(credentials.email);
|
|
18
|
+
if (!user) return null;
|
|
19
|
+
|
|
20
|
+
const valid = await verifyPassword(user, credentials.password);
|
|
21
|
+
if (!valid) return null;
|
|
22
|
+
|
|
23
|
+
return { id: user.id, email: user.email, role: user.role };
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
],
|
|
27
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge-safe auth configuration — shared between middleware and server.
|
|
3
|
+
* Contains only JWT/session/callbacks/pages config. No providers, no DB imports.
|
|
4
|
+
* Both instances use the same AUTH_SECRET for JWT signing/verification.
|
|
5
|
+
*
|
|
6
|
+
* Official pattern: https://authjs.dev/guides/edge-compatibility
|
|
7
|
+
*/
|
|
8
|
+
export const authConfig = {
|
|
9
|
+
providers: [],
|
|
10
|
+
session: { strategy: 'jwt' },
|
|
11
|
+
pages: { signIn: '/login' },
|
|
12
|
+
callbacks: {
|
|
13
|
+
jwt({ token, user }) {
|
|
14
|
+
if (user) {
|
|
15
|
+
token.role = user.role;
|
|
16
|
+
}
|
|
17
|
+
return token;
|
|
18
|
+
},
|
|
19
|
+
session({ session, token }) {
|
|
20
|
+
if (session.user) {
|
|
21
|
+
session.user.id = token.sub;
|
|
22
|
+
session.user.role = token.role;
|
|
23
|
+
}
|
|
24
|
+
return session;
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { handlers, auth } from './config.js';
|
|
2
|
+
|
|
3
|
+
// Re-export Auth.js route handlers (GET + POST for [...nextauth])
|
|
4
|
+
export const { GET, POST } = handlers;
|
|
5
|
+
|
|
6
|
+
// Re-export auth for session checking
|
|
7
|
+
export { auth };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get the auth state for the main page (server component).
|
|
11
|
+
* Returns both the session and whether setup is needed, in one call.
|
|
12
|
+
* DB import is dynamic so it doesn't get pulled in at module level.
|
|
13
|
+
*
|
|
14
|
+
* @returns {Promise<{ session: object|null, needsSetup: boolean }>}
|
|
15
|
+
*/
|
|
16
|
+
export async function getPageAuthState() {
|
|
17
|
+
const { getUserCount } = await import('../db/users.js');
|
|
18
|
+
const [session, userCount] = await Promise.all([
|
|
19
|
+
auth(),
|
|
20
|
+
Promise.resolve(getUserCount()),
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
session,
|
|
25
|
+
needsSetup: userCount === 0,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import NextAuth from 'next-auth';
|
|
2
|
+
import { authConfig } from './edge-config.js';
|
|
3
|
+
import { NextResponse } from 'next/server';
|
|
4
|
+
|
|
5
|
+
const { auth } = NextAuth(authConfig);
|
|
6
|
+
|
|
7
|
+
export const middleware = auth((req) => {
|
|
8
|
+
const { pathname } = req.nextUrl;
|
|
9
|
+
|
|
10
|
+
// API routes use their own centralized auth (checkAuth in api/index.js)
|
|
11
|
+
if (pathname.startsWith('/api')) return;
|
|
12
|
+
|
|
13
|
+
// Static assets from public/ — skip auth for common file extensions
|
|
14
|
+
if (/\.(?:svg|png|jpg|jpeg|gif|webp|ico|css|js|woff2?|ttf|eot|mp4|webm)$/i.test(pathname)) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// /login is the only unprotected page (login + first-user setup)
|
|
19
|
+
if (pathname === '/login') {
|
|
20
|
+
if (req.auth) return NextResponse.redirect(new URL('/', req.url));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Everything else requires auth
|
|
25
|
+
if (!req.auth) {
|
|
26
|
+
const response = NextResponse.redirect(new URL('/login', req.url));
|
|
27
|
+
|
|
28
|
+
// Clear stale session cookies that can't be decrypted (e.g. after AUTH_SECRET rotation
|
|
29
|
+
// or container restart). Auth.js clears these internally in route handlers via
|
|
30
|
+
// sessionStore.clean(), but NOT in middleware — so the bad cookie loops forever.
|
|
31
|
+
// Only session-token cookies are cleared; csrf-token and callback-url are left intact.
|
|
32
|
+
const cookieNames = Object.keys(req.cookies.getAll().reduce((acc, c) => { acc[c.name] = true; return acc; }, {}));
|
|
33
|
+
const staleSessionCookies = cookieNames.filter(name =>
|
|
34
|
+
name === 'authjs.session-token' ||
|
|
35
|
+
name === '__Secure-authjs.session-token' ||
|
|
36
|
+
/^authjs\.session-token\.\d+$/.test(name) ||
|
|
37
|
+
/^__Secure-authjs\.session-token\.\d+$/.test(name)
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (staleSessionCookies.length > 0) {
|
|
41
|
+
for (const name of staleSessionCookies) {
|
|
42
|
+
response.cookies.set(name, '', { maxAge: 0, path: '/' });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return response;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const config = {
|
|
51
|
+
// Exclude all _next internal paths (static chunks, HMR, images, Turbopack dev assets)
|
|
52
|
+
matcher: ['/((?!_next|favicon.ico).*)'],
|
|
53
|
+
};
|