@swarmclawai/swarmclaw 0.6.4 → 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.
- package/README.md +62 -30
- package/package.json +10 -1
- package/src/app/api/agents/[id]/clone/route.ts +40 -0
- package/src/app/api/agents/route.ts +39 -14
- package/src/app/api/chatrooms/[id]/chat/route.ts +58 -3
- package/src/app/api/chatrooms/[id]/moderate/route.ts +150 -0
- package/src/app/api/chatrooms/[id]/route.ts +34 -2
- package/src/app/api/chatrooms/route.ts +26 -3
- package/src/app/api/connectors/[id]/health/route.ts +64 -0
- package/src/app/api/connectors/route.ts +17 -2
- package/src/app/api/knowledge/route.ts +6 -1
- package/src/app/api/openclaw/doctor/route.ts +17 -0
- package/src/app/api/schedules/[id]/run/route.ts +3 -0
- package/src/app/api/sessions/[id]/chat/route.ts +5 -1
- package/src/app/api/sessions/route.ts +11 -2
- package/src/app/api/tasks/[id]/route.ts +18 -13
- package/src/app/api/tasks/route.ts +44 -1
- package/src/app/api/usage/route.ts +16 -7
- package/src/app/api/wallets/[id]/approve/route.ts +62 -0
- package/src/app/api/wallets/[id]/balance-history/route.ts +18 -0
- package/src/app/api/wallets/[id]/route.ts +118 -0
- package/src/app/api/wallets/[id]/send/route.ts +118 -0
- package/src/app/api/wallets/[id]/transactions/route.ts +18 -0
- package/src/app/api/wallets/route.ts +74 -0
- package/src/app/globals.css +8 -0
- package/src/cli/index.js +20 -0
- package/src/cli/index.ts +223 -39
- package/src/cli/spec.js +14 -0
- package/src/components/agents/agent-avatar.tsx +15 -1
- package/src/components/agents/agent-card.tsx +38 -6
- package/src/components/agents/agent-chat-list.tsx +79 -3
- package/src/components/agents/agent-sheet.tsx +191 -26
- package/src/components/auth/setup-wizard.tsx +268 -353
- package/src/components/chat/chat-area.tsx +24 -9
- package/src/components/chat/chat-header.tsx +48 -19
- package/src/components/chat/chat-tool-toggles.tsx +1 -1
- package/src/components/chat/delegation-banner.test.ts +27 -0
- package/src/components/chat/delegation-banner.tsx +109 -23
- package/src/components/chat/message-bubble.tsx +17 -16
- package/src/components/chat/message-list.tsx +6 -5
- package/src/components/chat/streaming-bubble.tsx +3 -2
- package/src/components/chat/thinking-indicator.tsx +3 -2
- package/src/components/chat/transfer-agent-picker.tsx +1 -1
- package/src/components/chatrooms/agent-hover-card.tsx +1 -1
- package/src/components/chatrooms/chatroom-input.tsx +1 -1
- package/src/components/chatrooms/chatroom-message.tsx +165 -23
- package/src/components/chatrooms/chatroom-sheet.tsx +289 -4
- package/src/components/chatrooms/chatroom-typing-bar.tsx +1 -1
- package/src/components/chatrooms/chatroom-view.tsx +62 -17
- package/src/components/connectors/connector-health.tsx +120 -0
- package/src/components/connectors/connector-list.tsx +1 -1
- package/src/components/connectors/connector-sheet.tsx +9 -0
- package/src/components/home/home-view.tsx +25 -3
- package/src/components/input/chat-input.tsx +8 -1
- package/src/components/knowledge/knowledge-list.tsx +1 -1
- package/src/components/knowledge/knowledge-sheet.tsx +1 -1
- package/src/components/layout/app-layout.tsx +35 -4
- package/src/components/memory/memory-agent-list.tsx +1 -1
- package/src/components/memory/memory-browser.tsx +1 -0
- package/src/components/memory/memory-card.tsx +3 -2
- package/src/components/memory/memory-detail.tsx +3 -3
- package/src/components/memory/memory-sheet.tsx +2 -2
- package/src/components/projects/project-detail.tsx +4 -4
- package/src/components/schedules/schedule-list.tsx +55 -9
- package/src/components/schedules/schedule-sheet.tsx +134 -23
- package/src/components/secrets/secret-sheet.tsx +1 -1
- package/src/components/secrets/secrets-list.tsx +1 -1
- package/src/components/sessions/session-card.tsx +1 -1
- package/src/components/shared/agent-picker-list.tsx +1 -1
- package/src/components/shared/agent-switch-dialog.tsx +1 -1
- package/src/components/shared/command-palette.tsx +237 -0
- package/src/components/shared/connector-platform-icon.tsx +1 -0
- package/src/components/shared/settings/section-user-preferences.tsx +4 -4
- package/src/components/skills/skill-list.tsx +1 -1
- package/src/components/skills/skill-sheet.tsx +1 -1
- package/src/components/tasks/task-board.tsx +3 -3
- package/src/components/tasks/task-card.tsx +22 -2
- package/src/components/tasks/task-sheet.tsx +112 -17
- package/src/components/usage/metrics-dashboard.tsx +13 -25
- package/src/components/wallets/wallet-approval-dialog.tsx +99 -0
- package/src/components/wallets/wallet-panel.tsx +616 -0
- package/src/components/wallets/wallet-section.tsx +100 -0
- package/src/hooks/use-swipe.ts +49 -0
- package/src/lib/providers/anthropic.ts +16 -2
- package/src/lib/providers/claude-cli.ts +7 -1
- package/src/lib/providers/index.ts +7 -0
- package/src/lib/providers/ollama.ts +16 -2
- package/src/lib/providers/openai.ts +7 -2
- package/src/lib/providers/openclaw.ts +6 -1
- package/src/lib/providers/provider-defaults.ts +7 -0
- package/src/lib/schedule-templates.ts +115 -0
- package/src/lib/server/agent-registry.ts +2 -2
- package/src/lib/server/alert-dispatch.ts +64 -0
- package/src/lib/server/chat-execution.ts +76 -4
- package/src/lib/server/chatroom-health.ts +60 -0
- package/src/lib/server/chatroom-helpers.test.ts +94 -0
- package/src/lib/server/chatroom-helpers.ts +86 -12
- package/src/lib/server/chatroom-routing.ts +65 -0
- package/src/lib/server/connectors/discord.ts +3 -0
- package/src/lib/server/connectors/email.ts +267 -0
- package/src/lib/server/connectors/inbound-audio-transcription.test.ts +191 -0
- package/src/lib/server/connectors/inbound-audio-transcription.ts +261 -0
- package/src/lib/server/connectors/manager.ts +239 -5
- package/src/lib/server/connectors/openclaw.ts +3 -0
- package/src/lib/server/connectors/slack.ts +6 -0
- package/src/lib/server/connectors/telegram.ts +18 -0
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp-text.test.ts +29 -0
- package/src/lib/server/connectors/whatsapp-text.ts +26 -0
- package/src/lib/server/connectors/whatsapp.ts +17 -5
- package/src/lib/server/cost.ts +70 -0
- package/src/lib/server/create-notification.ts +2 -0
- package/src/lib/server/daemon-state.ts +124 -0
- package/src/lib/server/dag-validation.ts +115 -0
- package/src/lib/server/memory-db.ts +12 -7
- package/src/lib/server/openclaw-doctor.ts +48 -0
- package/src/lib/server/orchestrator-lg.ts +12 -2
- package/src/lib/server/orchestrator.ts +6 -1
- package/src/lib/server/queue-followups.test.ts +224 -0
- package/src/lib/server/queue.ts +238 -24
- package/src/lib/server/scheduler.ts +3 -0
- package/src/lib/server/session-run-manager.ts +22 -1
- package/src/lib/server/session-tools/chatroom.ts +11 -2
- package/src/lib/server/session-tools/context-mgmt.ts +2 -2
- package/src/lib/server/session-tools/index.ts +8 -2
- package/src/lib/server/session-tools/memory.ts +23 -4
- package/src/lib/server/session-tools/openclaw-workspace.ts +132 -0
- package/src/lib/server/session-tools/shell.ts +1 -1
- package/src/lib/server/session-tools/wallet.ts +124 -0
- package/src/lib/server/session-tools/web.ts +2 -2
- package/src/lib/server/solana.ts +122 -0
- package/src/lib/server/storage.ts +158 -6
- package/src/lib/server/stream-agent-chat.ts +126 -63
- package/src/lib/server/task-mention.test.ts +41 -0
- package/src/lib/server/task-mention.ts +3 -2
- package/src/lib/setup-defaults.ts +277 -0
- package/src/lib/tool-definitions.ts +1 -0
- package/src/lib/validation/schemas.ts +69 -0
- package/src/lib/view-routes.ts +1 -0
- package/src/stores/use-app-store.ts +15 -3
- package/src/stores/use-chatroom-store.ts +52 -2
- package/src/types/index.ts +98 -2
- 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
|
-
|
|
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
|

|
|
19
19
|

|
|
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.
|
|
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
|
|
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
|
|
98
|
-
2.
|
|
99
|
-
3.
|
|
100
|
-
4.
|
|
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
|
-
-
|
|
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
|
|
@@ -118,20 +161,20 @@ Notes:
|
|
|
118
161
|
- **Orchestration** — Multi-agent workflows powered by LangGraph with automatic sub-agent routing, checkpointed execution, and rich delegation cards that link to sub-agent chat threads
|
|
119
162
|
- **Agentic Execution Policy** — Tool-first autonomous action loop with progress updates, evidence-driven answers, and better use of platform tools for long-lived work
|
|
120
163
|
- **Runtime Date/Time Grounding** — Session, orchestrator, chatroom, and connector prompts include authoritative current timestamp context to reduce stale-date behavior
|
|
121
|
-
- **Task Board** — Queue and track agent tasks with status, comments,
|
|
164
|
+
- **Task Board** — Queue and track agent tasks with status, comments, structured result artifacts (`outputFiles`, uploads), completion reports, and archiving. Strict capability policy pauses tasks for human approval before tool execution
|
|
122
165
|
- **Task Metrics API** — Built-in analytics endpoint for WIP, cycle times, throughput velocity, completion/failure by agent, and priority distribution
|
|
123
166
|
- **Background Daemon** — Auto-processes queued tasks and scheduled jobs with a 30s heartbeat plus recurring health monitoring
|
|
124
167
|
- **Scheduling** — Cron-based agent scheduling with human-friendly presets
|
|
125
168
|
- **Loop Runtime Controls** — Switch between bounded and ongoing loops with configurable step caps, runtime guards, heartbeat cadence, and timeout budgets
|
|
126
169
|
- **Session Run Queue** — Per-session queued runs with followup/steer/collect modes, collect coalescing for bursty inputs, and run-state APIs
|
|
127
170
|
- **Chat Iteration Workflow** — Edit-and-resend user turns, fork a new session from any message, bookmark key messages, use contextual follow-up suggestion chips, and auto-continue after tool access grants
|
|
128
|
-
- **Agent Chatrooms** — Multi-agent room conversations with `@mention` routing, chained agent replies, reactions,
|
|
171
|
+
- **Agent Chatrooms** — Multi-agent room conversations with `@mention` routing, chained agent replies, reactions, file/image-aware context, health-aware member filtering, and persistent context compaction for long-lived rooms
|
|
129
172
|
- **Live Chat Telemetry** — Thinking/tool/responding stream phases, live main-loop status badges, connector activity presence, tone indicator, and optional sound notifications
|
|
130
173
|
- **Global Search Palette** — `Cmd/Ctrl+K` search across agents, tasks, sessions, schedules, webhooks, and skills from anywhere in the app
|
|
131
174
|
- **Notification Center** — Real-time in-app notifications for task/schedule/daemon events with unread tracking, mark-all/clear-read controls, and optional action links
|
|
132
175
|
- **Preview-Rich Chat UI** — Side preview panel for tool outputs (image/browser/html/code), inline code/PDF previews for attachments, and image lightbox support
|
|
133
176
|
- **Voice Settings** — Per-instance ElevenLabs API key + voice ID for TTS replies, plus configurable speech recognition language for chat input
|
|
134
|
-
- **Chat Connectors** — Bridge agents to Discord, Slack, Telegram, WhatsApp, BlueBubbles (iMessage), Signal, Microsoft Teams, Google Chat, Matrix, and OpenClaw with media-aware inbound handling
|
|
177
|
+
- **Chat Connectors** — Bridge agents to Discord, Slack, Telegram, WhatsApp, BlueBubbles (iMessage), Signal, Microsoft Teams, Google Chat, Matrix, and OpenClaw with media-aware inbound handling, inbound voice-note transcription (ElevenLabs/OpenAI fallback), and WhatsApp-friendly plain-text formatting
|
|
135
178
|
- **Skills System** — Discover local skills, import skills from URL, and load OpenClaw `SKILL.md` files (frontmatter-compatible)
|
|
136
179
|
- **Execution Logging** — Structured audit trail for triggers, tool calls, file ops, commits, and errors in a dedicated `logs.db`
|
|
137
180
|
- **Context Management** — Auto-compaction of conversation history when approaching context limits, with manual `context_status` and `context_summarize` tools for agents
|
|
@@ -157,6 +200,7 @@ CREDENTIAL_SECRET=<auto-generated> # AES-256 encryption key for stored credentia
|
|
|
157
200
|
```
|
|
158
201
|
|
|
159
202
|
Data is stored in `data/swarmclaw.db` (SQLite with WAL mode), `data/memory.db` (agent memory with FTS5 + vector embeddings), `data/logs.db` (execution audit trail), and `data/langgraph-checkpoints.db` (orchestrator checkpoints). Back the `data/` directory up if you care about your sessions, agents, and credentials. Existing JSON file data is auto-migrated to SQLite on first run.
|
|
203
|
+
Agent wallet private keys are stored encrypted (AES-256 via `CREDENTIAL_SECRET`) in `data/swarmclaw.db` and are never returned by wallet API responses; keep `data/` out of version control.
|
|
160
204
|
|
|
161
205
|
The app listens on two ports: `PORT` (default 3456) for the HTTP/SSE API, and `PORT + 1` (default 3457) for WebSocket push notifications. The WS port can be customized with `--ws-port`.
|
|
162
206
|
|
|
@@ -221,22 +265,6 @@ src/
|
|
|
221
265
|
| OpenClaw | Per-Agent Gateway | Toggle in agent editor connects to any OpenClaw gateway via the bundled CLI. |
|
|
222
266
|
| Custom | API | Any OpenAI-compatible endpoint. Add via Providers sidebar. |
|
|
223
267
|
|
|
224
|
-
### OpenClaw
|
|
225
|
-
|
|
226
|
-
[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.
|
|
227
|
-
|
|
228
|
-
To connect an agent to an OpenClaw gateway:
|
|
229
|
-
|
|
230
|
-
1. Create or edit an agent
|
|
231
|
-
2. Toggle **OpenClaw Gateway** ON
|
|
232
|
-
3. Enter the gateway URL (e.g. `http://192.168.1.50:18789` or `https://my-vps:18789`)
|
|
233
|
-
4. Add a gateway token if authentication is enabled on the remote gateway
|
|
234
|
-
5. Click **Connect** — approve the device in your gateway's dashboard if prompted, then **Retry Connection**
|
|
235
|
-
|
|
236
|
-
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.
|
|
237
|
-
|
|
238
|
-
URLs without a protocol are auto-prefixed with `http://`. For remote gateways with TLS, use `https://` explicitly.
|
|
239
|
-
|
|
240
268
|
## Chat Connectors
|
|
241
269
|
|
|
242
270
|
Bridge any agent to a chat platform:
|
|
@@ -288,6 +316,7 @@ Agents can use the following tools when enabled:
|
|
|
288
316
|
| HTTP Request | Make direct API calls with method, headers, body, redirect control, and timeout |
|
|
289
317
|
| Git | Run structured git subcommands (`status`, `diff`, `log`, `add`, `commit`, `push`, etc.) with repo safety checks |
|
|
290
318
|
| Memory | Store and retrieve long-term memories with FTS5 + vector search, file references, image attachments, and linked memory graph traversal |
|
|
319
|
+
| Wallet | Manage an agent-linked Solana wallet (`wallet_tool`) to check balance/address, send SOL (limits + approval), and review transaction history |
|
|
291
320
|
| Sandbox | Run JS/TS (Deno) or Python code in an isolated sandbox. Created files are returned as downloadable artifacts |
|
|
292
321
|
| MCP Servers | Connect to external Model Context Protocol servers. Tools from MCP servers are injected as first-class agent tools |
|
|
293
322
|
|
|
@@ -624,7 +653,10 @@ swarmclaw tasks create --title "Fix flaky CI test" --description "Stabilize retr
|
|
|
624
653
|
# run setup diagnostics
|
|
625
654
|
swarmclaw setup doctor
|
|
626
655
|
|
|
627
|
-
#
|
|
656
|
+
# interactive setup (walks through provider selection, supports multiple providers)
|
|
657
|
+
swarmclaw setup init
|
|
658
|
+
|
|
659
|
+
# or non-interactive setup with flags
|
|
628
660
|
swarmclaw setup init --provider openai --api-key "$OPENAI_API_KEY"
|
|
629
661
|
|
|
630
662
|
# run memory maintenance analysis
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "0.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": {
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"build:ci": "NEXT_DISABLE_ESLINT=1 next build",
|
|
46
46
|
"start": "next start",
|
|
47
47
|
"start:standalone": "node .next/standalone/server.js",
|
|
48
|
+
"benchmark:autonomy": "node ./scripts/benchmark-autonomy-harness.mjs",
|
|
48
49
|
"lint": "eslint",
|
|
49
50
|
"lint:fix": "eslint --fix",
|
|
50
51
|
"lint:baseline": "node ./scripts/lint-baseline.mjs check",
|
|
@@ -63,8 +64,10 @@
|
|
|
63
64
|
"@multiavatar/multiavatar": "^1.0.7",
|
|
64
65
|
"@playwright/mcp": "^0.0.68",
|
|
65
66
|
"@slack/bolt": "^4.6.0",
|
|
67
|
+
"@solana/web3.js": "^1.98.4",
|
|
66
68
|
"@whiskeysockets/baileys": "^7.0.0-rc.9",
|
|
67
69
|
"better-sqlite3": "^12.6.2",
|
|
70
|
+
"bs58": "^5.0.0",
|
|
68
71
|
"cheerio": "^1.2.0",
|
|
69
72
|
"class-variance-authority": "^0.7.1",
|
|
70
73
|
"clsx": "^2.1.1",
|
|
@@ -75,9 +78,12 @@
|
|
|
75
78
|
"exceljs": "^4.4.0",
|
|
76
79
|
"grammy": "^1.40.0",
|
|
77
80
|
"highlight.js": "^11.11.1",
|
|
81
|
+
"imapflow": "^1.2.11",
|
|
78
82
|
"lucide-react": "^0.574.0",
|
|
83
|
+
"mailparser": "^3.9.3",
|
|
79
84
|
"next": "16.1.6",
|
|
80
85
|
"next-themes": "^0.4.6",
|
|
86
|
+
"nodemailer": "^8.0.1",
|
|
81
87
|
"openclaw": "^2026.2.26",
|
|
82
88
|
"pdf-parse": "^2.4.5",
|
|
83
89
|
"qrcode": "^1.5.4",
|
|
@@ -90,6 +96,7 @@
|
|
|
90
96
|
"recharts": "^3.7.0",
|
|
91
97
|
"rehype-highlight": "^7.0.2",
|
|
92
98
|
"remark-gfm": "^4.0.1",
|
|
99
|
+
"remove-markdown": "^0.6.3",
|
|
93
100
|
"sonner": "^2.0.7",
|
|
94
101
|
"tailwind-merge": "^3.4.1",
|
|
95
102
|
"ws": "^8.19.0",
|
|
@@ -99,7 +106,9 @@
|
|
|
99
106
|
"devDependencies": {
|
|
100
107
|
"@tailwindcss/postcss": "^4",
|
|
101
108
|
"@types/better-sqlite3": "^7.6.13",
|
|
109
|
+
"@types/mailparser": "^3.4.6",
|
|
102
110
|
"@types/node": "^20",
|
|
111
|
+
"@types/nodemailer": "^7.0.11",
|
|
103
112
|
"@types/qrcode": "^1.5.6",
|
|
104
113
|
"@types/react": "^19",
|
|
105
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(
|
|
10
|
-
|
|
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
|
|
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
|
|
21
|
-
description: body.description
|
|
22
|
-
systemPrompt: body.systemPrompt
|
|
23
|
-
provider: body.provider
|
|
24
|
-
model: body.model
|
|
25
|
-
credentialId: body.credentialId
|
|
26
|
-
apiEndpoint: normalizeProviderEndpoint(body.provider
|
|
27
|
-
isOrchestrator: body.isOrchestrator
|
|
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
|
}
|
|
@@ -8,11 +8,16 @@ import { getProvider } from '@/lib/providers'
|
|
|
8
8
|
import {
|
|
9
9
|
resolveApiKey,
|
|
10
10
|
parseMentions,
|
|
11
|
+
compactChatroomMessages,
|
|
11
12
|
buildChatroomSystemPrompt,
|
|
12
13
|
buildSyntheticSession,
|
|
13
14
|
buildAgentSystemPromptForChatroom,
|
|
14
15
|
buildHistoryForAgent,
|
|
16
|
+
isMuted,
|
|
15
17
|
} from '@/lib/server/chatroom-helpers'
|
|
18
|
+
import { filterHealthyChatroomAgents } from '@/lib/server/chatroom-health'
|
|
19
|
+
import { evaluateRoutingRules } from '@/lib/server/chatroom-routing'
|
|
20
|
+
import { markProviderFailure, markProviderSuccess } from '@/lib/server/provider-health'
|
|
16
21
|
import type { Chatroom, ChatroomMessage, Agent } from '@/types'
|
|
17
22
|
|
|
18
23
|
export const dynamic = 'force-dynamic'
|
|
@@ -45,10 +50,17 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
45
50
|
// Persist incoming message
|
|
46
51
|
const senderName = senderId === 'user' ? 'You' : (agents[senderId]?.name || senderId)
|
|
47
52
|
let mentions = parseMentions(text, agents, chatroom.agentIds)
|
|
48
|
-
//
|
|
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
|
|
49
59
|
if (chatroom.autoAddress && mentions.length === 0) {
|
|
50
60
|
mentions = [...chatroom.agentIds]
|
|
51
61
|
}
|
|
62
|
+
const mentionHealth = filterHealthyChatroomAgents(mentions, agents)
|
|
63
|
+
mentions = mentionHealth.healthyAgentIds
|
|
52
64
|
const userMessage: ChatroomMessage = {
|
|
53
65
|
id: genId(),
|
|
54
66
|
senderId,
|
|
@@ -63,6 +75,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
63
75
|
...(replyToId ? { replyToId } : {}),
|
|
64
76
|
}
|
|
65
77
|
chatroom.messages.push(userMessage)
|
|
78
|
+
compactChatroomMessages(chatroom)
|
|
66
79
|
chatroom.updatedAt = Date.now()
|
|
67
80
|
chatrooms[id] = chatroom
|
|
68
81
|
saveChatrooms(chatrooms)
|
|
@@ -94,6 +107,22 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
94
107
|
}
|
|
95
108
|
|
|
96
109
|
const processAgents = async () => {
|
|
110
|
+
if (mentionHealth.skipped.length > 0) {
|
|
111
|
+
const detail = mentionHealth.skipped
|
|
112
|
+
.map((row) => `${agents[row.agentId]?.name || row.agentId}: ${row.reason}`)
|
|
113
|
+
.join(', ')
|
|
114
|
+
writeEvent({ t: 'err', text: `Skipped agents: ${detail}` })
|
|
115
|
+
}
|
|
116
|
+
if (mentions.length === 0) {
|
|
117
|
+
writeEvent({ t: 'err', text: 'No healthy agents available in this chatroom. Check provider credentials/endpoints and retry.' })
|
|
118
|
+
writeEvent({ t: 'done' })
|
|
119
|
+
if (!closed) {
|
|
120
|
+
try { controller.close() } catch { /* already closed */ }
|
|
121
|
+
closed = true
|
|
122
|
+
}
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
|
|
97
126
|
// Build agent queue: start with mentioned agents, then chain
|
|
98
127
|
const initialQueue: Array<{ agentId: string; depth: number; contextMessage?: string }> = mentions.map((aid) => ({ agentId: aid, depth: 0 }))
|
|
99
128
|
const processed = new Set<string>()
|
|
@@ -107,6 +136,15 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
107
136
|
const agent = agents[item.agentId]
|
|
108
137
|
if (!agent) return []
|
|
109
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
|
+
|
|
110
148
|
// Pre-flight: check if the agent's provider is usable before attempting to stream
|
|
111
149
|
const providerInfo = getProvider(agent.provider)
|
|
112
150
|
const apiKey = resolveApiKey(agent.credentialId)
|
|
@@ -128,6 +166,11 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
128
166
|
try {
|
|
129
167
|
const freshChatrooms = loadChatrooms()
|
|
130
168
|
const freshChatroom = freshChatrooms[id] as Chatroom
|
|
169
|
+
if (compactChatroomMessages(freshChatroom)) {
|
|
170
|
+
freshChatrooms[id] = freshChatroom
|
|
171
|
+
saveChatrooms(freshChatrooms)
|
|
172
|
+
notify(`chatroom:${id}`)
|
|
173
|
+
}
|
|
131
174
|
|
|
132
175
|
const syntheticSession = buildSyntheticSession(agent, id)
|
|
133
176
|
const agentSystemPrompt = buildAgentSystemPromptForChatroom(agent)
|
|
@@ -170,16 +213,25 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
170
213
|
history,
|
|
171
214
|
})
|
|
172
215
|
|
|
173
|
-
const responseText = result.fullText || fullText
|
|
216
|
+
const responseText = result.finalResponse || result.fullText || fullText
|
|
174
217
|
|
|
175
218
|
// Don't persist empty or error-only messages — they pollute chat history
|
|
176
219
|
if (!responseText.trim() && agentError) {
|
|
220
|
+
markProviderFailure(agent.provider, agentError)
|
|
177
221
|
writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
|
|
178
222
|
return []
|
|
179
223
|
}
|
|
180
224
|
|
|
181
225
|
if (responseText.trim()) {
|
|
182
|
-
const
|
|
226
|
+
const parsedMentions = parseMentions(responseText, agents, freshChatroom.agentIds)
|
|
227
|
+
const chainedHealth = filterHealthyChatroomAgents(parsedMentions, agents)
|
|
228
|
+
const newMentions = chainedHealth.healthyAgentIds
|
|
229
|
+
if (chainedHealth.skipped.length > 0) {
|
|
230
|
+
const detail = chainedHealth.skipped
|
|
231
|
+
.map((row) => `${agents[row.agentId]?.name || row.agentId}: ${row.reason}`)
|
|
232
|
+
.join(', ')
|
|
233
|
+
writeEvent({ t: 'err', text: `Mentioned agents skipped: ${detail}`, agentId: agent.id, agentName: agent.name })
|
|
234
|
+
}
|
|
183
235
|
const agentMessage: ChatroomMessage = {
|
|
184
236
|
id: genId(),
|
|
185
237
|
senderId: agent.id,
|
|
@@ -198,16 +250,19 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
|
|
|
198
250
|
saveChatrooms(latestChatrooms)
|
|
199
251
|
notify(`chatroom:${id}`)
|
|
200
252
|
|
|
253
|
+
markProviderSuccess(agent.provider)
|
|
201
254
|
writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
|
|
202
255
|
|
|
203
256
|
// Return chained agent IDs — enriched context is built below when queuing
|
|
204
257
|
return newMentions.filter((mid) => !processed.has(mid) && freshChatroom.agentIds.includes(mid))
|
|
205
258
|
}
|
|
206
259
|
|
|
260
|
+
markProviderSuccess(agent.provider)
|
|
207
261
|
writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
|
|
208
262
|
return []
|
|
209
263
|
} catch (err: unknown) {
|
|
210
264
|
const msg = err instanceof Error ? err.message : String(err)
|
|
265
|
+
markProviderFailure(agent.provider, msg)
|
|
211
266
|
writeEvent({ t: 'err', text: `Agent ${agent.name} error: ${msg}`, agentId: agent.id })
|
|
212
267
|
writeEvent({ t: 'cr_agent_done', agentId: agent.id, agentName: agent.name })
|
|
213
268
|
return []
|
|
@@ -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
|
+
}
|