@swarmclawai/swarmclaw 0.7.7 → 0.8.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 +12 -14
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +23 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +46 -3
- package/src/app/api/connectors/route.ts +12 -8
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/projects/[id]/route.ts +6 -2
- package/src/app/api/projects/route.ts +4 -3
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/secrets/[id]/route.ts +1 -0
- package/src/app/api/secrets/route.ts +2 -1
- package/src/app/api/settings/route.ts +2 -0
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +257 -38
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-area.tsx +36 -19
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +48 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/delegation-banner.test.ts +14 -1
- package/src/components/chat/delegation-banner.tsx +1 -1
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/layout/app-layout.tsx +40 -23
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/projects/project-detail.tsx +217 -0
- package/src/components/projects/project-sheet.tsx +176 -4
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +45 -3
- package/src/components/shared/settings/section-voice.tsx +11 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +289 -34
- package/src/components/tasks/task-board.tsx +410 -25
- package/src/components/tasks/task-card.tsx +66 -8
- package/src/components/tasks/task-sheet.tsx +16 -4
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +33 -0
- package/src/lib/server/capability-router.ts +80 -19
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +378 -73
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager-reconnect.test.ts +47 -0
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +461 -137
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +84 -47
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +247 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +20 -11
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/main-agent-loop.test.ts +260 -0
- package/src/lib/server/main-agent-loop.ts +559 -14
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +3 -2
- package/src/lib/server/orchestrator.ts +2 -0
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +211 -6
- package/src/lib/server/project-context.ts +162 -0
- package/src/lib/server/project-utils.ts +150 -0
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +409 -2
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +527 -68
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +83 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector-inputs.test.ts +37 -0
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +366 -54
- package/src/lib/server/session-tools/context.ts +17 -3
- package/src/lib/server/session-tools/crud.ts +484 -84
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate-resume.test.ts +50 -0
- package/src/lib/server/session-tools/delegate.ts +102 -10
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/manage-tasks.test.ts +114 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +554 -75
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/platform-access.test.ts +58 -0
- package/src/lib/server/session-tools/platform.ts +60 -19
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +178 -0
- package/src/lib/server/session-tools/web.ts +621 -70
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +437 -2
- package/src/lib/server/stream-agent-chat.ts +957 -79
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-aliases.ts +2 -0
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-capability-policy.test.ts +24 -0
- package/src/lib/server/tool-capability-policy.ts +29 -1
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.test.ts +44 -0
- package/src/lib/server/tool-planning.ts +271 -0
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-definitions.ts +2 -1
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +249 -14
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { afterEach, test } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { persistGatewayHealthResult } from './[id]/health/route'
|
|
5
|
+
import { loadGatewayProfiles, saveGatewayProfiles } from '@/lib/server/storage'
|
|
6
|
+
|
|
7
|
+
const originalGateways = loadGatewayProfiles()
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
saveGatewayProfiles(originalGateways)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
test('persistGatewayHealthResult stores healthy verification details', () => {
|
|
14
|
+
const gateways = loadGatewayProfiles()
|
|
15
|
+
gateways['gateway-health-test'] = {
|
|
16
|
+
id: 'gateway-health-test',
|
|
17
|
+
name: 'Gateway Health Test',
|
|
18
|
+
provider: 'openclaw',
|
|
19
|
+
endpoint: 'http://127.0.0.1:18789/v1',
|
|
20
|
+
wsUrl: 'ws://127.0.0.1:18789',
|
|
21
|
+
credentialId: 'credential-1',
|
|
22
|
+
status: 'unknown',
|
|
23
|
+
notes: null,
|
|
24
|
+
tags: ['smoke'],
|
|
25
|
+
lastError: 'previous error',
|
|
26
|
+
lastCheckedAt: null,
|
|
27
|
+
lastModelCount: null,
|
|
28
|
+
discoveredHost: '127.0.0.1',
|
|
29
|
+
discoveredPort: 18789,
|
|
30
|
+
deployment: {
|
|
31
|
+
method: 'imported',
|
|
32
|
+
useCase: 'single-vps',
|
|
33
|
+
},
|
|
34
|
+
stats: null,
|
|
35
|
+
isDefault: false,
|
|
36
|
+
createdAt: 1,
|
|
37
|
+
updatedAt: 1,
|
|
38
|
+
}
|
|
39
|
+
saveGatewayProfiles(gateways)
|
|
40
|
+
|
|
41
|
+
const now = 1_777_777_777_000
|
|
42
|
+
const updated = persistGatewayHealthResult('gateway-health-test', {
|
|
43
|
+
ok: true,
|
|
44
|
+
endpoint: 'http://127.0.0.1:18789/v1',
|
|
45
|
+
wsUrl: 'ws://127.0.0.1:18789',
|
|
46
|
+
wsConnected: true,
|
|
47
|
+
httpCompatible: true,
|
|
48
|
+
authProvided: true,
|
|
49
|
+
model: 'default',
|
|
50
|
+
models: ['default', 'glm-5:cloud'],
|
|
51
|
+
modelsStatus: 200,
|
|
52
|
+
chatStatus: 200,
|
|
53
|
+
message: 'Gateway verified successfully.',
|
|
54
|
+
}, now)
|
|
55
|
+
|
|
56
|
+
const saved = loadGatewayProfiles()['gateway-health-test']
|
|
57
|
+
assert.ok(updated)
|
|
58
|
+
assert.equal(saved.status, 'healthy')
|
|
59
|
+
assert.equal(saved.lastCheckedAt, now)
|
|
60
|
+
assert.equal(saved.lastError, null)
|
|
61
|
+
assert.equal(saved.lastModelCount, 2)
|
|
62
|
+
assert.equal(saved.updatedAt, now)
|
|
63
|
+
assert.equal(saved.deployment?.method, 'imported')
|
|
64
|
+
assert.equal(saved.deployment?.lastVerifiedAt, now)
|
|
65
|
+
assert.equal(saved.deployment?.lastVerifiedOk, true)
|
|
66
|
+
assert.equal(saved.deployment?.lastVerifiedMessage, 'Gateway verified successfully.')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('persistGatewayHealthResult stores degraded/offline failures with the right message', () => {
|
|
70
|
+
const gateways = loadGatewayProfiles()
|
|
71
|
+
gateways['gateway-health-failure-test'] = {
|
|
72
|
+
id: 'gateway-health-failure-test',
|
|
73
|
+
name: 'Gateway Health Failure Test',
|
|
74
|
+
provider: 'openclaw',
|
|
75
|
+
endpoint: 'http://127.0.0.1:18888/v1',
|
|
76
|
+
wsUrl: 'ws://127.0.0.1:18888',
|
|
77
|
+
credentialId: null,
|
|
78
|
+
status: 'unknown',
|
|
79
|
+
notes: null,
|
|
80
|
+
tags: [],
|
|
81
|
+
lastError: null,
|
|
82
|
+
lastCheckedAt: null,
|
|
83
|
+
lastModelCount: null,
|
|
84
|
+
discoveredHost: '127.0.0.1',
|
|
85
|
+
discoveredPort: 18888,
|
|
86
|
+
deployment: null,
|
|
87
|
+
stats: null,
|
|
88
|
+
isDefault: false,
|
|
89
|
+
createdAt: 1,
|
|
90
|
+
updatedAt: 1,
|
|
91
|
+
}
|
|
92
|
+
saveGatewayProfiles(gateways)
|
|
93
|
+
|
|
94
|
+
const degraded = persistGatewayHealthResult('gateway-health-failure-test', {
|
|
95
|
+
ok: false,
|
|
96
|
+
endpoint: 'http://127.0.0.1:18888/v1',
|
|
97
|
+
wsUrl: 'ws://127.0.0.1:18888',
|
|
98
|
+
wsConnected: false,
|
|
99
|
+
httpCompatible: null,
|
|
100
|
+
authProvided: true,
|
|
101
|
+
model: null,
|
|
102
|
+
models: [],
|
|
103
|
+
modelsStatus: null,
|
|
104
|
+
chatStatus: null,
|
|
105
|
+
message: 'ignored',
|
|
106
|
+
error: 'Token rejected.',
|
|
107
|
+
}, 2_000)
|
|
108
|
+
assert.ok(degraded)
|
|
109
|
+
let saved = loadGatewayProfiles()['gateway-health-failure-test']
|
|
110
|
+
assert.equal(saved.status, 'degraded')
|
|
111
|
+
assert.equal(saved.lastError, 'Token rejected.')
|
|
112
|
+
assert.equal(saved.deployment?.lastVerifiedOk, false)
|
|
113
|
+
assert.equal(saved.deployment?.lastVerifiedMessage, 'Token rejected.')
|
|
114
|
+
|
|
115
|
+
const offline = persistGatewayHealthResult('gateway-health-failure-test', {
|
|
116
|
+
ok: false,
|
|
117
|
+
endpoint: 'http://127.0.0.1:18888/v1',
|
|
118
|
+
wsUrl: 'ws://127.0.0.1:18888',
|
|
119
|
+
wsConnected: false,
|
|
120
|
+
httpCompatible: null,
|
|
121
|
+
authProvided: false,
|
|
122
|
+
model: null,
|
|
123
|
+
models: [],
|
|
124
|
+
modelsStatus: null,
|
|
125
|
+
chatStatus: null,
|
|
126
|
+
message: 'ignored',
|
|
127
|
+
hint: 'Gateway unreachable.',
|
|
128
|
+
}, 3_000)
|
|
129
|
+
assert.ok(offline)
|
|
130
|
+
saved = loadGatewayProfiles()['gateway-health-failure-test']
|
|
131
|
+
assert.equal(saved.status, 'offline')
|
|
132
|
+
assert.equal(saved.lastError, 'Gateway unreachable.')
|
|
133
|
+
assert.equal(saved.deployment?.lastVerifiedOk, false)
|
|
134
|
+
assert.equal(saved.deployment?.lastVerifiedMessage, 'Gateway unreachable.')
|
|
135
|
+
})
|
|
@@ -32,6 +32,8 @@ function normalizeDeployment(value: unknown): OpenClawDeploymentConfig | null {
|
|
|
32
32
|
useCase: normalizeText(deployment.useCase) as OpenClawDeploymentConfig['useCase'],
|
|
33
33
|
exposure: normalizeText(deployment.exposure) as OpenClawDeploymentConfig['exposure'],
|
|
34
34
|
managedBy: normalizeText(deployment.managedBy) as OpenClawDeploymentConfig['managedBy'],
|
|
35
|
+
localInstanceId: normalizeText(deployment.localInstanceId),
|
|
36
|
+
localPort: normalizeNullableNumber(deployment.localPort),
|
|
35
37
|
targetHost: normalizeText(deployment.targetHost),
|
|
36
38
|
sshHost: normalizeText(deployment.sshHost),
|
|
37
39
|
sshUser: normalizeText(deployment.sshUser),
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import test, { afterEach } from 'node:test'
|
|
5
|
+
|
|
6
|
+
import { GET as getMcpServer, PUT as updateMcpServer, DELETE as deleteMcpServer } from './[id]/route'
|
|
7
|
+
import { POST as runMcpConformance } from './[id]/conformance/route'
|
|
8
|
+
import { POST as invokeMcpTool } from './[id]/invoke/route'
|
|
9
|
+
import { POST as testMcpServer } from './[id]/test/route'
|
|
10
|
+
import { GET as listMcpTools } from './[id]/tools/route'
|
|
11
|
+
import { GET as listMcpServers, POST as createMcpServer } from './route'
|
|
12
|
+
import { loadMcpServers, saveMcpServers } from '@/lib/server/storage'
|
|
13
|
+
|
|
14
|
+
const originalMcpServers = loadMcpServers()
|
|
15
|
+
const fixturePath = path.resolve(
|
|
16
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
17
|
+
'../../../lib/server/__fixtures__/fake-mcp-stdio-server.mjs',
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
function routeParams(id: string) {
|
|
21
|
+
return { params: Promise.resolve({ id }) }
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
saveMcpServers(originalMcpServers)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test('MCP server routes exercise a live stdio server end to end', async () => {
|
|
29
|
+
const createResponse = await createMcpServer(new Request('http://local/api/mcp-servers', {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: { 'content-type': 'application/json' },
|
|
32
|
+
body: JSON.stringify({
|
|
33
|
+
name: 'smoke',
|
|
34
|
+
transport: 'stdio',
|
|
35
|
+
command: process.execPath,
|
|
36
|
+
args: [fixturePath],
|
|
37
|
+
}),
|
|
38
|
+
}))
|
|
39
|
+
assert.equal(createResponse.status, 200)
|
|
40
|
+
const created = await createResponse.json() as Record<string, unknown>
|
|
41
|
+
const serverId = String(created.id)
|
|
42
|
+
assert.equal(created.name, 'smoke')
|
|
43
|
+
|
|
44
|
+
const listResponse = await listMcpServers(new Request('http://local/api/mcp-servers'))
|
|
45
|
+
assert.equal(listResponse.status, 200)
|
|
46
|
+
const listed = await listResponse.json() as Record<string, Record<string, unknown>>
|
|
47
|
+
assert.equal(listed[serverId]?.name, 'smoke')
|
|
48
|
+
|
|
49
|
+
const detailResponse = await getMcpServer(new Request(`http://local/api/mcp-servers/${serverId}`), routeParams(serverId))
|
|
50
|
+
assert.equal(detailResponse.status, 200)
|
|
51
|
+
const detail = await detailResponse.json() as Record<string, unknown>
|
|
52
|
+
assert.equal(detail.command, process.execPath)
|
|
53
|
+
|
|
54
|
+
const healthResponse = await testMcpServer(new Request(`http://local/api/mcp-servers/${serverId}/test`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
}), routeParams(serverId))
|
|
57
|
+
assert.equal(healthResponse.status, 200)
|
|
58
|
+
const health = await healthResponse.json() as Record<string, unknown>
|
|
59
|
+
assert.equal(health.ok, true)
|
|
60
|
+
assert.deepEqual(health.tools, ['mcp_smoke_ping', 'mcp_smoke_echo'])
|
|
61
|
+
|
|
62
|
+
const toolsResponse = await listMcpTools(new Request(`http://local/api/mcp-servers/${serverId}/tools`), routeParams(serverId))
|
|
63
|
+
assert.equal(toolsResponse.status, 200)
|
|
64
|
+
const tools = await toolsResponse.json() as Array<Record<string, unknown>>
|
|
65
|
+
assert.deepEqual(tools.map((tool) => tool.name), ['ping', 'echo'])
|
|
66
|
+
|
|
67
|
+
const invokeResponse = await invokeMcpTool(new Request(`http://local/api/mcp-servers/${serverId}/invoke`, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'content-type': 'application/json' },
|
|
70
|
+
body: JSON.stringify({
|
|
71
|
+
toolName: 'echo',
|
|
72
|
+
args: JSON.stringify({ message: 'hello from route test' }),
|
|
73
|
+
}),
|
|
74
|
+
}), routeParams(serverId))
|
|
75
|
+
assert.equal(invokeResponse.status, 200)
|
|
76
|
+
const invokePayload = await invokeResponse.json() as Record<string, unknown>
|
|
77
|
+
assert.equal(invokePayload.ok, true)
|
|
78
|
+
assert.equal(invokePayload.text, 'echo: hello from route test')
|
|
79
|
+
|
|
80
|
+
const conformanceResponse = await runMcpConformance(new Request(`http://local/api/mcp-servers/${serverId}/conformance`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: { 'content-type': 'application/json' },
|
|
83
|
+
body: JSON.stringify({ smokeToolName: 'ping' }),
|
|
84
|
+
}), routeParams(serverId))
|
|
85
|
+
assert.equal(conformanceResponse.status, 200)
|
|
86
|
+
const conformance = await conformanceResponse.json() as Record<string, unknown>
|
|
87
|
+
assert.equal(conformance.ok, true)
|
|
88
|
+
assert.equal(conformance.toolsCount, 2)
|
|
89
|
+
assert.equal(conformance.smokeToolName, 'ping')
|
|
90
|
+
|
|
91
|
+
const updateResponse = await updateMcpServer(new Request(`http://local/api/mcp-servers/${serverId}`, {
|
|
92
|
+
method: 'PUT',
|
|
93
|
+
headers: { 'content-type': 'application/json' },
|
|
94
|
+
body: JSON.stringify({ name: 'smoke-renamed' }),
|
|
95
|
+
}), routeParams(serverId))
|
|
96
|
+
assert.equal(updateResponse.status, 200)
|
|
97
|
+
const updated = await updateResponse.json() as Record<string, unknown>
|
|
98
|
+
assert.equal(updated.name, 'smoke-renamed')
|
|
99
|
+
|
|
100
|
+
const deleteResponse = await deleteMcpServer(new Request(`http://local/api/mcp-servers/${serverId}`, {
|
|
101
|
+
method: 'DELETE',
|
|
102
|
+
}), routeParams(serverId))
|
|
103
|
+
assert.equal(deleteResponse.status, 200)
|
|
104
|
+
assert.equal(loadMcpServers()[serverId], undefined)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('MCP invoke route validates required fields before connecting', async () => {
|
|
108
|
+
const serverId = 'mcp-validate-smoke'
|
|
109
|
+
const servers = loadMcpServers()
|
|
110
|
+
servers[serverId] = {
|
|
111
|
+
id: serverId,
|
|
112
|
+
name: 'smoke',
|
|
113
|
+
transport: 'stdio',
|
|
114
|
+
command: process.execPath,
|
|
115
|
+
args: [fixturePath],
|
|
116
|
+
createdAt: 1,
|
|
117
|
+
updatedAt: 1,
|
|
118
|
+
}
|
|
119
|
+
saveMcpServers(servers)
|
|
120
|
+
|
|
121
|
+
const response = await invokeMcpTool(new Request(`http://local/api/mcp-servers/${serverId}/invoke`, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: { 'content-type': 'application/json' },
|
|
124
|
+
body: JSON.stringify({ args: {} }),
|
|
125
|
+
}), routeParams(serverId))
|
|
126
|
+
|
|
127
|
+
assert.equal(response.status, 400)
|
|
128
|
+
const payload = await response.json() as Record<string, unknown>
|
|
129
|
+
assert.equal(payload.error, 'toolName is required')
|
|
130
|
+
})
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import {
|
|
3
3
|
buildOpenClawDeployBundle,
|
|
4
|
+
getOpenClawLocalDeployCollectionStatus,
|
|
5
|
+
getOpenClawRemoteDeployCollectionStatus,
|
|
4
6
|
deployOpenClawOverSsh,
|
|
5
7
|
getOpenClawLocalDeployStatus,
|
|
6
8
|
getOpenClawRemoteDeployStatus,
|
|
@@ -96,9 +98,15 @@ function parseSsh(value: unknown): Partial<OpenClawSshConfig> | null {
|
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
export async function GET() {
|
|
101
|
+
const locals = getOpenClawLocalDeployCollectionStatus()
|
|
102
|
+
const remotes = getOpenClawRemoteDeployCollectionStatus()
|
|
99
103
|
return NextResponse.json({
|
|
100
|
-
local: getOpenClawLocalDeployStatus(),
|
|
101
|
-
|
|
104
|
+
local: getOpenClawLocalDeployStatus(locals.primaryId),
|
|
105
|
+
locals: locals.items,
|
|
106
|
+
localPrimaryId: locals.primaryId,
|
|
107
|
+
remote: remotes.primaryId ? getOpenClawRemoteDeployStatus(remotes.primaryId) : null,
|
|
108
|
+
remotes: remotes.items,
|
|
109
|
+
remotePrimaryId: remotes.primaryId,
|
|
102
110
|
})
|
|
103
111
|
}
|
|
104
112
|
|
|
@@ -109,31 +117,44 @@ export async function POST(req: Request) {
|
|
|
109
117
|
try {
|
|
110
118
|
if (action === 'start-local') {
|
|
111
119
|
const result = await startOpenClawLocalDeploy({
|
|
120
|
+
localId: typeof body.localId === 'string' ? body.localId : null,
|
|
121
|
+
name: typeof body.name === 'string' ? body.name : null,
|
|
112
122
|
port: parsePort(body.port),
|
|
113
123
|
token: typeof body.token === 'string' ? body.token : null,
|
|
124
|
+
makePrimary: body.makePrimary !== false,
|
|
114
125
|
})
|
|
115
126
|
return NextResponse.json({
|
|
116
127
|
ok: true,
|
|
117
128
|
local: result.local,
|
|
129
|
+
locals: result.locals,
|
|
130
|
+
localPrimaryId: result.locals.find((item) => item.isPrimary)?.id || result.local.id,
|
|
118
131
|
token: result.token,
|
|
119
132
|
})
|
|
120
133
|
}
|
|
121
134
|
|
|
122
135
|
if (action === 'stop-local') {
|
|
136
|
+
const result = stopOpenClawLocalDeploy(typeof body.localId === 'string' ? body.localId : null)
|
|
123
137
|
return NextResponse.json({
|
|
124
138
|
ok: true,
|
|
125
|
-
local:
|
|
139
|
+
local: result.local,
|
|
140
|
+
locals: result.locals,
|
|
141
|
+
localPrimaryId: result.locals.find((item) => item.isPrimary)?.id || result.local.id,
|
|
126
142
|
})
|
|
127
143
|
}
|
|
128
144
|
|
|
129
145
|
if (action === 'restart-local') {
|
|
130
146
|
const result = await restartOpenClawLocalDeploy({
|
|
147
|
+
localId: typeof body.localId === 'string' ? body.localId : null,
|
|
148
|
+
name: typeof body.name === 'string' ? body.name : null,
|
|
131
149
|
port: parsePort(body.port),
|
|
132
150
|
token: typeof body.token === 'string' ? body.token : null,
|
|
151
|
+
makePrimary: body.makePrimary !== false,
|
|
133
152
|
})
|
|
134
153
|
return NextResponse.json({
|
|
135
154
|
ok: true,
|
|
136
155
|
local: result.local,
|
|
156
|
+
locals: result.locals,
|
|
157
|
+
localPrimaryId: result.locals.find((item) => item.isPrimary)?.id || result.local.id,
|
|
137
158
|
token: result.token,
|
|
138
159
|
})
|
|
139
160
|
}
|
|
@@ -157,6 +178,8 @@ export async function POST(req: Request) {
|
|
|
157
178
|
|
|
158
179
|
if (action === 'ssh-deploy') {
|
|
159
180
|
const result = await deployOpenClawOverSsh({
|
|
181
|
+
remoteId: typeof body.remoteId === 'string' ? body.remoteId : null,
|
|
182
|
+
name: typeof body.name === 'string' ? body.name : null,
|
|
160
183
|
template: parseTemplate(body.template),
|
|
161
184
|
target: typeof body.target === 'string' ? body.target : null,
|
|
162
185
|
token: typeof body.token === 'string' ? body.token : null,
|
|
@@ -166,10 +189,14 @@ export async function POST(req: Request) {
|
|
|
166
189
|
useCase: parseUseCase(body.useCase),
|
|
167
190
|
exposure: parseExposure(body.exposure),
|
|
168
191
|
ssh: parseSsh(body.ssh),
|
|
192
|
+
makePrimary: body.makePrimary !== false,
|
|
169
193
|
})
|
|
194
|
+
const remotes = getOpenClawRemoteDeployCollectionStatus()
|
|
170
195
|
return NextResponse.json({
|
|
171
196
|
ok: result.ok,
|
|
172
|
-
remote: getOpenClawRemoteDeployStatus(),
|
|
197
|
+
remote: getOpenClawRemoteDeployStatus(result.remoteId || remotes.primaryId),
|
|
198
|
+
remotes: remotes.items,
|
|
199
|
+
remotePrimaryId: remotes.primaryId,
|
|
173
200
|
processId: result.processId || null,
|
|
174
201
|
token: result.token,
|
|
175
202
|
bundle: result.bundle,
|
|
@@ -198,14 +225,20 @@ export async function POST(req: Request) {
|
|
|
198
225
|
} as const
|
|
199
226
|
const lifecycleAction = action as keyof typeof actionMap
|
|
200
227
|
const result = await runOpenClawRemoteLifecycle({
|
|
228
|
+
remoteId: typeof body.remoteId === 'string' ? body.remoteId : null,
|
|
229
|
+
name: typeof body.name === 'string' ? body.name : null,
|
|
201
230
|
action: actionMap[lifecycleAction],
|
|
202
231
|
ssh: parseSsh(body.ssh),
|
|
203
232
|
token: typeof body.token === 'string' ? body.token : null,
|
|
204
233
|
backupPath: typeof body.backupPath === 'string' ? body.backupPath : null,
|
|
234
|
+
makePrimary: body.makePrimary !== false,
|
|
205
235
|
})
|
|
236
|
+
const remotes = getOpenClawRemoteDeployCollectionStatus()
|
|
206
237
|
return NextResponse.json({
|
|
207
238
|
ok: result.ok,
|
|
208
|
-
remote: getOpenClawRemoteDeployStatus(),
|
|
239
|
+
remote: getOpenClawRemoteDeployStatus(result.remoteId || remotes.primaryId),
|
|
240
|
+
remotes: remotes.items,
|
|
241
|
+
remotePrimaryId: remotes.primaryId,
|
|
209
242
|
processId: result.processId || null,
|
|
210
243
|
token: result.token,
|
|
211
244
|
summary: result.summary,
|
|
@@ -1,22 +1,61 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import { getPluginManager, sanitizePluginFilename } from '@/lib/server/plugins'
|
|
3
|
+
import {
|
|
4
|
+
inferPluginInstallSourceFromUrl,
|
|
5
|
+
inferPluginPublisherSourceFromUrl,
|
|
6
|
+
normalizePluginInstallSource,
|
|
7
|
+
normalizePluginPublisherSource,
|
|
8
|
+
} from '@/lib/plugin-sources'
|
|
9
|
+
import {
|
|
10
|
+
buildPluginInstallCorsHeaders,
|
|
11
|
+
resolvePluginInstallCorsOrigin,
|
|
12
|
+
} from '@/lib/plugin-install-cors'
|
|
13
|
+
|
|
14
|
+
function json(body: Record<string, unknown>, status: number, origin: string | null) {
|
|
15
|
+
return NextResponse.json(body, {
|
|
16
|
+
status,
|
|
17
|
+
headers: buildPluginInstallCorsHeaders(origin),
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function OPTIONS(req: Request) {
|
|
22
|
+
const origin = resolvePluginInstallCorsOrigin(req.headers.get('origin'))
|
|
23
|
+
if (!origin) return NextResponse.json({ error: 'Origin not allowed' }, { status: 403 })
|
|
24
|
+
return new NextResponse(null, {
|
|
25
|
+
status: 204,
|
|
26
|
+
headers: buildPluginInstallCorsHeaders(origin),
|
|
27
|
+
})
|
|
28
|
+
}
|
|
3
29
|
|
|
4
30
|
export async function POST(req: Request) {
|
|
31
|
+
const origin = resolvePluginInstallCorsOrigin(req.headers.get('origin'))
|
|
5
32
|
const body = await req.json()
|
|
6
33
|
const url = typeof body?.url === 'string' ? body.url : ''
|
|
7
34
|
const filename = typeof body?.filename === 'string' ? body.filename : ''
|
|
35
|
+
const installMethod = body?.installMethod === 'marketplace' ? 'marketplace' : 'manual'
|
|
36
|
+
const sourceLabel = normalizePluginPublisherSource(body?.sourceLabel)
|
|
37
|
+
|| inferPluginPublisherSourceFromUrl(url)
|
|
38
|
+
|| 'manual'
|
|
39
|
+
const installSource = normalizePluginInstallSource(body?.installSource)
|
|
40
|
+
|| inferPluginInstallSourceFromUrl(url)
|
|
41
|
+
|| 'manual'
|
|
8
42
|
|
|
9
43
|
if (!url || !url.startsWith('https://')) {
|
|
10
|
-
return
|
|
44
|
+
return json(
|
|
11
45
|
{ error: 'URL must be a valid HTTPS URL' },
|
|
12
|
-
|
|
46
|
+
400,
|
|
47
|
+
origin,
|
|
13
48
|
)
|
|
14
49
|
}
|
|
15
50
|
|
|
16
51
|
try {
|
|
17
52
|
const sanitizedFilename = sanitizePluginFilename(filename)
|
|
18
|
-
const installed = await getPluginManager().installPluginFromUrl(url, sanitizedFilename
|
|
19
|
-
|
|
53
|
+
const installed = await getPluginManager().installPluginFromUrl(url, sanitizedFilename, {
|
|
54
|
+
source: installMethod,
|
|
55
|
+
sourceLabel,
|
|
56
|
+
installSource,
|
|
57
|
+
})
|
|
58
|
+
return json({ ok: true, filename: installed.filename, hash: installed.sourceHash }, 200, origin)
|
|
20
59
|
} catch (err: unknown) {
|
|
21
60
|
const msg = err instanceof Error ? err.message : String(err)
|
|
22
61
|
const isTimeout = /abort|timeout/i.test(msg)
|
|
@@ -25,9 +64,10 @@ export async function POST(req: Request) {
|
|
|
25
64
|
: isTimeout
|
|
26
65
|
? 504
|
|
27
66
|
: 500
|
|
28
|
-
return
|
|
67
|
+
return json(
|
|
29
68
|
{ error: isTimeout ? 'Download timed out — the plugin URL may be unreachable' : msg },
|
|
30
|
-
|
|
69
|
+
status,
|
|
70
|
+
origin,
|
|
31
71
|
)
|
|
32
72
|
}
|
|
33
73
|
}
|
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
+
import { inferPluginPublisherSourceFromUrl } from '@/lib/plugin-sources'
|
|
2
3
|
import { searchClawHub } from '@/lib/server/clawhub-client'
|
|
4
|
+
import type { PluginCatalogSource } from '@/types'
|
|
3
5
|
|
|
4
6
|
export const dynamic = 'force-dynamic'
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
interface RegistryPluginEntry {
|
|
9
|
+
id?: string
|
|
10
|
+
name?: string
|
|
11
|
+
description?: string
|
|
12
|
+
url?: string
|
|
13
|
+
author?: string
|
|
14
|
+
version?: string
|
|
15
|
+
tags?: string[]
|
|
16
|
+
openclaw?: boolean
|
|
17
|
+
downloads?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const REGISTRY_URLS: Array<{ url: string; catalogSource: PluginCatalogSource }> = [
|
|
21
|
+
{ url: 'https://swarmclaw.ai/registry/plugins.json', catalogSource: 'swarmclaw-site' },
|
|
22
|
+
{ url: 'https://raw.githubusercontent.com/swarmclawai/swarmforge/main/registry.json', catalogSource: 'swarmforge' },
|
|
9
23
|
]
|
|
10
24
|
const CACHE_TTL = 5 * 60 * 1000 // 5 minutes
|
|
11
25
|
|
|
@@ -33,34 +47,43 @@ export async function GET(req: Request) {
|
|
|
33
47
|
}
|
|
34
48
|
|
|
35
49
|
const allPlugins: Record<string, unknown>[] = []
|
|
50
|
+
const registryPlugins = new Map<string, Record<string, unknown>>()
|
|
36
51
|
|
|
37
52
|
// 1. Fetch SwarmClaw Registry
|
|
38
|
-
for (const
|
|
53
|
+
for (const registry of REGISTRY_URLS) {
|
|
39
54
|
try {
|
|
40
|
-
const res = await fetch(
|
|
55
|
+
const res = await fetch(registry.url, { cache: 'no-store' })
|
|
41
56
|
if (!res.ok) continue
|
|
42
57
|
|
|
43
58
|
const data = await res.json()
|
|
44
|
-
const
|
|
59
|
+
const entries = Array.isArray(data) ? data as RegistryPluginEntry[] : []
|
|
60
|
+
const filtered = entries.filter((p) => {
|
|
45
61
|
if (!p || typeof p.name !== 'string' || typeof p.description !== 'string') return false
|
|
46
62
|
return !query || p.name.toLowerCase().includes(query.toLowerCase()) || p.description.toLowerCase().includes(query.toLowerCase())
|
|
47
63
|
})
|
|
48
64
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
id
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
for (const p of filtered) {
|
|
66
|
+
const normalizedUrl = normalizeRegistryPluginUrl(p.url) || p.url
|
|
67
|
+
const id = p.id || (p.name || '').toLowerCase().replace(/[^a-z0-9]/g, '_')
|
|
68
|
+
if (registryPlugins.has(id)) continue
|
|
69
|
+
registryPlugins.set(id, {
|
|
70
|
+
...p,
|
|
71
|
+
id,
|
|
72
|
+
url: normalizedUrl,
|
|
73
|
+
source: inferPluginPublisherSourceFromUrl(normalizedUrl) || 'swarmforge',
|
|
74
|
+
catalogSource: registry.catalogSource,
|
|
75
|
+
})
|
|
76
|
+
}
|
|
56
77
|
} catch (err: unknown) {
|
|
57
78
|
console.warn('[marketplace] SC Registry failed:', {
|
|
58
|
-
registryUrl,
|
|
79
|
+
registryUrl: registry.url,
|
|
59
80
|
error: err instanceof Error ? err.message : String(err),
|
|
60
81
|
})
|
|
61
82
|
}
|
|
62
83
|
}
|
|
63
84
|
|
|
85
|
+
allPlugins.push(...registryPlugins.values())
|
|
86
|
+
|
|
64
87
|
// 2. Fetch ClawHub Skills/Plugins
|
|
65
88
|
try {
|
|
66
89
|
const hubResults = await searchClawHub(query)
|
|
@@ -71,12 +94,22 @@ export async function GET(req: Request) {
|
|
|
71
94
|
author: s.author,
|
|
72
95
|
version: s.version || '1.0.0',
|
|
73
96
|
url: s.url,
|
|
74
|
-
source: 'clawhub'
|
|
97
|
+
source: 'clawhub',
|
|
98
|
+
catalogSource: 'clawhub',
|
|
75
99
|
})))
|
|
76
100
|
} catch (err: unknown) {
|
|
77
101
|
console.warn('[marketplace] ClawHub failed:', err instanceof Error ? err.message : String(err))
|
|
78
102
|
}
|
|
79
103
|
|
|
104
|
+
allPlugins.sort((a, b) => {
|
|
105
|
+
const catalogA = typeof a.catalogSource === 'string' ? a.catalogSource : ''
|
|
106
|
+
const catalogB = typeof b.catalogSource === 'string' ? b.catalogSource : ''
|
|
107
|
+
if (catalogA !== catalogB) return catalogA.localeCompare(catalogB)
|
|
108
|
+
const nameA = typeof a.name === 'string' ? a.name : ''
|
|
109
|
+
const nameB = typeof b.name === 'string' ? b.name : ''
|
|
110
|
+
return nameA.localeCompare(nameB)
|
|
111
|
+
})
|
|
112
|
+
|
|
80
113
|
// Update cache only for empty queries
|
|
81
114
|
if (!query) {
|
|
82
115
|
cache = { data: allPlugins, fetchedAt: now }
|