@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.
- package/README.md +16 -52
- package/next.config.ts +9 -4
- package/package.json +18 -10
- package/scripts/build-bootstrap-env.mjs +24 -0
- package/scripts/run-next-build.mjs +74 -0
- package/scripts/run-next-typegen.mjs +61 -0
- package/src/app/api/.well-known/agent-card/route.ts +46 -0
- package/src/app/api/a2a/route.ts +56 -0
- package/src/app/api/a2a/tasks/[taskId]/status/route.ts +49 -0
- package/src/app/api/approvals/route.test.ts +29 -3
- package/src/app/api/approvals/route.ts +13 -7
- package/src/app/api/chats/[id]/chat/route.test.ts +64 -0
- package/src/app/api/chats/[id]/chat/route.ts +24 -8
- package/src/app/api/chats/[id]/deploy/route.ts +2 -2
- package/src/app/api/chats/chat-route.test.ts +68 -0
- package/src/app/api/connectors/[id]/doctor/route.test.ts +97 -0
- package/src/app/api/connectors/[id]/doctor/route.ts +26 -1
- package/src/app/api/connectors/connector-doctor-route.test.ts +1 -0
- package/src/app/api/logs/route.test.ts +61 -0
- package/src/app/api/logs/route.ts +35 -0
- package/src/app/api/openclaw/sync/route.ts +1 -1
- package/src/app/api/swarmfeed/channels/route.ts +14 -0
- package/src/app/api/swarmfeed/posts/route.ts +60 -0
- package/src/app/api/swarmfeed/route.ts +37 -0
- package/src/app/api/tts/route.test.ts +82 -0
- package/src/app/api/tts/route.ts +13 -6
- package/src/app/api/tts/stream/route.ts +12 -5
- package/src/app/error.tsx +32 -0
- package/src/app/global-error.tsx +33 -0
- package/src/app/protocols/builder/[templateId]/page.tsx +93 -0
- package/src/app/protocols/page.tsx +16 -7
- package/src/app/swarmfeed/page.tsx +7 -0
- package/src/cli/index.js +22 -0
- package/src/cli/spec.js +9 -0
- package/src/components/agents/agent-avatar.tsx +2 -5
- package/src/components/agents/agent-sheet.tsx +10 -0
- package/src/components/auth/access-key-gate.tsx +25 -0
- package/src/components/layout/error-boundary.tsx +12 -30
- package/src/components/layout/error-fallback.tsx +61 -0
- package/src/components/layout/sidebar-rail.tsx +52 -0
- package/src/components/protocols/builder/edge-editor.tsx +43 -0
- package/src/components/protocols/builder/edge-types/branch-edge.tsx +33 -0
- package/src/components/protocols/builder/edge-types/default-edge.tsx +18 -0
- package/src/components/protocols/builder/edge-types/index.ts +3 -0
- package/src/components/protocols/builder/edge-types/loop-edge.tsx +19 -0
- package/src/components/protocols/builder/node-inspector.tsx +227 -0
- package/src/components/protocols/builder/node-palette.tsx +97 -0
- package/src/components/protocols/builder/node-types/branch-node.tsx +34 -0
- package/src/components/protocols/builder/node-types/complete-node.tsx +17 -0
- package/src/components/protocols/builder/node-types/for-each-node.tsx +21 -0
- package/src/components/protocols/builder/node-types/index.ts +9 -0
- package/src/components/protocols/builder/node-types/join-node.tsx +18 -0
- package/src/components/protocols/builder/node-types/loop-node.tsx +22 -0
- package/src/components/protocols/builder/node-types/parallel-node.tsx +31 -0
- package/src/components/protocols/builder/node-types/phase-node.tsx +52 -0
- package/src/components/protocols/builder/node-types/subflow-node.tsx +23 -0
- package/src/components/protocols/builder/node-types/swarm-node.tsx +26 -0
- package/src/components/protocols/builder/protocol-builder-canvas.tsx +184 -0
- package/src/components/protocols/builder/run-overlay.tsx +29 -0
- package/src/components/protocols/builder/template-gallery.tsx +53 -0
- package/src/components/protocols/builder/validation-panel.tsx +57 -0
- package/src/components/skills/skills-workspace.tsx +1 -9
- package/src/features/protocols/builder/hooks/index.ts +2 -0
- package/src/features/protocols/builder/hooks/use-canvas-validation.ts +14 -0
- package/src/features/protocols/builder/hooks/use-run-overlay.ts +39 -0
- package/src/features/protocols/builder/hooks/use-template-sync.ts +45 -0
- package/src/features/protocols/builder/protocol-builder-store.ts +233 -0
- package/src/features/protocols/builder/utils/node-position-layout.ts +41 -0
- package/src/features/protocols/builder/utils/nodes-to-template.test.ts +179 -0
- package/src/features/protocols/builder/utils/nodes-to-template.ts +49 -0
- package/src/features/protocols/builder/utils/template-to-nodes.test.ts +314 -0
- package/src/features/protocols/builder/utils/template-to-nodes.ts +169 -0
- package/src/features/protocols/builder/validators/dag-validator.test.ts +150 -0
- package/src/features/protocols/builder/validators/dag-validator.ts +119 -0
- package/src/features/swarmfeed/agent-social-settings.tsx +277 -0
- package/src/features/swarmfeed/compose-post.tsx +139 -0
- package/src/features/swarmfeed/feed-page.tsx +136 -0
- package/src/features/swarmfeed/post-card.tsx +114 -0
- package/src/features/swarmfeed/queries.ts +28 -0
- package/src/lib/a2a/agent-card.ts +61 -0
- package/src/lib/a2a/auth.ts +54 -0
- package/src/lib/a2a/client.ts +133 -0
- package/src/lib/a2a/discovery.ts +116 -0
- package/src/lib/a2a/handlers.ts +176 -0
- package/src/lib/a2a/json-rpc-router.ts +38 -0
- package/src/lib/a2a/types.ts +95 -0
- package/src/lib/app/navigation.ts +1 -0
- package/src/lib/app/report-client-error.ts +52 -0
- package/src/lib/app/view-constants.ts +9 -1
- package/src/lib/providers/anthropic.ts +119 -107
- package/src/lib/providers/ollama.ts +34 -14
- package/src/lib/providers/openai.ts +154 -142
- package/src/lib/providers/openclaw.ts +3 -3
- package/src/lib/server/agents/main-agent-loop.test.ts +94 -0
- package/src/lib/server/agents/main-agent-loop.ts +377 -41
- package/src/lib/server/chat-execution/chat-execution.ts +12 -7
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +19 -12
- package/src/lib/server/connectors/swarmdock.ts +1 -1
- package/src/lib/server/extensions.ts +11 -0
- package/src/lib/server/messages/message-repository.ts +31 -0
- package/src/lib/server/openclaw/sync.ts +4 -4
- package/src/lib/server/protocols/protocol-a2a-delegate.ts +135 -0
- package/src/lib/server/protocols/protocol-normalization.ts +1 -0
- package/src/lib/server/protocols/protocol-step-helpers.test.ts +1 -1
- package/src/lib/server/protocols/protocol-step-helpers.ts +1 -0
- package/src/lib/server/protocols/protocol-step-processors.ts +2 -0
- package/src/lib/server/protocols/protocol-types.ts +1 -0
- package/src/lib/server/provider-health.ts +19 -3
- package/src/lib/server/safe-parse-body.test.ts +32 -0
- package/src/lib/server/safe-parse-body.ts +20 -3
- package/src/lib/server/session-tools/delegate.ts +151 -77
- package/src/lib/server/storage-auth.ts +10 -2
- package/src/lib/server/storage-normalization.ts +11 -0
- package/src/lib/server/storage.ts +113 -4
- package/src/lib/server/working-state/service.test.ts +2 -3
- package/src/lib/server/working-state/service.ts +37 -6
- package/src/lib/swarmfeed-client.ts +157 -0
- package/src/lib/validation/schemas.ts +1 -1
- package/src/stores/slices/data-slice.ts +3 -0
- package/src/stores/use-approval-store.ts +4 -1
- package/src/types/agent.ts +31 -1
- package/src/types/index.ts +1 -0
- package/src/types/protocol.ts +19 -0
- package/src/types/session.ts +1 -1
- package/src/types/swarmfeed.ts +30 -0
- package/tsconfig.json +1 -2
|
@@ -123,6 +123,70 @@ test('chat route keeps long-lived user runs alive after stream disconnect and re
|
|
|
123
123
|
assert.ok(output.perfLabels.includes('chat-execution/llm-round-trip'))
|
|
124
124
|
})
|
|
125
125
|
|
|
126
|
+
test('chat route rejects malformed JSON with a 400 before queueing work', () => {
|
|
127
|
+
const output = runWithTempDataDir<{
|
|
128
|
+
status: number
|
|
129
|
+
payload: { error?: string }
|
|
130
|
+
runCount: number
|
|
131
|
+
}>(`
|
|
132
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
133
|
+
const routeMod = await import('./src/app/api/chats/[id]/chat/route')
|
|
134
|
+
const runsMod = await import('@/lib/server/runtime/session-run-manager')
|
|
135
|
+
const storage = storageMod.default || storageMod
|
|
136
|
+
const route = routeMod.default || routeMod
|
|
137
|
+
const runs = runsMod.default || runsMod
|
|
138
|
+
|
|
139
|
+
const now = Date.now()
|
|
140
|
+
storage.saveAgents({
|
|
141
|
+
agent_1: {
|
|
142
|
+
id: 'agent_1',
|
|
143
|
+
name: 'Malformed Agent',
|
|
144
|
+
provider: 'openai',
|
|
145
|
+
model: 'gpt-4o-mini',
|
|
146
|
+
extensions: [],
|
|
147
|
+
createdAt: now,
|
|
148
|
+
updatedAt: now,
|
|
149
|
+
},
|
|
150
|
+
})
|
|
151
|
+
storage.saveSessions({
|
|
152
|
+
sess_1: {
|
|
153
|
+
id: 'sess_1',
|
|
154
|
+
name: 'Malformed Session',
|
|
155
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
156
|
+
user: 'workbench',
|
|
157
|
+
provider: 'openai',
|
|
158
|
+
model: 'gpt-4o-mini',
|
|
159
|
+
claudeSessionId: null,
|
|
160
|
+
messages: [],
|
|
161
|
+
createdAt: now,
|
|
162
|
+
lastActiveAt: now,
|
|
163
|
+
sessionType: 'human',
|
|
164
|
+
agentId: 'agent_1',
|
|
165
|
+
extensions: [],
|
|
166
|
+
},
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const response = await route.POST(
|
|
170
|
+
new Request('http://local/api/chats/sess_1/chat', {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers: { 'content-type': 'application/json' },
|
|
173
|
+
body: '{bad-json',
|
|
174
|
+
}),
|
|
175
|
+
{ params: Promise.resolve({ id: 'sess_1' }) },
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
console.log(JSON.stringify({
|
|
179
|
+
status: response.status,
|
|
180
|
+
payload: await response.json(),
|
|
181
|
+
runCount: runs.listRuns({ sessionId: 'sess_1' }).length,
|
|
182
|
+
}))
|
|
183
|
+
`, { prefix: 'swarmclaw-chat-route-invalid-json-' })
|
|
184
|
+
|
|
185
|
+
assert.equal(output.status, 400)
|
|
186
|
+
assert.equal(output.payload.error, 'Invalid or missing request body')
|
|
187
|
+
assert.equal(output.runCount, 0)
|
|
188
|
+
})
|
|
189
|
+
|
|
126
190
|
test('chat route heartbeat runs stay internal and do not persist terminal ack text', () => {
|
|
127
191
|
const output = runWithTempDataDir<{
|
|
128
192
|
events: Array<{ t?: string; text?: string }>
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
2
4
|
import { enqueueSessionRun, type SessionQueueMode } from '@/lib/server/runtime/session-run-manager'
|
|
3
5
|
import { log } from '@/lib/server/logger'
|
|
6
|
+
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
4
7
|
|
|
5
8
|
export const dynamic = 'force-dynamic'
|
|
6
9
|
export const maxDuration = 300
|
|
7
10
|
|
|
11
|
+
const ChatRouteBodySchema = z.object({
|
|
12
|
+
message: z.string().optional().default(''),
|
|
13
|
+
imagePath: z.string().optional(),
|
|
14
|
+
imageUrl: z.string().optional(),
|
|
15
|
+
attachedFiles: z.array(z.string()).optional(),
|
|
16
|
+
internal: z.boolean().optional().default(false),
|
|
17
|
+
queueMode: z.enum(['steer', 'collect', 'followup']).optional(),
|
|
18
|
+
replyToId: z.string().optional(),
|
|
19
|
+
}).passthrough()
|
|
20
|
+
|
|
8
21
|
function normalizeQueueMode(raw: unknown, internal: boolean): SessionQueueMode {
|
|
9
22
|
if (raw === 'steer' || raw === 'collect' || raw === 'followup') return raw
|
|
10
23
|
return internal ? 'collect' : 'followup'
|
|
@@ -13,15 +26,17 @@ function normalizeQueueMode(raw: unknown, internal: boolean): SessionQueueMode {
|
|
|
13
26
|
export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
14
27
|
try {
|
|
15
28
|
const { id } = await params
|
|
16
|
-
const body = await req
|
|
29
|
+
const { data: body, error } = await safeParseBody(req, ChatRouteBodySchema)
|
|
30
|
+
if (error) return error
|
|
17
31
|
|
|
18
|
-
const message =
|
|
19
|
-
const imagePath =
|
|
20
|
-
const imageUrl =
|
|
21
|
-
const attachedFiles =
|
|
22
|
-
const internal = body.internal
|
|
32
|
+
const message = body.message
|
|
33
|
+
const imagePath = body.imagePath
|
|
34
|
+
const imageUrl = body.imageUrl
|
|
35
|
+
const attachedFiles = body.attachedFiles
|
|
36
|
+
const internal = body.internal
|
|
23
37
|
const queueMode = normalizeQueueMode(body.queueMode, internal)
|
|
24
|
-
const replyToId =
|
|
38
|
+
const replyToId = body.replyToId
|
|
39
|
+
const source = internal ? 'heartbeat' : 'chat'
|
|
25
40
|
|
|
26
41
|
const hasFiles = !!(imagePath || imageUrl || (attachedFiles && attachedFiles.length > 0))
|
|
27
42
|
if (!message.trim() && !hasFiles) {
|
|
@@ -50,7 +65,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
50
65
|
imageUrl,
|
|
51
66
|
attachedFiles,
|
|
52
67
|
internal,
|
|
53
|
-
source
|
|
68
|
+
source,
|
|
54
69
|
mode: queueMode,
|
|
55
70
|
onEvent: (ev) => writeEvent(ev as unknown as Record<string, unknown>),
|
|
56
71
|
replyToId,
|
|
@@ -78,6 +93,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
78
93
|
status: run.deduped ? 'deduped' : run.coalesced ? 'coalesced' : 'queued',
|
|
79
94
|
position: run.position,
|
|
80
95
|
internal,
|
|
96
|
+
source,
|
|
81
97
|
mode: queueMode,
|
|
82
98
|
},
|
|
83
99
|
}),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
-
import { execSync } from 'child_process'
|
|
2
|
+
import { execSync, execFileSync } from 'child_process'
|
|
3
3
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
4
4
|
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
5
5
|
import { log } from '@/lib/server/logger'
|
|
@@ -21,7 +21,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
21
21
|
execSync('git add -A', opts)
|
|
22
22
|
let committed = false
|
|
23
23
|
try {
|
|
24
|
-
|
|
24
|
+
execFileSync('git', ['commit', '-m', msg], opts)
|
|
25
25
|
committed = true
|
|
26
26
|
} catch (ce: unknown) {
|
|
27
27
|
const ex = ce as { stdout?: string; stderr?: string }
|
|
@@ -0,0 +1,68 @@
|
|
|
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('chat route rejects malformed JSON with a 400 before queueing work', () => {
|
|
7
|
+
const output = runWithTempDataDir<{
|
|
8
|
+
status: number
|
|
9
|
+
payload: { error?: string }
|
|
10
|
+
runCount: number
|
|
11
|
+
}>(`
|
|
12
|
+
const storageMod = await import('./src/lib/server/storage')
|
|
13
|
+
const routeMod = await import('./src/app/api/chats/[id]/chat/route')
|
|
14
|
+
const runsMod = await import('@/lib/server/runtime/session-run-manager')
|
|
15
|
+
const storage = storageMod.default || storageMod
|
|
16
|
+
const route = routeMod.default || routeMod
|
|
17
|
+
const runs = runsMod.default || runsMod
|
|
18
|
+
|
|
19
|
+
const now = Date.now()
|
|
20
|
+
storage.saveAgents({
|
|
21
|
+
agent_1: {
|
|
22
|
+
id: 'agent_1',
|
|
23
|
+
name: 'Malformed Agent',
|
|
24
|
+
provider: 'openai',
|
|
25
|
+
model: 'gpt-4o-mini',
|
|
26
|
+
extensions: [],
|
|
27
|
+
createdAt: now,
|
|
28
|
+
updatedAt: now,
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
storage.saveSessions({
|
|
32
|
+
sess_1: {
|
|
33
|
+
id: 'sess_1',
|
|
34
|
+
name: 'Malformed Session',
|
|
35
|
+
cwd: process.env.WORKSPACE_DIR,
|
|
36
|
+
user: 'workbench',
|
|
37
|
+
provider: 'openai',
|
|
38
|
+
model: 'gpt-4o-mini',
|
|
39
|
+
claudeSessionId: null,
|
|
40
|
+
messages: [],
|
|
41
|
+
createdAt: now,
|
|
42
|
+
lastActiveAt: now,
|
|
43
|
+
sessionType: 'human',
|
|
44
|
+
agentId: 'agent_1',
|
|
45
|
+
extensions: [],
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const response = await route.POST(
|
|
50
|
+
new Request('http://local/api/chats/sess_1/chat', {
|
|
51
|
+
method: 'POST',
|
|
52
|
+
headers: { 'content-type': 'application/json' },
|
|
53
|
+
body: '{bad-json',
|
|
54
|
+
}),
|
|
55
|
+
{ params: Promise.resolve({ id: 'sess_1' }) },
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
console.log(JSON.stringify({
|
|
59
|
+
status: response.status,
|
|
60
|
+
payload: await response.json(),
|
|
61
|
+
runCount: runs.listRuns({ sessionId: 'sess_1' }).length,
|
|
62
|
+
}))
|
|
63
|
+
`, { prefix: 'swarmclaw-chat-route-invalid-json-' })
|
|
64
|
+
|
|
65
|
+
assert.equal(output.status, 400)
|
|
66
|
+
assert.equal(output.payload.error, 'Invalid or missing request body')
|
|
67
|
+
assert.equal(output.runCount, 0)
|
|
68
|
+
})
|
|
@@ -0,0 +1,97 @@
|
|
|
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('connector doctor route rejects malformed JSON with a 400', () => {
|
|
7
|
+
const output = runWithTempDataDir<{
|
|
8
|
+
status: number
|
|
9
|
+
payload: { error?: string }
|
|
10
|
+
}>(`
|
|
11
|
+
const repoMod = await import('./src/lib/server/connectors/connector-repository')
|
|
12
|
+
const routeMod = await import('./src/app/api/connectors/[id]/doctor/route')
|
|
13
|
+
const repo = repoMod.default || repoMod
|
|
14
|
+
const route = routeMod.default || routeMod
|
|
15
|
+
|
|
16
|
+
repo.saveConnectors({
|
|
17
|
+
conn_1: {
|
|
18
|
+
id: 'conn_1',
|
|
19
|
+
name: 'Doctor Test',
|
|
20
|
+
platform: 'discord',
|
|
21
|
+
agentId: 'agent_1',
|
|
22
|
+
chatroomId: null,
|
|
23
|
+
credentialId: null,
|
|
24
|
+
config: {},
|
|
25
|
+
isEnabled: true,
|
|
26
|
+
status: 'running',
|
|
27
|
+
createdAt: 1,
|
|
28
|
+
updatedAt: 1,
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const response = await route.POST(new Request('http://local/api/connectors/conn_1/doctor', {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: { 'content-type': 'application/json' },
|
|
35
|
+
body: '{bad-json',
|
|
36
|
+
}), { params: Promise.resolve({ id: 'conn_1' }) })
|
|
37
|
+
|
|
38
|
+
console.log(JSON.stringify({
|
|
39
|
+
status: response.status,
|
|
40
|
+
payload: await response.json(),
|
|
41
|
+
}))
|
|
42
|
+
`, { prefix: 'swarmclaw-connector-doctor-route-' })
|
|
43
|
+
|
|
44
|
+
assert.equal(output.status, 400)
|
|
45
|
+
assert.equal(output.payload.error, 'Invalid or missing request body')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('connector doctor route returns a preview report for valid input', () => {
|
|
49
|
+
const output = runWithTempDataDir<{
|
|
50
|
+
status: number
|
|
51
|
+
payload: { warnings?: string[]; policy?: { mode?: string } }
|
|
52
|
+
}>(`
|
|
53
|
+
const repoMod = await import('./src/lib/server/connectors/connector-repository')
|
|
54
|
+
const routeMod = await import('./src/app/api/connectors/[id]/doctor/route')
|
|
55
|
+
const repo = repoMod.default || repoMod
|
|
56
|
+
const route = routeMod.default || routeMod
|
|
57
|
+
|
|
58
|
+
repo.saveConnectors({
|
|
59
|
+
conn_1: {
|
|
60
|
+
id: 'conn_1',
|
|
61
|
+
name: 'Doctor Test',
|
|
62
|
+
platform: 'discord',
|
|
63
|
+
agentId: 'agent_1',
|
|
64
|
+
chatroomId: null,
|
|
65
|
+
credentialId: null,
|
|
66
|
+
config: {},
|
|
67
|
+
isEnabled: true,
|
|
68
|
+
status: 'running',
|
|
69
|
+
createdAt: 1,
|
|
70
|
+
updatedAt: 1,
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const response = await route.POST(new Request('http://local/api/connectors/conn_1/doctor', {
|
|
75
|
+
method: 'POST',
|
|
76
|
+
headers: { 'content-type': 'application/json' },
|
|
77
|
+
body: JSON.stringify({
|
|
78
|
+
sampleMsg: {
|
|
79
|
+
channelId: 'channel-1',
|
|
80
|
+
channelName: 'general',
|
|
81
|
+
senderId: 'user-1',
|
|
82
|
+
senderName: 'User',
|
|
83
|
+
text: 'hello',
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
}), { params: Promise.resolve({ id: 'conn_1' }) })
|
|
87
|
+
|
|
88
|
+
console.log(JSON.stringify({
|
|
89
|
+
status: response.status,
|
|
90
|
+
payload: await response.json(),
|
|
91
|
+
}))
|
|
92
|
+
`, { prefix: 'swarmclaw-connector-doctor-route-' })
|
|
93
|
+
|
|
94
|
+
assert.equal(output.status, 200)
|
|
95
|
+
assert.ok(Array.isArray(output.payload.warnings))
|
|
96
|
+
assert.ok(output.payload.policy)
|
|
97
|
+
})
|
|
@@ -1,10 +1,34 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
2
4
|
import { notFound } from '@/lib/server/collection-helpers'
|
|
3
5
|
import { buildConnectorDoctorPreview, buildConnectorDoctorReport, type ConnectorDoctorPreviewInput } from '@/lib/server/connectors/doctor'
|
|
4
6
|
import { loadConnectors } from '@/lib/server/connectors/connector-repository'
|
|
7
|
+
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
5
8
|
|
|
6
9
|
export const dynamic = 'force-dynamic'
|
|
7
10
|
|
|
11
|
+
const ConnectorDoctorPreviewSchema: z.ZodType<ConnectorDoctorPreviewInput> = z.object({
|
|
12
|
+
id: z.unknown().optional(),
|
|
13
|
+
name: z.unknown().optional(),
|
|
14
|
+
platform: z.unknown().optional(),
|
|
15
|
+
agentId: z.unknown().optional(),
|
|
16
|
+
chatroomId: z.unknown().optional(),
|
|
17
|
+
credentialId: z.unknown().optional(),
|
|
18
|
+
config: z.record(z.string(), z.unknown()).optional(),
|
|
19
|
+
sampleMsg: z.object({
|
|
20
|
+
channelId: z.string().optional(),
|
|
21
|
+
channelName: z.string().optional(),
|
|
22
|
+
senderId: z.string().optional(),
|
|
23
|
+
senderName: z.string().optional(),
|
|
24
|
+
text: z.string().optional(),
|
|
25
|
+
isGroup: z.boolean().optional(),
|
|
26
|
+
messageId: z.string().optional(),
|
|
27
|
+
replyToMessageId: z.string().optional(),
|
|
28
|
+
threadId: z.string().optional(),
|
|
29
|
+
}).passthrough().nullable().optional(),
|
|
30
|
+
}).passthrough()
|
|
31
|
+
|
|
8
32
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
9
33
|
const { id } = await params
|
|
10
34
|
const connectors = loadConnectors()
|
|
@@ -20,7 +44,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
20
44
|
const baseConnector = connectors[id]
|
|
21
45
|
if (!baseConnector) return notFound()
|
|
22
46
|
|
|
23
|
-
const body = await req
|
|
47
|
+
const { data: body, error } = await safeParseBody(req, ConnectorDoctorPreviewSchema)
|
|
48
|
+
if (error) return error
|
|
24
49
|
const connector = buildConnectorDoctorPreview({ baseConnector, input: body, fallbackId: id })
|
|
25
50
|
return NextResponse.json(buildConnectorDoctorReport(connector, body.sampleMsg, { baseConnector }))
|
|
26
51
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './[id]/doctor/route.test'
|
|
@@ -0,0 +1,61 @@
|
|
|
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('logs route accepts client-side error reports and persists them', () => {
|
|
7
|
+
const output = runWithTempDataDir<{
|
|
8
|
+
status: number
|
|
9
|
+
payload: { ok?: boolean }
|
|
10
|
+
entries: Array<{ tag?: string; message?: string; data?: string }>
|
|
11
|
+
}>(`
|
|
12
|
+
const routeMod = await import('./src/app/api/logs/route')
|
|
13
|
+
const route = routeMod.default || routeMod
|
|
14
|
+
|
|
15
|
+
const postResponse = await route.POST(new Request('http://local/api/logs', {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
headers: { 'content-type': 'application/json' },
|
|
18
|
+
body: JSON.stringify({
|
|
19
|
+
source: 'error-boundary',
|
|
20
|
+
message: 'Client render failed',
|
|
21
|
+
componentStack: 'at DemoComponent',
|
|
22
|
+
}),
|
|
23
|
+
}))
|
|
24
|
+
|
|
25
|
+
const getResponse = await route.GET(new Request('http://local/api/logs?lines=5&search=Client%20render%20failed'))
|
|
26
|
+
|
|
27
|
+
console.log(JSON.stringify({
|
|
28
|
+
status: postResponse.status,
|
|
29
|
+
payload: await postResponse.json(),
|
|
30
|
+
entries: (await getResponse.json()).entries,
|
|
31
|
+
}))
|
|
32
|
+
`, { prefix: 'swarmclaw-logs-route-' })
|
|
33
|
+
|
|
34
|
+
assert.equal(output.status, 200)
|
|
35
|
+
assert.equal(output.payload.ok, true)
|
|
36
|
+
assert.ok(output.entries.some((entry) => entry.tag === 'error-boundary' && entry.message === 'Client render failed'))
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('logs route rejects malformed client error payloads with a 400', () => {
|
|
40
|
+
const output = runWithTempDataDir<{
|
|
41
|
+
status: number
|
|
42
|
+
payload: { error?: string }
|
|
43
|
+
}>(`
|
|
44
|
+
const routeMod = await import('./src/app/api/logs/route')
|
|
45
|
+
const route = routeMod.default || routeMod
|
|
46
|
+
|
|
47
|
+
const response = await route.POST(new Request('http://local/api/logs', {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'content-type': 'application/json' },
|
|
50
|
+
body: '{bad-json',
|
|
51
|
+
}))
|
|
52
|
+
|
|
53
|
+
console.log(JSON.stringify({
|
|
54
|
+
status: response.status,
|
|
55
|
+
payload: await response.json(),
|
|
56
|
+
}))
|
|
57
|
+
`, { prefix: 'swarmclaw-logs-route-' })
|
|
58
|
+
|
|
59
|
+
assert.equal(output.status, 400)
|
|
60
|
+
assert.equal(output.payload.error, 'Invalid or missing request body')
|
|
61
|
+
})
|
|
@@ -1,10 +1,25 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server'
|
|
2
2
|
import fs from 'fs'
|
|
3
|
+
import { z } from 'zod'
|
|
4
|
+
|
|
3
5
|
import { APP_LOG_PATH } from '@/lib/server/data-dir'
|
|
6
|
+
import { log } from '@/lib/server/logger'
|
|
7
|
+
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
4
8
|
|
|
5
9
|
/** Max bytes to read from the tail of the log file (256 KB). */
|
|
6
10
|
const TAIL_BYTES = 256 * 1024
|
|
7
11
|
|
|
12
|
+
const ClientLogSchema = z.object({
|
|
13
|
+
source: z.string().trim().min(1).max(120).optional().default('client'),
|
|
14
|
+
message: z.string().trim().min(1).max(1000),
|
|
15
|
+
stack: z.string().max(8000).optional(),
|
|
16
|
+
componentStack: z.string().max(8000).optional(),
|
|
17
|
+
digest: z.string().max(200).optional(),
|
|
18
|
+
url: z.string().max(2000).optional(),
|
|
19
|
+
pathname: z.string().max(1000).optional(),
|
|
20
|
+
userAgent: z.string().max(1000).optional(),
|
|
21
|
+
})
|
|
22
|
+
|
|
8
23
|
export async function GET(req: Request) {
|
|
9
24
|
const { searchParams } = new URL(req.url)
|
|
10
25
|
const lines = parseInt(searchParams.get('lines') || '200', 10)
|
|
@@ -76,6 +91,26 @@ export async function DELETE() {
|
|
|
76
91
|
}
|
|
77
92
|
}
|
|
78
93
|
|
|
94
|
+
export async function POST(req: Request) {
|
|
95
|
+
const { data: body, error } = await safeParseBody(req, ClientLogSchema)
|
|
96
|
+
if (error) return error
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
log.error(body.source, body.message, {
|
|
100
|
+
stack: body.stack,
|
|
101
|
+
componentStack: body.componentStack,
|
|
102
|
+
digest: body.digest,
|
|
103
|
+
url: body.url,
|
|
104
|
+
pathname: body.pathname,
|
|
105
|
+
userAgent: body.userAgent,
|
|
106
|
+
})
|
|
107
|
+
return NextResponse.json({ ok: true })
|
|
108
|
+
} catch (err: unknown) {
|
|
109
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
110
|
+
return NextResponse.json({ error: message }, { status: 500 })
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
79
114
|
function parseLine(line: string) {
|
|
80
115
|
// Format: [2026-02-19T17:06:00.000Z] [INFO] [tag] message | data
|
|
81
116
|
const match = line.match(/^\[([^\]]+)\]\s+\[(\w+)\]\s+\[([^\]]+)\]\s+(.*)$/)
|
|
@@ -4,7 +4,7 @@ import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
|
4
4
|
export const dynamic = 'force-dynamic'
|
|
5
5
|
|
|
6
6
|
const VALID_ACTIONS = new Set(['push', 'pull', 'both'])
|
|
7
|
-
const VALID_TYPES: SyncType[] = ['memory', 'workspace', 'schedules', 'credentials', '
|
|
7
|
+
const VALID_TYPES: SyncType[] = ['memory', 'workspace', 'schedules', 'credentials', 'extensions']
|
|
8
8
|
|
|
9
9
|
export async function POST(req: Request) {
|
|
10
10
|
try {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { getChannels } from '@/lib/swarmfeed-client'
|
|
3
|
+
|
|
4
|
+
export const dynamic = 'force-dynamic'
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
try {
|
|
8
|
+
const channels = await getChannels()
|
|
9
|
+
return NextResponse.json({ channels })
|
|
10
|
+
} catch (err: unknown) {
|
|
11
|
+
const message = err instanceof Error ? err.message : 'Failed to fetch channels'
|
|
12
|
+
return NextResponse.json({ error: message }, { status: 502 })
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { createPost, getFeed } from '@/lib/swarmfeed-client'
|
|
3
|
+
import { getAgent } from '@/lib/server/agents/agent-repository'
|
|
4
|
+
import { safeParseBody } from '@/lib/server/safe-parse-body'
|
|
5
|
+
import type { Agent } from '@/types'
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic'
|
|
8
|
+
|
|
9
|
+
export async function GET(req: Request) {
|
|
10
|
+
const { searchParams } = new URL(req.url)
|
|
11
|
+
const cursor = searchParams.get('cursor') || undefined
|
|
12
|
+
const limitStr = searchParams.get('limit')
|
|
13
|
+
const limit = limitStr ? Math.max(1, Math.min(100, Number(limitStr) || 20)) : undefined
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const result = await getFeed('for_you', { cursor, limit })
|
|
17
|
+
return NextResponse.json(result)
|
|
18
|
+
} catch (err: unknown) {
|
|
19
|
+
const message = err instanceof Error ? err.message : 'Failed to fetch posts'
|
|
20
|
+
return NextResponse.json({ error: message }, { status: 502 })
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function POST(req: Request) {
|
|
25
|
+
const { data: body, error } = await safeParseBody<{
|
|
26
|
+
agentId?: string
|
|
27
|
+
content?: string
|
|
28
|
+
channelId?: string
|
|
29
|
+
parentId?: string
|
|
30
|
+
}>(req)
|
|
31
|
+
if (error) return error
|
|
32
|
+
|
|
33
|
+
if (!body?.agentId || typeof body.agentId !== 'string') {
|
|
34
|
+
return NextResponse.json({ error: 'agentId is required' }, { status: 400 })
|
|
35
|
+
}
|
|
36
|
+
if (!body.content || typeof body.content !== 'string' || !body.content.trim()) {
|
|
37
|
+
return NextResponse.json({ error: 'content is required' }, { status: 400 })
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Look up the agent's SwarmFeed API key
|
|
41
|
+
const agent = getAgent(body.agentId) as Agent | undefined
|
|
42
|
+
if (!agent?.swarmfeedApiKey) {
|
|
43
|
+
return NextResponse.json(
|
|
44
|
+
{ error: 'Agent not registered on SwarmFeed. Enable SwarmFeed in agent settings first.' },
|
|
45
|
+
{ status: 400 },
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const post = await createPost(agent.swarmfeedApiKey, {
|
|
51
|
+
content: body.content.trim(),
|
|
52
|
+
channelId: typeof body.channelId === 'string' ? body.channelId : undefined,
|
|
53
|
+
parentId: typeof body.parentId === 'string' ? body.parentId : undefined,
|
|
54
|
+
})
|
|
55
|
+
return NextResponse.json(post)
|
|
56
|
+
} catch (err: unknown) {
|
|
57
|
+
const message = err instanceof Error ? err.message : 'Failed to create post'
|
|
58
|
+
return NextResponse.json({ error: message }, { status: 502 })
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { getFeed } from '@/lib/swarmfeed-client'
|
|
3
|
+
import { loadAgents } from '@/lib/server/storage'
|
|
4
|
+
import type { FeedType } from '@/types/swarmfeed'
|
|
5
|
+
import type { Agent } from '@/types'
|
|
6
|
+
|
|
7
|
+
export const dynamic = 'force-dynamic'
|
|
8
|
+
|
|
9
|
+
const VALID_FEED_TYPES = new Set<FeedType>(['for_you', 'following', 'channel', 'trending'])
|
|
10
|
+
|
|
11
|
+
export async function GET(req: Request) {
|
|
12
|
+
const { searchParams } = new URL(req.url)
|
|
13
|
+
const type = (searchParams.get('type') || 'for_you') as FeedType
|
|
14
|
+
if (!VALID_FEED_TYPES.has(type)) {
|
|
15
|
+
return NextResponse.json({ error: 'Invalid feed type' }, { status: 400 })
|
|
16
|
+
}
|
|
17
|
+
const channelId = searchParams.get('channelId') || undefined
|
|
18
|
+
const cursor = searchParams.get('cursor') || undefined
|
|
19
|
+
const limitStr = searchParams.get('limit')
|
|
20
|
+
const limit = limitStr ? Math.max(1, Math.min(100, Number(limitStr) || 20)) : undefined
|
|
21
|
+
|
|
22
|
+
// For authenticated feeds (following), find the first enabled agent's API key
|
|
23
|
+
let agentApiKey: string | undefined
|
|
24
|
+
if (type === 'following') {
|
|
25
|
+
const agents = Object.values(loadAgents()) as Agent[]
|
|
26
|
+
const feedAgent = agents.find((a) => a.swarmfeedEnabled && a.swarmfeedApiKey)
|
|
27
|
+
agentApiKey = feedAgent?.swarmfeedApiKey ?? undefined
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const result = await getFeed(type, { channelId, cursor, limit }, agentApiKey)
|
|
32
|
+
return NextResponse.json(result)
|
|
33
|
+
} catch (err: unknown) {
|
|
34
|
+
const message = err instanceof Error ? err.message : 'Failed to fetch feed'
|
|
35
|
+
return NextResponse.json({ error: message }, { status: 502 })
|
|
36
|
+
}
|
|
37
|
+
}
|