@swarmclawai/swarmclaw 0.2.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/README.md +577 -0
- package/bin/server-cmd.js +359 -0
- package/bin/swarmclaw.js +29 -0
- package/bin/swarmclaw.mjs +1504 -0
- package/next.config.ts +33 -0
- package/package.json +112 -0
- package/postcss.config.mjs +7 -0
- package/public/branding/swarmclaw-org-avatar.png +0 -0
- package/public/branding/swarmclaw-org-avatar.svg +58 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/screenshots/agents.png +0 -0
- package/public/screenshots/connectors.png +0 -0
- package/public/screenshots/dashboard.png +0 -0
- package/public/screenshots/new-session-openclaw.png +0 -0
- package/public/screenshots/providers.png +0 -0
- package/public/screenshots/schedules.png +0 -0
- package/public/screenshots/tasks.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/src/app/api/agents/[id]/route.ts +30 -0
- package/src/app/api/agents/[id]/thread/route.ts +66 -0
- package/src/app/api/agents/generate/route.ts +42 -0
- package/src/app/api/agents/route.ts +33 -0
- package/src/app/api/auth/route.ts +25 -0
- package/src/app/api/claude-skills/route.ts +42 -0
- package/src/app/api/clawhub/install/route.ts +39 -0
- package/src/app/api/clawhub/search/route.ts +11 -0
- package/src/app/api/connectors/[id]/route.ts +79 -0
- package/src/app/api/connectors/route.ts +60 -0
- package/src/app/api/credentials/[id]/route.ts +14 -0
- package/src/app/api/credentials/route.ts +31 -0
- package/src/app/api/daemon/health-check/route.ts +11 -0
- package/src/app/api/daemon/route.ts +22 -0
- package/src/app/api/dirs/pick/route.ts +60 -0
- package/src/app/api/dirs/route.ts +29 -0
- package/src/app/api/documents/[id]/route.ts +47 -0
- package/src/app/api/documents/route.ts +93 -0
- package/src/app/api/files/serve/route.ts +69 -0
- package/src/app/api/generate/info/route.ts +12 -0
- package/src/app/api/generate/route.ts +106 -0
- package/src/app/api/ip/route.ts +6 -0
- package/src/app/api/knowledge/[id]/route.ts +61 -0
- package/src/app/api/knowledge/route.ts +48 -0
- package/src/app/api/knowledge/upload/route.ts +86 -0
- package/src/app/api/logs/route.ts +65 -0
- package/src/app/api/mcp-servers/[id]/route.ts +32 -0
- package/src/app/api/mcp-servers/[id]/test/route.ts +23 -0
- package/src/app/api/mcp-servers/[id]/tools/route.ts +32 -0
- package/src/app/api/mcp-servers/route.ts +27 -0
- package/src/app/api/memory/[id]/route.ts +126 -0
- package/src/app/api/memory/maintenance/route.ts +63 -0
- package/src/app/api/memory/route.ts +111 -0
- package/src/app/api/memory-images/[filename]/route.ts +36 -0
- package/src/app/api/orchestrator/run/route.ts +43 -0
- package/src/app/api/plugins/install/route.ts +58 -0
- package/src/app/api/plugins/marketplace/route.ts +33 -0
- package/src/app/api/plugins/route.ts +21 -0
- package/src/app/api/preview-server/route.ts +339 -0
- package/src/app/api/providers/[id]/models/route.ts +29 -0
- package/src/app/api/providers/[id]/route.ts +34 -0
- package/src/app/api/providers/configs/route.ts +7 -0
- package/src/app/api/providers/ollama/route.ts +30 -0
- package/src/app/api/providers/openclaw/health/route.ts +23 -0
- package/src/app/api/providers/route.ts +28 -0
- package/src/app/api/runs/[id]/route.ts +9 -0
- package/src/app/api/runs/route.ts +13 -0
- package/src/app/api/schedules/[id]/route.ts +28 -0
- package/src/app/api/schedules/[id]/run/route.ts +104 -0
- package/src/app/api/schedules/route.ts +78 -0
- package/src/app/api/secrets/[id]/route.ts +29 -0
- package/src/app/api/secrets/route.ts +42 -0
- package/src/app/api/sessions/[id]/browser/route.ts +13 -0
- package/src/app/api/sessions/[id]/chat/route.ts +96 -0
- package/src/app/api/sessions/[id]/clear/route.ts +19 -0
- package/src/app/api/sessions/[id]/deploy/route.ts +34 -0
- package/src/app/api/sessions/[id]/devserver/route.ts +69 -0
- package/src/app/api/sessions/[id]/mailbox/route.ts +70 -0
- package/src/app/api/sessions/[id]/main-loop/route.ts +94 -0
- package/src/app/api/sessions/[id]/messages/route.ts +9 -0
- package/src/app/api/sessions/[id]/retry/route.ts +28 -0
- package/src/app/api/sessions/[id]/route.ts +103 -0
- package/src/app/api/sessions/[id]/stop/route.ts +13 -0
- package/src/app/api/sessions/heartbeat/route.ts +26 -0
- package/src/app/api/sessions/route.ts +85 -0
- package/src/app/api/settings/route.ts +58 -0
- package/src/app/api/setup/check-provider/route.ts +326 -0
- package/src/app/api/setup/doctor/route.ts +250 -0
- package/src/app/api/skills/[id]/route.ts +40 -0
- package/src/app/api/skills/import/route.ts +69 -0
- package/src/app/api/skills/route.ts +28 -0
- package/src/app/api/tasks/[id]/route.ts +102 -0
- package/src/app/api/tasks/route.ts +115 -0
- package/src/app/api/tts/route.ts +40 -0
- package/src/app/api/upload/route.ts +18 -0
- package/src/app/api/uploads/[filename]/route.ts +59 -0
- package/src/app/api/usage/route.ts +35 -0
- package/src/app/api/version/route.ts +81 -0
- package/src/app/api/version/update/route.ts +95 -0
- package/src/app/api/webhooks/[id]/history/route.ts +13 -0
- package/src/app/api/webhooks/[id]/route.ts +204 -0
- package/src/app/api/webhooks/route.ts +37 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +370 -0
- package/src/app/layout.tsx +52 -0
- package/src/app/page.tsx +172 -0
- package/src/cli/index.js +1232 -0
- package/src/cli/index.test.js +281 -0
- package/src/cli/index.ts +1158 -0
- package/src/cli/spec.js +284 -0
- package/src/components/agents/agent-card.tsx +219 -0
- package/src/components/agents/agent-chat-list.tsx +165 -0
- package/src/components/agents/agent-list.tsx +110 -0
- package/src/components/agents/agent-sheet.tsx +1220 -0
- package/src/components/auth/access-key-gate.tsx +248 -0
- package/src/components/auth/setup-wizard.tsx +940 -0
- package/src/components/auth/user-picker.tsx +88 -0
- package/src/components/chat/chat-area.tsx +406 -0
- package/src/components/chat/chat-header.tsx +491 -0
- package/src/components/chat/chat-tool-toggles.tsx +161 -0
- package/src/components/chat/code-block.tsx +146 -0
- package/src/components/chat/dev-server-bar.tsx +39 -0
- package/src/components/chat/message-bubble.tsx +486 -0
- package/src/components/chat/message-list.tsx +299 -0
- package/src/components/chat/session-debug-panel.tsx +196 -0
- package/src/components/chat/streaming-bubble.tsx +85 -0
- package/src/components/chat/thinking-indicator.tsx +26 -0
- package/src/components/chat/tool-call-bubble.tsx +438 -0
- package/src/components/chat/tool-request-banner.tsx +103 -0
- package/src/components/connectors/connector-list.tsx +196 -0
- package/src/components/connectors/connector-sheet.tsx +804 -0
- package/src/components/input/chat-input.tsx +235 -0
- package/src/components/knowledge/knowledge-list.tsx +206 -0
- package/src/components/knowledge/knowledge-sheet.tsx +316 -0
- package/src/components/layout/app-layout.tsx +1016 -0
- package/src/components/layout/daemon-indicator.tsx +56 -0
- package/src/components/layout/mobile-header.tsx +31 -0
- package/src/components/layout/network-banner.tsx +17 -0
- package/src/components/layout/update-banner.tsx +130 -0
- package/src/components/logs/log-list.tsx +358 -0
- package/src/components/mcp-servers/mcp-server-list.tsx +122 -0
- package/src/components/mcp-servers/mcp-server-sheet.tsx +243 -0
- package/src/components/memory/memory-card.tsx +63 -0
- package/src/components/memory/memory-detail.tsx +339 -0
- package/src/components/memory/memory-list.tsx +198 -0
- package/src/components/memory/memory-sheet.tsx +70 -0
- package/src/components/plugins/plugin-list.tsx +60 -0
- package/src/components/plugins/plugin-sheet.tsx +311 -0
- package/src/components/providers/provider-list.tsx +96 -0
- package/src/components/providers/provider-sheet.tsx +542 -0
- package/src/components/runs/run-list.tsx +231 -0
- package/src/components/schedules/schedule-card.tsx +63 -0
- package/src/components/schedules/schedule-list.tsx +76 -0
- package/src/components/schedules/schedule-sheet.tsx +336 -0
- package/src/components/secrets/secret-sheet.tsx +180 -0
- package/src/components/secrets/secrets-list.tsx +91 -0
- package/src/components/sessions/new-session-sheet.tsx +478 -0
- package/src/components/sessions/session-card.tsx +144 -0
- package/src/components/sessions/session-list.tsx +202 -0
- package/src/components/shared/ai-gen-block.tsx +77 -0
- package/src/components/shared/avatar.tsx +48 -0
- package/src/components/shared/bottom-sheet.tsx +30 -0
- package/src/components/shared/confirm-dialog.tsx +47 -0
- package/src/components/shared/connector-platform-icon.tsx +113 -0
- package/src/components/shared/dir-browser.tsx +285 -0
- package/src/components/shared/dropdown.tsx +55 -0
- package/src/components/shared/icon-button.tsx +25 -0
- package/src/components/shared/settings/plugin-manager.tsx +207 -0
- package/src/components/shared/settings/section-capability-policy.tsx +93 -0
- package/src/components/shared/settings/section-embedding.tsx +99 -0
- package/src/components/shared/settings/section-heartbeat.tsx +168 -0
- package/src/components/shared/settings/section-memory.tsx +77 -0
- package/src/components/shared/settings/section-orchestrator.tsx +108 -0
- package/src/components/shared/settings/section-providers.tsx +181 -0
- package/src/components/shared/settings/section-runtime-loop.tsx +183 -0
- package/src/components/shared/settings/section-secrets.tsx +132 -0
- package/src/components/shared/settings/section-user-preferences.tsx +24 -0
- package/src/components/shared/settings/section-voice.tsx +53 -0
- package/src/components/shared/settings/settings-sheet.tsx +88 -0
- package/src/components/shared/settings/types.ts +7 -0
- package/src/components/shared/settings/utils.ts +13 -0
- package/src/components/shared/settings-sheet.tsx +1 -0
- package/src/components/shared/skeleton.tsx +19 -0
- package/src/components/shared/usage-badge.tsx +28 -0
- package/src/components/skills/clawhub-browser.tsx +225 -0
- package/src/components/skills/skill-list.tsx +70 -0
- package/src/components/skills/skill-sheet.tsx +254 -0
- package/src/components/tasks/task-board.tsx +96 -0
- package/src/components/tasks/task-card.tsx +179 -0
- package/src/components/tasks/task-column.tsx +73 -0
- package/src/components/tasks/task-list.tsx +118 -0
- package/src/components/tasks/task-sheet.tsx +415 -0
- package/src/components/ui/avatar.tsx +109 -0
- package/src/components/ui/badge.tsx +48 -0
- package/src/components/ui/button.tsx +64 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/sonner.tsx +22 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +56 -0
- package/src/components/usage/usage-list.tsx +105 -0
- package/src/components/webhooks/webhook-list.tsx +166 -0
- package/src/components/webhooks/webhook-sheet.tsx +402 -0
- package/src/hooks/use-auto-resize.ts +20 -0
- package/src/hooks/use-media-query.ts +21 -0
- package/src/hooks/use-speech-recognition.ts +83 -0
- package/src/instrumentation.ts +8 -0
- package/src/lib/agents.ts +13 -0
- package/src/lib/api-client.ts +100 -0
- package/src/lib/chat.ts +60 -0
- package/src/lib/memory.ts +42 -0
- package/src/lib/openclaw-endpoint.test.ts +48 -0
- package/src/lib/openclaw-endpoint.ts +67 -0
- package/src/lib/provider-config.ts +13 -0
- package/src/lib/providers/anthropic.ts +135 -0
- package/src/lib/providers/claude-cli.ts +202 -0
- package/src/lib/providers/codex-cli.ts +260 -0
- package/src/lib/providers/index.ts +351 -0
- package/src/lib/providers/ollama.ts +131 -0
- package/src/lib/providers/openai.ts +164 -0
- package/src/lib/providers/openclaw.ts +330 -0
- package/src/lib/providers/opencode-cli.ts +164 -0
- package/src/lib/runtime-loop.ts +15 -0
- package/src/lib/schedule-dedupe.test.ts +84 -0
- package/src/lib/schedule-dedupe.ts +174 -0
- package/src/lib/schedule-name.ts +62 -0
- package/src/lib/schedules.ts +16 -0
- package/src/lib/server/agent-registry.ts +70 -0
- package/src/lib/server/api-routes.test.ts +362 -0
- package/src/lib/server/autonomy-contract.ts +200 -0
- package/src/lib/server/build-llm.ts +155 -0
- package/src/lib/server/capability-router.test.ts +21 -0
- package/src/lib/server/capability-router.ts +172 -0
- package/src/lib/server/chat-execution.ts +894 -0
- package/src/lib/server/clawhub-client.test.ts +161 -0
- package/src/lib/server/clawhub-client.ts +26 -0
- package/src/lib/server/connectors/connector-routing.test.ts +243 -0
- package/src/lib/server/connectors/discord.ts +116 -0
- package/src/lib/server/connectors/googlechat.ts +66 -0
- package/src/lib/server/connectors/manager.ts +559 -0
- package/src/lib/server/connectors/matrix.ts +78 -0
- package/src/lib/server/connectors/media.ts +149 -0
- package/src/lib/server/connectors/openclaw.test.ts +375 -0
- package/src/lib/server/connectors/openclaw.ts +1132 -0
- package/src/lib/server/connectors/signal.ts +183 -0
- package/src/lib/server/connectors/slack.ts +258 -0
- package/src/lib/server/connectors/teams.ts +94 -0
- package/src/lib/server/connectors/telegram.ts +221 -0
- package/src/lib/server/connectors/types.ts +62 -0
- package/src/lib/server/connectors/whatsapp.ts +349 -0
- package/src/lib/server/context-manager.ts +232 -0
- package/src/lib/server/cost.ts +31 -0
- package/src/lib/server/daemon-state.ts +354 -0
- package/src/lib/server/data-dir.ts +3 -0
- package/src/lib/server/embeddings.ts +111 -0
- package/src/lib/server/execution-log.ts +257 -0
- package/src/lib/server/gateway/protocol.test.ts +54 -0
- package/src/lib/server/gateway/protocol.ts +114 -0
- package/src/lib/server/heartbeat-service.ts +366 -0
- package/src/lib/server/knowledge-db.test.ts +441 -0
- package/src/lib/server/logger.ts +47 -0
- package/src/lib/server/main-agent-loop.ts +1017 -0
- package/src/lib/server/mcp-client.test.ts +342 -0
- package/src/lib/server/mcp-client.ts +130 -0
- package/src/lib/server/memory-db.ts +1078 -0
- package/src/lib/server/memory-graph.test.ts +153 -0
- package/src/lib/server/memory-graph.ts +138 -0
- package/src/lib/server/openclaw-health.ts +245 -0
- package/src/lib/server/orchestrator-lg.ts +431 -0
- package/src/lib/server/orchestrator.ts +364 -0
- package/src/lib/server/playwright-proxy.mjs +70 -0
- package/src/lib/server/plugins.ts +229 -0
- package/src/lib/server/process-manager.ts +327 -0
- package/src/lib/server/provider-health.ts +113 -0
- package/src/lib/server/queue.ts +859 -0
- package/src/lib/server/runtime-settings.ts +119 -0
- package/src/lib/server/scheduler.ts +196 -0
- package/src/lib/server/session-mailbox.ts +129 -0
- package/src/lib/server/session-run-manager.ts +512 -0
- package/src/lib/server/session-tools/connector.ts +124 -0
- package/src/lib/server/session-tools/context-mgmt.ts +103 -0
- package/src/lib/server/session-tools/context.ts +114 -0
- package/src/lib/server/session-tools/crud.ts +673 -0
- package/src/lib/server/session-tools/delegate.ts +708 -0
- package/src/lib/server/session-tools/file.ts +264 -0
- package/src/lib/server/session-tools/index.ts +164 -0
- package/src/lib/server/session-tools/memory.ts +230 -0
- package/src/lib/server/session-tools/session-info.ts +422 -0
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +166 -0
- package/src/lib/server/session-tools/shell.ts +171 -0
- package/src/lib/server/session-tools/web.ts +408 -0
- package/src/lib/server/session-tools.ts +9 -0
- package/src/lib/server/skills-normalize.ts +130 -0
- package/src/lib/server/storage-mcp.test.ts +161 -0
- package/src/lib/server/storage.ts +670 -0
- package/src/lib/server/stream-agent-chat.ts +571 -0
- package/src/lib/server/task-reports.ts +122 -0
- package/src/lib/server/task-result.ts +161 -0
- package/src/lib/server/task-validation.test.ts +27 -0
- package/src/lib/server/task-validation.ts +90 -0
- package/src/lib/server/tool-capability-policy.test.ts +58 -0
- package/src/lib/server/tool-capability-policy.ts +262 -0
- package/src/lib/sessions.ts +68 -0
- package/src/lib/tasks.ts +20 -0
- package/src/lib/tts.ts +42 -0
- package/src/lib/upload.ts +10 -0
- package/src/lib/utils.ts +6 -0
- package/src/proxy.ts +43 -0
- package/src/stores/use-app-store.ts +468 -0
- package/src/stores/use-chat-store.ts +323 -0
- package/src/types/index.ts +621 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadSessions } from '@/lib/server/storage'
|
|
3
|
+
|
|
4
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
5
|
+
const { id } = await params
|
|
6
|
+
const sessions = loadSessions()
|
|
7
|
+
if (!sessions[id]) return new NextResponse(null, { status: 404 })
|
|
8
|
+
return NextResponse.json(sessions[id].messages)
|
|
9
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadSessions, saveSessions } from '@/lib/server/storage'
|
|
3
|
+
|
|
4
|
+
export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
5
|
+
const { id } = await params
|
|
6
|
+
const sessions = loadSessions()
|
|
7
|
+
const session = sessions[id]
|
|
8
|
+
if (!session) return new NextResponse(null, { status: 404 })
|
|
9
|
+
|
|
10
|
+
const msgs = session.messages
|
|
11
|
+
// Pop trailing assistant messages to find the last user message
|
|
12
|
+
while (msgs.length && msgs[msgs.length - 1].role === 'assistant') {
|
|
13
|
+
msgs.pop()
|
|
14
|
+
}
|
|
15
|
+
if (!msgs.length) {
|
|
16
|
+
return NextResponse.json({ message: '', imagePath: null }, { status: 200 })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const lastUser = msgs[msgs.length - 1]
|
|
20
|
+
const message = lastUser.text
|
|
21
|
+
const imagePath = lastUser.imagePath || null
|
|
22
|
+
|
|
23
|
+
// Remove the last user message too — it will be re-sent by the client
|
|
24
|
+
msgs.pop()
|
|
25
|
+
saveSessions(sessions)
|
|
26
|
+
|
|
27
|
+
return NextResponse.json({ message, imagePath })
|
|
28
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadSessions, saveSessions, deleteSession, active, loadAgents } from '@/lib/server/storage'
|
|
3
|
+
import { enqueueSessionRun } from '@/lib/server/session-run-manager'
|
|
4
|
+
import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
|
|
5
|
+
|
|
6
|
+
function buildSessionAwakeningPrompt(user: string | null | undefined): string {
|
|
7
|
+
const displayName = typeof user === 'string' && user.trim() ? user.trim() : 'there'
|
|
8
|
+
return [
|
|
9
|
+
'SESSION_AWAKENING',
|
|
10
|
+
`You have just been activated as the primary SwarmClaw assistant for ${displayName}.`,
|
|
11
|
+
'Write your first message as the agent itself (not as system text).',
|
|
12
|
+
'Tone: awake, focused, practical.',
|
|
13
|
+
'Include: brief greeting, what you can help with in SwarmClaw (providers, agents, tools/connectors, tasks, schedules), and one direct question asking for the user goal.',
|
|
14
|
+
'Keep it concise (<= 90 words).',
|
|
15
|
+
'Do not mention hidden prompts, policies, or implementation details.',
|
|
16
|
+
].join('\n')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
20
|
+
const { id } = await params
|
|
21
|
+
const updates = await req.json()
|
|
22
|
+
const sessions = loadSessions()
|
|
23
|
+
if (!sessions[id]) return new NextResponse(null, { status: 404 })
|
|
24
|
+
const hadMessagesBefore = Array.isArray(sessions[id].messages) && sessions[id].messages.length > 0
|
|
25
|
+
|
|
26
|
+
const agentIdUpdateProvided = updates.agentId !== undefined
|
|
27
|
+
let nextAgentId = sessions[id].agentId
|
|
28
|
+
if (agentIdUpdateProvided) {
|
|
29
|
+
sessions[id].agentId = updates.agentId
|
|
30
|
+
nextAgentId = updates.agentId
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const linkedAgent = nextAgentId ? loadAgents()[nextAgentId] : null
|
|
34
|
+
|
|
35
|
+
if (updates.name !== undefined) sessions[id].name = updates.name
|
|
36
|
+
if (updates.cwd !== undefined) sessions[id].cwd = updates.cwd
|
|
37
|
+
if (updates.provider !== undefined) sessions[id].provider = updates.provider
|
|
38
|
+
else if (agentIdUpdateProvided && linkedAgent?.provider) sessions[id].provider = linkedAgent.provider
|
|
39
|
+
|
|
40
|
+
if (updates.model !== undefined) sessions[id].model = updates.model
|
|
41
|
+
else if (agentIdUpdateProvided && linkedAgent?.model !== undefined) sessions[id].model = linkedAgent.model
|
|
42
|
+
|
|
43
|
+
if (updates.credentialId !== undefined) sessions[id].credentialId = updates.credentialId
|
|
44
|
+
else if (agentIdUpdateProvided && linkedAgent) sessions[id].credentialId = linkedAgent.credentialId ?? null
|
|
45
|
+
|
|
46
|
+
if (updates.tools !== undefined) sessions[id].tools = updates.tools
|
|
47
|
+
else if (agentIdUpdateProvided && linkedAgent) sessions[id].tools = Array.isArray(linkedAgent.tools) ? linkedAgent.tools : []
|
|
48
|
+
|
|
49
|
+
if (updates.apiEndpoint !== undefined) {
|
|
50
|
+
sessions[id].apiEndpoint = normalizeProviderEndpoint(
|
|
51
|
+
updates.provider || sessions[id].provider,
|
|
52
|
+
updates.apiEndpoint,
|
|
53
|
+
)
|
|
54
|
+
} else if (agentIdUpdateProvided && linkedAgent) {
|
|
55
|
+
sessions[id].apiEndpoint = normalizeProviderEndpoint(
|
|
56
|
+
linkedAgent.provider,
|
|
57
|
+
linkedAgent.apiEndpoint ?? null,
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
if (updates.heartbeatEnabled !== undefined) sessions[id].heartbeatEnabled = updates.heartbeatEnabled
|
|
61
|
+
if (updates.heartbeatIntervalSec !== undefined) sessions[id].heartbeatIntervalSec = updates.heartbeatIntervalSec
|
|
62
|
+
if (updates.pinned !== undefined) sessions[id].pinned = !!updates.pinned
|
|
63
|
+
if (!Array.isArray(sessions[id].messages)) sessions[id].messages = []
|
|
64
|
+
|
|
65
|
+
const shouldKickoffAwakening = sessions[id].name === '__main__'
|
|
66
|
+
&& agentIdUpdateProvided
|
|
67
|
+
&& !!sessions[id].agentId
|
|
68
|
+
&& !hadMessagesBefore
|
|
69
|
+
&& sessions[id].messages.length === 0
|
|
70
|
+
|
|
71
|
+
saveSessions(sessions)
|
|
72
|
+
|
|
73
|
+
if (shouldKickoffAwakening) {
|
|
74
|
+
try {
|
|
75
|
+
enqueueSessionRun({
|
|
76
|
+
sessionId: id,
|
|
77
|
+
message: buildSessionAwakeningPrompt(sessions[id].user),
|
|
78
|
+
internal: true,
|
|
79
|
+
source: 'session-awakening',
|
|
80
|
+
mode: 'steer',
|
|
81
|
+
dedupeKey: `session-awakening:${id}`,
|
|
82
|
+
})
|
|
83
|
+
} catch {
|
|
84
|
+
// Best-effort kickoff only.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return NextResponse.json(sessions[id])
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
92
|
+
const { id } = await params
|
|
93
|
+
const sessions = loadSessions()
|
|
94
|
+
if (sessions[id]?.name === '__main__') {
|
|
95
|
+
return new NextResponse('Cannot delete main chat session', { status: 403 })
|
|
96
|
+
}
|
|
97
|
+
if (active.has(id)) {
|
|
98
|
+
try { active.get(id).kill() } catch {}
|
|
99
|
+
active.delete(id)
|
|
100
|
+
}
|
|
101
|
+
deleteSession(id)
|
|
102
|
+
return new NextResponse('OK')
|
|
103
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { active } from '@/lib/server/storage'
|
|
3
|
+
import { cancelSessionRuns } from '@/lib/server/session-run-manager'
|
|
4
|
+
|
|
5
|
+
export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
6
|
+
const { id } = await params
|
|
7
|
+
const cancel = cancelSessionRuns(id, 'Stopped by user')
|
|
8
|
+
if (active.has(id)) {
|
|
9
|
+
try { active.get(id).kill() } catch {}
|
|
10
|
+
active.delete(id)
|
|
11
|
+
}
|
|
12
|
+
return NextResponse.json({ ok: true, ...cancel })
|
|
13
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { disableAllSessionHeartbeats, loadSettings, saveSettings } from '@/lib/server/storage'
|
|
3
|
+
import { cancelAllHeartbeatRuns } from '@/lib/server/session-run-manager'
|
|
4
|
+
|
|
5
|
+
export async function POST(req: Request) {
|
|
6
|
+
const body = await req.json().catch(() => ({}))
|
|
7
|
+
const action = typeof body?.action === 'string' ? body.action : 'disable_all'
|
|
8
|
+
if (action !== 'disable_all') {
|
|
9
|
+
return NextResponse.json({ error: 'Unsupported action' }, { status: 400 })
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const updatedSessions = disableAllSessionHeartbeats()
|
|
13
|
+
const settings = loadSettings()
|
|
14
|
+
if ((settings.heartbeatIntervalSec ?? 120) !== 0) {
|
|
15
|
+
settings.heartbeatIntervalSec = 0
|
|
16
|
+
saveSettings(settings)
|
|
17
|
+
}
|
|
18
|
+
const { cancelledQueued, abortedRunning } = cancelAllHeartbeatRuns('Heartbeat disabled via global switch')
|
|
19
|
+
|
|
20
|
+
return NextResponse.json({
|
|
21
|
+
ok: true,
|
|
22
|
+
updatedSessions,
|
|
23
|
+
cancelledQueued,
|
|
24
|
+
abortedRunning,
|
|
25
|
+
})
|
|
26
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import crypto from 'crypto'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
import path from 'path'
|
|
5
|
+
import { loadSessions, saveSessions, deleteSession, active, loadAgents } from '@/lib/server/storage'
|
|
6
|
+
import { getSessionRunState } from '@/lib/server/session-run-manager'
|
|
7
|
+
import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
|
|
8
|
+
|
|
9
|
+
export async function GET() {
|
|
10
|
+
const sessions = loadSessions()
|
|
11
|
+
for (const id of Object.keys(sessions)) {
|
|
12
|
+
const run = getSessionRunState(id)
|
|
13
|
+
sessions[id].active = active.has(id) || !!run.runningRunId
|
|
14
|
+
sessions[id].queuedCount = run.queueLength
|
|
15
|
+
sessions[id].currentRunId = run.runningRunId || null
|
|
16
|
+
}
|
|
17
|
+
return NextResponse.json(sessions)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function DELETE(req: Request) {
|
|
21
|
+
const { ids } = await req.json() as { ids: string[] }
|
|
22
|
+
if (!Array.isArray(ids) || !ids.length) {
|
|
23
|
+
return new NextResponse('Missing ids', { status: 400 })
|
|
24
|
+
}
|
|
25
|
+
const sessions = loadSessions()
|
|
26
|
+
for (const id of ids) {
|
|
27
|
+
if (sessions[id]?.name === '__main__') continue
|
|
28
|
+
if (active.has(id)) {
|
|
29
|
+
try { active.get(id).kill() } catch {}
|
|
30
|
+
active.delete(id)
|
|
31
|
+
}
|
|
32
|
+
deleteSession(id)
|
|
33
|
+
}
|
|
34
|
+
return NextResponse.json({ deleted: ids.length })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function POST(req: Request) {
|
|
38
|
+
const body = await req.json()
|
|
39
|
+
let cwd = (body.cwd || '').trim()
|
|
40
|
+
if (cwd.startsWith('~/')) cwd = path.join(os.homedir(), cwd.slice(2))
|
|
41
|
+
else if (cwd === '~' || !cwd) cwd = os.homedir()
|
|
42
|
+
|
|
43
|
+
const id = body.id || crypto.randomBytes(4).toString('hex')
|
|
44
|
+
const sessions = loadSessions()
|
|
45
|
+
const agent = body.agentId ? loadAgents()[body.agentId] : null
|
|
46
|
+
const requestedTools = Array.isArray(body.tools) ? body.tools : null
|
|
47
|
+
const resolvedTools = requestedTools ?? (Array.isArray(agent?.tools) ? agent.tools : [])
|
|
48
|
+
|
|
49
|
+
// If session with this ID already exists, return it as-is
|
|
50
|
+
if (body.id && sessions[id]) {
|
|
51
|
+
return NextResponse.json(sessions[id])
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const sessionName = body.name || 'New Session'
|
|
55
|
+
|
|
56
|
+
sessions[id] = {
|
|
57
|
+
id, name: sessionName, cwd,
|
|
58
|
+
user: body.user || 'wayde',
|
|
59
|
+
provider: body.provider || agent?.provider || 'claude-cli',
|
|
60
|
+
model: body.model || agent?.model || '',
|
|
61
|
+
credentialId: body.credentialId || agent?.credentialId || null,
|
|
62
|
+
apiEndpoint: normalizeProviderEndpoint(
|
|
63
|
+
body.provider || agent?.provider || 'claude-cli',
|
|
64
|
+
body.apiEndpoint || agent?.apiEndpoint || null,
|
|
65
|
+
),
|
|
66
|
+
claudeSessionId: null,
|
|
67
|
+
codexThreadId: null,
|
|
68
|
+
opencodeSessionId: null,
|
|
69
|
+
delegateResumeIds: {
|
|
70
|
+
claudeCode: null,
|
|
71
|
+
codex: null,
|
|
72
|
+
opencode: null,
|
|
73
|
+
},
|
|
74
|
+
messages: Array.isArray(body.messages) ? body.messages : [],
|
|
75
|
+
createdAt: Date.now(), lastActiveAt: Date.now(),
|
|
76
|
+
sessionType: body.sessionType || 'human',
|
|
77
|
+
agentId: body.agentId || null,
|
|
78
|
+
parentSessionId: body.parentSessionId || null,
|
|
79
|
+
tools: resolvedTools,
|
|
80
|
+
heartbeatEnabled: body.heartbeatEnabled ?? null,
|
|
81
|
+
heartbeatIntervalSec: body.heartbeatIntervalSec ?? null,
|
|
82
|
+
}
|
|
83
|
+
saveSessions(sessions)
|
|
84
|
+
return NextResponse.json(sessions[id])
|
|
85
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { loadSettings, saveSettings } from '@/lib/server/storage'
|
|
3
|
+
|
|
4
|
+
const MEMORY_DEPTH_MIN = 0
|
|
5
|
+
const MEMORY_DEPTH_MAX = 12
|
|
6
|
+
const MEMORY_PER_LOOKUP_MIN = 1
|
|
7
|
+
const MEMORY_PER_LOOKUP_MAX = 200
|
|
8
|
+
const MEMORY_LINKED_MIN = 0
|
|
9
|
+
const MEMORY_LINKED_MAX = 1000
|
|
10
|
+
|
|
11
|
+
function parseIntSetting(value: unknown, fallback: number, min: number, max: number): number {
|
|
12
|
+
const parsed = typeof value === 'number'
|
|
13
|
+
? value
|
|
14
|
+
: typeof value === 'string'
|
|
15
|
+
? Number.parseInt(value, 10)
|
|
16
|
+
: Number.NaN
|
|
17
|
+
if (!Number.isFinite(parsed)) return fallback
|
|
18
|
+
return Math.max(min, Math.min(max, Math.trunc(parsed)))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function GET() {
|
|
22
|
+
return NextResponse.json(loadSettings())
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function PUT(req: Request) {
|
|
26
|
+
const body = await req.json()
|
|
27
|
+
const settings = loadSettings()
|
|
28
|
+
Object.assign(settings, body)
|
|
29
|
+
|
|
30
|
+
const nextDepth = parseIntSetting(
|
|
31
|
+
settings.memoryReferenceDepth ?? settings.memoryMaxDepth,
|
|
32
|
+
3,
|
|
33
|
+
MEMORY_DEPTH_MIN,
|
|
34
|
+
MEMORY_DEPTH_MAX,
|
|
35
|
+
)
|
|
36
|
+
const nextPerLookup = parseIntSetting(
|
|
37
|
+
settings.maxMemoriesPerLookup ?? settings.memoryMaxPerLookup,
|
|
38
|
+
20,
|
|
39
|
+
MEMORY_PER_LOOKUP_MIN,
|
|
40
|
+
MEMORY_PER_LOOKUP_MAX,
|
|
41
|
+
)
|
|
42
|
+
const nextLinked = parseIntSetting(
|
|
43
|
+
settings.maxLinkedMemoriesExpanded,
|
|
44
|
+
60,
|
|
45
|
+
MEMORY_LINKED_MIN,
|
|
46
|
+
MEMORY_LINKED_MAX,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
// Keep new and legacy keys synchronized for backward compatibility.
|
|
50
|
+
settings.memoryReferenceDepth = nextDepth
|
|
51
|
+
settings.memoryMaxDepth = nextDepth
|
|
52
|
+
settings.maxMemoriesPerLookup = nextPerLookup
|
|
53
|
+
settings.memoryMaxPerLookup = nextPerLookup
|
|
54
|
+
settings.maxLinkedMemoriesExpanded = nextLinked
|
|
55
|
+
|
|
56
|
+
saveSettings(settings)
|
|
57
|
+
return NextResponse.json(settings)
|
|
58
|
+
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { WebSocket } from 'ws'
|
|
3
|
+
import { randomUUID } from 'crypto'
|
|
4
|
+
import { loadCredentials, decryptKey } from '@/lib/server/storage'
|
|
5
|
+
import { buildOpenClawConnectParams, getDeviceId } from '@/lib/providers/openclaw'
|
|
6
|
+
|
|
7
|
+
type SetupProvider =
|
|
8
|
+
| 'openai'
|
|
9
|
+
| 'anthropic'
|
|
10
|
+
| 'google'
|
|
11
|
+
| 'deepseek'
|
|
12
|
+
| 'groq'
|
|
13
|
+
| 'together'
|
|
14
|
+
| 'mistral'
|
|
15
|
+
| 'xai'
|
|
16
|
+
| 'fireworks'
|
|
17
|
+
| 'ollama'
|
|
18
|
+
| 'openclaw'
|
|
19
|
+
|
|
20
|
+
const OPENAI_COMPATIBLE_PROVIDER_INFO: Record<
|
|
21
|
+
'openai' | 'google' | 'deepseek' | 'groq' | 'together' | 'mistral' | 'xai' | 'fireworks',
|
|
22
|
+
{ name: string; defaultEndpoint: string }
|
|
23
|
+
> = {
|
|
24
|
+
openai: { name: 'OpenAI', defaultEndpoint: 'https://api.openai.com/v1' },
|
|
25
|
+
google: { name: 'Google Gemini', defaultEndpoint: 'https://generativelanguage.googleapis.com/v1beta/openai' },
|
|
26
|
+
deepseek: { name: 'DeepSeek', defaultEndpoint: 'https://api.deepseek.com/v1' },
|
|
27
|
+
groq: { name: 'Groq', defaultEndpoint: 'https://api.groq.com/openai/v1' },
|
|
28
|
+
together: { name: 'Together AI', defaultEndpoint: 'https://api.together.xyz/v1' },
|
|
29
|
+
mistral: { name: 'Mistral AI', defaultEndpoint: 'https://api.mistral.ai/v1' },
|
|
30
|
+
xai: { name: 'xAI (Grok)', defaultEndpoint: 'https://api.x.ai/v1' },
|
|
31
|
+
fireworks: { name: 'Fireworks AI', defaultEndpoint: 'https://api.fireworks.ai/inference/v1' },
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface SetupCheckBody {
|
|
35
|
+
provider?: string
|
|
36
|
+
apiKey?: string
|
|
37
|
+
credentialId?: string
|
|
38
|
+
endpoint?: string
|
|
39
|
+
model?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function clean(value: unknown): string {
|
|
43
|
+
return typeof value === 'string' ? value.trim() : ''
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseBody(input: unknown): SetupCheckBody {
|
|
47
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) return {}
|
|
48
|
+
return input as SetupCheckBody
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function parseErrorMessage(res: Response, fallback: string): Promise<string> {
|
|
52
|
+
const text = await res.text().catch(() => '')
|
|
53
|
+
if (!text) return fallback
|
|
54
|
+
try {
|
|
55
|
+
const parsed = JSON.parse(text)
|
|
56
|
+
if (typeof parsed?.error?.message === 'string' && parsed.error.message.trim()) return parsed.error.message.trim()
|
|
57
|
+
if (typeof parsed?.error === 'string' && parsed.error.trim()) return parsed.error.trim()
|
|
58
|
+
if (typeof parsed?.message === 'string' && parsed.message.trim()) return parsed.message.trim()
|
|
59
|
+
if (typeof parsed?.detail === 'string' && parsed.detail.trim()) return parsed.detail.trim()
|
|
60
|
+
} catch {
|
|
61
|
+
// Non-JSON response body.
|
|
62
|
+
}
|
|
63
|
+
return text.slice(0, 300).trim() || fallback
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function checkOpenAiCompatible(
|
|
67
|
+
providerName: string,
|
|
68
|
+
apiKey: string,
|
|
69
|
+
endpointRaw: string,
|
|
70
|
+
defaultEndpoint: string,
|
|
71
|
+
): Promise<{ ok: boolean; message: string; normalizedEndpoint: string }> {
|
|
72
|
+
const normalizedEndpoint = (endpointRaw || defaultEndpoint).replace(/\/+$/, '')
|
|
73
|
+
const res = await fetch(`${normalizedEndpoint}/models`, {
|
|
74
|
+
headers: {
|
|
75
|
+
authorization: `Bearer ${apiKey}`,
|
|
76
|
+
},
|
|
77
|
+
signal: AbortSignal.timeout(10_000),
|
|
78
|
+
cache: 'no-store',
|
|
79
|
+
})
|
|
80
|
+
if (!res.ok) {
|
|
81
|
+
const detail = await parseErrorMessage(res, `${providerName} returned ${res.status}.`)
|
|
82
|
+
return { ok: false, message: detail, normalizedEndpoint }
|
|
83
|
+
}
|
|
84
|
+
const payload = await res.json().catch(() => ({} as any))
|
|
85
|
+
const count = Array.isArray(payload?.data) ? payload.data.length : 0
|
|
86
|
+
return {
|
|
87
|
+
ok: true,
|
|
88
|
+
message: count > 0 ? `Connected to ${providerName}. ${count} model(s) available.` : `Connected to ${providerName}.`,
|
|
89
|
+
normalizedEndpoint,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function checkAnthropic(apiKey: string, modelRaw: string): Promise<{ ok: boolean; message: string }> {
|
|
94
|
+
const model = modelRaw || 'claude-sonnet-4-6'
|
|
95
|
+
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: {
|
|
98
|
+
'x-api-key': apiKey,
|
|
99
|
+
'anthropic-version': '2023-06-01',
|
|
100
|
+
'content-type': 'application/json',
|
|
101
|
+
},
|
|
102
|
+
body: JSON.stringify({
|
|
103
|
+
model,
|
|
104
|
+
max_tokens: 12,
|
|
105
|
+
messages: [{ role: 'user', content: 'Reply with ANTHROPIC_SETUP_OK' }],
|
|
106
|
+
}),
|
|
107
|
+
signal: AbortSignal.timeout(15_000),
|
|
108
|
+
cache: 'no-store',
|
|
109
|
+
})
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
const detail = await parseErrorMessage(res, `Anthropic returned ${res.status}.`)
|
|
112
|
+
return { ok: false, message: detail }
|
|
113
|
+
}
|
|
114
|
+
const payload = await res.json().catch(() => ({} as any))
|
|
115
|
+
const text = typeof payload?.content?.[0]?.text === 'string' ? payload.content[0].text : ''
|
|
116
|
+
return { ok: true, message: text ? `Connected to Anthropic. Sample: ${text.slice(0, 120)}` : 'Connected to Anthropic.' }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function checkOllama(endpointRaw: string): Promise<{ ok: boolean; message: string; normalizedEndpoint: string; recommendedModel?: string }> {
|
|
120
|
+
const normalizedEndpoint = (endpointRaw || 'http://localhost:11434').replace(/\/+$/, '')
|
|
121
|
+
const res = await fetch(`${normalizedEndpoint}/api/tags`, {
|
|
122
|
+
signal: AbortSignal.timeout(8_000),
|
|
123
|
+
cache: 'no-store',
|
|
124
|
+
})
|
|
125
|
+
if (!res.ok) {
|
|
126
|
+
const detail = await parseErrorMessage(res, `Ollama returned ${res.status}.`)
|
|
127
|
+
return { ok: false, message: detail, normalizedEndpoint }
|
|
128
|
+
}
|
|
129
|
+
const payload = await res.json().catch(() => ({} as any))
|
|
130
|
+
const models = Array.isArray(payload?.models) ? payload.models : []
|
|
131
|
+
const firstModel = typeof models[0]?.name === 'string'
|
|
132
|
+
? String(models[0].name).replace(/:latest$/, '')
|
|
133
|
+
: undefined
|
|
134
|
+
if (models.length === 0) {
|
|
135
|
+
return {
|
|
136
|
+
ok: true,
|
|
137
|
+
message: 'Connected to Ollama, but no models are installed yet. Run `ollama pull <model>` to add one.',
|
|
138
|
+
normalizedEndpoint,
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
ok: true,
|
|
143
|
+
message: `Connected to Ollama. ${models.length} model(s) available.`,
|
|
144
|
+
normalizedEndpoint,
|
|
145
|
+
recommendedModel: firstModel,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function normalizeOpenClawUrl(raw: string): { httpUrl: string; wsUrl: string } {
|
|
150
|
+
let url = (raw || 'http://localhost:18789').replace(/\/+$/, '')
|
|
151
|
+
if (!/^(https?|wss?):\/\//i.test(url)) url = `http://${url}`
|
|
152
|
+
const httpUrl = url.replace(/^ws:/i, 'http:').replace(/^wss:/i, 'https:')
|
|
153
|
+
const wsUrl = httpUrl.replace(/^http:/i, 'ws:').replace(/^https:/i, 'wss:')
|
|
154
|
+
return { httpUrl, wsUrl }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function checkOpenClaw(apiKey: string, endpointRaw: string): Promise<{ ok: boolean; message: string; normalizedEndpoint: string; deviceId?: string; errorCode?: string }> {
|
|
158
|
+
const { httpUrl: normalizedEndpoint, wsUrl } = normalizeOpenClawUrl(endpointRaw)
|
|
159
|
+
const token = apiKey || undefined
|
|
160
|
+
const deviceId = getDeviceId()
|
|
161
|
+
|
|
162
|
+
// Always connect with device auth — the gateway may accept token-only for
|
|
163
|
+
// the connect handshake but still require device identity for agent ops.
|
|
164
|
+
const result = await attemptOpenClawConnect(wsUrl, token, true)
|
|
165
|
+
if (result.ok) {
|
|
166
|
+
return { ok: true, message: 'Connected to OpenClaw gateway.', normalizedEndpoint, deviceId }
|
|
167
|
+
}
|
|
168
|
+
if (result.errorCode === 'PAIRING_REQUIRED') {
|
|
169
|
+
return {
|
|
170
|
+
ok: false,
|
|
171
|
+
errorCode: 'PAIRING_REQUIRED',
|
|
172
|
+
message: 'Device needs approval on your gateway.',
|
|
173
|
+
normalizedEndpoint,
|
|
174
|
+
deviceId,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (result.errorCode === 'DEVICE_AUTH_INVALID') {
|
|
178
|
+
return {
|
|
179
|
+
ok: false,
|
|
180
|
+
errorCode: 'DEVICE_AUTH_INVALID',
|
|
181
|
+
message: 'Device signature rejected. Your gateway may be running a different protocol version.',
|
|
182
|
+
normalizedEndpoint,
|
|
183
|
+
deviceId,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { ok: false, message: result.message, normalizedEndpoint, deviceId, errorCode: result.errorCode }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function attemptOpenClawConnect(
|
|
190
|
+
wsUrl: string,
|
|
191
|
+
token: string | undefined,
|
|
192
|
+
useDeviceAuth: boolean,
|
|
193
|
+
): Promise<{ ok: boolean; message: string; errorCode?: string }> {
|
|
194
|
+
return new Promise((resolve) => {
|
|
195
|
+
let settled = false
|
|
196
|
+
const done = (result: { ok: boolean; message: string; errorCode?: string }) => {
|
|
197
|
+
if (settled) return
|
|
198
|
+
settled = true
|
|
199
|
+
clearTimeout(timer)
|
|
200
|
+
try { ws.close() } catch {}
|
|
201
|
+
resolve(result)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const timer = setTimeout(() => {
|
|
205
|
+
done({ ok: false, message: 'Connection timed out. Verify the gateway URL and network access.' })
|
|
206
|
+
}, 10_000)
|
|
207
|
+
|
|
208
|
+
const ws = new WebSocket(wsUrl)
|
|
209
|
+
let connectId: string | null = null
|
|
210
|
+
|
|
211
|
+
ws.on('message', (data) => {
|
|
212
|
+
try {
|
|
213
|
+
const msg = JSON.parse(data.toString())
|
|
214
|
+
if (msg.event === 'connect.challenge') {
|
|
215
|
+
connectId = randomUUID()
|
|
216
|
+
ws.send(JSON.stringify({
|
|
217
|
+
type: 'req',
|
|
218
|
+
id: connectId,
|
|
219
|
+
method: 'connect',
|
|
220
|
+
params: buildOpenClawConnectParams(token, msg.payload?.nonce, { useDeviceAuth }),
|
|
221
|
+
}))
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
if (msg.type === 'res' && msg.id === connectId) {
|
|
225
|
+
if (msg.ok) {
|
|
226
|
+
done({ ok: true, message: 'Connected.' })
|
|
227
|
+
} else {
|
|
228
|
+
const message = msg.error?.message || 'Gateway rejected the connection.'
|
|
229
|
+
// Extract error code from structured field or infer from message text
|
|
230
|
+
let errorCode = (msg.error?.details?.code ?? msg.error?.code) as string | undefined
|
|
231
|
+
if (!errorCode) {
|
|
232
|
+
const m = message.toLowerCase()
|
|
233
|
+
if (m.includes('pairing') || m.includes('not paired') || m.includes('pending approval')) errorCode = 'PAIRING_REQUIRED'
|
|
234
|
+
else if (m.includes('signature') || m.includes('device') || m.includes('identity')) errorCode = 'DEVICE_AUTH_INVALID'
|
|
235
|
+
}
|
|
236
|
+
done({ ok: false, message, errorCode })
|
|
237
|
+
}
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
} catch {
|
|
241
|
+
done({ ok: false, message: 'Unexpected response from gateway.' })
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
ws.on('error', (err) => {
|
|
246
|
+
done({ ok: false, message: `Connection failed: ${err.message}` })
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
ws.on('close', (code, reason) => {
|
|
250
|
+
if (code === 1008) {
|
|
251
|
+
done({ ok: false, message: `Unauthorized: ${reason?.toString() || 'invalid token'}` })
|
|
252
|
+
} else {
|
|
253
|
+
done({ ok: false, message: `Connection closed (${code})` })
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export async function POST(req: Request) {
|
|
260
|
+
const body = parseBody(await req.json().catch(() => ({})))
|
|
261
|
+
const provider = clean(body.provider) as SetupProvider
|
|
262
|
+
let apiKey = clean(body.apiKey)
|
|
263
|
+
const credentialId = clean(body.credentialId)
|
|
264
|
+
const endpoint = clean(body.endpoint)
|
|
265
|
+
const model = clean(body.model)
|
|
266
|
+
|
|
267
|
+
// Resolve credentialId to an API key if no raw key was provided
|
|
268
|
+
if (!apiKey && credentialId) {
|
|
269
|
+
try {
|
|
270
|
+
const creds = loadCredentials()
|
|
271
|
+
const cred = creds[credentialId]
|
|
272
|
+
if (cred?.encryptedKey) {
|
|
273
|
+
apiKey = decryptKey(cred.encryptedKey)
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
276
|
+
return NextResponse.json({ ok: false, message: 'Failed to decrypt credential.' }, { status: 500 })
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!provider) {
|
|
281
|
+
return NextResponse.json({ ok: false, message: 'Provider is required.' }, { status: 400 })
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
switch (provider) {
|
|
286
|
+
case 'openai': {
|
|
287
|
+
if (!apiKey) return NextResponse.json({ ok: false, message: 'OpenAI API key is required.' })
|
|
288
|
+
const info = OPENAI_COMPATIBLE_PROVIDER_INFO.openai
|
|
289
|
+
const result = await checkOpenAiCompatible(info.name, apiKey, endpoint, info.defaultEndpoint)
|
|
290
|
+
return NextResponse.json(result)
|
|
291
|
+
}
|
|
292
|
+
case 'anthropic': {
|
|
293
|
+
if (!apiKey) return NextResponse.json({ ok: false, message: 'Anthropic API key is required.' })
|
|
294
|
+
const result = await checkAnthropic(apiKey, model)
|
|
295
|
+
return NextResponse.json(result)
|
|
296
|
+
}
|
|
297
|
+
case 'google':
|
|
298
|
+
case 'deepseek':
|
|
299
|
+
case 'groq':
|
|
300
|
+
case 'together':
|
|
301
|
+
case 'mistral':
|
|
302
|
+
case 'xai':
|
|
303
|
+
case 'fireworks': {
|
|
304
|
+
const info = OPENAI_COMPATIBLE_PROVIDER_INFO[provider]
|
|
305
|
+
if (!apiKey) return NextResponse.json({ ok: false, message: `${info.name} API key is required.` })
|
|
306
|
+
const result = await checkOpenAiCompatible(info.name, apiKey, endpoint, info.defaultEndpoint)
|
|
307
|
+
return NextResponse.json(result)
|
|
308
|
+
}
|
|
309
|
+
case 'ollama': {
|
|
310
|
+
const result = await checkOllama(endpoint)
|
|
311
|
+
return NextResponse.json(result)
|
|
312
|
+
}
|
|
313
|
+
case 'openclaw': {
|
|
314
|
+
const result = await checkOpenClaw(apiKey, endpoint)
|
|
315
|
+
return NextResponse.json(result)
|
|
316
|
+
}
|
|
317
|
+
default:
|
|
318
|
+
return NextResponse.json({ ok: false, message: `Unsupported provider: ${provider}` }, { status: 400 })
|
|
319
|
+
}
|
|
320
|
+
} catch (err: any) {
|
|
321
|
+
const message = err?.name === 'TimeoutError'
|
|
322
|
+
? 'Connection check timed out. Verify endpoint/network and try again.'
|
|
323
|
+
: (err?.message || 'Failed to validate provider setup.')
|
|
324
|
+
return NextResponse.json({ ok: false, message }, { status: 500 })
|
|
325
|
+
}
|
|
326
|
+
}
|