@swarmclawai/swarmclaw 0.6.8 → 0.7.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 (166) hide show
  1. package/README.md +70 -45
  2. package/next.config.ts +31 -6
  3. package/package.json +3 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +1 -0
  5. package/src/app/api/agents/route.ts +18 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/clawhub/install/route.ts +2 -2
  8. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  9. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  10. package/src/app/api/memory/route.ts +36 -5
  11. package/src/app/api/notifications/route.ts +3 -0
  12. package/src/app/api/plugins/install/route.ts +57 -5
  13. package/src/app/api/plugins/marketplace/route.ts +73 -22
  14. package/src/app/api/plugins/route.ts +61 -1
  15. package/src/app/api/plugins/ui/route.ts +34 -0
  16. package/src/app/api/settings/route.ts +62 -0
  17. package/src/app/api/setup/doctor/route.ts +22 -5
  18. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  19. package/src/app/api/tasks/[id]/route.ts +11 -3
  20. package/src/app/api/tasks/route.ts +8 -2
  21. package/src/app/globals.css +27 -0
  22. package/src/app/page.tsx +10 -5
  23. package/src/cli/index.js +13 -0
  24. package/src/components/activity/activity-feed.tsx +9 -2
  25. package/src/components/agents/agent-avatar.tsx +5 -1
  26. package/src/components/agents/agent-card.tsx +55 -9
  27. package/src/components/agents/agent-sheet.tsx +86 -29
  28. package/src/components/agents/inspector-panel.tsx +1 -1
  29. package/src/components/auth/access-key-gate.tsx +63 -54
  30. package/src/components/auth/user-picker.tsx +37 -32
  31. package/src/components/chat/chat-area.tsx +11 -0
  32. package/src/components/chat/chat-header.tsx +69 -25
  33. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  34. package/src/components/chat/code-block.tsx +3 -1
  35. package/src/components/chat/exec-approval-card.tsx +8 -1
  36. package/src/components/chat/message-bubble.tsx +164 -4
  37. package/src/components/chat/message-list.tsx +30 -4
  38. package/src/components/chat/session-approval-card.tsx +80 -0
  39. package/src/components/chat/streaming-bubble.tsx +6 -5
  40. package/src/components/chat/thinking-indicator.tsx +48 -12
  41. package/src/components/chat/tool-request-banner.tsx +39 -20
  42. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  43. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  44. package/src/components/connectors/connector-list.tsx +33 -11
  45. package/src/components/connectors/connector-sheet.tsx +29 -6
  46. package/src/components/home/home-view.tsx +20 -14
  47. package/src/components/input/chat-input.tsx +22 -1
  48. package/src/components/knowledge/knowledge-list.tsx +17 -18
  49. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  50. package/src/components/layout/app-layout.tsx +73 -21
  51. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  52. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  53. package/src/components/memory/memory-list.tsx +20 -13
  54. package/src/components/plugins/plugin-list.tsx +213 -59
  55. package/src/components/plugins/plugin-sheet.tsx +119 -24
  56. package/src/components/projects/project-list.tsx +17 -9
  57. package/src/components/providers/provider-list.tsx +21 -6
  58. package/src/components/providers/provider-sheet.tsx +42 -25
  59. package/src/components/runs/run-list.tsx +17 -13
  60. package/src/components/schedules/schedule-card.tsx +10 -3
  61. package/src/components/schedules/schedule-list.tsx +2 -2
  62. package/src/components/schedules/schedule-sheet.tsx +19 -7
  63. package/src/components/secrets/secret-sheet.tsx +7 -2
  64. package/src/components/secrets/secrets-list.tsx +18 -5
  65. package/src/components/sessions/new-session-sheet.tsx +183 -376
  66. package/src/components/sessions/session-card.tsx +10 -2
  67. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  68. package/src/components/shared/command-palette.tsx +13 -5
  69. package/src/components/shared/empty-state.tsx +20 -8
  70. package/src/components/shared/notification-center.tsx +134 -86
  71. package/src/components/shared/profile-sheet.tsx +4 -0
  72. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  73. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  74. package/src/components/shared/settings/section-runtime-loop.tsx +144 -0
  75. package/src/components/skills/clawhub-browser.tsx +1 -0
  76. package/src/components/skills/skill-list.tsx +31 -12
  77. package/src/components/skills/skill-sheet.tsx +20 -7
  78. package/src/components/tasks/approvals-panel.tsx +170 -66
  79. package/src/components/tasks/task-board.tsx +20 -12
  80. package/src/components/tasks/task-card.tsx +21 -7
  81. package/src/components/tasks/task-column.tsx +4 -3
  82. package/src/components/tasks/task-list.tsx +1 -1
  83. package/src/components/tasks/task-sheet.tsx +130 -1
  84. package/src/components/ui/dialog.tsx +1 -0
  85. package/src/components/ui/sheet.tsx +1 -0
  86. package/src/components/usage/metrics-dashboard.tsx +66 -64
  87. package/src/components/wallets/wallet-panel.tsx +65 -41
  88. package/src/components/wallets/wallet-section.tsx +9 -3
  89. package/src/components/webhooks/webhook-list.tsx +21 -12
  90. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  91. package/src/lib/approval-display.test.ts +45 -0
  92. package/src/lib/approval-display.ts +62 -0
  93. package/src/lib/clipboard.ts +38 -0
  94. package/src/lib/memory.ts +8 -0
  95. package/src/lib/providers/claude-cli.ts +5 -3
  96. package/src/lib/providers/index.ts +67 -21
  97. package/src/lib/runtime-loop.ts +3 -2
  98. package/src/lib/server/approvals.ts +150 -0
  99. package/src/lib/server/chat-execution.ts +223 -62
  100. package/src/lib/server/clawhub-client.ts +82 -6
  101. package/src/lib/server/connectors/manager.ts +27 -1
  102. package/src/lib/server/cost.test.ts +73 -0
  103. package/src/lib/server/cost.ts +165 -34
  104. package/src/lib/server/daemon-state.ts +42 -0
  105. package/src/lib/server/data-dir.ts +18 -1
  106. package/src/lib/server/integrity-monitor.ts +208 -0
  107. package/src/lib/server/llm-response-cache.test.ts +102 -0
  108. package/src/lib/server/llm-response-cache.ts +227 -0
  109. package/src/lib/server/main-agent-loop.ts +1 -1
  110. package/src/lib/server/main-session.ts +6 -3
  111. package/src/lib/server/mcp-conformance.test.ts +18 -0
  112. package/src/lib/server/mcp-conformance.ts +233 -0
  113. package/src/lib/server/memory-db.ts +180 -17
  114. package/src/lib/server/memory-retrieval.test.ts +56 -0
  115. package/src/lib/server/orchestrator-lg.ts +4 -1
  116. package/src/lib/server/orchestrator.ts +4 -3
  117. package/src/lib/server/plugins.ts +650 -142
  118. package/src/lib/server/process-manager.ts +18 -0
  119. package/src/lib/server/queue.ts +253 -11
  120. package/src/lib/server/runtime-settings.ts +9 -0
  121. package/src/lib/server/session-run-manager.test.ts +23 -0
  122. package/src/lib/server/session-run-manager.ts +11 -1
  123. package/src/lib/server/session-tools/canvas.ts +85 -50
  124. package/src/lib/server/session-tools/chatroom.ts +130 -127
  125. package/src/lib/server/session-tools/connector.ts +233 -454
  126. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  127. package/src/lib/server/session-tools/crud.ts +84 -7
  128. package/src/lib/server/session-tools/delegate.ts +351 -752
  129. package/src/lib/server/session-tools/discovery.ts +198 -0
  130. package/src/lib/server/session-tools/edit_file.ts +82 -0
  131. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  132. package/src/lib/server/session-tools/file.ts +257 -425
  133. package/src/lib/server/session-tools/git.ts +87 -47
  134. package/src/lib/server/session-tools/http.ts +85 -33
  135. package/src/lib/server/session-tools/index.ts +205 -160
  136. package/src/lib/server/session-tools/memory.ts +152 -265
  137. package/src/lib/server/session-tools/monitor.ts +126 -0
  138. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  139. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  140. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  141. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  142. package/src/lib/server/session-tools/platform.ts +86 -0
  143. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  144. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  145. package/src/lib/server/session-tools/sandbox.ts +175 -148
  146. package/src/lib/server/session-tools/schedule.ts +66 -31
  147. package/src/lib/server/session-tools/session-info.ts +104 -410
  148. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  149. package/src/lib/server/session-tools/shell.ts +171 -143
  150. package/src/lib/server/session-tools/subagent.ts +77 -77
  151. package/src/lib/server/session-tools/wallet.ts +182 -106
  152. package/src/lib/server/session-tools/web.ts +179 -349
  153. package/src/lib/server/storage.ts +24 -0
  154. package/src/lib/server/stream-agent-chat.ts +301 -244
  155. package/src/lib/server/task-quality-gate.test.ts +44 -0
  156. package/src/lib/server/task-quality-gate.ts +67 -0
  157. package/src/lib/server/task-validation.test.ts +78 -0
  158. package/src/lib/server/task-validation.ts +67 -2
  159. package/src/lib/server/tool-aliases.ts +68 -0
  160. package/src/lib/server/tool-capability-policy.ts +23 -5
  161. package/src/lib/tasks.ts +7 -1
  162. package/src/lib/tool-definitions.ts +23 -23
  163. package/src/lib/validation/schemas.ts +12 -0
  164. package/src/lib/view-routes.ts +2 -24
  165. package/src/stores/use-app-store.ts +23 -1
  166. package/src/types/index.ts +121 -7
package/README.md CHANGED
@@ -12,7 +12,7 @@ The orchestration dashboard for OpenClaw. Manage a swarm of OpenClaws + 14 other
12
12
 
13
13
  Inspired by [OpenClaw](https://github.com/openclaw).
14
14
 
15
- **[Documentation](https://swarmclaw.ai/docs)** | **[Website](https://swarmclaw.ai)**
15
+ **[Documentation](https://swarmclaw.ai/docs)** | **[Plugin Tutorial](https://swarmclaw.ai/docs/plugin-tutorial)** | **[Website](https://swarmclaw.ai)**
16
16
 
17
17
  ![Dashboard](public/screenshots/dashboard.png)
18
18
  ![Agent Builder](public/screenshots/agents.png)
@@ -85,7 +85,7 @@ curl -fsSL https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/install.
85
85
  ```
86
86
 
87
87
  The installer resolves the latest stable release tag and installs that version by default.
88
- To pin a version: `SWARMCLAW_VERSION=v0.6.6 curl ... | bash`
88
+ To pin a version: `SWARMCLAW_VERSION=v0.7.0 curl ... | bash`
89
89
 
90
90
  Or run locally from the repo (friendly for non-technical users):
91
91
 
@@ -201,8 +201,9 @@ Notes:
201
201
  All config lives in `.env.local` (auto-generated):
202
202
 
203
203
  ```
204
- ACCESS_KEY=<your-access-key> # Auth key for the dashboard
205
- CREDENTIAL_SECRET=<auto-generated> # AES-256 encryption key for stored credentials
204
+ ACCESS_KEY=<your-access-key> # Auth key for the dashboard
205
+ CREDENTIAL_SECRET=<auto-generated> # AES-256 encryption key for stored credentials
206
+ SWARMCLAW_PLUGIN_FAILURE_THRESHOLD=3 # Consecutive failures before auto-disabling a plugin
206
207
  ```
207
208
 
208
209
  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.
@@ -449,54 +450,68 @@ When enabled, new memories get vector embeddings. Search uses both FTS5 keyword
449
450
 
450
451
  Agents and sessions can have **fallback credentials**. If the primary API key gets a 401, 429, or 500 error, SwarmClaw automatically retries with the next credential. Configure fallback keys in the agent builder UI.
451
452
 
452
- ## Plugins
453
+ ## Plugin System
453
454
 
454
- Extend agent behavior with JS plugins. Three ways to install:
455
+ SwarmClaw features a powerful, modular plugin system designed for both agent enhancement and application extensibility. It is fully compatible with the **OpenClaw** plugin format.
455
456
 
456
- 1. **Marketplace** Browse and install approved plugins from Settings Plugins Marketplace
457
- 2. **URL** — Install from any HTTPS URL via Settings → Plugins → Install from URL
458
- 3. **Manual** — Drop `.js` files into `data/plugins/`
457
+ Plugins can be managed in **Settings Plugins** and installed via the Marketplace, URL, or by dropping `.js` files into `data/plugins/`.
459
458
 
460
- ### Plugin Format (SwarmClaw)
459
+ Docs:
460
+ - Full docs: https://swarmclaw.ai/docs
461
+ - Plugin tutorial: https://swarmclaw.ai/docs/plugin-tutorial
462
+
463
+ ### Extension Points
464
+
465
+ Unlike standard tool systems, SwarmClaw plugins can modify the application itself:
466
+
467
+ - **Agent Tools**: Define custom tools that agents can autonomously discover and use.
468
+ - **Lifecycle Hooks**: Intercept events like `beforeAgentStart`, `afterToolExec`, and `onMessage`.
469
+ - **UI Extensions**:
470
+ - `sidebarItems`: Inject new navigation links into the main sidebar.
471
+ - `headerWidgets`: Add status badges or indicators to the chat header (e.g., Wallet Balance).
472
+ - `chatInputActions`: Add custom action buttons next to the chat input (e.g., "Quick Scan").
473
+ - `plugin-ui` Messages: Render rich, interactive React cards in the chat stream.
474
+ - **Deep Chat Hooks**:
475
+ - `transformInboundMessage`: Modify user messages before they reach the agent.
476
+ - `transformOutboundMessage`: Modify agent responses before they are saved or displayed.
477
+ - **Custom Providers**: Add new LLM backends (e.g., a specialized local model or a new API).
478
+ - **Custom Connectors**: Build new chat platform bridges (e.g., a proprietary internal messenger).
479
+
480
+ ### Autonomous Capability Discovery
481
+
482
+ Agents in SwarmClaw are "aware" of the plugin system. If an agent lacks a tool needed for a task, it can:
483
+ 1. **Discover**: Scan the system for all installed plugins.
484
+ 2. **Search Marketplace**: Autonomously search **ClawHub** and the **SwarmClaw Registry** for new capabilities.
485
+ 3. **Request Access**: Prompt the user in-chat to enable a specific installed plugin.
486
+ 4. **Install Request**: Suggest installing a new plugin from a marketplace URL to fill a capability gap (requires user approval).
487
+
488
+ ### Example Plugin (SwarmClaw Format)
461
489
 
462
490
  ```js
463
491
  module.exports = {
464
- name: 'my-plugin',
465
- description: 'What it does',
466
- hooks: {
467
- beforeAgentStart: async ({ session, message }) => { /* ... */ },
468
- afterAgentComplete: async ({ session, response }) => { /* ... */ },
469
- beforeToolExec: async ({ toolName, input }) => { /* return { abort: true } to cancel */ },
470
- afterToolExec: async ({ toolName, input, output }) => { /* ... */ },
471
- onMessage: async ({ session, message }) => { /* ... */ },
472
- onTaskComplete: async ({ taskId, result }) => { /* ... */ },
473
- onAgentDelegation: async ({ sourceAgentId, targetAgentId, task }) => { /* ... */ },
492
+ name: 'my-custom-extension',
493
+ ui: {
494
+ sidebarItems: [{ id: 'dashboard', label: 'My View', href: '/custom-view' }],
495
+ headerWidgets: [{ id: 'status', label: '🟢 Active' }]
474
496
  },
475
- // Plugins can also define custom tools that agents can use
476
- tools: [
477
- {
478
- name: 'my_custom_tool',
479
- description: 'Does something amazing',
480
- parameters: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
481
- execute: async (args, ctx) => 'Result: ' + args.query,
482
- },
483
- ],
484
- }
497
+ tools: [{
498
+ name: 'custom_action',
499
+ description: 'Perform a specialized task',
500
+ parameters: { type: 'object', properties: { input: { type: 'string' } } },
501
+ execute: async (args) => {
502
+ // Logic here
503
+ return { kind: 'plugin-ui', text: 'Rich result card content' };
504
+ }
505
+ }]
506
+ };
485
507
  ```
486
508
 
487
- ### OpenClaw Plugin Compatibility
509
+ ### Lifecycle Management
488
510
 
489
- SwarmClaw natively supports the OpenClaw plugin format. Drop an OpenClaw plugin into `data/plugins/` and it works automatically — lifecycle hooks are mapped:
490
-
491
- | OpenClaw Hook | SwarmClaw Hook |
492
- |-|-|
493
- | `onAgentStart` | `beforeAgentStart` |
494
- | `onAgentComplete` | `afterAgentComplete` |
495
- | `onToolCall` | `beforeToolExec` |
496
- | `onToolResult` | `afterToolExec` |
497
- | `onMessage` | `onMessage` |
498
-
499
- Plugin API: `GET /api/plugins`, `POST /api/plugins`, `GET /api/plugins/marketplace`, `POST /api/plugins/install`.
511
+ - **Versioning**: All plugins support semantic versioning (e.g., `v1.2.3`).
512
+ - **Updates**: Plugins can be updated individually or in bulk via the Plugins manager.
513
+ - **Hot-Reload**: The system automatically reloads plugin logic when a file is updated or a new plugin is installed.
514
+ - **Stability Guardrails**: Consecutive plugin failures are tracked in `data/plugin-failures.json`; failing plugins are auto-disabled, a warning notification is emitted in-app, and users can re-enable manually from the Plugins manager.
500
515
 
501
516
  ## Deploy to a VPS
502
517
 
@@ -586,7 +601,7 @@ npm run dev:webpack
586
601
  ### First-Run Helpers
587
602
 
588
603
  ```bash
589
- npm run setup:easy # setup only (does not start server)
604
+ npm run setup:easy # setup only (installs Deno if missing; does not start server)
590
605
  npm run quickstart # setup + start dev server
591
606
  npm run quickstart:prod # setup + build + start production server
592
607
  npm run update:easy # safe update helper for local installs
@@ -597,8 +612,8 @@ npm run update:easy # safe update helper for local installs
597
612
  SwarmClaw uses tag-based releases (`vX.Y.Z`) as the stable channel.
598
613
 
599
614
  ```bash
600
- # example patch release
601
- npm version patch
615
+ # example minor release (v0.7.0 style)
616
+ npm version minor
602
617
  git push origin main --follow-tags
603
618
  ```
604
619
 
@@ -607,6 +622,16 @@ On `v*` tags, GitHub Actions will:
607
622
  2. Create a GitHub Release
608
623
  3. Build and publish Docker images to `ghcr.io/swarmclawai/swarmclaw` (`:vX.Y.Z`, `:latest`, `:sha-*`)
609
624
 
625
+ #### v0.7.0 Release Readiness Notes
626
+
627
+ Before shipping `v0.7.0`, confirm the following user-facing changes are reflected in docs:
628
+
629
+ 1. Plugins UI now shows richer installed plugin details (description fallback, source/status, capability badges, and clearer detail sheet metadata).
630
+ 2. Marketplace plugin installs normalize legacy URLs from `swarmclawai/plugins` to `swarmclawai/swarmforge` to avoid 404 installs.
631
+ 3. Hydration fixes removed nested `<button>` structures in list-card UIs (Plugins, Providers, Secrets, MCP Servers).
632
+ 4. Clipboard actions now use a browser-safe fallback when `navigator.clipboard` is unavailable.
633
+ 5. Site docs/release notes are updated in `swarmclaw-site` (especially `content/docs/plugins.md`, `content/docs/release-notes.md`, and `public/registry/plugins.json`).
634
+
610
635
  ## CLI
611
636
 
612
637
  SwarmClaw ships a built-in CLI for core operational workflows:
package/next.config.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import type { NextConfig } from "next";
2
2
  import { execSync } from "child_process";
3
+ import { networkInterfaces } from "os";
4
+ import { DIRECT_NAV_SEGMENTS } from "./view-route-paths";
3
5
 
4
6
  function getGitSha(): string {
5
7
  try {
@@ -9,6 +11,33 @@ function getGitSha(): string {
9
11
  }
10
12
  }
11
13
 
14
+ function getAllowedDevOrigins(): string[] {
15
+ const allowed = new Set<string>([
16
+ 'localhost',
17
+ '127.0.0.1',
18
+ '0.0.0.0',
19
+ ])
20
+
21
+ // Include all active local IPv4 interfaces so LAN devices can load /_next assets in dev.
22
+ for (const interfaces of Object.values(networkInterfaces())) {
23
+ for (const iface of interfaces ?? []) {
24
+ if ((iface.family === 'IPv4' || (iface.family as string | number) === 4) && !iface.internal) {
25
+ allowed.add(iface.address)
26
+ }
27
+ }
28
+ }
29
+
30
+ // Optional override for custom origins/hosts, e.g. `NEXT_ALLOWED_DEV_ORIGINS=host1,host2`.
31
+ const extra = (process.env.NEXT_ALLOWED_DEV_ORIGINS ?? '')
32
+ .split(',')
33
+ .map((v) => v.trim())
34
+ .filter(Boolean)
35
+ .map((v) => v.replace(/^https?:\/\//, '').replace(/\/$/, ''))
36
+ for (const host of extra) allowed.add(host)
37
+
38
+ return [...allowed]
39
+ }
40
+
12
41
  const nextConfig: NextConfig = {
13
42
  output: 'standalone',
14
43
  turbopack: {
@@ -35,13 +64,9 @@ const nextConfig: NextConfig = {
35
64
  '@whiskeysockets/baileys',
36
65
  'qrcode',
37
66
  ],
38
- allowedDevOrigins: [
39
- 'localhost',
40
- '127.0.0.1',
41
- '0.0.0.0',
42
- ],
67
+ allowedDevOrigins: getAllowedDevOrigins(),
43
68
  async rewrites() {
44
- const views = 'agents|chatrooms|schedules|memory|tasks|secrets|providers|skills|connectors|webhooks|mcp-servers|knowledge|plugins|usage|runs|logs|settings|projects|activity'
69
+ const views = DIRECT_NAV_SEGMENTS.join('|')
45
70
  return [
46
71
  {
47
72
  source: `/:view(${views})`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "0.6.8",
3
+ "version": "0.7.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": {
@@ -52,7 +52,8 @@
52
52
  "lint:baseline:update": "node ./scripts/lint-baseline.mjs update",
53
53
  "cli": "node ./bin/swarmclaw.js",
54
54
  "test:cli": "node --test src/cli/index.test.js",
55
- "test:openclaw": "tsx --test src/lib/server/connectors/openclaw.test.ts src/lib/openclaw-endpoint.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/tool-capability-policy.test.ts",
55
+ "test:openclaw": "tsx --test src/lib/server/connectors/openclaw.test.ts src/lib/openclaw-endpoint.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/server/task-validation.test.ts src/lib/server/task-quality-gate.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts",
56
+ "test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
56
57
  "postinstall": "node ./scripts/postinstall.mjs"
57
58
  },
58
59
  "dependencies": {
@@ -49,6 +49,7 @@ export async function POST(req: Request, { params }: { params: Promise<{ id: str
49
49
  createdAt: now,
50
50
  lastActiveAt: now,
51
51
  active: false,
52
+ mainSession: true,
52
53
  sessionType: 'human' as const,
53
54
  agentId,
54
55
  tools: agent.tools || [],
@@ -1,9 +1,9 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { genId } from '@/lib/id'
3
- import { loadAgents, saveAgents, logActivity } from '@/lib/server/storage'
3
+ import { loadAgents, loadSessions, loadUsage, 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'
6
+ import { getAgentSpendWindows } from '@/lib/server/cost'
7
7
  import { AgentCreateSchema, formatZodError } from '@/lib/validation/schemas'
8
8
  import { z } from 'zod'
9
9
  export const dynamic = 'force-dynamic'
@@ -11,10 +11,19 @@ export const dynamic = 'force-dynamic'
11
11
 
12
12
  export async function GET(req: Request) {
13
13
  const agents = loadAgents()
14
- // Enrich agents that have a monthly budget with current spend
14
+ const sessions = loadSessions()
15
+ const usage = loadUsage()
16
+ // Enrich agents that have spend limits with current spend windows
15
17
  for (const agent of Object.values(agents)) {
16
- if (typeof agent.monthlyBudget === 'number' && agent.monthlyBudget > 0) {
17
- agent.monthlySpend = getAgentMonthlySpend(agent.id)
18
+ if (
19
+ (typeof agent.monthlyBudget === 'number' && agent.monthlyBudget > 0)
20
+ || (typeof agent.dailyBudget === 'number' && agent.dailyBudget > 0)
21
+ || (typeof agent.hourlyBudget === 'number' && agent.hourlyBudget > 0)
22
+ ) {
23
+ const spend = getAgentSpendWindows(agent.id, Date.now(), { sessions, usage })
24
+ if (typeof agent.monthlyBudget === 'number' && agent.monthlyBudget > 0) agent.monthlySpend = spend.monthly
25
+ if (typeof agent.dailyBudget === 'number' && agent.dailyBudget > 0) agent.dailySpend = spend.daily
26
+ if (typeof agent.hourlyBudget === 'number' && agent.hourlyBudget > 0) agent.hourlySpend = spend.hourly
18
27
  }
19
28
  }
20
29
 
@@ -54,6 +63,10 @@ export async function POST(req: Request) {
54
63
  capabilities: body.capabilities,
55
64
  thinkingLevel: body.thinkingLevel || undefined,
56
65
  autoRecovery: body.autoRecovery || false,
66
+ monthlyBudget: body.monthlyBudget ?? null,
67
+ dailyBudget: body.dailyBudget ?? null,
68
+ hourlyBudget: body.hourlyBudget ?? null,
69
+ budgetAction: body.budgetAction || 'warn',
57
70
  soul: body.soul || undefined,
58
71
  createdAt: now,
59
72
  updatedAt: now,
@@ -0,0 +1,22 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { listPendingApprovals, submitDecision } from '@/lib/server/approvals'
3
+
4
+ export const dynamic = 'force-dynamic'
5
+
6
+ export async function GET() {
7
+ return NextResponse.json(listPendingApprovals())
8
+ }
9
+
10
+ export async function POST(req: Request) {
11
+ try {
12
+ const body = await req.json()
13
+ const { id, approved } = body
14
+ if (!id || typeof approved !== 'boolean') {
15
+ return NextResponse.json({ error: 'id and approved required' }, { status: 400 })
16
+ }
17
+ await submitDecision(id, approved)
18
+ return NextResponse.json({ ok: true })
19
+ } catch (err: unknown) {
20
+ return NextResponse.json({ error: err instanceof Error ? err.message : String(err) }, { status: 500 })
21
+ }
22
+ }
@@ -11,9 +11,9 @@ export async function POST(req: Request) {
11
11
  if (!content) {
12
12
  try {
13
13
  content = await fetchSkillContent(url)
14
- } catch (err: any) {
14
+ } catch (err: unknown) {
15
15
  return NextResponse.json(
16
- { error: err.message || 'Failed to fetch skill content' },
16
+ { error: err instanceof Error ? err.message : 'Failed to fetch skill content' },
17
17
  { status: 502 }
18
18
  )
19
19
  }
@@ -0,0 +1,26 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadMcpServers } from '@/lib/server/storage'
3
+ import { notFound } from '@/lib/server/collection-helpers'
4
+ import { runMcpConformanceCheck } from '@/lib/server/mcp-conformance'
5
+
6
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
7
+ const { id } = await params
8
+ const servers = loadMcpServers()
9
+ const server = servers[id]
10
+ if (!server) return notFound()
11
+
12
+ const body = await req.json().catch(() => ({}))
13
+ const timeoutMs = typeof body?.timeoutMs === 'number' ? body.timeoutMs : undefined
14
+ const smokeToolName = typeof body?.smokeToolName === 'string' ? body.smokeToolName : undefined
15
+ const smokeToolArgs = body?.smokeToolArgs && typeof body.smokeToolArgs === 'object' && !Array.isArray(body.smokeToolArgs)
16
+ ? body.smokeToolArgs
17
+ : undefined
18
+
19
+ const result = await runMcpConformanceCheck(server, {
20
+ timeoutMs,
21
+ smokeToolName,
22
+ smokeToolArgs,
23
+ })
24
+
25
+ return NextResponse.json(result, { status: result.ok ? 200 : 502 })
26
+ }
@@ -0,0 +1,81 @@
1
+ import { NextResponse } from 'next/server'
2
+ import { loadMcpServers } from '@/lib/server/storage'
3
+ import { notFound } from '@/lib/server/collection-helpers'
4
+ import { connectMcpServer, disconnectMcpServer } from '@/lib/server/mcp-client'
5
+
6
+ function parseArgs(value: unknown): Record<string, unknown> {
7
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
8
+ return value as Record<string, unknown>
9
+ }
10
+ if (typeof value === 'string' && value.trim()) {
11
+ try {
12
+ const parsed = JSON.parse(value)
13
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
14
+ return parsed as Record<string, unknown>
15
+ }
16
+ } catch {
17
+ // Handled by caller via fallback validation error.
18
+ }
19
+ }
20
+ return {}
21
+ }
22
+
23
+ export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) {
24
+ const { id } = await params
25
+ const servers = loadMcpServers()
26
+ const server = servers[id]
27
+ if (!server) return notFound()
28
+
29
+ const body = await req.json().catch(() => null)
30
+ const toolName = typeof body?.toolName === 'string' ? body.toolName.trim() : ''
31
+ if (!toolName) {
32
+ return NextResponse.json({ error: 'toolName is required' }, { status: 400 })
33
+ }
34
+
35
+ const argsRaw = body?.args
36
+ if (
37
+ argsRaw !== undefined
38
+ && typeof argsRaw !== 'string'
39
+ && (typeof argsRaw !== 'object' || Array.isArray(argsRaw))
40
+ ) {
41
+ return NextResponse.json({ error: 'args must be an object or JSON string' }, { status: 400 })
42
+ }
43
+ const args = parseArgs(argsRaw)
44
+
45
+ let client: unknown
46
+ let transport: unknown
47
+ try {
48
+ const conn = await connectMcpServer(server)
49
+ client = conn.client
50
+ transport = conn.transport
51
+ const result = await (client as { callTool: (opts: { name: string; arguments: Record<string, unknown> }) => Promise<Record<string, unknown>> }).callTool({
52
+ name: toolName,
53
+ arguments: args,
54
+ })
55
+ const textParts = Array.isArray(result?.content)
56
+ ? (result.content as Array<Record<string, unknown>>)
57
+ .filter((part) => part?.type === 'text' && typeof part?.text === 'string')
58
+ .map((part) => part.text as string)
59
+ : []
60
+ const text = textParts.join('\n').trim() || '(no text output)'
61
+
62
+ return NextResponse.json({
63
+ ok: true,
64
+ toolName,
65
+ args,
66
+ text,
67
+ result,
68
+ isError: result?.isError === true,
69
+ })
70
+ } catch (err: unknown) {
71
+ return NextResponse.json(
72
+ { ok: false, error: err instanceof Error ? err.message : 'MCP tool invocation failed' },
73
+ { status: 500 },
74
+ )
75
+ } finally {
76
+ if (client && transport) {
77
+ await disconnectMcpServer(client, transport)
78
+ }
79
+ }
80
+ }
81
+
@@ -1,7 +1,16 @@
1
1
  import { genId } from '@/lib/id'
2
2
  import fs from 'fs'
3
3
  import { NextResponse } from 'next/server'
4
- import { getMemoryDb, getMemoryLookupLimits, storeMemoryImageAsset, storeMemoryImageFromDataUrl } from '@/lib/server/memory-db'
4
+ import {
5
+ filterMemoriesByScope,
6
+ getMemoryDb,
7
+ getMemoryLookupLimits,
8
+ normalizeMemoryScopeMode,
9
+ storeMemoryImageAsset,
10
+ storeMemoryImageFromDataUrl,
11
+ type MemoryRerankMode,
12
+ type MemoryScopeFilter,
13
+ } from '@/lib/server/memory-db'
5
14
  import { resolveLookupRequest } from '@/lib/server/memory-graph'
6
15
  import type { MemoryReference, FileReference, MemoryImage } from '@/types'
7
16
 
@@ -21,10 +30,16 @@ export async function GET(req: Request) {
21
30
  const { searchParams } = new URL(req.url)
22
31
  const q = searchParams.get('q')
23
32
  const agentId = searchParams.get('agentId')
33
+ const rawScope = searchParams.get('scope')
24
34
  const envelope = searchParams.get('envelope') === 'true'
25
35
  const requestedDepth = parseOptionalInt(searchParams.get('depth'))
26
36
  const requestedLimit = parseOptionalInt(searchParams.get('limit'))
27
37
  const requestedLinkedLimit = parseOptionalInt(searchParams.get('linkedLimit'))
38
+ const scopeMode = normalizeMemoryScopeMode(rawScope ?? (agentId ? 'agent' : 'all'))
39
+ const scopeSessionId = searchParams.get('scopeSessionId')
40
+ const scopeProjectRoot = searchParams.get('projectRoot')
41
+ const rerankRaw = searchParams.get('rerank')
42
+ const rerankMode: MemoryRerankMode = rerankRaw === 'semantic' || rerankRaw === 'lexical' ? rerankRaw : 'balanced'
28
43
 
29
44
  const counts = searchParams.get('counts') === 'true'
30
45
  const db = getMemoryDb()
@@ -39,14 +54,27 @@ export async function GET(req: Request) {
39
54
  limit: requestedLimit,
40
55
  linkedLimit: requestedLinkedLimit,
41
56
  })
57
+ const scopeFilter: MemoryScopeFilter = {
58
+ mode: scopeMode,
59
+ agentId: agentId || null,
60
+ sessionId: scopeSessionId || null,
61
+ projectRoot: scopeProjectRoot || null,
62
+ }
42
63
 
43
64
  if (q) {
44
65
  if (limits.maxDepth > 0) {
45
- const result = db.searchWithLinked(q, agentId || undefined, limits.maxDepth, limits.maxPerLookup, limits.maxLinkedExpansion)
66
+ const result = db.searchWithLinked(
67
+ q,
68
+ agentId || undefined,
69
+ limits.maxDepth,
70
+ limits.maxPerLookup,
71
+ limits.maxLinkedExpansion,
72
+ { scope: scopeFilter, rerankMode },
73
+ )
46
74
  if (envelope) return NextResponse.json(result)
47
75
  return NextResponse.json(result.entries)
48
76
  }
49
- const base = db.search(q, agentId || undefined)
77
+ const base = db.search(q, agentId || undefined, { scope: scopeFilter, rerankMode })
50
78
  const entries = base.slice(0, limits.maxPerLookup)
51
79
  if (envelope) {
52
80
  return NextResponse.json({
@@ -59,11 +87,14 @@ export async function GET(req: Request) {
59
87
  return NextResponse.json(entries)
60
88
  }
61
89
 
62
- const entries = db.list(agentId || undefined, limits.maxPerLookup)
90
+ const scanLimit = Math.max(limits.maxPerLookup, 200)
91
+ const listed = db.list(undefined, scanLimit)
92
+ const filtered = filterMemoriesByScope(listed, scopeFilter)
93
+ const entries = filtered.slice(0, limits.maxPerLookup)
63
94
  if (envelope) {
64
95
  return NextResponse.json({
65
96
  entries,
66
- truncated: false,
97
+ truncated: filtered.length > entries.length,
67
98
  expandedLinkedCount: 0,
68
99
  limits,
69
100
  })
@@ -13,6 +13,9 @@ export async function GET(req: Request) {
13
13
  const all = loadNotifications()
14
14
  let entries = Object.values(all) as AppNotification[]
15
15
 
16
+ // Approval requests now have a dedicated Approvals view/badge; keep notifications focused on ops/events.
17
+ entries = entries.filter((e) => e.entityType !== 'approval')
18
+
16
19
  if (unreadOnly) {
17
20
  entries = entries.filter((e) => !e.read)
18
21
  }
@@ -1,12 +1,43 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import fs from 'fs'
3
3
  import path from 'path'
4
+ import { getPluginManager } from '@/lib/server/plugins'
4
5
 
5
6
  const PLUGINS_DIR = path.join(process.cwd(), 'data', 'plugins')
6
7
 
8
+ function toRawUrl(url: string): string {
9
+ if (url.includes('github.com') && url.includes('/blob/')) {
10
+ return url.replace('github.com', 'raw.githubusercontent.com').replace('/blob/', '/')
11
+ }
12
+ if (url.includes('gist.github.com')) {
13
+ return url.endsWith('/raw') ? url : `${url}/raw`
14
+ }
15
+ return url
16
+ }
17
+
18
+ function normalizeMarketplaceUrl(url: string): string {
19
+ const trimmed = typeof url === 'string' ? url.trim() : ''
20
+ if (!trimmed) return trimmed
21
+
22
+ let normalized = trimmed
23
+ .replace('github.com/swarmclawai/plugins/', 'github.com/swarmclawai/swarmforge/')
24
+ .replace('raw.githubusercontent.com/swarmclawai/plugins/', 'raw.githubusercontent.com/swarmclawai/swarmforge/')
25
+
26
+ normalized = toRawUrl(normalized)
27
+
28
+ // Legacy registry entries used master and old repo names.
29
+ normalized = normalized
30
+ .replace('/swarmclawai/swarmforge/master/', '/swarmclawai/swarmforge/main/')
31
+ .replace('/swarmclawai/plugins/master/', '/swarmclawai/swarmforge/main/')
32
+ .replace('/swarmclawai/plugins/main/', '/swarmclawai/swarmforge/main/')
33
+
34
+ return normalized
35
+ }
36
+
7
37
  export async function POST(req: Request) {
8
38
  const body = await req.json()
9
39
  const { url, filename } = body
40
+ const rawUrl = normalizeMarketplaceUrl(url)
10
41
 
11
42
  // Validate URL
12
43
  if (!url || typeof url !== 'string' || !url.startsWith('https://')) {
@@ -34,11 +65,27 @@ export async function POST(req: Request) {
34
65
  }
35
66
 
36
67
  try {
37
- const res = await fetch(url)
68
+ const res = await fetch(rawUrl, { signal: AbortSignal.timeout(15_000) })
38
69
  if (!res.ok) {
39
- throw new Error(`Download failed: ${res.status}`)
70
+ return NextResponse.json(
71
+ { error: `Download failed (HTTP ${res.status}) from ${rawUrl}` },
72
+ { status: 502 },
73
+ )
40
74
  }
41
- const code = await res.text()
75
+ const contentType = res.headers.get('content-type') || ''
76
+ let code = await res.text()
77
+
78
+ // Reject HTML responses (likely a GitHub page, not raw content)
79
+ if (contentType.includes('text/html') && code.includes('<!DOCTYPE')) {
80
+ return NextResponse.json(
81
+ { error: 'URL returned an HTML page instead of JavaScript. Use a raw/direct link to the .js file.' },
82
+ { status: 400 },
83
+ )
84
+ }
85
+
86
+ // Compatibility fix: Strip node-fetch requires if present, as modern Node has global fetch
87
+ code = code.replace(/const\s+fetch\s*=\s*require\(['"]node-fetch['"]\);?/g, '// node-fetch stripped for compatibility')
88
+ code = code.replace(/import\s+fetch\s+from\s+['"]node-fetch['"];?/g, '// node-fetch stripped for compatibility')
42
89
 
43
90
  // Ensure plugins directory exists
44
91
  if (!fs.existsSync(PLUGINS_DIR)) {
@@ -48,11 +95,16 @@ export async function POST(req: Request) {
48
95
  const dest = path.join(PLUGINS_DIR, sanitized)
49
96
  fs.writeFileSync(dest, code, 'utf8')
50
97
 
98
+ // Force plugin manager to re-scan so the new plugin appears in listings
99
+ getPluginManager().reload()
100
+
51
101
  return NextResponse.json({ ok: true, filename: sanitized })
52
102
  } catch (err: unknown) {
103
+ const msg = err instanceof Error ? err.message : String(err)
104
+ const isTimeout = msg.includes('abort') || msg.includes('timeout')
53
105
  return NextResponse.json(
54
- { error: 'Failed to install plugin', message: err instanceof Error ? err.message : String(err) },
55
- { status: 500 },
106
+ { error: isTimeout ? 'Download timed out — the plugin URL may be unreachable' : `Install failed: ${msg}` },
107
+ { status: isTimeout ? 504 : 500 },
56
108
  )
57
109
  }
58
110
  }