@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.
- package/README.md +25 -3
- package/bin/swarmclaw.js +35 -1
- package/package.json +3 -3
- package/scripts/run-next-build.mjs +87 -2
- package/src/app/api/agents/[id]/route.ts +8 -0
- package/src/app/api/agents/agents-route.test.ts +114 -0
- package/src/app/api/agents/route.ts +10 -0
- package/src/app/api/connectors/route.ts +25 -13
- package/src/app/api/credentials/[id]/route.ts +8 -1
- package/src/app/api/schedules/[id]/route.ts +8 -0
- package/src/app/api/secrets/[id]/route.ts +13 -1
- package/src/app/api/setup/check-provider/route.ts +45 -0
- package/src/app/api/setup/doctor/route.ts +5 -0
- package/src/cli/binary.test.js +11 -0
- package/src/cli/index.js +4 -4
- package/src/cli/index.test.js +5 -2
- package/src/cli/index.ts +1 -1
- package/src/components/agents/agent-sheet.tsx +16 -4
- package/src/components/agents/inspector-panel.tsx +5 -0
- package/src/components/auth/setup-wizard/step-agents.tsx +19 -1
- package/src/components/chat/activity-moment.tsx +3 -0
- package/src/components/chat/chat-header.tsx +23 -2
- package/src/components/chat/tool-call-bubble.tsx +20 -0
- package/src/hooks/setup-done-detection.test.ts +4 -2
- package/src/hooks/setup-done-detection.ts +1 -1
- package/src/lib/ollama-mode.test.ts +18 -1
- package/src/lib/ollama-mode.ts +8 -0
- package/src/lib/orchestrator-config.ts +5 -0
- package/src/lib/provider-sets.ts +4 -4
- package/src/lib/providers/acp-client.ts +116 -0
- package/src/lib/providers/cli-utils.test.ts +9 -1
- package/src/lib/providers/cli-utils.ts +89 -4
- package/src/lib/providers/cursor-cli.ts +172 -0
- package/src/lib/providers/goose.ts +149 -0
- package/src/lib/providers/index.ts +29 -1
- package/src/lib/providers/openai.ts +5 -1
- package/src/lib/providers/qwen-code-cli.ts +152 -0
- package/src/lib/server/agents/agent-availability.ts +2 -2
- package/src/lib/server/agents/agent-thread-session.ts +8 -0
- package/src/lib/server/agents/task-session.ts +8 -0
- package/src/lib/server/capability-router.ts +8 -2
- package/src/lib/server/chat-execution/chat-execution-utils.ts +13 -0
- package/src/lib/server/chat-execution/chat-turn-finalization.ts +8 -0
- package/src/lib/server/chat-execution/chat-turn-preparation.ts +5 -1
- package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +13 -8
- package/src/lib/server/chat-execution/iteration-timers.test.ts +84 -0
- package/src/lib/server/chat-execution/iteration-timers.ts +18 -1
- package/src/lib/server/chat-execution/prompt-sections.ts +5 -3
- package/src/lib/server/chat-execution/stream-agent-chat.ts +7 -2
- package/src/lib/server/chatrooms/chatroom-helpers.ts +13 -0
- package/src/lib/server/chats/chat-session-service.ts +18 -0
- package/src/lib/server/connectors/session.ts +8 -0
- package/src/lib/server/context-manager.ts +5 -0
- package/src/lib/server/ollama-runtime.ts +2 -2
- package/src/lib/server/provider-health.ts +16 -4
- package/src/lib/server/provider-model-discovery.test.ts +8 -0
- package/src/lib/server/provider-model-discovery.ts +1 -1
- package/src/lib/server/runtime/daemon-state/core.ts +2 -2
- package/src/lib/server/runtime/heartbeat-service.ts +13 -10
- package/src/lib/server/runtime/queue/core.ts +30 -4
- package/src/lib/server/runtime/session-run-manager/enqueue.ts +1 -1
- package/src/lib/server/session-reset-policy.test.ts +16 -0
- package/src/lib/server/session-reset-policy.ts +9 -1
- package/src/lib/server/session-tools/context.ts +2 -2
- package/src/lib/server/session-tools/delegate.ts +334 -14
- package/src/lib/server/session-tools/index.ts +16 -3
- package/src/lib/server/session-tools/session-info.ts +4 -1
- package/src/lib/server/storage-auth-docker.test.ts +244 -0
- package/src/lib/server/storage-auth.test.ts +150 -0
- package/src/lib/server/storage-auth.ts +57 -22
- package/src/lib/server/storage-normalization.ts +19 -0
- package/src/lib/server/storage.ts +3 -0
- package/src/lib/server/tasks/task-resume.ts +23 -1
- package/src/lib/server/tool-aliases.ts +1 -1
- package/src/lib/server/tool-capability-policy.ts +4 -1
- package/src/lib/setup-defaults.test.ts +6 -0
- package/src/lib/setup-defaults.ts +146 -0
- package/src/lib/tool-definitions.ts +1 -1
- package/src/proxy.ts +7 -1
- package/src/types/misc.ts +4 -1
- package/src/types/provider.ts +1 -1
- 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
|
|
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**:
|
|
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.
|
|
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(
|
|
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,
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
|