@swarmclawai/swarmclaw 0.4.5 → 0.5.0

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 (113) hide show
  1. package/README.md +8 -2
  2. package/bin/server-cmd.js +28 -19
  3. package/next.config.ts +5 -0
  4. package/package.json +2 -1
  5. package/src/app/api/agents/[id]/route.ts +23 -5
  6. package/src/app/api/agents/trash/route.ts +44 -0
  7. package/src/app/api/connectors/[id]/route.ts +7 -4
  8. package/src/app/api/connectors/[id]/webhook/route.ts +6 -2
  9. package/src/app/api/openclaw/agent-files/route.ts +57 -0
  10. package/src/app/api/openclaw/approvals/route.ts +46 -0
  11. package/src/app/api/openclaw/config-sync/route.ts +33 -0
  12. package/src/app/api/openclaw/cron/route.ts +52 -0
  13. package/src/app/api/openclaw/directory/route.ts +4 -3
  14. package/src/app/api/openclaw/discover/route.ts +3 -2
  15. package/src/app/api/openclaw/dotenv-keys/route.ts +18 -0
  16. package/src/app/api/openclaw/exec-config/route.ts +41 -0
  17. package/src/app/api/openclaw/gateway/route.ts +72 -0
  18. package/src/app/api/openclaw/history/route.ts +109 -0
  19. package/src/app/api/openclaw/media/route.ts +53 -0
  20. package/src/app/api/openclaw/models/route.ts +12 -0
  21. package/src/app/api/openclaw/permissions/route.ts +39 -0
  22. package/src/app/api/openclaw/sandbox-env/route.ts +69 -0
  23. package/src/app/api/openclaw/skills/install/route.ts +32 -0
  24. package/src/app/api/openclaw/skills/remove/route.ts +24 -0
  25. package/src/app/api/openclaw/skills/route.ts +82 -0
  26. package/src/app/api/openclaw/sync/route.ts +3 -2
  27. package/src/app/api/projects/[id]/route.ts +1 -1
  28. package/src/app/api/projects/route.ts +1 -1
  29. package/src/app/api/secrets/[id]/route.ts +1 -1
  30. package/src/app/api/sessions/[id]/edit-resend/route.ts +22 -0
  31. package/src/app/api/sessions/[id]/fork/route.ts +44 -0
  32. package/src/app/api/sessions/[id]/messages/route.ts +18 -1
  33. package/src/app/api/sessions/[id]/route.ts +12 -3
  34. package/src/app/api/sessions/route.ts +6 -2
  35. package/src/app/globals.css +14 -0
  36. package/src/app/layout.tsx +5 -20
  37. package/src/cli/index.js +33 -1
  38. package/src/cli/spec.js +40 -0
  39. package/src/components/agents/agent-avatar.tsx +45 -0
  40. package/src/components/agents/agent-card.tsx +19 -5
  41. package/src/components/agents/agent-chat-list.tsx +31 -24
  42. package/src/components/agents/agent-files-editor.tsx +185 -0
  43. package/src/components/agents/agent-list.tsx +82 -3
  44. package/src/components/agents/agent-sheet.tsx +31 -0
  45. package/src/components/agents/cron-job-form.tsx +137 -0
  46. package/src/components/agents/exec-config-panel.tsx +147 -0
  47. package/src/components/agents/inspector-panel.tsx +310 -0
  48. package/src/components/agents/openclaw-skills-panel.tsx +230 -0
  49. package/src/components/agents/permission-preset-selector.tsx +79 -0
  50. package/src/components/agents/personality-builder.tsx +111 -0
  51. package/src/components/agents/sandbox-env-panel.tsx +72 -0
  52. package/src/components/agents/skill-install-dialog.tsx +102 -0
  53. package/src/components/agents/trash-list.tsx +109 -0
  54. package/src/components/chat/chat-area.tsx +14 -2
  55. package/src/components/chat/chat-header.tsx +168 -4
  56. package/src/components/chat/chat-preview-panel.tsx +113 -0
  57. package/src/components/chat/exec-approval-card.tsx +89 -0
  58. package/src/components/chat/message-bubble.tsx +218 -36
  59. package/src/components/chat/message-list.tsx +135 -31
  60. package/src/components/chat/streaming-bubble.tsx +59 -10
  61. package/src/components/chat/suggestions-bar.tsx +74 -0
  62. package/src/components/chat/thinking-indicator.tsx +20 -6
  63. package/src/components/chat/tool-call-bubble.tsx +89 -16
  64. package/src/components/chat/tool-request-banner.tsx +20 -2
  65. package/src/components/chat/trace-block.tsx +103 -0
  66. package/src/components/projects/project-list.tsx +1 -0
  67. package/src/components/settings/gateway-connection-panel.tsx +278 -0
  68. package/src/components/shared/avatar.tsx +13 -2
  69. package/src/components/shared/settings/settings-page.tsx +1 -0
  70. package/src/components/tasks/task-board.tsx +1 -1
  71. package/src/components/tasks/task-sheet.tsx +12 -12
  72. package/src/hooks/use-continuous-speech.ts +42 -5
  73. package/src/hooks/use-openclaw-gateway.ts +63 -0
  74. package/src/lib/notification-sounds.ts +58 -0
  75. package/src/lib/personality-parser.ts +97 -0
  76. package/src/lib/providers/openclaw.ts +17 -2
  77. package/src/lib/runtime-loop.ts +2 -2
  78. package/src/lib/server/chat-execution.ts +44 -2
  79. package/src/lib/server/connectors/bluebubbles.test.ts +17 -8
  80. package/src/lib/server/connectors/bluebubbles.ts +5 -2
  81. package/src/lib/server/connectors/googlechat.ts +14 -10
  82. package/src/lib/server/connectors/manager.ts +37 -15
  83. package/src/lib/server/connectors/openclaw.ts +1 -0
  84. package/src/lib/server/daemon-state.ts +11 -0
  85. package/src/lib/server/main-agent-loop.ts +2 -3
  86. package/src/lib/server/main-session.ts +21 -0
  87. package/src/lib/server/openclaw-config-sync.ts +107 -0
  88. package/src/lib/server/openclaw-exec-config.ts +52 -0
  89. package/src/lib/server/openclaw-gateway.ts +291 -0
  90. package/src/lib/server/openclaw-history-merge.ts +36 -0
  91. package/src/lib/server/openclaw-models.ts +56 -0
  92. package/src/lib/server/openclaw-permission-presets.ts +64 -0
  93. package/src/lib/server/openclaw-sync.ts +11 -10
  94. package/src/lib/server/queue.ts +2 -1
  95. package/src/lib/server/session-tools/connector.ts +4 -4
  96. package/src/lib/server/session-tools/delegate.ts +19 -3
  97. package/src/lib/server/session-tools/file.ts +20 -20
  98. package/src/lib/server/session-tools/index.ts +2 -2
  99. package/src/lib/server/session-tools/openclaw-nodes.ts +6 -6
  100. package/src/lib/server/session-tools/sandbox.ts +2 -2
  101. package/src/lib/server/session-tools/search-providers.ts +13 -6
  102. package/src/lib/server/session-tools/session-tools-wiring.test.ts +2 -2
  103. package/src/lib/server/session-tools/shell.ts +1 -1
  104. package/src/lib/server/session-tools/web.ts +8 -8
  105. package/src/lib/server/storage.ts +62 -11
  106. package/src/lib/server/stream-agent-chat.ts +24 -4
  107. package/src/lib/server/suggestions.ts +20 -0
  108. package/src/lib/server/ws-hub.ts +14 -0
  109. package/src/stores/use-app-store.ts +53 -1
  110. package/src/stores/use-approval-store.ts +78 -0
  111. package/src/stores/use-chat-store.ts +153 -5
  112. package/src/types/index.ts +127 -1
  113. package/tsconfig.json +13 -4
package/README.md CHANGED
@@ -25,7 +25,10 @@ Inspired by [OpenClaw](https://github.com/openclaw).
25
25
 
26
26
  - **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
27
27
  - **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)
28
+ - **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
28
29
  - **Agent Builder** — Create agents with custom personalities (soul), system prompts, tools, and skills. AI-powered generation from a description
30
+ - **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
31
+ - **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
29
32
  - **Agent Tools** — Shell, process control for long-running commands, files, edit file, send file, web search, web fetch, CLI delegation (Claude/Codex/OpenCode), Playwright browser automation, persistent memory, and sandboxed code execution (JS/TS via Deno, Python)
30
33
  - **Platform Tools** — Agents can manage other agents, tasks, schedules, skills, connectors, sessions, and encrypted secrets via built-in platform tools
31
34
  - **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
@@ -35,6 +38,9 @@ Inspired by [OpenClaw](https://github.com/openclaw).
35
38
  - **Scheduling** — Cron-based agent scheduling with human-friendly presets
36
39
  - **Loop Runtime Controls** — Switch between bounded and ongoing loops with configurable step caps, runtime guards, heartbeat cadence, and timeout budgets
37
40
  - **Session Run Queue** — Per-session queued runs with followup/steer/collect modes, collect coalescing for bursty inputs, and run-state APIs
41
+ - **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
42
+ - **Live Chat Telemetry** — Thinking/tool/responding stream phases, live main-loop status badges, connector activity presence, tone indicator, and optional sound notifications
43
+ - **Preview-Rich Chat UI** — Side preview panel for tool outputs (image/browser/html/code), inline code/PDF previews for attachments, and image lightbox support
38
44
  - **Voice Settings** — Per-instance ElevenLabs API key + voice ID for TTS replies, plus configurable speech recognition language for chat input
39
45
  - **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
46
  - **Skills System** — Discover local skills, import skills from URL, and load OpenClaw `SKILL.md` files (frontmatter-compatible)
@@ -53,7 +59,7 @@ Inspired by [OpenClaw](https://github.com/openclaw).
53
59
 
54
60
  ## Requirements
55
61
 
56
- - **Node.js** 20+
62
+ - **Node.js** 22.6+
57
63
  - **npm** 10+
58
64
  - **Claude Code CLI** (optional, for `claude-cli` provider) — [Install](https://docs.anthropic.com/en/docs/claude-code/overview)
59
65
  - **OpenAI Codex CLI** (optional, for `codex-cli` provider) — [Install](https://github.com/openai/codex)
@@ -75,7 +81,7 @@ curl -fsSL https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/install.
75
81
  ```
76
82
 
77
83
  The installer resolves the latest stable release tag and installs that version by default.
78
- To pin a version: `SWARMCLAW_VERSION=v0.1.0 curl ... | bash`
84
+ To pin a version: `SWARMCLAW_VERSION=v0.5.0 curl ... | bash`
79
85
 
80
86
  Or run locally from the repo (friendly for non-technical users):
81
87
 
package/bin/server-cmd.js CHANGED
@@ -17,14 +17,15 @@ const PID_FILE = path.join(SWARMCLAW_HOME, 'server.pid')
17
17
  const LOG_FILE = path.join(SWARMCLAW_HOME, 'server.log')
18
18
  const DATA_DIR = path.join(SWARMCLAW_HOME, 'data')
19
19
 
20
- // Files/directories to symlink from the npm package into SWARMCLAW_HOME
21
- const SYMLINKS = [
20
+ // Files/directories to copy from the npm package into SWARMCLAW_HOME
21
+ const BUILD_COPY_ENTRIES = [
22
22
  'src',
23
23
  'public',
24
24
  'next.config.ts',
25
25
  'tsconfig.json',
26
26
  'postcss.config.mjs',
27
27
  'package.json',
28
+ 'package-lock.json',
28
29
  ]
29
30
 
30
31
  // ---------------------------------------------------------------------------
@@ -61,16 +62,13 @@ function isProcessRunning(pid) {
61
62
  }
62
63
  }
63
64
 
64
- function symlink(src, dest) {
65
- // Remove existing symlink/file if present
66
- try {
67
- const stat = fs.lstatSync(dest)
68
- if (stat.isSymbolicLink() || stat.isFile()) {
69
- fs.unlinkSync(dest)
70
- }
71
- } catch {
72
- // doesn't exist
73
- }
65
+ function copyPath(src, dest, { dereference = true } = {}) {
66
+ fs.rmSync(dest, { recursive: true, force: true })
67
+ fs.cpSync(src, dest, { recursive: true, dereference })
68
+ }
69
+
70
+ function symlinkPath(src, dest) {
71
+ fs.rmSync(dest, { recursive: true, force: true })
74
72
  fs.symlinkSync(src, dest)
75
73
  }
76
74
 
@@ -89,8 +87,9 @@ function runBuild() {
89
87
  ensureDir(SWARMCLAW_HOME)
90
88
  ensureDir(DATA_DIR)
91
89
 
92
- // Create symlinks from PKG_ROOT into SWARMCLAW_HOME
93
- for (const entry of SYMLINKS) {
90
+ // Copy source/config into SWARMCLAW_HOME. Turbopack build currently rejects
91
+ // app source symlinks that point outside the workspace root.
92
+ for (const entry of BUILD_COPY_ENTRIES) {
94
93
  const src = path.join(PKG_ROOT, entry)
95
94
  const dest = path.join(SWARMCLAW_HOME, entry)
96
95
 
@@ -99,23 +98,33 @@ function runBuild() {
99
98
  continue
100
99
  }
101
100
 
102
- symlink(src, dest)
101
+ copyPath(src, dest)
103
102
  }
104
103
 
105
- // Symlink node_modules
104
+ // Reuse package dependencies via symlink to avoid multi-GB duplication in
105
+ // SWARMCLAW_HOME. Build runs with webpack mode for symlink compatibility.
106
106
  const nmSrc = path.join(PKG_ROOT, 'node_modules')
107
107
  const nmDest = path.join(SWARMCLAW_HOME, 'node_modules')
108
108
  if (fs.existsSync(nmSrc)) {
109
- symlink(nmSrc, nmDest)
109
+ symlinkPath(nmSrc, nmDest)
110
110
  } else {
111
111
  // If node_modules doesn't exist at PKG_ROOT, install
112
112
  log('Installing dependencies...')
113
- execSync('npm install --omit=dev', { cwd: SWARMCLAW_HOME, stdio: 'inherit' })
113
+ execSync('npm install', { cwd: SWARMCLAW_HOME, stdio: 'inherit' })
114
114
  }
115
115
 
116
116
  // Run Next.js build
117
117
  log('Building Next.js application (this may take a minute)...')
118
- execSync('npx next build', { cwd: SWARMCLAW_HOME, stdio: 'inherit' })
118
+ // Use webpack for production build reliability in packaged/fresh-install
119
+ // environments (Turbopack has intermittently failed during prerender).
120
+ execSync('npx next build --webpack', {
121
+ cwd: SWARMCLAW_HOME,
122
+ stdio: 'inherit',
123
+ env: {
124
+ ...process.env,
125
+ SWARMCLAW_BUILD_MODE: '1',
126
+ },
127
+ })
119
128
 
120
129
  // Write built marker
121
130
  fs.writeFileSync(BUILT_MARKER, JSON.stringify({ builtAt: new Date().toISOString(), version: getVersion() }))
package/next.config.ts CHANGED
@@ -11,6 +11,11 @@ function getGitSha(): string {
11
11
 
12
12
  const nextConfig: NextConfig = {
13
13
  output: 'standalone',
14
+ experimental: {
15
+ // Disable Turbopack persistent cache — concurrent HMR writes cause
16
+ // "Another write batch or compaction is already active" errors
17
+ turbopackFileSystemCacheForDev: false,
18
+ },
14
19
  env: {
15
20
  NEXT_PUBLIC_GIT_SHA: getGitSha(),
16
21
  NEXT_PUBLIC_WS_PORT: String((Number(process.env.PORT) || 3456) + 1),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "0.4.5",
3
+ "version": "0.5.0",
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": {
@@ -58,6 +58,7 @@
58
58
  "@langchain/core": "^1.1.26",
59
59
  "@langchain/langgraph": "^1.1.5",
60
60
  "@langchain/openai": "^1.2.8",
61
+ "@multiavatar/multiavatar": "^1.0.7",
61
62
  "@playwright/mcp": "^0.0.68",
62
63
  "@slack/bolt": "^4.6.0",
63
64
  "@whiskeysockets/baileys": "^7.0.0-rc.9",
@@ -1,10 +1,10 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { loadAgents, saveAgents, deleteAgent } from '@/lib/server/storage'
2
+ import { loadAgents, saveAgents, loadSessions, saveSessions } from '@/lib/server/storage'
3
3
  import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
4
- import { mutateItem, deleteItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
4
+ import { mutateItem, notFound, type CollectionOps } from '@/lib/server/collection-helpers'
5
5
 
6
6
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
- const ops: CollectionOps<any> = { load: loadAgents, save: saveAgents, deleteFn: deleteAgent, topic: 'agents' }
7
+ const ops: CollectionOps<any> = { load: () => loadAgents({ includeTrashed: true }), save: saveAgents, topic: 'agents' }
8
8
 
9
9
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
10
10
  const { id } = await params
@@ -27,6 +27,24 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
27
27
 
28
28
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
29
29
  const { id } = await params
30
- if (!deleteItem(ops, id)) return notFound()
31
- return NextResponse.json({ ok: true })
30
+ // Soft delete set trashedAt instead of removing the record
31
+ const result = mutateItem(ops, id, (agent) => {
32
+ agent.trashedAt = Date.now()
33
+ return agent
34
+ })
35
+ if (!result) return notFound()
36
+
37
+ // Detach sessions from the trashed agent
38
+ const sessions = loadSessions()
39
+ let detachedSessions = 0
40
+ for (const session of Object.values(sessions) as Array<Record<string, unknown>>) {
41
+ if (!session || session.agentId !== id) continue
42
+ session.agentId = null
43
+ detachedSessions += 1
44
+ }
45
+ if (detachedSessions > 0) {
46
+ saveSessions(sessions)
47
+ }
48
+
49
+ return NextResponse.json({ ok: true, detachedSessions })
32
50
  }
@@ -0,0 +1,44 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadTrashedAgents, loadAgents, saveAgents, deleteAgent } from '@/lib/server/storage'
3
+ import { notify } from '@/lib/server/ws-hub'
4
+ import { badRequest, notFound } from '@/lib/server/collection-helpers'
5
+
6
+ /** GET — list trashed agents */
7
+ export async function GET() {
8
+ return NextResponse.json(loadTrashedAgents())
9
+ }
10
+
11
+ /** POST { id } — restore a trashed agent */
12
+ export async function POST(req: Request) {
13
+ const body = await req.json()
14
+ const id = body?.id as string | undefined
15
+ if (!id) return badRequest('Missing agent id')
16
+
17
+ const all = loadAgents({ includeTrashed: true })
18
+ const agent = all[id]
19
+ if (!agent) return notFound()
20
+ if (!agent.trashedAt) return badRequest('Agent is not trashed')
21
+
22
+ delete agent.trashedAt
23
+ agent.updatedAt = Date.now()
24
+ all[id] = agent
25
+ saveAgents(all)
26
+ notify('agents')
27
+ return NextResponse.json(agent)
28
+ }
29
+
30
+ /** DELETE { id } — permanently delete a trashed agent */
31
+ export async function DELETE(req: Request) {
32
+ const body = await req.json()
33
+ const id = body?.id as string | undefined
34
+ if (!id) return badRequest('Missing agent id')
35
+
36
+ const all = loadAgents({ includeTrashed: true })
37
+ const agent = all[id]
38
+ if (!agent) return notFound()
39
+ if (!agent.trashedAt) return badRequest('Agent must be trashed before permanent deletion')
40
+
41
+ deleteAgent(id)
42
+ notify('agents')
43
+ return NextResponse.json({ ok: true })
44
+ }
@@ -9,14 +9,17 @@ export async function GET(_req: Request, { params }: { params: Promise<{ id: str
9
9
  const connector = connectors[id]
10
10
  if (!connector) return notFound()
11
11
 
12
- // Merge runtime status and QR code
12
+ // Merge runtime status, QR code, and presence
13
13
  try {
14
- const { getConnectorStatus, getConnectorQR, isConnectorAuthenticated, hasConnectorCredentials } = await import('@/lib/server/connectors/manager')
14
+ const { getConnectorStatus, getConnectorQR, isConnectorAuthenticated, hasConnectorCredentials, getConnectorPresence } = await import('@/lib/server/connectors/manager')
15
15
  connector.status = getConnectorStatus(id)
16
16
  const qr = getConnectorQR(id)
17
17
  if (qr) connector.qrDataUrl = qr
18
18
  connector.authenticated = isConnectorAuthenticated(id)
19
19
  connector.hasCredentials = hasConnectorCredentials(id)
20
+ if (connector.status === 'running') {
21
+ connector.presence = getConnectorPresence(id)
22
+ }
20
23
  } catch { /* ignore */ }
21
24
 
22
25
  return NextResponse.json(connector)
@@ -41,10 +44,10 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
41
44
  } else {
42
45
  await manager.repairConnector(id)
43
46
  }
44
- } catch (err: any) {
47
+ } catch (err: unknown) {
45
48
  // Re-read to get the error state saved by startConnector
46
49
  const fresh = loadConnectors()
47
- return NextResponse.json(fresh[id] || { error: err.message }, { status: 500 })
50
+ return NextResponse.json(fresh[id] || { error: err instanceof Error ? err.message : String(err) }, { status: 500 })
48
51
  }
49
52
  // Re-read the connector after manager modified it
50
53
  const fresh = loadConnectors()
@@ -58,6 +58,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
58
58
  try {
59
59
  if (connector.platform === 'teams') {
60
60
  const handlerKey = `__swarmclaw_teams_handler_${connector.id}__`
61
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- dynamic globalThis handler registered at runtime by connector
61
62
  const handler = (globalThis as any)[handlerKey]
62
63
  if (typeof handler !== 'function') {
63
64
  return NextResponse.json({ error: 'Teams connector is not running or not ready' }, { status: 409 })
@@ -68,6 +69,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
68
69
 
69
70
  if (connector.platform === 'googlechat') {
70
71
  const handlerKey = `__swarmclaw_googlechat_handler_${connector.id}__`
72
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- dynamic globalThis handler registered at runtime by connector
71
73
  const handler = (globalThis as any)[handlerKey]
72
74
  if (typeof handler !== 'function') {
73
75
  return NextResponse.json({ error: 'Google Chat connector is not running or not ready' }, { status: 409 })
@@ -81,6 +83,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
81
83
 
82
84
  if (connector.platform === 'bluebubbles') {
83
85
  const handlerKey = `__swarmclaw_bluebubbles_handler_${connector.id}__`
86
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- dynamic globalThis handler registered at runtime by connector
84
87
  const handler = (globalThis as any)[handlerKey]
85
88
  if (typeof handler !== 'function') {
86
89
  return NextResponse.json({ error: 'BlueBubbles connector is not running or not ready' }, { status: 409 })
@@ -93,7 +96,8 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
93
96
  }
94
97
 
95
98
  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 })
99
+ } catch (err: unknown) {
100
+ const message = err instanceof Error ? err.message : 'Webhook processing failed'
101
+ return NextResponse.json({ error: message }, { status: 500 })
98
102
  }
99
103
  }
@@ -0,0 +1,57 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
3
+
4
+ const AGENT_FILES = ['SOUL.md', 'IDENTITY.md', 'USER.md', 'TOOLS.md', 'HEARTBEAT.md', 'MEMORY.md', 'AGENTS.md'] as const
5
+
6
+ /** GET ?agentId=X — fetch all agent files from gateway */
7
+ export async function GET(req: Request) {
8
+ const { searchParams } = new URL(req.url)
9
+ const agentId = searchParams.get('agentId')
10
+ if (!agentId) {
11
+ return NextResponse.json({ error: 'Missing agentId' }, { status: 400 })
12
+ }
13
+
14
+ const gw = await ensureGatewayConnected()
15
+ if (!gw) {
16
+ return NextResponse.json({ error: 'OpenClaw gateway not connected' }, { status: 503 })
17
+ }
18
+
19
+ const files: Record<string, { content: string; error?: string }> = {}
20
+ await Promise.all(
21
+ AGENT_FILES.map(async (filename) => {
22
+ try {
23
+ const result = await gw.rpc('agents.files.get', { agentId, filename }) as { content?: string } | undefined
24
+ files[filename] = { content: result?.content ?? '' }
25
+ } catch (err: unknown) {
26
+ files[filename] = { content: '', error: err instanceof Error ? err.message : String(err) }
27
+ }
28
+ }),
29
+ )
30
+
31
+ return NextResponse.json(files)
32
+ }
33
+
34
+ /** PUT { agentId, filename, content } — save an agent file */
35
+ export async function PUT(req: Request) {
36
+ const body = await req.json()
37
+ const { agentId, filename, content } = body as { agentId?: string; filename?: string; content?: string }
38
+ if (!agentId || !filename) {
39
+ return NextResponse.json({ error: 'Missing agentId or filename' }, { status: 400 })
40
+ }
41
+ if (!AGENT_FILES.includes(filename as typeof AGENT_FILES[number])) {
42
+ return NextResponse.json({ error: `Invalid filename: ${filename}` }, { status: 400 })
43
+ }
44
+
45
+ const gw = await ensureGatewayConnected()
46
+ if (!gw) {
47
+ return NextResponse.json({ error: 'OpenClaw gateway not connected' }, { status: 503 })
48
+ }
49
+
50
+ try {
51
+ await gw.rpc('agents.files.set', { agentId, filename, content: content ?? '' })
52
+ return NextResponse.json({ ok: true })
53
+ } catch (err: unknown) {
54
+ const message = err instanceof Error ? err.message : String(err)
55
+ return NextResponse.json({ error: message }, { status: 502 })
56
+ }
57
+ }
@@ -0,0 +1,46 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
3
+ import type { PendingExecApproval, ExecApprovalDecision } from '@/types'
4
+
5
+ /** GET — fetch pending execution approvals from gateway */
6
+ export async function GET() {
7
+ const gw = await ensureGatewayConnected()
8
+ if (!gw) {
9
+ return NextResponse.json([], { status: 200 })
10
+ }
11
+
12
+ try {
13
+ const result = await gw.rpc('exec.approvals.get') as PendingExecApproval[] | undefined
14
+ return NextResponse.json(result ?? [])
15
+ } catch {
16
+ return NextResponse.json([])
17
+ }
18
+ }
19
+
20
+ /** POST { id, decision } — resolve an execution approval */
21
+ export async function POST(req: Request) {
22
+ const body = await req.json()
23
+ const { id, decision } = body as { id?: string; decision?: ExecApprovalDecision }
24
+
25
+ if (!id || !decision) {
26
+ return NextResponse.json({ error: 'Missing id or decision' }, { status: 400 })
27
+ }
28
+
29
+ const validDecisions: ExecApprovalDecision[] = ['allow-once', 'allow-always', 'deny']
30
+ if (!validDecisions.includes(decision)) {
31
+ return NextResponse.json({ error: 'Invalid decision' }, { status: 400 })
32
+ }
33
+
34
+ const gw = await ensureGatewayConnected()
35
+ if (!gw) {
36
+ return NextResponse.json({ error: 'OpenClaw gateway not connected' }, { status: 503 })
37
+ }
38
+
39
+ try {
40
+ await gw.rpc('exec.approvals.resolve', { id, decision })
41
+ return NextResponse.json({ ok: true })
42
+ } catch (err: unknown) {
43
+ const message = err instanceof Error ? err.message : String(err)
44
+ return NextResponse.json({ error: message }, { status: 502 })
45
+ }
46
+ }
@@ -0,0 +1,33 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { detectConfigIssues, repairConfigIssue } from '@/lib/server/openclaw-config-sync'
3
+
4
+ /** GET — detect configuration issues */
5
+ export async function GET() {
6
+ try {
7
+ const issues = await detectConfigIssues()
8
+ return NextResponse.json({ issues })
9
+ } catch (err: unknown) {
10
+ const message = err instanceof Error ? err.message : String(err)
11
+ return NextResponse.json({ error: message }, { status: 502 })
12
+ }
13
+ }
14
+
15
+ /** POST { issueId } — repair a specific issue */
16
+ export async function POST(req: Request) {
17
+ const body = await req.json()
18
+ const { issueId } = body as { issueId?: string }
19
+ if (!issueId) {
20
+ return NextResponse.json({ error: 'Missing issueId' }, { status: 400 })
21
+ }
22
+
23
+ try {
24
+ const result = await repairConfigIssue(issueId)
25
+ if (!result.ok) {
26
+ return NextResponse.json({ error: result.error }, { status: 502 })
27
+ }
28
+ return NextResponse.json({ ok: true })
29
+ } catch (err: unknown) {
30
+ const message = err instanceof Error ? err.message : String(err)
31
+ return NextResponse.json({ error: message }, { status: 502 })
32
+ }
33
+ }
@@ -0,0 +1,52 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
3
+ import type { GatewayCronJob } from '@/types'
4
+
5
+ /** GET — list all cron jobs from gateway */
6
+ export async function GET() {
7
+ const gw = await ensureGatewayConnected()
8
+ if (!gw) {
9
+ return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
10
+ }
11
+
12
+ try {
13
+ const result = await gw.rpc('cron.list', { includeDisabled: true }) as GatewayCronJob[] | undefined
14
+ return NextResponse.json(result ?? [])
15
+ } catch (err: unknown) {
16
+ const message = err instanceof Error ? err.message : String(err)
17
+ return NextResponse.json({ error: message }, { status: 502 })
18
+ }
19
+ }
20
+
21
+ /** POST { action, ...params } — add/run/remove cron jobs */
22
+ export async function POST(req: Request) {
23
+ const body = await req.json()
24
+ const { action, ...params } = body as { action: string; [key: string]: unknown }
25
+
26
+ const gw = await ensureGatewayConnected()
27
+ if (!gw) {
28
+ return NextResponse.json({ error: 'Gateway not connected' }, { status: 503 })
29
+ }
30
+
31
+ try {
32
+ switch (action) {
33
+ case 'add': {
34
+ const result = await gw.rpc('cron.add', params.job)
35
+ return NextResponse.json({ ok: true, result })
36
+ }
37
+ case 'run': {
38
+ const result = await gw.rpc('cron.run', { id: params.id, mode: 'force' })
39
+ return NextResponse.json({ ok: true, result })
40
+ }
41
+ case 'remove': {
42
+ await gw.rpc('cron.remove', { id: params.id })
43
+ return NextResponse.json({ ok: true })
44
+ }
45
+ default:
46
+ return NextResponse.json({ error: `Unknown action: ${action}` }, { status: 400 })
47
+ }
48
+ } catch (err: unknown) {
49
+ const message = err instanceof Error ? err.message : String(err)
50
+ return NextResponse.json({ error: message }, { status: 502 })
51
+ }
52
+ }
@@ -3,7 +3,7 @@ export const dynamic = 'force-dynamic'
3
3
 
4
4
  export async function GET() {
5
5
  try {
6
- const { listRunningConnectors, getRunningInstance } = await import('@/lib/server/connectors/manager')
6
+ const { listRunningConnectors } = await import('@/lib/server/connectors/manager')
7
7
  const openclawConnectors = listRunningConnectors('openclaw')
8
8
 
9
9
  if (!openclawConnectors.length) {
@@ -20,7 +20,8 @@ export async function GET() {
20
20
  })),
21
21
  note: 'Directory listing requires OpenClaw gateway directory.list RPC support.',
22
22
  })
23
- } catch (err: any) {
24
- return NextResponse.json({ error: err.message || 'Directory listing failed' }, { status: 500 })
23
+ } catch (err: unknown) {
24
+ const message = err instanceof Error ? err.message : 'Directory listing failed'
25
+ return NextResponse.json({ error: message }, { status: 500 })
25
26
  }
26
27
  }
@@ -55,7 +55,8 @@ export async function GET() {
55
55
 
56
56
  const results = await Promise.all(probes)
57
57
  return NextResponse.json({ gateways: results })
58
- } catch (err: any) {
59
- return NextResponse.json({ error: err.message || 'Discovery failed' }, { status: 500 })
58
+ } catch (err: unknown) {
59
+ const message = err instanceof Error ? err.message : 'Discovery failed'
60
+ return NextResponse.json({ error: message }, { status: 500 })
60
61
  }
61
62
  }
@@ -0,0 +1,18 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { ensureGatewayConnected } from '@/lib/server/openclaw-gateway'
3
+
4
+ /** GET — list env var keys from gateway .env */
5
+ export async function GET() {
6
+ const gw = await ensureGatewayConnected()
7
+ if (!gw) {
8
+ return NextResponse.json({ error: 'OpenClaw gateway not connected' }, { status: 503 })
9
+ }
10
+
11
+ try {
12
+ const result = await gw.rpc('env.keys') as string[] | undefined
13
+ return NextResponse.json(result ?? [])
14
+ } catch (err: unknown) {
15
+ const message = err instanceof Error ? err.message : String(err)
16
+ return NextResponse.json({ error: message }, { status: 502 })
17
+ }
18
+ }
@@ -0,0 +1,41 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { getExecConfig, setExecConfig } from '@/lib/server/openclaw-exec-config'
3
+ import type { ExecApprovalConfig } from '@/types'
4
+
5
+ /** GET ?agentId=X — fetch exec approval config */
6
+ export async function GET(req: Request) {
7
+ const { searchParams } = new URL(req.url)
8
+ const agentId = searchParams.get('agentId')
9
+ if (!agentId) {
10
+ return NextResponse.json({ error: 'Missing agentId' }, { status: 400 })
11
+ }
12
+
13
+ try {
14
+ const snapshot = await getExecConfig(agentId)
15
+ return NextResponse.json(snapshot)
16
+ } catch (err: unknown) {
17
+ const message = err instanceof Error ? err.message : String(err)
18
+ return NextResponse.json({ error: message }, { status: 502 })
19
+ }
20
+ }
21
+
22
+ /** PUT { agentId, config, baseHash } — save exec approval config */
23
+ export async function PUT(req: Request) {
24
+ const body = await req.json()
25
+ const { agentId, config, baseHash } = body as {
26
+ agentId?: string
27
+ config?: ExecApprovalConfig
28
+ baseHash?: string
29
+ }
30
+ if (!agentId || !config) {
31
+ return NextResponse.json({ error: 'Missing agentId or config' }, { status: 400 })
32
+ }
33
+
34
+ try {
35
+ const result = await setExecConfig(agentId, config, baseHash ?? '')
36
+ return NextResponse.json(result)
37
+ } catch (err: unknown) {
38
+ const message = err instanceof Error ? err.message : String(err)
39
+ return NextResponse.json({ error: message }, { status: 502 })
40
+ }
41
+ }