@swarmclawai/swarmclaw 0.7.3 → 0.7.5
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 +47 -40
- package/bin/package-manager.js +157 -0
- package/bin/package-manager.test.js +90 -0
- package/bin/server-cmd.js +38 -7
- package/bin/swarmclaw.js +54 -4
- package/bin/update-cmd.js +48 -10
- package/bin/update-cmd.test.js +55 -0
- package/package.json +8 -3
- package/scripts/postinstall.mjs +26 -0
- package/src/app/api/agents/[id]/route.ts +17 -0
- package/src/app/api/agents/[id]/thread/route.ts +4 -87
- package/src/app/api/agents/route.ts +23 -1
- package/src/app/api/auth/route.ts +1 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +16 -5
- package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
- package/src/app/api/chatrooms/[id]/route.ts +6 -0
- package/src/app/api/chats/[id]/route.ts +12 -0
- package/src/app/api/chats/heartbeat/route.ts +2 -1
- package/src/app/api/chats/route.ts +7 -1
- package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
- package/src/app/api/external-agents/[id]/route.ts +31 -0
- package/src/app/api/external-agents/register/route.ts +3 -0
- package/src/app/api/external-agents/route.ts +66 -0
- package/src/app/api/gateways/[id]/health/route.ts +28 -0
- package/src/app/api/gateways/[id]/route.ts +79 -0
- package/src/app/api/gateways/route.ts +57 -0
- package/src/app/api/openclaw/gateway/route.ts +10 -7
- package/src/app/api/openclaw/skills/route.ts +1 -1
- package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
- package/src/app/api/schedules/[id]/route.ts +38 -9
- package/src/app/api/schedules/route.ts +51 -28
- package/src/app/api/settings/route.ts +6 -10
- package/src/app/api/setup/doctor/route.ts +6 -4
- package/src/app/api/tasks/[id]/route.ts +2 -1
- package/src/app/api/tasks/bulk/route.ts +2 -2
- package/src/app/page.tsx +126 -15
- package/src/cli/binary.test.js +142 -0
- package/src/cli/index.js +34 -11
- package/src/cli/index.test.js +195 -0
- package/src/cli/index.ts +20 -4
- package/src/cli/server-cmd.test.js +59 -0
- package/src/cli/spec.js +20 -2
- package/src/components/agents/agent-sheet.tsx +249 -7
- package/src/components/agents/inspector-panel.tsx +3 -2
- package/src/components/agents/sandbox-env-panel.tsx +4 -1
- package/src/components/auth/setup-wizard.tsx +970 -275
- package/src/components/chat/chat-area.tsx +41 -14
- package/src/components/chat/chat-card.tsx +2 -1
- package/src/components/chat/chat-header.tsx +8 -13
- package/src/components/chat/chat-list.tsx +58 -20
- package/src/components/chat/message-list.tsx +142 -18
- package/src/components/chatrooms/chatroom-input.tsx +96 -33
- package/src/components/chatrooms/chatroom-list.tsx +141 -72
- package/src/components/chatrooms/chatroom-message.tsx +7 -6
- package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
- package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
- package/src/components/chatrooms/chatroom-view.tsx +157 -86
- package/src/components/chatrooms/reaction-picker.tsx +38 -33
- package/src/components/gateways/gateway-sheet.tsx +567 -0
- package/src/components/input/chat-input.tsx +135 -86
- package/src/components/layout/app-layout.tsx +2 -0
- package/src/components/memory/memory-browser.tsx +71 -6
- package/src/components/memory/memory-card.tsx +18 -0
- package/src/components/memory/memory-detail.tsx +58 -31
- package/src/components/memory/memory-sheet.tsx +32 -4
- package/src/components/projects/project-detail.tsx +7 -2
- package/src/components/providers/provider-list.tsx +158 -2
- package/src/components/providers/provider-sheet.tsx +81 -70
- package/src/components/shared/bottom-sheet.tsx +31 -15
- package/src/components/shared/confirm-dialog.tsx +45 -30
- package/src/components/shared/model-combobox.tsx +90 -8
- package/src/components/shared/settings/section-heartbeat.tsx +11 -6
- package/src/components/shared/settings/section-orchestrator.tsx +3 -0
- package/src/components/shared/settings/settings-page.tsx +5 -3
- package/src/components/tasks/approvals-panel.tsx +7 -1
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
- package/src/lib/heartbeat-defaults.ts +48 -0
- package/src/lib/memory-presentation.ts +59 -0
- package/src/lib/provider-model-discovery-client.ts +29 -0
- package/src/lib/providers/index.ts +12 -5
- package/src/lib/runtime-loop.ts +105 -3
- package/src/lib/safe-storage.ts +6 -1
- package/src/lib/server/agent-runtime-config.test.ts +141 -0
- package/src/lib/server/agent-runtime-config.ts +277 -0
- package/src/lib/server/agent-thread-session.test.ts +85 -0
- package/src/lib/server/agent-thread-session.ts +123 -0
- package/src/lib/server/approvals-auto-approve.test.ts +59 -0
- package/src/lib/server/build-llm.test.ts +13 -5
- package/src/lib/server/chat-execution-tool-events.test.ts +87 -2
- package/src/lib/server/chat-execution.ts +159 -71
- package/src/lib/server/chatroom-helpers.test.ts +7 -0
- package/src/lib/server/chatroom-helpers.ts +99 -6
- package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
- package/src/lib/server/connectors/manager.ts +89 -61
- package/src/lib/server/connectors/slack.ts +1 -1
- package/src/lib/server/daemon-state.ts +3 -2
- package/src/lib/server/data-dir.test.ts +56 -0
- package/src/lib/server/data-dir.ts +15 -9
- package/src/lib/server/eval/agent-regression.test.ts +47 -0
- package/src/lib/server/eval/agent-regression.ts +1742 -0
- package/src/lib/server/eval/runner.ts +11 -1
- package/src/lib/server/eval/store.ts +2 -1
- package/src/lib/server/heartbeat-service.ts +23 -8
- package/src/lib/server/heartbeat-wake.ts +6 -2
- package/src/lib/server/main-agent-loop.ts +13 -6
- package/src/lib/server/openclaw-exec-config.ts +4 -2
- package/src/lib/server/openclaw-gateway.ts +123 -36
- package/src/lib/server/orchestrator-lg.ts +1 -2
- package/src/lib/server/orchestrator.ts +3 -2
- package/src/lib/server/plugins.test.ts +9 -1
- package/src/lib/server/plugins.ts +12 -2
- package/src/lib/server/provider-model-discovery.ts +481 -0
- package/src/lib/server/queue.ts +1 -1
- package/src/lib/server/runtime-settings.test.ts +119 -0
- package/src/lib/server/runtime-settings.ts +12 -92
- package/src/lib/server/schedule-normalization.ts +187 -0
- package/src/lib/server/session-tools/autonomy-tools.test.ts +23 -0
- package/src/lib/server/session-tools/crud.ts +27 -3
- package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
- package/src/lib/server/session-tools/discovery.ts +18 -8
- package/src/lib/server/session-tools/file-normalize.test.ts +5 -0
- package/src/lib/server/session-tools/file.ts +8 -2
- package/src/lib/server/session-tools/http.ts +9 -3
- package/src/lib/server/session-tools/index.ts +31 -1
- package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
- package/src/lib/server/session-tools/monitor.ts +14 -7
- package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
- package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
- package/src/lib/server/session-tools/platform.ts +1 -1
- package/src/lib/server/session-tools/plugin-creator.ts +9 -2
- package/src/lib/server/session-tools/sandbox.ts +51 -92
- package/src/lib/server/session-tools/session-info.ts +22 -1
- package/src/lib/server/session-tools/session-tools-wiring.test.ts +23 -0
- package/src/lib/server/session-tools/shell.ts +2 -2
- package/src/lib/server/session-tools/subagent.ts +3 -1
- package/src/lib/server/session-tools/web.ts +73 -30
- package/src/lib/server/storage.ts +29 -3
- package/src/lib/server/stream-agent-chat.test.ts +61 -0
- package/src/lib/server/stream-agent-chat.ts +139 -4
- package/src/lib/server/structured-extract.ts +1 -1
- package/src/lib/server/task-mention.ts +0 -1
- package/src/lib/server/tool-aliases.ts +37 -6
- package/src/lib/server/tool-capability-policy.ts +1 -1
- package/src/lib/setup-defaults.ts +352 -11
- package/src/lib/tool-definitions.ts +3 -4
- package/src/lib/validation/schemas.ts +55 -1
- package/src/stores/use-app-store.ts +43 -1
- package/src/stores/use-chatroom-store.ts +153 -26
- package/src/types/index.ts +189 -6
- package/src/app/api/chats/[id]/main-loop/route.ts +0 -13
package/src/app/page.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { useEffect, useState, useCallback } from 'react'
|
|
|
4
4
|
import { useAppStore } from '@/stores/use-app-store'
|
|
5
5
|
import { initAudioContext } from '@/lib/tts'
|
|
6
6
|
import { getStoredAccessKey, clearStoredAccessKey, api } from '@/lib/api-client'
|
|
7
|
+
import { safeStorageGet, safeStorageRemove, safeStorageSet } from '@/lib/safe-storage'
|
|
7
8
|
import { connectWs, disconnectWs } from '@/lib/ws-client'
|
|
8
9
|
import { fetchWithTimeout } from '@/lib/fetch-timeout'
|
|
9
10
|
import { useWs } from '@/hooks/use-ws'
|
|
@@ -12,10 +13,17 @@ import { UserPicker } from '@/components/auth/user-picker'
|
|
|
12
13
|
import { SetupWizard } from '@/components/auth/setup-wizard'
|
|
13
14
|
import { AppLayout } from '@/components/layout/app-layout'
|
|
14
15
|
import { useViewRouter } from '@/hooks/use-view-router'
|
|
16
|
+
import type { Agent } from '@/types'
|
|
15
17
|
|
|
16
18
|
const AUTH_CHECK_TIMEOUT_MS = 8_000
|
|
19
|
+
const POST_AUTH_BOOTSTRAP_TIMEOUT_MS = 8_000
|
|
17
20
|
|
|
18
|
-
function FullScreenLoader(
|
|
21
|
+
function FullScreenLoader(props: {
|
|
22
|
+
stage?: string | null
|
|
23
|
+
stalled?: boolean
|
|
24
|
+
onReload?: () => void
|
|
25
|
+
onReset?: () => void
|
|
26
|
+
}) {
|
|
19
27
|
return (
|
|
20
28
|
<div className="h-full flex flex-col items-center justify-center bg-bg overflow-hidden select-none">
|
|
21
29
|
{/* Animated orbital ring */}
|
|
@@ -106,6 +114,42 @@ function FullScreenLoader() {
|
|
|
106
114
|
/>
|
|
107
115
|
</div>
|
|
108
116
|
|
|
117
|
+
{props.stage ? (
|
|
118
|
+
<p
|
|
119
|
+
className="mt-4 text-[12px] text-text-3"
|
|
120
|
+
style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.4s both' }}
|
|
121
|
+
>
|
|
122
|
+
{props.stage}
|
|
123
|
+
</p>
|
|
124
|
+
) : null}
|
|
125
|
+
|
|
126
|
+
{props.stalled ? (
|
|
127
|
+
<div
|
|
128
|
+
className="mt-6 max-w-[360px] px-4 text-center"
|
|
129
|
+
style={{ animation: 'fade-up 0.6s var(--ease-spring) 0.5s both' }}
|
|
130
|
+
>
|
|
131
|
+
<p className="text-[12px] text-text-2">
|
|
132
|
+
Startup is taking longer than expected. This usually means the browser kept stale local state while the dev server restarted.
|
|
133
|
+
</p>
|
|
134
|
+
<div className="mt-4 flex items-center justify-center gap-3">
|
|
135
|
+
<button
|
|
136
|
+
type="button"
|
|
137
|
+
onClick={props.onReload}
|
|
138
|
+
className="px-4 py-2 rounded-[12px] border border-white/[0.08] bg-surface text-[12px] text-text-2 transition-colors hover:bg-surface-2"
|
|
139
|
+
>
|
|
140
|
+
Reload
|
|
141
|
+
</button>
|
|
142
|
+
<button
|
|
143
|
+
type="button"
|
|
144
|
+
onClick={props.onReset}
|
|
145
|
+
className="px-4 py-2 rounded-[12px] border border-white/[0.08] bg-transparent text-[12px] text-text-3 transition-colors hover:bg-white/[0.04]"
|
|
146
|
+
>
|
|
147
|
+
Reset Local Session
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
) : null}
|
|
152
|
+
|
|
109
153
|
{/* Loading animation keyframes */}
|
|
110
154
|
<style>{`
|
|
111
155
|
@keyframes sc-orbit {
|
|
@@ -150,8 +194,9 @@ export default function Home() {
|
|
|
150
194
|
|
|
151
195
|
const [authChecked, setAuthChecked] = useState(false)
|
|
152
196
|
const [authenticated, setAuthenticated] = useState(false)
|
|
197
|
+
const [bootTimedOut, setBootTimedOut] = useState(false)
|
|
153
198
|
const [setupDone, setSetupDone] = useState<boolean | null>(() => {
|
|
154
|
-
if (
|
|
199
|
+
if (safeStorageGet('sc_setup_done') === '1') return true
|
|
155
200
|
return null
|
|
156
201
|
})
|
|
157
202
|
|
|
@@ -183,7 +228,8 @@ export default function Home() {
|
|
|
183
228
|
setAuthenticated(false)
|
|
184
229
|
}
|
|
185
230
|
} catch {
|
|
186
|
-
|
|
231
|
+
clearStoredAccessKey()
|
|
232
|
+
setAuthenticated(false)
|
|
187
233
|
} finally {
|
|
188
234
|
setAuthChecked(true)
|
|
189
235
|
}
|
|
@@ -193,7 +239,10 @@ export default function Home() {
|
|
|
193
239
|
const syncUserFromServer = useCallback(async () => {
|
|
194
240
|
if (currentUser) return // already have a name locally
|
|
195
241
|
try {
|
|
196
|
-
const settings = await api<{ userName?: string }>('GET', '/settings'
|
|
242
|
+
const settings = await api<{ userName?: string }>('GET', '/settings', undefined, {
|
|
243
|
+
timeoutMs: POST_AUTH_BOOTSTRAP_TIMEOUT_MS,
|
|
244
|
+
retries: 0,
|
|
245
|
+
})
|
|
197
246
|
if (settings.userName) {
|
|
198
247
|
setUser(settings.userName)
|
|
199
248
|
}
|
|
@@ -229,11 +278,14 @@ export default function Home() {
|
|
|
229
278
|
let cancelled = false
|
|
230
279
|
;(async () => {
|
|
231
280
|
try {
|
|
232
|
-
const
|
|
233
|
-
|
|
281
|
+
const agents = await api<Record<string, Agent>>('GET', '/agents', undefined, {
|
|
282
|
+
timeoutMs: POST_AUTH_BOOTSTRAP_TIMEOUT_MS,
|
|
283
|
+
retries: 0,
|
|
284
|
+
})
|
|
234
285
|
if (cancelled) return
|
|
286
|
+
useAppStore.setState({ agents })
|
|
235
287
|
|
|
236
|
-
const {
|
|
288
|
+
const { currentAgentId, appSettings } = useAppStore.getState()
|
|
237
289
|
// Priority: persisted agent > settings default > first agent
|
|
238
290
|
const targetId = (currentAgentId && agents[currentAgentId])
|
|
239
291
|
? currentAgentId
|
|
@@ -256,14 +308,23 @@ export default function Home() {
|
|
|
256
308
|
let cancelled = false
|
|
257
309
|
;(async () => {
|
|
258
310
|
try {
|
|
259
|
-
const [
|
|
260
|
-
api<{ setupCompleted?: boolean }>('GET', '/settings'
|
|
261
|
-
|
|
311
|
+
const [settingsResult, credsResult] = await Promise.allSettled([
|
|
312
|
+
api<{ setupCompleted?: boolean }>('GET', '/settings', undefined, {
|
|
313
|
+
timeoutMs: POST_AUTH_BOOTSTRAP_TIMEOUT_MS,
|
|
314
|
+
retries: 0,
|
|
315
|
+
}),
|
|
316
|
+
api<Record<string, unknown>>('GET', '/credentials', undefined, {
|
|
317
|
+
timeoutMs: POST_AUTH_BOOTSTRAP_TIMEOUT_MS,
|
|
318
|
+
retries: 0,
|
|
319
|
+
}),
|
|
262
320
|
])
|
|
263
321
|
if (cancelled) return
|
|
322
|
+
const settings = settingsResult.status === 'fulfilled' ? settingsResult.value : {}
|
|
323
|
+
const creds = credsResult.status === 'fulfilled' ? credsResult.value : {}
|
|
324
|
+
const bothFailed = settingsResult.status === 'rejected' && credsResult.status === 'rejected'
|
|
264
325
|
const hasCreds = Object.keys(creds).length > 0
|
|
265
|
-
const done = settings.setupCompleted === true || hasCreds
|
|
266
|
-
if (done)
|
|
326
|
+
const done = bothFailed ? true : settings.setupCompleted === true || hasCreds
|
|
327
|
+
if (done) safeStorageSet('sc_setup_done', '1')
|
|
267
328
|
setSetupDone(done)
|
|
268
329
|
} catch {
|
|
269
330
|
if (!cancelled) setSetupDone(true) // on error, skip wizard
|
|
@@ -293,10 +354,60 @@ export default function Home() {
|
|
|
293
354
|
|
|
294
355
|
useViewRouter()
|
|
295
356
|
|
|
296
|
-
|
|
357
|
+
const bootStage = !hydrated
|
|
358
|
+
? 'Restoring local session'
|
|
359
|
+
: !authChecked
|
|
360
|
+
? 'Checking access'
|
|
361
|
+
: authenticated && currentUser && setupDone === null
|
|
362
|
+
? 'Loading setup state'
|
|
363
|
+
: authenticated && currentUser && !agentReady
|
|
364
|
+
? 'Restoring agent workspace'
|
|
365
|
+
: null
|
|
366
|
+
|
|
367
|
+
useEffect(() => {
|
|
368
|
+
if (!bootStage) {
|
|
369
|
+
setBootTimedOut(false)
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
const timer = window.setTimeout(() => setBootTimedOut(true), 15_000)
|
|
373
|
+
return () => window.clearTimeout(timer)
|
|
374
|
+
}, [bootStage])
|
|
375
|
+
|
|
376
|
+
const reloadApp = useCallback(() => {
|
|
377
|
+
window.location.reload()
|
|
378
|
+
}, [])
|
|
379
|
+
|
|
380
|
+
const resetLocalSession = useCallback(() => {
|
|
381
|
+
clearStoredAccessKey()
|
|
382
|
+
disconnectWs()
|
|
383
|
+
safeStorageRemove('sc_user')
|
|
384
|
+
safeStorageRemove('sc_agent')
|
|
385
|
+
safeStorageRemove('sc_setup_done')
|
|
386
|
+
window.location.assign('/')
|
|
387
|
+
}, [])
|
|
388
|
+
|
|
389
|
+
if (!hydrated || !authChecked) {
|
|
390
|
+
return (
|
|
391
|
+
<FullScreenLoader
|
|
392
|
+
stage={bootStage}
|
|
393
|
+
stalled={bootTimedOut}
|
|
394
|
+
onReload={reloadApp}
|
|
395
|
+
onReset={resetLocalSession}
|
|
396
|
+
/>
|
|
397
|
+
)
|
|
398
|
+
}
|
|
297
399
|
if (!authenticated) return <AccessKeyGate onAuthenticated={() => setAuthenticated(true)} />
|
|
298
400
|
if (!currentUser) return <UserPicker />
|
|
299
|
-
if (setupDone === null || !agentReady)
|
|
300
|
-
|
|
401
|
+
if (setupDone === null || !agentReady) {
|
|
402
|
+
return (
|
|
403
|
+
<FullScreenLoader
|
|
404
|
+
stage={bootStage}
|
|
405
|
+
stalled={bootTimedOut}
|
|
406
|
+
onReload={reloadApp}
|
|
407
|
+
onReset={resetLocalSession}
|
|
408
|
+
/>
|
|
409
|
+
)
|
|
410
|
+
}
|
|
411
|
+
if (!setupDone) return <SetupWizard onComplete={() => { safeStorageSet('sc_setup_done', '1'); setSetupDone(true) }} />
|
|
301
412
|
return <AppLayout />
|
|
302
413
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
3
|
+
|
|
4
|
+
const test = require('node:test')
|
|
5
|
+
const assert = require('node:assert/strict')
|
|
6
|
+
const fs = require('node:fs')
|
|
7
|
+
const os = require('node:os')
|
|
8
|
+
const path = require('node:path')
|
|
9
|
+
const { spawnSync } = require('node:child_process')
|
|
10
|
+
const { buildLegacyTsCliArgs } = require('../../bin/swarmclaw.js')
|
|
11
|
+
|
|
12
|
+
const CLI_BIN = path.join(__dirname, '..', '..', 'bin', 'swarmclaw.js')
|
|
13
|
+
const PACKAGE_JSON = require('../../package.json')
|
|
14
|
+
const APP_ROOT = path.join(__dirname, '..', '..')
|
|
15
|
+
|
|
16
|
+
function runBinary(args, options = {}) {
|
|
17
|
+
return spawnSync(process.execPath, [CLI_BIN, ...args], {
|
|
18
|
+
cwd: options.cwd || APP_ROOT,
|
|
19
|
+
env: {
|
|
20
|
+
...process.env,
|
|
21
|
+
...options.env,
|
|
22
|
+
},
|
|
23
|
+
encoding: 'utf8',
|
|
24
|
+
})
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function runWithMockedFetch(args, options = {}) {
|
|
28
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-binary-fetch-'))
|
|
29
|
+
const capturePath = path.join(tmpDir, 'capture.json')
|
|
30
|
+
const preloadPath = path.join(tmpDir, 'mock-fetch.cjs')
|
|
31
|
+
|
|
32
|
+
fs.writeFileSync(
|
|
33
|
+
preloadPath,
|
|
34
|
+
`
|
|
35
|
+
const fs = require('node:fs')
|
|
36
|
+
globalThis.fetch = async (url, init = {}) => {
|
|
37
|
+
const capture = {
|
|
38
|
+
url: String(url),
|
|
39
|
+
method: init.method || 'GET',
|
|
40
|
+
headers: init.headers || {},
|
|
41
|
+
body: typeof init.body === 'string'
|
|
42
|
+
? init.body
|
|
43
|
+
: (Buffer.isBuffer(init.body) ? init.body.toString('utf8') : null),
|
|
44
|
+
}
|
|
45
|
+
fs.writeFileSync(process.env.SWARMCLAW_TEST_CAPTURE, JSON.stringify(capture), 'utf8')
|
|
46
|
+
return new Response(JSON.stringify([]), {
|
|
47
|
+
status: 200,
|
|
48
|
+
headers: { 'content-type': 'application/json' },
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
`,
|
|
52
|
+
'utf8',
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const nodeOptions = [process.env.NODE_OPTIONS, `--require=${preloadPath}`]
|
|
56
|
+
.filter(Boolean)
|
|
57
|
+
.join(' ')
|
|
58
|
+
|
|
59
|
+
const result = runBinary(args, {
|
|
60
|
+
...options,
|
|
61
|
+
env: {
|
|
62
|
+
...options.env,
|
|
63
|
+
NODE_OPTIONS: nodeOptions,
|
|
64
|
+
SWARMCLAW_TEST_CAPTURE: capturePath,
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
const capture = fs.existsSync(capturePath)
|
|
69
|
+
? JSON.parse(fs.readFileSync(capturePath, 'utf8'))
|
|
70
|
+
: null
|
|
71
|
+
|
|
72
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
73
|
+
return { result, capture }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
test('legacy-routed binary commands honor SWARMCLAW_API_KEY', () => {
|
|
77
|
+
const { result, capture } = runWithMockedFetch(
|
|
78
|
+
['runs', 'list', '--raw', '--url', 'http://localhost:3456'],
|
|
79
|
+
{
|
|
80
|
+
env: {
|
|
81
|
+
SWARMCLAW_API_KEY: 'legacy-api-key',
|
|
82
|
+
SWARMCLAW_ACCESS_KEY: '',
|
|
83
|
+
SC_ACCESS_KEY: '',
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
assert.equal(result.status, 0, result.stderr)
|
|
89
|
+
assert.equal(result.stdout.trim(), '[]')
|
|
90
|
+
assert.equal(capture.headers['X-Access-Key'], 'legacy-api-key')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('legacy-routed binary commands fall back to platform-api-key.txt', () => {
|
|
94
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-binary-keyfile-'))
|
|
95
|
+
fs.writeFileSync(path.join(tmpDir, 'platform-api-key.txt'), 'file-fallback-key\n', 'utf8')
|
|
96
|
+
|
|
97
|
+
const { result, capture } = runWithMockedFetch(
|
|
98
|
+
['runs', 'list', '--raw', '--url', 'http://localhost:3456'],
|
|
99
|
+
{
|
|
100
|
+
cwd: tmpDir,
|
|
101
|
+
env: {
|
|
102
|
+
SWARMCLAW_API_KEY: '',
|
|
103
|
+
SWARMCLAW_ACCESS_KEY: '',
|
|
104
|
+
SC_ACCESS_KEY: '',
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
assert.equal(result.status, 0, result.stderr)
|
|
110
|
+
assert.equal(result.stdout.trim(), '[]')
|
|
111
|
+
assert.equal(capture.headers['X-Access-Key'], 'file-fallback-key')
|
|
112
|
+
|
|
113
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('binary server help exits successfully', () => {
|
|
117
|
+
const result = runBinary(['server', '--help'])
|
|
118
|
+
assert.equal(result.status, 0, result.stderr)
|
|
119
|
+
assert.match(result.stdout, /Usage: swarmclaw server/i)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('binary update help exits successfully', () => {
|
|
123
|
+
const result = runBinary(['update', '--help'])
|
|
124
|
+
assert.equal(result.status, 0, result.stderr)
|
|
125
|
+
assert.match(result.stdout, /Usage: swarmclaw update/i)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('binary version output matches package version', () => {
|
|
129
|
+
const result = runBinary(['--version'])
|
|
130
|
+
assert.equal(result.status, 0, result.stderr)
|
|
131
|
+
assert.equal(result.stdout.trim(), `${PACKAGE_JSON.name} ${PACKAGE_JSON.version}`)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('legacy TS launcher falls back to tsx import when strip-types is unavailable', () => {
|
|
135
|
+
const cliPath = path.join(APP_ROOT, 'src', 'cli', 'index.ts')
|
|
136
|
+
const args = buildLegacyTsCliArgs(cliPath, ['runs', 'list'], {
|
|
137
|
+
supportsStripTypes: false,
|
|
138
|
+
hasTsxRuntime: true,
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
assert.deepEqual(args, ['--no-warnings', '--import', 'tsx', cliPath, 'runs', 'list'])
|
|
142
|
+
})
|
package/src/cli/index.js
CHANGED
|
@@ -181,6 +181,17 @@ const COMMAND_GROUPS = [
|
|
|
181
181
|
cmd('suite', 'POST', '/eval/suite', 'Run a full eval suite against an agent', { expectsJsonBody: true }),
|
|
182
182
|
],
|
|
183
183
|
},
|
|
184
|
+
{
|
|
185
|
+
name: 'external-agents',
|
|
186
|
+
description: 'Manage external agent runtimes',
|
|
187
|
+
commands: [
|
|
188
|
+
cmd('list', 'GET', '/external-agents', 'List external agent runtimes'),
|
|
189
|
+
cmd('create', 'POST', '/external-agents', 'Register an external agent runtime', { expectsJsonBody: true }),
|
|
190
|
+
cmd('update', 'PUT', '/external-agents/:id', 'Update an external agent runtime', { expectsJsonBody: true }),
|
|
191
|
+
cmd('delete', 'DELETE', '/external-agents/:id', 'Delete an external agent runtime'),
|
|
192
|
+
cmd('heartbeat', 'POST', '/external-agents/:id/heartbeat', 'Record an external agent heartbeat', { expectsJsonBody: true }),
|
|
193
|
+
],
|
|
194
|
+
},
|
|
184
195
|
{
|
|
185
196
|
name: 'files',
|
|
186
197
|
description: 'Serve and manage local files',
|
|
@@ -189,6 +200,17 @@ const COMMAND_GROUPS = [
|
|
|
189
200
|
cmd('open', 'POST', '/files/open', 'Open a local file path via the host default app/browser', { expectsJsonBody: true }),
|
|
190
201
|
],
|
|
191
202
|
},
|
|
203
|
+
{
|
|
204
|
+
name: 'gateways',
|
|
205
|
+
description: 'Manage named OpenClaw gateway profiles',
|
|
206
|
+
commands: [
|
|
207
|
+
cmd('list', 'GET', '/gateways', 'List configured gateway profiles'),
|
|
208
|
+
cmd('create', 'POST', '/gateways', 'Create a gateway profile', { expectsJsonBody: true }),
|
|
209
|
+
cmd('update', 'PUT', '/gateways/:id', 'Update a gateway profile', { expectsJsonBody: true }),
|
|
210
|
+
cmd('delete', 'DELETE', '/gateways/:id', 'Delete a gateway profile'),
|
|
211
|
+
cmd('health', 'GET', '/gateways/:id/health', 'Run a gateway health check'),
|
|
212
|
+
],
|
|
213
|
+
},
|
|
192
214
|
{
|
|
193
215
|
name: 'ip',
|
|
194
216
|
description: 'Get local IP/port metadata',
|
|
@@ -364,6 +386,7 @@ const COMMAND_GROUPS = [
|
|
|
364
386
|
cmd('update', 'PUT', '/providers/:id', 'Update provider', { expectsJsonBody: true }),
|
|
365
387
|
cmd('delete', 'DELETE', '/providers/:id', 'Delete provider'),
|
|
366
388
|
cmd('configs', 'GET', '/providers/configs', 'List saved provider configs'),
|
|
389
|
+
cmd('discover-models', 'GET', '/providers/:id/discover-models', 'Discover provider models via endpoint or credential checks'),
|
|
367
390
|
cmd('ollama', 'GET', '/providers/ollama', 'List local Ollama models (use --query endpoint=http://localhost:11434)'),
|
|
368
391
|
cmd('openclaw-health', 'GET', '/providers/openclaw/health', 'Probe OpenClaw endpoint/auth (use --query endpoint= --query credentialId= --query model=)'),
|
|
369
392
|
cmd('models', 'GET', '/providers/:id/models', 'Get provider model overrides'),
|
|
@@ -429,10 +452,6 @@ const COMMAND_GROUPS = [
|
|
|
429
452
|
cmd('messages-delete', 'DELETE', '/chats/:id/messages', 'Delete a message from a chat', { expectsJsonBody: true }),
|
|
430
453
|
cmd('fork', 'POST', '/chats/:id/fork', 'Fork chat from a specific message index', { expectsJsonBody: true }),
|
|
431
454
|
cmd('edit-resend', 'POST', '/chats/:id/edit-resend', 'Edit and resend from a specific message index', { expectsJsonBody: true }),
|
|
432
|
-
cmd('main-loop', 'GET', '/chats/:id/main-loop', 'Get main mission loop state'),
|
|
433
|
-
cmd('main-loop-action', 'POST', '/chats/:id/main-loop', 'Control main mission loop (pause/resume/set_goal/set_mode/clear_events/nudge)', {
|
|
434
|
-
expectsJsonBody: true,
|
|
435
|
-
}),
|
|
436
455
|
cmd('chat', 'POST', '/chats/:id/chat', 'Send chat message (streaming)', {
|
|
437
456
|
expectsJsonBody: true,
|
|
438
457
|
responseType: 'sse',
|
|
@@ -677,7 +696,7 @@ function normalizeBaseUrl(raw) {
|
|
|
677
696
|
|
|
678
697
|
function resolveAccessKey(opts, env, cwd) {
|
|
679
698
|
if (opts.accessKey) return String(opts.accessKey).trim()
|
|
680
|
-
const envKey = env.SWARMCLAW_API_KEY || env.SC_ACCESS_KEY || ''
|
|
699
|
+
const envKey = env.SWARMCLAW_API_KEY || env.SC_ACCESS_KEY || env.SWARMCLAW_ACCESS_KEY || ''
|
|
681
700
|
if (envKey) return String(envKey).trim()
|
|
682
701
|
|
|
683
702
|
const keyFile = path.join(cwd, 'platform-api-key.txt')
|
|
@@ -927,9 +946,11 @@ async function consumeSse(body, stdout, stderr, jsonOutput) {
|
|
|
927
946
|
const reader = body.getReader()
|
|
928
947
|
const decoder = new TextDecoder()
|
|
929
948
|
let buffer = ''
|
|
949
|
+
const eventBoundary = /\r?\n\r?\n/
|
|
930
950
|
|
|
931
951
|
function flushChunk(rawChunk) {
|
|
932
952
|
const lines = rawChunk
|
|
953
|
+
.replace(/\r\n/g, '\n')
|
|
933
954
|
.split('\n')
|
|
934
955
|
.map((line) => line.trimEnd())
|
|
935
956
|
.filter(Boolean)
|
|
@@ -972,12 +993,14 @@ async function consumeSse(body, stdout, stderr, jsonOutput) {
|
|
|
972
993
|
if (done) break
|
|
973
994
|
buffer += decoder.decode(value, { stream: true })
|
|
974
995
|
|
|
975
|
-
let
|
|
976
|
-
while (
|
|
996
|
+
let match = eventBoundary.exec(buffer)
|
|
997
|
+
while (match) {
|
|
998
|
+
const splitIndex = match.index
|
|
999
|
+
const delimiterLength = match[0].length
|
|
977
1000
|
const chunk = buffer.slice(0, splitIndex)
|
|
978
|
-
buffer = buffer.slice(splitIndex +
|
|
1001
|
+
buffer = buffer.slice(splitIndex + delimiterLength)
|
|
979
1002
|
flushChunk(chunk)
|
|
980
|
-
|
|
1003
|
+
match = eventBoundary.exec(buffer)
|
|
981
1004
|
}
|
|
982
1005
|
}
|
|
983
1006
|
|
|
@@ -1090,7 +1113,7 @@ function renderGeneralHelp() {
|
|
|
1090
1113
|
'',
|
|
1091
1114
|
'Global options:',
|
|
1092
1115
|
' --base-url <url> API base URL (default: http://localhost:3456)',
|
|
1093
|
-
' --access-key <key> Access key override (else SWARMCLAW_API_KEY or platform-api-key.txt)',
|
|
1116
|
+
' --access-key <key> Access key override (else SWARMCLAW_API_KEY/SWARMCLAW_ACCESS_KEY or platform-api-key.txt)',
|
|
1094
1117
|
' --data <json|@file|-> Request JSON body',
|
|
1095
1118
|
' --query key=value Query parameter (repeatable)',
|
|
1096
1119
|
' --header key=value Extra HTTP header (repeatable)',
|
|
@@ -1215,7 +1238,7 @@ async function runCli(argv, deps = {}) {
|
|
|
1215
1238
|
}
|
|
1216
1239
|
|
|
1217
1240
|
const accessKey = resolveAccessKey(parsed.opts, env, cwd)
|
|
1218
|
-
const baseUrl = parsed.opts.baseUrl || env.SWARMCLAW_BASE_URL || 'http://localhost:3456'
|
|
1241
|
+
const baseUrl = parsed.opts.baseUrl || env.SWARMCLAW_BASE_URL || env.SWARMCLAW_URL || 'http://localhost:3456'
|
|
1219
1242
|
|
|
1220
1243
|
const headerEntries = []
|
|
1221
1244
|
for (const raw of parsed.opts.headers) {
|
package/src/cli/index.test.js
CHANGED
|
@@ -163,6 +163,37 @@ test('runCli sends authenticated request and emits compact JSON when --json is s
|
|
|
163
163
|
assert.equal(stderr.toString(), '')
|
|
164
164
|
})
|
|
165
165
|
|
|
166
|
+
test('runCli falls back to platform-api-key.txt when env key is missing', async () => {
|
|
167
|
+
const stdout = makeWritable()
|
|
168
|
+
const stderr = makeWritable()
|
|
169
|
+
const calls = []
|
|
170
|
+
|
|
171
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-cli-keyfile-'))
|
|
172
|
+
fs.writeFileSync(path.join(tmpDir, 'platform-api-key.txt'), 'file-key\n', 'utf8')
|
|
173
|
+
|
|
174
|
+
const fetchImpl = async (url, init) => {
|
|
175
|
+
calls.push({ url: String(url), init })
|
|
176
|
+
return jsonResponse({ ok: true })
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const exitCode = await runCli(
|
|
180
|
+
['runs', 'list', '--json'],
|
|
181
|
+
{
|
|
182
|
+
fetchImpl,
|
|
183
|
+
stdout,
|
|
184
|
+
stderr,
|
|
185
|
+
env: {},
|
|
186
|
+
cwd: tmpDir,
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
assert.equal(exitCode, 0)
|
|
191
|
+
assert.equal(calls[0].init.headers['X-Access-Key'], 'file-key')
|
|
192
|
+
assert.equal(stderr.toString(), '')
|
|
193
|
+
|
|
194
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
195
|
+
})
|
|
196
|
+
|
|
166
197
|
test('upload command sends binary body and x-filename header', async () => {
|
|
167
198
|
const stdout = makeWritable()
|
|
168
199
|
const stderr = makeWritable()
|
|
@@ -197,6 +228,32 @@ test('upload command sends binary body and x-filename header', async () => {
|
|
|
197
228
|
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
198
229
|
})
|
|
199
230
|
|
|
231
|
+
test('binary responses require --out when stdout is a TTY', async () => {
|
|
232
|
+
const stdout = makeWritable()
|
|
233
|
+
stdout.isTTY = true
|
|
234
|
+
const stderr = makeWritable()
|
|
235
|
+
|
|
236
|
+
const fetchImpl = async () =>
|
|
237
|
+
new Response(Buffer.from('hello'), {
|
|
238
|
+
status: 200,
|
|
239
|
+
headers: { 'content-type': 'application/octet-stream' },
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
const exitCode = await runCli(
|
|
243
|
+
['uploads', 'get', 'artifact.bin'],
|
|
244
|
+
{
|
|
245
|
+
fetchImpl,
|
|
246
|
+
stdout,
|
|
247
|
+
stderr,
|
|
248
|
+
env: {},
|
|
249
|
+
cwd: process.cwd(),
|
|
250
|
+
}
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
assert.equal(exitCode, 1)
|
|
254
|
+
assert.match(stderr.toString(), /binary response requires --out <file>/i)
|
|
255
|
+
})
|
|
256
|
+
|
|
200
257
|
test('wait polls run endpoint until terminal state', async () => {
|
|
201
258
|
const stdout = makeWritable()
|
|
202
259
|
const stderr = makeWritable()
|
|
@@ -236,6 +293,144 @@ test('wait polls run endpoint until terminal state', async () => {
|
|
|
236
293
|
assert.match(stdout.toString(), /"status": "completed"/)
|
|
237
294
|
})
|
|
238
295
|
|
|
296
|
+
test('runCli parses CRLF-delimited SSE events correctly', async () => {
|
|
297
|
+
const stdout = makeWritable()
|
|
298
|
+
const stderr = makeWritable()
|
|
299
|
+
|
|
300
|
+
const fetchImpl = async () => new Response(
|
|
301
|
+
'data: {"t":"md","text":"first"}\r\n\r\ndata: {"t":"md","text":"second"}\r\n\r\n',
|
|
302
|
+
{
|
|
303
|
+
status: 200,
|
|
304
|
+
headers: { 'content-type': 'text/event-stream' },
|
|
305
|
+
}
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
const exitCode = await runCli(
|
|
309
|
+
['chatrooms', 'chat', 'room-1', '--data', '{}'],
|
|
310
|
+
{
|
|
311
|
+
fetchImpl,
|
|
312
|
+
stdout,
|
|
313
|
+
stderr,
|
|
314
|
+
env: {},
|
|
315
|
+
cwd: process.cwd(),
|
|
316
|
+
}
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
assert.equal(exitCode, 0)
|
|
320
|
+
assert.equal(stdout.toString(), 'first\nsecond\n')
|
|
321
|
+
assert.equal(stderr.toString(), '')
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
test('binary responses require --out when stdout is a TTY', async () => {
|
|
325
|
+
const stdout = makeWritable()
|
|
326
|
+
stdout.isTTY = true
|
|
327
|
+
const stderr = makeWritable()
|
|
328
|
+
|
|
329
|
+
const fetchImpl = async () => new Response(Buffer.from('ok'), {
|
|
330
|
+
status: 200,
|
|
331
|
+
headers: { 'content-type': 'application/octet-stream' },
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
const exitCode = await runCli(
|
|
335
|
+
['memory-images', 'get', 'image-1.png'],
|
|
336
|
+
{
|
|
337
|
+
fetchImpl,
|
|
338
|
+
stdout,
|
|
339
|
+
stderr,
|
|
340
|
+
env: {},
|
|
341
|
+
cwd: process.cwd(),
|
|
342
|
+
}
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
assert.equal(exitCode, 1)
|
|
346
|
+
assert.equal(stdout.toString(), '')
|
|
347
|
+
assert.match(stderr.toString(), /binary response requires --out <file>/i)
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
test('client-side collection lookups fail cleanly when the entity is missing', async () => {
|
|
351
|
+
const stdout = makeWritable()
|
|
352
|
+
const stderr = makeWritable()
|
|
353
|
+
|
|
354
|
+
const fetchImpl = async () => jsonResponse([{ id: 'agent-2', name: 'Other Agent' }])
|
|
355
|
+
|
|
356
|
+
const exitCode = await runCli(
|
|
357
|
+
['agents', 'get', 'agent-1'],
|
|
358
|
+
{
|
|
359
|
+
fetchImpl,
|
|
360
|
+
stdout,
|
|
361
|
+
stderr,
|
|
362
|
+
env: {},
|
|
363
|
+
cwd: process.cwd(),
|
|
364
|
+
}
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
assert.equal(exitCode, 1)
|
|
368
|
+
assert.equal(stdout.toString(), '')
|
|
369
|
+
assert.match(stderr.toString(), /entity not found for id: agent-1/i)
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
test('runCli loads request JSON from @file inputs', async () => {
|
|
373
|
+
const stdout = makeWritable()
|
|
374
|
+
const stderr = makeWritable()
|
|
375
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-cli-data-'))
|
|
376
|
+
const dataPath = path.join(tmpDir, 'payload.json')
|
|
377
|
+
fs.writeFileSync(dataPath, JSON.stringify({ title: 'From file', status: 'backlog' }), 'utf8')
|
|
378
|
+
|
|
379
|
+
const calls = []
|
|
380
|
+
const fetchImpl = async (url, init) => {
|
|
381
|
+
calls.push({ url: String(url), init })
|
|
382
|
+
return jsonResponse({ ok: true })
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const exitCode = await runCli(
|
|
386
|
+
['tasks', 'create', '--data', `@${dataPath}`],
|
|
387
|
+
{
|
|
388
|
+
fetchImpl,
|
|
389
|
+
stdout,
|
|
390
|
+
stderr,
|
|
391
|
+
env: {},
|
|
392
|
+
cwd: process.cwd(),
|
|
393
|
+
}
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
assert.equal(exitCode, 0)
|
|
397
|
+
assert.equal(calls.length, 1)
|
|
398
|
+
assert.equal(calls[0].init.headers['Content-Type'], 'application/json')
|
|
399
|
+
assert.deepEqual(JSON.parse(calls[0].init.body), { title: 'From file', status: 'backlog' })
|
|
400
|
+
|
|
401
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
402
|
+
})
|
|
403
|
+
|
|
404
|
+
test('runCli falls back to platform-api-key.txt when no env key is provided', async () => {
|
|
405
|
+
const stdout = makeWritable()
|
|
406
|
+
const stderr = makeWritable()
|
|
407
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-cli-key-'))
|
|
408
|
+
fs.writeFileSync(path.join(tmpDir, 'platform-api-key.txt'), 'file-key\n', 'utf8')
|
|
409
|
+
|
|
410
|
+
const calls = []
|
|
411
|
+
const fetchImpl = async (url, init) => {
|
|
412
|
+
calls.push({ url: String(url), init })
|
|
413
|
+
return jsonResponse({ ok: true })
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const exitCode = await runCli(
|
|
417
|
+
['runs', 'list'],
|
|
418
|
+
{
|
|
419
|
+
fetchImpl,
|
|
420
|
+
stdout,
|
|
421
|
+
stderr,
|
|
422
|
+
env: {},
|
|
423
|
+
cwd: tmpDir,
|
|
424
|
+
}
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
assert.equal(exitCode, 0)
|
|
428
|
+
assert.equal(calls.length, 1)
|
|
429
|
+
assert.equal(calls[0].init.headers['X-Access-Key'], 'file-key')
|
|
430
|
+
|
|
431
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
432
|
+
})
|
|
433
|
+
|
|
239
434
|
test('all command definitions execute with a mocked API transport', async () => {
|
|
240
435
|
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-cli-all-'))
|
|
241
436
|
const uploadPath = path.join(tmpDir, 'upload.txt')
|