@swarmclawai/swarmclaw 0.3.0 → 0.4.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.
Files changed (118) hide show
  1. package/README.md +20 -11
  2. package/bin/server-cmd.js +14 -7
  3. package/bin/swarmclaw.js +3 -1
  4. package/bin/update-cmd.js +120 -0
  5. package/next.config.ts +2 -0
  6. package/package.json +3 -1
  7. package/src/app/api/agents/[id]/route.ts +3 -0
  8. package/src/app/api/agents/[id]/thread/route.ts +2 -1
  9. package/src/app/api/agents/route.ts +5 -1
  10. package/src/app/api/auth/route.ts +3 -1
  11. package/src/app/api/claude-skills/route.ts +3 -1
  12. package/src/app/api/connectors/[id]/route.ts +4 -0
  13. package/src/app/api/connectors/route.ts +6 -1
  14. package/src/app/api/credentials/route.ts +3 -1
  15. package/src/app/api/daemon/route.ts +6 -1
  16. package/src/app/api/ip/route.ts +3 -1
  17. package/src/app/api/mcp-servers/route.ts +3 -1
  18. package/src/app/api/orchestrator/graph/route.ts +25 -0
  19. package/src/app/api/plugins/marketplace/route.ts +3 -1
  20. package/src/app/api/plugins/route.ts +3 -1
  21. package/src/app/api/providers/[id]/route.ts +3 -0
  22. package/src/app/api/providers/configs/route.ts +3 -1
  23. package/src/app/api/providers/route.ts +5 -1
  24. package/src/app/api/schedules/[id]/route.ts +3 -0
  25. package/src/app/api/schedules/route.ts +6 -1
  26. package/src/app/api/secrets/route.ts +3 -1
  27. package/src/app/api/sessions/[id]/chat/route.ts +5 -2
  28. package/src/app/api/sessions/route.ts +9 -2
  29. package/src/app/api/settings/route.ts +3 -1
  30. package/src/app/api/setup/doctor/route.ts +1 -0
  31. package/src/app/api/setup/openclaw-device/route.ts +3 -1
  32. package/src/app/api/skills/route.ts +3 -1
  33. package/src/app/api/tasks/[id]/approve/route.ts +73 -0
  34. package/src/app/api/tasks/[id]/route.ts +3 -0
  35. package/src/app/api/tasks/route.ts +3 -0
  36. package/src/app/api/usage/route.ts +3 -1
  37. package/src/app/api/version/route.ts +3 -1
  38. package/src/app/api/webhooks/[id]/route.ts +2 -1
  39. package/src/app/api/webhooks/route.ts +3 -1
  40. package/src/app/icon.svg +58 -0
  41. package/src/app/page.tsx +8 -2
  42. package/src/cli/index.js +1 -9
  43. package/src/cli/index.ts +51 -1
  44. package/src/cli/spec.js +0 -8
  45. package/src/components/agents/agent-card.tsx +1 -1
  46. package/src/components/agents/agent-sheet.tsx +63 -80
  47. package/src/components/chat/chat-area.tsx +44 -30
  48. package/src/components/chat/chat-tool-toggles.tsx +12 -53
  49. package/src/components/chat/message-bubble.tsx +110 -42
  50. package/src/components/chat/tool-call-bubble.tsx +41 -3
  51. package/src/components/chat/tool-request-banner.tsx +1 -9
  52. package/src/components/connectors/connector-list.tsx +3 -8
  53. package/src/components/connectors/connector-sheet.tsx +24 -29
  54. package/src/components/input/chat-input.tsx +72 -56
  55. package/src/components/knowledge/knowledge-list.tsx +27 -31
  56. package/src/components/layout/app-layout.tsx +92 -71
  57. package/src/components/layout/daemon-indicator.tsx +3 -5
  58. package/src/components/logs/log-list.tsx +5 -9
  59. package/src/components/mcp-servers/mcp-server-list.tsx +24 -2
  60. package/src/components/memory/memory-detail.tsx +1 -1
  61. package/src/components/plugins/plugin-list.tsx +227 -27
  62. package/src/components/providers/provider-list.tsx +46 -13
  63. package/src/components/providers/provider-sheet.tsx +0 -45
  64. package/src/components/runs/run-list.tsx +6 -15
  65. package/src/components/schedules/schedule-card.tsx +54 -4
  66. package/src/components/schedules/schedule-list.tsx +6 -3
  67. package/src/components/schedules/schedule-sheet.tsx +0 -47
  68. package/src/components/secrets/secrets-list.tsx +20 -2
  69. package/src/components/sessions/new-session-sheet.tsx +8 -9
  70. package/src/components/shared/connector-platform-icon.tsx +22 -20
  71. package/src/components/shared/model-combobox.tsx +148 -0
  72. package/src/components/shared/settings/section-heartbeat.tsx +7 -39
  73. package/src/components/shared/settings/section-orchestrator.tsx +8 -9
  74. package/src/components/skills/skill-list.tsx +260 -34
  75. package/src/components/skills/skill-sheet.tsx +0 -45
  76. package/src/components/tasks/task-board.tsx +3 -6
  77. package/src/components/tasks/task-card.tsx +43 -1
  78. package/src/components/tasks/task-list.tsx +3 -5
  79. package/src/components/tasks/task-sheet.tsx +0 -44
  80. package/src/components/usage/usage-list.tsx +12 -4
  81. package/src/hooks/use-ws.ts +66 -0
  82. package/src/instrumentation.ts +2 -0
  83. package/src/lib/chat.ts +14 -2
  84. package/src/lib/providers/anthropic.ts +1 -1
  85. package/src/lib/providers/index.ts +2 -0
  86. package/src/lib/providers/ollama.ts +1 -1
  87. package/src/lib/providers/openai.ts +33 -12
  88. package/src/lib/server/chat-execution.ts +19 -4
  89. package/src/lib/server/connectors/manager.ts +9 -3
  90. package/src/lib/server/context-manager.ts +1 -1
  91. package/src/lib/server/daemon-state.ts +3 -0
  92. package/src/lib/server/data-dir.ts +1 -0
  93. package/src/lib/server/heartbeat-service.ts +67 -3
  94. package/src/lib/server/langgraph-checkpoint.ts +274 -0
  95. package/src/lib/server/main-agent-loop.ts +61 -2
  96. package/src/lib/server/orchestrator-lg.ts +394 -13
  97. package/src/lib/server/orchestrator.ts +25 -5
  98. package/src/lib/server/queue.ts +17 -3
  99. package/src/lib/server/session-run-manager.ts +6 -1
  100. package/src/lib/server/session-tools/delegate.ts +2 -2
  101. package/src/lib/server/session-tools/index.ts +2 -0
  102. package/src/lib/server/session-tools/sandbox.ts +164 -0
  103. package/src/lib/server/storage-mcp.test.ts +25 -2
  104. package/src/lib/server/storage.ts +24 -7
  105. package/src/lib/server/stream-agent-chat.ts +77 -22
  106. package/src/lib/server/task-validation.test.ts +23 -0
  107. package/src/lib/server/task-validation.ts +5 -3
  108. package/src/lib/server/ws-hub.ts +85 -0
  109. package/src/lib/tool-definitions.ts +42 -0
  110. package/src/lib/upload.ts +7 -1
  111. package/src/lib/ws-client.ts +124 -0
  112. package/src/stores/use-chat-store.ts +33 -13
  113. package/src/types/index.ts +8 -1
  114. package/src/app/api/agents/generate/route.ts +0 -42
  115. package/src/app/api/generate/info/route.ts +0 -12
  116. package/src/app/api/generate/route.ts +0 -106
  117. package/src/app/favicon.ico +0 -0
  118. package/src/components/shared/ai-gen-block.tsx +0 -77
@@ -1,6 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadSchedules, saveSchedules, deleteSchedule } from '@/lib/server/storage'
3
3
  import { resolveScheduleName } from '@/lib/schedule-name'
4
+ import { notify } from '@/lib/server/ws-hub'
4
5
 
5
6
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
6
7
  const { id } = await params
@@ -16,6 +17,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
16
17
  taskPrompt: schedules[id].taskPrompt,
17
18
  })
18
19
  saveSchedules(schedules)
20
+ notify('schedules')
19
21
  return NextResponse.json(schedules[id])
20
22
  }
21
23
 
@@ -24,5 +26,6 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
24
26
  const schedules = loadSchedules()
25
27
  if (!schedules[id]) return new NextResponse(null, { status: 404 })
26
28
  deleteSchedule(id)
29
+ notify('schedules')
27
30
  return NextResponse.json('ok')
28
31
  }
@@ -3,8 +3,11 @@ import crypto from 'crypto'
3
3
  import { loadSchedules, saveSchedules } from '@/lib/server/storage'
4
4
  import { resolveScheduleName } from '@/lib/schedule-name'
5
5
  import { findDuplicateSchedule } from '@/lib/schedule-dedupe'
6
+ import { notify } from '@/lib/server/ws-hub'
7
+ export const dynamic = 'force-dynamic'
6
8
 
7
- export async function GET() {
9
+
10
+ export async function GET(_req: Request) {
8
11
  return NextResponse.json(loadSchedules())
9
12
  }
10
13
 
@@ -43,6 +46,7 @@ export async function POST(req: Request) {
43
46
  mutableDuplicate.updatedAt = now
44
47
  if (duplicateId) schedules[duplicateId] = duplicate
45
48
  saveSchedules(schedules)
49
+ notify('schedules')
46
50
  }
47
51
  return NextResponse.json(duplicate)
48
52
  }
@@ -74,5 +78,6 @@ export async function POST(req: Request) {
74
78
  createdAt: now,
75
79
  }
76
80
  saveSchedules(schedules)
81
+ notify('schedules')
77
82
  return NextResponse.json(schedules[id])
78
83
  }
@@ -1,8 +1,10 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import crypto from 'crypto'
3
3
  import { loadSecrets, saveSecrets, encryptKey } from '@/lib/server/storage'
4
+ export const dynamic = 'force-dynamic'
4
5
 
5
- export async function GET() {
6
+
7
+ export async function GET(_req: Request) {
6
8
  // Return secrets WITHOUT the encrypted values (just metadata)
7
9
  const secrets = loadSecrets()
8
10
  const safe = Object.fromEntries(
@@ -14,11 +14,13 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
14
14
  const message = typeof body.message === 'string' ? body.message : ''
15
15
  const imagePath = typeof body.imagePath === 'string' ? body.imagePath : undefined
16
16
  const imageUrl = typeof body.imageUrl === 'string' ? body.imageUrl : undefined
17
+ const attachedFiles = Array.isArray(body.attachedFiles) ? body.attachedFiles.filter((f: unknown) => typeof f === 'string') as string[] : undefined
17
18
  const internal = body.internal === true
18
19
  const queueMode = normalizeQueueMode(body.queueMode, internal)
19
20
 
20
- if (!message.trim()) {
21
- return NextResponse.json({ error: 'message is required' }, { status: 400 })
21
+ const hasFiles = !!(imagePath || imageUrl || (attachedFiles && attachedFiles.length > 0))
22
+ if (!message.trim() && !hasFiles) {
23
+ return NextResponse.json({ error: 'message or file is required' }, { status: 400 })
22
24
  }
23
25
 
24
26
  const encoder = new TextEncoder()
@@ -39,6 +41,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
39
41
  message,
40
42
  imagePath,
41
43
  imageUrl,
44
+ attachedFiles,
42
45
  internal,
43
46
  source: internal ? 'heartbeat' : 'chat',
44
47
  mode: queueMode,
@@ -3,10 +3,14 @@ import crypto from 'crypto'
3
3
  import os from 'os'
4
4
  import path from 'path'
5
5
  import { loadSessions, saveSessions, deleteSession, active, loadAgents } from '@/lib/server/storage'
6
+ import { WORKSPACE_DIR } from '@/lib/server/data-dir'
7
+ import { notify } from '@/lib/server/ws-hub'
6
8
  import { getSessionRunState } from '@/lib/server/session-run-manager'
7
9
  import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
10
+ export const dynamic = 'force-dynamic'
8
11
 
9
- export async function GET() {
12
+
13
+ export async function GET(_req: Request) {
10
14
  const sessions = loadSessions()
11
15
  for (const id of Object.keys(sessions)) {
12
16
  const run = getSessionRunState(id)
@@ -31,6 +35,7 @@ export async function DELETE(req: Request) {
31
35
  }
32
36
  deleteSession(id)
33
37
  }
38
+ notify('sessions')
34
39
  return NextResponse.json({ deleted: ids.length })
35
40
  }
36
41
 
@@ -38,7 +43,8 @@ export async function POST(req: Request) {
38
43
  const body = await req.json()
39
44
  let cwd = (body.cwd || '').trim()
40
45
  if (cwd.startsWith('~/')) cwd = path.join(os.homedir(), cwd.slice(2))
41
- else if (cwd === '~' || !cwd) cwd = os.homedir()
46
+ else if (cwd === '~') cwd = os.homedir()
47
+ else if (!cwd) cwd = WORKSPACE_DIR
42
48
 
43
49
  const id = body.id || crypto.randomBytes(4).toString('hex')
44
50
  const sessions = loadSessions()
@@ -81,5 +87,6 @@ export async function POST(req: Request) {
81
87
  heartbeatIntervalSec: body.heartbeatIntervalSec ?? null,
82
88
  }
83
89
  saveSessions(sessions)
90
+ notify('sessions')
84
91
  return NextResponse.json(sessions[id])
85
92
  }
@@ -1,5 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadSettings, saveSettings } from '@/lib/server/storage'
3
+ export const dynamic = 'force-dynamic'
4
+
3
5
 
4
6
  const MEMORY_DEPTH_MIN = 0
5
7
  const MEMORY_DEPTH_MAX = 12
@@ -18,7 +20,7 @@ function parseIntSetting(value: unknown, fallback: number, min: number, max: num
18
20
  return Math.max(min, Math.min(max, Math.trunc(parsed)))
19
21
  }
20
22
 
21
- export async function GET() {
23
+ export async function GET(_req: Request) {
22
24
  return NextResponse.json(loadSettings())
23
25
  }
24
26
 
@@ -165,6 +165,7 @@ export async function GET(req: Request) {
165
165
  { id: 'claude-cli', label: 'Claude Code CLI', command: 'claude' },
166
166
  { id: 'codex-cli', label: 'OpenAI Codex CLI', command: 'codex' },
167
167
  { id: 'opencode-cli', label: 'OpenCode CLI', command: 'opencode' },
168
+ { id: 'deno', label: 'Deno (sandbox runtime)', command: 'deno' },
168
169
  ]
169
170
 
170
171
  for (const binary of optionalBinaries) {
@@ -1,7 +1,9 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { getDeviceId } from '@/lib/providers/openclaw'
3
+ export const dynamic = 'force-dynamic'
3
4
 
4
- export async function GET() {
5
+
6
+ export async function GET(_req: Request) {
5
7
  try {
6
8
  const deviceId = getDeviceId()
7
9
  return NextResponse.json({ deviceId })
@@ -2,8 +2,10 @@ import { NextResponse } from 'next/server'
2
2
  import crypto from 'crypto'
3
3
  import { loadSkills, saveSkills } from '@/lib/server/storage'
4
4
  import { normalizeSkillPayload } from '@/lib/server/skills-normalize'
5
+ export const dynamic = 'force-dynamic'
5
6
 
6
- export async function GET() {
7
+
8
+ export async function GET(_req: Request) {
7
9
  return NextResponse.json(loadSkills())
8
10
  }
9
11
 
@@ -0,0 +1,73 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadTasks, saveTasks, loadAgents } from '@/lib/server/storage'
3
+ import { notify } from '@/lib/server/ws-hub'
4
+ import { getCheckpointSaver } from '@/lib/server/langgraph-checkpoint'
5
+
6
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
7
+ const { id } = await params
8
+ const body = await req.json()
9
+ const approved = body.approved === true
10
+
11
+ const tasks = loadTasks()
12
+ const task = tasks[id]
13
+ if (!task) return new NextResponse(null, { status: 404 })
14
+ if (!task.pendingApproval) {
15
+ return NextResponse.json({ error: 'No pending approval on this task' }, { status: 400 })
16
+ }
17
+
18
+ const { threadId } = task.pendingApproval
19
+
20
+ if (!approved) {
21
+ // Reject: clear approval, delete checkpoint, fail the task
22
+ task.pendingApproval = null
23
+ task.status = 'failed'
24
+ task.error = 'Tool execution rejected by user'
25
+ task.updatedAt = Date.now()
26
+ saveTasks(tasks)
27
+ await getCheckpointSaver().deleteThread(threadId)
28
+ notify('tasks')
29
+ return NextResponse.json({ status: 'rejected' })
30
+ }
31
+
32
+ // Approve: clear pendingApproval, resume the graph
33
+ const agents = loadAgents()
34
+ const agent = agents[task.agentId]
35
+ if (!agent) {
36
+ return NextResponse.json({ error: 'Agent not found' }, { status: 400 })
37
+ }
38
+
39
+ task.pendingApproval = null
40
+ task.updatedAt = Date.now()
41
+ saveTasks(tasks)
42
+ notify('tasks')
43
+
44
+ // Resume in the background
45
+ const sessionId = task.sessionId || ''
46
+ setImmediate(async () => {
47
+ try {
48
+ const { resumeLangGraphOrchestrator } = await import('@/lib/server/orchestrator-lg')
49
+ const result = await resumeLangGraphOrchestrator(agent, sessionId, threadId)
50
+ const t2 = loadTasks()
51
+ if (t2[id] && !t2[id].pendingApproval) {
52
+ // Only mark completed if not paused again
53
+ if (t2[id].status === 'running') {
54
+ t2[id].result = result
55
+ }
56
+ t2[id].updatedAt = Date.now()
57
+ saveTasks(t2)
58
+ notify('tasks')
59
+ }
60
+ } catch (err: any) {
61
+ console.error(`[approve] Resume failed for task ${id}:`, err.message)
62
+ const t2 = loadTasks()
63
+ if (t2[id]) {
64
+ t2[id].error = err.message || String(err)
65
+ t2[id].updatedAt = Date.now()
66
+ saveTasks(t2)
67
+ notify('tasks')
68
+ }
69
+ }
70
+ })
71
+
72
+ return NextResponse.json({ status: 'approved', resuming: true })
73
+ }
@@ -5,6 +5,7 @@ import { disableSessionHeartbeat, enqueueTask, validateCompletedTasksQueue } fro
5
5
  import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
6
6
  import { formatValidationFailure, validateTaskCompletion } from '@/lib/server/task-validation'
7
7
  import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
8
+ import { notify } from '@/lib/server/ws-hub'
8
9
 
9
10
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
10
11
  // Keep completed queue integrity even if daemon is not running.
@@ -80,6 +81,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
80
81
  enqueueTask(id)
81
82
  }
82
83
 
84
+ notify('tasks')
83
85
  return NextResponse.json(tasks[id])
84
86
  }
85
87
 
@@ -98,5 +100,6 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
98
100
  text: `Task archived: "${tasks[id].title}" (${id}).`,
99
101
  })
100
102
 
103
+ notify('tasks')
101
104
  return NextResponse.json(tasks[id])
102
105
  }
@@ -5,6 +5,7 @@ import { enqueueTask, validateCompletedTasksQueue } from '@/lib/server/queue'
5
5
  import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
6
6
  import { formatValidationFailure, validateTaskCompletion } from '@/lib/server/task-validation'
7
7
  import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
8
+ import { notify } from '@/lib/server/ws-hub'
8
9
 
9
10
  export async function GET(req: Request) {
10
11
  // Keep completed queue integrity even if daemon is not running.
@@ -47,6 +48,7 @@ export async function DELETE(req: Request) {
47
48
  removed++
48
49
  }
49
50
  }
51
+ notify('tasks')
50
52
  return NextResponse.json({ removed, remaining: Object.keys(tasks).length - removed })
51
53
  }
52
54
 
@@ -111,5 +113,6 @@ export async function POST(req: Request) {
111
113
  if (tasks[id].status === 'queued') {
112
114
  enqueueTask(id)
113
115
  }
116
+ notify('tasks')
114
117
  return NextResponse.json(tasks[id])
115
118
  }
@@ -1,7 +1,9 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadUsage } from '@/lib/server/storage'
3
+ export const dynamic = 'force-dynamic'
3
4
 
4
- export async function GET() {
5
+
6
+ export async function GET(_req: Request) {
5
7
  const usage = loadUsage()
6
8
  // Compute summary
7
9
  let totalTokens = 0
@@ -1,5 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { execSync } from 'child_process'
3
+ export const dynamic = 'force-dynamic'
4
+
3
5
 
4
6
  let cachedRemote: {
5
7
  sha: string
@@ -31,7 +33,7 @@ function getHeadStableTag(): string | null {
31
33
  return tags.find((tag) => RELEASE_TAG_RE.test(tag)) || null
32
34
  }
33
35
 
34
- export async function GET() {
36
+ export async function GET(_req: Request) {
35
37
  try {
36
38
  const localSha = run('git rev-parse --short HEAD')
37
39
  const localTag = getHeadStableTag()
@@ -1,6 +1,7 @@
1
1
  import crypto from 'crypto'
2
2
  import { NextResponse } from 'next/server'
3
3
  import { loadAgents, loadSessions, loadWebhooks, saveSessions, saveWebhooks, appendWebhookLog } from '@/lib/server/storage'
4
+ import { WORKSPACE_DIR } from '@/lib/server/data-dir'
4
5
  import { enqueueSessionRun } from '@/lib/server/session-run-manager'
5
6
 
6
7
  function normalizeEvents(value: unknown): string[] {
@@ -137,7 +138,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
137
138
  session = {
138
139
  id: sessionId,
139
140
  name: sessionName,
140
- cwd: process.cwd(),
141
+ cwd: WORKSPACE_DIR,
141
142
  user: 'system',
142
143
  provider: agent.provider || 'claude-cli',
143
144
  model: agent.model || '',
@@ -1,6 +1,8 @@
1
1
  import crypto from 'crypto'
2
2
  import { NextResponse } from 'next/server'
3
3
  import { loadWebhooks, saveWebhooks } from '@/lib/server/storage'
4
+ export const dynamic = 'force-dynamic'
5
+
4
6
 
5
7
  function normalizeEvents(value: unknown): string[] {
6
8
  if (!Array.isArray(value)) return []
@@ -10,7 +12,7 @@ function normalizeEvents(value: unknown): string[] {
10
12
  .filter(Boolean)
11
13
  }
12
14
 
13
- export async function GET() {
15
+ export async function GET(_req: Request) {
14
16
  return NextResponse.json(loadWebhooks())
15
17
  }
16
18
 
@@ -0,0 +1,58 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" role="img" aria-labelledby="title desc">
2
+ <title id="title">SwarmClaw Lobster Avatar</title>
3
+ <desc id="desc">SwarmClaw org avatar using an OpenClaw-inspired lobster mark with swarm accents.</desc>
4
+
5
+ <defs>
6
+ <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
7
+ <stop offset="0%" stop-color="#050B18"/>
8
+ <stop offset="100%" stop-color="#111827"/>
9
+ </linearGradient>
10
+ <radialGradient id="glow" cx="50%" cy="38%" r="62%">
11
+ <stop offset="0%" stop-color="#22d3ee" stop-opacity="0.22"/>
12
+ <stop offset="100%" stop-color="#22d3ee" stop-opacity="0"/>
13
+ </radialGradient>
14
+ <linearGradient id="lobster-gradient" x1="0%" y1="0%" x2="100%" y2="100%">
15
+ <stop offset="0%" stop-color="#ff6a5f"/>
16
+ <stop offset="100%" stop-color="#a41318"/>
17
+ </linearGradient>
18
+ <filter id="soft-shadow" x="-20%" y="-20%" width="140%" height="140%">
19
+ <feDropShadow dx="0" dy="16" stdDeviation="18" flood-color="#020617" flood-opacity="0.55"/>
20
+ </filter>
21
+ </defs>
22
+
23
+ <rect x="40" y="40" width="944" height="944" rx="216" fill="url(#bg)"/>
24
+ <rect x="40" y="40" width="944" height="944" rx="216" fill="url(#glow)"/>
25
+ <rect x="56" y="56" width="912" height="912" rx="200" fill="none" stroke="#334155" stroke-width="6"/>
26
+
27
+ <!-- swarm accents -->
28
+ <g stroke="#22d3ee" stroke-opacity="0.8" stroke-width="10" fill="none" stroke-linecap="round">
29
+ <path d="M182 286 C232 236, 314 224, 378 252"/>
30
+ <path d="M842 286 C792 236, 710 224, 646 252"/>
31
+ <path d="M202 760 C270 814, 350 826, 420 806"/>
32
+ <path d="M822 760 C754 814, 674 826, 604 806"/>
33
+ </g>
34
+ <g fill="#67e8f9">
35
+ <circle cx="172" cy="282" r="14"/>
36
+ <circle cx="852" cy="282" r="14"/>
37
+ <circle cx="198" cy="760" r="12"/>
38
+ <circle cx="826" cy="760" r="12"/>
39
+ </g>
40
+
41
+ <!-- OpenClaw-inspired lobster mark -->
42
+ <g transform="translate(152 156) scale(6)" filter="url(#soft-shadow)">
43
+ <!-- Body -->
44
+ <path d="M60 10 C30 10 15 35 15 55 C15 75 30 95 45 100 L45 110 L55 110 L55 100 C55 100 60 102 65 100 L65 110 L75 110 L75 100 C90 95 105 75 105 55 C105 35 90 10 60 10Z" fill="url(#lobster-gradient)"/>
45
+ <!-- Left Claw -->
46
+ <path d="M20 45 C5 40 0 50 5 60 C10 70 20 65 25 55 C28 48 25 45 20 45Z" fill="url(#lobster-gradient)"/>
47
+ <!-- Right Claw -->
48
+ <path d="M100 45 C115 40 120 50 115 60 C110 70 100 65 95 55 C92 48 95 45 100 45Z" fill="url(#lobster-gradient)"/>
49
+ <!-- Antenna -->
50
+ <path d="M45 15 Q35 5 30 8" stroke="#ff8b84" stroke-width="3" stroke-linecap="round"/>
51
+ <path d="M75 15 Q85 5 90 8" stroke="#ff8b84" stroke-width="3" stroke-linecap="round"/>
52
+ <!-- Eyes -->
53
+ <circle cx="45" cy="35" r="6" fill="#030712"/>
54
+ <circle cx="75" cy="35" r="6" fill="#030712"/>
55
+ <circle cx="46" cy="34" r="2.5" fill="#22d3ee"/>
56
+ <circle cx="76" cy="34" r="2.5" fill="#22d3ee"/>
57
+ </g>
58
+ </svg>
package/src/app/page.tsx CHANGED
@@ -4,6 +4,8 @@ import { useEffect, useState, useCallback } from 'react'
4
4
  import { useAppStore } from '@/stores/use-app-store'
5
5
  import { initAudioContext } from '@/lib/tts'
6
6
  import { getStoredAccessKey, clearStoredAccessKey, api } from '@/lib/api-client'
7
+ import { connectWs, disconnectWs } from '@/lib/ws-client'
8
+ import { useWs } from '@/hooks/use-ws'
7
9
  import { AccessKeyGate } from '@/components/auth/access-key-gate'
8
10
  import { UserPicker } from '@/components/auth/user-picker'
9
11
  import { SetupWizard } from '@/components/auth/setup-wizard'
@@ -70,14 +72,17 @@ export default function Home() {
70
72
 
71
73
  useEffect(() => {
72
74
  if (!authenticated) return
75
+ const key = getStoredAccessKey()
76
+ if (key) connectWs(key)
73
77
  syncUserFromServer()
74
78
  loadNetworkInfo()
75
79
  loadSettings()
76
80
  loadSessions()
77
- const interval = setInterval(loadSessions, 5000)
78
- return () => clearInterval(interval)
81
+ return () => { disconnectWs() }
79
82
  }, [authenticated])
80
83
 
84
+ useWs('sessions', loadSessions, 5000)
85
+
81
86
  // Auto-select default agent's thread on load
82
87
  useEffect(() => {
83
88
  if (!authenticated || !currentUser) return
@@ -156,6 +161,7 @@ export default function Home() {
156
161
 
157
162
  useEffect(() => {
158
163
  const handler = () => {
164
+ disconnectWs()
159
165
  setAuthenticated(false)
160
166
  setAuthChecked(true)
161
167
  }
package/src/cli/index.js CHANGED
@@ -17,7 +17,6 @@ const COMMAND_GROUPS = [
17
17
  cmd('create', 'POST', '/agents', 'Create an agent', { expectsJsonBody: true }),
18
18
  cmd('update', 'PUT', '/agents/:id', 'Update an agent', { expectsJsonBody: true }),
19
19
  cmd('delete', 'DELETE', '/agents/:id', 'Delete an agent'),
20
- cmd('generate', 'POST', '/agents/generate', 'Generate agent definition from prompt', { expectsJsonBody: true }),
21
20
  cmd('thread', 'POST', '/agents/:id/thread', 'Get or create agent thread session'),
22
21
  ],
23
22
  },
@@ -123,14 +122,6 @@ const COMMAND_GROUPS = [
123
122
  cmd('serve', 'GET', '/files/serve', 'Serve a local file (use --query path=/abs/path)'),
124
123
  ],
125
124
  },
126
- {
127
- name: 'generate',
128
- description: 'AI generation endpoints',
129
- commands: [
130
- cmd('run', 'POST', '/generate', 'Generate schedule/task/skill/provider payload', { expectsJsonBody: true }),
131
- cmd('info', 'GET', '/generate/info', 'Get generation provider/model info'),
132
- ],
133
- },
134
125
  {
135
126
  name: 'ip',
136
127
  description: 'Get local IP/port metadata',
@@ -335,6 +326,7 @@ const COMMAND_GROUPS = [
335
326
  commands: [
336
327
  cmd('check-provider', 'POST', '/setup/check-provider', 'Validate provider credentials/endpoint', { expectsJsonBody: true }),
337
328
  cmd('doctor', 'GET', '/setup/doctor', 'Run local setup diagnostics'),
329
+ cmd('openclaw-device', 'GET', '/setup/openclaw-device', 'Show the local OpenClaw device ID'),
338
330
  ],
339
331
  },
340
332
  {
package/src/cli/index.ts CHANGED
@@ -901,6 +901,13 @@ export function buildProgram(): Command {
901
901
  })
902
902
  })
903
903
 
904
+ setup
905
+ .command('openclaw-device')
906
+ .description('Show the local OpenClaw device ID')
907
+ .action(async function () {
908
+ await runWithHandler(this as Command, (ctx) => apiRequest(ctx, 'GET', '/setup/openclaw-device'))
909
+ })
910
+
904
911
  const connectors = program.command('connectors').description('Manage connectors')
905
912
 
906
913
  connectors
@@ -1133,14 +1140,57 @@ export function buildProgram(): Command {
1133
1140
  console.log('Run: swarmclaw server --help')
1134
1141
  })
1135
1142
 
1143
+ program
1144
+ .command('update')
1145
+ .description('Pull the latest SwarmClaw release via git')
1146
+ .action(() => {
1147
+ console.log('The update command is handled directly by the swarmclaw binary.')
1148
+ console.log('Run: swarmclaw update --help')
1149
+ })
1150
+
1136
1151
  return program
1137
1152
  }
1138
1153
 
1154
+ async function checkForUpdate(baseUrl: string, accessKey: string): Promise<void> {
1155
+ try {
1156
+ const url = `${baseUrl}/api/version`
1157
+ const headers: Record<string, string> = {}
1158
+ if (accessKey) headers['X-Access-Key'] = accessKey
1159
+ const controller = new AbortController()
1160
+ const timeout = setTimeout(() => controller.abort(), 2000)
1161
+ const res = await fetch(url, { headers, signal: controller.signal })
1162
+ clearTimeout(timeout)
1163
+ if (!res.ok) return
1164
+ const data = (await res.json()) as { updateAvailable?: boolean; behindBy?: number }
1165
+ if (data.updateAvailable && data.behindBy) {
1166
+ process.stderr.write(`\n Update available (${data.behindBy} behind). Run: swarmclaw update\n`)
1167
+ }
1168
+ } catch {
1169
+ // Server unreachable or timed out — silently skip
1170
+ }
1171
+ }
1172
+
1139
1173
  export async function runCli(argv: string[] = process.argv.slice(2)): Promise<number> {
1140
1174
  const program = buildProgram()
1141
1175
  try {
1176
+ // Skip update hint for commands that don't talk to the server
1177
+ const skipHint = !argv.length || ['update', 'server', '--help', '-h'].includes(argv[0])
1178
+ const hintPromise = skipHint
1179
+ ? null
1180
+ : checkForUpdate(
1181
+ normalizeBaseUrl(process.env.SWARMCLAW_URL || process.env.SWARMCLAW_BASE_URL || 'http://localhost:3456'),
1182
+ (process.env.SWARMCLAW_ACCESS_KEY || '').trim(),
1183
+ )
1184
+
1142
1185
  await program.parseAsync(['node', 'swarmclaw', ...argv])
1143
- return (process.exitCode as number | undefined) ?? 0
1186
+ const code = (process.exitCode as number | undefined) ?? 0
1187
+
1188
+ // Wait briefly for the hint if the command succeeded
1189
+ if (hintPromise && code === 0) {
1190
+ await Promise.race([hintPromise, new Promise((r) => setTimeout(r, 2000))])
1191
+ }
1192
+
1193
+ return code
1144
1194
  } catch (err) {
1145
1195
  const msg = err instanceof Error ? err.message : String(err)
1146
1196
  console.error(msg)
package/src/cli/spec.js CHANGED
@@ -7,7 +7,6 @@ const COMMAND_GROUPS = {
7
7
  create: { description: 'Create an agent', method: 'POST', path: '/agents' },
8
8
  update: { description: 'Update an agent', method: 'PUT', path: '/agents/:id', params: ['id'] },
9
9
  delete: { description: 'Delete an agent', method: 'DELETE', path: '/agents/:id', params: ['id'] },
10
- generate: { description: 'Generate an agent definition', method: 'POST', path: '/agents/generate' },
11
10
  },
12
11
  },
13
12
  auth: {
@@ -97,13 +96,6 @@ const COMMAND_GROUPS = {
97
96
  },
98
97
  },
99
98
  },
100
- generate: {
101
- description: 'Structured AI generation helpers',
102
- commands: {
103
- create: { description: 'Generate object from prompt/type', method: 'POST', path: '/generate' },
104
- info: { description: 'Get active generator provider/model', method: 'GET', path: '/generate/info' },
105
- },
106
- },
107
99
  logs: {
108
100
  description: 'Application logs',
109
101
  commands: {
@@ -56,7 +56,7 @@ export function AgentCard({ agent, isDefault, onSetDefault }: Props) {
56
56
  await loadSessions()
57
57
  setMessages([])
58
58
  setCurrentSession(result.sessionId)
59
- setActiveView('sessions')
59
+ setActiveView('agents')
60
60
  }
61
61
  } catch (err) {
62
62
  console.error('Orchestrator run failed:', err)