@swarmclawai/swarmclaw 0.6.7 → 0.7.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 (203) hide show
  1. package/README.md +82 -39
  2. package/next.config.ts +31 -6
  3. package/package.json +3 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +1 -0
  5. package/src/app/api/agents/route.ts +19 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
  8. package/src/app/api/clawhub/install/route.ts +2 -2
  9. package/src/app/api/eval/run/route.ts +37 -0
  10. package/src/app/api/eval/scenarios/route.ts +24 -0
  11. package/src/app/api/eval/suite/route.ts +29 -0
  12. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  13. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  14. package/src/app/api/memory/graph/route.ts +46 -0
  15. package/src/app/api/memory/route.ts +36 -5
  16. package/src/app/api/notifications/route.ts +3 -0
  17. package/src/app/api/plugins/install/route.ts +57 -5
  18. package/src/app/api/plugins/marketplace/route.ts +73 -22
  19. package/src/app/api/plugins/route.ts +61 -1
  20. package/src/app/api/plugins/ui/route.ts +34 -0
  21. package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
  22. package/src/app/api/sessions/[id]/restore/route.ts +36 -0
  23. package/src/app/api/settings/route.ts +62 -0
  24. package/src/app/api/setup/doctor/route.ts +22 -5
  25. package/src/app/api/souls/[id]/route.ts +65 -0
  26. package/src/app/api/souls/route.ts +70 -0
  27. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  28. package/src/app/api/tasks/[id]/route.ts +16 -3
  29. package/src/app/api/tasks/route.ts +10 -2
  30. package/src/app/api/usage/route.ts +9 -2
  31. package/src/app/globals.css +27 -0
  32. package/src/app/page.tsx +10 -5
  33. package/src/cli/index.js +37 -0
  34. package/src/components/activity/activity-feed.tsx +9 -2
  35. package/src/components/agents/agent-avatar.tsx +5 -1
  36. package/src/components/agents/agent-card.tsx +55 -9
  37. package/src/components/agents/agent-sheet.tsx +112 -34
  38. package/src/components/agents/inspector-panel.tsx +1 -1
  39. package/src/components/agents/soul-library-picker.tsx +84 -13
  40. package/src/components/auth/access-key-gate.tsx +63 -54
  41. package/src/components/auth/user-picker.tsx +37 -32
  42. package/src/components/chat/activity-moment.tsx +2 -0
  43. package/src/components/chat/chat-area.tsx +11 -0
  44. package/src/components/chat/chat-header.tsx +69 -25
  45. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  46. package/src/components/chat/checkpoint-timeline.tsx +112 -0
  47. package/src/components/chat/code-block.tsx +3 -1
  48. package/src/components/chat/exec-approval-card.tsx +8 -1
  49. package/src/components/chat/message-bubble.tsx +164 -4
  50. package/src/components/chat/message-list.tsx +46 -4
  51. package/src/components/chat/session-approval-card.tsx +80 -0
  52. package/src/components/chat/session-debug-panel.tsx +106 -84
  53. package/src/components/chat/streaming-bubble.tsx +6 -5
  54. package/src/components/chat/task-approval-card.tsx +78 -0
  55. package/src/components/chat/thinking-indicator.tsx +48 -12
  56. package/src/components/chat/tool-call-bubble.tsx +3 -0
  57. package/src/components/chat/tool-request-banner.tsx +39 -20
  58. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  59. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  60. package/src/components/connectors/connector-list.tsx +33 -11
  61. package/src/components/connectors/connector-sheet.tsx +37 -7
  62. package/src/components/home/home-view.tsx +54 -24
  63. package/src/components/input/chat-input.tsx +22 -1
  64. package/src/components/knowledge/knowledge-list.tsx +17 -18
  65. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  66. package/src/components/layout/app-layout.tsx +87 -19
  67. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  68. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  69. package/src/components/memory/memory-browser.tsx +73 -45
  70. package/src/components/memory/memory-graph-view.tsx +203 -0
  71. package/src/components/memory/memory-list.tsx +20 -13
  72. package/src/components/plugins/plugin-list.tsx +214 -60
  73. package/src/components/plugins/plugin-sheet.tsx +119 -24
  74. package/src/components/projects/project-list.tsx +17 -9
  75. package/src/components/providers/provider-list.tsx +21 -6
  76. package/src/components/providers/provider-sheet.tsx +42 -25
  77. package/src/components/runs/run-list.tsx +17 -13
  78. package/src/components/schedules/schedule-card.tsx +10 -3
  79. package/src/components/schedules/schedule-list.tsx +2 -2
  80. package/src/components/schedules/schedule-sheet.tsx +28 -9
  81. package/src/components/secrets/secret-sheet.tsx +7 -2
  82. package/src/components/secrets/secrets-list.tsx +18 -5
  83. package/src/components/sessions/new-session-sheet.tsx +183 -376
  84. package/src/components/sessions/session-card.tsx +10 -2
  85. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  86. package/src/components/shared/command-palette.tsx +13 -5
  87. package/src/components/shared/empty-state.tsx +20 -8
  88. package/src/components/shared/hint-tip.tsx +31 -0
  89. package/src/components/shared/notification-center.tsx +134 -86
  90. package/src/components/shared/profile-sheet.tsx +4 -0
  91. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  92. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  93. package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
  94. package/src/components/skills/clawhub-browser.tsx +1 -0
  95. package/src/components/skills/skill-list.tsx +31 -12
  96. package/src/components/skills/skill-sheet.tsx +20 -7
  97. package/src/components/tasks/approvals-panel.tsx +224 -0
  98. package/src/components/tasks/task-board.tsx +20 -12
  99. package/src/components/tasks/task-card.tsx +21 -7
  100. package/src/components/tasks/task-column.tsx +4 -3
  101. package/src/components/tasks/task-list.tsx +1 -1
  102. package/src/components/tasks/task-sheet.tsx +130 -1
  103. package/src/components/ui/dialog.tsx +1 -0
  104. package/src/components/ui/sheet.tsx +1 -0
  105. package/src/components/usage/metrics-dashboard.tsx +72 -48
  106. package/src/components/wallets/wallet-panel.tsx +65 -41
  107. package/src/components/wallets/wallet-section.tsx +9 -3
  108. package/src/components/webhooks/webhook-list.tsx +21 -12
  109. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  110. package/src/lib/approval-display.test.ts +45 -0
  111. package/src/lib/approval-display.ts +62 -0
  112. package/src/lib/clipboard.ts +38 -0
  113. package/src/lib/memory.ts +8 -0
  114. package/src/lib/providers/claude-cli.ts +5 -3
  115. package/src/lib/providers/index.ts +67 -21
  116. package/src/lib/runtime-loop.ts +3 -2
  117. package/src/lib/server/approvals.ts +150 -0
  118. package/src/lib/server/chat-execution.ts +319 -74
  119. package/src/lib/server/chatroom-helpers.ts +63 -5
  120. package/src/lib/server/chatroom-orchestration.ts +74 -0
  121. package/src/lib/server/clawhub-client.ts +82 -6
  122. package/src/lib/server/connectors/manager.ts +27 -1
  123. package/src/lib/server/context-manager.ts +132 -50
  124. package/src/lib/server/cost.test.ts +73 -0
  125. package/src/lib/server/cost.ts +165 -34
  126. package/src/lib/server/daemon-state.ts +112 -1
  127. package/src/lib/server/data-dir.ts +18 -1
  128. package/src/lib/server/eval/runner.ts +126 -0
  129. package/src/lib/server/eval/scenarios.ts +218 -0
  130. package/src/lib/server/eval/scorer.ts +96 -0
  131. package/src/lib/server/eval/store.ts +37 -0
  132. package/src/lib/server/eval/types.ts +48 -0
  133. package/src/lib/server/execution-log.ts +12 -8
  134. package/src/lib/server/guardian.ts +34 -0
  135. package/src/lib/server/heartbeat-service.ts +53 -1
  136. package/src/lib/server/integrity-monitor.ts +208 -0
  137. package/src/lib/server/langgraph-checkpoint.ts +10 -0
  138. package/src/lib/server/link-understanding.ts +55 -0
  139. package/src/lib/server/llm-response-cache.test.ts +102 -0
  140. package/src/lib/server/llm-response-cache.ts +227 -0
  141. package/src/lib/server/main-agent-loop.ts +115 -16
  142. package/src/lib/server/main-session.ts +6 -3
  143. package/src/lib/server/mcp-conformance.test.ts +18 -0
  144. package/src/lib/server/mcp-conformance.ts +233 -0
  145. package/src/lib/server/memory-db.ts +193 -19
  146. package/src/lib/server/memory-retrieval.test.ts +56 -0
  147. package/src/lib/server/mmr.ts +73 -0
  148. package/src/lib/server/orchestrator-lg.ts +7 -1
  149. package/src/lib/server/orchestrator.ts +4 -3
  150. package/src/lib/server/plugins.ts +662 -132
  151. package/src/lib/server/process-manager.ts +18 -0
  152. package/src/lib/server/query-expansion.ts +57 -0
  153. package/src/lib/server/queue.ts +280 -11
  154. package/src/lib/server/runtime-settings.ts +9 -0
  155. package/src/lib/server/session-run-manager.test.ts +23 -0
  156. package/src/lib/server/session-run-manager.ts +32 -2
  157. package/src/lib/server/session-tools/canvas.ts +85 -50
  158. package/src/lib/server/session-tools/chatroom.ts +130 -127
  159. package/src/lib/server/session-tools/connector.ts +233 -454
  160. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  161. package/src/lib/server/session-tools/crud.ts +84 -7
  162. package/src/lib/server/session-tools/delegate.ts +351 -752
  163. package/src/lib/server/session-tools/discovery.ts +198 -0
  164. package/src/lib/server/session-tools/edit_file.ts +82 -0
  165. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  166. package/src/lib/server/session-tools/file.ts +257 -425
  167. package/src/lib/server/session-tools/git.ts +87 -47
  168. package/src/lib/server/session-tools/http.ts +95 -33
  169. package/src/lib/server/session-tools/index.ts +217 -138
  170. package/src/lib/server/session-tools/memory.ts +154 -239
  171. package/src/lib/server/session-tools/monitor.ts +126 -0
  172. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  173. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  174. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  175. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  176. package/src/lib/server/session-tools/platform.ts +86 -0
  177. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  178. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  179. package/src/lib/server/session-tools/sandbox.ts +175 -148
  180. package/src/lib/server/session-tools/schedule.ts +78 -0
  181. package/src/lib/server/session-tools/session-info.ts +104 -410
  182. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  183. package/src/lib/server/session-tools/shell.ts +171 -143
  184. package/src/lib/server/session-tools/subagent.ts +77 -77
  185. package/src/lib/server/session-tools/wallet.ts +182 -106
  186. package/src/lib/server/session-tools/web.ts +181 -327
  187. package/src/lib/server/storage.ts +36 -0
  188. package/src/lib/server/stream-agent-chat.ts +348 -242
  189. package/src/lib/server/task-quality-gate.test.ts +44 -0
  190. package/src/lib/server/task-quality-gate.ts +67 -0
  191. package/src/lib/server/task-validation.test.ts +78 -0
  192. package/src/lib/server/task-validation.ts +67 -2
  193. package/src/lib/server/tool-aliases.ts +68 -0
  194. package/src/lib/server/tool-capability-policy.ts +24 -5
  195. package/src/lib/server/tool-retry.ts +62 -0
  196. package/src/lib/server/transcript-repair.ts +72 -0
  197. package/src/lib/setup-defaults.ts +1 -0
  198. package/src/lib/tasks.ts +7 -1
  199. package/src/lib/tool-definitions.ts +24 -23
  200. package/src/lib/validation/schemas.ts +13 -0
  201. package/src/lib/view-routes.ts +2 -23
  202. package/src/stores/use-app-store.ts +23 -1
  203. package/src/types/index.ts +155 -10
@@ -0,0 +1,65 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadSouls, saveSouls, deleteSoul, logActivity } from '@/lib/server/storage'
3
+ import { SOUL_LIBRARY } from '@/lib/soul-library'
4
+ import { notify } from '@/lib/server/ws-hub'
5
+
6
+ export const dynamic = 'force-dynamic'
7
+
8
+ /** GET /api/souls/[id] */
9
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
10
+ const { id } = await params
11
+
12
+ // Check static library first
13
+ const staticSoul = SOUL_LIBRARY.find(s => s.id === id)
14
+ if (staticSoul) return NextResponse.json(staticSoul)
15
+
16
+ const souls = loadSouls()
17
+ if (!souls[id]) return NextResponse.json({ error: 'Soul not found' }, { status: 404 })
18
+ return NextResponse.json(souls[id])
19
+ }
20
+
21
+ /** PUT /api/souls/[id] — update custom soul */
22
+ export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
23
+ const { id } = await params
24
+ const body = await req.json()
25
+
26
+ // Can only update custom souls
27
+ const souls = loadSouls()
28
+ if (!souls[id]) {
29
+ return NextResponse.json({ error: 'Only custom souls can be modified via this endpoint' }, { status: 403 })
30
+ }
31
+
32
+ const updated = { ...souls[id], ...body, id, updatedAt: Date.now() }
33
+ souls[id] = updated
34
+ saveSouls(souls)
35
+
36
+ notify('souls')
37
+ return NextResponse.json(updated)
38
+ }
39
+
40
+ /** DELETE /api/souls/[id] — delete custom soul */
41
+ export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
42
+ const { id } = await params
43
+
44
+ // Only allow deleting custom ones
45
+ const souls = loadSouls()
46
+ if (!souls[id]) {
47
+ const isStatic = SOUL_LIBRARY.some(s => s.id === id)
48
+ if (isStatic) return NextResponse.json({ error: 'Cannot delete static library souls' }, { status: 403 })
49
+ return NextResponse.json({ error: 'Soul not found' }, { status: 404 })
50
+ }
51
+
52
+ const name = souls[id].name
53
+ deleteSoul(id)
54
+
55
+ logActivity({
56
+ entityType: 'soul',
57
+ entityId: id,
58
+ action: 'deleted',
59
+ actor: 'user',
60
+ summary: `Custom soul deleted: "${name}"`
61
+ })
62
+
63
+ notify('souls')
64
+ return NextResponse.json({ deleted: id })
65
+ }
@@ -0,0 +1,70 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { SOUL_LIBRARY, type SoulTemplate } from '@/lib/soul-library'
3
+ import { loadSouls, saveSouls, logActivity } from '@/lib/server/storage'
4
+ import { genId } from '@/lib/id'
5
+ import { notify } from '@/lib/server/ws-hub'
6
+
7
+ export const dynamic = 'force-dynamic'
8
+
9
+ /** GET /api/souls — returns merged list of static library and custom user souls */
10
+ export async function GET(req: Request) {
11
+ const customSouls = loadSouls()
12
+ const { searchParams } = new URL(req.url)
13
+ const query = searchParams.get('q')?.toLowerCase() || ''
14
+ const archetype = searchParams.get('archetype')
15
+
16
+ const merged: SoulTemplate[] = [
17
+ ...SOUL_LIBRARY,
18
+ ...Object.values(customSouls) as SoulTemplate[],
19
+ ]
20
+
21
+ let filtered = merged
22
+ if (archetype && archetype !== 'All') {
23
+ filtered = filtered.filter((s) => s.archetype === archetype)
24
+ }
25
+ if (query) {
26
+ filtered = filtered.filter(
27
+ (s) =>
28
+ s.name.toLowerCase().includes(query) ||
29
+ s.description.toLowerCase().includes(query) ||
30
+ s.tags.some((t) => t.toLowerCase().includes(query)) ||
31
+ s.soul.toLowerCase().includes(query),
32
+ )
33
+ }
34
+
35
+ return NextResponse.json(filtered)
36
+ }
37
+
38
+ /** POST /api/souls — create a custom soul */
39
+ export async function POST(req: Request) {
40
+ const body = await req.json()
41
+ if (!body.name || !body.soul) {
42
+ return NextResponse.json({ error: 'Name and soul content are required' }, { status: 400 })
43
+ }
44
+
45
+ const id = body.id || `custom-${genId()}`
46
+ const souls = loadSouls()
47
+
48
+ const newSoul: SoulTemplate = {
49
+ id,
50
+ name: body.name,
51
+ description: body.description || '',
52
+ soul: body.soul,
53
+ tags: Array.isArray(body.tags) ? body.tags : [],
54
+ archetype: body.archetype || 'Custom',
55
+ }
56
+
57
+ souls[id] = newSoul
58
+ saveSouls(souls)
59
+
60
+ logActivity({
61
+ entityType: 'soul',
62
+ entityId: id,
63
+ action: 'created',
64
+ actor: 'user',
65
+ summary: `Custom soul created: "${newSoul.name}"`
66
+ })
67
+
68
+ notify('souls')
69
+ return NextResponse.json(newSoul)
70
+ }
@@ -58,11 +58,12 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
58
58
  saveTasks(t2)
59
59
  notify('tasks')
60
60
  }
61
- } catch (err: any) {
62
- console.error(`[approve] Resume failed for task ${id}:`, err.message)
61
+ } catch (err: unknown) {
62
+ const errMsg = err instanceof Error ? err.message : String(err)
63
+ console.error(`[approve] Resume failed for task ${id}:`, errMsg)
63
64
  const t2 = loadTasks()
64
65
  if (t2[id]) {
65
- t2[id].error = err.message || String(err)
66
+ t2[id].error = errMsg
66
67
  t2[id].updatedAt = Date.now()
67
68
  saveTasks(t2)
68
69
  notify('tasks')
@@ -1,8 +1,8 @@
1
1
  import { genId } from '@/lib/id'
2
2
  import { NextResponse } from 'next/server'
3
- import { loadTasks, saveTasks, logActivity } from '@/lib/server/storage'
3
+ import { loadTasks, saveTasks, logActivity, loadSettings } from '@/lib/server/storage'
4
4
  import { notFound } from '@/lib/server/collection-helpers'
5
- import { disableSessionHeartbeat, enqueueTask, validateCompletedTasksQueue } from '@/lib/server/queue'
5
+ import { disableSessionHeartbeat, enqueueTask, recoverStalledRunningTasks, validateCompletedTasksQueue } from '@/lib/server/queue'
6
6
  import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
7
7
  import { formatValidationFailure, validateTaskCompletion } from '@/lib/server/task-validation'
8
8
  import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
@@ -11,10 +11,13 @@ import { createNotification } from '@/lib/server/create-notification'
11
11
  import { enqueueSystemEvent } from '@/lib/server/system-events'
12
12
  import { requestHeartbeatNow } from '@/lib/server/heartbeat-wake'
13
13
  import { validateDag, cascadeUnblock } from '@/lib/server/dag-validation'
14
+ import { getPluginManager } from '@/lib/server/plugins'
15
+ import { normalizeTaskQualityGate } from '@/lib/server/task-quality-gate'
14
16
 
15
17
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
16
18
  // Keep completed queue integrity even if daemon is not running.
17
19
  validateCompletedTasksQueue()
20
+ recoverStalledRunningTasks()
18
21
 
19
22
  const { id } = await params
20
23
  const tasks = loadTasks()
@@ -25,6 +28,7 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
25
28
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
26
29
  const { id } = await params
27
30
  const body = await req.json()
31
+ const settings = loadSettings()
28
32
  const tasks = loadTasks()
29
33
  if (!tasks[id]) return notFound()
30
34
 
@@ -47,6 +51,11 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
47
51
  tasks[id].comments.push(body.appendComment)
48
52
  tasks[id].updatedAt = Date.now()
49
53
  } else {
54
+ if (Object.prototype.hasOwnProperty.call(body, 'qualityGate')) {
55
+ body.qualityGate = body.qualityGate
56
+ ? normalizeTaskQualityGate(body.qualityGate, settings)
57
+ : null
58
+ }
50
59
  Object.assign(tasks[id], body, { updatedAt: Date.now() })
51
60
  // Explicitly clear nullable fields when sent as null (Object.assign copies null but not undefined)
52
61
  if (body.projectId === null) delete tasks[id].projectId
@@ -62,7 +71,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
62
71
  if (tasks[id].status === 'completed') {
63
72
  const report = ensureTaskCompletionReport(tasks[id])
64
73
  if (report?.relativePath) tasks[id].completionReportPath = report.relativePath
65
- const validation = validateTaskCompletion(tasks[id], { report })
74
+ const validation = validateTaskCompletion(tasks[id], { report, settings })
66
75
  tasks[id].validation = validation
67
76
  if (validation.ok) {
68
77
  tasks[id].completedAt = tasks[id].completedAt || Date.now()
@@ -100,6 +109,10 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
100
109
  entityType: 'task',
101
110
  entityId: id,
102
111
  })
112
+
113
+ if (tasks[id].status === 'completed') {
114
+ getPluginManager().runHook('onTaskComplete', { taskId: id, result: tasks[id].result })
115
+ }
103
116
 
104
117
  // Enqueue system event + heartbeat wake
105
118
  if (tasks[id].sessionId) {
@@ -3,7 +3,7 @@ import { genId } from '@/lib/id'
3
3
  import { loadTasks, saveTasks, loadSettings, loadAgents, logActivity } from '@/lib/server/storage'
4
4
  import { TaskCreateSchema, formatZodError } from '@/lib/validation/schemas'
5
5
  import { z } from 'zod'
6
- import { enqueueTask, validateCompletedTasksQueue } from '@/lib/server/queue'
6
+ import { enqueueTask, recoverStalledRunningTasks, validateCompletedTasksQueue } from '@/lib/server/queue'
7
7
  import { ensureTaskCompletionReport } from '@/lib/server/task-reports'
8
8
  import { formatValidationFailure, validateTaskCompletion } from '@/lib/server/task-validation'
9
9
  import { pushMainLoopEventToMainSessions } from '@/lib/server/main-agent-loop'
@@ -11,10 +11,13 @@ import { notify } from '@/lib/server/ws-hub'
11
11
  import { computeTaskFingerprint, findDuplicateTask } from '@/lib/task-dedupe'
12
12
  import { resolveTaskAgentFromDescription } from '@/lib/server/task-mention'
13
13
  import { validateDag } from '@/lib/server/dag-validation'
14
+ import { getPluginManager } from '@/lib/server/plugins'
15
+ import { normalizeTaskQualityGate } from '@/lib/server/task-quality-gate'
14
16
 
15
17
  export async function GET(req: Request) {
16
18
  // Keep completed queue integrity even if daemon is not running.
17
19
  validateCompletedTasksQueue()
20
+ recoverStalledRunningTasks()
18
21
 
19
22
  const { searchParams } = new URL(req.url)
20
23
  const includeArchived = searchParams.get('includeArchived') === 'true'
@@ -68,6 +71,9 @@ export async function POST(req: Request) {
68
71
  const now = Date.now()
69
72
  const tasks = loadTasks()
70
73
  const settings = loadSettings()
74
+ const normalizedQualityGate = body.qualityGate
75
+ ? normalizeTaskQualityGate(body.qualityGate, settings)
76
+ : null
71
77
  const maxAttempts = Number.isFinite(Number(body.maxAttempts))
72
78
  ? Math.max(1, Math.min(20, Math.trunc(Number(body.maxAttempts))))
73
79
  : Math.max(1, Math.min(20, Math.trunc(Number(settings.defaultTaskMaxAttempts ?? 3))))
@@ -146,6 +152,7 @@ export async function POST(req: Request) {
146
152
  customFields: body.customFields && typeof body.customFields === 'object' ? body.customFields : undefined,
147
153
  priority: ['low', 'medium', 'high', 'critical'].includes(body.priority) ? body.priority : undefined,
148
154
  fingerprint: computeTaskFingerprint(body.title || 'Untitled Task', body.agentId || ''),
155
+ qualityGate: normalizedQualityGate,
149
156
  }
150
157
 
151
158
  // Dedup: if a non-terminal task with same fingerprint exists, return it
@@ -157,11 +164,12 @@ export async function POST(req: Request) {
157
164
  if (tasks[id].status === 'completed') {
158
165
  const report = ensureTaskCompletionReport(tasks[id])
159
166
  if (report?.relativePath) tasks[id].completionReportPath = report.relativePath
160
- const validation = validateTaskCompletion(tasks[id], { report })
167
+ const validation = validateTaskCompletion(tasks[id], { report, settings })
161
168
  tasks[id].validation = validation
162
169
  if (validation.ok) {
163
170
  tasks[id].completedAt = Date.now()
164
171
  tasks[id].error = null
172
+ getPluginManager().runHook('onTaskComplete', { taskId: id, result: tasks[id].result })
165
173
  } else {
166
174
  tasks[id].status = 'failed'
167
175
  tasks[id].completedAt = null
@@ -94,12 +94,14 @@ export async function GET(req: Request) {
94
94
  errorCount: number
95
95
  lastUsed: number
96
96
  models: Set<string>
97
+ totalDurationMs: number
98
+ latencyCount: number
97
99
  }> = {}
98
100
 
99
101
  for (const r of records) {
100
102
  const prov = r.provider || 'unknown'
101
103
  if (!healthAccum[prov]) {
102
- healthAccum[prov] = { totalRequests: 0, successCount: 0, errorCount: 0, lastUsed: 0, models: new Set() }
104
+ healthAccum[prov] = { totalRequests: 0, successCount: 0, errorCount: 0, lastUsed: 0, models: new Set(), totalDurationMs: 0, latencyCount: 0 }
103
105
  }
104
106
  const h = healthAccum[prov]
105
107
  h.totalRequests += 1
@@ -107,6 +109,11 @@ export async function GET(req: Request) {
107
109
  h.successCount += 1
108
110
  if ((r.timestamp || 0) > h.lastUsed) h.lastUsed = r.timestamp || 0
109
111
  if (r.model) h.models.add(r.model)
112
+
113
+ if (typeof r.durationMs === 'number' && r.durationMs > 0) {
114
+ h.totalDurationMs += r.durationMs
115
+ h.latencyCount += 1
116
+ }
110
117
  }
111
118
 
112
119
  const providerHealth: Record<string, {
@@ -125,7 +132,7 @@ export async function GET(req: Request) {
125
132
  successCount: h.successCount,
126
133
  errorCount: h.errorCount,
127
134
  errorRate: h.totalRequests > 0 ? h.errorCount / h.totalRequests : 0,
128
- avgLatencyMs: 0, // UsageRecord does not track latency
135
+ avgLatencyMs: h.latencyCount > 0 ? h.totalDurationMs / h.latencyCount : 0,
129
136
  lastUsed: h.lastUsed,
130
137
  models: Array.from(h.models),
131
138
  }
@@ -171,6 +171,12 @@ textarea::-webkit-scrollbar { width: 0; }
171
171
  textarea:hover { scrollbar-width: thin; }
172
172
  textarea:hover::-webkit-scrollbar { width: 6px; }
173
173
 
174
+ /* Improve scroll behavior on iOS/iPadOS nested panes */
175
+ .overflow-y-auto,
176
+ .overflow-auto {
177
+ -webkit-overflow-scrolling: touch;
178
+ }
179
+
174
180
  /* Selection */
175
181
  ::selection { background: rgba(99,102,241,0.3); }
176
182
 
@@ -250,6 +256,23 @@ textarea:hover::-webkit-scrollbar { width: 6px; }
250
256
  0% { background-position: -200% center; }
251
257
  100% { background-position: 200% center; }
252
258
  }
259
+ @keyframes shimmer-bar {
260
+ 0% { transform: translateX(-100%); }
261
+ 100% { transform: translateX(100%); }
262
+ }
263
+ @keyframes spring-in {
264
+ 0% { transform: scale(0.9) translateY(10px); opacity: 0; }
265
+ 70% { transform: scale(1.02) translateY(-2px); opacity: 1; }
266
+ 100% { transform: scale(1) translateY(0); opacity: 1; }
267
+ }
268
+ @keyframes pulse-subtle {
269
+ 0%, 100% { opacity: 1; transform: scale(1); }
270
+ 50% { opacity: 0.8; transform: scale(1.02); }
271
+ }
272
+ @keyframes glow-line {
273
+ 0% { left: -100%; }
274
+ 100% { left: 100%; }
275
+ }
253
276
  @keyframes gradient-drift {
254
277
  0% { background-position: 0% 50%; }
255
278
  50% { background-position: 100% 50%; }
@@ -259,6 +282,10 @@ textarea:hover::-webkit-scrollbar { width: 6px; }
259
282
  from { opacity: 0; transform: translateY(10px); }
260
283
  to { opacity: 1; transform: translateY(0); }
261
284
  }
285
+ @keyframes fade-up {
286
+ from { opacity: 0; transform: translateY(10px); }
287
+ to { opacity: 1; transform: translateY(0); }
288
+ }
262
289
 
263
290
  /* Heartbeat float animation */
264
291
  @keyframes heartbeat-float {
package/src/app/page.tsx CHANGED
@@ -92,14 +92,14 @@ function FullScreenLoader() {
92
92
  background: 'linear-gradient(135deg, rgba(255,255,255,0.6), rgba(129, 140, 248, 0.8))',
93
93
  WebkitBackgroundClip: 'text',
94
94
  WebkitTextFillColor: 'transparent',
95
- animation: 'sc-text-fade 2s ease-in-out infinite alternate',
95
+ animation: 'sc-text-fade 2s ease-in-out infinite alternate, fade-up 0.6s var(--ease-spring) 0.2s both',
96
96
  }}
97
97
  >
98
98
  SwarmClaw
99
99
  </div>
100
100
 
101
101
  {/* Loading bar */}
102
- <div className="mt-4 w-[100px] h-[2px] rounded-full bg-white/[0.06] overflow-hidden">
102
+ <div className="mt-4 w-[100px] h-[2px] rounded-full bg-white/[0.06] overflow-hidden" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.3s both' }}>
103
103
  <div
104
104
  className="h-full rounded-full bg-accent-bright/60"
105
105
  style={{ animation: 'sc-progress 1.5s ease-in-out infinite' }}
@@ -150,7 +150,10 @@ export default function Home() {
150
150
 
151
151
  const [authChecked, setAuthChecked] = useState(false)
152
152
  const [authenticated, setAuthenticated] = useState(false)
153
- const [setupDone, setSetupDone] = useState<boolean | null>(null)
153
+ const [setupDone, setSetupDone] = useState<boolean | null>(() => {
154
+ if (typeof window !== 'undefined' && localStorage.getItem('sc_setup_done') === '1') return true
155
+ return null
156
+ })
154
157
 
155
158
  const checkAuth = useCallback(async () => {
156
159
  const key = getStoredAccessKey()
@@ -252,7 +255,9 @@ export default function Home() {
252
255
  ])
253
256
  if (cancelled) return
254
257
  const hasCreds = Object.keys(creds).length > 0
255
- setSetupDone(settings.setupCompleted === true || hasCreds)
258
+ const done = settings.setupCompleted === true || hasCreds
259
+ if (done) localStorage.setItem('sc_setup_done', '1')
260
+ setSetupDone(done)
256
261
  } catch {
257
262
  if (!cancelled) setSetupDone(true) // on error, skip wizard
258
263
  }
@@ -285,6 +290,6 @@ export default function Home() {
285
290
  if (!authenticated) return <AccessKeyGate onAuthenticated={() => setAuthenticated(true)} />
286
291
  if (!currentUser) return <UserPicker />
287
292
  if (setupDone === null || !agentReady) return <FullScreenLoader />
288
- if (!setupDone) return <SetupWizard onComplete={() => setSetupDone(true)} />
293
+ if (!setupDone) return <SetupWizard onComplete={() => { localStorage.setItem('sc_setup_done', '1'); setSetupDone(true) }} />
289
294
  return <AppLayout />
290
295
  }
package/src/cli/index.js CHANGED
@@ -42,6 +42,14 @@ const COMMAND_GROUPS = [
42
42
  }),
43
43
  ],
44
44
  },
45
+ {
46
+ name: 'approvals',
47
+ description: 'Manage runtime approvals',
48
+ commands: [
49
+ cmd('list', 'GET', '/approvals', 'List pending approvals'),
50
+ cmd('resolve', 'POST', '/approvals', 'Approve/reject a pending approval', { expectsJsonBody: true }),
51
+ ],
52
+ },
45
53
  {
46
54
  name: 'claude-skills',
47
55
  description: 'Read local Claude skills directory metadata',
@@ -160,6 +168,16 @@ const COMMAND_GROUPS = [
160
168
  cmd('delete', 'DELETE', '/documents/:id', 'Delete document'),
161
169
  ],
162
170
  },
171
+ {
172
+ name: 'eval',
173
+ description: 'Run agent evaluation scenarios',
174
+ commands: [
175
+ cmd('scenarios', 'GET', '/eval/scenarios', 'List available eval scenarios'),
176
+ cmd('status', 'GET', '/eval/run', 'Get eval run status'),
177
+ cmd('run', 'POST', '/eval/run', 'Run an eval scenario against an agent', { expectsJsonBody: true }),
178
+ cmd('suite', 'POST', '/eval/suite', 'Run a full eval suite against an agent', { expectsJsonBody: true }),
179
+ ],
180
+ },
163
181
  {
164
182
  name: 'files',
165
183
  description: 'Serve and manage local files',
@@ -209,6 +227,7 @@ const COMMAND_GROUPS = [
209
227
  cmd('delete', 'DELETE', '/memory/:id', 'Delete memory entry'),
210
228
  cmd('maintenance', 'GET', '/memory/maintenance', 'Analyze memory dedupe/prune candidates'),
211
229
  cmd('maintenance-run', 'POST', '/memory/maintenance', 'Run memory dedupe/prune maintenance', { expectsJsonBody: true }),
230
+ cmd('graph', 'GET', '/memory/graph', 'Get memory graph (nodes and links) for visualization'),
212
231
  ],
213
232
  },
214
233
  {
@@ -240,6 +259,8 @@ const COMMAND_GROUPS = [
240
259
  cmd('delete', 'DELETE', '/mcp-servers/:id', 'Delete MCP server'),
241
260
  cmd('test', 'POST', '/mcp-servers/:id/test', 'Test MCP server connection'),
242
261
  cmd('tools', 'GET', '/mcp-servers/:id/tools', 'List tools available on an MCP server'),
262
+ cmd('conformance', 'POST', '/mcp-servers/:id/conformance', 'Run MCP conformance checks for a server', { expectsJsonBody: true }),
263
+ cmd('invoke', 'POST', '/mcp-servers/:id/invoke', 'Invoke an MCP tool on a server', { expectsJsonBody: true }),
243
264
  ],
244
265
  },
245
266
  {
@@ -320,8 +341,11 @@ const COMMAND_GROUPS = [
320
341
  commands: [
321
342
  cmd('list', 'GET', '/plugins', 'List installed plugins'),
322
343
  cmd('set', 'POST', '/plugins', 'Enable/disable plugin', { expectsJsonBody: true }),
344
+ cmd('delete', 'DELETE', '/plugins', 'Delete an external plugin (use --query filename=plugin.js)'),
345
+ cmd('update', 'PATCH', '/plugins', 'Update a plugin (use --query id=plugin.js or --query all=true)'),
323
346
  cmd('install', 'POST', '/plugins/install', 'Install plugin from URL', { expectsJsonBody: true }),
324
347
  cmd('marketplace', 'GET', '/plugins/marketplace', 'Get marketplace catalog'),
348
+ cmd('ui', 'GET', '/plugins/ui', 'List plugin UI extensions (use --query type=sidebar|header|chat_actions|connectors)'),
325
349
  ],
326
350
  },
327
351
  {
@@ -428,6 +452,8 @@ const COMMAND_GROUPS = [
428
452
  expectsJsonBody: true,
429
453
  defaultBody: { action: 'status' },
430
454
  }),
455
+ cmd('checkpoints', 'GET', '/sessions/:id/checkpoints', 'List checkpoint history for a session'),
456
+ cmd('restore', 'POST', '/sessions/:id/restore', 'Restore session to a previous checkpoint', { expectsJsonBody: true }),
431
457
  ],
432
458
  },
433
459
  {
@@ -459,6 +485,17 @@ const COMMAND_GROUPS = [
459
485
  cmd('import', 'POST', '/skills/import', 'Import skill from URL', { expectsJsonBody: true }),
460
486
  ],
461
487
  },
488
+ {
489
+ name: 'souls',
490
+ description: 'Browse and manage soul library templates',
491
+ commands: [
492
+ cmd('list', 'GET', '/souls', 'List soul templates'),
493
+ cmd('get', 'GET', '/souls/:id', 'Get soul template by id'),
494
+ cmd('create', 'POST', '/souls', 'Create custom soul template', { expectsJsonBody: true }),
495
+ cmd('update', 'PUT', '/souls/:id', 'Update soul template', { expectsJsonBody: true }),
496
+ cmd('delete', 'DELETE', '/souls/:id', 'Delete soul template'),
497
+ ],
498
+ },
462
499
  {
463
500
  name: 'tasks',
464
501
  description: 'Manage task board items',
@@ -65,8 +65,15 @@ export function ActivityFeed() {
65
65
  <div className="text-center text-text-3 text-[14px] mt-16">No activity yet</div>
66
66
  ) : (
67
67
  <div className="space-y-1">
68
- {entries.map((entry: ActivityEntry) => (
69
- <div key={entry.id} className="flex items-start gap-3 py-3 border-b border-white/[0.04]">
68
+ {entries.map((entry: ActivityEntry, idx: number) => (
69
+ <div
70
+ key={entry.id}
71
+ className="flex items-start gap-3 py-3 border-b border-white/[0.04]"
72
+ style={{
73
+ animation: 'fade-up 0.5s var(--ease-spring) both',
74
+ animationDelay: `${Math.min(idx * 0.03, 0.5)}s`
75
+ }}
76
+ >
70
77
  <div className="w-8 h-8 rounded-[8px] bg-surface-2 flex items-center justify-center text-[12px] font-700 text-text-3 shrink-0">
71
78
  {ENTITY_ICONS[entry.entityType] || '?'}
72
79
  </div>
@@ -38,7 +38,11 @@ export function AgentAvatar({ seed, avatarUrl, name, size = 32, className = '',
38
38
  const dot = status && status !== 'idle' ? (
39
39
  <span
40
40
  className={`absolute -bottom-0.5 -right-0.5 rounded-full ${STATUS_COLORS[status]} ring-2 ring-[#0f0f1a]`}
41
- style={{ width: dotSize, height: dotSize }}
41
+ style={{
42
+ width: dotSize,
43
+ height: dotSize,
44
+ animation: status === 'online' ? 'pulse-subtle 2s ease-in-out infinite' : undefined
45
+ }}
42
46
  title={status === 'busy' ? 'Busy' : 'Online'}
43
47
  />
44
48
  ) : null
@@ -45,6 +45,22 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
45
45
  const approvals = useApprovalStore((s) => s.approvals)
46
46
  const pendingApprovalCount = Object.values(approvals).filter((a) => a.agentId === agent.id).length
47
47
  const [heartbeatPulse, setHeartbeatPulse] = useState(false)
48
+ const monthlyBudget = typeof agent.monthlyBudget === 'number' && agent.monthlyBudget > 0
49
+ ? agent.monthlyBudget
50
+ : null
51
+ const hasMonthlyBudget = monthlyBudget !== null
52
+ const spendWindows = [
53
+ {
54
+ key: '1h',
55
+ spend: agent.hourlySpend ?? 0,
56
+ budget: typeof agent.hourlyBudget === 'number' && agent.hourlyBudget > 0 ? agent.hourlyBudget : null,
57
+ },
58
+ {
59
+ key: '24h',
60
+ spend: agent.dailySpend ?? 0,
61
+ budget: typeof agent.dailyBudget === 'number' && agent.dailyBudget > 0 ? agent.dailyBudget : null,
62
+ },
63
+ ].filter((entry) => entry.budget !== null)
48
64
  useWs(`heartbeat:agent:${agent.id}`, () => {
49
65
  setHeartbeatPulse(true)
50
66
  setTimeout(() => setHeartbeatPulse(false), 1500)
@@ -226,28 +242,58 @@ export function AgentCard({ agent, isDefault, isRunning, isOnline, isSelected, o
226
242
  <span>Cost: ${agent.totalCost.toFixed(2)}</span>
227
243
  )}
228
244
  </div>
229
- {typeof agent.monthlyBudget === 'number' && agent.monthlyBudget > 0 && (
245
+ {hasMonthlyBudget && (
230
246
  <div className="mt-2">
231
247
  <div className="flex items-center justify-between text-[10px] text-text-3/60 mb-1">
232
- <span>${(agent.monthlySpend ?? 0).toFixed(2)} / ${agent.monthlyBudget.toFixed(2)}</span>
233
- <span className={`font-600 ${(agent.monthlySpend ?? 0) >= agent.monthlyBudget ? 'text-red-400' : 'text-text-3/50'}`}>
248
+ <span>${(agent.monthlySpend ?? 0).toFixed(2)} / ${monthlyBudget.toFixed(2)}</span>
249
+ <span className={`font-600 ${(agent.monthlySpend ?? 0) >= monthlyBudget ? 'text-red-400' : 'text-text-3/50'}`}>
234
250
  {agent.budgetAction === 'block' ? 'hard cap' : 'soft cap'}
235
251
  </span>
236
252
  </div>
237
- <div className="h-1 rounded-full bg-white/[0.06] overflow-hidden">
253
+ <div className="h-1 rounded-full bg-white/[0.06] overflow-hidden relative">
238
254
  <div
239
- className={`h-full rounded-full transition-all duration-300 ${
240
- (agent.monthlySpend ?? 0) >= agent.monthlyBudget
255
+ className={`h-full rounded-full transition-all duration-300 relative ${
256
+ (agent.monthlySpend ?? 0) >= monthlyBudget
241
257
  ? 'bg-red-400'
242
- : (agent.monthlySpend ?? 0) >= agent.monthlyBudget * 0.8
258
+ : (agent.monthlySpend ?? 0) >= monthlyBudget * 0.8
243
259
  ? 'bg-amber-400'
244
260
  : 'bg-accent'
245
261
  }`}
246
- style={{ width: `${Math.min(100, ((agent.monthlySpend ?? 0) / agent.monthlyBudget) * 100)}%` }}
247
- />
262
+ style={{ width: `${Math.min(100, ((agent.monthlySpend ?? 0) / monthlyBudget) * 100)}%` }}
263
+ >
264
+ {/* Shimmer overlay for active feel */}
265
+ <div
266
+ className="absolute inset-0 w-full h-full bg-gradient-to-r from-transparent via-white/20 to-transparent"
267
+ style={{ animation: 'shimmer-bar 2s linear infinite' }}
268
+ />
269
+ </div>
248
270
  </div>
249
271
  </div>
250
272
  )}
273
+ {spendWindows.length > 0 && (
274
+ <div className="mt-2 flex flex-wrap gap-1.5">
275
+ {spendWindows.map((entry) => {
276
+ const budget = entry.budget as number
277
+ const ratio = budget > 0 ? (entry.spend / budget) : 0
278
+ const overCap = ratio >= 1
279
+ const nearCap = !overCap && ratio >= 0.8
280
+ return (
281
+ <span
282
+ key={entry.key}
283
+ className={`text-[10px] px-2 py-0.5 rounded-[6px] border ${
284
+ overCap
285
+ ? 'text-red-400 border-red-400/25 bg-red-400/[0.06]'
286
+ : nearCap
287
+ ? 'text-amber-400 border-amber-400/20 bg-amber-400/[0.06]'
288
+ : 'text-text-3/70 border-white/[0.08] bg-white/[0.03]'
289
+ }`}
290
+ >
291
+ {entry.key}: ${entry.spend.toFixed(2)} / ${budget.toFixed(2)}
292
+ </span>
293
+ )
294
+ })}
295
+ </div>
296
+ )}
251
297
  </div>
252
298
 
253
299
  <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>