@swarmclawai/swarmclaw 1.5.49 → 1.5.51
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
CHANGED
|
@@ -81,8 +81,11 @@ Extension tutorial: https://swarmclaw.ai/docs/extension-tutorial
|
|
|
81
81
|
Download the one-click installer from [swarmclaw.ai/downloads](https://swarmclaw.ai/downloads).
|
|
82
82
|
Available for macOS (Apple Silicon & Intel), Windows, and Linux (AppImage + .deb).
|
|
83
83
|
|
|
84
|
-
Current builds are
|
|
85
|
-
- **macOS:** right-click the app in Finder → **Open** → **Open** to bypass Gatekeeper.
|
|
84
|
+
Current builds are ad-hoc signed but not notarized, so on first launch:
|
|
85
|
+
- **macOS:** right-click the app in Finder → **Open** → **Open** to bypass Gatekeeper. If macOS instead reports *"SwarmClaw is damaged and can't be opened"* (common on Apple Silicon when the dmg was quarantined by Safari), strip the quarantine attribute and relaunch:
|
|
86
|
+
```bash
|
|
87
|
+
xattr -dr com.apple.quarantine /Applications/SwarmClaw.app
|
|
88
|
+
```
|
|
86
89
|
- **Windows:** if SmartScreen appears, click **More info** → **Run anyway**.
|
|
87
90
|
- **Linux (AppImage):** `chmod +x` the downloaded file, then run it.
|
|
88
91
|
|
|
@@ -396,6 +399,22 @@ Operational docs: https://swarmclaw.ai/docs/observability
|
|
|
396
399
|
|
|
397
400
|
## Releases
|
|
398
401
|
|
|
402
|
+
### v1.5.51 Highlights
|
|
403
|
+
|
|
404
|
+
- **Desktop app now actually opens and renders on macOS**: packaged builds were broken in v1.5.50 by a stack of independent issues that each masked the next. This release unblocks the cold-boot path end to end. Measured cold-boot time on a populated install: ~1 second to first `/api/healthz` response, down from a hard 60-second timeout.
|
|
405
|
+
- Ad-hoc code signing (`identity: '-'`) via a new `scripts/electron-after-pack.cjs` hook that runs `codesign --sign - --force --deep` after electron-builder packages the bundle. The bundle identifier is now sealed as `ai.swarmclaw.desktop` with all 74k resources sealed, so quarantined dmgs surface as "unidentified developer" (right-click → Open) instead of the more confusing "damaged" error.
|
|
406
|
+
- Per-architecture native module sync: the afterPack hook copies `better-sqlite3`, `@mongodb-js/zstd`, `node-liblzma`, and `utf-8-validate` `.node` binaries from the electron-builder-rebuilt root `node_modules` into the packaged `.next/standalone/node_modules`. Without this, the standalone server hit `ERR_DLOPEN_FAILED: NODE_MODULE_VERSION 137` on launch because Next.js's output-tracing copied the Node-ABI build of better-sqlite3 into standalone while electron-builder only rebuilt the root tree for Electron's ABI.
|
|
407
|
+
- `scripts/run-next-build.mjs` now copies `mdn-data` (used by `css-tree` via `jsdom`) into standalone alongside the existing `css-tree/data` patch, so pages that depend on it don't 500 with `Cannot find module 'mdn-data/css/at-rules.json'`.
|
|
408
|
+
- `isomorphic-dompurify` replaced by the browser-only `dompurify` in `agent-avatar.tsx`. The isomorphic wrapper was pulling `jsdom`'s ESM-only `@exodus/bytes` dep into every server bundle the avatar was referenced from, which blew up SSR under Electron 33 (Node 20.18) with `ERR_REQUIRE_ESM` on every page.
|
|
409
|
+
- Session-consolidation migrations, `initWsServer`, and `ensureDaemonStarted` moved into a `setImmediate` deferred block in `src/instrumentation.ts` so Next.js can bind the HTTP listener before per-install work runs.
|
|
410
|
+
- **App icon fixed**: the Dock no longer shows Electron's default `exec` placeholder. `scripts/gen-icons.mjs` generates `resources/icon.icns`, `resources/icon.ico`, and `resources/icon.png` from `public/branding/swarmclaw-org-avatar.png`; the main process sets the Dock icon at launch and passes it to every `BrowserWindow`.
|
|
411
|
+
- **Embedded server log file + improved failure dialog**: the Electron wrapper now tees the child Next.js server's stdout/stderr into `<userData>/logs/server.log` (`~/Library/Application Support/@swarmclawai/swarmclaw/logs/server.log` on macOS, 1 MB rotation). If startup fails or the server exits, the error dialog shows the tail of the log inline and exposes an **Open Logs Folder** button that jumps Finder straight to the file. This is what made root-cause debugging possible in the first place — if you hit any kind of regression here, grab that log and open an issue.
|
|
412
|
+
- **Embedded server timeout raised from 60s to 5 minutes**: a safety net. On a healthy install the server is up in about a second; 300 seconds is there for pathological cold boots (very large data dirs, contested Apple Silicon Gatekeeper verification on unsigned binaries, etc.) and should never be hit in normal use.
|
|
413
|
+
|
|
414
|
+
### v1.5.50 Highlights
|
|
415
|
+
|
|
416
|
+
- **Fix: opencode-web remote instances no longer fail with `EACCES`**: SwarmClaw used to send the local workspace path (e.g. `/root/.swarmclaw/workspace`) as a `directory=` query parameter on every opencode-web request. Remote opencode-web instances tried to `lstat` that path and rejected the call. The provider now auto-detects local vs. remote from the endpoint hostname (`localhost`, `127.0.0.1`, `::1`, `0.0.0.0`) and only sends `directory=` when the endpoint is local. Thanks to [@SteamedFish](https://github.com/SteamedFish) for the detailed root-cause writeup in [#45](https://github.com/swarmclawai/swarmclaw/issues/45).
|
|
417
|
+
|
|
399
418
|
### v1.5.49 Highlights
|
|
400
419
|
|
|
401
420
|
- **Autonomous Missions**: a new first-class concept for long-running, goal-driven agent work. Hand your agent team a goal on Friday, come back Monday to see what they shipped. Each mission carries a title, a natural-language objective, bulleted success criteria, hard budgets (USD, tokens, turns, wallclock), periodic markdown reports, and a full milestone timeline. Missions drive any session through the existing heartbeat pipeline, so delegation to Claude Code, Codex, OpenCode, Cursor, Droid, Goose, Qwen, or native SwarmClaw agents all work without changes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@swarmclawai/swarmclaw",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.51",
|
|
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
|
"main": "electron-dist/main.js",
|
|
6
6
|
"license": "MIT",
|
|
@@ -101,10 +101,10 @@
|
|
|
101
101
|
"@langchain/langgraph": "^1.2.2",
|
|
102
102
|
"@langchain/openai": "^1.2.8",
|
|
103
103
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
104
|
+
"@multiavatar/multiavatar": "^1.0.7",
|
|
104
105
|
"@opentelemetry/api": "^1.9.1",
|
|
105
106
|
"@opentelemetry/exporter-trace-otlp-http": "^0.214.0",
|
|
106
107
|
"@opentelemetry/sdk-node": "^0.214.0",
|
|
107
|
-
"@multiavatar/multiavatar": "^1.0.7",
|
|
108
108
|
"@playwright/mcp": "^0.0.68",
|
|
109
109
|
"@slack/bolt": "^4.6.0",
|
|
110
110
|
"@swarmdock/sdk": "^0.5.3",
|
|
@@ -127,17 +127,17 @@
|
|
|
127
127
|
"class-variance-authority": "^0.7.1",
|
|
128
128
|
"clsx": "^2.1.1",
|
|
129
129
|
"commander": "^13.1.0",
|
|
130
|
-
"electron-updater": "^6.3.9",
|
|
131
130
|
"cron-parser": "^5.5.0",
|
|
132
131
|
"cronstrue": "^3.12.0",
|
|
133
132
|
"dagre": "^0.8.5",
|
|
134
133
|
"discord.js": "^14.25.1",
|
|
134
|
+
"electron-updater": "^6.3.9",
|
|
135
135
|
"ethers": "^6.16.0",
|
|
136
136
|
"exceljs": "^4.4.0",
|
|
137
137
|
"grammy": "^1.40.0",
|
|
138
138
|
"highlight.js": "^11.11.1",
|
|
139
|
+
"dompurify": "^3.3.3",
|
|
139
140
|
"imapflow": "^1.2.11",
|
|
140
|
-
"isomorphic-dompurify": "^3.7.1",
|
|
141
141
|
"just-bash": "^2.14.0",
|
|
142
142
|
"langchain": "^1.2.30",
|
|
143
143
|
"lucide-react": "^0.574.0",
|
|
@@ -175,7 +175,8 @@
|
|
|
175
175
|
"electron": "^33.3.0",
|
|
176
176
|
"electron-builder": "^25.1.8",
|
|
177
177
|
"eslint": "^9",
|
|
178
|
-
"eslint-config-next": "16.1.7"
|
|
178
|
+
"eslint-config-next": "16.1.7",
|
|
179
|
+
"png-to-ico": "^3.0.1"
|
|
179
180
|
},
|
|
180
181
|
"optionalDependencies": {
|
|
181
182
|
"botbuilder": "^4.23.3",
|
|
@@ -164,14 +164,27 @@ export function repairStandaloneCssTreeData(cwd = process.cwd()) {
|
|
|
164
164
|
const standaloneDir = path.join(cwd, '.next', 'standalone')
|
|
165
165
|
if (!fs.existsSync(standaloneDir)) return false
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
if (fs.existsSync(dataDst)) return false
|
|
167
|
+
let repaired = false
|
|
169
168
|
|
|
170
|
-
const
|
|
171
|
-
|
|
169
|
+
const cssTreeDst = path.join(standaloneDir, 'node_modules', 'css-tree', 'data')
|
|
170
|
+
const cssTreeSrc = path.join(cwd, 'node_modules', 'css-tree', 'data')
|
|
171
|
+
if (!fs.existsSync(cssTreeDst) && fs.existsSync(cssTreeSrc)) {
|
|
172
|
+
fs.cpSync(cssTreeSrc, cssTreeDst, { recursive: true, force: true })
|
|
173
|
+
repaired = true
|
|
174
|
+
}
|
|
172
175
|
|
|
173
|
-
|
|
174
|
-
|
|
176
|
+
// css-tree's CJS entry calls require('mdn-data/css/*.json') at load time,
|
|
177
|
+
// and Next's output-tracing does not pull the raw JSON data files into the
|
|
178
|
+
// standalone tree. Copy them in so jsdom (via css-tree) loads correctly
|
|
179
|
+
// under the packaged app.
|
|
180
|
+
const mdnDataDst = path.join(standaloneDir, 'node_modules', 'mdn-data')
|
|
181
|
+
const mdnDataSrc = path.join(cwd, 'node_modules', 'mdn-data')
|
|
182
|
+
if (!fs.existsSync(mdnDataDst) && fs.existsSync(mdnDataSrc)) {
|
|
183
|
+
fs.cpSync(mdnDataSrc, mdnDataDst, { recursive: true, force: true })
|
|
184
|
+
repaired = true
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return repaired
|
|
175
188
|
}
|
|
176
189
|
|
|
177
190
|
export function repairStandaloneNextMetadata(cwd = process.cwd()) {
|
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import { useMemo } from 'react'
|
|
4
4
|
import multiavatar from '@multiavatar/multiavatar'
|
|
5
|
-
import DOMPurify from '
|
|
5
|
+
import DOMPurify from 'dompurify'
|
|
6
6
|
|
|
7
|
+
// The browser DOMPurify package runs client-side only; this component is a
|
|
8
|
+
// 'use client' boundary so sanitizeSvg only executes after hydration where
|
|
9
|
+
// `window` is available. We previously used isomorphic-dompurify, but that
|
|
10
|
+
// pulls jsdom (and its @exodus/bytes ESM deps) into every server bundle the
|
|
11
|
+
// component is referenced from, which breaks SSR under Electron 33's Node 20.
|
|
7
12
|
function sanitizeSvg(svg: string): string {
|
|
13
|
+
if (typeof window === 'undefined') return svg
|
|
8
14
|
return DOMPurify.sanitize(svg, { USE_PROFILES: { svg: true, svgFilters: true } })
|
|
9
15
|
}
|
|
10
16
|
|
package/src/instrumentation.ts
CHANGED
|
@@ -10,25 +10,29 @@ export async function register() {
|
|
|
10
10
|
const { initWsServer, closeWsServer } = await import('./lib/server/ws-hub')
|
|
11
11
|
const { ensureDaemonStarted } = await import('@/lib/server/runtime/daemon-state')
|
|
12
12
|
await ensureOpenTelemetryStarted()
|
|
13
|
-
|
|
14
|
-
// One-time migration: backfill allKnownPeerIds on existing connector sessions
|
|
15
|
-
try {
|
|
16
|
-
const { backfillAllKnownPeerIds, pruneThreadConnectorMirrors } = await import('@/lib/server/connectors/session-consolidation')
|
|
17
|
-
backfillAllKnownPeerIds()
|
|
18
|
-
pruneThreadConnectorMirrors()
|
|
19
|
-
} catch (err) {
|
|
20
|
-
log.error(TAG, 'connector session consolidation failed:', err)
|
|
21
|
-
}
|
|
22
13
|
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
14
|
+
// Defer migrations, WS init, and daemon startup so the HTTP listener can bind
|
|
15
|
+
// and /api/healthz can respond immediately. Heavy per-install work (session
|
|
16
|
+
// migrations on large data dirs, daemon recovery) no longer gates first boot.
|
|
17
|
+
setImmediate(() => {
|
|
18
|
+
void (async () => {
|
|
19
|
+
try {
|
|
20
|
+
const { backfillAllKnownPeerIds, pruneThreadConnectorMirrors } = await import('@/lib/server/connectors/session-consolidation')
|
|
21
|
+
backfillAllKnownPeerIds()
|
|
22
|
+
pruneThreadConnectorMirrors()
|
|
23
|
+
} catch (err) {
|
|
24
|
+
log.error(TAG, 'connector session consolidation failed:', err)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (isWorkerOnly) {
|
|
28
|
+
log.info(TAG, 'Booting in WORKER ONLY mode')
|
|
29
|
+
ensureDaemonStarted('worker-boot')
|
|
30
|
+
} else {
|
|
31
|
+
initWsServer()
|
|
32
|
+
ensureDaemonStarted('instrumentation')
|
|
33
|
+
}
|
|
34
|
+
})()
|
|
35
|
+
})
|
|
32
36
|
|
|
33
37
|
// Graceful shutdown: stop background services and close WS connections
|
|
34
38
|
const shutdownState = hmrSingleton('__swarmclaw_shutdown_state__', () => ({
|
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
parseModelId,
|
|
7
7
|
joinUrl,
|
|
8
8
|
SseLineParser,
|
|
9
|
+
isLocalEndpoint,
|
|
10
|
+
buildDirectoryQuery,
|
|
9
11
|
} from '@/lib/providers/opencode-web'
|
|
10
12
|
|
|
11
13
|
describe('opencode-web parseBasicAuth', () => {
|
|
@@ -77,6 +79,49 @@ describe('opencode-web joinUrl', () => {
|
|
|
77
79
|
})
|
|
78
80
|
})
|
|
79
81
|
|
|
82
|
+
describe('opencode-web isLocalEndpoint', () => {
|
|
83
|
+
it('returns true for loopback hostnames', () => {
|
|
84
|
+
assert.equal(isLocalEndpoint('http://localhost:4096'), true)
|
|
85
|
+
assert.equal(isLocalEndpoint('http://127.0.0.1:4096'), true)
|
|
86
|
+
assert.equal(isLocalEndpoint('http://[::1]:4096'), true)
|
|
87
|
+
assert.equal(isLocalEndpoint('http://0.0.0.0:4096'), true)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('honours https and no-port variants', () => {
|
|
91
|
+
assert.equal(isLocalEndpoint('https://localhost'), true)
|
|
92
|
+
assert.equal(isLocalEndpoint('https://127.0.0.1/'), true)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('is case-insensitive on hostname', () => {
|
|
96
|
+
assert.equal(isLocalEndpoint('http://LOCALHOST:4096'), true)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('returns false for public hostnames and LAN addresses', () => {
|
|
100
|
+
assert.equal(isLocalEndpoint('http://example.com'), false)
|
|
101
|
+
assert.equal(isLocalEndpoint('https://opencode.example.internal'), false)
|
|
102
|
+
assert.equal(isLocalEndpoint('http://192.168.1.100:4096'), false)
|
|
103
|
+
assert.equal(isLocalEndpoint('http://10.0.0.5:4096'), false)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('fails safe (remote) on malformed input', () => {
|
|
107
|
+
assert.equal(isLocalEndpoint('not-a-url'), false)
|
|
108
|
+
assert.equal(isLocalEndpoint(''), false)
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('opencode-web buildDirectoryQuery', () => {
|
|
113
|
+
it('returns an empty string when cwd is null / undefined / empty', () => {
|
|
114
|
+
assert.equal(buildDirectoryQuery(null), '')
|
|
115
|
+
assert.equal(buildDirectoryQuery(undefined), '')
|
|
116
|
+
assert.equal(buildDirectoryQuery(''), '')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('returns a URL-encoded directory query when cwd is set', () => {
|
|
120
|
+
assert.equal(buildDirectoryQuery('/root/.swarmclaw/workspace'), '?directory=%2Froot%2F.swarmclaw%2Fworkspace')
|
|
121
|
+
assert.equal(buildDirectoryQuery('/tmp/has space'), '?directory=%2Ftmp%2Fhas%20space')
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
80
125
|
describe('opencode-web SseLineParser', () => {
|
|
81
126
|
it('emits one event per data: line and ignores comments / event: / id:', () => {
|
|
82
127
|
const events: unknown[] = []
|
|
@@ -60,6 +60,35 @@ export function joinUrl(baseUrl: string, path: string): string {
|
|
|
60
60
|
return `${base}${suffix}`
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
const LOCAL_HOSTNAMES = new Set(['localhost', '127.0.0.1', '::1', '0.0.0.0'])
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Returns true when the endpoint points at an opencode-web instance on the
|
|
67
|
+
* same machine as swarmclaw. Only local instances share swarmclaw's filesystem,
|
|
68
|
+
* so the `directory=` query parameter (which names a local workspace path) is
|
|
69
|
+
* only meaningful there. Remote instances get an `EACCES` when they try to
|
|
70
|
+
* `lstat` a path that exists only on the swarmclaw host (issue #45).
|
|
71
|
+
*
|
|
72
|
+
* Malformed URLs are treated as remote. That is the safe default: better to
|
|
73
|
+
* omit the directory hint than to leak a local path to an unknown host.
|
|
74
|
+
*/
|
|
75
|
+
export function isLocalEndpoint(endpoint: string): boolean {
|
|
76
|
+
try {
|
|
77
|
+
let hostname = new URL(endpoint).hostname
|
|
78
|
+
if (hostname.startsWith('[') && hostname.endsWith(']')) {
|
|
79
|
+
hostname = hostname.slice(1, -1)
|
|
80
|
+
}
|
|
81
|
+
return LOCAL_HOSTNAMES.has(hostname.toLowerCase())
|
|
82
|
+
} catch {
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function buildDirectoryQuery(cwd: string | null | undefined): string {
|
|
88
|
+
if (!cwd) return ''
|
|
89
|
+
return `?directory=${encodeURIComponent(cwd)}`
|
|
90
|
+
}
|
|
91
|
+
|
|
63
92
|
/**
|
|
64
93
|
* Stateful SSE line parser. Buffers across chunk boundaries and emits
|
|
65
94
|
* one parsed JSON object per `data:` line. Lines that do not start with
|
|
@@ -125,11 +154,11 @@ interface CreateSessionResponse { id?: string; sessionID?: string; sessionId?: s
|
|
|
125
154
|
|
|
126
155
|
async function createSession(opts: {
|
|
127
156
|
endpoint: string
|
|
128
|
-
cwd: string
|
|
157
|
+
cwd: string | null
|
|
129
158
|
authHeader: string | undefined
|
|
130
159
|
signal: AbortSignal
|
|
131
160
|
}): Promise<string> {
|
|
132
|
-
const url = `${joinUrl(opts.endpoint, '/session')}
|
|
161
|
+
const url = `${joinUrl(opts.endpoint, '/session')}${buildDirectoryQuery(opts.cwd)}`
|
|
133
162
|
const res = await fetch(url, {
|
|
134
163
|
method: 'POST',
|
|
135
164
|
headers: {
|
|
@@ -157,14 +186,14 @@ async function createSession(opts: {
|
|
|
157
186
|
async function postPrompt(opts: {
|
|
158
187
|
endpoint: string
|
|
159
188
|
sessionId: string
|
|
160
|
-
cwd: string
|
|
189
|
+
cwd: string | null
|
|
161
190
|
prompt: string
|
|
162
191
|
providerID: string
|
|
163
192
|
modelID: string
|
|
164
193
|
authHeader: string | undefined
|
|
165
194
|
signal: AbortSignal
|
|
166
195
|
}): Promise<{ status: number }> {
|
|
167
|
-
const url = `${joinUrl(opts.endpoint, `/session/${encodeURIComponent(opts.sessionId)}/prompt_async`)}
|
|
196
|
+
const url = `${joinUrl(opts.endpoint, `/session/${encodeURIComponent(opts.sessionId)}/prompt_async`)}${buildDirectoryQuery(opts.cwd)}`
|
|
168
197
|
const res = await fetch(url, {
|
|
169
198
|
method: 'POST',
|
|
170
199
|
headers: {
|
|
@@ -209,7 +238,9 @@ export function streamOpenCodeWebChat(opts: StreamChatOptions): Promise<string>
|
|
|
209
238
|
const { session, message, systemPrompt, apiKey, write, active, signal } = opts
|
|
210
239
|
|
|
211
240
|
const endpoint = (session.apiEndpoint as string | undefined) || DEFAULT_ENDPOINT
|
|
212
|
-
const cwd = (
|
|
241
|
+
const cwd = isLocalEndpoint(endpoint)
|
|
242
|
+
? ((session.cwd as string | undefined) || process.cwd())
|
|
243
|
+
: null
|
|
213
244
|
const auth = parseBasicAuth(apiKey)
|
|
214
245
|
const authHeader = buildAuthHeader(auth)
|
|
215
246
|
const { providerID, modelID } = parseModelId(session.model as string | undefined)
|