@swarmclawai/swarmclaw 0.7.5 → 0.7.6

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.
@@ -1,9 +1,19 @@
1
1
  'use client'
2
2
 
3
3
  import { useCallback, useEffect, useState } from 'react'
4
+ import { toast } from 'sonner'
5
+ import { OpenClawDeployPanel } from '@/components/openclaw/openclaw-deploy-panel'
4
6
  import { useAppStore } from '@/stores/use-app-store'
5
7
  import { useWs } from '@/hooks/use-ws'
6
8
  import { api } from '@/lib/api-client'
9
+ import type { Credential } from '@/types'
10
+
11
+ interface OpenClawDeployDraft {
12
+ endpoint: string
13
+ token?: string
14
+ name?: string
15
+ notes?: string
16
+ }
7
17
 
8
18
  export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
9
19
  const providers = useAppStore((s) => s.providers)
@@ -21,6 +31,8 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
21
31
  const setGatewaySheetOpen = useAppStore((s) => s.setGatewaySheetOpen)
22
32
  const setEditingGatewayId = useAppStore((s) => s.setEditingGatewayId)
23
33
  const [loaded, setLoaded] = useState(false)
34
+ const [deployDraft, setDeployDraft] = useState<OpenClawDeployDraft | null>(null)
35
+ const [savingDeploy, setSavingDeploy] = useState(false)
24
36
 
25
37
  const refresh = useCallback(async () => {
26
38
  await Promise.all([loadProviders(), loadProviderConfigs(), loadGatewayProfiles(), loadExternalAgents(), loadCredentials()])
@@ -66,6 +78,57 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
66
78
  await loadGatewayProfiles()
67
79
  }
68
80
 
81
+ const handleDeployApply = (patch: { endpoint?: string; token?: string; name?: string; notes?: string }) => {
82
+ if (!patch.endpoint) return
83
+ setDeployDraft({
84
+ endpoint: patch.endpoint,
85
+ token: patch.token,
86
+ name: patch.name,
87
+ notes: patch.notes,
88
+ })
89
+ }
90
+
91
+ const handleSavePreparedGateway = async () => {
92
+ if (!deployDraft?.endpoint) return
93
+ setSavingDeploy(true)
94
+ try {
95
+ let nextCredentialId: string | null = null
96
+ if (deployDraft.token?.trim()) {
97
+ const credential = await api<Credential>('POST', '/credentials', {
98
+ provider: 'openclaw',
99
+ name: `${deployDraft.name || 'OpenClaw Gateway'} token`,
100
+ apiKey: deployDraft.token.trim(),
101
+ })
102
+ nextCredentialId = credential.id
103
+ }
104
+
105
+ const existing = gatewayProfiles.find((gateway) => gateway.endpoint === deployDraft.endpoint) || null
106
+ const nextTags = Array.from(new Set([...(existing?.tags || []), 'managed-deploy']))
107
+ const payload = {
108
+ name: deployDraft.name || existing?.name || 'OpenClaw Gateway',
109
+ endpoint: deployDraft.endpoint,
110
+ credentialId: nextCredentialId || existing?.credentialId || null,
111
+ notes: deployDraft.notes || existing?.notes || 'Managed OpenClaw deploy prepared from SwarmClaw.',
112
+ tags: nextTags,
113
+ isDefault: existing?.isDefault === true || gatewayProfiles.length === 0,
114
+ }
115
+
116
+ if (existing) {
117
+ await api('PUT', `/gateways/${existing.id}`, payload)
118
+ } else {
119
+ await api('POST', '/gateways', payload)
120
+ }
121
+
122
+ await Promise.all([loadGatewayProfiles(), loadCredentials()])
123
+ setDeployDraft(null)
124
+ toast.success(existing ? 'Gateway profile updated' : 'Gateway profile saved')
125
+ } catch (err: unknown) {
126
+ toast.error(err instanceof Error ? err.message : 'Failed to save prepared gateway')
127
+ } finally {
128
+ setSavingDeploy(false)
129
+ }
130
+ }
131
+
69
132
  // Merge built-in providers with custom configs
70
133
  const builtinItems = providers.map((p) => ({
71
134
  id: p.id,
@@ -180,15 +243,47 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
180
243
  <div className="mt-8 mb-4 flex items-center justify-between">
181
244
  <div className="text-[12px] font-700 uppercase tracking-[0.08em] text-text-3/60">OpenClaw Gateways</div>
182
245
  {!inSidebar && (
183
- <button
184
- type="button"
185
- onClick={() => handleEditGateway(null)}
186
- className="px-3 py-1.5 rounded-[8px] border border-white/[0.08] bg-transparent text-[11px] font-700 text-text-2 hover:bg-white/[0.04] transition-all cursor-pointer"
187
- >
188
- + New Gateway
189
- </button>
246
+ <div className="flex items-center gap-2">
247
+ <button
248
+ type="button"
249
+ onClick={() => handleEditGateway(null)}
250
+ className="px-3 py-1.5 rounded-[8px] border border-white/[0.08] bg-transparent text-[11px] font-700 text-text-2 hover:bg-white/[0.04] transition-all cursor-pointer"
251
+ >
252
+ + New Gateway
253
+ </button>
254
+ </div>
190
255
  )}
191
256
  </div>
257
+ {!inSidebar && (
258
+ <div className="mb-4 rounded-[16px] border border-white/[0.06] bg-white/[0.02] p-4">
259
+ <OpenClawDeployPanel
260
+ compact
261
+ title="Deploy OpenClaw Control Planes"
262
+ description="Use official OpenClaw sources only. Start a local control plane on this machine, or generate a pre-configured remote bundle for Docker VPS hosts like Hetzner, DigitalOcean, Vultr, Linode, Lightsail, plus Render, Fly.io, and Railway."
263
+ onApply={handleDeployApply}
264
+ />
265
+ {deployDraft?.endpoint && (
266
+ <div className="mt-3 flex flex-wrap items-center justify-between gap-3 rounded-[12px] border border-emerald-500/20 bg-emerald-500/[0.05] px-4 py-3">
267
+ <div>
268
+ <div className="text-[13px] font-700 text-emerald-300">Prepared gateway profile</div>
269
+ <div className="mt-1 text-[12px] text-text-3">
270
+ {deployDraft.name || 'OpenClaw Gateway'} · <code className="text-text-2">{deployDraft.endpoint}</code>
271
+ </div>
272
+ </div>
273
+ <div className="flex flex-wrap gap-2">
274
+ <button
275
+ type="button"
276
+ onClick={() => void handleSavePreparedGateway()}
277
+ disabled={savingDeploy}
278
+ className="rounded-[10px] bg-accent-bright px-3.5 py-2 text-[12px] font-700 text-white border-none cursor-pointer hover:brightness-110 transition-all disabled:opacity-40"
279
+ >
280
+ {savingDeploy ? 'Saving…' : 'Save Prepared Gateway'}
281
+ </button>
282
+ </div>
283
+ </div>
284
+ )}
285
+ </div>
286
+ )}
192
287
  <div className={inSidebar ? 'space-y-2' : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3'}>
193
288
  {gatewayProfiles.map((gateway, idx) => (
194
289
  <div
@@ -246,7 +341,7 @@ export function ProviderList({ inSidebar }: { inSidebar?: boolean }) {
246
341
  ))}
247
342
  {gatewayProfiles.length === 0 && (
248
343
  <div className="p-4 rounded-[14px] border border-dashed border-white/[0.08] text-[13px] text-text-3/70">
249
- No gateway profiles yet. Add one to route OpenClaw agents by named control plane instead of a singleton default.
344
+ No gateway profiles yet. Use Smart Deploy above for a local runtime, a Docker VPS bundle, or a hosted OpenClaw deployment profile.
250
345
  </div>
251
346
  )}
252
347
  </div>
@@ -127,9 +127,13 @@ export interface HeartbeatConfig {
127
127
  target: string | null
128
128
  }
129
129
 
130
+ interface HeartbeatFileSession {
131
+ cwd?: string | null
132
+ }
133
+
130
134
  const DEFAULT_HEARTBEAT_PROMPT = 'Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.'
131
135
 
132
- function readHeartbeatFile(session: any): string {
136
+ function readHeartbeatFile(session: HeartbeatFileSession): string {
133
137
  try {
134
138
  const filePath = path.join(session.cwd || WORKSPACE_DIR, 'HEARTBEAT.md')
135
139
  if (fs.existsSync(filePath)) {
@@ -0,0 +1,67 @@
1
+ import assert from 'node:assert/strict'
2
+ import { test } from 'node:test'
3
+ import {
4
+ buildOpenClawDeployBundle,
5
+ getOpenClawLocalDeployStatus,
6
+ } from './openclaw-deploy.ts'
7
+
8
+ test('docker smart deploy bundle uses official image and provider-specific metadata', () => {
9
+ const bundle = buildOpenClawDeployBundle({
10
+ template: 'docker',
11
+ provider: 'digitalocean',
12
+ target: 'gateway.example.com',
13
+ token: 'test-token',
14
+ })
15
+
16
+ assert.equal(bundle.template, 'docker')
17
+ assert.equal(bundle.provider, 'digitalocean')
18
+ assert.equal(bundle.providerLabel, 'DigitalOcean')
19
+ assert.equal(bundle.endpoint, 'https://gateway.example.com/v1')
20
+ assert.equal(bundle.wsUrl, 'wss://gateway.example.com')
21
+ assert.match(bundle.summary, /official OpenClaw Docker image/i)
22
+ assert.deepEqual(bundle.files.map((file) => file.name), [
23
+ 'cloud-init.yaml',
24
+ '.env',
25
+ 'docker-compose.yml',
26
+ 'bootstrap.sh',
27
+ ])
28
+
29
+ const envFile = bundle.files.find((file) => file.name === '.env')
30
+ assert.ok(envFile)
31
+ assert.match(envFile.content, /OPENCLAW_IMAGE=openclaw:latest/)
32
+ assert.match(envFile.content, /OPENCLAW_GATEWAY_TOKEN=test-token/)
33
+
34
+ const cloudInit = bundle.files.find((file) => file.name === 'cloud-init.yaml')
35
+ assert.ok(cloudInit)
36
+ assert.match(cloudInit.content, /docker\.io/)
37
+ assert.match(cloudInit.content, /docker pull "\$\{OPENCLAW_IMAGE:-openclaw:latest\}"/)
38
+ assert.match(cloudInit.content, /\/opt\/openclaw\/docker-compose\.yml/)
39
+ })
40
+
41
+ test('render bundle stays aligned with the official repo flow', () => {
42
+ const bundle = buildOpenClawDeployBundle({
43
+ template: 'render',
44
+ target: 'https://openclaw.onrender.com',
45
+ token: 'render-token',
46
+ })
47
+
48
+ assert.equal(bundle.template, 'render')
49
+ assert.equal(bundle.providerLabel, 'Render')
50
+ assert.equal(bundle.endpoint, 'https://openclaw.onrender.com/v1')
51
+ assert.equal(bundle.token, 'render-token')
52
+ assert.deepEqual(bundle.files.map((file) => file.name), [
53
+ 'render.yaml',
54
+ 'OPENCLAW_GATEWAY_TOKEN.txt',
55
+ ])
56
+ assert.match(bundle.runbook[0] || '', /official OpenClaw GitHub repo/i)
57
+ })
58
+
59
+ test('local deploy status exposes a sensible default endpoint before startup', () => {
60
+ const status = getOpenClawLocalDeployStatus()
61
+
62
+ assert.equal(status.running, false)
63
+ assert.equal(status.port, 18789)
64
+ assert.equal(status.endpoint, 'http://127.0.0.1:18789/v1')
65
+ assert.equal(status.wsUrl, 'ws://127.0.0.1:18789')
66
+ assert.match(status.launchCommand, /npx openclaw gateway run/)
67
+ })