@swarmclawai/swarmclaw 1.5.31 → 1.5.34

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 (82) hide show
  1. package/README.md +25 -3
  2. package/bin/swarmclaw.js +35 -1
  3. package/package.json +3 -3
  4. package/scripts/run-next-build.mjs +87 -2
  5. package/src/app/api/agents/[id]/route.ts +8 -0
  6. package/src/app/api/agents/agents-route.test.ts +114 -0
  7. package/src/app/api/agents/route.ts +10 -0
  8. package/src/app/api/connectors/route.ts +25 -13
  9. package/src/app/api/credentials/[id]/route.ts +8 -1
  10. package/src/app/api/schedules/[id]/route.ts +8 -0
  11. package/src/app/api/secrets/[id]/route.ts +13 -1
  12. package/src/app/api/setup/check-provider/route.ts +45 -0
  13. package/src/app/api/setup/doctor/route.ts +5 -0
  14. package/src/cli/binary.test.js +11 -0
  15. package/src/cli/index.js +4 -4
  16. package/src/cli/index.test.js +5 -2
  17. package/src/cli/index.ts +1 -1
  18. package/src/components/agents/agent-sheet.tsx +16 -4
  19. package/src/components/agents/inspector-panel.tsx +5 -0
  20. package/src/components/auth/setup-wizard/step-agents.tsx +19 -1
  21. package/src/components/chat/activity-moment.tsx +3 -0
  22. package/src/components/chat/chat-header.tsx +23 -2
  23. package/src/components/chat/tool-call-bubble.tsx +20 -0
  24. package/src/hooks/setup-done-detection.test.ts +4 -2
  25. package/src/hooks/setup-done-detection.ts +1 -1
  26. package/src/lib/ollama-mode.test.ts +18 -1
  27. package/src/lib/ollama-mode.ts +8 -0
  28. package/src/lib/orchestrator-config.ts +5 -0
  29. package/src/lib/provider-sets.ts +4 -4
  30. package/src/lib/providers/acp-client.ts +116 -0
  31. package/src/lib/providers/cli-utils.test.ts +9 -1
  32. package/src/lib/providers/cli-utils.ts +89 -4
  33. package/src/lib/providers/cursor-cli.ts +172 -0
  34. package/src/lib/providers/goose.ts +149 -0
  35. package/src/lib/providers/index.ts +29 -1
  36. package/src/lib/providers/openai.ts +5 -1
  37. package/src/lib/providers/qwen-code-cli.ts +152 -0
  38. package/src/lib/server/agents/agent-availability.ts +2 -2
  39. package/src/lib/server/agents/agent-thread-session.ts +8 -0
  40. package/src/lib/server/agents/task-session.ts +8 -0
  41. package/src/lib/server/capability-router.ts +8 -2
  42. package/src/lib/server/chat-execution/chat-execution-utils.ts +13 -0
  43. package/src/lib/server/chat-execution/chat-turn-finalization.ts +8 -0
  44. package/src/lib/server/chat-execution/chat-turn-preparation.ts +5 -1
  45. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +13 -8
  46. package/src/lib/server/chat-execution/iteration-timers.test.ts +84 -0
  47. package/src/lib/server/chat-execution/iteration-timers.ts +18 -1
  48. package/src/lib/server/chat-execution/prompt-sections.ts +5 -3
  49. package/src/lib/server/chat-execution/stream-agent-chat.ts +7 -2
  50. package/src/lib/server/chatrooms/chatroom-helpers.ts +13 -0
  51. package/src/lib/server/chats/chat-session-service.ts +18 -0
  52. package/src/lib/server/connectors/session.ts +8 -0
  53. package/src/lib/server/context-manager.ts +5 -0
  54. package/src/lib/server/ollama-runtime.ts +2 -2
  55. package/src/lib/server/provider-health.ts +16 -4
  56. package/src/lib/server/provider-model-discovery.test.ts +8 -0
  57. package/src/lib/server/provider-model-discovery.ts +1 -1
  58. package/src/lib/server/runtime/daemon-state/core.ts +2 -2
  59. package/src/lib/server/runtime/heartbeat-service.ts +13 -10
  60. package/src/lib/server/runtime/queue/core.ts +30 -4
  61. package/src/lib/server/runtime/session-run-manager/enqueue.ts +1 -1
  62. package/src/lib/server/session-reset-policy.test.ts +16 -0
  63. package/src/lib/server/session-reset-policy.ts +9 -1
  64. package/src/lib/server/session-tools/context.ts +2 -2
  65. package/src/lib/server/session-tools/delegate.ts +334 -14
  66. package/src/lib/server/session-tools/index.ts +16 -3
  67. package/src/lib/server/session-tools/session-info.ts +4 -1
  68. package/src/lib/server/storage-auth-docker.test.ts +244 -0
  69. package/src/lib/server/storage-auth.test.ts +150 -0
  70. package/src/lib/server/storage-auth.ts +57 -22
  71. package/src/lib/server/storage-normalization.ts +19 -0
  72. package/src/lib/server/storage.ts +3 -0
  73. package/src/lib/server/tasks/task-resume.ts +23 -1
  74. package/src/lib/server/tool-aliases.ts +1 -1
  75. package/src/lib/server/tool-capability-policy.ts +4 -1
  76. package/src/lib/setup-defaults.test.ts +6 -0
  77. package/src/lib/setup-defaults.ts +146 -0
  78. package/src/lib/tool-definitions.ts +1 -1
  79. package/src/proxy.ts +7 -1
  80. package/src/types/misc.ts +4 -1
  81. package/src/types/provider.ts +1 -1
  82. package/src/types/session.ts +9 -0
package/README.md CHANGED
@@ -39,6 +39,10 @@ Extension tutorial: https://swarmclaw.ai/docs/extension-tutorial
39
39
  <td align="center"><img src="doc/assets/logos/codex.svg" width="32" alt="Codex"><br><sub>Codex</sub></td>
40
40
  <td align="center"><img src="doc/assets/logos/gemini-cli.svg" width="32" alt="Gemini CLI"><br><sub>Gemini CLI</sub></td>
41
41
  <td align="center"><img src="doc/assets/logos/opencode.svg" width="32" alt="OpenCode"><br><sub>OpenCode</sub></td>
42
+ <td align="center"><img src="doc/assets/logos/copilot-cli.svg" width="32" alt="Copilot CLI"><br><sub>Copilot</sub></td>
43
+ <td align="center"><img src="doc/assets/logos/cursor-cli.svg" width="32" alt="Cursor Agent CLI"><br><sub>Cursor</sub></td>
44
+ <td align="center"><img src="doc/assets/logos/qwen-code-cli.svg" width="32" alt="Qwen Code CLI"><br><sub>Qwen Code</sub></td>
45
+ <td align="center"><img src="doc/assets/logos/goose.svg" width="32" alt="Goose"><br><sub>Goose</sub></td>
42
46
  <td align="center"><img src="doc/assets/logos/anthropic.svg" width="32" alt="Anthropic"><br><sub>Anthropic</sub></td>
43
47
  <td align="center"><img src="doc/assets/logos/openai.svg" width="32" alt="OpenAI"><br><sub>OpenAI</sub></td>
44
48
  <td align="center"><img src="public/provider-logos/openrouter.png" width="32" alt="OpenRouter"><br><sub>OpenRouter</sub></td>
@@ -61,7 +65,7 @@ Extension tutorial: https://swarmclaw.ai/docs/extension-tutorial
61
65
  - Node.js 22.6+ (`nvm use` will pick up the repo's `.nvmrc`, which matches CI)
62
66
  - npm 10+ or another supported package manager
63
67
  - Docker Desktop is recommended for sandbox browser execution
64
- - Optional provider CLIs if you want delegated CLI backends such as Claude Code, Codex, OpenCode, or Gemini
68
+ - Optional provider CLIs if you want delegated CLI backends such as Claude Code, Codex, OpenCode, Gemini, Copilot, Cursor Agent, Qwen Code, or Goose
65
69
 
66
70
  ## Quick Start
67
71
 
@@ -147,10 +151,10 @@ Full hosted deployment guides live at https://swarmclaw.ai/docs/deployment
147
151
 
148
152
  ## Core Capabilities
149
153
 
150
- - **Providers**: OpenClaw, OpenAI, OpenRouter, Anthropic, Ollama, Hermes Agent, Google, DeepSeek, Groq, Together, Mistral, xAI, Fireworks, Nebius, DeepInfra, plus compatible custom endpoints.
154
+ - **Providers**: 23 built-in — Claude Code CLI, Codex CLI, OpenCode CLI, Gemini CLI, Copilot CLI, Cursor Agent CLI, Qwen Code CLI, Goose, Anthropic, OpenAI, OpenRouter, Google Gemini, DeepSeek, Groq, Together, Mistral, xAI, Fireworks, Nebius, DeepInfra, Ollama, OpenClaw, and Hermes Agent, plus compatible custom endpoints.
151
155
  - **OpenRouter**: <img src="public/provider-logos/openrouter.png" alt="OpenRouter logo" width="20" height="20" /> Use OpenRouter as a first-class built-in provider with its standard OpenAI-compatible endpoint and routed model IDs such as `openai/gpt-4.1-mini`.
152
156
  - **Hermes Agent**: <img src="public/provider-logos/hermes-agent.png" alt="Hermes Agent logo" width="20" height="20" /> Connect Hermes through its OpenAI-compatible API server, locally or through a reachable remote `/v1` endpoint.
153
- - **Delegation**: built-in delegation to Claude Code, Codex CLI, OpenCode CLI, Gemini CLI, and native SwarmClaw subagents.
157
+ - **Delegation**: built-in delegation to Claude Code, Codex CLI, OpenCode CLI, Gemini CLI, Cursor Agent CLI, Qwen Code CLI, and native SwarmClaw subagents.
154
158
  - **Autonomy**: heartbeat loops, schedules, background jobs, task execution, supervisor recovery, and agent wakeups.
155
159
  - **Orchestration**: durable structured execution with branching, repeat loops, parallel branches, explicit joins, restart-safe run state, and contextual launch from chats, chatrooms, tasks, schedules, and API flows.
156
160
  - **Structured Sessions**: reusable bounded runs with templates, facilitators, participants, hidden live rooms, chatroom `/breakout`, durable transcripts, outputs, operator controls, and a visible protocols template gallery plus visual builder.
@@ -371,10 +375,28 @@ Operational docs: https://swarmclaw.ai/docs/observability
371
375
 
372
376
  ## Releases
373
377
 
378
+ ### v1.5.34 Highlights
379
+
380
+ - **Ollama Cloud auth fix**: SwarmClaw now normalizes `api.ollama.com` and `www.ollama.com` to `ollama.com` before making authenticated requests, avoiding the redirect that was dropping authorization headers and causing false provider-health/runtime failures.
381
+ - **Chat execution context hardening**: tool invocation now resolves names case-insensitively, oversized tool results are truncated before they are fed back into the model, and proactive grounding/heartbeat prompts stay smaller under pressure to reduce avoidable context blowouts.
382
+ - **API compatibility fixes**: OpenAI-compatible streaming now captures reasoning deltas from providers that emit them outside `delta.content`, and A2A endpoints are exempt from the main proxy access-key gate so they can rely on their own auth scheme.
383
+
384
+ ### v1.5.33 Highlights
385
+
386
+ - **CLI global flag compatibility**: legacy-routed commands now honor the documented `--access-key` and `--base-url` aliases even when they appear after the subcommand, so authenticated CLI automation works the same across binary entry points.
387
+ - **Docker build memory hardening**: production Next.js builds now size `--max-old-space-size` from the detected container/cgroup memory limit, with `SWARMCLAW_BUILD_MAX_OLD_SPACE_SIZE_MB` available as an explicit override for constrained Docker Desktop and CI environments.
388
+
374
389
  ### v1.5.31 Highlights
375
390
 
376
391
  - **Fix Docker first-run crash**: resolved `EISDIR: illegal operation on a directory, read` error when running `docker compose up` without a pre-existing `.env.local` file. Docker was creating a directory mount instead of a file, which crashed Next.js on startup. Replaced the file bind mount with `env_file` directive using `required: false`.
377
392
 
393
+ ### v1.5.4 Highlights
394
+
395
+ - **Cursor Agent CLI built-in provider**: Cursor Agent CLI is now a first-class worker provider with session continuity, headless execution, and delegation support.
396
+ - **Qwen Code CLI built-in provider**: Qwen Code CLI is now available as a built-in worker provider and delegation backend with structured headless execution support.
397
+ - **Goose built-in provider**: Goose is now supported as a runtime-managed worker provider, using its own local auth and provider configuration while preserving SwarmClaw session continuity.
398
+ - **CLI setup and health parity**: setup flows, provider checks, setup doctor, and provider-facing UI now recognize Cursor, Qwen Code, and Goose alongside the existing CLI-backed providers.
399
+
378
400
  ### v1.5.3 Highlights
379
401
 
380
402
  - **Copilot CLI v1.x compatibility**: the `copilot-cli` provider now handles the current event format (`assistant.message_delta`, `assistant.message`, updated `result` payload) while keeping backward compatibility with the legacy format. Also fixes `--resume` flag syntax. (Community contribution by [@borislavnnikolov](https://github.com/borislavnnikolov) -- PR #36)
package/bin/swarmclaw.js CHANGED
@@ -19,6 +19,11 @@ const TS_CLI_ACTIONS = Object.freeze({
19
19
  webhooks: new Set(['list', 'get', 'create', 'update', 'delete', 'trigger']),
20
20
  })
21
21
 
22
+ const LEGACY_TS_CLI_ALIAS_MAP = Object.freeze({
23
+ '--base-url': '--url',
24
+ '--access-key': '--key',
25
+ })
26
+
22
27
  function shouldUseLegacyTsCli(argv) {
23
28
  const group = argv[0]
24
29
  const action = argv[1]
@@ -62,9 +67,37 @@ function buildLegacyTsCliArgs(cliPath, argv, options = {}) {
62
67
  return null
63
68
  }
64
69
 
70
+ function normalizeLegacyTsCliArgv(argv) {
71
+ const normalized = []
72
+
73
+ for (const token of argv) {
74
+ if (!token.startsWith('--')) {
75
+ normalized.push(token)
76
+ continue
77
+ }
78
+
79
+ const eqIndex = token.indexOf('=')
80
+ const flag = eqIndex > -1 ? token.slice(0, eqIndex) : token
81
+ const mappedFlag = LEGACY_TS_CLI_ALIAS_MAP[flag]
82
+
83
+ if (!mappedFlag) {
84
+ normalized.push(token)
85
+ continue
86
+ }
87
+
88
+ if (eqIndex > -1) {
89
+ normalized.push(`${mappedFlag}=${token.slice(eqIndex + 1)}`)
90
+ } else {
91
+ normalized.push(mappedFlag)
92
+ }
93
+ }
94
+
95
+ return normalized
96
+ }
97
+
65
98
  function runLegacyTsCli(argv) {
66
99
  const cliPath = path.join(__dirname, '..', 'src', 'cli', 'index.ts')
67
- const args = buildLegacyTsCliArgs(cliPath, argv)
100
+ const args = buildLegacyTsCliArgs(cliPath, normalizeLegacyTsCliArgv(argv))
68
101
  const env = normalizeLegacyCliEnv(process.env)
69
102
  if (!args) {
70
103
  process.stderr.write('Legacy CLI commands require Node 22.6+ or an available local tsx runtime.\n')
@@ -237,6 +270,7 @@ if (require.main === module) {
237
270
  module.exports = {
238
271
  buildLegacyTsCliArgs,
239
272
  hasTsxRuntime,
273
+ normalizeLegacyTsCliArgv,
240
274
  TS_CLI_ACTIONS,
241
275
  normalizeLegacyCliEnv,
242
276
  printPackageVersion,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swarmclawai/swarmclaw",
3
- "version": "1.5.31",
3
+ "version": "1.5.34",
4
4
  "description": "Build and run autonomous AI agents with OpenClaw, Hermes, multiple model providers, orchestration, delegation, memory, skills, schedules, and chat connectors.",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -72,9 +72,9 @@
72
72
  "lint:baseline:update": "node ./scripts/lint-baseline.mjs update",
73
73
  "cli": "node ./bin/swarmclaw.js",
74
74
  "test:cli": "node --test src/cli/*.test.js bin/*.test.js scripts/postinstall.test.mjs scripts/run-next-build.test.mjs scripts/run-next-typegen.test.mjs",
75
- "test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts",
75
+ "test:setup": "tsx --test src/app/api/setup/check-provider/route.test.ts src/lib/server/provider-model-discovery.test.ts src/components/auth/setup-wizard/utils.test.ts src/components/auth/setup-wizard/types.test.ts src/hooks/setup-done-detection.test.ts src/lib/setup-defaults.test.ts src/lib/server/storage-auth.test.ts src/lib/server/storage-auth-docker.test.ts",
76
76
  "test:openclaw": "tsx --test src/lib/openclaw/openclaw-agent-id.test.ts src/lib/openclaw/openclaw-endpoint.test.ts src/lib/server/agents/agent-runtime-config.test.ts src/lib/server/build-llm.test.ts src/lib/server/connectors/connector-routing.test.ts src/lib/server/connectors/openclaw.test.ts src/lib/server/connectors/swarmdock.test.ts src/lib/server/gateway/protocol.test.ts src/lib/server/llm-response-cache.test.ts src/lib/server/mcp-conformance.test.ts src/lib/server/openclaw/agent-resolver.test.ts src/lib/server/openclaw/deploy.test.ts src/lib/server/openclaw/skills-normalize.test.ts src/lib/server/session-tools/openclaw-nodes.test.ts src/lib/server/session-tools/swarmdock.test.ts src/lib/server/tasks/task-quality-gate.test.ts src/lib/server/tasks/task-validation.test.ts src/lib/server/tool-capability-policy.test.ts src/lib/providers/openclaw-exports.test.ts src/app/api/openclaw/dashboard-url/route.test.ts",
77
- "test:runtime": "tsx --test src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/app/api/approvals/route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/tts/route.test.ts",
77
+ "test:runtime": "tsx --test src/lib/server/knowledge-sources.test.ts src/lib/server/chat-execution/chat-execution-grounding.test.ts src/lib/server/chat-execution/iteration-timers.test.ts src/lib/server/protocols/protocol-service.test.ts src/lib/server/runtime/run-ledger.test.ts src/lib/server/observability/otel-config.test.ts src/lib/server/safe-parse-body.test.ts src/app/api/approvals/route.test.ts src/app/api/agents/agents-route.test.ts src/app/api/chats/chat-route.test.ts src/app/api/connectors/connector-doctor-route.test.ts src/app/api/healthz/route.test.ts src/app/api/logs/route.test.ts src/app/api/tts/route.test.ts",
78
78
  "test:builder": "tsx --test src/features/protocols/builder/utils/nodes-to-template.test.ts src/features/protocols/builder/utils/template-to-nodes.test.ts src/features/protocols/builder/validators/dag-validator.test.ts",
79
79
  "test:e2e": "tsx .workbench/browser-e2e/run.ts",
80
80
  "test:mcp:conformance": "node --import tsx ./scripts/mcp-conformance-check.ts",
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import fs from 'node:fs'
4
+ import os from 'node:os'
4
5
  import path from 'node:path'
5
6
  import { spawnSync } from 'node:child_process'
6
7
  import { createRequire } from 'node:module'
@@ -11,6 +12,17 @@ import { ensureBuildBootstrapPaths } from './build-bootstrap-env.mjs'
11
12
  const require = createRequire(import.meta.url)
12
13
 
13
14
  export const DEFAULT_MAX_OLD_SPACE_SIZE_MB = '8192'
15
+ export const MIN_MAX_OLD_SPACE_SIZE_MB = 1024
16
+ export const FALLBACK_MIN_MAX_OLD_SPACE_SIZE_MB = 512
17
+ export const RESERVED_BUILD_MEMORY_MB = 768
18
+ export const MAX_OLD_SPACE_RATIO = 0.75
19
+ export const LOW_MEMORY_RATIO = 0.6
20
+ export const BUILD_MAX_OLD_SPACE_SIZE_ENV = 'SWARMCLAW_BUILD_MAX_OLD_SPACE_SIZE_MB'
21
+ export const CGROUP_MEMORY_LIMIT_PATHS = [
22
+ '/sys/fs/cgroup/memory.max',
23
+ '/sys/fs/cgroup/memory/memory.limit_in_bytes',
24
+ ]
25
+ export const UNBOUNDED_MEMORY_LIMIT_BYTES = 1n << 60n
14
26
  export const TRACE_COPY_WARNING = 'Failed to copy traced files'
15
27
  export const NEXT_STANDALONE_METADATA_RELATIVE_DIR = path.join(
16
28
  'node_modules',
@@ -24,6 +36,74 @@ export const REQUIRED_NEXT_METADATA_FILES = [
24
36
  'is-metadata-route.js',
25
37
  ]
26
38
 
39
+ function parsePositiveInteger(value) {
40
+ const parsed = Number.parseInt(String(value ?? '').trim(), 10)
41
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : null
42
+ }
43
+
44
+ export function readCgroupMemoryLimitBytes(
45
+ paths = CGROUP_MEMORY_LIMIT_PATHS,
46
+ existsSync = fs.existsSync,
47
+ readFileSync = fs.readFileSync,
48
+ ) {
49
+ for (const filePath of paths) {
50
+ if (!existsSync(filePath)) continue
51
+
52
+ let raw = ''
53
+ try {
54
+ raw = String(readFileSync(filePath, 'utf8')).trim()
55
+ } catch {
56
+ continue
57
+ }
58
+
59
+ if (!raw || raw === 'max') continue
60
+
61
+ try {
62
+ const bytes = BigInt(raw)
63
+ if (bytes <= 0n || bytes >= UNBOUNDED_MEMORY_LIMIT_BYTES) continue
64
+ return Number(bytes)
65
+ } catch {
66
+ continue
67
+ }
68
+ }
69
+
70
+ return null
71
+ }
72
+
73
+ export function deriveMaxOldSpaceSizeMb(memoryLimitBytes, defaultMaxOldSpaceSizeMb = DEFAULT_MAX_OLD_SPACE_SIZE_MB) {
74
+ const defaultMb = parsePositiveInteger(defaultMaxOldSpaceSizeMb) ?? Number.parseInt(DEFAULT_MAX_OLD_SPACE_SIZE_MB, 10)
75
+ const limitMb = Math.floor(Number(memoryLimitBytes) / (1024 * 1024))
76
+ if (!Number.isFinite(limitMb) || limitMb <= 0) return String(defaultMb)
77
+
78
+ const constrainedCandidate = Math.min(
79
+ defaultMb,
80
+ limitMb - RESERVED_BUILD_MEMORY_MB,
81
+ Math.floor(limitMb * MAX_OLD_SPACE_RATIO),
82
+ )
83
+ if (constrainedCandidate >= MIN_MAX_OLD_SPACE_SIZE_MB) {
84
+ return String(constrainedCandidate)
85
+ }
86
+
87
+ return String(Math.max(
88
+ FALLBACK_MIN_MAX_OLD_SPACE_SIZE_MB,
89
+ Math.min(defaultMb, Math.floor(limitMb * LOW_MEMORY_RATIO)),
90
+ ))
91
+ }
92
+
93
+ export function resolveNextBuildMaxOldSpaceSizeMb(
94
+ env = process.env,
95
+ options = {},
96
+ ) {
97
+ const explicit = parsePositiveInteger(env[BUILD_MAX_OLD_SPACE_SIZE_ENV])
98
+ if (explicit) return String(explicit)
99
+
100
+ const readLimitBytes = options.readCgroupMemoryLimitBytes ?? readCgroupMemoryLimitBytes
101
+ const totalMemFn = options.totalMem ?? os.totalmem
102
+ const memoryLimitBytes = readLimitBytes() ?? totalMemFn()
103
+
104
+ return deriveMaxOldSpaceSizeMb(memoryLimitBytes, DEFAULT_MAX_OLD_SPACE_SIZE_MB)
105
+ }
106
+
27
107
  export function mergeNodeOptions(nodeOptions = '', maxOldSpaceSizeMb = DEFAULT_MAX_OLD_SPACE_SIZE_MB) {
28
108
  const trimmed = nodeOptions.trim()
29
109
  if (/(^|\s)--max-old-space-size(?:=|\s|$)/.test(trimmed)) return trimmed
@@ -120,12 +200,17 @@ export function repairStandaloneNextMetadata(cwd = process.cwd()) {
120
200
  return true
121
201
  }
122
202
 
123
- export function runNextBuild(args = process.argv.slice(2), env = process.env, cwd = process.cwd()) {
203
+ export function runNextBuild(
204
+ args = process.argv.slice(2),
205
+ env = process.env,
206
+ cwd = process.cwd(),
207
+ maxOldSpaceSizeMb = resolveNextBuildMaxOldSpaceSizeMb(env),
208
+ ) {
124
209
  const nextBin = require.resolve('next/dist/bin/next')
125
210
  return spawnSync(process.execPath, [nextBin, 'build', '--webpack', ...args], {
126
211
  stdio: 'pipe',
127
212
  encoding: 'utf-8',
128
- env: buildNextBuildEnv(env, DEFAULT_MAX_OLD_SPACE_SIZE_MB, cwd),
213
+ env: buildNextBuildEnv(env, maxOldSpaceSizeMb, cwd),
129
214
  cwd,
130
215
  })
131
216
  }
@@ -1,9 +1,17 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { notFound } from '@/lib/server/collection-helpers'
3
3
  import { trashAgent, updateAgent } from '@/lib/server/agents/agent-service'
4
+ import { loadAgent } from '@/lib/server/agents/agent-repository'
4
5
  import { notify } from '@/lib/server/ws-hub'
5
6
  import { safeParseBody } from '@/lib/server/safe-parse-body'
6
7
 
8
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
9
+ const { id } = await params
10
+ const agent = loadAgent(id)
11
+ if (!agent) return notFound()
12
+ return NextResponse.json(agent)
13
+ }
14
+
7
15
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
8
16
  const { id } = await params
9
17
  const { data: body, error } = await safeParseBody(req)
@@ -0,0 +1,114 @@
1
+ import assert from 'node:assert/strict'
2
+ import test, { afterEach } from 'node:test'
3
+
4
+ // Disable daemon autostart during tests
5
+ process.env.SWARMCLAW_DAEMON_AUTOSTART = '0'
6
+
7
+ import { GET as getAgent } from './[id]/route'
8
+ import { POST as createAgent } from './route'
9
+ import { loadAgents, saveAgents } from '@/lib/server/storage'
10
+
11
+ const originalAgents = loadAgents()
12
+
13
+ function routeParams(id: string) {
14
+ return { params: Promise.resolve({ id }) }
15
+ }
16
+
17
+ function seedAgent(id: string, overrides: Record<string, unknown> = {}) {
18
+ const agents = loadAgents()
19
+ const now = Date.now()
20
+ agents[id] = {
21
+ id,
22
+ name: 'Test Agent',
23
+ description: 'Route test',
24
+ systemPrompt: '',
25
+ provider: 'ollama',
26
+ model: 'qwen3.5',
27
+ credentialId: null,
28
+ fallbackCredentialIds: [],
29
+ apiEndpoint: null,
30
+ gatewayProfileId: null,
31
+ extensions: [],
32
+ createdAt: now,
33
+ updatedAt: now,
34
+ ...overrides,
35
+ }
36
+ saveAgents(agents)
37
+ }
38
+
39
+ afterEach(() => {
40
+ saveAgents(originalAgents)
41
+ })
42
+
43
+ // --- GET /api/agents/:id ---
44
+
45
+ test('GET /api/agents/:id returns the agent when it exists', async () => {
46
+ seedAgent('agent-get-test', { name: 'GetMe' })
47
+
48
+ const response = await getAgent(
49
+ new Request('http://local/api/agents/agent-get-test'),
50
+ routeParams('agent-get-test'),
51
+ )
52
+
53
+ assert.equal(response.status, 200)
54
+ const body = await response.json()
55
+ assert.equal(body.id, 'agent-get-test')
56
+ assert.equal(body.name, 'GetMe')
57
+ })
58
+
59
+ test('GET /api/agents/:id returns 404 for a non-existent agent', async () => {
60
+ const response = await getAgent(
61
+ new Request('http://local/api/agents/does-not-exist'),
62
+ routeParams('does-not-exist'),
63
+ )
64
+
65
+ assert.equal(response.status, 404)
66
+ const body = await response.json()
67
+ assert.equal(body.error, 'Not found')
68
+ })
69
+
70
+ // --- POST /api/agents (provider validation) ---
71
+
72
+ test('POST /api/agents rejects an unknown provider with a 400', async () => {
73
+ const response = await createAgent(new Request('http://local/api/agents', {
74
+ method: 'POST',
75
+ headers: { 'content-type': 'application/json' },
76
+ body: JSON.stringify({ name: 'Bad Provider Agent', provider: 'nonexistent_provider', model: 'x' }),
77
+ }))
78
+
79
+ assert.equal(response.status, 400)
80
+ const body = await response.json()
81
+ assert.equal(body.error, 'Validation failed')
82
+ assert.ok(body.issues.some((i: { path: string; message: string }) => i.path === 'provider'))
83
+ })
84
+
85
+ test('POST /api/agents accepts a valid provider and creates the agent', async () => {
86
+ const response = await createAgent(new Request('http://local/api/agents', {
87
+ method: 'POST',
88
+ headers: { 'content-type': 'application/json' },
89
+ body: JSON.stringify({ name: 'Good Agent', provider: 'ollama', model: 'qwen3.5' }),
90
+ }))
91
+
92
+ assert.equal(response.status, 200)
93
+ const body = await response.json()
94
+ assert.equal(body.name, 'Good Agent')
95
+ assert.equal(body.provider, 'ollama')
96
+ assert.ok(body.id)
97
+
98
+ // Clean up
99
+ const agents = loadAgents()
100
+ delete agents[body.id]
101
+ saveAgents(agents)
102
+ })
103
+
104
+ test('POST /api/agents rejects missing required fields with a 400', async () => {
105
+ const response = await createAgent(new Request('http://local/api/agents', {
106
+ method: 'POST',
107
+ headers: { 'content-type': 'application/json' },
108
+ body: JSON.stringify({}),
109
+ }))
110
+
111
+ assert.equal(response.status, 400)
112
+ const body = await response.json()
113
+ assert.equal(body.error, 'Validation failed')
114
+ })
@@ -3,6 +3,7 @@ import { perf } from '@/lib/server/runtime/perf'
3
3
  import { listAgentsForApi, createAgent } from '@/lib/server/agents/agent-service'
4
4
  import { AgentCreateSchema, formatZodError } from '@/lib/validation/schemas'
5
5
  import { ensureDaemonProcessRunning } from '@/lib/server/daemon/controller'
6
+ import { getProvider } from '@/lib/providers'
6
7
  import { z } from 'zod'
7
8
  import { safeParseBody } from '@/lib/server/safe-parse-body'
8
9
  import { loadSettings } from '@/lib/server/storage'
@@ -40,6 +41,15 @@ export async function POST(req: Request) {
40
41
  }
41
42
  const body = parsed.data as unknown as Record<string, unknown>
42
43
 
44
+ // Validate provider exists
45
+ const providerId = String(body.provider || '')
46
+ if (providerId && !getProvider(providerId)) {
47
+ return NextResponse.json(
48
+ { error: 'Validation failed', issues: [{ path: 'provider', message: `Unknown provider: "${providerId}"` }] },
49
+ { status: 400 },
50
+ )
51
+ }
52
+
43
53
  // Check approval policy — if enabled, create an approval request instead of the agent
44
54
  const settings = loadSettings()
45
55
  if (settings.approvalPolicies?.requireApprovalForAgentCreate) {
@@ -13,21 +13,33 @@ import { ensureDaemonProcessRunning } from '@/lib/server/daemon/controller'
13
13
  export const dynamic = 'force-dynamic'
14
14
 
15
15
  export async function GET() {
16
- const endPerf = perf.start('api', 'GET /api/connectors')
17
- const connectors = await listConnectorsWithRuntime()
18
- endPerf({ count: Object.keys(connectors).length })
19
- return NextResponse.json(connectors)
16
+ try {
17
+ const endPerf = perf.start('api', 'GET /api/connectors')
18
+ const connectors = await listConnectorsWithRuntime()
19
+ endPerf({ count: Object.keys(connectors).length })
20
+ return NextResponse.json(connectors)
21
+ } catch (err) {
22
+ return NextResponse.json({ error: err instanceof Error ? err.message : String(err) }, { status: 500 })
23
+ }
20
24
  }
21
25
 
22
26
  export async function POST(req: Request) {
23
- await ensureDaemonProcessRunning('api/connectors:post')
24
- const { data: raw, error } = await safeParseBody<Record<string, unknown>>(req)
25
- if (error) return error
26
- const parsed = ConnectorCreateSchema.safeParse(raw)
27
- if (!parsed.success) {
28
- return NextResponse.json(formatZodError(parsed.error as z.ZodError), { status: 400 })
27
+ try {
28
+ await ensureDaemonProcessRunning('api/connectors:post')
29
+ const { data: raw, error } = await safeParseBody<Record<string, unknown>>(req)
30
+ if (error) return error
31
+ const parsed = ConnectorCreateSchema.safeParse(raw)
32
+ if (!parsed.success) {
33
+ return NextResponse.json(formatZodError(parsed.error as z.ZodError), { status: 400 })
34
+ }
35
+ const connector = createConnector(parsed.data as unknown as Record<string, unknown>)
36
+ try {
37
+ await autoStartConnectorIfNeeded(connector, parsed.data as unknown as Record<string, unknown>)
38
+ } catch {
39
+ // Auto-start failure is non-fatal — the connector is still saved.
40
+ }
41
+ return NextResponse.json(await getConnectorWithRuntime(connector.id) || connector)
42
+ } catch (err) {
43
+ return NextResponse.json({ error: err instanceof Error ? err.message : String(err) }, { status: 500 })
29
44
  }
30
- const connector = createConnector(parsed.data as unknown as Record<string, unknown>)
31
- await autoStartConnectorIfNeeded(connector, parsed.data as unknown as Record<string, unknown>)
32
- return NextResponse.json(await getConnectorWithRuntime(connector.id) || connector)
33
45
  }
@@ -1,11 +1,18 @@
1
1
  import { NextResponse } from 'next/server'
2
- import { deleteCredentialRecord } from '@/lib/server/credentials/credential-service'
2
+ import { deleteCredentialRecord, getCredentialSummary } from '@/lib/server/credentials/credential-service'
3
3
  import { notFound } from '@/lib/server/collection-helpers'
4
4
  import { log } from '@/lib/server/logger'
5
5
  import { logActivity } from '@/lib/server/activity/activity-log'
6
6
 
7
7
  const TAG = 'api-credentials'
8
8
 
9
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
10
+ const { id } = await params
11
+ const summary = getCredentialSummary(id)
12
+ if (!summary) return notFound()
13
+ return NextResponse.json(summary)
14
+ }
15
+
9
16
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
10
17
  const { id: credId } = await params
11
18
  if (!deleteCredentialRecord(credId)) {
@@ -1,11 +1,19 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { notFound } from '@/lib/server/collection-helpers'
3
3
  import { safeParseBody } from '@/lib/server/safe-parse-body'
4
+ import { loadSchedule } from '@/lib/server/schedules/schedule-repository'
4
5
  import {
5
6
  deleteScheduleFromRoute,
6
7
  updateScheduleFromRoute,
7
8
  } from '@/lib/server/schedules/schedule-route-service'
8
9
 
10
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
11
+ const { id } = await params
12
+ const schedule = loadSchedule(id)
13
+ if (!schedule) return notFound()
14
+ return NextResponse.json(schedule)
15
+ }
16
+
9
17
  export async function PUT(req: Request, { params }: { params: Promise<{ id: string }> }) {
10
18
  const { id } = await params
11
19
  const { data: body, error } = await safeParseBody(req)
@@ -6,6 +6,17 @@ import { safeParseBody } from '@/lib/server/safe-parse-body'
6
6
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
7
  const ops: CollectionOps<any> = { load: loadSecrets, save: saveSecrets }
8
8
 
9
+ export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
10
+ const { id } = await params
11
+ const secrets = loadSecrets()
12
+ const secret = secrets[id]
13
+ if (!secret) return notFound()
14
+ // Never expose the encrypted value
15
+ const safe = { ...(secret as Record<string, unknown>) }
16
+ delete safe.encryptedValue
17
+ return NextResponse.json(safe)
18
+ }
19
+
9
20
  export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
10
21
  const { id } = await params
11
22
  if (!deleteItem(ops, id)) return notFound()
@@ -26,6 +37,7 @@ export async function PUT(req: Request, { params }: { params: Promise<{ id: stri
26
37
  return secret
27
38
  })
28
39
  if (!result) return notFound()
29
- const { encryptedValue, ...safe } = result as Record<string, unknown>
40
+ const safe = { ...(result as Record<string, unknown>) }
41
+ delete safe.encryptedValue
30
42
  return NextResponse.json(safe)
31
43
  }
@@ -1,11 +1,20 @@
1
1
  import { NextResponse } from 'next/server'
2
2
  import { loadCredentials, decryptKey } from '@/lib/server/storage'
3
3
  import { getDeviceId, wsConnect, rpcOnConnectedGateway } from '@/lib/providers/openclaw'
4
+ import { buildCliEnv, probeCliAuth, resolveCliBinary } from '@/lib/providers/cli-utils'
4
5
  import { OPENAI_COMPATIBLE_DEFAULTS } from '@/lib/server/provider-health'
5
6
  import { resolveOllamaRuntimeConfig } from '@/lib/server/ollama-runtime'
6
7
  import { normalizeOllamaSetupEndpoint, normalizeOpenClawUrl, parseErrorMessage } from './helpers'
7
8
 
8
9
  type SetupProvider =
10
+ | 'claude-cli'
11
+ | 'codex-cli'
12
+ | 'opencode-cli'
13
+ | 'gemini-cli'
14
+ | 'copilot-cli'
15
+ | 'cursor-cli'
16
+ | 'qwen-code-cli'
17
+ | 'goose'
9
18
  | 'openai'
10
19
  | 'openrouter'
11
20
  | 'anthropic'
@@ -22,6 +31,8 @@ type SetupProvider =
22
31
  | 'openclaw'
23
32
  | 'hermes'
24
33
 
34
+ type CliSetupProvider = 'claude-cli' | 'codex-cli' | 'opencode-cli' | 'gemini-cli' | 'copilot-cli' | 'cursor-cli' | 'qwen-code-cli' | 'goose'
35
+
25
36
  interface SetupCheckBody {
26
37
  provider?: string
27
38
  apiKey?: string
@@ -266,6 +277,34 @@ async function checkOpenClaw(apiKey: string, endpointRaw: string): Promise<{ ok:
266
277
  return { ok: true, message: 'Connected to OpenClaw gateway.', normalizedEndpoint, deviceId, recommendedModel }
267
278
  }
268
279
 
280
+ function checkCliProvider(provider: CliSetupProvider): { ok: boolean; message: string } {
281
+ const env = buildCliEnv()
282
+ const config = {
283
+ 'claude-cli': { binary: 'claude', backend: 'claude' as const, label: 'Claude Code CLI' },
284
+ 'codex-cli': { binary: 'codex', backend: 'codex' as const, label: 'OpenAI Codex CLI' },
285
+ 'opencode-cli': { binary: 'opencode', backend: 'opencode' as const, label: 'OpenCode CLI' },
286
+ 'gemini-cli': { binary: 'gemini', backend: 'gemini' as const, label: 'Gemini CLI' },
287
+ 'copilot-cli': { binary: 'copilot', backend: 'copilot' as const, label: 'GitHub Copilot CLI' },
288
+ 'cursor-cli': { binary: 'cursor-agent', backend: 'cursor' as const, label: 'Cursor Agent CLI' },
289
+ 'qwen-code-cli': { binary: 'qwen', backend: 'qwen' as const, label: 'Qwen Code CLI' },
290
+ goose: { binary: 'goose', backend: 'goose' as const, label: 'Goose CLI' },
291
+ }[provider]
292
+
293
+ if (!config) return { ok: false, message: 'Unknown CLI provider.' }
294
+ const binary = resolveCliBinary(config.binary)
295
+ if (!binary) {
296
+ return {
297
+ ok: false,
298
+ message: `${config.label} is not installed. Install \`${config.binary}\` and ensure it is on your PATH.`,
299
+ }
300
+ }
301
+ const auth = probeCliAuth(binary, config.backend, env, process.cwd())
302
+ if (!auth.authenticated) {
303
+ return { ok: false, message: auth.errorMessage || `${config.label} is not configured.` }
304
+ }
305
+ return { ok: true, message: `${config.label} is installed and ready.` }
306
+ }
307
+
269
308
  export async function POST(req: Request) {
270
309
  const body = parseBody(await req.json().catch(() => ({})))
271
310
  const provider = clean(body.provider) as SetupProvider
@@ -273,6 +312,7 @@ export async function POST(req: Request) {
273
312
  const credentialId = clean(body.credentialId)
274
313
  const endpoint = clean(body.endpoint)
275
314
  const model = clean(body.model)
315
+ const CLI_PROVIDERS = new Set<CliSetupProvider>(['claude-cli', 'codex-cli', 'opencode-cli', 'gemini-cli', 'copilot-cli', 'cursor-cli', 'qwen-code-cli', 'goose'])
276
316
 
277
317
  // Resolve credentialId to an API key if no raw key was provided
278
318
  if (!apiKey && credentialId) {
@@ -287,6 +327,11 @@ export async function POST(req: Request) {
287
327
  }
288
328
  }
289
329
 
330
+ if (CLI_PROVIDERS.has(provider as CliSetupProvider)) {
331
+ const result = checkCliProvider(provider as CliSetupProvider)
332
+ return NextResponse.json(result)
333
+ }
334
+
290
335
  if (!provider) {
291
336
  return NextResponse.json({ ok: false, message: 'Provider is required.' }, { status: 400 })
292
337
  }
@@ -171,6 +171,11 @@ export async function GET(req: Request) {
171
171
  { id: 'claude-cli', label: 'Claude Code CLI', command: 'claude' },
172
172
  { id: 'codex-cli', label: 'OpenAI Codex CLI', command: 'codex' },
173
173
  { id: 'opencode-cli', label: 'OpenCode CLI', command: 'opencode' },
174
+ { id: 'gemini-cli', label: 'Gemini CLI', command: 'gemini' },
175
+ { id: 'copilot-cli', label: 'GitHub Copilot CLI', command: 'copilot' },
176
+ { id: 'cursor-cli', label: 'Cursor Agent CLI', command: 'cursor-agent' },
177
+ { id: 'qwen-code-cli', label: 'Qwen Code CLI', command: 'qwen' },
178
+ { id: 'goose', label: 'Goose CLI', command: 'goose' },
174
179
  { id: 'google-workspace-cli', label: 'Google Workspace CLI', command: 'gws' },
175
180
  ]
176
181