@swarmclawai/swarmclaw 0.7.3 → 0.7.5

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.
Files changed (152) hide show
  1. package/README.md +47 -40
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +17 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +4 -87
  12. package/src/app/api/agents/route.ts +23 -1
  13. package/src/app/api/auth/route.ts +1 -1
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +16 -5
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/route.ts +12 -0
  19. package/src/app/api/chats/heartbeat/route.ts +2 -1
  20. package/src/app/api/chats/route.ts +7 -1
  21. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  22. package/src/app/api/external-agents/[id]/route.ts +31 -0
  23. package/src/app/api/external-agents/register/route.ts +3 -0
  24. package/src/app/api/external-agents/route.ts +66 -0
  25. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  26. package/src/app/api/gateways/[id]/route.ts +79 -0
  27. package/src/app/api/gateways/route.ts +57 -0
  28. package/src/app/api/openclaw/gateway/route.ts +10 -7
  29. package/src/app/api/openclaw/skills/route.ts +1 -1
  30. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  31. package/src/app/api/schedules/[id]/route.ts +38 -9
  32. package/src/app/api/schedules/route.ts +51 -28
  33. package/src/app/api/settings/route.ts +6 -10
  34. package/src/app/api/setup/doctor/route.ts +6 -4
  35. package/src/app/api/tasks/[id]/route.ts +2 -1
  36. package/src/app/api/tasks/bulk/route.ts +2 -2
  37. package/src/app/page.tsx +126 -15
  38. package/src/cli/binary.test.js +142 -0
  39. package/src/cli/index.js +34 -11
  40. package/src/cli/index.test.js +195 -0
  41. package/src/cli/index.ts +20 -4
  42. package/src/cli/server-cmd.test.js +59 -0
  43. package/src/cli/spec.js +20 -2
  44. package/src/components/agents/agent-sheet.tsx +249 -7
  45. package/src/components/agents/inspector-panel.tsx +3 -2
  46. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  47. package/src/components/auth/setup-wizard.tsx +970 -275
  48. package/src/components/chat/chat-area.tsx +41 -14
  49. package/src/components/chat/chat-card.tsx +2 -1
  50. package/src/components/chat/chat-header.tsx +8 -13
  51. package/src/components/chat/chat-list.tsx +58 -20
  52. package/src/components/chat/message-list.tsx +142 -18
  53. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  54. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  55. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  56. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +157 -86
  59. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  60. package/src/components/gateways/gateway-sheet.tsx +567 -0
  61. package/src/components/input/chat-input.tsx +135 -86
  62. package/src/components/layout/app-layout.tsx +2 -0
  63. package/src/components/memory/memory-browser.tsx +71 -6
  64. package/src/components/memory/memory-card.tsx +18 -0
  65. package/src/components/memory/memory-detail.tsx +58 -31
  66. package/src/components/memory/memory-sheet.tsx +32 -4
  67. package/src/components/projects/project-detail.tsx +7 -2
  68. package/src/components/providers/provider-list.tsx +158 -2
  69. package/src/components/providers/provider-sheet.tsx +81 -70
  70. package/src/components/shared/bottom-sheet.tsx +31 -15
  71. package/src/components/shared/confirm-dialog.tsx +45 -30
  72. package/src/components/shared/model-combobox.tsx +90 -8
  73. package/src/components/shared/settings/section-heartbeat.tsx +11 -6
  74. package/src/components/shared/settings/section-orchestrator.tsx +3 -0
  75. package/src/components/shared/settings/settings-page.tsx +5 -3
  76. package/src/components/tasks/approvals-panel.tsx +7 -1
  77. package/src/components/ui/dialog.tsx +2 -2
  78. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  79. package/src/lib/heartbeat-defaults.ts +48 -0
  80. package/src/lib/memory-presentation.ts +59 -0
  81. package/src/lib/provider-model-discovery-client.ts +29 -0
  82. package/src/lib/providers/index.ts +12 -5
  83. package/src/lib/runtime-loop.ts +105 -3
  84. package/src/lib/safe-storage.ts +6 -1
  85. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  86. package/src/lib/server/agent-runtime-config.ts +277 -0
  87. package/src/lib/server/agent-thread-session.test.ts +85 -0
  88. package/src/lib/server/agent-thread-session.ts +123 -0
  89. package/src/lib/server/approvals-auto-approve.test.ts +59 -0
  90. package/src/lib/server/build-llm.test.ts +13 -5
  91. package/src/lib/server/chat-execution-tool-events.test.ts +87 -2
  92. package/src/lib/server/chat-execution.ts +159 -71
  93. package/src/lib/server/chatroom-helpers.test.ts +7 -0
  94. package/src/lib/server/chatroom-helpers.ts +99 -6
  95. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  96. package/src/lib/server/connectors/manager.ts +89 -61
  97. package/src/lib/server/connectors/slack.ts +1 -1
  98. package/src/lib/server/daemon-state.ts +3 -2
  99. package/src/lib/server/data-dir.test.ts +56 -0
  100. package/src/lib/server/data-dir.ts +15 -9
  101. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  102. package/src/lib/server/eval/agent-regression.ts +1742 -0
  103. package/src/lib/server/eval/runner.ts +11 -1
  104. package/src/lib/server/eval/store.ts +2 -1
  105. package/src/lib/server/heartbeat-service.ts +23 -8
  106. package/src/lib/server/heartbeat-wake.ts +6 -2
  107. package/src/lib/server/main-agent-loop.ts +13 -6
  108. package/src/lib/server/openclaw-exec-config.ts +4 -2
  109. package/src/lib/server/openclaw-gateway.ts +123 -36
  110. package/src/lib/server/orchestrator-lg.ts +1 -2
  111. package/src/lib/server/orchestrator.ts +3 -2
  112. package/src/lib/server/plugins.test.ts +9 -1
  113. package/src/lib/server/plugins.ts +12 -2
  114. package/src/lib/server/provider-model-discovery.ts +481 -0
  115. package/src/lib/server/queue.ts +1 -1
  116. package/src/lib/server/runtime-settings.test.ts +119 -0
  117. package/src/lib/server/runtime-settings.ts +12 -92
  118. package/src/lib/server/schedule-normalization.ts +187 -0
  119. package/src/lib/server/session-tools/autonomy-tools.test.ts +23 -0
  120. package/src/lib/server/session-tools/crud.ts +27 -3
  121. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  122. package/src/lib/server/session-tools/discovery.ts +18 -8
  123. package/src/lib/server/session-tools/file-normalize.test.ts +5 -0
  124. package/src/lib/server/session-tools/file.ts +8 -2
  125. package/src/lib/server/session-tools/http.ts +9 -3
  126. package/src/lib/server/session-tools/index.ts +31 -1
  127. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  128. package/src/lib/server/session-tools/monitor.ts +14 -7
  129. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  130. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  131. package/src/lib/server/session-tools/platform.ts +1 -1
  132. package/src/lib/server/session-tools/plugin-creator.ts +9 -2
  133. package/src/lib/server/session-tools/sandbox.ts +51 -92
  134. package/src/lib/server/session-tools/session-info.ts +22 -1
  135. package/src/lib/server/session-tools/session-tools-wiring.test.ts +23 -0
  136. package/src/lib/server/session-tools/shell.ts +2 -2
  137. package/src/lib/server/session-tools/subagent.ts +3 -1
  138. package/src/lib/server/session-tools/web.ts +73 -30
  139. package/src/lib/server/storage.ts +29 -3
  140. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  141. package/src/lib/server/stream-agent-chat.ts +139 -4
  142. package/src/lib/server/structured-extract.ts +1 -1
  143. package/src/lib/server/task-mention.ts +0 -1
  144. package/src/lib/server/tool-aliases.ts +37 -6
  145. package/src/lib/server/tool-capability-policy.ts +1 -1
  146. package/src/lib/setup-defaults.ts +352 -11
  147. package/src/lib/tool-definitions.ts +3 -4
  148. package/src/lib/validation/schemas.ts +55 -1
  149. package/src/stores/use-app-store.ts +43 -1
  150. package/src/stores/use-chatroom-store.ts +153 -26
  151. package/src/types/index.ts +189 -6
  152. package/src/app/api/chats/[id]/main-loop/route.ts +0 -13
@@ -0,0 +1,31 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadExternalAgents, saveExternalAgents } from '@/lib/server/storage'
3
+ import { mutateItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
4
+ import { notify } from '@/lib/server/ws-hub'
5
+ export const dynamic = 'force-dynamic'
6
+
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ const ops: CollectionOps<any> = { load: loadExternalAgents, save: saveExternalAgents, topic: 'external_agents' }
9
+
10
+ export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
11
+ const { id } = await params
12
+ const body = await req.json().catch(() => ({}))
13
+ const result = mutateItem(ops, id, (runtime) => ({
14
+ ...runtime,
15
+ ...body,
16
+ id,
17
+ updatedAt: Date.now(),
18
+ }))
19
+ if (!result) return notFound()
20
+ return NextResponse.json(result)
21
+ }
22
+
23
+ export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
24
+ const { id } = await params
25
+ const items = loadExternalAgents()
26
+ if (!items[id]) return notFound()
27
+ delete items[id]
28
+ saveExternalAgents(items)
29
+ notify('external_agents')
30
+ return NextResponse.json({ ok: true })
31
+ }
@@ -0,0 +1,3 @@
1
+ import { POST } from '../route'
2
+ export const dynamic = 'force-dynamic'
3
+ export { POST }
@@ -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
+ }
@@ -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
+ }
@@ -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 gw = getGateway()
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
  }
@@ -4,7 +4,7 @@ import { resolveOpenClawGatewayAgentId } from '@/lib/server/openclaw-agent-resol
4
4
  import { normalizeOpenClawSkillsPayload } from '@/lib/server/openclaw-skills-normalize'
5
5
  import { loadAgents, saveAgents } from '@/lib/server/storage'
6
6
  import { notify } from '@/lib/server/ws-hub'
7
- import type { OpenClawSkillEntry, SkillAllowlistMode } from '@/types'
7
+ import type { SkillAllowlistMode } from '@/types'
8
8
 
9
9
  /** GET ?agentId=X — fetch skills from gateway with eligibility */
10
10
  export async function GET(req: Request) {
@@ -0,0 +1,27 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { discoverProviderModels } from '@/lib/server/provider-model-discovery'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ export async function GET(
7
+ req: Request,
8
+ { params }: { params: Promise<{ id: string }> },
9
+ ) {
10
+ const { id } = await params
11
+ const { searchParams } = new URL(req.url)
12
+ const result = await discoverProviderModels({
13
+ providerId: id,
14
+ credentialId: searchParams.get('credentialId'),
15
+ endpoint: searchParams.get('endpoint'),
16
+ force: searchParams.get('force') === '1',
17
+ requiresApiKey: searchParams.has('requiresApiKey')
18
+ ? searchParams.get('requiresApiKey') !== '0'
19
+ : undefined,
20
+ })
21
+
22
+ return NextResponse.json(result, {
23
+ headers: {
24
+ 'Cache-Control': 'private, no-store',
25
+ },
26
+ })
27
+ }
@@ -1,7 +1,9 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadSchedules, saveSchedules, deleteSchedule } from '@/lib/server/storage'
2
+ import { loadAgents, loadSchedules, loadSessions, saveSchedules, deleteSchedule } from '@/lib/server/storage'
3
+ import { WORKSPACE_DIR } from '@/lib/server/data-dir'
3
4
  import { resolveScheduleName } from '@/lib/schedule-name'
4
5
  import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
6
+ import { normalizeSchedulePayload } from '@/lib/server/schedule-normalization'
5
7
 
6
8
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
9
  const ops: CollectionOps<any> = { load: loadSchedules, save: saveSchedules, deleteFn: deleteSchedule, topic: 'schedules' }
@@ -9,15 +11,42 @@ const ops: CollectionOps<any> = { load: loadSchedules, save: saveSchedules, dele
9
11
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
10
12
  const { id } = await params
11
13
  const body = await req.json()
12
- const result = mutateItem(ops, id, (schedule) => {
13
- Object.assign(schedule, body)
14
- schedule.id = id
15
- schedule.name = resolveScheduleName({
16
- name: schedule.name,
17
- taskPrompt: schedule.taskPrompt,
14
+ const sessions = loadSessions()
15
+ const agents = loadAgents()
16
+ let result = null
17
+ try {
18
+ result = mutateItem(ops, id, (schedule) => {
19
+ const sessionCwd = typeof schedule.createdInSessionId === 'string'
20
+ ? sessions[schedule.createdInSessionId]?.cwd
21
+ : null
22
+ const normalized = normalizeSchedulePayload({
23
+ ...schedule,
24
+ ...(body as Record<string, unknown>),
25
+ id,
26
+ }, {
27
+ cwd: sessionCwd || WORKSPACE_DIR,
28
+ now: Date.now(),
29
+ })
30
+ if (!normalized.ok) throw new Error(normalized.error)
31
+ const nextSchedule = {
32
+ ...schedule,
33
+ ...normalized.value,
34
+ id,
35
+ updatedAt: Date.now(),
36
+ }
37
+ if (!agents[String(nextSchedule.agentId)]) {
38
+ throw new Error(`Agent not found: ${String(nextSchedule.agentId)}`)
39
+ }
40
+ nextSchedule.name = resolveScheduleName({
41
+ name: nextSchedule.name,
42
+ taskPrompt: nextSchedule.taskPrompt,
43
+ })
44
+ return nextSchedule
18
45
  })
19
- return schedule
20
- })
46
+ } catch (error: unknown) {
47
+ const message = error instanceof Error ? error.message : String(error)
48
+ return NextResponse.json({ error: message }, { status: 400 })
49
+ }
21
50
  if (!result) return notFound()
22
51
  return NextResponse.json(result)
23
52
  }
@@ -1,11 +1,31 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { genId } from '@/lib/id'
3
- import { loadSchedules, saveSchedules } from '@/lib/server/storage'
3
+ import { loadAgents, loadSchedules, saveSchedules } from '@/lib/server/storage'
4
+ import { WORKSPACE_DIR } from '@/lib/server/data-dir'
5
+ import { normalizeSchedulePayload } from '@/lib/server/schedule-normalization'
4
6
  import { resolveScheduleName } from '@/lib/schedule-name'
5
7
  import { findDuplicateSchedule } from '@/lib/schedule-dedupe'
6
8
  import { notify } from '@/lib/server/ws-hub'
7
9
  export const dynamic = 'force-dynamic'
8
10
 
11
+ function asString(value: unknown): string {
12
+ return typeof value === 'string' ? value.trim() : ''
13
+ }
14
+
15
+ function asPositiveInt(value: unknown): number | null {
16
+ const parsed = typeof value === 'number'
17
+ ? value
18
+ : typeof value === 'string'
19
+ ? Number.parseInt(value, 10)
20
+ : Number.NaN
21
+ if (!Number.isFinite(parsed)) return null
22
+ const intValue = Math.trunc(parsed)
23
+ return intValue > 0 ? intValue : null
24
+ }
25
+
26
+ function asScheduleType(value: unknown): 'cron' | 'interval' | 'once' {
27
+ return value === 'cron' || value === 'interval' || value === 'once' ? value : 'cron'
28
+ }
9
29
 
10
30
  export async function GET(_req: Request) {
11
31
  return NextResponse.json(loadSchedules())
@@ -15,28 +35,46 @@ export async function POST(req: Request) {
15
35
  const body = await req.json()
16
36
  const now = Date.now()
17
37
  const schedules = loadSchedules()
18
- const scheduleType = body.scheduleType || 'cron'
38
+ const normalizedSchedule = normalizeSchedulePayload(body as Record<string, unknown>, {
39
+ cwd: WORKSPACE_DIR,
40
+ now,
41
+ })
42
+ if (!normalizedSchedule.ok) {
43
+ return NextResponse.json({ error: normalizedSchedule.error }, { status: 400 })
44
+ }
45
+
46
+ const candidate = normalizedSchedule.value
47
+ const agents = loadAgents()
48
+ if (!agents[String(candidate.agentId)]) {
49
+ return NextResponse.json({ error: `Agent not found: ${String(candidate.agentId)}` }, { status: 400 })
50
+ }
51
+ const scheduleType = asScheduleType(candidate.scheduleType)
52
+ const candidateAgentId = asString(candidate.agentId) || null
53
+ const candidateTaskPrompt = asString(candidate.taskPrompt)
54
+ const candidateCron = asString(candidate.cron) || null
55
+ const candidateIntervalMs = asPositiveInt(candidate.intervalMs)
56
+ const candidateRunAt = asPositiveInt(candidate.runAt)
19
57
 
20
58
  const duplicate = findDuplicateSchedule(schedules, {
21
- agentId: body.agentId || null,
22
- taskPrompt: body.taskPrompt || '',
59
+ agentId: candidateAgentId,
60
+ taskPrompt: candidateTaskPrompt,
23
61
  scheduleType,
24
- cron: body.cron,
25
- intervalMs: body.intervalMs,
26
- runAt: body.runAt,
62
+ cron: candidateCron,
63
+ intervalMs: candidateIntervalMs,
64
+ runAt: candidateRunAt,
27
65
  })
28
66
  if (duplicate) {
29
67
  const duplicateId = duplicate.id || ''
30
68
  let changed = false
31
69
  const nextName = resolveScheduleName({
32
- name: body.name ?? duplicate.name,
33
- taskPrompt: body.taskPrompt ?? duplicate.taskPrompt,
70
+ name: candidate.name ?? duplicate.name,
71
+ taskPrompt: candidate.taskPrompt ?? duplicate.taskPrompt,
34
72
  })
35
73
  if (nextName && nextName !== duplicate.name) {
36
74
  duplicate.name = nextName
37
75
  changed = true
38
76
  }
39
- const normalizedStatus = typeof body.status === 'string' ? body.status.trim().toLowerCase() : ''
77
+ const normalizedStatus = typeof candidate.status === 'string' ? candidate.status.trim().toLowerCase() : ''
40
78
  if ((normalizedStatus === 'active' || normalizedStatus === 'paused') && duplicate.status !== normalizedStatus) {
41
79
  duplicate.status = normalizedStatus as 'active' | 'paused'
42
80
  changed = true
@@ -53,29 +91,14 @@ export async function POST(req: Request) {
53
91
 
54
92
  const id = genId()
55
93
 
56
- let nextRunAt: number | undefined
57
- if (scheduleType === 'once' && body.runAt) {
58
- nextRunAt = body.runAt
59
- } else if (scheduleType === 'interval' && body.intervalMs) {
60
- nextRunAt = now + body.intervalMs
61
- } else if (scheduleType === 'cron') {
62
- // nextRunAt will be computed by the scheduler engine
63
- nextRunAt = undefined
64
- }
65
-
66
94
  schedules[id] = {
67
95
  id,
68
- name: resolveScheduleName({ name: body.name, taskPrompt: body.taskPrompt }),
69
- agentId: body.agentId,
70
- taskPrompt: body.taskPrompt || '',
96
+ ...candidate,
97
+ name: resolveScheduleName({ name: candidate.name, taskPrompt: candidate.taskPrompt }),
71
98
  scheduleType,
72
- cron: body.cron,
73
- intervalMs: body.intervalMs,
74
- runAt: body.runAt,
75
99
  lastRunAt: undefined,
76
- nextRunAt,
77
- status: body.status || 'active',
78
100
  createdAt: now,
101
+ updatedAt: now,
79
102
  }
80
103
  saveSchedules(schedules)
81
104
  notify('schedules')
@@ -1,6 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
+ import { normalizeHeartbeatSettingFields } from '@/lib/heartbeat-defaults'
2
3
  import { loadPublicSettings, loadSettings, saveSettings } from '@/lib/server/storage'
3
- import { DEFAULT_DELEGATION_MAX_DEPTH } from '@/lib/runtime-loop'
4
+ import { normalizeRuntimeSettingFields } from '@/lib/runtime-loop'
4
5
  export const dynamic = 'force-dynamic'
5
6
 
6
7
 
@@ -10,8 +11,6 @@ const MEMORY_PER_LOOKUP_MIN = 1
10
11
  const MEMORY_PER_LOOKUP_MAX = 200
11
12
  const MEMORY_LINKED_MIN = 0
12
13
  const MEMORY_LINKED_MAX = 1000
13
- const DELEGATION_DEPTH_MIN = 1
14
- const DELEGATION_DEPTH_MAX = 12
15
14
  const RESPONSE_CACHE_TTL_MIN_SEC = 5
16
15
  const RESPONSE_CACHE_TTL_MAX_SEC = 7 * 24 * 3600
17
16
  const RESPONSE_CACHE_MAX_ENTRIES_MIN = 1
@@ -83,12 +82,8 @@ export async function PUT(req: Request) {
83
82
  MEMORY_LINKED_MIN,
84
83
  MEMORY_LINKED_MAX,
85
84
  )
86
- const nextDelegationDepth = parseIntSetting(
87
- settings.delegationMaxDepth,
88
- DEFAULT_DELEGATION_MAX_DEPTH,
89
- DELEGATION_DEPTH_MIN,
90
- DELEGATION_DEPTH_MAX,
91
- )
85
+ const normalizedRuntime = normalizeRuntimeSettingFields(settings)
86
+ const normalizedHeartbeat = normalizeHeartbeatSettingFields(settings)
92
87
  const nextResponseCacheTtlSec = parseIntSetting(
93
88
  settings.responseCacheTtlSec,
94
89
  15 * 60,
@@ -120,7 +115,8 @@ export async function PUT(req: Request) {
120
115
  settings.maxMemoriesPerLookup = nextPerLookup
121
116
  settings.memoryMaxPerLookup = nextPerLookup
122
117
  settings.maxLinkedMemoriesExpanded = nextLinked
123
- settings.delegationMaxDepth = nextDelegationDepth
118
+ Object.assign(settings, normalizedRuntime)
119
+ Object.assign(settings, normalizedHeartbeat)
124
120
  settings.responseCacheTtlSec = nextResponseCacheTtlSec
125
121
  settings.responseCacheMaxEntries = nextResponseCacheMaxEntries
126
122
  settings.responseCacheEnabled = parseBoolSetting(settings.responseCacheEnabled, true)
@@ -90,12 +90,14 @@ export async function GET(req: Request) {
90
90
  const checkedAt = Date.now()
91
91
 
92
92
  const nodeVersion = process.versions.node
93
- const nodeMajor = Number.parseInt(String(nodeVersion).split('.')[0] || '0', 10)
94
- if (nodeMajor >= 20) {
93
+ const [nodeMajorRaw, nodeMinorRaw] = String(nodeVersion).split('.')
94
+ const nodeMajor = Number.parseInt(nodeMajorRaw || '0', 10)
95
+ const nodeMinor = Number.parseInt(nodeMinorRaw || '0', 10)
96
+ if (nodeMajor > 22 || (nodeMajor === 22 && nodeMinor >= 6)) {
95
97
  pushCheck(checks, 'node-version', 'Node.js version', 'pass', `Detected Node ${nodeVersion}.`, true)
96
98
  } else {
97
- pushCheck(checks, 'node-version', 'Node.js version', 'fail', `Detected Node ${nodeVersion}. Node 20+ is required.`, true)
98
- actions.push('Install Node.js 20 or newer from https://nodejs.org and rerun setup.')
99
+ pushCheck(checks, 'node-version', 'Node.js version', 'fail', `Detected Node ${nodeVersion}. Node 22.6+ is required.`, true)
100
+ actions.push('Install Node.js 22.6 or newer from https://nodejs.org and rerun setup.')
99
101
  }
100
102
 
101
103
  const npmCheck = run('npm', ['--version'], 5_000)
@@ -13,6 +13,7 @@ import { requestHeartbeatNow } from '@/lib/server/heartbeat-wake'
13
13
  import { validateDag, cascadeUnblock } from '@/lib/server/dag-validation'
14
14
  import { getPluginManager } from '@/lib/server/plugins'
15
15
  import { normalizeTaskQualityGate } from '@/lib/server/task-quality-gate'
16
+ import type { BoardTask } from '@/types'
16
17
  import '@/lib/server/builtin-plugins'
17
18
 
18
19
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
@@ -151,7 +152,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
151
152
  if (unblockedIds.length > 0) {
152
153
  upsertStoredItems('tasks', [
153
154
  [id, tasks[id]],
154
- ...unblockedIds.map((uid) => [uid, tasks[uid]] as [string, any]),
155
+ ...unblockedIds.map((uid) => [uid, tasks[uid]] as [string, BoardTask]),
155
156
  ])
156
157
  for (const uid of unblockedIds) {
157
158
  enqueueTask(uid)
@@ -4,7 +4,7 @@ import { enqueueTask, disableSessionHeartbeat } from '@/lib/server/queue'
4
4
  import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
5
5
  import { notify } from '@/lib/server/ws-hub'
6
6
  import { createNotification } from '@/lib/server/create-notification'
7
- import type { BoardTaskStatus } from '@/types'
7
+ import type { BoardTask, BoardTaskStatus } from '@/types'
8
8
 
9
9
  const VALID_STATUSES: BoardTaskStatus[] = ['backlog', 'queued', 'running', 'completed', 'failed', 'archived']
10
10
 
@@ -82,7 +82,7 @@ export async function POST(req: Request) {
82
82
  }
83
83
  }
84
84
 
85
- upsertStoredItems('tasks', results.map((id) => [id, tasks[id]] as [string, any]))
85
+ upsertStoredItems('tasks', results.map((id) => [id, tasks[id]] as [string, BoardTask]))
86
86
 
87
87
  if (updated > 0) {
88
88
  const action = body.status