@swarmclawai/swarmclaw 0.7.2 → 0.7.4
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 +116 -50
- package/bin/package-manager.js +157 -0
- package/bin/package-manager.test.js +90 -0
- package/bin/server-cmd.js +38 -7
- package/bin/swarmclaw.js +54 -4
- package/bin/update-cmd.js +48 -10
- package/bin/update-cmd.test.js +55 -0
- package/package.json +8 -3
- package/scripts/postinstall.mjs +26 -0
- package/src/app/api/agents/[id]/route.ts +43 -0
- package/src/app/api/agents/[id]/thread/route.ts +39 -8
- package/src/app/api/agents/route.ts +35 -2
- package/src/app/api/auth/route.ts +77 -8
- package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
- package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/route.ts +6 -0
- package/src/app/api/chats/[id]/browser/route.ts +5 -1
- package/src/app/api/chats/[id]/chat/route.ts +7 -3
- package/src/app/api/chats/[id]/messages/route.ts +19 -13
- package/src/app/api/chats/[id]/route.ts +30 -0
- package/src/app/api/chats/[id]/stop/route.ts +6 -1
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/route.ts +23 -1
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
- package/src/app/api/connectors/doctor/route.ts +13 -0
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
- package/src/app/api/external-agents/[id]/route.ts +31 -0
- package/src/app/api/external-agents/register/route.ts +3 -0
- package/src/app/api/external-agents/route.ts +66 -0
- package/src/app/api/files/open/route.ts +16 -14
- package/src/app/api/gateways/[id]/health/route.ts +28 -0
- package/src/app/api/gateways/[id]/route.ts +79 -0
- package/src/app/api/gateways/route.ts +57 -0
- package/src/app/api/memory/maintenance/route.ts +11 -1
- package/src/app/api/openclaw/agent-files/route.ts +27 -4
- package/src/app/api/openclaw/gateway/route.ts +10 -7
- package/src/app/api/openclaw/skills/route.ts +12 -4
- package/src/app/api/plugins/dependencies/route.ts +24 -0
- package/src/app/api/plugins/install/route.ts +15 -92
- package/src/app/api/plugins/route.ts +3 -26
- package/src/app/api/plugins/settings/route.ts +17 -12
- package/src/app/api/plugins/ui/route.ts +1 -0
- package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
- package/src/app/api/schedules/[id]/route.ts +38 -9
- package/src/app/api/schedules/route.ts +51 -28
- package/src/app/api/settings/route.ts +55 -17
- package/src/app/api/setup/doctor/route.ts +6 -4
- package/src/app/api/tasks/[id]/route.ts +16 -6
- package/src/app/api/tasks/bulk/route.ts +3 -3
- package/src/app/api/tasks/route.ts +9 -4
- package/src/app/api/webhooks/[id]/route.ts +8 -1
- package/src/app/page.tsx +135 -17
- package/src/cli/binary.test.js +142 -0
- package/src/cli/index.js +38 -11
- package/src/cli/index.test.js +195 -0
- package/src/cli/index.ts +21 -12
- package/src/cli/server-cmd.test.js +59 -0
- package/src/cli/spec.js +20 -2
- package/src/components/agents/agent-card.tsx +15 -12
- package/src/components/agents/agent-chat-list.tsx +101 -1
- package/src/components/agents/agent-list.tsx +46 -9
- package/src/components/agents/agent-sheet.tsx +456 -23
- package/src/components/agents/inspector-panel.tsx +110 -49
- package/src/components/agents/sandbox-env-panel.tsx +4 -1
- package/src/components/auth/access-key-gate.tsx +36 -97
- package/src/components/auth/setup-wizard.tsx +970 -275
- package/src/components/chat/chat-area.tsx +70 -27
- package/src/components/chat/chat-card.tsx +6 -21
- package/src/components/chat/chat-header.tsx +263 -366
- package/src/components/chat/chat-list.tsx +62 -26
- package/src/components/chat/checkpoint-timeline.tsx +1 -1
- package/src/components/chat/message-list.tsx +145 -19
- package/src/components/chatrooms/chatroom-input.tsx +96 -33
- package/src/components/chatrooms/chatroom-list.tsx +141 -72
- package/src/components/chatrooms/chatroom-message.tsx +7 -6
- package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
- package/src/components/chatrooms/chatroom-view.tsx +422 -209
- package/src/components/chatrooms/reaction-picker.tsx +38 -33
- package/src/components/connectors/connector-list.tsx +265 -127
- package/src/components/connectors/connector-sheet.tsx +217 -0
- package/src/components/gateways/gateway-sheet.tsx +567 -0
- package/src/components/home/home-view.tsx +128 -4
- package/src/components/input/chat-input.tsx +135 -86
- package/src/components/layout/app-layout.tsx +385 -194
- package/src/components/layout/mobile-header.tsx +26 -8
- package/src/components/memory/memory-browser.tsx +71 -6
- package/src/components/memory/memory-card.tsx +18 -0
- package/src/components/memory/memory-detail.tsx +58 -31
- package/src/components/memory/memory-sheet.tsx +32 -4
- package/src/components/plugins/plugin-list.tsx +15 -3
- package/src/components/plugins/plugin-sheet.tsx +118 -9
- package/src/components/projects/project-detail.tsx +189 -1
- package/src/components/providers/provider-list.tsx +158 -2
- package/src/components/providers/provider-sheet.tsx +81 -70
- package/src/components/shared/agent-picker-list.tsx +2 -2
- package/src/components/shared/bottom-sheet.tsx +31 -15
- package/src/components/shared/command-palette.tsx +111 -24
- package/src/components/shared/confirm-dialog.tsx +45 -30
- package/src/components/shared/model-combobox.tsx +90 -8
- package/src/components/shared/settings/plugin-manager.tsx +20 -4
- package/src/components/shared/settings/section-capability-policy.tsx +105 -0
- package/src/components/shared/settings/section-heartbeat.tsx +88 -6
- package/src/components/shared/settings/section-orchestrator.tsx +6 -3
- package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
- package/src/components/shared/settings/section-secrets.tsx +6 -6
- package/src/components/shared/settings/section-user-preferences.tsx +1 -1
- package/src/components/shared/settings/section-voice.tsx +5 -1
- package/src/components/shared/settings/section-web-search.tsx +10 -2
- package/src/components/shared/settings/settings-page.tsx +248 -47
- package/src/components/tasks/approvals-panel.tsx +211 -18
- package/src/components/tasks/task-board.tsx +242 -46
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/usage/metrics-dashboard.tsx +74 -1
- package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
- package/src/components/wallets/wallet-panel.tsx +17 -5
- package/src/components/webhooks/webhook-sheet.tsx +7 -7
- package/src/lib/auth.ts +17 -0
- package/src/lib/chat-streaming-state.test.ts +108 -0
- package/src/lib/chat-streaming-state.ts +108 -0
- package/src/lib/heartbeat-defaults.ts +48 -0
- package/src/lib/memory-presentation.ts +59 -0
- package/src/lib/openclaw-agent-id.test.ts +14 -0
- package/src/lib/openclaw-agent-id.ts +31 -0
- package/src/lib/provider-model-discovery-client.ts +29 -0
- package/src/lib/providers/index.ts +12 -5
- package/src/lib/runtime-loop.ts +105 -3
- package/src/lib/safe-storage.ts +6 -1
- package/src/lib/server/agent-assignment.test.ts +112 -0
- package/src/lib/server/agent-assignment.ts +169 -0
- package/src/lib/server/agent-runtime-config.test.ts +141 -0
- package/src/lib/server/agent-runtime-config.ts +277 -0
- package/src/lib/server/approval-connector-notify.test.ts +253 -0
- package/src/lib/server/approvals-auto-approve.test.ts +264 -0
- package/src/lib/server/approvals.ts +483 -75
- package/src/lib/server/autonomy-runtime.test.ts +341 -0
- package/src/lib/server/browser-state.test.ts +118 -0
- package/src/lib/server/browser-state.ts +123 -0
- package/src/lib/server/build-llm.test.ts +44 -0
- package/src/lib/server/build-llm.ts +11 -4
- package/src/lib/server/builtin-plugins.ts +34 -0
- package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
- package/src/lib/server/chat-execution.ts +402 -125
- package/src/lib/server/chatroom-health.test.ts +26 -0
- package/src/lib/server/chatroom-health.ts +2 -3
- package/src/lib/server/chatroom-helpers.test.ts +74 -2
- package/src/lib/server/chatroom-helpers.ts +144 -11
- package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
- package/src/lib/server/connectors/discord.ts +175 -11
- package/src/lib/server/connectors/doctor.test.ts +80 -0
- package/src/lib/server/connectors/doctor.ts +116 -0
- package/src/lib/server/connectors/manager.ts +994 -130
- package/src/lib/server/connectors/policy.test.ts +222 -0
- package/src/lib/server/connectors/policy.ts +452 -0
- package/src/lib/server/connectors/slack.ts +189 -10
- package/src/lib/server/connectors/telegram.ts +65 -15
- package/src/lib/server/connectors/thread-context.test.ts +44 -0
- package/src/lib/server/connectors/thread-context.ts +72 -0
- package/src/lib/server/connectors/types.ts +41 -11
- package/src/lib/server/daemon-state.ts +62 -3
- package/src/lib/server/data-dir.ts +13 -0
- package/src/lib/server/delegation-jobs.test.ts +140 -0
- package/src/lib/server/delegation-jobs.ts +248 -0
- package/src/lib/server/document-utils.test.ts +47 -0
- package/src/lib/server/document-utils.ts +397 -0
- package/src/lib/server/eval/agent-regression.test.ts +47 -0
- package/src/lib/server/eval/agent-regression.ts +1742 -0
- package/src/lib/server/eval/runner.ts +11 -1
- package/src/lib/server/eval/store.ts +2 -1
- package/src/lib/server/heartbeat-service.ts +23 -43
- package/src/lib/server/heartbeat-source.test.ts +22 -0
- package/src/lib/server/heartbeat-source.ts +7 -0
- package/src/lib/server/identity-continuity.test.ts +77 -0
- package/src/lib/server/identity-continuity.ts +127 -0
- package/src/lib/server/mailbox-utils.ts +347 -0
- package/src/lib/server/main-agent-loop.ts +31 -964
- package/src/lib/server/memory-db.ts +4 -6
- package/src/lib/server/memory-tiers.ts +40 -0
- package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
- package/src/lib/server/openclaw-agent-resolver.ts +128 -0
- package/src/lib/server/openclaw-exec-config.ts +6 -5
- package/src/lib/server/openclaw-gateway.ts +123 -36
- package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
- package/src/lib/server/openclaw-skills-normalize.ts +136 -0
- package/src/lib/server/openclaw-sync.ts +3 -2
- package/src/lib/server/orchestrator-lg.ts +18 -8
- package/src/lib/server/orchestrator.ts +5 -4
- package/src/lib/server/playwright-proxy.mjs +27 -3
- package/src/lib/server/plugins.test.ts +215 -0
- package/src/lib/server/plugins.ts +832 -69
- package/src/lib/server/provider-health.ts +33 -3
- package/src/lib/server/provider-model-discovery.ts +481 -0
- package/src/lib/server/queue.ts +4 -21
- package/src/lib/server/runtime-settings.test.ts +119 -0
- package/src/lib/server/runtime-settings.ts +12 -92
- package/src/lib/server/schedule-normalization.ts +187 -0
- package/src/lib/server/scheduler.ts +2 -0
- package/src/lib/server/session-archive-memory.test.ts +85 -0
- package/src/lib/server/session-archive-memory.ts +230 -0
- package/src/lib/server/session-mailbox.ts +8 -18
- package/src/lib/server/session-reset-policy.test.ts +99 -0
- package/src/lib/server/session-reset-policy.ts +311 -0
- package/src/lib/server/session-run-manager.ts +33 -80
- package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
- package/src/lib/server/session-tools/calendar.ts +2 -12
- package/src/lib/server/session-tools/connector.ts +109 -8
- package/src/lib/server/session-tools/context.ts +14 -2
- package/src/lib/server/session-tools/crawl.ts +447 -0
- package/src/lib/server/session-tools/crud.ts +96 -34
- package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
- package/src/lib/server/session-tools/delegate.ts +406 -20
- package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
- package/src/lib/server/session-tools/discovery.ts +40 -12
- package/src/lib/server/session-tools/document.ts +283 -0
- package/src/lib/server/session-tools/email.ts +1 -3
- package/src/lib/server/session-tools/extract.ts +137 -0
- package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
- package/src/lib/server/session-tools/file-send.test.ts +84 -1
- package/src/lib/server/session-tools/file.ts +243 -24
- package/src/lib/server/session-tools/http.ts +9 -3
- package/src/lib/server/session-tools/human-loop.ts +227 -0
- package/src/lib/server/session-tools/image-gen.ts +1 -3
- package/src/lib/server/session-tools/index.ts +87 -2
- package/src/lib/server/session-tools/mailbox.ts +276 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
- package/src/lib/server/session-tools/memory.ts +35 -3
- package/src/lib/server/session-tools/monitor.ts +162 -12
- package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
- package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
- package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
- package/src/lib/server/session-tools/platform.ts +142 -4
- package/src/lib/server/session-tools/plugin-creator.ts +95 -25
- package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
- package/src/lib/server/session-tools/replicate.ts +1 -3
- package/src/lib/server/session-tools/sandbox.ts +51 -92
- package/src/lib/server/session-tools/schedule.ts +20 -10
- package/src/lib/server/session-tools/session-info.ts +58 -4
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
- package/src/lib/server/session-tools/shell.ts +2 -2
- package/src/lib/server/session-tools/subagent.ts +195 -27
- package/src/lib/server/session-tools/table.ts +587 -0
- package/src/lib/server/session-tools/wallet.ts +13 -10
- package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
- package/src/lib/server/session-tools/web.ts +947 -108
- package/src/lib/server/storage.ts +255 -10
- package/src/lib/server/stream-agent-chat.test.ts +61 -0
- package/src/lib/server/stream-agent-chat.ts +185 -25
- package/src/lib/server/structured-extract.test.ts +72 -0
- package/src/lib/server/structured-extract.ts +373 -0
- package/src/lib/server/task-mention.test.ts +16 -2
- package/src/lib/server/task-mention.ts +61 -11
- package/src/lib/server/tool-aliases.ts +80 -12
- package/src/lib/server/tool-capability-policy.ts +7 -1
- package/src/lib/server/tool-retry.ts +2 -0
- package/src/lib/server/watch-jobs.test.ts +173 -0
- package/src/lib/server/watch-jobs.ts +532 -0
- package/src/lib/server/ws-hub.ts +5 -3
- package/src/lib/setup-defaults.ts +352 -11
- package/src/lib/tool-definitions.ts +3 -4
- package/src/lib/validation/schemas.test.ts +26 -0
- package/src/lib/validation/schemas.ts +62 -1
- package/src/lib/ws-client.ts +14 -12
- package/src/proxy.ts +5 -5
- package/src/stores/use-app-store.ts +43 -7
- package/src/stores/use-chat-store.ts +31 -2
- package/src/stores/use-chatroom-store.ts +153 -26
- package/src/types/index.ts +470 -44
- package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
- package/src/components/chat/new-chat-sheet.tsx +0 -253
- package/src/lib/server/main-session.ts +0 -17
- package/src/lib/server/session-run-manager.test.ts +0 -26
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { genId } from '@/lib/id'
|
|
3
|
+
import { formatZodError, ExternalAgentRegisterSchema } from '@/lib/validation/schemas'
|
|
4
|
+
import { loadExternalAgents, saveExternalAgents } from '@/lib/server/storage'
|
|
5
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
6
|
+
import type { ExternalAgentRuntime } from '@/types'
|
|
7
|
+
import { z } from 'zod'
|
|
8
|
+
export const dynamic = 'force-dynamic'
|
|
9
|
+
|
|
10
|
+
function withDerivedStatus(record: ExternalAgentRuntime): ExternalAgentRuntime {
|
|
11
|
+
const now = Date.now()
|
|
12
|
+
const lastSeenAt = typeof record.lastSeenAt === 'number' ? record.lastSeenAt : null
|
|
13
|
+
const staleMs = 3 * 60_000
|
|
14
|
+
if (!lastSeenAt) return { ...record, status: record.status || 'offline' }
|
|
15
|
+
if (record.status === 'offline') return record
|
|
16
|
+
return {
|
|
17
|
+
...record,
|
|
18
|
+
status: now - lastSeenAt > staleMs ? 'stale' : (record.status || 'online'),
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function GET() {
|
|
23
|
+
const runtimes = loadExternalAgents()
|
|
24
|
+
const items: ExternalAgentRuntime[] = Object.values(runtimes)
|
|
25
|
+
.map((item) => withDerivedStatus(item))
|
|
26
|
+
.sort((a, b) => (b.lastSeenAt || b.updatedAt || 0) - (a.lastSeenAt || a.updatedAt || 0))
|
|
27
|
+
return NextResponse.json(items)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function POST(req: Request) {
|
|
31
|
+
const raw = await req.json().catch(() => ({}))
|
|
32
|
+
const parsed = ExternalAgentRegisterSchema.safeParse(raw)
|
|
33
|
+
if (!parsed.success) {
|
|
34
|
+
return NextResponse.json(formatZodError(parsed.error as z.ZodError), { status: 400 })
|
|
35
|
+
}
|
|
36
|
+
const body = parsed.data
|
|
37
|
+
const now = Date.now()
|
|
38
|
+
const items = loadExternalAgents()
|
|
39
|
+
const id = body.id || `external-${genId()}`
|
|
40
|
+
const existing = items[id]
|
|
41
|
+
items[id] = {
|
|
42
|
+
...existing,
|
|
43
|
+
id,
|
|
44
|
+
name: body.name.trim(),
|
|
45
|
+
sourceType: body.sourceType,
|
|
46
|
+
status: body.status || existing?.status || 'online',
|
|
47
|
+
provider: (body.provider as ExternalAgentRuntime['provider']) || null,
|
|
48
|
+
model: body.model || null,
|
|
49
|
+
workspace: body.workspace || null,
|
|
50
|
+
transport: body.transport || null,
|
|
51
|
+
endpoint: body.endpoint || null,
|
|
52
|
+
agentId: body.agentId || null,
|
|
53
|
+
gatewayProfileId: body.gatewayProfileId || null,
|
|
54
|
+
capabilities: body.capabilities,
|
|
55
|
+
labels: body.labels,
|
|
56
|
+
metadata: body.metadata,
|
|
57
|
+
tokenStats: body.tokenStats,
|
|
58
|
+
lastHeartbeatAt: existing?.lastHeartbeatAt || now,
|
|
59
|
+
lastSeenAt: now,
|
|
60
|
+
createdAt: existing?.createdAt || now,
|
|
61
|
+
updatedAt: now,
|
|
62
|
+
}
|
|
63
|
+
saveExternalAgents(items)
|
|
64
|
+
notify('external_agents')
|
|
65
|
+
return NextResponse.json(items[id])
|
|
66
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import {
|
|
2
|
+
import { spawn } from 'child_process'
|
|
3
3
|
import fs from 'fs'
|
|
4
4
|
import path from 'path'
|
|
5
5
|
|
|
@@ -19,25 +19,27 @@ export async function POST(req: Request) {
|
|
|
19
19
|
const isDir = fs.statSync(resolved).isDirectory()
|
|
20
20
|
const platform = process.platform
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
let
|
|
22
|
+
let command: string
|
|
23
|
+
let args: string[]
|
|
24
24
|
if (platform === 'darwin') {
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
command = 'open'
|
|
26
|
+
args = isDir ? [resolved] : ['-R', resolved]
|
|
27
27
|
} else if (platform === 'win32') {
|
|
28
|
-
|
|
28
|
+
command = 'explorer'
|
|
29
|
+
args = isDir ? [resolved] : [`/select,${resolved}`]
|
|
29
30
|
} else {
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
command = 'xdg-open'
|
|
32
|
+
args = [isDir ? resolved : path.dirname(resolved)]
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
return new Promise<NextResponse>((resolve) => {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
const child = spawn(command, args, { stdio: 'ignore' })
|
|
37
|
+
child.once('error', (err) => {
|
|
38
|
+
resolve(NextResponse.json({ error: err.message }, { status: 500 }))
|
|
39
|
+
})
|
|
40
|
+
child.once('spawn', () => {
|
|
41
|
+
child.unref()
|
|
42
|
+
resolve(NextResponse.json({ ok: true }))
|
|
41
43
|
})
|
|
42
44
|
})
|
|
43
45
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { probeOpenClawHealth } from '@/lib/server/openclaw-health'
|
|
3
|
+
import { loadGatewayProfiles, saveGatewayProfiles } from '@/lib/server/storage'
|
|
4
|
+
import { notFound } from '@/lib/server/collection-helpers'
|
|
5
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
6
|
+
export const dynamic = 'force-dynamic'
|
|
7
|
+
|
|
8
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
9
|
+
const { id } = await params
|
|
10
|
+
const gateways = loadGatewayProfiles()
|
|
11
|
+
const gateway = gateways[id]
|
|
12
|
+
if (!gateway) return notFound()
|
|
13
|
+
|
|
14
|
+
const result = await probeOpenClawHealth({
|
|
15
|
+
endpoint: gateway.endpoint,
|
|
16
|
+
credentialId: gateway.credentialId || null,
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
gateway.status = result.ok ? 'healthy' : (result.authProvided ? 'degraded' : 'offline')
|
|
20
|
+
gateway.lastCheckedAt = Date.now()
|
|
21
|
+
gateway.lastError = result.ok ? null : (result.error || result.hint || 'Gateway health check failed.')
|
|
22
|
+
gateway.lastModelCount = Array.isArray(result.models) ? result.models.length : 0
|
|
23
|
+
gateway.updatedAt = Date.now()
|
|
24
|
+
saveGatewayProfiles(gateways)
|
|
25
|
+
notify('gateways')
|
|
26
|
+
|
|
27
|
+
return NextResponse.json(result)
|
|
28
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { normalizeOpenClawEndpoint } from '@/lib/openclaw-endpoint'
|
|
3
|
+
import { loadAgents, loadGatewayProfiles, saveAgents, saveGatewayProfiles } from '@/lib/server/storage'
|
|
4
|
+
import { mutateItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
|
|
5
|
+
import type { Agent, AgentRoutingTarget, GatewayProfile } from '@/types'
|
|
6
|
+
|
|
7
|
+
const ops: CollectionOps<GatewayProfile> = {
|
|
8
|
+
load: loadGatewayProfiles,
|
|
9
|
+
save: saveGatewayProfiles,
|
|
10
|
+
topic: 'gateways',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function normalizeTags(value: unknown): string[] {
|
|
14
|
+
if (!Array.isArray(value)) return []
|
|
15
|
+
return value
|
|
16
|
+
.map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
21
|
+
const { id } = await params
|
|
22
|
+
const body = await req.json().catch(() => ({}))
|
|
23
|
+
const result = mutateItem(ops, id, (gateway, all) => {
|
|
24
|
+
if (body.isDefault === true) {
|
|
25
|
+
for (const [candidateId, candidate] of Object.entries(all)) {
|
|
26
|
+
if (candidateId === id || !candidate || typeof candidate !== 'object') continue
|
|
27
|
+
candidate.isDefault = false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (body.name !== undefined) gateway.name = String(body.name || '').trim() || gateway.name
|
|
31
|
+
if (body.endpoint !== undefined) gateway.endpoint = normalizeOpenClawEndpoint(body.endpoint || undefined)
|
|
32
|
+
if (body.wsUrl !== undefined) gateway.wsUrl = body.wsUrl || null
|
|
33
|
+
if (body.credentialId !== undefined) gateway.credentialId = body.credentialId || null
|
|
34
|
+
if (body.status !== undefined) gateway.status = body.status || 'unknown'
|
|
35
|
+
if (body.notes !== undefined) gateway.notes = body.notes || null
|
|
36
|
+
if (body.tags !== undefined) gateway.tags = normalizeTags(body.tags)
|
|
37
|
+
if (body.lastError !== undefined) gateway.lastError = body.lastError || null
|
|
38
|
+
if (body.lastCheckedAt !== undefined) gateway.lastCheckedAt = body.lastCheckedAt || null
|
|
39
|
+
if (body.lastModelCount !== undefined) gateway.lastModelCount = body.lastModelCount || null
|
|
40
|
+
if (body.discoveredHost !== undefined) gateway.discoveredHost = body.discoveredHost || null
|
|
41
|
+
if (body.discoveredPort !== undefined) gateway.discoveredPort = body.discoveredPort || null
|
|
42
|
+
if (body.isDefault !== undefined) gateway.isDefault = body.isDefault === true
|
|
43
|
+
gateway.updatedAt = Date.now()
|
|
44
|
+
return gateway
|
|
45
|
+
})
|
|
46
|
+
if (!result) return notFound()
|
|
47
|
+
return NextResponse.json(result)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
51
|
+
const { id } = await params
|
|
52
|
+
const gateways = loadGatewayProfiles()
|
|
53
|
+
if (!gateways[id]) return notFound()
|
|
54
|
+
delete gateways[id]
|
|
55
|
+
saveGatewayProfiles(gateways)
|
|
56
|
+
|
|
57
|
+
const agents = loadAgents({ includeTrashed: true })
|
|
58
|
+
let agentChanged = false
|
|
59
|
+
for (const agent of Object.values(agents) as Agent[]) {
|
|
60
|
+
if (agent.gatewayProfileId === id) {
|
|
61
|
+
agent.gatewayProfileId = null
|
|
62
|
+
agentChanged = true
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(agent.routingTargets)) {
|
|
65
|
+
const nextTargets = agent.routingTargets.map((target: AgentRoutingTarget) => (
|
|
66
|
+
target.gatewayProfileId === id
|
|
67
|
+
? { ...target, gatewayProfileId: null }
|
|
68
|
+
: target
|
|
69
|
+
))
|
|
70
|
+
if (JSON.stringify(nextTargets) !== JSON.stringify(agent.routingTargets)) {
|
|
71
|
+
agent.routingTargets = nextTargets
|
|
72
|
+
agentChanged = true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (agentChanged) saveAgents(agents)
|
|
77
|
+
|
|
78
|
+
return NextResponse.json({ ok: true })
|
|
79
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { genId } from '@/lib/id'
|
|
3
|
+
import { normalizeOpenClawEndpoint } from '@/lib/openclaw-endpoint'
|
|
4
|
+
import { getGatewayProfiles } from '@/lib/server/agent-runtime-config'
|
|
5
|
+
import { loadGatewayProfiles, saveGatewayProfiles } from '@/lib/server/storage'
|
|
6
|
+
import { notify } from '@/lib/server/ws-hub'
|
|
7
|
+
export const dynamic = 'force-dynamic'
|
|
8
|
+
|
|
9
|
+
function normalizeTags(value: unknown): string[] {
|
|
10
|
+
if (!Array.isArray(value)) return []
|
|
11
|
+
return value
|
|
12
|
+
.map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
|
|
13
|
+
.filter(Boolean)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function GET() {
|
|
17
|
+
return NextResponse.json(getGatewayProfiles('openclaw'))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function POST(req: Request) {
|
|
21
|
+
const body = await req.json().catch(() => ({}))
|
|
22
|
+
const endpoint = normalizeOpenClawEndpoint(body.endpoint || undefined)
|
|
23
|
+
const now = Date.now()
|
|
24
|
+
const gateways = loadGatewayProfiles()
|
|
25
|
+
const id = body.id || `gateway-${genId()}`
|
|
26
|
+
const isDefault = body.isDefault === true
|
|
27
|
+
|
|
28
|
+
if (isDefault) {
|
|
29
|
+
for (const gateway of Object.values(gateways) as Array<Record<string, unknown>>) {
|
|
30
|
+
gateway.isDefault = false
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
gateways[id] = {
|
|
35
|
+
id,
|
|
36
|
+
name: typeof body.name === 'string' && body.name.trim() ? body.name.trim() : 'OpenClaw Gateway',
|
|
37
|
+
provider: 'openclaw',
|
|
38
|
+
endpoint,
|
|
39
|
+
wsUrl: body.wsUrl || null,
|
|
40
|
+
credentialId: body.credentialId || null,
|
|
41
|
+
status: body.status || 'unknown',
|
|
42
|
+
notes: typeof body.notes === 'string' ? body.notes : null,
|
|
43
|
+
tags: normalizeTags(body.tags),
|
|
44
|
+
lastError: null,
|
|
45
|
+
lastCheckedAt: null,
|
|
46
|
+
lastModelCount: null,
|
|
47
|
+
discoveredHost: typeof body.discoveredHost === 'string' ? body.discoveredHost : null,
|
|
48
|
+
discoveredPort: typeof body.discoveredPort === 'number' ? body.discoveredPort : null,
|
|
49
|
+
isDefault,
|
|
50
|
+
createdAt: now,
|
|
51
|
+
updatedAt: now,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
saveGatewayProfiles(gateways)
|
|
55
|
+
notify('gateways')
|
|
56
|
+
return NextResponse.json(gateways[id])
|
|
57
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
+
import path from 'path'
|
|
2
3
|
import { getMemoryDb } from '@/lib/server/memory-db'
|
|
3
4
|
import { loadSettings } from '@/lib/server/storage'
|
|
5
|
+
import { syncAllSessionArchiveMemories } from '@/lib/server/session-archive-memory'
|
|
6
|
+
import { DATA_DIR } from '@/lib/server/data-dir'
|
|
4
7
|
|
|
5
8
|
function parseBool(value: unknown, fallback: boolean): boolean {
|
|
6
9
|
if (typeof value === 'boolean') return value
|
|
@@ -33,10 +36,13 @@ export async function GET(req: Request) {
|
|
|
33
36
|
24 * 365,
|
|
34
37
|
)
|
|
35
38
|
const analyzed = db.analyzeMaintenance(ttlHours)
|
|
39
|
+
const archiveSync = syncAllSessionArchiveMemories()
|
|
36
40
|
return NextResponse.json({
|
|
37
41
|
ok: true,
|
|
38
42
|
ttlHours,
|
|
39
43
|
analyzed,
|
|
44
|
+
archiveSync,
|
|
45
|
+
archiveExportDir: path.join(DATA_DIR, 'session-archives'),
|
|
40
46
|
})
|
|
41
47
|
}
|
|
42
48
|
|
|
@@ -46,6 +52,9 @@ export async function POST(req: Request) {
|
|
|
46
52
|
const db = getMemoryDb()
|
|
47
53
|
const ttlHours = parseIntBounded(body?.ttlHours ?? settings.memoryWorkingTtlHours, 24, 1, 24 * 365)
|
|
48
54
|
const maxDeletes = parseIntBounded(body?.maxDeletes, 500, 1, 20_000)
|
|
55
|
+
const archiveSync = body?.syncArchives === false
|
|
56
|
+
? { synced: 0, skipped: 0, sessionIds: [] }
|
|
57
|
+
: syncAllSessionArchiveMemories()
|
|
49
58
|
const result = db.maintain({
|
|
50
59
|
ttlHours,
|
|
51
60
|
maxDeletes,
|
|
@@ -57,7 +66,8 @@ export async function POST(req: Request) {
|
|
|
57
66
|
ok: true,
|
|
58
67
|
ttlHours,
|
|
59
68
|
maxDeletes,
|
|
69
|
+
archiveSync,
|
|
70
|
+
archiveExportDir: path.join(DATA_DIR, 'session-archives'),
|
|
60
71
|
...result,
|
|
61
72
|
})
|
|
62
73
|
}
|
|
63
|
-
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
|
|
3
|
+
import { resolveOpenClawGatewayAgentId } from '@/lib/server/openclaw-agent-resolver'
|
|
3
4
|
|
|
4
5
|
const AGENT_FILES = ['SOUL.md', 'IDENTITY.md', 'USER.md', 'TOOLS.md', 'HEARTBEAT.md', 'MEMORY.md', 'AGENTS.md'] as const
|
|
5
6
|
|
|
@@ -16,12 +17,24 @@ export async function GET(req: Request) {
|
|
|
16
17
|
return NextResponse.json({ error: 'OpenClaw gateway not connected' }, { status: 503 })
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
let gatewayAgentId: string
|
|
21
|
+
try {
|
|
22
|
+
gatewayAgentId = await resolveOpenClawGatewayAgentId(agentId, gw)
|
|
23
|
+
} catch (err: unknown) {
|
|
24
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
25
|
+
const status = message.includes('not an OpenClaw agent') ? 400 : 404
|
|
26
|
+
return NextResponse.json({ error: message }, { status })
|
|
27
|
+
}
|
|
28
|
+
|
|
19
29
|
const files: Record<string, { content: string; error?: string }> = {}
|
|
20
30
|
await Promise.all(
|
|
21
31
|
AGENT_FILES.map(async (filename) => {
|
|
22
32
|
try {
|
|
23
|
-
const result = await gw.rpc('agents.files.get', {
|
|
24
|
-
|
|
33
|
+
const result = await gw.rpc('agents.files.get', {
|
|
34
|
+
agentId: gatewayAgentId,
|
|
35
|
+
name: filename,
|
|
36
|
+
}) as { file?: { content?: string } } | undefined
|
|
37
|
+
files[filename] = { content: result?.file?.content ?? '' }
|
|
25
38
|
} catch (err: unknown) {
|
|
26
39
|
files[filename] = { content: '', error: err instanceof Error ? err.message : String(err) }
|
|
27
40
|
}
|
|
@@ -48,10 +61,20 @@ export async function PUT(req: Request) {
|
|
|
48
61
|
}
|
|
49
62
|
|
|
50
63
|
try {
|
|
51
|
-
await
|
|
64
|
+
const gatewayAgentId = await resolveOpenClawGatewayAgentId(agentId, gw)
|
|
65
|
+
await gw.rpc('agents.files.set', {
|
|
66
|
+
agentId: gatewayAgentId,
|
|
67
|
+
name: filename,
|
|
68
|
+
content: content ?? '',
|
|
69
|
+
})
|
|
52
70
|
return NextResponse.json({ ok: true })
|
|
53
71
|
} catch (err: unknown) {
|
|
54
72
|
const message = err instanceof Error ? err.message : String(err)
|
|
55
|
-
|
|
73
|
+
const status = message.includes('not an OpenClaw agent')
|
|
74
|
+
? 400
|
|
75
|
+
: message.includes('not found')
|
|
76
|
+
? 404
|
|
77
|
+
: 502
|
|
78
|
+
return NextResponse.json({ error: message }, { status })
|
|
56
79
|
}
|
|
57
80
|
}
|
|
@@ -5,6 +5,7 @@ import { ensureGatewayConnected, getGateway, disconnectGateway, manualConnect }
|
|
|
5
5
|
export async function POST(req: Request) {
|
|
6
6
|
const body = await req.json()
|
|
7
7
|
const { method, params } = body as { method?: string; params?: Record<string, unknown> }
|
|
8
|
+
const profileId = typeof params?.profileId === 'string' ? params.profileId : undefined
|
|
8
9
|
if (!method || typeof method !== 'string') {
|
|
9
10
|
return NextResponse.json({ error: 'Missing RPC method' }, { status: 400 })
|
|
10
11
|
}
|
|
@@ -14,7 +15,7 @@ export async function POST(req: Request) {
|
|
|
14
15
|
try {
|
|
15
16
|
const url = (params?.url as string) || undefined
|
|
16
17
|
const token = (params?.token as string) || undefined
|
|
17
|
-
const ok = await manualConnect(url, token)
|
|
18
|
+
const ok = await manualConnect(url, token, profileId)
|
|
18
19
|
return NextResponse.json({ ok })
|
|
19
20
|
} catch (err: unknown) {
|
|
20
21
|
return NextResponse.json({ ok: false, error: err instanceof Error ? err.message : String(err) }, { status: 502 })
|
|
@@ -22,13 +23,13 @@ export async function POST(req: Request) {
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
if (method === 'gateway.disconnect') {
|
|
25
|
-
disconnectGateway()
|
|
26
|
+
disconnectGateway(profileId)
|
|
26
27
|
return NextResponse.json({ ok: true })
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
// Reload mode get/set
|
|
30
31
|
if (method === 'gateway.reload-mode.get') {
|
|
31
|
-
const gw = await ensureGatewayConnected()
|
|
32
|
+
const gw = await ensureGatewayConnected({ profileId })
|
|
32
33
|
if (!gw) return NextResponse.json({ error: 'Not connected' }, { status: 503 })
|
|
33
34
|
try {
|
|
34
35
|
const config = await gw.rpc('config.get') as Record<string, unknown> | undefined
|
|
@@ -40,7 +41,7 @@ export async function POST(req: Request) {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
if (method === 'gateway.reload-mode.set') {
|
|
43
|
-
const gw = await ensureGatewayConnected()
|
|
44
|
+
const gw = await ensureGatewayConnected({ profileId })
|
|
44
45
|
if (!gw) return NextResponse.json({ error: 'Not connected' }, { status: 503 })
|
|
45
46
|
try {
|
|
46
47
|
await gw.rpc('config.set', { reloadMode: params?.mode })
|
|
@@ -51,7 +52,7 @@ export async function POST(req: Request) {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
// General RPC proxy
|
|
54
|
-
const gw = await ensureGatewayConnected()
|
|
55
|
+
const gw = await ensureGatewayConnected({ profileId })
|
|
55
56
|
if (!gw) {
|
|
56
57
|
return NextResponse.json({ error: 'OpenClaw gateway not connected' }, { status: 503 })
|
|
57
58
|
}
|
|
@@ -66,7 +67,9 @@ export async function POST(req: Request) {
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
/** GET — check gateway connection status */
|
|
69
|
-
export async function GET() {
|
|
70
|
-
const
|
|
70
|
+
export async function GET(req: Request) {
|
|
71
|
+
const { searchParams } = new URL(req.url)
|
|
72
|
+
const profileId = searchParams.get('profileId') || undefined
|
|
73
|
+
const gw = getGateway(profileId || undefined)
|
|
71
74
|
return NextResponse.json({ connected: !!gw?.connected })
|
|
72
75
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
|
|
3
|
+
import { resolveOpenClawGatewayAgentId } from '@/lib/server/openclaw-agent-resolver'
|
|
4
|
+
import { normalizeOpenClawSkillsPayload } from '@/lib/server/openclaw-skills-normalize'
|
|
3
5
|
import { loadAgents, saveAgents } from '@/lib/server/storage'
|
|
4
6
|
import { notify } from '@/lib/server/ws-hub'
|
|
5
|
-
import type {
|
|
7
|
+
import type { SkillAllowlistMode } from '@/types'
|
|
6
8
|
|
|
7
9
|
/** GET ?agentId=X — fetch skills from gateway with eligibility */
|
|
8
10
|
export async function GET(req: Request) {
|
|
@@ -18,11 +20,17 @@ export async function GET(req: Request) {
|
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
try {
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
+
const gatewayAgentId = await resolveOpenClawGatewayAgentId(agentId, gw)
|
|
24
|
+
const result = await gw.rpc('skills.status', { agentId: gatewayAgentId }) as unknown
|
|
25
|
+
return NextResponse.json(normalizeOpenClawSkillsPayload(result))
|
|
23
26
|
} catch (err: unknown) {
|
|
24
27
|
const message = err instanceof Error ? err.message : String(err)
|
|
25
|
-
|
|
28
|
+
const status = message.includes('not an OpenClaw agent')
|
|
29
|
+
? 400
|
|
30
|
+
: message.includes('not found')
|
|
31
|
+
? 404
|
|
32
|
+
: 502
|
|
33
|
+
return NextResponse.json({ error: message }, { status })
|
|
26
34
|
}
|
|
27
35
|
}
|
|
28
36
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { getPluginManager } from '@/lib/server/plugins'
|
|
3
|
+
|
|
4
|
+
export async function POST(req: Request) {
|
|
5
|
+
const body = await req.json()
|
|
6
|
+
const filename = typeof body?.filename === 'string' ? body.filename : ''
|
|
7
|
+
const packageManager = typeof body?.packageManager === 'string' ? body.packageManager : undefined
|
|
8
|
+
|
|
9
|
+
if (!filename) {
|
|
10
|
+
return NextResponse.json({ error: 'filename is required' }, { status: 400 })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const result = await getPluginManager().installPluginDependencies(filename, {
|
|
15
|
+
packageManager: packageManager as import('@/types').PluginPackageManager | undefined,
|
|
16
|
+
})
|
|
17
|
+
return NextResponse.json({ ok: true, dependencyInfo: result })
|
|
18
|
+
} catch (err: unknown) {
|
|
19
|
+
return NextResponse.json(
|
|
20
|
+
{ error: err instanceof Error ? err.message : String(err) },
|
|
21
|
+
{ status: 400 },
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -1,110 +1,33 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import
|
|
3
|
-
import path from 'path'
|
|
4
|
-
import { getPluginManager } from '@/lib/server/plugins'
|
|
5
|
-
|
|
6
|
-
const PLUGINS_DIR = path.join(process.cwd(), 'data', 'plugins')
|
|
7
|
-
|
|
8
|
-
function toRawUrl(url: string): string {
|
|
9
|
-
if (url.includes('github.com') && url.includes('/blob/')) {
|
|
10
|
-
return url.replace('github.com', 'raw.githubusercontent.com').replace('/blob/', '/')
|
|
11
|
-
}
|
|
12
|
-
if (url.includes('gist.github.com')) {
|
|
13
|
-
return url.endsWith('/raw') ? url : `${url}/raw`
|
|
14
|
-
}
|
|
15
|
-
return url
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function normalizeMarketplaceUrl(url: string): string {
|
|
19
|
-
const trimmed = typeof url === 'string' ? url.trim() : ''
|
|
20
|
-
if (!trimmed) return trimmed
|
|
21
|
-
|
|
22
|
-
let normalized = trimmed
|
|
23
|
-
.replace('github.com/swarmclawai/plugins/', 'github.com/swarmclawai/swarmforge/')
|
|
24
|
-
.replace('raw.githubusercontent.com/swarmclawai/plugins/', 'raw.githubusercontent.com/swarmclawai/swarmforge/')
|
|
25
|
-
|
|
26
|
-
normalized = toRawUrl(normalized)
|
|
27
|
-
|
|
28
|
-
// Legacy registry entries used master and old repo names.
|
|
29
|
-
normalized = normalized
|
|
30
|
-
.replace('/swarmclawai/swarmforge/master/', '/swarmclawai/swarmforge/main/')
|
|
31
|
-
.replace('/swarmclawai/plugins/master/', '/swarmclawai/swarmforge/main/')
|
|
32
|
-
.replace('/swarmclawai/plugins/main/', '/swarmclawai/swarmforge/main/')
|
|
33
|
-
|
|
34
|
-
return normalized
|
|
35
|
-
}
|
|
2
|
+
import { getPluginManager, sanitizePluginFilename } from '@/lib/server/plugins'
|
|
36
3
|
|
|
37
4
|
export async function POST(req: Request) {
|
|
38
5
|
const body = await req.json()
|
|
39
|
-
const
|
|
40
|
-
const
|
|
6
|
+
const url = typeof body?.url === 'string' ? body.url : ''
|
|
7
|
+
const filename = typeof body?.filename === 'string' ? body.filename : ''
|
|
41
8
|
|
|
42
|
-
|
|
43
|
-
if (!url || typeof url !== 'string' || !url.startsWith('https://')) {
|
|
9
|
+
if (!url || !url.startsWith('https://')) {
|
|
44
10
|
return NextResponse.json(
|
|
45
11
|
{ error: 'URL must be a valid HTTPS URL' },
|
|
46
12
|
{ status: 400 },
|
|
47
13
|
)
|
|
48
14
|
}
|
|
49
15
|
|
|
50
|
-
// Validate filename
|
|
51
|
-
if (!filename || typeof filename !== 'string' || !filename.endsWith('.js')) {
|
|
52
|
-
return NextResponse.json(
|
|
53
|
-
{ error: 'Filename must end in .js' },
|
|
54
|
-
{ status: 400 },
|
|
55
|
-
)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Path traversal protection
|
|
59
|
-
const sanitized = path.basename(filename)
|
|
60
|
-
if (sanitized !== filename || filename.includes('..')) {
|
|
61
|
-
return NextResponse.json(
|
|
62
|
-
{ error: 'Invalid filename' },
|
|
63
|
-
{ status: 400 },
|
|
64
|
-
)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
16
|
try {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
{ error: `Download failed (HTTP ${res.status}) from ${rawUrl}` },
|
|
72
|
-
{ status: 502 },
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
const contentType = res.headers.get('content-type') || ''
|
|
76
|
-
let code = await res.text()
|
|
77
|
-
|
|
78
|
-
// Reject HTML responses (likely a GitHub page, not raw content)
|
|
79
|
-
if (contentType.includes('text/html') && code.includes('<!DOCTYPE')) {
|
|
80
|
-
return NextResponse.json(
|
|
81
|
-
{ error: 'URL returned an HTML page instead of JavaScript. Use a raw/direct link to the .js file.' },
|
|
82
|
-
{ status: 400 },
|
|
83
|
-
)
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Compatibility fix: Strip node-fetch requires if present, as modern Node has global fetch
|
|
87
|
-
code = code.replace(/const\s+fetch\s*=\s*require\(['"]node-fetch['"]\);?/g, '// node-fetch stripped for compatibility')
|
|
88
|
-
code = code.replace(/import\s+fetch\s+from\s+['"]node-fetch['"];?/g, '// node-fetch stripped for compatibility')
|
|
89
|
-
|
|
90
|
-
// Ensure plugins directory exists
|
|
91
|
-
if (!fs.existsSync(PLUGINS_DIR)) {
|
|
92
|
-
fs.mkdirSync(PLUGINS_DIR, { recursive: true })
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const dest = path.join(PLUGINS_DIR, sanitized)
|
|
96
|
-
fs.writeFileSync(dest, code, 'utf8')
|
|
97
|
-
|
|
98
|
-
// Force plugin manager to re-scan so the new plugin appears in listings
|
|
99
|
-
getPluginManager().reload()
|
|
100
|
-
|
|
101
|
-
return NextResponse.json({ ok: true, filename: sanitized })
|
|
17
|
+
const sanitizedFilename = sanitizePluginFilename(filename)
|
|
18
|
+
const installed = await getPluginManager().installPluginFromUrl(url, sanitizedFilename)
|
|
19
|
+
return NextResponse.json({ ok: true, filename: installed.filename, hash: installed.sourceHash })
|
|
102
20
|
} catch (err: unknown) {
|
|
103
21
|
const msg = err instanceof Error ? err.message : String(err)
|
|
104
|
-
const isTimeout =
|
|
22
|
+
const isTimeout = /abort|timeout/i.test(msg)
|
|
23
|
+
const status = /valid HTTPS URL|Filename|Invalid filename|HTML page|too large/i.test(msg)
|
|
24
|
+
? 400
|
|
25
|
+
: isTimeout
|
|
26
|
+
? 504
|
|
27
|
+
: 500
|
|
105
28
|
return NextResponse.json(
|
|
106
|
-
{ error: isTimeout ? 'Download timed out — the plugin URL may be unreachable' :
|
|
107
|
-
{ status
|
|
29
|
+
{ error: isTimeout ? 'Download timed out — the plugin URL may be unreachable' : msg },
|
|
30
|
+
{ status },
|
|
108
31
|
)
|
|
109
32
|
}
|
|
110
33
|
}
|