@swarmclawai/swarmclaw 0.6.6 → 0.6.7

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 (80) hide show
  1. package/README.md +57 -27
  2. package/package.json +6 -1
  3. package/src/app/api/agents/[id]/clone/route.ts +40 -0
  4. package/src/app/api/agents/route.ts +39 -14
  5. package/src/app/api/chatrooms/[id]/chat/route.ts +17 -1
  6. package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
  7. package/src/app/api/chatrooms/[id]/route.ts +19 -1
  8. package/src/app/api/chatrooms/route.ts +12 -2
  9. package/src/app/api/connectors/[id]/health/route.ts +64 -0
  10. package/src/app/api/connectors/route.ts +17 -2
  11. package/src/app/api/knowledge/route.ts +6 -1
  12. package/src/app/api/openclaw/doctor/route.ts +17 -0
  13. package/src/app/api/sessions/[id]/chat/route.ts +5 -1
  14. package/src/app/api/sessions/route.ts +11 -2
  15. package/src/app/api/tasks/[id]/route.ts +18 -13
  16. package/src/app/api/tasks/route.ts +20 -1
  17. package/src/app/api/usage/route.ts +16 -7
  18. package/src/cli/index.js +5 -0
  19. package/src/cli/index.ts +223 -39
  20. package/src/components/agents/agent-card.tsx +37 -6
  21. package/src/components/agents/agent-chat-list.tsx +78 -2
  22. package/src/components/agents/agent-sheet.tsx +79 -0
  23. package/src/components/auth/setup-wizard.tsx +268 -353
  24. package/src/components/chat/chat-area.tsx +22 -7
  25. package/src/components/chat/message-bubble.tsx +14 -14
  26. package/src/components/chat/message-list.tsx +1 -1
  27. package/src/components/chatrooms/chatroom-message.tsx +164 -22
  28. package/src/components/chatrooms/chatroom-sheet.tsx +288 -3
  29. package/src/components/chatrooms/chatroom-view.tsx +62 -17
  30. package/src/components/connectors/connector-health.tsx +120 -0
  31. package/src/components/connectors/connector-sheet.tsx +9 -0
  32. package/src/components/home/home-view.tsx +23 -2
  33. package/src/components/input/chat-input.tsx +8 -1
  34. package/src/components/layout/app-layout.tsx +17 -1
  35. package/src/components/schedules/schedule-list.tsx +55 -9
  36. package/src/components/schedules/schedule-sheet.tsx +134 -23
  37. package/src/components/shared/command-palette.tsx +237 -0
  38. package/src/components/shared/connector-platform-icon.tsx +1 -0
  39. package/src/components/tasks/task-card.tsx +22 -2
  40. package/src/components/tasks/task-sheet.tsx +91 -16
  41. package/src/components/usage/metrics-dashboard.tsx +13 -25
  42. package/src/hooks/use-swipe.ts +49 -0
  43. package/src/lib/providers/anthropic.ts +16 -2
  44. package/src/lib/providers/claude-cli.ts +7 -1
  45. package/src/lib/providers/index.ts +7 -0
  46. package/src/lib/providers/ollama.ts +16 -2
  47. package/src/lib/providers/openai.ts +7 -2
  48. package/src/lib/providers/openclaw.ts +6 -1
  49. package/src/lib/providers/provider-defaults.ts +7 -0
  50. package/src/lib/schedule-templates.ts +115 -0
  51. package/src/lib/server/alert-dispatch.ts +64 -0
  52. package/src/lib/server/chat-execution.ts +41 -1
  53. package/src/lib/server/chatroom-helpers.ts +22 -1
  54. package/src/lib/server/chatroom-routing.ts +65 -0
  55. package/src/lib/server/connectors/discord.ts +3 -0
  56. package/src/lib/server/connectors/email.ts +267 -0
  57. package/src/lib/server/connectors/manager.ts +159 -3
  58. package/src/lib/server/connectors/openclaw.ts +3 -0
  59. package/src/lib/server/connectors/slack.ts +6 -0
  60. package/src/lib/server/connectors/telegram.ts +18 -0
  61. package/src/lib/server/connectors/types.ts +2 -0
  62. package/src/lib/server/connectors/whatsapp.ts +9 -0
  63. package/src/lib/server/cost.ts +70 -0
  64. package/src/lib/server/create-notification.ts +2 -0
  65. package/src/lib/server/daemon-state.ts +124 -0
  66. package/src/lib/server/dag-validation.ts +115 -0
  67. package/src/lib/server/memory-db.ts +12 -7
  68. package/src/lib/server/openclaw-doctor.ts +48 -0
  69. package/src/lib/server/queue.ts +12 -0
  70. package/src/lib/server/session-run-manager.ts +22 -1
  71. package/src/lib/server/session-tools/index.ts +2 -0
  72. package/src/lib/server/session-tools/memory.ts +22 -3
  73. package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
  74. package/src/lib/server/storage.ts +120 -6
  75. package/src/lib/setup-defaults.ts +277 -0
  76. package/src/lib/validation/schemas.ts +69 -0
  77. package/src/stores/use-app-store.ts +7 -3
  78. package/src/stores/use-chatroom-store.ts +52 -2
  79. package/src/types/index.ts +38 -1
  80. package/tsconfig.json +2 -1
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  <img src="https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/public/branding/swarmclaw-org-avatar.png" alt="SwarmClaw lobster logo" width="120" />
9
9
  </p>
10
10
 
11
- Self-hosted AI agent orchestration dashboard. Manage multiple AI providers, orchestrate agent swarms, schedule tasks, and bridge agents to chat platforms — all from a single mobile-friendly interface.
11
+ The orchestration dashboard for OpenClaw. Manage a swarm of OpenClaws + 14 other AI providers, orchestrate LangGraph workflows, schedule tasks, and bridge agents to 10+ chat platforms — all from one self-hosted UI.
12
12
 
13
13
  Inspired by [OpenClaw](https://github.com/openclaw).
14
14
 
@@ -18,6 +18,44 @@ Inspired by [OpenClaw](https://github.com/openclaw).
18
18
  ![Agent Builder](public/screenshots/agents.png)
19
19
  ![Task Board](public/screenshots/tasks.png)
20
20
 
21
+ ## OpenClaw Integration
22
+
23
+ SwarmClaw was built for OpenClaw users who outgrew a single agent. Connect each SwarmClaw agent to a different OpenClaw gateway (one local, several remote) and manage the whole swarm from one UI.
24
+
25
+ SwarmClaw includes the `openclaw` CLI as a bundled dependency, so there is no separate OpenClaw CLI install step.
26
+
27
+ The OpenClaw Control Plane in SwarmClaw adds:
28
+ - Reload mode switching (`hot`, `hybrid`, `full`)
29
+ - Config issue detection and guided repair
30
+ - Remote history sync
31
+ - Live execution approval handling
32
+
33
+ The Agent Inspector Panel lets you edit OpenClaw files (`SOUL.md`, `IDENTITY.md`, `USER.md`), tune personality/system behavior, and manage OpenClaw-compatible skills. SwarmClaw also supports importing OpenClaw `SKILL.md` files from URL.
34
+
35
+ To connect an agent to an OpenClaw gateway:
36
+
37
+ 1. Create or edit an agent
38
+ 2. Toggle **OpenClaw Gateway** ON
39
+ 3. Enter the gateway URL (e.g. `http://192.168.1.50:18789` or `https://my-vps:18789`)
40
+ 4. Add a gateway token if authentication is enabled on the remote gateway
41
+ 5. Click **Connect** — approve the device in your gateway's dashboard if prompted, then **Retry Connection**
42
+
43
+ Each agent can point to a **different** OpenClaw gateway — one local, several remote. This is how you manage a **swarm of OpenClaws** from a single dashboard.
44
+
45
+ URLs without a protocol are auto-prefixed with `http://`. For remote gateways with TLS, use `https://` explicitly.
46
+
47
+ ## SwarmClaw ClawHub Skill
48
+
49
+ Use the `swarmclaw` ClawHub skill when you want an OpenClaw agent to operate your SwarmClaw control plane directly from chat: list agents, dispatch tasks, check sessions, run diagnostics, and coordinate multi-agent work.
50
+
51
+ Install it from ClawHub:
52
+
53
+ ```bash
54
+ clawhub install swarmclaw
55
+ ```
56
+
57
+ Skill source and runbook: [`swarmclaw/SKILL.md`](swarmclaw/SKILL.md).
58
+
21
59
  - Always use the access key authentication (generated on first run)
22
60
  - Never expose port 3456 without a reverse proxy + TLS
23
61
  - Review agent system prompts before giving them shell or browser tools
@@ -47,7 +85,7 @@ curl -fsSL https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/install.
47
85
  ```
48
86
 
49
87
  The installer resolves the latest stable release tag and installs that version by default.
50
- To pin a version: `SWARMCLAW_VERSION=v0.6.2 curl ... | bash`
88
+ To pin a version: `SWARMCLAW_VERSION=v0.6.6 curl ... | bash`
51
89
 
52
90
  Or run locally from the repo (friendly for non-technical users):
53
91
 
@@ -80,11 +118,16 @@ You can complete first-time setup from terminal:
80
118
  # Start the app (if not already running)
81
119
  npm run dev
82
120
 
83
- # In another terminal, run setup with your provider
121
+ # In another terminal, run interactive setup (walks you through provider selection)
122
+ node ./bin/swarmclaw.js setup init
123
+
124
+ # Or pass flags directly for non-interactive setup
84
125
  node ./bin/swarmclaw.js setup init --provider openai --api-key "$OPENAI_API_KEY"
85
126
  ```
86
127
 
87
128
  Notes:
129
+ - When run with no flags in a TTY, `setup init` enters interactive mode — pick providers, enter keys, name agents, and add multiple providers in one session.
130
+ - Use `--no-interactive` to force flag-only mode.
88
131
  - On a fresh instance, `setup init` can auto-discover and claim the first-run access key from `/api/auth`.
89
132
  - For existing installs, pass `--key <ACCESS_KEY>` (or set `SWARMCLAW_ACCESS_KEY`).
90
133
  - `setup init` performs provider validation, stores credentials, creates a starter agent, and marks setup complete.
@@ -94,22 +137,22 @@ Notes:
94
137
 
95
138
  After login, SwarmClaw opens a guided wizard designed for non-technical users:
96
139
 
97
- 1. Choose a provider: **OpenAI**, **Anthropic**, or **Ollama**
98
- 2. Add only required fields (API key and/or endpoint)
99
- 3. Click **Check Connection** for live validation before continuing
100
- 4. (Optional) click **Run System Check** for setup diagnostics
101
- 5. Create a starter assistant (advanced settings are optional)
140
+ 1. **Choose a provider** — Pick from all 11 supported providers (OpenAI, Anthropic, Google Gemini, DeepSeek, Groq, Together AI, Mistral, xAI, Fireworks, OpenClaw, Ollama)
141
+ 2. **Connect provider** — Enter only required fields (API key and/or endpoint), then click **Check Connection** for live validation
142
+ 3. **Create your agent** — Each provider gets a unique default name (e.g. Atlas for OpenAI, Claude for Anthropic, Bolt for Groq). Choose **Create & Add Another** to set up multiple providers, or **Create & Finish** to continue
143
+ 4. **Summary** Review all created agents and discover connectors (Discord, Slack, Telegram, WhatsApp)
102
144
 
103
145
  Notes:
146
+ - You can add multiple providers in a single wizard session — configured providers are dimmed and shown as chips.
104
147
  - Ollama checks can auto-suggest a model from the connected endpoint.
105
- - OpenClaw is configured per-agent via the **OpenClaw Gateway** toggle (not in the setup wizard).
106
- - You can skip setup and configure everything later in the sidebar.
148
+ - You can skip setup at any step and configure everything later in the sidebar.
107
149
 
108
150
  ## Features
109
151
 
110
152
  - **15 Built-in Providers** — Claude Code CLI, OpenAI Codex CLI, OpenCode CLI, Anthropic, OpenAI, Google Gemini, DeepSeek, Groq, Together AI, Mistral AI, xAI (Grok), Fireworks AI, Ollama, plus custom OpenAI-compatible endpoints
111
153
  - **OpenClaw Gateway** — Per-agent toggle to connect any agent to a local or remote OpenClaw gateway. Each agent gets its own gateway URL and token — run a swarm of OpenClaws from one dashboard. The `openclaw` CLI ships as a bundled dependency (no separate install needed)
112
154
  - **OpenClaw Control Plane** — Built-in gateway connection controls, reload mode switching (hot/hybrid/full), config issue detection/repair, remote history sync, and live execution approval handling
155
+ - **Gateway Watchdog** — Proactive gateway health monitoring with auto-repair via `openclaw doctor`, outbound ops alerts to Discord/Slack/custom webhooks, workspace backup/rollback/history tools for agents, and connector liveness detection
113
156
  - **Agent Builder** — Create agents with custom personalities (soul), system prompts, tools, and skills. AI-powered generation from a description
114
157
  - **Agent Inspector Panel** — Per-agent side panel for OpenClaw file editing (`SOUL.md`, `IDENTITY.md`, `USER.md`, etc.), guided personality editing, skill install/enable/remove, permission presets, sandbox env allowlist, and cron automations
115
158
  - **Agent Fleet Management** — Avatar seeds with generated avatars, running/approval fleet filters, soft-delete agent trash with restore/permanent delete, and approval counters in agent cards
@@ -222,22 +265,6 @@ src/
222
265
  | OpenClaw | Per-Agent Gateway | Toggle in agent editor connects to any OpenClaw gateway via the bundled CLI. |
223
266
  | Custom | API | Any OpenAI-compatible endpoint. Add via Providers sidebar. |
224
267
 
225
- ### OpenClaw
226
-
227
- [OpenClaw](https://github.com/openclaw/openclaw) is an open-source autonomous AI agent that runs on your own devices. SwarmClaw includes the `openclaw` CLI as a bundled dependency — no separate install needed.
228
-
229
- To connect an agent to an OpenClaw gateway:
230
-
231
- 1. Create or edit an agent
232
- 2. Toggle **OpenClaw Gateway** ON
233
- 3. Enter the gateway URL (e.g. `http://192.168.1.50:18789` or `https://my-vps:18789`)
234
- 4. Add a gateway token if authentication is enabled on the remote gateway
235
- 5. Click **Connect** — approve the device in your gateway's dashboard if prompted, then **Retry Connection**
236
-
237
- Each agent can point to a **different** OpenClaw gateway — one local, several remote. This is how you manage a **swarm of OpenClaws** from a single dashboard.
238
-
239
- URLs without a protocol are auto-prefixed with `http://`. For remote gateways with TLS, use `https://` explicitly.
240
-
241
268
  ## Chat Connectors
242
269
 
243
270
  Bridge any agent to a chat platform:
@@ -626,7 +653,10 @@ swarmclaw tasks create --title "Fix flaky CI test" --description "Stabilize retr
626
653
  # run setup diagnostics
627
654
  swarmclaw setup doctor
628
655
 
629
- # complete setup from CLI (example: OpenAI)
656
+ # interactive setup (walks through provider selection, supports multiple providers)
657
+ swarmclaw setup init
658
+
659
+ # or non-interactive setup with flags
630
660
  swarmclaw setup init --provider openai --api-key "$OPENAI_API_KEY"
631
661
 
632
662
  # run memory maintenance analysis
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "0.6.6",
3
+ "version": "0.6.7",
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": {
@@ -78,9 +78,12 @@
78
78
  "exceljs": "^4.4.0",
79
79
  "grammy": "^1.40.0",
80
80
  "highlight.js": "^11.11.1",
81
+ "imapflow": "^1.2.11",
81
82
  "lucide-react": "^0.574.0",
83
+ "mailparser": "^3.9.3",
82
84
  "next": "16.1.6",
83
85
  "next-themes": "^0.4.6",
86
+ "nodemailer": "^8.0.1",
84
87
  "openclaw": "^2026.2.26",
85
88
  "pdf-parse": "^2.4.5",
86
89
  "qrcode": "^1.5.4",
@@ -103,7 +106,9 @@
103
106
  "devDependencies": {
104
107
  "@tailwindcss/postcss": "^4",
105
108
  "@types/better-sqlite3": "^7.6.13",
109
+ "@types/mailparser": "^3.4.6",
106
110
  "@types/node": "^20",
111
+ "@types/nodemailer": "^7.0.11",
107
112
  "@types/qrcode": "^1.5.6",
108
113
  "@types/react": "^19",
109
114
  "@types/react-dom": "^19",
@@ -0,0 +1,40 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadAgents, saveAgents, logActivity } from '@/lib/server/storage'
3
+ import { notify } from '@/lib/server/ws-hub'
4
+
5
+ export async function POST(_req: Request, { params }: { params: Promise<{ id: string }> }) {
6
+ const { id } = await params
7
+ const agents = loadAgents({ includeTrashed: true })
8
+ const source = agents[id]
9
+ if (!source) {
10
+ return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
11
+ }
12
+
13
+ const newId = crypto.randomUUID()
14
+ const now = Date.now()
15
+
16
+ // Deep-copy the source agent, then override clone-specific fields
17
+ const cloned = JSON.parse(JSON.stringify(source)) as Record<string, unknown>
18
+ cloned.id = newId
19
+ cloned.name = `${source.name} (Copy)`
20
+ cloned.createdAt = now
21
+ cloned.updatedAt = now
22
+ cloned.totalCost = 0
23
+ cloned.lastUsedAt = undefined
24
+ cloned.threadSessionId = null
25
+ cloned.pinned = false
26
+ cloned.trashedAt = undefined
27
+
28
+ agents[newId] = cloned
29
+ saveAgents(agents)
30
+ logActivity({
31
+ entityType: 'agent',
32
+ entityId: newId,
33
+ action: 'created',
34
+ actor: 'user',
35
+ summary: `Agent cloned from "${source.name}": "${cloned.name}"`,
36
+ })
37
+ notify('agents')
38
+
39
+ return NextResponse.json(cloned)
40
+ }
@@ -3,32 +3,57 @@ import { genId } from '@/lib/id'
3
3
  import { loadAgents, saveAgents, logActivity } from '@/lib/server/storage'
4
4
  import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
5
5
  import { notify } from '@/lib/server/ws-hub'
6
+ import { getAgentMonthlySpend } from '@/lib/server/cost'
7
+ import { AgentCreateSchema, formatZodError } from '@/lib/validation/schemas'
8
+ import { z } from 'zod'
6
9
  export const dynamic = 'force-dynamic'
7
10
 
8
11
 
9
- export async function GET(_req: Request) {
10
- return NextResponse.json(loadAgents())
12
+ export async function GET(req: Request) {
13
+ const agents = loadAgents()
14
+ // Enrich agents that have a monthly budget with current spend
15
+ for (const agent of Object.values(agents)) {
16
+ if (typeof agent.monthlyBudget === 'number' && agent.monthlyBudget > 0) {
17
+ agent.monthlySpend = getAgentMonthlySpend(agent.id)
18
+ }
19
+ }
20
+
21
+ const { searchParams } = new URL(req.url)
22
+ const limitParam = searchParams.get('limit')
23
+ if (!limitParam) return NextResponse.json(agents)
24
+
25
+ const limit = Math.max(1, Number(limitParam) || 50)
26
+ const offset = Math.max(0, Number(searchParams.get('offset')) || 0)
27
+ const all = Object.values(agents).sort((a, b) => b.updatedAt - a.updatedAt)
28
+ const items = all.slice(offset, offset + limit)
29
+ return NextResponse.json({ items, total: all.length, hasMore: offset + limit < all.length })
11
30
  }
12
31
 
13
32
  export async function POST(req: Request) {
14
- const body = await req.json()
33
+ const raw = await req.json()
34
+ const parsed = AgentCreateSchema.safeParse(raw)
35
+ if (!parsed.success) {
36
+ return NextResponse.json(formatZodError(parsed.error as z.ZodError), { status: 400 })
37
+ }
38
+ const body = parsed.data
15
39
  const id = genId()
16
40
  const now = Date.now()
17
41
  const agents = loadAgents()
18
42
  agents[id] = {
19
43
  id,
20
- name: body.name || 'Unnamed Agent',
21
- description: body.description || '',
22
- systemPrompt: body.systemPrompt || '',
23
- provider: body.provider || 'claude-cli',
24
- model: body.model || '',
25
- credentialId: body.credentialId || null,
26
- apiEndpoint: normalizeProviderEndpoint(body.provider || 'claude-cli', body.apiEndpoint || null),
27
- isOrchestrator: body.isOrchestrator || false,
28
- subAgentIds: body.subAgentIds || [],
29
- tools: body.tools || [],
30
- capabilities: body.capabilities || [],
44
+ name: body.name,
45
+ description: body.description,
46
+ systemPrompt: body.systemPrompt,
47
+ provider: body.provider,
48
+ model: body.model,
49
+ credentialId: body.credentialId,
50
+ apiEndpoint: normalizeProviderEndpoint(body.provider, body.apiEndpoint || null),
51
+ isOrchestrator: body.isOrchestrator,
52
+ subAgentIds: body.subAgentIds,
53
+ tools: body.tools,
54
+ capabilities: body.capabilities,
31
55
  thinkingLevel: body.thinkingLevel || undefined,
56
+ soul: body.soul || undefined,
32
57
  createdAt: now,
33
58
  updatedAt: now,
34
59
  }
@@ -13,8 +13,10 @@ import {
13
13
  buildSyntheticSession,
14
14
  buildAgentSystemPromptForChatroom,
15
15
  buildHistoryForAgent,
16
+ isMuted,
16
17
  } from '@/lib/server/chatroom-helpers'
17
18
  import { filterHealthyChatroomAgents } from '@/lib/server/chatroom-health'
19
+ import { evaluateRoutingRules } from '@/lib/server/chatroom-routing'
18
20
  import { markProviderFailure, markProviderSuccess } from '@/lib/server/provider-health'
19
21
  import type { Chatroom, ChatroomMessage, Agent } from '@/types'
20
22
 
@@ -48,7 +50,12 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
48
50
  // Persist incoming message
49
51
  const senderName = senderId === 'user' ? 'You' : (agents[senderId]?.name || senderId)
50
52
  let mentions = parseMentions(text, agents, chatroom.agentIds)
51
- // Auto-address: if enabled and no explicit mentions, address all agents
53
+ // Routing rules: if no explicit mentions, evaluate keyword/capability rules
54
+ if (mentions.length === 0 && chatroom.routingRules?.length) {
55
+ const agentList = chatroom.agentIds.map((aid) => agents[aid]).filter(Boolean)
56
+ mentions = evaluateRoutingRules(text, chatroom.routingRules, agentList)
57
+ }
58
+ // Auto-address: if enabled and still no mentions, address all agents
52
59
  if (chatroom.autoAddress && mentions.length === 0) {
53
60
  mentions = [...chatroom.agentIds]
54
61
  }
@@ -129,6 +136,15 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
129
136
  const agent = agents[item.agentId]
130
137
  if (!agent) return []
131
138
 
139
+ // Skip muted agents
140
+ const freshForMuteCheck = loadChatrooms()[id] as Chatroom | undefined
141
+ if (freshForMuteCheck && isMuted(freshForMuteCheck, item.agentId)) {
142
+ writeEvent({ t: 'cr_agent_start', agentId: agent.id, agentName: agent.name })
143
+ writeEvent({ t: 'err', text: `${agent.name} is muted`, agentId: agent.id, agentName: agent.name })
144
+ writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
145
+ return []
146
+ }
147
+
132
148
  // Pre-flight: check if the agent's provider is usable before attempting to stream
133
149
  const providerInfo = getProvider(agent.provider)
134
150
  const apiKey = resolveApiKey(agent.credentialId)
@@ -0,0 +1,150 @@
1
+ import { NextResponse } from 'next/server'
2
+ import crypto from 'crypto'
3
+ import { loadChatrooms, saveChatrooms, appendModerationLog } from '@/lib/server/storage'
4
+ import { notify } from '@/lib/server/ws-hub'
5
+ import { notFound } from '@/lib/server/collection-helpers'
6
+ import { getMembers } from '@/lib/server/chatroom-helpers'
7
+ import type { Chatroom, ChatroomMember } from '@/types'
8
+
9
+ export const dynamic = 'force-dynamic'
10
+
11
+ interface ModerationBody {
12
+ action: 'delete-message' | 'mute' | 'unmute' | 'set-role'
13
+ targetAgentId: string
14
+ messageId?: string
15
+ role?: 'admin' | 'moderator' | 'member'
16
+ muteDurationMinutes?: number
17
+ }
18
+
19
+ function isValidAction(action: unknown): action is ModerationBody['action'] {
20
+ return typeof action === 'string' && ['delete-message', 'mute', 'unmute', 'set-role'].includes(action)
21
+ }
22
+
23
+ function isValidRole(role: unknown): role is ChatroomMember['role'] {
24
+ return typeof role === 'string' && ['admin', 'moderator', 'member'].includes(role)
25
+ }
26
+
27
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
28
+ const { id } = await params
29
+ const body = await req.json() as Record<string, unknown>
30
+
31
+ const chatrooms = loadChatrooms()
32
+ const chatroom = chatrooms[id] as Chatroom | undefined
33
+ if (!chatroom) return notFound()
34
+
35
+ const action = body.action
36
+ const targetAgentId = typeof body.targetAgentId === 'string' ? body.targetAgentId : ''
37
+
38
+ if (!isValidAction(action)) {
39
+ return NextResponse.json({ error: 'Invalid action. Must be: delete-message, mute, unmute, or set-role' }, { status: 400 })
40
+ }
41
+ if (!targetAgentId) {
42
+ return NextResponse.json({ error: 'targetAgentId is required' }, { status: 400 })
43
+ }
44
+ if (!chatroom.agentIds.includes(targetAgentId)) {
45
+ return NextResponse.json({ error: 'Agent is not a member of this chatroom' }, { status: 400 })
46
+ }
47
+
48
+ // Ensure members array exists (backward compat)
49
+ if (!chatroom.members) {
50
+ chatroom.members = getMembers(chatroom)
51
+ }
52
+
53
+ const logId = crypto.randomBytes(8).toString('hex')
54
+
55
+ switch (action) {
56
+ case 'delete-message': {
57
+ const messageId = typeof body.messageId === 'string' ? body.messageId : ''
58
+ if (!messageId) {
59
+ return NextResponse.json({ error: 'messageId is required for delete-message' }, { status: 400 })
60
+ }
61
+ const msgIndex = chatroom.messages.findIndex((m) => m.id === messageId)
62
+ if (msgIndex === -1) {
63
+ return NextResponse.json({ error: 'Message not found' }, { status: 404 })
64
+ }
65
+ const deleted = chatroom.messages.splice(msgIndex, 1)[0]
66
+ // Also remove from pinned if it was pinned
67
+ if (chatroom.pinnedMessageIds) {
68
+ chatroom.pinnedMessageIds = chatroom.pinnedMessageIds.filter((pid) => pid !== messageId)
69
+ }
70
+ appendModerationLog(logId, {
71
+ id: logId,
72
+ chatroomId: id,
73
+ action: 'delete-message',
74
+ targetAgentId,
75
+ messageId,
76
+ messagePreview: deleted.text.slice(0, 100),
77
+ timestamp: Date.now(),
78
+ })
79
+ break
80
+ }
81
+
82
+ case 'mute': {
83
+ const minutes = typeof body.muteDurationMinutes === 'number' && body.muteDurationMinutes > 0
84
+ ? body.muteDurationMinutes
85
+ : 30
86
+ const mutedUntil = new Date(Date.now() + minutes * 60 * 1000).toISOString()
87
+ const memberIdx = chatroom.members.findIndex((m) => m.agentId === targetAgentId)
88
+ if (memberIdx >= 0) {
89
+ chatroom.members[memberIdx].mutedUntil = mutedUntil
90
+ } else {
91
+ chatroom.members.push({ agentId: targetAgentId, role: 'member', mutedUntil })
92
+ }
93
+ appendModerationLog(logId, {
94
+ id: logId,
95
+ chatroomId: id,
96
+ action: 'mute',
97
+ targetAgentId,
98
+ muteDurationMinutes: minutes,
99
+ mutedUntil,
100
+ timestamp: Date.now(),
101
+ })
102
+ break
103
+ }
104
+
105
+ case 'unmute': {
106
+ const memberIdx = chatroom.members.findIndex((m) => m.agentId === targetAgentId)
107
+ if (memberIdx >= 0) {
108
+ delete chatroom.members[memberIdx].mutedUntil
109
+ }
110
+ appendModerationLog(logId, {
111
+ id: logId,
112
+ chatroomId: id,
113
+ action: 'unmute',
114
+ targetAgentId,
115
+ timestamp: Date.now(),
116
+ })
117
+ break
118
+ }
119
+
120
+ case 'set-role': {
121
+ const role = body.role
122
+ if (!isValidRole(role)) {
123
+ return NextResponse.json({ error: 'role must be: admin, moderator, or member' }, { status: 400 })
124
+ }
125
+ const memberIdx = chatroom.members.findIndex((m) => m.agentId === targetAgentId)
126
+ if (memberIdx >= 0) {
127
+ chatroom.members[memberIdx].role = role
128
+ } else {
129
+ chatroom.members.push({ agentId: targetAgentId, role })
130
+ }
131
+ appendModerationLog(logId, {
132
+ id: logId,
133
+ chatroomId: id,
134
+ action: 'set-role',
135
+ targetAgentId,
136
+ role,
137
+ timestamp: Date.now(),
138
+ })
139
+ break
140
+ }
141
+ }
142
+
143
+ chatroom.updatedAt = Date.now()
144
+ chatrooms[id] = chatroom
145
+ saveChatrooms(chatrooms)
146
+ notify('chatrooms')
147
+ notify(`chatroom:${id}`)
148
+
149
+ return NextResponse.json(chatroom)
150
+ }
@@ -1,5 +1,5 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadChatrooms, saveChatrooms, loadAgents } from '@/lib/server/storage'
2
+ import { loadChatrooms, saveChatrooms, loadAgents, loadConnectors, saveConnectors } from '@/lib/server/storage'
3
3
  import { notify } from '@/lib/server/ws-hub'
4
4
  import { notFound } from '@/lib/server/collection-helpers'
5
5
  import { genId } from '@/lib/id'
@@ -27,6 +27,9 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
27
27
  if (body.autoAddress !== undefined) {
28
28
  chatroom.autoAddress = Boolean(body.autoAddress)
29
29
  }
30
+ if (body.routingRules !== undefined) {
31
+ chatroom.routingRules = Array.isArray(body.routingRules) ? body.routingRules : undefined
32
+ }
30
33
 
31
34
  // Diff agentIds and inject join/leave system messages
32
35
  if (Array.isArray(body.agentIds)) {
@@ -91,6 +94,21 @@ export async function DELETE(_req: Request, { params }: { params: Promise<{ id:
91
94
  const chatrooms = loadChatrooms()
92
95
  if (!chatrooms[id]) return notFound()
93
96
 
97
+ // Cascade: null out chatroomId on any connectors that reference this chatroom
98
+ const connectors = loadConnectors()
99
+ let connectorsDirty = false
100
+ for (const connector of Object.values(connectors)) {
101
+ if (connector.chatroomId === id) {
102
+ connector.chatroomId = null
103
+ connector.updatedAt = Date.now()
104
+ connectorsDirty = true
105
+ }
106
+ }
107
+ if (connectorsDirty) {
108
+ saveConnectors(connectors)
109
+ notify('connectors')
110
+ }
111
+
94
112
  delete chatrooms[id]
95
113
  saveChatrooms(chatrooms)
96
114
  notify('chatrooms')
@@ -2,6 +2,8 @@ import { NextResponse } from 'next/server'
2
2
  import { genId } from '@/lib/id'
3
3
  import { loadChatrooms, saveChatrooms, loadAgents } from '@/lib/server/storage'
4
4
  import { notify } from '@/lib/server/ws-hub'
5
+ import { ChatroomCreateSchema, formatZodError } from '@/lib/validation/schemas'
6
+ import { z } from 'zod'
5
7
  import type { Chatroom, ChatroomMessage } from '@/types'
6
8
 
7
9
  export const dynamic = 'force-dynamic'
@@ -12,11 +14,16 @@ export async function GET() {
12
14
  }
13
15
 
14
16
  export async function POST(req: Request) {
15
- const body = await req.json()
17
+ const raw = await req.json()
18
+ const parsed = ChatroomCreateSchema.safeParse(raw)
19
+ if (!parsed.success) {
20
+ return NextResponse.json(formatZodError(parsed.error as z.ZodError), { status: 400 })
21
+ }
22
+ const body = parsed.data
16
23
  const chatrooms = loadChatrooms()
17
24
  const id = genId()
18
25
 
19
- const requestedAgentIds: string[] = Array.isArray(body.agentIds) ? body.agentIds : []
26
+ const requestedAgentIds: string[] = body.agentIds
20
27
  const knownAgents = loadAgents()
21
28
  const invalidAgentIds = requestedAgentIds.filter((agentId) => !knownAgents[agentId])
22
29
  if (invalidAgentIds.length > 0) {
@@ -51,6 +58,9 @@ export async function POST(req: Request) {
51
58
  messages: joinMessages,
52
59
  chatMode,
53
60
  autoAddress,
61
+ ...(Array.isArray(body.routingRules) && body.routingRules.length > 0
62
+ ? { routingRules: body.routingRules }
63
+ : {}),
54
64
  createdAt: now,
55
65
  updatedAt: now,
56
66
  }
@@ -0,0 +1,64 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadConnectors, loadConnectorHealth } from '@/lib/server/storage'
3
+ import { notFound } from '@/lib/server/collection-helpers'
4
+ import type { ConnectorHealthEvent } from '@/types'
5
+
6
+ export async function GET(req: Request, { params }: { params: Promise<{ id: string }> }) {
7
+ const { id } = await params
8
+ const connectors = loadConnectors()
9
+ if (!connectors[id]) return notFound()
10
+
11
+ const url = new URL(req.url)
12
+ const since = url.searchParams.get('since')
13
+
14
+ const allHealth = loadConnectorHealth()
15
+ const events: ConnectorHealthEvent[] = []
16
+
17
+ for (const raw of Object.values(allHealth)) {
18
+ const entry = raw as ConnectorHealthEvent
19
+ if (entry.connectorId !== id) continue
20
+ if (since && entry.timestamp < since) continue
21
+ events.push(entry)
22
+ }
23
+
24
+ // Sort by timestamp ascending
25
+ events.sort((a, b) => a.timestamp.localeCompare(b.timestamp))
26
+
27
+ // Compute uptime percentage
28
+ const uptimePercent = computeUptime(events)
29
+
30
+ return NextResponse.json({ events, uptimePercent })
31
+ }
32
+
33
+ function computeUptime(events: ConnectorHealthEvent[]): number {
34
+ if (events.length === 0) return 0
35
+
36
+ const firstTime = new Date(events[0].timestamp).getTime()
37
+ const now = Date.now()
38
+ const totalMs = now - firstTime
39
+ if (totalMs <= 0) return 100
40
+
41
+ let uptimeMs = 0
42
+ let lastUpAt: number | null = null
43
+
44
+ for (const ev of events) {
45
+ const t = new Date(ev.timestamp).getTime()
46
+ if (ev.event === 'started' || ev.event === 'reconnected') {
47
+ if (lastUpAt === null) {
48
+ lastUpAt = t
49
+ }
50
+ } else if (ev.event === 'stopped' || ev.event === 'error' || ev.event === 'disconnected') {
51
+ if (lastUpAt !== null) {
52
+ uptimeMs += t - lastUpAt
53
+ lastUpAt = null
54
+ }
55
+ }
56
+ }
57
+
58
+ // If still up, count time until now
59
+ if (lastUpAt !== null) {
60
+ uptimeMs += now - lastUpAt
61
+ }
62
+
63
+ return Math.round((uptimeMs / totalMs) * 10000) / 100
64
+ }
@@ -2,6 +2,8 @@ import { NextResponse } from 'next/server'
2
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
+ import { ConnectorCreateSchema, formatZodError } from '@/lib/validation/schemas'
6
+ import { z } from 'zod'
5
7
  import type { Connector } from '@/types'
6
8
  export const dynamic = 'force-dynamic'
7
9
 
@@ -10,7 +12,7 @@ export async function GET(_req: Request) {
10
12
  const connectors = loadConnectors()
11
13
  // Merge runtime status from manager
12
14
  try {
13
- const { getConnectorStatus, isConnectorAuthenticated, hasConnectorCredentials, getConnectorQR } = await import('@/lib/server/connectors/manager')
15
+ const { getConnectorStatus, isConnectorAuthenticated, hasConnectorCredentials, getConnectorQR, getReconnectState } = await import('@/lib/server/connectors/manager')
14
16
  for (const c of Object.values(connectors) as Connector[]) {
15
17
  c.status = getConnectorStatus(c.id)
16
18
  if (c.platform === 'whatsapp') {
@@ -19,13 +21,26 @@ export async function GET(_req: Request) {
19
21
  const qr = getConnectorQR(c.id)
20
22
  if (qr) c.qrDataUrl = qr
21
23
  }
24
+ // Surface reconnect state if connector is in a recovery cycle
25
+ const rState = getReconnectState(c.id)
26
+ if (rState) {
27
+ const ext = c as unknown as Record<string, unknown>
28
+ ext.reconnectAttempts = rState.attempts
29
+ ext.nextRetryAt = rState.nextRetryAt
30
+ ext.reconnectError = rState.error
31
+ }
22
32
  }
23
33
  } catch { /* manager not loaded yet */ }
24
34
  return NextResponse.json(connectors)
25
35
  }
26
36
 
27
37
  export async function POST(req: Request) {
28
- const body = await req.json()
38
+ const raw = await req.json()
39
+ const parsed = ConnectorCreateSchema.safeParse(raw)
40
+ if (!parsed.success) {
41
+ return NextResponse.json(formatZodError(parsed.error as z.ZodError), { status: 400 })
42
+ }
43
+ const body = parsed.data
29
44
  const connectors = loadConnectors()
30
45
  const id = genId()
31
46