@swarmclawai/swarmclaw 0.4.0 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +13 -2
  2. package/next.config.ts +8 -0
  3. package/package.json +2 -1
  4. package/src/app/api/agents/[id]/route.ts +20 -21
  5. package/src/app/api/agents/[id]/thread/route.ts +2 -2
  6. package/src/app/api/agents/route.ts +3 -2
  7. package/src/app/api/clawhub/install/route.ts +2 -2
  8. package/src/app/api/connectors/[id]/route.ts +10 -3
  9. package/src/app/api/connectors/[id]/webhook/route.ts +99 -0
  10. package/src/app/api/connectors/route.ts +6 -3
  11. package/src/app/api/credentials/[id]/route.ts +2 -1
  12. package/src/app/api/credentials/route.ts +2 -2
  13. package/src/app/api/documents/route.ts +2 -2
  14. package/src/app/api/files/serve/route.ts +8 -0
  15. package/src/app/api/knowledge/[id]/route.ts +5 -4
  16. package/src/app/api/knowledge/upload/route.ts +2 -2
  17. package/src/app/api/mcp-servers/[id]/route.ts +11 -14
  18. package/src/app/api/mcp-servers/[id]/test/route.ts +2 -1
  19. package/src/app/api/mcp-servers/[id]/tools/route.ts +2 -1
  20. package/src/app/api/mcp-servers/route.ts +2 -2
  21. package/src/app/api/memory/[id]/route.ts +9 -8
  22. package/src/app/api/memory/route.ts +2 -2
  23. package/src/app/api/memory-images/[filename]/route.ts +2 -1
  24. package/src/app/api/openclaw/directory/route.ts +26 -0
  25. package/src/app/api/openclaw/discover/route.ts +61 -0
  26. package/src/app/api/openclaw/sync/route.ts +30 -0
  27. package/src/app/api/orchestrator/run/route.ts +2 -2
  28. package/src/app/api/projects/[id]/route.ts +55 -0
  29. package/src/app/api/projects/route.ts +27 -0
  30. package/src/app/api/providers/[id]/models/route.ts +2 -1
  31. package/src/app/api/providers/[id]/route.ts +13 -15
  32. package/src/app/api/providers/route.ts +2 -2
  33. package/src/app/api/schedules/[id]/route.ts +16 -18
  34. package/src/app/api/schedules/[id]/run/route.ts +4 -3
  35. package/src/app/api/schedules/route.ts +2 -2
  36. package/src/app/api/secrets/[id]/route.ts +16 -17
  37. package/src/app/api/secrets/route.ts +2 -2
  38. package/src/app/api/sessions/[id]/clear/route.ts +2 -1
  39. package/src/app/api/sessions/[id]/deploy/route.ts +2 -1
  40. package/src/app/api/sessions/[id]/devserver/route.ts +2 -1
  41. package/src/app/api/sessions/[id]/messages/route.ts +2 -1
  42. package/src/app/api/sessions/[id]/retry/route.ts +2 -1
  43. package/src/app/api/sessions/[id]/route.ts +2 -1
  44. package/src/app/api/sessions/route.ts +2 -2
  45. package/src/app/api/skills/[id]/route.ts +23 -21
  46. package/src/app/api/skills/import/route.ts +2 -2
  47. package/src/app/api/skills/route.ts +2 -2
  48. package/src/app/api/tasks/[id]/approve/route.ts +2 -1
  49. package/src/app/api/tasks/[id]/route.ts +6 -5
  50. package/src/app/api/tasks/route.ts +2 -2
  51. package/src/app/api/tts/stream/route.ts +48 -0
  52. package/src/app/api/upload/route.ts +2 -2
  53. package/src/app/api/uploads/[filename]/route.ts +4 -1
  54. package/src/app/api/webhooks/[id]/route.ts +29 -31
  55. package/src/app/api/webhooks/route.ts +2 -2
  56. package/src/app/page.tsx +3 -24
  57. package/src/cli/index.js +28 -0
  58. package/src/cli/index.ts +1 -1
  59. package/src/cli/spec.js +2 -0
  60. package/src/components/agents/agent-list.tsx +3 -1
  61. package/src/components/agents/agent-sheet.tsx +116 -14
  62. package/src/components/chat/chat-area.tsx +27 -4
  63. package/src/components/chat/chat-header.tsx +141 -29
  64. package/src/components/chat/tool-call-bubble.tsx +9 -3
  65. package/src/components/chat/voice-overlay.tsx +80 -0
  66. package/src/components/connectors/connector-list.tsx +6 -2
  67. package/src/components/connectors/connector-sheet.tsx +31 -7
  68. package/src/components/layout/app-layout.tsx +47 -25
  69. package/src/components/projects/project-list.tsx +122 -0
  70. package/src/components/projects/project-sheet.tsx +135 -0
  71. package/src/components/schedules/schedule-list.tsx +3 -1
  72. package/src/components/sessions/new-session-sheet.tsx +6 -6
  73. package/src/components/sessions/session-card.tsx +1 -1
  74. package/src/components/sessions/session-list.tsx +7 -7
  75. package/src/components/shared/connector-platform-icon.tsx +4 -0
  76. package/src/components/shared/settings/section-heartbeat.tsx +1 -1
  77. package/src/components/shared/settings/section-orchestrator.tsx +1 -2
  78. package/src/components/shared/settings/section-web-search.tsx +56 -0
  79. package/src/components/shared/settings/settings-page.tsx +73 -0
  80. package/src/components/skills/skill-list.tsx +2 -1
  81. package/src/components/tasks/task-list.tsx +5 -2
  82. package/src/hooks/use-continuous-speech.ts +144 -0
  83. package/src/hooks/use-view-router.ts +52 -0
  84. package/src/hooks/use-voice-conversation.ts +80 -0
  85. package/src/lib/id.ts +6 -0
  86. package/src/lib/projects.ts +13 -0
  87. package/src/lib/provider-sets.ts +5 -0
  88. package/src/lib/providers/anthropic.ts +14 -1
  89. package/src/lib/providers/index.ts +6 -0
  90. package/src/lib/providers/ollama.ts +9 -1
  91. package/src/lib/providers/openai.ts +9 -1
  92. package/src/lib/providers/openclaw.ts +11 -0
  93. package/src/lib/server/api-routes.test.ts +5 -6
  94. package/src/lib/server/build-llm.ts +17 -4
  95. package/src/lib/server/chat-execution.ts +38 -4
  96. package/src/lib/server/collection-helpers.ts +54 -0
  97. package/src/lib/server/connectors/bluebubbles.test.ts +208 -0
  98. package/src/lib/server/connectors/bluebubbles.ts +357 -0
  99. package/src/lib/server/connectors/connector-routing.test.ts +1 -1
  100. package/src/lib/server/connectors/googlechat.ts +46 -7
  101. package/src/lib/server/connectors/manager.ts +392 -3
  102. package/src/lib/server/connectors/media.ts +2 -2
  103. package/src/lib/server/connectors/openclaw.ts +64 -0
  104. package/src/lib/server/connectors/pairing.test.ts +99 -0
  105. package/src/lib/server/connectors/pairing.ts +256 -0
  106. package/src/lib/server/connectors/signal.ts +1 -0
  107. package/src/lib/server/connectors/teams.ts +5 -5
  108. package/src/lib/server/connectors/types.ts +10 -0
  109. package/src/lib/server/execution-log.ts +3 -3
  110. package/src/lib/server/heartbeat-service.ts +1 -1
  111. package/src/lib/server/knowledge-db.test.ts +2 -33
  112. package/src/lib/server/main-agent-loop.ts +6 -6
  113. package/src/lib/server/memory-db.ts +6 -6
  114. package/src/lib/server/openclaw-approvals.ts +105 -0
  115. package/src/lib/server/openclaw-sync.ts +496 -0
  116. package/src/lib/server/orchestrator-lg.ts +30 -9
  117. package/src/lib/server/orchestrator.ts +4 -4
  118. package/src/lib/server/process-manager.ts +2 -2
  119. package/src/lib/server/queue.ts +22 -10
  120. package/src/lib/server/scheduler.ts +2 -2
  121. package/src/lib/server/session-mailbox.ts +2 -2
  122. package/src/lib/server/session-run-manager.ts +2 -2
  123. package/src/lib/server/session-tools/connector.ts +51 -4
  124. package/src/lib/server/session-tools/crud.ts +3 -3
  125. package/src/lib/server/session-tools/delegate.ts +3 -3
  126. package/src/lib/server/session-tools/file.ts +176 -3
  127. package/src/lib/server/session-tools/index.ts +2 -0
  128. package/src/lib/server/session-tools/memory.ts +2 -2
  129. package/src/lib/server/session-tools/openclaw-nodes.ts +112 -0
  130. package/src/lib/server/session-tools/sandbox.ts +33 -0
  131. package/src/lib/server/session-tools/search-providers.ts +270 -0
  132. package/src/lib/server/session-tools/session-info.ts +2 -2
  133. package/src/lib/server/session-tools/web.ts +47 -66
  134. package/src/lib/server/storage.ts +12 -0
  135. package/src/lib/server/stream-agent-chat.ts +29 -0
  136. package/src/lib/server/task-result.test.ts +44 -0
  137. package/src/lib/server/task-result.ts +14 -0
  138. package/src/lib/tool-definitions.ts +5 -3
  139. package/src/lib/tts-stream.ts +130 -0
  140. package/src/lib/view-routes.ts +28 -0
  141. package/src/proxy.ts +3 -0
  142. package/src/stores/use-app-store.ts +28 -1
  143. package/src/stores/use-chat-store.ts +9 -1
  144. package/src/types/index.ts +27 -2
package/README.md CHANGED
@@ -36,7 +36,7 @@ Inspired by [OpenClaw](https://github.com/openclaw).
36
36
  - **Loop Runtime Controls** — Switch between bounded and ongoing loops with configurable step caps, runtime guards, heartbeat cadence, and timeout budgets
37
37
  - **Session Run Queue** — Per-session queued runs with followup/steer/collect modes, collect coalescing for bursty inputs, and run-state APIs
38
38
  - **Voice Settings** — Per-instance ElevenLabs API key + voice ID for TTS replies, plus configurable speech recognition language for chat input
39
- - **Chat Connectors** — Bridge agents to Discord, Slack, Telegram, and WhatsApp with media-aware inbound handling
39
+ - **Chat Connectors** — Bridge agents to Discord, Slack, Telegram, WhatsApp, BlueBubbles (iMessage), Signal, Microsoft Teams, Google Chat, Matrix, and OpenClaw with media-aware inbound handling
40
40
  - **Skills System** — Discover local skills, import skills from URL, and load OpenClaw `SKILL.md` files (frontmatter-compatible)
41
41
  - **Execution Logging** — Structured audit trail for triggers, tool calls, file ops, commits, and errors in a dedicated `logs.db`
42
42
  - **Context Management** — Auto-compaction of conversation history when approaching context limits, with manual `context_status` and `context_summarize` tools for agents
@@ -229,6 +229,12 @@ Bridge any agent to a chat platform:
229
229
  | Slack | @slack/bolt | Bot token + app token (Socket Mode) |
230
230
  | Telegram | grammy | Bot token from @BotFather |
231
231
  | WhatsApp | baileys | QR code pairing (shown in browser) |
232
+ | BlueBubbles | Custom webhook bridge | Server URL + password/webhook secret |
233
+ | Signal | signal-cli | `signal-cli` binary + linked phone |
234
+ | Microsoft Teams | botbuilder | Bot Framework credentials + webhook ingress |
235
+ | Google Chat | googleapis | Service account + webhook ingress |
236
+ | Matrix | matrix-bot-sdk | Homeserver URL + access token |
237
+ | OpenClaw | gateway protocol | OpenClaw connector credentials |
232
238
 
233
239
  Connector sessions preserve attachment visibility in chat context:
234
240
  - WhatsApp media is decoded and persisted to `/api/uploads/...` when possible
@@ -237,7 +243,12 @@ Connector sessions preserve attachment visibility in chat context:
237
243
 
238
244
  Agents automatically suppress replies to simple acknowledgments ("ok", "thanks", thumbs-up, etc.) via a `NO_MESSAGE` response — conversations feel natural without a forced reply to every message. This is handled at the connector layer, so agents can return `NO_MESSAGE` as their response content and the platform won't deliver anything to the channel.
239
245
 
240
- For proactive outreach, `connector_message_tool` supports text plus optional `imageUrl` / `fileUrl` / `mediaPath` (local file path) payloads. All four platforms (WhatsApp, Discord, Slack, Telegram) support local file sending via `mediaPath` with auto-detected MIME types.
246
+ For proactive outreach, `connector_message_tool` supports text plus optional `imageUrl` / `fileUrl` / `mediaPath` (local file path) payloads. WhatsApp, Discord, Slack, and Telegram support local file sending via `mediaPath` with auto-detected MIME types.
247
+
248
+ Connector ingress now also supports optional pairing/allowlist policy:
249
+ - `dmPolicy: allowlist` blocks unknown senders until approved
250
+ - `/pair` flow lets approved admins generate and approve pairing codes
251
+ - `/think` command can set connector thread thinking level (`low`, `medium`, `high`)
241
252
 
242
253
  ## Agent Tools
243
254
 
package/next.config.ts CHANGED
@@ -30,6 +30,14 @@ const nextConfig: NextConfig = {
30
30
  '127.0.0.1',
31
31
  '0.0.0.0',
32
32
  ],
33
+ async rewrites() {
34
+ return [
35
+ {
36
+ source: '/:view(agents|schedules|memory|tasks|secrets|providers|skills|connectors|webhooks|mcp-servers|knowledge|plugins|usage|runs|logs|settings|projects)',
37
+ destination: '/',
38
+ },
39
+ ]
40
+ },
33
41
  };
34
42
 
35
43
  export default nextConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "0.4.0",
3
+ "version": "0.4.5",
4
4
  "description": "Self-hosted AI agent orchestration dashboard — manage LLM providers, orchestrate agent swarms, schedule tasks, and bridge agents to chat platforms.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -69,6 +69,7 @@
69
69
  "cron-parser": "^5.5.0",
70
70
  "cronstrue": "^3.12.0",
71
71
  "discord.js": "^14.25.1",
72
+ "exceljs": "^4.4.0",
72
73
  "grammy": "^1.40.0",
73
74
  "highlight.js": "^11.11.1",
74
75
  "lucide-react": "^0.574.0",
@@ -1,33 +1,32 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadAgents, saveAgents, deleteAgent } from '@/lib/server/storage'
3
3
  import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
4
- import { notify } from '@/lib/server/ws-hub'
4
+ import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
5
+
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ const ops: CollectionOps<any> = { load: loadAgents, save: saveAgents, deleteFn: deleteAgent, topic: 'agents' }
5
8
 
6
9
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
7
10
  const { id } = await params
8
11
  const body = await req.json()
9
- const agents = loadAgents()
10
- if (!agents[id]) return new NextResponse(null, { status: 404 })
11
-
12
- Object.assign(agents[id], body, { updatedAt: Date.now() })
13
- if (body.apiEndpoint !== undefined) {
14
- agents[id].apiEndpoint = normalizeProviderEndpoint(
15
- body.provider || agents[id].provider,
16
- body.apiEndpoint,
17
- )
18
- }
19
- delete (agents[id] as Record<string, unknown>).id // prevent id overwrite
20
- agents[id].id = id
21
- saveAgents(agents)
22
- notify('agents')
23
- return NextResponse.json(agents[id])
12
+ const result = mutateItem(ops, id, (agent) => {
13
+ Object.assign(agent, body, { updatedAt: Date.now() })
14
+ if (body.apiEndpoint !== undefined) {
15
+ agent.apiEndpoint = normalizeProviderEndpoint(
16
+ body.provider || agent.provider,
17
+ body.apiEndpoint,
18
+ )
19
+ }
20
+ delete (agent as Record<string, unknown>).id
21
+ agent.id = id
22
+ return agent
23
+ })
24
+ if (!result) return notFound()
25
+ return NextResponse.json(result)
24
26
  }
25
27
 
26
28
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
27
29
  const { id } = await params
28
- const agents = loadAgents()
29
- if (!agents[id]) return new NextResponse(null, { status: 404 })
30
- deleteAgent(id)
31
- notify('agents')
32
- return NextResponse.json('ok')
30
+ if (!deleteItem(ops, id)) return notFound()
31
+ return NextResponse.json({ ok: true })
33
32
  }
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
- import crypto from 'crypto'
2
+ import { genId } from '@/lib/id'
3
3
  import { loadAgents, saveAgents, loadSessions, saveSessions } from '@/lib/server/storage'
4
4
  import { WORKSPACE_DIR } from '@/lib/server/data-dir'
5
5
 
@@ -32,7 +32,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
32
32
  }
33
33
 
34
34
  // Create a new thread session
35
- const sessionId = `agent-thread-${agentId}-${crypto.randomBytes(4).toString('hex')}`
35
+ const sessionId = `agent-thread-${agentId}-${genId()}`
36
36
  const now = Date.now()
37
37
  const session = {
38
38
  id: sessionId,
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
- import crypto from 'crypto'
2
+ import { genId } from '@/lib/id'
3
3
  import { loadAgents, saveAgents } from '@/lib/server/storage'
4
4
  import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
5
5
  import { notify } from '@/lib/server/ws-hub'
@@ -12,7 +12,7 @@ export async function GET(_req: Request) {
12
12
 
13
13
  export async function POST(req: Request) {
14
14
  const body = await req.json()
15
- const id = crypto.randomBytes(4).toString('hex')
15
+ const id = genId()
16
16
  const now = Date.now()
17
17
  const agents = loadAgents()
18
18
  agents[id] = {
@@ -28,6 +28,7 @@ export async function POST(req: Request) {
28
28
  subAgentIds: body.subAgentIds || [],
29
29
  tools: body.tools || [],
30
30
  capabilities: body.capabilities || [],
31
+ thinkingLevel: body.thinkingLevel || undefined,
31
32
  createdAt: now,
32
33
  updatedAt: now,
33
34
  }
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
- import crypto from 'crypto'
2
+ import { genId } from '@/lib/id'
3
3
  import { loadSkills, saveSkills } from '@/lib/server/storage'
4
4
  import { fetchSkillContent } from '@/lib/server/clawhub-client'
5
5
 
@@ -20,7 +20,7 @@ export async function POST(req: Request) {
20
20
  }
21
21
 
22
22
  const skills = loadSkills()
23
- const id = crypto.randomBytes(4).toString('hex')
23
+ const id = genId()
24
24
  skills[id] = {
25
25
  id,
26
26
  name,
@@ -1,12 +1,13 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadConnectors, saveConnectors } from '@/lib/server/storage'
3
3
  import { notify } from '@/lib/server/ws-hub'
4
+ import { notFound } from '@/lib/server/collection-helpers'
4
5
 
5
6
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
6
7
  const { id } = await params
7
8
  const connectors = loadConnectors()
8
9
  const connector = connectors[id]
9
- if (!connector) return NextResponse.json({ error: 'Not found' }, { status: 404 })
10
+ if (!connector) return notFound()
10
11
 
11
12
  // Merge runtime status and QR code
12
13
  try {
@@ -26,7 +27,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
26
27
  const body = await req.json()
27
28
  const connectors = loadConnectors()
28
29
  const connector = connectors[id]
29
- if (!connector) return NextResponse.json({ error: 'Not found' }, { status: 404 })
30
+ if (!connector) return notFound()
30
31
 
31
32
  // Handle start/stop/repair actions — these modify connector state internally,
32
33
  // so re-read from storage after to avoid overwriting with stale data
@@ -68,7 +69,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
68
69
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
69
70
  const { id } = await params
70
71
  const connectors = loadConnectors()
71
- if (!connectors[id]) return NextResponse.json({ error: 'Not found' }, { status: 404 })
72
+ if (!connectors[id]) return notFound()
72
73
 
73
74
  // Stop if running
74
75
  try {
@@ -76,6 +77,12 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
76
77
  await stopConnector(id)
77
78
  } catch { /* ignore */ }
78
79
 
80
+ // Clear persisted pairing state when connector is deleted.
81
+ try {
82
+ const { clearConnectorPairingState } = await import('@/lib/server/connectors/pairing')
83
+ clearConnectorPairingState(id)
84
+ } catch { /* ignore */ }
85
+
79
86
  delete connectors[id]
80
87
  saveConnectors(connectors)
81
88
  notify('connectors')
@@ -0,0 +1,99 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadConnectors } from '@/lib/server/storage'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ function readSecret(req: Request): string {
7
+ const url = new URL(req.url)
8
+ return (
9
+ req.headers.get('x-connector-secret')
10
+ || url.searchParams.get('secret')
11
+ || ''
12
+ ).trim()
13
+ }
14
+
15
+ function parseWebhookBody(rawBody: string): Record<string, unknown> {
16
+ const trimmed = rawBody.trim()
17
+ if (!trimmed) return {}
18
+
19
+ try {
20
+ const parsed = JSON.parse(trimmed)
21
+ if (Array.isArray(parsed)) return { data: parsed }
22
+ return parsed && typeof parsed === 'object' ? parsed as Record<string, unknown> : {}
23
+ } catch {
24
+ // Fall back to URL-encoded payloads used by some webhook providers.
25
+ const params = new URLSearchParams(rawBody)
26
+ const nested = params.get('payload') || params.get('data') || params.get('message') || ''
27
+ if (nested) {
28
+ try {
29
+ const parsedNested = JSON.parse(nested)
30
+ if (Array.isArray(parsedNested)) return { data: parsedNested }
31
+ return parsedNested && typeof parsedNested === 'object'
32
+ ? parsedNested as Record<string, unknown>
33
+ : {}
34
+ } catch {
35
+ // Ignore malformed nested JSON and return flat map below.
36
+ }
37
+ }
38
+ const out: Record<string, unknown> = {}
39
+ for (const [key, value] of params.entries()) out[key] = value
40
+ return out
41
+ }
42
+ }
43
+
44
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
45
+ const { id } = await params
46
+ const connectors = loadConnectors()
47
+ const connector = connectors[id]
48
+ if (!connector) return NextResponse.json({ error: 'Connector not found' }, { status: 404 })
49
+
50
+ const requiredSecret = String(connector.config?.webhookSecret || '').trim()
51
+ if (requiredSecret && readSecret(req) !== requiredSecret) {
52
+ return NextResponse.json({ error: 'Invalid connector webhook secret' }, { status: 401 })
53
+ }
54
+
55
+ const rawBody = await req.text().catch(() => '')
56
+ const payload = parseWebhookBody(rawBody)
57
+
58
+ try {
59
+ if (connector.platform === 'teams') {
60
+ const handlerKey = `__swarmclaw_teams_handler_${connector.id}__`
61
+ const handler = (globalThis as any)[handlerKey]
62
+ if (typeof handler !== 'function') {
63
+ return NextResponse.json({ error: 'Teams connector is not running or not ready' }, { status: 409 })
64
+ }
65
+ await handler(payload)
66
+ return NextResponse.json({ ok: true })
67
+ }
68
+
69
+ if (connector.platform === 'googlechat') {
70
+ const handlerKey = `__swarmclaw_googlechat_handler_${connector.id}__`
71
+ const handler = (globalThis as any)[handlerKey]
72
+ if (typeof handler !== 'function') {
73
+ return NextResponse.json({ error: 'Google Chat connector is not running or not ready' }, { status: 409 })
74
+ }
75
+ const result = await handler(payload)
76
+ if (result && typeof result === 'object' && Object.keys(result).length > 0) {
77
+ return NextResponse.json(result)
78
+ }
79
+ return NextResponse.json({})
80
+ }
81
+
82
+ if (connector.platform === 'bluebubbles') {
83
+ const handlerKey = `__swarmclaw_bluebubbles_handler_${connector.id}__`
84
+ const handler = (globalThis as any)[handlerKey]
85
+ if (typeof handler !== 'function') {
86
+ return NextResponse.json({ error: 'BlueBubbles connector is not running or not ready' }, { status: 409 })
87
+ }
88
+ const result = await handler(payload)
89
+ if (result && typeof result === 'object' && Object.keys(result).length > 0) {
90
+ return NextResponse.json(result)
91
+ }
92
+ return NextResponse.json({})
93
+ }
94
+
95
+ return NextResponse.json({ error: `Platform "${connector.platform}" does not support connector webhook ingress.` }, { status: 400 })
96
+ } catch (err: any) {
97
+ return NextResponse.json({ error: err?.message || 'Webhook processing failed' }, { status: 500 })
98
+ }
99
+ }
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
- import crypto from 'crypto'
2
+ import { genId } from '@/lib/id'
3
3
  import { loadConnectors, saveConnectors } from '@/lib/server/storage'
4
4
  import { notify } from '@/lib/server/ws-hub'
5
5
  import type { Connector } from '@/types'
@@ -27,7 +27,7 @@ export async function GET(_req: Request) {
27
27
  export async function POST(req: Request) {
28
28
  const body = await req.json()
29
29
  const connectors = loadConnectors()
30
- const id = crypto.randomBytes(4).toString('hex')
30
+ const id = genId()
31
31
 
32
32
  const connector: Connector = {
33
33
  id,
@@ -48,7 +48,10 @@ export async function POST(req: Request) {
48
48
  notify('connectors')
49
49
 
50
50
  // Auto-start if connector has credentials (or is WhatsApp which uses QR)
51
- const hasCredentials = connector.platform === 'whatsapp' || connector.platform === 'openclaw' || !!connector.credentialId
51
+ const hasCredentials = connector.platform === 'whatsapp'
52
+ || connector.platform === 'openclaw'
53
+ || (connector.platform === 'bluebubbles' && (!!connector.credentialId || !!connector.config.password))
54
+ || !!connector.credentialId
52
55
  if (hasCredentials && body.autoStart !== false) {
53
56
  try {
54
57
  const { startConnector } = await import('@/lib/server/connectors/manager')
@@ -1,11 +1,12 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadCredentials, saveCredentials } from '@/lib/server/storage'
3
+ import { notFound } from '@/lib/server/collection-helpers'
3
4
 
4
5
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
5
6
  const { id: credId } = await params
6
7
  const creds = loadCredentials()
7
8
  if (!creds[credId]) {
8
- return new NextResponse(null, { status: 404 })
9
+ return notFound()
9
10
  }
10
11
  delete creds[credId]
11
12
  saveCredentials(creds)
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
- import crypto from 'crypto'
2
+ import { genId } from '@/lib/id'
3
3
  import { loadCredentials, saveCredentials, encryptKey } from '@/lib/server/storage'
4
4
  export const dynamic = 'force-dynamic'
5
5
 
@@ -18,7 +18,7 @@ export async function POST(req: Request) {
18
18
  if (!provider || !apiKey) {
19
19
  return NextResponse.json({ error: 'provider and apiKey are required' }, { status: 400 })
20
20
  }
21
- const id = 'cred_' + crypto.randomBytes(6).toString('hex')
21
+ const id = 'cred_' + genId(6)
22
22
  const creds = loadCredentials()
23
23
  creds[id] = {
24
24
  id,
@@ -1,4 +1,4 @@
1
- import crypto from 'crypto'
1
+ import { genId } from '@/lib/id'
2
2
  import { NextResponse } from 'next/server'
3
3
  import { loadDocuments, saveDocuments } from '@/lib/server/storage'
4
4
 
@@ -69,7 +69,7 @@ export async function POST(req: Request) {
69
69
  const body = await req.json().catch(() => ({}))
70
70
  const now = Date.now()
71
71
  const docs = loadDocuments()
72
- const id = body.id || crypto.randomBytes(6).toString('hex')
72
+ const id = body.id || genId(6)
73
73
  const fileName = body.fileName || body.filename || ''
74
74
  const title = body.title || fileName || 'Untitled Document'
75
75
  const content = typeof body.content === 'string' ? body.content : ''
@@ -21,6 +21,14 @@ const MIME_MAP: Record<string, string> = {
21
21
  '.jsx': 'text/plain',
22
22
  '.py': 'text/plain',
23
23
  '.sh': 'text/plain',
24
+ '.pdf': 'application/pdf',
25
+ '.csv': 'text/csv',
26
+ '.xml': 'application/xml',
27
+ '.zip': 'application/zip',
28
+ '.doc': 'application/msword',
29
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
30
+ '.xls': 'application/vnd.ms-excel',
31
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
24
32
  }
25
33
 
26
34
  const MAX_SIZE = 10 * 1024 * 1024 // 10MB
@@ -1,4 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
+ import { notFound } from '@/lib/server/collection-helpers'
2
3
  import { getMemoryDb } from '@/lib/server/memory-db'
3
4
 
4
5
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
@@ -6,7 +7,7 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
6
7
  const db = getMemoryDb()
7
8
  const entry = db.get(id)
8
9
  if (!entry || entry.category !== 'knowledge') {
9
- return new NextResponse(null, { status: 404 })
10
+ return notFound()
10
11
  }
11
12
  return NextResponse.json(entry)
12
13
  }
@@ -16,7 +17,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
16
17
  const db = getMemoryDb()
17
18
  const existing = db.get(id)
18
19
  if (!existing || existing.category !== 'knowledge') {
19
- return new NextResponse(null, { status: 404 })
20
+ return notFound()
20
21
  }
21
22
 
22
23
  const body = await req.json().catch(() => null)
@@ -44,7 +45,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
44
45
 
45
46
  const updated = db.update(id, updates)
46
47
  if (!updated) {
47
- return new NextResponse(null, { status: 404 })
48
+ return notFound()
48
49
  }
49
50
  return NextResponse.json(updated)
50
51
  }
@@ -54,7 +55,7 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
54
55
  const db = getMemoryDb()
55
56
  const existing = db.get(id)
56
57
  if (!existing || existing.category !== 'knowledge') {
57
- return new NextResponse(null, { status: 404 })
58
+ return notFound()
58
59
  }
59
60
  db.delete(id)
60
61
  return NextResponse.json({ deleted: id })
@@ -1,7 +1,7 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import fs from 'fs'
3
3
  import path from 'path'
4
- import crypto from 'crypto'
4
+ import { genId } from '@/lib/id'
5
5
  import { UPLOAD_DIR } from '@/lib/server/storage'
6
6
 
7
7
  const TEXT_EXTS = new Set([
@@ -40,7 +40,7 @@ export async function POST(req: Request) {
40
40
 
41
41
  // Save file to uploads
42
42
  if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true })
43
- const safeName = crypto.randomBytes(4).toString('hex') + '-' + filename.replace(/[^a-zA-Z0-9._-]/g, '_')
43
+ const safeName = genId() + '-' + filename.replace(/[^a-zA-Z0-9._-]/g, '_')
44
44
  const filePath = path.join(UPLOAD_DIR, safeName)
45
45
  fs.writeFileSync(filePath, buf)
46
46
 
@@ -1,32 +1,29 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadMcpServers, saveMcpServers, deleteMcpServer } from '@/lib/server/storage'
3
+ import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
4
+
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ const ops: CollectionOps<any> = { load: loadMcpServers, save: saveMcpServers, deleteFn: deleteMcpServer }
3
7
 
4
8
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
5
9
  const { id } = await params
6
10
  const servers = loadMcpServers()
7
- if (!servers[id]) return new NextResponse(null, { status: 404 })
11
+ if (!servers[id]) return notFound()
8
12
  return NextResponse.json(servers[id])
9
13
  }
10
14
 
11
15
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
12
16
  const { id } = await params
13
17
  const body = await req.json()
14
- const servers = loadMcpServers()
15
- if (!servers[id]) return new NextResponse(null, { status: 404 })
16
- servers[id] = {
17
- ...servers[id],
18
- ...body,
19
- id,
20
- updatedAt: Date.now(),
21
- }
22
- saveMcpServers(servers)
23
- return NextResponse.json(servers[id])
18
+ const result = mutateItem(ops, id, (server) => ({
19
+ ...server, ...body, id, updatedAt: Date.now(),
20
+ }))
21
+ if (!result) return notFound()
22
+ return NextResponse.json(result)
24
23
  }
25
24
 
26
25
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
27
26
  const { id } = await params
28
- const servers = loadMcpServers()
29
- if (!servers[id]) return new NextResponse(null, { status: 404 })
30
- deleteMcpServer(id)
27
+ if (!deleteItem(ops, id)) return notFound()
31
28
  return NextResponse.json({ deleted: id })
32
29
  }
@@ -1,12 +1,13 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadMcpServers } from '@/lib/server/storage'
3
+ import { notFound } from '@/lib/server/collection-helpers'
3
4
  import { connectMcpServer, mcpToolsToLangChain, disconnectMcpServer } from '@/lib/server/mcp-client'
4
5
 
5
6
  export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
6
7
  const { id } = await params
7
8
  const servers = loadMcpServers()
8
9
  const server = servers[id]
9
- if (!server) return new NextResponse(null, { status: 404 })
10
+ if (!server) return notFound()
10
11
 
11
12
  try {
12
13
  const { client, transport } = await connectMcpServer(server)
@@ -1,12 +1,13 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadMcpServers } from '@/lib/server/storage'
3
+ import { notFound } from '@/lib/server/collection-helpers'
3
4
  import { connectMcpServer, disconnectMcpServer } from '@/lib/server/mcp-client'
4
5
 
5
6
  export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
6
7
  const { id } = await params
7
8
  const servers = loadMcpServers()
8
9
  const config = servers[id]
9
- if (!config) return new NextResponse(null, { status: 404 })
10
+ if (!config) return notFound()
10
11
 
11
12
  let client: any
12
13
  let transport: any
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
- import crypto from 'crypto'
2
+ import { genId } from '@/lib/id'
3
3
  import { loadMcpServers, saveMcpServers } from '@/lib/server/storage'
4
4
  export const dynamic = 'force-dynamic'
5
5
 
@@ -11,7 +11,7 @@ export async function GET(_req: Request) {
11
11
  export async function POST(req: Request) {
12
12
  const body = await req.json()
13
13
  const servers = loadMcpServers()
14
- const id = crypto.randomBytes(4).toString('hex')
14
+ const id = genId()
15
15
  servers[id] = {
16
16
  id,
17
17
  name: body.name,
@@ -1,6 +1,7 @@
1
- import crypto from 'crypto'
1
+ import { genId } from '@/lib/id'
2
2
  import fs from 'fs'
3
3
  import { NextResponse } from 'next/server'
4
+ import { notFound } from '@/lib/server/collection-helpers'
4
5
  import { getMemoryDb, getMemoryLookupLimits, storeMemoryImageAsset, storeMemoryImageFromDataUrl } from '@/lib/server/memory-db'
5
6
  import { resolveLookupRequest } from '@/lib/server/memory-graph'
6
7
  import type { MemoryImage } from '@/types'
@@ -42,7 +43,7 @@ export async function GET(req: Request, { params }: { params: Promise<{ id: stri
42
43
 
43
44
  if (limits.maxDepth <= 0) {
44
45
  const entry = db.get(id)
45
- if (!entry) return new NextResponse(null, { status: 404 })
46
+ if (!entry) return notFound()
46
47
  if (envelope) {
47
48
  return NextResponse.json({
48
49
  entries: [entry],
@@ -55,7 +56,7 @@ export async function GET(req: Request, { params }: { params: Promise<{ id: stri
55
56
  }
56
57
 
57
58
  const result = db.getWithLinked(id, limits.maxDepth, limits.maxPerLookup, limits.maxLinkedExpansion)
58
- if (!result) return new NextResponse(null, { status: 404 })
59
+ if (!result) return notFound()
59
60
  if (envelope) return NextResponse.json(result)
60
61
  return NextResponse.json(result.entries)
61
62
  }
@@ -78,7 +79,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
78
79
  const updated = linkAction === 'link'
79
80
  ? db.link(id, targetIds, true)
80
81
  : db.unlink(id, targetIds, true)
81
- if (!updated) return new NextResponse(null, { status: 404 })
82
+ if (!updated) return notFound()
82
83
  return NextResponse.json(updated)
83
84
  }
84
85
 
@@ -90,7 +91,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
90
91
  image = null
91
92
  } else if (inputImageDataUrl) {
92
93
  try {
93
- image = await storeMemoryImageFromDataUrl(inputImageDataUrl, `${id}-${crypto.randomBytes(2).toString('hex')}`)
94
+ image = await storeMemoryImageFromDataUrl(inputImageDataUrl, `${id}-${genId(2)}`)
94
95
  } catch (err) {
95
96
  return NextResponse.json({ error: err instanceof Error ? err.message : 'Invalid image data URL' }, { status: 400 })
96
97
  }
@@ -99,7 +100,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
99
100
  return NextResponse.json({ error: `Image file not found: ${inputImagePath}` }, { status: 400 })
100
101
  }
101
102
  try {
102
- image = await storeMemoryImageAsset(inputImagePath, `${id}-${crypto.randomBytes(2).toString('hex')}`)
103
+ image = await storeMemoryImageAsset(inputImagePath, `${id}-${genId(2)}`)
103
104
  } catch (err) {
104
105
  return NextResponse.json({ error: err instanceof Error ? err.message : 'Failed to store memory image' }, { status: 400 })
105
106
  }
@@ -114,7 +115,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
114
115
  ? String((image as { path: string }).path)
115
116
  : (typeof body.imagePath === 'string' ? body.imagePath : undefined),
116
117
  })
117
- if (!entry) return new NextResponse(null, { status: 404 })
118
+ if (!entry) return notFound()
118
119
  return NextResponse.json(entry)
119
120
  }
120
121
 
@@ -122,5 +123,5 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
122
123
  const { id } = await params
123
124
  const db = getMemoryDb()
124
125
  db.delete(id)
125
- return NextResponse.json('ok')
126
+ return NextResponse.json({ ok: true })
126
127
  }