@swarmclawai/swarmclaw 1.3.6 → 1.4.2

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 (126) hide show
  1. package/README.md +16 -52
  2. package/next.config.ts +9 -4
  3. package/package.json +18 -10
  4. package/scripts/build-bootstrap-env.mjs +24 -0
  5. package/scripts/run-next-build.mjs +74 -0
  6. package/scripts/run-next-typegen.mjs +61 -0
  7. package/src/app/api/.well-known/agent-card/route.ts +46 -0
  8. package/src/app/api/a2a/route.ts +56 -0
  9. package/src/app/api/a2a/tasks/[taskId]/status/route.ts +49 -0
  10. package/src/app/api/approvals/route.test.ts +29 -3
  11. package/src/app/api/approvals/route.ts +13 -7
  12. package/src/app/api/chats/[id]/chat/route.test.ts +64 -0
  13. package/src/app/api/chats/[id]/chat/route.ts +24 -8
  14. package/src/app/api/chats/[id]/deploy/route.ts +2 -2
  15. package/src/app/api/chats/chat-route.test.ts +68 -0
  16. package/src/app/api/connectors/[id]/doctor/route.test.ts +97 -0
  17. package/src/app/api/connectors/[id]/doctor/route.ts +26 -1
  18. package/src/app/api/connectors/connector-doctor-route.test.ts +1 -0
  19. package/src/app/api/logs/route.test.ts +61 -0
  20. package/src/app/api/logs/route.ts +35 -0
  21. package/src/app/api/openclaw/sync/route.ts +1 -1
  22. package/src/app/api/swarmfeed/channels/route.ts +14 -0
  23. package/src/app/api/swarmfeed/posts/route.ts +60 -0
  24. package/src/app/api/swarmfeed/route.ts +37 -0
  25. package/src/app/api/tts/route.test.ts +82 -0
  26. package/src/app/api/tts/route.ts +13 -6
  27. package/src/app/api/tts/stream/route.ts +12 -5
  28. package/src/app/error.tsx +32 -0
  29. package/src/app/global-error.tsx +33 -0
  30. package/src/app/protocols/builder/[templateId]/page.tsx +93 -0
  31. package/src/app/protocols/page.tsx +16 -7
  32. package/src/app/swarmfeed/page.tsx +7 -0
  33. package/src/cli/index.js +22 -0
  34. package/src/cli/spec.js +9 -0
  35. package/src/components/agents/agent-avatar.tsx +2 -5
  36. package/src/components/agents/agent-sheet.tsx +10 -0
  37. package/src/components/auth/access-key-gate.tsx +25 -0
  38. package/src/components/layout/error-boundary.tsx +12 -30
  39. package/src/components/layout/error-fallback.tsx +61 -0
  40. package/src/components/layout/sidebar-rail.tsx +52 -0
  41. package/src/components/protocols/builder/edge-editor.tsx +43 -0
  42. package/src/components/protocols/builder/edge-types/branch-edge.tsx +33 -0
  43. package/src/components/protocols/builder/edge-types/default-edge.tsx +18 -0
  44. package/src/components/protocols/builder/edge-types/index.ts +3 -0
  45. package/src/components/protocols/builder/edge-types/loop-edge.tsx +19 -0
  46. package/src/components/protocols/builder/node-inspector.tsx +227 -0
  47. package/src/components/protocols/builder/node-palette.tsx +97 -0
  48. package/src/components/protocols/builder/node-types/branch-node.tsx +34 -0
  49. package/src/components/protocols/builder/node-types/complete-node.tsx +17 -0
  50. package/src/components/protocols/builder/node-types/for-each-node.tsx +21 -0
  51. package/src/components/protocols/builder/node-types/index.ts +9 -0
  52. package/src/components/protocols/builder/node-types/join-node.tsx +18 -0
  53. package/src/components/protocols/builder/node-types/loop-node.tsx +22 -0
  54. package/src/components/protocols/builder/node-types/parallel-node.tsx +31 -0
  55. package/src/components/protocols/builder/node-types/phase-node.tsx +52 -0
  56. package/src/components/protocols/builder/node-types/subflow-node.tsx +23 -0
  57. package/src/components/protocols/builder/node-types/swarm-node.tsx +26 -0
  58. package/src/components/protocols/builder/protocol-builder-canvas.tsx +184 -0
  59. package/src/components/protocols/builder/run-overlay.tsx +29 -0
  60. package/src/components/protocols/builder/template-gallery.tsx +53 -0
  61. package/src/components/protocols/builder/validation-panel.tsx +57 -0
  62. package/src/components/skills/skills-workspace.tsx +1 -9
  63. package/src/features/protocols/builder/hooks/index.ts +2 -0
  64. package/src/features/protocols/builder/hooks/use-canvas-validation.ts +14 -0
  65. package/src/features/protocols/builder/hooks/use-run-overlay.ts +39 -0
  66. package/src/features/protocols/builder/hooks/use-template-sync.ts +45 -0
  67. package/src/features/protocols/builder/protocol-builder-store.ts +233 -0
  68. package/src/features/protocols/builder/utils/node-position-layout.ts +41 -0
  69. package/src/features/protocols/builder/utils/nodes-to-template.test.ts +179 -0
  70. package/src/features/protocols/builder/utils/nodes-to-template.ts +49 -0
  71. package/src/features/protocols/builder/utils/template-to-nodes.test.ts +314 -0
  72. package/src/features/protocols/builder/utils/template-to-nodes.ts +169 -0
  73. package/src/features/protocols/builder/validators/dag-validator.test.ts +150 -0
  74. package/src/features/protocols/builder/validators/dag-validator.ts +119 -0
  75. package/src/features/swarmfeed/agent-social-settings.tsx +277 -0
  76. package/src/features/swarmfeed/compose-post.tsx +139 -0
  77. package/src/features/swarmfeed/feed-page.tsx +136 -0
  78. package/src/features/swarmfeed/post-card.tsx +114 -0
  79. package/src/features/swarmfeed/queries.ts +28 -0
  80. package/src/lib/a2a/agent-card.ts +61 -0
  81. package/src/lib/a2a/auth.ts +54 -0
  82. package/src/lib/a2a/client.ts +133 -0
  83. package/src/lib/a2a/discovery.ts +116 -0
  84. package/src/lib/a2a/handlers.ts +176 -0
  85. package/src/lib/a2a/json-rpc-router.ts +38 -0
  86. package/src/lib/a2a/types.ts +95 -0
  87. package/src/lib/app/navigation.ts +1 -0
  88. package/src/lib/app/report-client-error.ts +52 -0
  89. package/src/lib/app/view-constants.ts +9 -1
  90. package/src/lib/providers/anthropic.ts +119 -107
  91. package/src/lib/providers/ollama.ts +34 -14
  92. package/src/lib/providers/openai.ts +154 -142
  93. package/src/lib/providers/openclaw.ts +3 -3
  94. package/src/lib/server/agents/main-agent-loop.test.ts +94 -0
  95. package/src/lib/server/agents/main-agent-loop.ts +377 -41
  96. package/src/lib/server/chat-execution/chat-execution.ts +12 -7
  97. package/src/lib/server/chat-execution/chat-turn-preparation.ts +19 -12
  98. package/src/lib/server/connectors/swarmdock.ts +1 -1
  99. package/src/lib/server/extensions.ts +11 -0
  100. package/src/lib/server/messages/message-repository.ts +31 -0
  101. package/src/lib/server/openclaw/sync.ts +4 -4
  102. package/src/lib/server/protocols/protocol-a2a-delegate.ts +135 -0
  103. package/src/lib/server/protocols/protocol-normalization.ts +1 -0
  104. package/src/lib/server/protocols/protocol-step-helpers.test.ts +1 -1
  105. package/src/lib/server/protocols/protocol-step-helpers.ts +1 -0
  106. package/src/lib/server/protocols/protocol-step-processors.ts +2 -0
  107. package/src/lib/server/protocols/protocol-types.ts +1 -0
  108. package/src/lib/server/provider-health.ts +19 -3
  109. package/src/lib/server/safe-parse-body.test.ts +32 -0
  110. package/src/lib/server/safe-parse-body.ts +20 -3
  111. package/src/lib/server/session-tools/delegate.ts +151 -77
  112. package/src/lib/server/storage-auth.ts +10 -2
  113. package/src/lib/server/storage-normalization.ts +11 -0
  114. package/src/lib/server/storage.ts +113 -4
  115. package/src/lib/server/working-state/service.test.ts +2 -3
  116. package/src/lib/server/working-state/service.ts +37 -6
  117. package/src/lib/swarmfeed-client.ts +157 -0
  118. package/src/lib/validation/schemas.ts +1 -1
  119. package/src/stores/slices/data-slice.ts +3 -0
  120. package/src/stores/use-approval-store.ts +4 -1
  121. package/src/types/agent.ts +31 -1
  122. package/src/types/index.ts +1 -0
  123. package/src/types/protocol.ts +19 -0
  124. package/src/types/session.ts +1 -1
  125. package/src/types/swarmfeed.ts +30 -0
  126. package/tsconfig.json +1 -2
@@ -0,0 +1,82 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+
4
+ import { runWithTempDataDir } from '@/lib/server/test-utils/run-with-temp-data-dir'
5
+
6
+ test('tts routes reject malformed JSON with a 400', () => {
7
+ const output = runWithTempDataDir<{
8
+ ttsStatus: number
9
+ ttsPayload: { error?: string }
10
+ streamStatus: number
11
+ streamPayload: { error?: string }
12
+ }>(`
13
+ const ttsRouteMod = await import('./src/app/api/tts/route')
14
+ const ttsStreamRouteMod = await import('./src/app/api/tts/stream/route')
15
+ const ttsRoute = ttsRouteMod.default || ttsRouteMod
16
+ const ttsStreamRoute = ttsStreamRouteMod.default || ttsStreamRouteMod
17
+
18
+ const ttsResponse = await ttsRoute.POST(new Request('http://local/api/tts', {
19
+ method: 'POST',
20
+ headers: { 'content-type': 'application/json' },
21
+ body: '{bad-json',
22
+ }))
23
+
24
+ const ttsStreamResponse = await ttsStreamRoute.POST(new Request('http://local/api/tts/stream', {
25
+ method: 'POST',
26
+ headers: { 'content-type': 'application/json' },
27
+ body: '{bad-json',
28
+ }))
29
+
30
+ console.log(JSON.stringify({
31
+ ttsStatus: ttsResponse.status,
32
+ ttsPayload: await ttsResponse.json(),
33
+ streamStatus: ttsStreamResponse.status,
34
+ streamPayload: await ttsStreamResponse.json(),
35
+ }))
36
+ `, { prefix: 'swarmclaw-tts-route-' })
37
+
38
+ assert.equal(output.ttsStatus, 400)
39
+ assert.equal(output.ttsPayload.error, 'Invalid or missing request body')
40
+ assert.equal(output.streamStatus, 400)
41
+ assert.equal(output.streamPayload.error, 'Invalid or missing request body')
42
+ })
43
+
44
+ test('tts routes reject empty text with a validation error', () => {
45
+ const output = runWithTempDataDir<{
46
+ ttsStatus: number
47
+ ttsPayload: { error?: string; issues?: Array<{ path: string; message: string }> }
48
+ streamStatus: number
49
+ streamPayload: { error?: string; issues?: Array<{ path: string; message: string }> }
50
+ }>(`
51
+ const ttsRouteMod = await import('./src/app/api/tts/route')
52
+ const ttsStreamRouteMod = await import('./src/app/api/tts/stream/route')
53
+ const ttsRoute = ttsRouteMod.default || ttsRouteMod
54
+ const ttsStreamRoute = ttsStreamRouteMod.default || ttsStreamRouteMod
55
+
56
+ const ttsResponse = await ttsRoute.POST(new Request('http://local/api/tts', {
57
+ method: 'POST',
58
+ headers: { 'content-type': 'application/json' },
59
+ body: JSON.stringify({ text: ' ' }),
60
+ }))
61
+
62
+ const ttsStreamResponse = await ttsStreamRoute.POST(new Request('http://local/api/tts/stream', {
63
+ method: 'POST',
64
+ headers: { 'content-type': 'application/json' },
65
+ body: JSON.stringify({ text: ' ' }),
66
+ }))
67
+
68
+ console.log(JSON.stringify({
69
+ ttsStatus: ttsResponse.status,
70
+ ttsPayload: await ttsResponse.json(),
71
+ streamStatus: ttsStreamResponse.status,
72
+ streamPayload: await ttsStreamResponse.json(),
73
+ }))
74
+ `, { prefix: 'swarmclaw-tts-route-' })
75
+
76
+ assert.equal(output.ttsStatus, 400)
77
+ assert.equal(output.ttsPayload.error, 'Validation failed')
78
+ assert.deepEqual(output.ttsPayload.issues, [{ path: 'text', message: 'No text provided' }])
79
+ assert.equal(output.streamStatus, 400)
80
+ assert.equal(output.streamPayload.error, 'Validation failed')
81
+ assert.deepEqual(output.streamPayload.issues, [{ path: 'text', message: 'No text provided' }])
82
+ })
@@ -1,14 +1,21 @@
1
1
  import { NextResponse } from 'next/server'
2
+ import { z } from 'zod'
3
+
2
4
  import { explainElevenLabsError, resolveElevenLabsConfig, synthesizeElevenLabsMp3 } from '@/lib/server/elevenlabs'
5
+ import { safeParseBody } from '@/lib/server/safe-parse-body'
6
+
7
+ const TtsRequestSchema = z.object({
8
+ text: z.string().trim().min(1, 'No text provided'),
9
+ voiceId: z.string().nullable().optional(),
10
+ })
3
11
 
4
12
  export async function POST(req: Request) {
13
+ const { data: body, error } = await safeParseBody(req, TtsRequestSchema)
14
+ if (error) return error
15
+
5
16
  try {
6
- const { text, voiceId } = await req.json()
7
- if (!String(text || '').trim()) {
8
- return new NextResponse('No text provided', { status: 400 })
9
- }
10
- resolveElevenLabsConfig(voiceId)
11
- const audioBuffer = await synthesizeElevenLabsMp3({ text: String(text || ''), voiceId })
17
+ resolveElevenLabsConfig(body.voiceId)
18
+ const audioBuffer = await synthesizeElevenLabsMp3({ text: body.text, voiceId: body.voiceId })
12
19
  return new NextResponse(new Uint8Array(audioBuffer), {
13
20
  headers: {
14
21
  'Content-Type': 'audio/mpeg',
@@ -1,12 +1,19 @@
1
+ import { z } from 'zod'
2
+
1
3
  import { explainElevenLabsError, requestElevenLabsMp3Stream } from '@/lib/server/elevenlabs'
4
+ import { safeParseBody } from '@/lib/server/safe-parse-body'
5
+
6
+ const TtsStreamRequestSchema = z.object({
7
+ text: z.string().trim().min(1, 'No text provided'),
8
+ voiceId: z.string().nullable().optional(),
9
+ })
2
10
 
3
11
  export async function POST(req: Request) {
12
+ const { data: body, error } = await safeParseBody(req, TtsStreamRequestSchema)
13
+ if (error) return error
14
+
4
15
  try {
5
- const { text, voiceId } = await req.json()
6
- if (!String(text || '').trim()) {
7
- return new Response('No text provided', { status: 400 })
8
- }
9
- const apiRes = await requestElevenLabsMp3Stream({ text: String(text || ''), voiceId })
16
+ const apiRes = await requestElevenLabsMp3Stream({ text: body.text, voiceId: body.voiceId })
10
17
  return new Response(apiRes.body, {
11
18
  headers: {
12
19
  'Content-Type': 'audio/mpeg',
@@ -0,0 +1,32 @@
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+
5
+ import { ErrorFallback } from '@/components/layout/error-fallback'
6
+ import { reportClientError } from '@/lib/app/report-client-error'
7
+
8
+ export default function AppError({
9
+ error,
10
+ reset,
11
+ }: {
12
+ error: Error & { digest?: string }
13
+ reset: () => void
14
+ }) {
15
+ useEffect(() => {
16
+ reportClientError({
17
+ source: 'app-error',
18
+ error,
19
+ digest: error.digest,
20
+ })
21
+ }, [error])
22
+
23
+ return (
24
+ <ErrorFallback
25
+ message="A route-level error interrupted the current view. Try the request again or reload the app."
26
+ primaryLabel="Try Again"
27
+ onPrimaryAction={() => reset()}
28
+ secondaryLabel="Reload"
29
+ onSecondaryAction={() => window.location.reload()}
30
+ />
31
+ )
32
+ }
@@ -0,0 +1,33 @@
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+
5
+ import './globals.css'
6
+
7
+ import { ErrorFallback } from '@/components/layout/error-fallback'
8
+ import { reportClientError } from '@/lib/app/report-client-error'
9
+
10
+ export default function GlobalError({
11
+ error,
12
+ }: {
13
+ error: Error & { digest?: string }
14
+ }) {
15
+ useEffect(() => {
16
+ reportClientError({
17
+ source: 'global-error',
18
+ error,
19
+ digest: error.digest,
20
+ })
21
+ }, [error])
22
+
23
+ return (
24
+ <html lang="en" className="dark">
25
+ <body className="antialiased">
26
+ <ErrorFallback
27
+ message="A fatal application error occurred before the normal shell could recover. Reload the app to continue."
28
+ onPrimaryAction={() => window.location.reload()}
29
+ />
30
+ </body>
31
+ </html>
32
+ )
33
+ }
@@ -0,0 +1,93 @@
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+ import { useParams, useRouter } from 'next/navigation'
5
+ import { useProtocolTemplatesQuery } from '@/features/protocols/queries'
6
+ import { useProtocolBuilderStore } from '@/features/protocols/builder/protocol-builder-store'
7
+ import { templateToNodes } from '@/features/protocols/builder/utils/template-to-nodes'
8
+ import { getNodeLayout } from '@/features/protocols/builder/utils/node-position-layout'
9
+ import { ProtocolBuilderCanvas } from '@/components/protocols/builder/protocol-builder-canvas'
10
+ import { useTemplateSync } from '@/features/protocols/builder/hooks/use-template-sync'
11
+ import { useCanvasValidation } from '@/features/protocols/builder/hooks/use-canvas-validation'
12
+
13
+ export default function ProtocolBuilderPage() {
14
+ const params = useParams()
15
+ const router = useRouter()
16
+ const templateId = params.templateId as string
17
+
18
+ const { data: templates, isLoading } = useProtocolTemplatesQuery()
19
+ const loadTemplate = useProtocolBuilderStore((s) => s.loadTemplate)
20
+ const reset = useProtocolBuilderStore((s) => s.reset)
21
+
22
+ // Auto-sync to server
23
+ useTemplateSync(2000)
24
+
25
+ // Validate on changes
26
+ useCanvasValidation()
27
+
28
+ // Load template on mount
29
+ useEffect(() => {
30
+ if (!templates) return
31
+ const template = templates.find((t) => t.id === templateId)
32
+ if (!template) return
33
+
34
+ const { nodes, edges } = templateToNodes(template)
35
+ const positioned = getNodeLayout(nodes, edges)
36
+ loadTemplate(template, positioned, edges)
37
+ }, [templateId, templates, loadTemplate])
38
+
39
+ // Cleanup on unmount
40
+ useEffect(() => {
41
+ return () => reset()
42
+ }, [reset])
43
+
44
+ if (isLoading) {
45
+ return (
46
+ <div className="flex h-screen items-center justify-center">
47
+ <div className="text-sm text-muted-foreground">Loading builder...</div>
48
+ </div>
49
+ )
50
+ }
51
+
52
+ const template = templates?.find((t) => t.id === templateId)
53
+ if (!template) {
54
+ return (
55
+ <div className="flex h-screen flex-col items-center justify-center gap-3">
56
+ <div className="text-sm text-muted-foreground">Template not found</div>
57
+ <button
58
+ onClick={() => router.push('/protocols')}
59
+ className="text-sm text-blue-500 hover:underline"
60
+ >
61
+ Back to protocols
62
+ </button>
63
+ </div>
64
+ )
65
+ }
66
+
67
+ return (
68
+ <div className="flex h-full flex-col">
69
+ {/* Header */}
70
+ <div className="flex items-center justify-between border-b px-4 py-2">
71
+ <div className="flex items-center gap-3">
72
+ <button
73
+ onClick={() => router.push('/protocols')}
74
+ className="text-sm text-muted-foreground hover:text-foreground"
75
+ >
76
+ &larr; Protocols
77
+ </button>
78
+ <span className="text-sm font-semibold">{template.name}</span>
79
+ {template.builtIn && (
80
+ <span className="rounded bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground">
81
+ Built-in
82
+ </span>
83
+ )}
84
+ </div>
85
+ </div>
86
+
87
+ {/* Canvas */}
88
+ <div className="flex-1 p-3">
89
+ <ProtocolBuilderCanvas />
90
+ </div>
91
+ </div>
92
+ )
93
+ }
@@ -657,13 +657,22 @@ export default function ProtocolsPage() {
657
657
  : editingTemplateId ? 'Save template' : 'Create template'}
658
658
  </button>
659
659
  {editingTemplateId && (
660
- <button
661
- type="button"
662
- onClick={() => void handleDeleteTemplate(editingTemplateId)}
663
- className="rounded-[10px] border border-red-500/20 bg-red-500/10 px-3 py-2 text-[12px] font-700 text-red-200 transition-all hover:bg-red-500/14 cursor-pointer"
664
- >
665
- {templatePending === `delete:${editingTemplateId}` ? 'Deleting…' : 'Delete template'}
666
- </button>
660
+ <>
661
+ <button
662
+ type="button"
663
+ onClick={() => router.push(`/protocols/builder/${editingTemplateId}`)}
664
+ className="rounded-[10px] border border-blue-500/20 bg-blue-500/10 px-3 py-2 text-[12px] font-700 text-blue-200 transition-all hover:bg-blue-500/14 cursor-pointer"
665
+ >
666
+ Visual Builder
667
+ </button>
668
+ <button
669
+ type="button"
670
+ onClick={() => void handleDeleteTemplate(editingTemplateId)}
671
+ className="rounded-[10px] border border-red-500/20 bg-red-500/10 px-3 py-2 text-[12px] font-700 text-red-200 transition-all hover:bg-red-500/14 cursor-pointer"
672
+ >
673
+ {templatePending === `delete:${editingTemplateId}` ? 'Deleting…' : 'Delete template'}
674
+ </button>
675
+ </>
667
676
  )}
668
677
  </div>
669
678
  </div>
@@ -0,0 +1,7 @@
1
+ 'use client'
2
+
3
+ import { FeedPage } from '@/features/swarmfeed/feed-page'
4
+
5
+ export default function SwarmFeedPage() {
6
+ return <FeedPage />
7
+ }
package/src/cli/index.js CHANGED
@@ -220,6 +220,15 @@ const COMMAND_GROUPS = [
220
220
  cmd('suite', 'POST', '/eval/suite', 'Run a full eval suite against an agent', { expectsJsonBody: true }),
221
221
  ],
222
222
  },
223
+ {
224
+ name: 'a2a',
225
+ description: 'A2A Protocol gateway',
226
+ commands: [
227
+ cmd('send', 'POST', '/a2a', 'Send a JSON-RPC request to the A2A endpoint', { expectsJsonBody: true }),
228
+ cmd('agent-card', 'GET', '/.well-known/agent-card', 'Get agent card for a SwarmClaw agent'),
229
+ cmd('task-status', 'GET', '/a2a/tasks/:taskId/status', 'Check A2A task status'),
230
+ ],
231
+ },
223
232
  {
224
233
  name: 'external-agents',
225
234
  description: 'Manage external agent runtimes',
@@ -289,6 +298,9 @@ const COMMAND_GROUPS = [
289
298
  commands: [
290
299
  cmd('list', 'GET', '/logs', 'List logs (use --query lines=200, --query level=INFO,ERROR)'),
291
300
  cmd('clear', 'DELETE', '/logs', 'Clear logs file'),
301
+ cmd('report', 'POST', '/logs', 'Write a client/browser error entry to the application log', {
302
+ expectsJsonBody: true,
303
+ }),
292
304
  ],
293
305
  },
294
306
  {
@@ -784,6 +796,16 @@ const COMMAND_GROUPS = [
784
796
  cmd('delete', 'DELETE', '/goals/:id', 'Delete a goal'),
785
797
  ],
786
798
  },
799
+ {
800
+ name: 'swarmfeed',
801
+ description: 'SwarmFeed social network',
802
+ commands: [
803
+ cmd('feed', 'GET', '/swarmfeed', 'Get SwarmFeed timeline'),
804
+ cmd('channels', 'GET', '/swarmfeed/channels', 'List SwarmFeed channels'),
805
+ cmd('posts', 'GET', '/swarmfeed/posts', 'Get recent posts'),
806
+ cmd('post', 'POST', '/swarmfeed/posts', 'Create a post', { expectsJsonBody: true }),
807
+ ],
808
+ },
787
809
  ]
788
810
 
789
811
  const GROUP_MAP = new Map(COMMAND_GROUPS.map((group) => [group.name, group]))
package/src/cli/spec.js CHANGED
@@ -176,6 +176,14 @@ const COMMAND_GROUPS = {
176
176
  heartbeat: { description: 'Record an external agent heartbeat', method: 'POST', path: '/external-agents/:id/heartbeat', params: ['id'] },
177
177
  },
178
178
  },
179
+ a2a: {
180
+ description: 'A2A Protocol gateway',
181
+ commands: {
182
+ send: { description: 'Send a JSON-RPC request to the A2A endpoint', method: 'POST', path: '/a2a' },
183
+ 'agent-card': { description: 'Get agent card for a SwarmClaw agent', method: 'GET', path: '/.well-known/agent-card' },
184
+ 'task-status': { description: 'Check A2A task status', method: 'GET', path: '/a2a/tasks/:taskId/status', params: ['taskId'] },
185
+ },
186
+ },
179
187
  uploads: {
180
188
  description: 'Manage uploaded artifacts',
181
189
  commands: {
@@ -207,6 +215,7 @@ const COMMAND_GROUPS = {
207
215
  commands: {
208
216
  list: { description: 'Fetch logs (supports --query lines=200,level=INFO)', method: 'GET', path: '/logs' },
209
217
  clear: { description: 'Clear log file', method: 'DELETE', path: '/logs' },
218
+ report: { description: 'Write a client/browser error entry to the application log', method: 'POST', path: '/logs' },
210
219
  },
211
220
  },
212
221
 
@@ -2,13 +2,10 @@
2
2
 
3
3
  import { useMemo } from 'react'
4
4
  import multiavatar from '@multiavatar/multiavatar'
5
+ import DOMPurify from 'isomorphic-dompurify'
5
6
 
6
- /** Strip scripts/event handlers from SVG to prevent XSS */
7
7
  function sanitizeSvg(svg: string): string {
8
- return svg
9
- .replace(/<script[\s\S]*?<\/script>/gi, '')
10
- .replace(/\bon\w+\s*=\s*"[^"]*"/gi, '')
11
- .replace(/\bon\w+\s*=\s*'[^']*'/gi, '')
8
+ return DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true, svgFilters: true } })
12
9
  }
13
10
 
14
11
  interface Props {
@@ -28,6 +28,7 @@ import { errorMessage } from '@/lib/shared-utils'
28
28
  import { getDefaultAgentToolIds } from '@/lib/agent-default-tools'
29
29
  import { getEnabledExtensionIds, getEnabledToolIds } from '@/lib/capability-selection'
30
30
  import { buildAgentSelectableProviders, resolveAgentSelectableProviderCredentials } from '@/lib/agent-provider-options'
31
+ import { AgentSocialSettings } from '@/features/swarmfeed/agent-social-settings'
31
32
 
32
33
  const HB_PRESETS = [1800, 3600, 7200, 21600, 43200] as const
33
34
  const FALLBACK_ELEVENLABS_VOICE_ID = 'JBFqnCBsd6RMkjVDRZzb'
@@ -1973,6 +1974,15 @@ export function AgentSheet() {
1973
1974
  </SectionCard>
1974
1975
  )}
1975
1976
 
1977
+ {editing && (
1978
+ <SectionCard
1979
+ title="Social Network"
1980
+ description="SwarmFeed integration — let this agent post and engage on the social feed."
1981
+ >
1982
+ <AgentSocialSettings agent={editing} />
1983
+ </SectionCard>
1984
+ )}
1985
+
1976
1986
  {!WORKER_ONLY_PROVIDER_IDS.has(provider) && (
1977
1987
  <AdvancedSettingsSection
1978
1988
  open={showAdvancedSettings}
@@ -9,6 +9,11 @@ interface AccessKeyGateProps {
9
9
  }
10
10
 
11
11
  const AUTH_CHECK_TIMEOUT_MS = 8_000
12
+ const NETWORK_LINKS = [
13
+ { href: 'https://www.swarmdock.ai', label: 'SwarmDock' },
14
+ { href: 'https://swarmrecall.ai', label: 'SwarmRecall' },
15
+ { href: 'https://swarmrelay.ai', label: 'SwarmRelay' },
16
+ ]
12
17
 
13
18
  function isExpectedAuthCheckError(err: unknown): boolean {
14
19
  return isAbortError(err) || isTimeoutError(err)
@@ -421,6 +426,26 @@ export function AccessKeyGate({ onAuthenticated }: AccessKeyGateProps) {
421
426
  </form>
422
427
  </>
423
428
  )}
429
+
430
+ <div className="mt-10 border-t border-white/[0.06] pt-5" style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.35s both' }}>
431
+ <p className="text-[10px] font-700 uppercase tracking-[0.18em] text-text-3/55">
432
+ Network
433
+ </p>
434
+ <div className="mt-3 flex flex-wrap items-center justify-center gap-2.5">
435
+ {NETWORK_LINKS.map((link) => (
436
+ <a
437
+ key={link.href}
438
+ href={link.href}
439
+ target="_blank"
440
+ rel="noopener noreferrer"
441
+ className="rounded-full border border-white/[0.08] bg-white/[0.03] px-3 py-1.5 text-[12px] text-text-3
442
+ no-underline transition-all duration-200 hover:border-white/[0.14] hover:bg-white/[0.06] hover:text-text"
443
+ >
444
+ {link.label}
445
+ </a>
446
+ ))}
447
+ </div>
448
+ </div>
424
449
  </div>
425
450
  </div>
426
451
  )
@@ -3,6 +3,9 @@
3
3
  import { Component } from 'react'
4
4
  import type { ReactNode, ErrorInfo } from 'react'
5
5
 
6
+ import { reportClientError } from '@/lib/app/report-client-error'
7
+ import { ErrorFallback } from '@/components/layout/error-fallback'
8
+
6
9
  export class ErrorBoundary extends Component<{ children: ReactNode }, { hasError: boolean }> {
7
10
  constructor(props: { children: ReactNode }) {
8
11
  super(props)
@@ -15,41 +18,20 @@ export class ErrorBoundary extends Component<{ children: ReactNode }, { hasError
15
18
  }
16
19
 
17
20
  componentDidCatch(error: Error, info: ErrorInfo) {
18
- console.error('ErrorBoundary caught:', error, info)
21
+ reportClientError({
22
+ source: 'error-boundary',
23
+ error,
24
+ componentStack: info.componentStack,
25
+ })
19
26
  }
20
27
 
21
28
  render() {
22
29
  if (this.state.hasError) {
23
30
  return (
24
- <div className="flex-1 flex flex-col items-center justify-center px-8 bg-bg">
25
- <div className="text-center max-w-[400px]">
26
- <div className="w-14 h-14 rounded-[16px] bg-red-500/10 border border-red-500/20 flex items-center justify-center mx-auto mb-5">
27
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-red-400">
28
- <circle cx="12" cy="12" r="10" />
29
- <line x1="12" y1="8" x2="12" y2="12" />
30
- <line x1="12" y1="16" x2="12.01" y2="16" />
31
- </svg>
32
- </div>
33
- <h2 className="font-display text-[22px] font-700 text-text mb-2 tracking-[-0.02em]">
34
- Something went wrong
35
- </h2>
36
- <p className="text-[14px] text-text-3 mb-6">
37
- An unexpected error occurred. Try reloading the page.
38
- </p>
39
- <button
40
- onClick={() => window.location.reload()}
41
- className="inline-flex items-center gap-2 px-6 py-3 rounded-[12px] border-none bg-accent-bright text-white text-[14px] font-600 cursor-pointer
42
- hover:brightness-110 active:scale-[0.97] transition-all shadow-[0_4px_16px_rgba(99,102,241,0.2)]"
43
- style={{ fontFamily: 'inherit' }}
44
- >
45
- <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
46
- <polyline points="23 4 23 10 17 10" />
47
- <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
48
- </svg>
49
- Reload
50
- </button>
51
- </div>
52
- </div>
31
+ <ErrorFallback
32
+ message="An unexpected dashboard error occurred. Reload the page to recover."
33
+ onPrimaryAction={() => window.location.reload()}
34
+ />
53
35
  )
54
36
  }
55
37
 
@@ -0,0 +1,61 @@
1
+ 'use client'
2
+
3
+ type ErrorFallbackProps = {
4
+ title?: string
5
+ message?: string
6
+ primaryLabel?: string
7
+ onPrimaryAction?: () => void
8
+ secondaryLabel?: string
9
+ onSecondaryAction?: () => void
10
+ }
11
+
12
+ export function ErrorFallback({
13
+ title = 'Something went wrong',
14
+ message = 'An unexpected error occurred. Try again or reload the page.',
15
+ primaryLabel = 'Reload',
16
+ onPrimaryAction,
17
+ secondaryLabel,
18
+ onSecondaryAction,
19
+ }: ErrorFallbackProps) {
20
+ return (
21
+ <div className="flex min-h-[50vh] flex-1 flex-col items-center justify-center bg-bg px-8">
22
+ <div className="max-w-[420px] text-center">
23
+ <div className="mx-auto mb-5 flex h-14 w-14 items-center justify-center rounded-[16px] border border-red-500/20 bg-red-500/10">
24
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-red-400">
25
+ <circle cx="12" cy="12" r="10" />
26
+ <line x1="12" y1="8" x2="12" y2="12" />
27
+ <line x1="12" y1="16" x2="12.01" y2="16" />
28
+ </svg>
29
+ </div>
30
+ <h2 className="mb-2 font-display text-[22px] font-700 tracking-[-0.02em] text-text">
31
+ {title}
32
+ </h2>
33
+ <p className="mb-6 text-[14px] text-text-3">
34
+ {message}
35
+ </p>
36
+ <div className="flex flex-wrap items-center justify-center gap-3">
37
+ <button
38
+ onClick={onPrimaryAction}
39
+ className="inline-flex cursor-pointer items-center gap-2 rounded-[12px] border-none bg-accent-bright px-6 py-3 text-[14px] font-600 text-white shadow-[0_4px_16px_rgba(99,102,241,0.2)] transition-all hover:brightness-110 active:scale-[0.97]"
40
+ style={{ fontFamily: 'inherit' }}
41
+ >
42
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round">
43
+ <polyline points="23 4 23 10 17 10" />
44
+ <path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10" />
45
+ </svg>
46
+ {primaryLabel}
47
+ </button>
48
+ {secondaryLabel && onSecondaryAction ? (
49
+ <button
50
+ onClick={onSecondaryAction}
51
+ className="inline-flex cursor-pointer items-center rounded-[12px] border border-border bg-transparent px-5 py-3 text-[14px] font-600 text-text transition-colors hover:bg-panel/60"
52
+ style={{ fontFamily: 'inherit' }}
53
+ >
54
+ {secondaryLabel}
55
+ </button>
56
+ ) : null}
57
+ </div>
58
+ </div>
59
+ </div>
60
+ )
61
+ }