@swarmclawai/swarmclaw 1.5.31 → 1.5.33
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 +5 -0
- package/bin/swarmclaw.js +35 -1
- package/package.json +2 -2
- 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 +10 -0
- 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/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/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 +3 -0
- 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 +3 -1
- package/src/lib/server/chat-execution/stream-agent-chat.ts +5 -0
- 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/provider-health.ts +13 -3
- 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/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 +5 -2
- 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/types/misc.ts +4 -1
- package/src/types/provider.ts +1 -1
- package/src/types/session.ts +9 -0
package/README.md
CHANGED
|
@@ -371,6 +371,11 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
371
371
|
|
|
372
372
|
## Releases
|
|
373
373
|
|
|
374
|
+
### v1.5.33 Highlights
|
|
375
|
+
|
|
376
|
+
- **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.
|
|
377
|
+
- **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.
|
|
378
|
+
|
|
374
379
|
### v1.5.31 Highlights
|
|
375
380
|
|
|
376
381
|
- **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`.
|
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.33",
|
|
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,7 +72,7 @@
|
|
|
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
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",
|
|
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",
|
|
@@ -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,16 @@ 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 { encryptedValue, ...safe } = secret as Record<string, unknown>
|
|
16
|
+
return NextResponse.json(safe)
|
|
17
|
+
}
|
|
18
|
+
|
|
9
19
|
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
10
20
|
const { id } = await params
|
|
11
21
|
if (!deleteItem(ops, id)) return notFound()
|
|
@@ -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
|
|
package/src/cli/binary.test.js
CHANGED
|
@@ -113,6 +113,17 @@ test('legacy-routed binary commands fall back to platform-api-key.txt', () => {
|
|
|
113
113
|
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
114
114
|
})
|
|
115
115
|
|
|
116
|
+
test('legacy-routed binary commands accept documented global aliases after the subcommand', () => {
|
|
117
|
+
const { result, capture } = runWithMockedFetch(
|
|
118
|
+
['agents', 'list', '--access-key', 'alias-key', '--base-url', 'http://127.0.0.1:4567', '--json'],
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
assert.equal(result.status, 0, result.stderr)
|
|
122
|
+
assert.equal(result.stdout.trim(), '[]')
|
|
123
|
+
assert.equal(capture.headers['X-Access-Key'], 'alias-key')
|
|
124
|
+
assert.equal(capture.url, 'http://127.0.0.1:4567/api/agents')
|
|
125
|
+
})
|
|
126
|
+
|
|
116
127
|
test('binary server help exits successfully', () => {
|
|
117
128
|
const result = runBinary(['server', '--help'])
|
|
118
129
|
assert.equal(result.status, 0, result.stderr)
|
package/src/cli/index.js
CHANGED
|
@@ -15,7 +15,7 @@ const COMMAND_GROUPS = [
|
|
|
15
15
|
description: 'Manage agents',
|
|
16
16
|
commands: [
|
|
17
17
|
cmd('list', 'GET', '/agents', 'List agents'),
|
|
18
|
-
cmd('get', 'GET', '/agents/:id', 'Get an agent by id'
|
|
18
|
+
cmd('get', 'GET', '/agents/:id', 'Get an agent by id'),
|
|
19
19
|
cmd('create', 'POST', '/agents', 'Create an agent', { expectsJsonBody: true }),
|
|
20
20
|
cmd('update', 'PUT', '/agents/:id', 'Update an agent', { expectsJsonBody: true }),
|
|
21
21
|
cmd('delete', 'DELETE', '/agents/:id', 'Delete an agent'),
|
|
@@ -142,7 +142,7 @@ const COMMAND_GROUPS = [
|
|
|
142
142
|
description: 'Manage encrypted provider credentials',
|
|
143
143
|
commands: [
|
|
144
144
|
cmd('list', 'GET', '/credentials', 'List credentials'),
|
|
145
|
-
cmd('get', 'GET', '/credentials/:id', 'Get credential metadata by id'
|
|
145
|
+
cmd('get', 'GET', '/credentials/:id', 'Get credential metadata by id'),
|
|
146
146
|
cmd('create', 'POST', '/credentials', 'Create credential', { expectsJsonBody: true }),
|
|
147
147
|
cmd('delete', 'DELETE', '/credentials/:id', 'Delete credential'),
|
|
148
148
|
],
|
|
@@ -541,7 +541,7 @@ const COMMAND_GROUPS = [
|
|
|
541
541
|
description: 'Manage schedules',
|
|
542
542
|
commands: [
|
|
543
543
|
cmd('list', 'GET', '/schedules', 'List schedules'),
|
|
544
|
-
cmd('get', 'GET', '/schedules/:id', 'Get schedule by id'
|
|
544
|
+
cmd('get', 'GET', '/schedules/:id', 'Get schedule by id'),
|
|
545
545
|
cmd('create', 'POST', '/schedules', 'Create schedule', { expectsJsonBody: true }),
|
|
546
546
|
cmd('update', 'PUT', '/schedules/:id', 'Update schedule', { expectsJsonBody: true }),
|
|
547
547
|
cmd('delete', 'DELETE', '/schedules/:id', 'Delete schedule'),
|
|
@@ -553,7 +553,7 @@ const COMMAND_GROUPS = [
|
|
|
553
553
|
description: 'Manage reusable encrypted secrets',
|
|
554
554
|
commands: [
|
|
555
555
|
cmd('list', 'GET', '/secrets', 'List secrets metadata'),
|
|
556
|
-
cmd('get', 'GET', '/secrets/:id', 'Get secret metadata by id'
|
|
556
|
+
cmd('get', 'GET', '/secrets/:id', 'Get secret metadata by id'),
|
|
557
557
|
cmd('create', 'POST', '/secrets', 'Create secret', { expectsJsonBody: true }),
|
|
558
558
|
cmd('update', 'PUT', '/secrets/:id', 'Update secret metadata', { expectsJsonBody: true }),
|
|
559
559
|
cmd('delete', 'DELETE', '/secrets/:id', 'Delete secret'),
|
package/src/cli/index.test.js
CHANGED
|
@@ -453,7 +453,10 @@ test('client-side collection lookups fail cleanly when the entity is missing', a
|
|
|
453
453
|
const stdout = makeWritable()
|
|
454
454
|
const stderr = makeWritable()
|
|
455
455
|
|
|
456
|
-
const fetchImpl = async () =>
|
|
456
|
+
const fetchImpl = async () => new Response(JSON.stringify({ error: 'Not found' }), {
|
|
457
|
+
status: 404,
|
|
458
|
+
headers: { 'content-type': 'application/json' },
|
|
459
|
+
})
|
|
457
460
|
|
|
458
461
|
const exitCode = await runCli(
|
|
459
462
|
['agents', 'get', 'agent-1'],
|
|
@@ -468,7 +471,7 @@ test('client-side collection lookups fail cleanly when the entity is missing', a
|
|
|
468
471
|
|
|
469
472
|
assert.equal(exitCode, 1)
|
|
470
473
|
assert.equal(stdout.toString(), '')
|
|
471
|
-
assert.match(stderr.toString(), /
|
|
474
|
+
assert.match(stderr.toString(), /not found/i)
|
|
472
475
|
})
|
|
473
476
|
|
|
474
477
|
test('runCli loads request JSON from @file inputs', async () => {
|