@swarmclawai/swarmclaw 0.7.8 → 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 -15
- 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 +22 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +26 -1
- 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/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/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 +73 -24
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +44 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- 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/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/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 +7 -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 +191 -95
- package/src/components/tasks/task-board.tsx +273 -2
- package/src/components/tasks/task-card.tsx +38 -9
- 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 +11 -0
- package/src/lib/server/capability-router.ts +26 -1
- 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 +353 -72
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +362 -63
- 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 +1 -1
- 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 +189 -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 +15 -10
- 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/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 +2 -2
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +205 -5
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +262 -0
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +293 -61
- 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 +52 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +348 -61
- package/src/lib/server/session-tools/context.ts +12 -3
- package/src/lib/server/session-tools/crud.ts +221 -10
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate.ts +64 -8
- 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/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +546 -79
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- 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 +162 -1
- package/src/lib/server/session-tools/web.ts +468 -64
- 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 +419 -9
- package/src/lib/server/stream-agent-chat.ts +887 -83
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- 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.ts +4 -2
- 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-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 +210 -14
|
@@ -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 }
|
|
@@ -4,6 +4,7 @@ import http from 'http'
|
|
|
4
4
|
import fs from 'fs'
|
|
5
5
|
import path from 'path'
|
|
6
6
|
import { localIP } from '@/lib/server/storage'
|
|
7
|
+
import { resolveDevServerLaunchDir } from '@/lib/server/devserver-launch'
|
|
7
8
|
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
9
10
|
// MIME types for static server
|
|
@@ -77,6 +78,13 @@ interface ProjectInfo {
|
|
|
77
78
|
framework?: string // e.g. 'vite', 'next', 'cra'
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
function buildFrameworkArgs(framework: string | undefined, port: number): string[] {
|
|
82
|
+
if (framework === 'next') {
|
|
83
|
+
return ['--', '--hostname', '0.0.0.0', '--port', String(port)]
|
|
84
|
+
}
|
|
85
|
+
return ['--', '--port', String(port), '--host', '0.0.0.0']
|
|
86
|
+
}
|
|
87
|
+
|
|
80
88
|
function detectProject(dir: string): ProjectInfo {
|
|
81
89
|
const pkgPath = path.join(dir, 'package.json')
|
|
82
90
|
if (!fs.existsSync(pkgPath)) {
|
|
@@ -168,7 +176,7 @@ function createStaticServer(dir: string): http.Server {
|
|
|
168
176
|
// npm dev server
|
|
169
177
|
// ---------------------------------------------------------------------------
|
|
170
178
|
|
|
171
|
-
async function startNpmServer(dir: string, command: string[], port: number): Promise<PreviewServer> {
|
|
179
|
+
async function startNpmServer(dir: string, command: string[], port: number, framework?: string): Promise<PreviewServer> {
|
|
172
180
|
// Install deps if node_modules missing
|
|
173
181
|
if (!fs.existsSync(path.join(dir, 'node_modules'))) {
|
|
174
182
|
console.log(`[preview] Installing dependencies in ${dir}`)
|
|
@@ -190,7 +198,7 @@ async function startNpmServer(dir: string, command: string[], port: number): Pro
|
|
|
190
198
|
const args = [...command.slice(1)]
|
|
191
199
|
const cmdName = command[0]
|
|
192
200
|
|
|
193
|
-
const proc = spawn(cmdName, [...args,
|
|
201
|
+
const proc = spawn(cmdName, [...args, ...buildFrameworkArgs(framework, port)], {
|
|
194
202
|
cwd: dir,
|
|
195
203
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
196
204
|
env,
|
|
@@ -234,6 +242,10 @@ async function startNpmServer(dir: string, command: string[], port: number): Pro
|
|
|
234
242
|
|
|
235
243
|
// Wait for the server to start and detect the actual port
|
|
236
244
|
await new Promise((resolve) => setTimeout(resolve, 5000))
|
|
245
|
+
if (proc.exitCode !== null) {
|
|
246
|
+
servers.delete(dirKey(dir))
|
|
247
|
+
throw new Error(`npm dev server exited early with code ${proc.exitCode}\n${log.slice(-4000)}`)
|
|
248
|
+
}
|
|
237
249
|
entry.port = detectedPort
|
|
238
250
|
entry.log = log
|
|
239
251
|
|
|
@@ -263,7 +275,8 @@ export async function POST(req: Request) {
|
|
|
263
275
|
}
|
|
264
276
|
|
|
265
277
|
const dir = resolveServeDir(filePath)
|
|
266
|
-
const
|
|
278
|
+
const launch = resolveDevServerLaunchDir(dir)
|
|
279
|
+
const key = dirKey(launch.launchDir)
|
|
267
280
|
|
|
268
281
|
if (action === 'start') {
|
|
269
282
|
if (servers.has(key)) {
|
|
@@ -274,16 +287,18 @@ export async function POST(req: Request) {
|
|
|
274
287
|
return NextResponse.json({ error: 'Directory not found' }, { status: 404 })
|
|
275
288
|
}
|
|
276
289
|
|
|
277
|
-
const project = detectProject(
|
|
290
|
+
const project = detectProject(launch.launchDir)
|
|
278
291
|
const port = await findFreePort()
|
|
279
292
|
|
|
280
293
|
if (project.type === 'npm' && project.devCommand) {
|
|
281
|
-
console.log(`[preview] Detected ${project.framework} project in ${
|
|
294
|
+
console.log(`[preview] Detected ${project.framework} project in ${launch.launchDir}, running: ${project.devCommand.join(' ')}`)
|
|
282
295
|
try {
|
|
283
|
-
const entry = await startNpmServer(
|
|
296
|
+
const entry = await startNpmServer(launch.launchDir, project.devCommand, port, project.framework)
|
|
284
297
|
return NextResponse.json({
|
|
285
298
|
...buildResponse(entry),
|
|
286
299
|
framework: project.framework,
|
|
300
|
+
inputDir: dir,
|
|
301
|
+
launchDir: launch.launchDir,
|
|
287
302
|
})
|
|
288
303
|
} catch (err: unknown) {
|
|
289
304
|
console.error(`[preview] npm server failed, falling back to static:`, err)
|
|
@@ -313,15 +328,15 @@ export async function POST(req: Request) {
|
|
|
313
328
|
}
|
|
314
329
|
if (srv.server) srv.server.close()
|
|
315
330
|
servers.delete(key)
|
|
316
|
-
console.log(`[preview] Stopped server for ${
|
|
331
|
+
console.log(`[preview] Stopped server for ${launch.launchDir}`)
|
|
317
332
|
}
|
|
318
|
-
return NextResponse.json({ running: false, dir })
|
|
333
|
+
return NextResponse.json({ running: false, dir: launch.launchDir })
|
|
319
334
|
|
|
320
335
|
} else if (action === 'status') {
|
|
321
336
|
if (servers.has(key)) {
|
|
322
337
|
return NextResponse.json(buildResponse(servers.get(key)!))
|
|
323
338
|
}
|
|
324
|
-
return NextResponse.json({ running: false, dir })
|
|
339
|
+
return NextResponse.json({ running: false, dir: launch.launchDir })
|
|
325
340
|
|
|
326
341
|
} else if (action === 'list') {
|
|
327
342
|
const list = Array.from(servers.values()).map((s) => ({
|
|
@@ -331,8 +346,8 @@ export async function POST(req: Request) {
|
|
|
331
346
|
return NextResponse.json({ servers: list })
|
|
332
347
|
|
|
333
348
|
} else if (action === 'detect') {
|
|
334
|
-
const project = detectProject(
|
|
335
|
-
return NextResponse.json({ dir, ...project })
|
|
349
|
+
const project = detectProject(launch.launchDir)
|
|
350
|
+
return NextResponse.json({ dir, launchDir: launch.launchDir, frameworkHint: launch.framework, ...project })
|
|
336
351
|
}
|
|
337
352
|
|
|
338
353
|
return NextResponse.json({ error: 'Invalid action' }, { status: 400 })
|
|
@@ -2,6 +2,7 @@ import { NextResponse } from 'next/server'
|
|
|
2
2
|
import { genId } from '@/lib/id'
|
|
3
3
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
4
4
|
import { loadSchedules, saveSchedules, loadAgents, loadTasks, saveTasks } from '@/lib/server/storage'
|
|
5
|
+
import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agent-availability'
|
|
5
6
|
import { enqueueTask } from '@/lib/server/queue'
|
|
6
7
|
import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
|
|
7
8
|
import { getScheduleSignatureKey } from '@/lib/schedule-dedupe'
|
|
@@ -20,6 +21,9 @@ export async function POST(_req: Request, { params }: { params: Promise<{ id: st
|
|
|
20
21
|
const agents = loadAgents()
|
|
21
22
|
const agent = agents[schedule.agentId]
|
|
22
23
|
if (!agent) return NextResponse.json({ error: 'Agent not found' }, { status: 400 })
|
|
24
|
+
if (isAgentDisabled(agent)) {
|
|
25
|
+
return NextResponse.json({ error: buildAgentDisabledMessage(agent, 'run schedules') }, { status: 409 })
|
|
26
|
+
}
|
|
23
27
|
|
|
24
28
|
const tasks = loadTasks()
|
|
25
29
|
const scheduleSignature = getScheduleSignatureKey(schedule)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import test, { afterEach } from 'node:test'
|
|
3
|
+
|
|
4
|
+
import { POST as createSchedule } from './route'
|
|
5
|
+
import { POST as runSchedule } from './[id]/run/route'
|
|
6
|
+
import { loadAgents, loadSchedules, saveAgents, saveSchedules } from '@/lib/server/storage'
|
|
7
|
+
|
|
8
|
+
const originalAgents = loadAgents()
|
|
9
|
+
const originalSchedules = loadSchedules()
|
|
10
|
+
|
|
11
|
+
function routeParams(id: string) {
|
|
12
|
+
return { params: Promise.resolve({ id }) }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function seedAgent(id: string, overrides: Record<string, unknown> = {}) {
|
|
16
|
+
const agents = loadAgents()
|
|
17
|
+
const now = Date.now()
|
|
18
|
+
agents[id] = {
|
|
19
|
+
id,
|
|
20
|
+
name: 'Schedule Test Agent',
|
|
21
|
+
description: 'Schedule smoke test agent',
|
|
22
|
+
systemPrompt: 'Handle schedules.',
|
|
23
|
+
provider: 'openai',
|
|
24
|
+
model: 'gpt-4o-mini',
|
|
25
|
+
credentialId: null,
|
|
26
|
+
fallbackCredentialIds: [],
|
|
27
|
+
apiEndpoint: null,
|
|
28
|
+
plugins: ['manage_schedules'],
|
|
29
|
+
createdAt: now,
|
|
30
|
+
updatedAt: now,
|
|
31
|
+
...overrides,
|
|
32
|
+
}
|
|
33
|
+
saveAgents(agents)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
saveAgents(originalAgents)
|
|
38
|
+
saveSchedules(originalSchedules)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('POST /api/schedules rejects disabled agents', async () => {
|
|
42
|
+
seedAgent('schedule-disabled-agent', { disabled: true })
|
|
43
|
+
|
|
44
|
+
const response = await createSchedule(new Request('http://local/api/schedules', {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { 'content-type': 'application/json' },
|
|
47
|
+
body: JSON.stringify({
|
|
48
|
+
agentId: 'schedule-disabled-agent',
|
|
49
|
+
name: 'Disabled smoke',
|
|
50
|
+
taskPrompt: 'Send a reminder',
|
|
51
|
+
scheduleType: 'once',
|
|
52
|
+
runAt: Date.now() + 60_000,
|
|
53
|
+
status: 'active',
|
|
54
|
+
}),
|
|
55
|
+
}))
|
|
56
|
+
|
|
57
|
+
assert.equal(response.status, 409)
|
|
58
|
+
const payload = await response.json() as Record<string, unknown>
|
|
59
|
+
assert.match(String(payload.error || ''), /disabled/i)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('POST /api/schedules/[id]/run rejects disabled agents', async () => {
|
|
63
|
+
seedAgent('schedule-run-disabled-agent', { disabled: true })
|
|
64
|
+
const schedules = loadSchedules()
|
|
65
|
+
schedules['schedule-disabled-run'] = {
|
|
66
|
+
id: 'schedule-disabled-run',
|
|
67
|
+
name: 'Disabled Run',
|
|
68
|
+
agentId: 'schedule-run-disabled-agent',
|
|
69
|
+
taskPrompt: 'Send a reminder',
|
|
70
|
+
scheduleType: 'once',
|
|
71
|
+
runAt: Date.now() + 60_000,
|
|
72
|
+
status: 'active',
|
|
73
|
+
createdAt: Date.now(),
|
|
74
|
+
updatedAt: Date.now(),
|
|
75
|
+
}
|
|
76
|
+
saveSchedules(schedules)
|
|
77
|
+
|
|
78
|
+
const response = await runSchedule(
|
|
79
|
+
new Request('http://local/api/schedules/schedule-disabled-run/run', { method: 'POST' }),
|
|
80
|
+
routeParams('schedule-disabled-run'),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
assert.equal(response.status, 409)
|
|
84
|
+
const payload = await response.json() as Record<string, unknown>
|
|
85
|
+
assert.match(String(payload.error || ''), /disabled/i)
|
|
86
|
+
})
|
|
@@ -3,6 +3,7 @@ import { genId } from '@/lib/id'
|
|
|
3
3
|
import { loadAgents, loadSchedules, saveSchedules } from '@/lib/server/storage'
|
|
4
4
|
import { WORKSPACE_DIR } from '@/lib/server/data-dir'
|
|
5
5
|
import { normalizeSchedulePayload } from '@/lib/server/schedule-normalization'
|
|
6
|
+
import { buildAgentDisabledMessage, isAgentDisabled } from '@/lib/server/agent-availability'
|
|
6
7
|
import { resolveScheduleName } from '@/lib/schedule-name'
|
|
7
8
|
import { findDuplicateSchedule } from '@/lib/schedule-dedupe'
|
|
8
9
|
import { notify } from '@/lib/server/ws-hub'
|
|
@@ -45,9 +46,13 @@ export async function POST(req: Request) {
|
|
|
45
46
|
|
|
46
47
|
const candidate = normalizedSchedule.value
|
|
47
48
|
const agents = loadAgents()
|
|
48
|
-
|
|
49
|
+
const agent = agents[String(candidate.agentId)]
|
|
50
|
+
if (!agent) {
|
|
49
51
|
return NextResponse.json({ error: `Agent not found: ${String(candidate.agentId)}` }, { status: 400 })
|
|
50
52
|
}
|
|
53
|
+
if (isAgentDisabled(agent)) {
|
|
54
|
+
return NextResponse.json({ error: buildAgentDisabledMessage(agent, 'take scheduled work') }, { status: 409 })
|
|
55
|
+
}
|
|
51
56
|
const scheduleType = asScheduleType(candidate.scheduleType)
|
|
52
57
|
const candidateAgentId = asString(candidate.agentId) || null
|
|
53
58
|
const candidateTaskPrompt = asString(candidate.taskPrompt)
|