@swarmclawai/swarmclaw 0.7.3 → 0.7.4

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.
Files changed (147) hide show
  1. package/README.md +47 -40
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +17 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +3 -1
  12. package/src/app/api/agents/route.ts +23 -1
  13. package/src/app/api/auth/route.ts +1 -1
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +16 -5
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/route.ts +12 -0
  19. package/src/app/api/chats/heartbeat/route.ts +2 -1
  20. package/src/app/api/chats/route.ts +7 -1
  21. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  22. package/src/app/api/external-agents/[id]/route.ts +31 -0
  23. package/src/app/api/external-agents/register/route.ts +3 -0
  24. package/src/app/api/external-agents/route.ts +66 -0
  25. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  26. package/src/app/api/gateways/[id]/route.ts +79 -0
  27. package/src/app/api/gateways/route.ts +57 -0
  28. package/src/app/api/openclaw/gateway/route.ts +10 -7
  29. package/src/app/api/openclaw/skills/route.ts +1 -1
  30. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  31. package/src/app/api/schedules/[id]/route.ts +38 -9
  32. package/src/app/api/schedules/route.ts +51 -28
  33. package/src/app/api/settings/route.ts +6 -10
  34. package/src/app/api/setup/doctor/route.ts +6 -4
  35. package/src/app/api/tasks/[id]/route.ts +2 -1
  36. package/src/app/api/tasks/bulk/route.ts +2 -2
  37. package/src/app/page.tsx +126 -15
  38. package/src/cli/binary.test.js +142 -0
  39. package/src/cli/index.js +34 -11
  40. package/src/cli/index.test.js +195 -0
  41. package/src/cli/index.ts +20 -4
  42. package/src/cli/server-cmd.test.js +59 -0
  43. package/src/cli/spec.js +20 -2
  44. package/src/components/agents/agent-sheet.tsx +249 -7
  45. package/src/components/agents/inspector-panel.tsx +3 -2
  46. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  47. package/src/components/auth/setup-wizard.tsx +970 -275
  48. package/src/components/chat/chat-area.tsx +41 -14
  49. package/src/components/chat/chat-card.tsx +2 -1
  50. package/src/components/chat/chat-header.tsx +8 -13
  51. package/src/components/chat/chat-list.tsx +58 -20
  52. package/src/components/chat/message-list.tsx +142 -18
  53. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  54. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  55. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  56. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  57. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  58. package/src/components/chatrooms/chatroom-view.tsx +157 -86
  59. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  60. package/src/components/gateways/gateway-sheet.tsx +567 -0
  61. package/src/components/input/chat-input.tsx +135 -86
  62. package/src/components/layout/app-layout.tsx +2 -0
  63. package/src/components/memory/memory-browser.tsx +71 -6
  64. package/src/components/memory/memory-card.tsx +18 -0
  65. package/src/components/memory/memory-detail.tsx +58 -31
  66. package/src/components/memory/memory-sheet.tsx +32 -4
  67. package/src/components/projects/project-detail.tsx +7 -2
  68. package/src/components/providers/provider-list.tsx +158 -2
  69. package/src/components/providers/provider-sheet.tsx +81 -70
  70. package/src/components/shared/bottom-sheet.tsx +31 -15
  71. package/src/components/shared/confirm-dialog.tsx +45 -30
  72. package/src/components/shared/model-combobox.tsx +90 -8
  73. package/src/components/shared/settings/section-heartbeat.tsx +11 -6
  74. package/src/components/shared/settings/section-orchestrator.tsx +3 -0
  75. package/src/components/shared/settings/settings-page.tsx +5 -3
  76. package/src/components/tasks/approvals-panel.tsx +7 -1
  77. package/src/components/ui/dialog.tsx +2 -2
  78. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  79. package/src/lib/heartbeat-defaults.ts +48 -0
  80. package/src/lib/memory-presentation.ts +59 -0
  81. package/src/lib/provider-model-discovery-client.ts +29 -0
  82. package/src/lib/providers/index.ts +12 -5
  83. package/src/lib/runtime-loop.ts +105 -3
  84. package/src/lib/safe-storage.ts +6 -1
  85. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  86. package/src/lib/server/agent-runtime-config.ts +277 -0
  87. package/src/lib/server/approvals-auto-approve.test.ts +59 -0
  88. package/src/lib/server/build-llm.test.ts +13 -5
  89. package/src/lib/server/chat-execution-tool-events.test.ts +87 -2
  90. package/src/lib/server/chat-execution.ts +159 -71
  91. package/src/lib/server/chatroom-helpers.test.ts +7 -0
  92. package/src/lib/server/chatroom-helpers.ts +99 -6
  93. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  94. package/src/lib/server/connectors/manager.ts +89 -61
  95. package/src/lib/server/connectors/slack.ts +1 -1
  96. package/src/lib/server/daemon-state.ts +3 -2
  97. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  98. package/src/lib/server/eval/agent-regression.ts +1742 -0
  99. package/src/lib/server/eval/runner.ts +11 -1
  100. package/src/lib/server/eval/store.ts +2 -1
  101. package/src/lib/server/heartbeat-service.ts +10 -4
  102. package/src/lib/server/main-agent-loop.ts +13 -6
  103. package/src/lib/server/openclaw-exec-config.ts +4 -2
  104. package/src/lib/server/openclaw-gateway.ts +123 -36
  105. package/src/lib/server/orchestrator-lg.ts +1 -2
  106. package/src/lib/server/orchestrator.ts +3 -2
  107. package/src/lib/server/plugins.test.ts +9 -1
  108. package/src/lib/server/plugins.ts +12 -2
  109. package/src/lib/server/provider-model-discovery.ts +481 -0
  110. package/src/lib/server/queue.ts +1 -1
  111. package/src/lib/server/runtime-settings.test.ts +119 -0
  112. package/src/lib/server/runtime-settings.ts +12 -92
  113. package/src/lib/server/schedule-normalization.ts +187 -0
  114. package/src/lib/server/session-tools/autonomy-tools.test.ts +23 -0
  115. package/src/lib/server/session-tools/crud.ts +27 -3
  116. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  117. package/src/lib/server/session-tools/discovery.ts +18 -8
  118. package/src/lib/server/session-tools/file-normalize.test.ts +5 -0
  119. package/src/lib/server/session-tools/file.ts +8 -2
  120. package/src/lib/server/session-tools/http.ts +9 -3
  121. package/src/lib/server/session-tools/index.ts +31 -1
  122. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  123. package/src/lib/server/session-tools/monitor.ts +14 -7
  124. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  125. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  126. package/src/lib/server/session-tools/platform.ts +1 -1
  127. package/src/lib/server/session-tools/plugin-creator.ts +9 -2
  128. package/src/lib/server/session-tools/sandbox.ts +51 -92
  129. package/src/lib/server/session-tools/session-info.ts +22 -1
  130. package/src/lib/server/session-tools/session-tools-wiring.test.ts +23 -0
  131. package/src/lib/server/session-tools/shell.ts +2 -2
  132. package/src/lib/server/session-tools/subagent.ts +3 -1
  133. package/src/lib/server/session-tools/web.ts +73 -30
  134. package/src/lib/server/storage.ts +29 -3
  135. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  136. package/src/lib/server/stream-agent-chat.ts +139 -4
  137. package/src/lib/server/structured-extract.ts +1 -1
  138. package/src/lib/server/task-mention.ts +0 -1
  139. package/src/lib/server/tool-aliases.ts +37 -6
  140. package/src/lib/server/tool-capability-policy.ts +1 -1
  141. package/src/lib/setup-defaults.ts +352 -11
  142. package/src/lib/tool-definitions.ts +3 -4
  143. package/src/lib/validation/schemas.ts +55 -1
  144. package/src/stores/use-app-store.ts +43 -1
  145. package/src/stores/use-chatroom-store.ts +153 -26
  146. package/src/types/index.ts +189 -6
  147. package/src/app/api/chats/[id]/main-loop/route.ts +0 -13
package/README.md CHANGED
@@ -37,6 +37,8 @@ SwarmClaw was built for OpenClaw users who outgrew a single agent. Connect each
37
37
 
38
38
  SwarmClaw includes the `openclaw` CLI as a bundled dependency, so there is no separate OpenClaw CLI install step.
39
39
 
40
+ The Providers screen now supports named OpenClaw gateway profiles with discovery, health checks, default-gateway selection, and an External Agent Runtimes view for remote workers that register/heartbeat into SwarmClaw.
41
+
40
42
  The OpenClaw Control Plane in SwarmClaw adds:
41
43
  - Reload mode switching (`hot`, `hybrid`, `full`)
42
44
  - Config issue detection and guided repair
@@ -47,13 +49,13 @@ The Agent Inspector Panel lets you edit OpenClaw files (`SOUL.md`, `IDENTITY.md`
47
49
 
48
50
  To connect an agent to an OpenClaw gateway:
49
51
 
50
- 1. Create or edit an agent
51
- 2. Toggle **OpenClaw Gateway** ON
52
- 3. Enter the gateway URL (e.g. `http://192.168.1.50:18789` or `https://my-vps:18789`)
53
- 4. Add a gateway token if authentication is enabled on the remote gateway
52
+ 1. Optional: create a named gateway profile in **Providers** and mark a default
53
+ 2. Create or edit an agent
54
+ 3. Toggle **OpenClaw Gateway** ON
55
+ 4. Select a saved gateway profile or enter a direct gateway URL/token override
54
56
  5. Click **Connect** — approve the device in your gateway's dashboard if prompted, then **Retry Connection**
55
57
 
56
- Each agent can point to a **different** OpenClaw gateway — one local, several remote. This is how you manage a **swarm of OpenClaws** from a single dashboard.
58
+ Each agent can point to a **different** OpenClaw gateway profile or direct endpoint — one local, several remote. This is how you manage a **swarm of OpenClaws** from a single dashboard.
57
59
 
58
60
  URLs without a protocol are auto-prefixed with `http://`. For remote gateways with TLS, use `https://` explicitly.
59
61
 
@@ -77,21 +79,37 @@ Skill source and runbook: [`swarmclaw/SKILL.md`](swarmclaw/SKILL.md).
77
79
  ## Requirements
78
80
 
79
81
  - **Node.js** 22.6+
80
- - **npm** 10+
82
+ - **Node.js** 22.6+
83
+ - One of: **npm** 10+, **pnpm**, **Yarn**, or **Bun**
81
84
  - **Claude Code CLI** (optional, for `claude-cli` provider) — [Install](https://docs.anthropic.com/en/docs/claude-code/overview)
82
85
  - **OpenAI Codex CLI** (optional, for `codex-cli` provider) — [Install](https://github.com/openai/codex)
83
86
  - **OpenCode CLI** (optional, for `opencode-cli` provider) — [Install](https://github.com/opencode-ai/opencode)
84
87
  - **Gemini CLI** (optional, for `delegate` backend `gemini`) — install and authenticate `gemini` on your host
88
+ - **Deno** (required for `sandbox_exec`) — auto-installed by `npm run quickstart` / `npm run setup:easy` when missing
85
89
 
86
90
  ## Quick Start
87
91
 
88
- ### npm (recommended)
92
+ SwarmClaw is published to the npm registry once and can be installed with `npm`, `pnpm`, `yarn`, or `bun`. There is no separate package-manager signup for end users.
93
+
94
+ ### Global install
89
95
 
90
96
  ```bash
91
97
  npm i -g @swarmclawai/swarmclaw
98
+ pnpm add -g @swarmclawai/swarmclaw
99
+ yarn global add @swarmclawai/swarmclaw
100
+ bun add -g @swarmclawai/swarmclaw
92
101
  swarmclaw
93
102
  ```
94
103
 
104
+ ### One-off run
105
+
106
+ ```bash
107
+ npx @swarmclawai/swarmclaw
108
+ pnpm dlx @swarmclawai/swarmclaw
109
+ yarn dlx @swarmclawai/swarmclaw
110
+ bunx @swarmclawai/swarmclaw
111
+ ```
112
+
95
113
  ### Install script
96
114
 
97
115
  ```bash
@@ -99,7 +117,7 @@ curl -fsSL https://raw.githubusercontent.com/swarmclawai/swarmclaw/main/install.
99
117
  ```
100
118
 
101
119
  The installer resolves the latest stable release tag and installs that version by default.
102
- To pin a version: `SWARMCLAW_VERSION=v0.7.3 curl ... | bash`
120
+ To pin a version: `SWARMCLAW_VERSION=v0.7.4 curl ... | bash`
103
121
 
104
122
  Or run locally from the repo (friendly for non-technical users):
105
123
 
@@ -111,10 +129,19 @@ npm run quickstart
111
129
 
112
130
  `npm run quickstart` will:
113
131
  - Check Node/npm versions
132
+ - Install Deno if the sandbox runtime is missing
114
133
  - Install dependencies
115
134
  - Prepare `.env.local` and `data/`
116
135
  - Start the app at `http://localhost:3456`
117
136
 
137
+ If you prefer another package manager for local development:
138
+
139
+ ```bash
140
+ pnpm install && pnpm dev
141
+ yarn install && yarn dev
142
+ bun install && bun run dev
143
+ ```
144
+
118
145
  `postinstall` rebuilds `better-sqlite3` natively. If you install with `--ignore-scripts`, run `npm rebuild better-sqlite3` manually.
119
146
 
120
147
  On first launch, SwarmClaw will:
@@ -143,7 +170,7 @@ Notes:
143
170
  - When run with no flags in a TTY, `setup init` enters interactive mode — pick providers, enter keys, name agents, and add multiple providers in one session.
144
171
  - Use `--no-interactive` to force flag-only mode.
145
172
  - On a fresh instance, `setup init` can auto-discover and claim the first-run access key from `/api/auth`.
146
- - For existing installs, pass `--key <ACCESS_KEY>` (or set `SWARMCLAW_ACCESS_KEY`).
173
+ - For existing installs, pass `--key <ACCESS_KEY>` or set `SWARMCLAW_ACCESS_KEY` / `SWARMCLAW_API_KEY`.
147
174
  - `setup init` performs provider validation, stores credentials, creates a starter agent, and marks setup complete.
148
175
  - Use `--skip-check` to bypass connection validation.
149
176
 
@@ -164,7 +191,7 @@ Notes:
164
191
  ## Features
165
192
 
166
193
  - **15 providers out of the box** - CLI providers + major hosted APIs + OpenAI-compatible custom endpoints
167
- - **OpenClaw-native control plane** - per-agent gateway mapping, reload modes, sync, and approval flows
194
+ - **OpenClaw-native control plane** - named gateway profiles, external runtimes, reload modes, sync, and approval flows
168
195
  - **Agent builder + inspector** - personality/system tuning, skill management, and OpenClaw file editing
169
196
  - **Rich toolset** - shell, files, browser, git, sandbox execution, memory, MCP, and delegation
170
197
  - **Platform automation** - agents can manage tasks, schedules, chats, connectors, secrets, and more
@@ -289,6 +316,7 @@ Connector ingress now also supports optional pairing/allowlist policy:
289
316
  - `dmPolicy: allowlist` blocks unknown senders until approved
290
317
  - `/pair` flow lets approved admins generate and approve pairing codes
291
318
  - `/think` command can set connector thread thinking level (`low`, `medium`, `high`)
319
+ - Session overrides also support per-thread `/reply`, `/scope`, `/thread`, `/provider`, `/model`, `/idle`, `/maxage`, and `/reset` controls
292
320
 
293
321
  ## Agent Tools
294
322
 
@@ -315,7 +343,7 @@ Agents can use the following tools when enabled:
315
343
  | Image Generation | Generate images from prompts (`generate_image`) via OpenAI, Stability, Replicate, fal.ai, Together, Fireworks, BFL, or custom endpoints; saved to uploads |
316
344
  | Email | Send outbound email via SMTP (`email`) with `send`/`status` actions |
317
345
  | Calendar | Manage Google/Outlook events (`calendar`) with list/create/update/delete/status actions |
318
- | Sandbox | Run JS/TS (Deno) or Python code in an isolated sandbox. Created files are returned as downloadable artifacts |
346
+ | Sandbox | Run JS/TS in a Deno sandbox when custom code is necessary. If Deno is unavailable it fails closed with guidance; for simple API calls, prefer HTTP Request. |
319
347
  | MCP Servers | Connect to external Model Context Protocol servers. Tools from MCP servers are injected as first-class agent tools |
320
348
 
321
349
  ### Platform Tools
@@ -370,21 +398,6 @@ Daemon runtime also triggers memory consolidation (daily summary generation plus
370
398
  - **API:** `GET /api/daemon` (status), `POST /api/daemon` with `{"action": "start"}` or `{"action": "stop"}`
371
399
  - Auto-starts on first authenticated runtime traffic (`/api/auth` or `/api/daemon`) unless `SWARMCLAW_DAEMON_AUTOSTART=0`
372
400
 
373
- ## Main Agent Loop
374
-
375
- For autonomous long-running missions, enable the **Main Loop** on an agent-thread or orchestrated chat. This lets an agent pursue a goal continuously with heartbeat-driven progress checks and automatic followups.
376
-
377
- - **Heartbeat prompts:** `SWARM_MAIN_MISSION_TICK` triggers on each heartbeat, giving the agent its goal, status, and pending events
378
- - **Auto-followup:** When an agent returns `[MAIN_LOOP_META] {"follow_up":true}`, the loop schedules another tick after `delay_sec`
379
- - **Mission state:** Tracks `goal`, `status` (idle/progress/blocked/ok), `summary`, `nextAction`, `autonomyMode` (assist/autonomous), and pending events
380
- - **Autonomy modes:**
381
- - `autonomous`: Agent executes safe actions without confirmation, only asks when blocked by permissions/credentials
382
- - `assist`: Agent asks before irreversible external actions (sending messages, purchases, account mutations)
383
- - **API:** `POST /api/chats/[id]/main-loop` with `{"tick":true}` to trigger a mission tick
384
- - **CLI:** `swarmclaw chats main-loop <id>` to inspect loop state, or `swarmclaw chats main-loop-action <id> --data '{"action":"nudge"}'` to control it
385
-
386
- Use this for background agents that should "keep working" on a goal until blocked or complete.
387
-
388
401
  ## Loop Modes
389
402
 
390
403
  Configure loop behavior in **Settings → Runtime & Loop Controls**:
@@ -657,7 +670,7 @@ npm run update:easy # safe update helper for local installs
657
670
  SwarmClaw uses tag-based releases (`vX.Y.Z`) as the stable channel.
658
671
 
659
672
  ```bash
660
- # example patch release (v0.7.3 style)
673
+ # example patch release (v0.7.4 style)
661
674
  npm version patch
662
675
  git push origin main --follow-tags
663
676
  ```
@@ -667,18 +680,15 @@ On `v*` tags, GitHub Actions will:
667
680
  2. Create a GitHub Release
668
681
  3. Build and publish Docker images to `ghcr.io/swarmclawai/swarmclaw` (`:vX.Y.Z`, `:latest`, `:sha-*`)
669
682
 
670
- #### v0.7.3 Release Readiness Notes
683
+ #### v0.7.4 Release Readiness Notes
671
684
 
672
- Before shipping `v0.7.3`, confirm the following user-facing changes are reflected in docs:
685
+ Before shipping `v0.7.4`, confirm the following user-facing changes are reflected in docs:
673
686
 
674
- 1. New primitive plugins are documented in README and site docs: `mailbox`, `ask_human`, `document`, `extract`, `table`, and `crawl`.
675
- 2. Browser persistence, higher-level browser actions, durable watch jobs, and delegation handles are covered in docs.
676
- 3. Plugin docs mention canonical built-in IDs, source-backed installs/updates, hot reload, and encrypted `secret` settings.
677
- 4. Plugin docs cover per-plugin workspaces and dependency installs for plugins that ship with a `package.json` manifest.
678
- 5. Connector docs and CLI docs cover the connector doctor diagnostics endpoints/commands.
679
- 6. The plugin tutorial reflects the current hook behavior, including `beforeToolExec` input rewriting and `settingsFields`.
680
- 7. Site install/version strings are updated to `v0.7.3`, including the release notes index, install snippets, and sidebar footer.
681
- 8. Provider docs mention that configured OpenAI models can populate the model dropdown while still allowing custom/manual entries.
687
+ 1. Sandbox docs are updated everywhere to reflect the current Deno-only `sandbox_exec` behavior and the guidance to prefer `http_request` for simple API calls.
688
+ 2. OpenClaw docs cover the current gateway/runtime behavior, including per-agent gateway routing, control-plane actions, and inspector-side advanced controls.
689
+ 3. Site and README install/version strings are updated to `v0.7.4`, including install snippets, release notes index text, and sidebar/footer labels.
690
+ 4. Release notes summarize the user-visible setup/auth/runtime changes from the current worktree, especially gateway/external-agent/setup flow improvements.
691
+ 5. CLI and tool docs do not reference removed or non-functional surfaces such as the old `openclaw_sandbox` bridge.
682
692
 
683
693
  ## CLI
684
694
 
@@ -713,9 +723,6 @@ swarmclaw agents list
713
723
  swarmclaw chats create --name "Main Ops" --agent-id <agentId>
714
724
  swarmclaw chats list
715
725
 
716
- # run a main loop action
717
- swarmclaw chats main-loop-action <chatId> --data '{"action":"nudge"}'
718
-
719
726
  # run setup diagnostics
720
727
  swarmclaw setup doctor
721
728
  ```
@@ -0,0 +1,157 @@
1
+ 'use strict'
2
+ /* eslint-disable @typescript-eslint/no-require-imports */
3
+
4
+ const fs = require('node:fs')
5
+ const path = require('node:path')
6
+
7
+ const LOCKFILE_NAMES = [
8
+ 'package-lock.json',
9
+ 'pnpm-lock.yaml',
10
+ 'yarn.lock',
11
+ 'bun.lock',
12
+ 'bun.lockb',
13
+ ]
14
+ const INSTALL_METADATA_FILE = '.swarmclaw-install.json'
15
+
16
+ function normalizePackageManager(raw) {
17
+ switch (String(raw || '').trim().toLowerCase()) {
18
+ case 'pnpm':
19
+ case 'yarn':
20
+ case 'bun':
21
+ case 'npm':
22
+ return String(raw).trim().toLowerCase()
23
+ default:
24
+ return null
25
+ }
26
+ }
27
+
28
+ function detectPackageManagerFromUserAgent(userAgent) {
29
+ const normalized = String(userAgent || '').toLowerCase()
30
+ if (normalized.startsWith('pnpm/')) return 'pnpm'
31
+ if (normalized.startsWith('yarn/')) return 'yarn'
32
+ if (normalized.startsWith('bun/')) return 'bun'
33
+ if (normalized.startsWith('npm/')) return 'npm'
34
+ return null
35
+ }
36
+
37
+ function readInstallMetadata(rootDir) {
38
+ const metadataPath = path.join(rootDir, INSTALL_METADATA_FILE)
39
+ if (!fs.existsSync(metadataPath)) return null
40
+ try {
41
+ const raw = JSON.parse(fs.readFileSync(metadataPath, 'utf8'))
42
+ return raw && typeof raw === 'object' ? raw : null
43
+ } catch {
44
+ return null
45
+ }
46
+ }
47
+
48
+ function detectPackageManager(rootDir, env = process.env) {
49
+ const envOverride = normalizePackageManager(env.SWARMCLAW_PACKAGE_MANAGER)
50
+ if (envOverride) return envOverride
51
+
52
+ const installMetadata = readInstallMetadata(rootDir)
53
+ const installManager = normalizePackageManager(installMetadata?.packageManager)
54
+ if (installManager) return installManager
55
+
56
+ if (fs.existsSync(path.join(rootDir, 'bun.lock')) || fs.existsSync(path.join(rootDir, 'bun.lockb'))) return 'bun'
57
+ if (fs.existsSync(path.join(rootDir, 'pnpm-lock.yaml'))) return 'pnpm'
58
+ if (fs.existsSync(path.join(rootDir, 'yarn.lock'))) return 'yarn'
59
+ if (fs.existsSync(path.join(rootDir, 'package-lock.json'))) return 'npm'
60
+
61
+ const userAgentManager = detectPackageManagerFromUserAgent(env.npm_config_user_agent)
62
+ if (userAgentManager) return userAgentManager
63
+ return 'npm'
64
+ }
65
+
66
+ function getInstallCommand(packageManager, omitDev = false) {
67
+ switch (packageManager) {
68
+ case 'pnpm':
69
+ return omitDev
70
+ ? { command: 'pnpm', args: ['install', '--prod'] }
71
+ : { command: 'pnpm', args: ['install'] }
72
+ case 'yarn':
73
+ return omitDev
74
+ ? { command: 'yarn', args: ['install', '--production=true'] }
75
+ : { command: 'yarn', args: ['install'] }
76
+ case 'bun':
77
+ return omitDev
78
+ ? { command: 'bun', args: ['install', '--production'] }
79
+ : { command: 'bun', args: ['install'] }
80
+ case 'npm':
81
+ default:
82
+ return omitDev
83
+ ? { command: 'npm', args: ['install', '--omit=dev'] }
84
+ : { command: 'npm', args: ['install'] }
85
+ }
86
+ }
87
+
88
+ function getGlobalUpdateCommand(packageManager, packageName) {
89
+ return getGlobalUpdateSpec(packageManager, packageName).display
90
+ }
91
+
92
+ function getGlobalUpdateSpec(packageManager, packageName) {
93
+ switch (packageManager) {
94
+ case 'pnpm':
95
+ return {
96
+ command: 'pnpm',
97
+ args: ['add', '-g', `${packageName}@latest`],
98
+ display: `pnpm add -g ${packageName}@latest`,
99
+ }
100
+ case 'yarn':
101
+ return {
102
+ command: 'yarn',
103
+ args: ['global', 'add', `${packageName}@latest`],
104
+ display: `yarn global add ${packageName}@latest`,
105
+ }
106
+ case 'bun':
107
+ return {
108
+ command: 'bun',
109
+ args: ['add', '-g', `${packageName}@latest`],
110
+ display: `bun add -g ${packageName}@latest`,
111
+ }
112
+ case 'npm':
113
+ default:
114
+ return {
115
+ command: 'npm',
116
+ args: ['update', '-g', packageName],
117
+ display: `npm update -g ${packageName}`,
118
+ }
119
+ }
120
+ }
121
+
122
+ function getRunScriptCommand(packageManager, scriptName) {
123
+ switch (packageManager) {
124
+ case 'pnpm':
125
+ return { command: 'pnpm', args: [scriptName] }
126
+ case 'yarn':
127
+ return { command: 'yarn', args: [scriptName] }
128
+ case 'bun':
129
+ return { command: 'bun', args: ['run', scriptName] }
130
+ case 'npm':
131
+ default:
132
+ return { command: 'npm', args: ['run', scriptName] }
133
+ }
134
+ }
135
+
136
+ function dependenciesChanged(diffText) {
137
+ if (!diffText) return false
138
+ return String(diffText)
139
+ .split('\n')
140
+ .map((line) => line.trim())
141
+ .filter(Boolean)
142
+ .some((file) => file === 'package.json' || LOCKFILE_NAMES.includes(file))
143
+ }
144
+
145
+ module.exports = {
146
+ dependenciesChanged,
147
+ detectPackageManager,
148
+ detectPackageManagerFromUserAgent,
149
+ getGlobalUpdateCommand,
150
+ getGlobalUpdateSpec,
151
+ getInstallCommand,
152
+ getRunScriptCommand,
153
+ INSTALL_METADATA_FILE,
154
+ LOCKFILE_NAMES,
155
+ normalizePackageManager,
156
+ readInstallMetadata,
157
+ }
@@ -0,0 +1,90 @@
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
+
10
+ const {
11
+ INSTALL_METADATA_FILE,
12
+ LOCKFILE_NAMES,
13
+ dependenciesChanged,
14
+ detectPackageManager,
15
+ detectPackageManagerFromUserAgent,
16
+ getGlobalUpdateSpec,
17
+ getInstallCommand,
18
+ getRunScriptCommand,
19
+ } = require('./package-manager.js')
20
+
21
+ test('detectPackageManagerFromUserAgent parses supported package managers', () => {
22
+ assert.equal(detectPackageManagerFromUserAgent('pnpm/10.6.1 npm/? node/v22.6.0 darwin arm64'), 'pnpm')
23
+ assert.equal(detectPackageManagerFromUserAgent('yarn/4.7.0 npm/? node/v22.6.0 darwin arm64'), 'yarn')
24
+ assert.equal(detectPackageManagerFromUserAgent('bun/1.2.10 npm/? node/v22.6.0 darwin arm64'), 'bun')
25
+ assert.equal(detectPackageManagerFromUserAgent('npm/10.9.2 node/v22.6.0 darwin arm64'), 'npm')
26
+ })
27
+
28
+ test('detectPackageManager prefers the lockfile present in the workspace', () => {
29
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-pm-'))
30
+
31
+ fs.writeFileSync(path.join(tmpDir, 'pnpm-lock.yaml'), 'lock', 'utf8')
32
+ assert.equal(detectPackageManager(tmpDir), 'pnpm')
33
+
34
+ fs.writeFileSync(path.join(tmpDir, 'bun.lockb'), 'lock', 'utf8')
35
+ assert.equal(detectPackageManager(tmpDir), 'bun')
36
+
37
+ fs.rmSync(tmpDir, { recursive: true, force: true })
38
+ })
39
+
40
+ test('detectPackageManager falls back to npm when no lockfile exists', () => {
41
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-pm-empty-'))
42
+ assert.equal(detectPackageManager(tmpDir), 'npm')
43
+ fs.rmSync(tmpDir, { recursive: true, force: true })
44
+ })
45
+
46
+ test('detectPackageManager uses install metadata when present', () => {
47
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'swarmclaw-pm-meta-'))
48
+ fs.writeFileSync(
49
+ path.join(tmpDir, INSTALL_METADATA_FILE),
50
+ JSON.stringify({ packageManager: 'yarn' }),
51
+ 'utf8',
52
+ )
53
+ assert.equal(detectPackageManager(tmpDir), 'yarn')
54
+ fs.rmSync(tmpDir, { recursive: true, force: true })
55
+ })
56
+
57
+ test('dependenciesChanged recognizes package.json and all supported lockfiles', () => {
58
+ assert.equal(dependenciesChanged('package.json\nsrc/app.ts'), true)
59
+ for (const lockfile of LOCKFILE_NAMES) {
60
+ assert.equal(dependenciesChanged(`${lockfile}\nREADME.md`), true)
61
+ }
62
+ assert.equal(dependenciesChanged('README.md\nsrc/index.ts'), false)
63
+ })
64
+
65
+ test('getInstallCommand returns manager-specific install arguments', () => {
66
+ assert.deepEqual(getInstallCommand('npm', true), { command: 'npm', args: ['install', '--omit=dev'] })
67
+ assert.deepEqual(getInstallCommand('pnpm', false), { command: 'pnpm', args: ['install'] })
68
+ assert.deepEqual(getInstallCommand('yarn', true), { command: 'yarn', args: ['install', '--production=true'] })
69
+ assert.deepEqual(getInstallCommand('bun', true), { command: 'bun', args: ['install', '--production'] })
70
+ })
71
+
72
+ test('getRunScriptCommand returns manager-specific script launchers', () => {
73
+ assert.deepEqual(getRunScriptCommand('npm', 'build'), { command: 'npm', args: ['run', 'build'] })
74
+ assert.deepEqual(getRunScriptCommand('pnpm', 'start'), { command: 'pnpm', args: ['start'] })
75
+ assert.deepEqual(getRunScriptCommand('yarn', 'dev'), { command: 'yarn', args: ['dev'] })
76
+ assert.deepEqual(getRunScriptCommand('bun', 'start'), { command: 'bun', args: ['run', 'start'] })
77
+ })
78
+
79
+ test('getGlobalUpdateSpec returns manager-specific update commands', () => {
80
+ assert.deepEqual(getGlobalUpdateSpec('npm', '@swarmclawai/swarmclaw'), {
81
+ command: 'npm',
82
+ args: ['update', '-g', '@swarmclawai/swarmclaw'],
83
+ display: 'npm update -g @swarmclawai/swarmclaw',
84
+ })
85
+ assert.deepEqual(getGlobalUpdateSpec('pnpm', '@swarmclawai/swarmclaw'), {
86
+ command: 'pnpm',
87
+ args: ['add', '-g', '@swarmclawai/swarmclaw@latest'],
88
+ display: 'pnpm add -g @swarmclawai/swarmclaw@latest',
89
+ })
90
+ })
package/bin/server-cmd.js CHANGED
@@ -1,10 +1,16 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict'
3
+ /* eslint-disable @typescript-eslint/no-require-imports */
3
4
 
4
5
  const fs = require('node:fs')
5
6
  const path = require('node:path')
6
- const { spawn, execSync } = require('node:child_process')
7
+ const { spawn, execFileSync } = require('node:child_process')
7
8
  const os = require('node:os')
9
+ const {
10
+ LOCKFILE_NAMES,
11
+ detectPackageManager,
12
+ getInstallCommand,
13
+ } = require('./package-manager.js')
8
14
 
9
15
  // ---------------------------------------------------------------------------
10
16
  // Paths
@@ -26,7 +32,7 @@ const BUILD_COPY_ENTRIES = [
26
32
  'tsconfig.json',
27
33
  'postcss.config.mjs',
28
34
  'package.json',
29
- 'package-lock.json',
35
+ ...LOCKFILE_NAMES,
30
36
  ]
31
37
 
32
38
  // ---------------------------------------------------------------------------
@@ -73,13 +79,26 @@ function symlinkPath(src, dest) {
73
79
  fs.symlinkSync(src, dest)
74
80
  }
75
81
 
82
+ function readBuiltInfo() {
83
+ if (!fs.existsSync(BUILT_MARKER)) return null
84
+ try {
85
+ const raw = JSON.parse(fs.readFileSync(BUILT_MARKER, 'utf8'))
86
+ return raw && typeof raw === 'object' ? raw : null
87
+ } catch {
88
+ return null
89
+ }
90
+ }
91
+
76
92
  // ---------------------------------------------------------------------------
77
93
  // Build
78
94
  // ---------------------------------------------------------------------------
79
95
 
80
96
  function needsBuild(forceBuild) {
81
97
  if (forceBuild) return true
82
- if (!fs.existsSync(BUILT_MARKER)) return true
98
+ const info = readBuiltInfo()
99
+ if (!info) return true
100
+ if (info.version !== getVersion()) return true
101
+ if (!findStandaloneServer()) return true
83
102
  return false
84
103
  }
85
104
 
@@ -110,15 +129,18 @@ function runBuild() {
110
129
  symlinkPath(nmSrc, nmDest)
111
130
  } else {
112
131
  // If node_modules doesn't exist at PKG_ROOT, install
113
- log('Installing dependencies...')
114
- execSync('npm install', { cwd: SWARMCLAW_HOME, stdio: 'inherit' })
132
+ const packageManager = detectPackageManager(SWARMCLAW_HOME, process.env)
133
+ const install = getInstallCommand(packageManager)
134
+ log(`Installing dependencies with ${packageManager}...`)
135
+ execFileSync(install.command, install.args, { cwd: SWARMCLAW_HOME, stdio: 'inherit' })
115
136
  }
116
137
 
117
138
  // Run Next.js build
118
139
  log('Building Next.js application (this may take a minute)...')
119
140
  // Use webpack for production build reliability in packaged/fresh-install
120
141
  // environments (Turbopack has intermittently failed during prerender).
121
- execSync('npx next build --webpack', {
142
+ const nextCli = path.join(SWARMCLAW_HOME, 'node_modules', 'next', 'dist', 'bin', 'next')
143
+ execFileSync(process.execPath, [nextCli, 'build', '--webpack'], {
122
144
  cwd: SWARMCLAW_HOME,
123
145
  stdio: 'inherit',
124
146
  env: {
@@ -373,4 +395,13 @@ function main() {
373
395
  startServer({ port, wsPort, host, detach })
374
396
  }
375
397
 
376
- main()
398
+ if (require.main === module) {
399
+ main()
400
+ }
401
+
402
+ module.exports = {
403
+ getVersion,
404
+ main,
405
+ needsBuild,
406
+ readBuiltInfo,
407
+ }
package/bin/swarmclaw.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict'
3
+ /* eslint-disable @typescript-eslint/no-require-imports */
3
4
 
4
5
  const path = require('node:path')
5
6
  const { spawnSync } = require('node:child_process')
@@ -34,12 +35,45 @@ function shouldUseLegacyTsCli(argv) {
34
35
  return actions.has(action)
35
36
  }
36
37
 
38
+ function supportsStripTypes() {
39
+ return process.allowedNodeEnvironmentFlags.has('--experimental-strip-types')
40
+ }
41
+
42
+ function hasTsxRuntime() {
43
+ try {
44
+ require.resolve('tsx/package.json')
45
+ return true
46
+ } catch {
47
+ return false
48
+ }
49
+ }
50
+
51
+ function buildLegacyTsCliArgs(cliPath, argv, options = {}) {
52
+ const stripTypesSupported = options.supportsStripTypes ?? supportsStripTypes()
53
+ if (stripTypesSupported) {
54
+ return ['--no-warnings', '--experimental-strip-types', cliPath, ...argv]
55
+ }
56
+
57
+ const tsxAvailable = options.hasTsxRuntime ?? hasTsxRuntime()
58
+ if (tsxAvailable) {
59
+ return ['--no-warnings', '--import', 'tsx', cliPath, ...argv]
60
+ }
61
+
62
+ return null
63
+ }
64
+
37
65
  function runLegacyTsCli(argv) {
38
66
  const cliPath = path.join(__dirname, '..', 'src', 'cli', 'index.ts')
67
+ const args = buildLegacyTsCliArgs(cliPath, argv)
68
+ const env = normalizeLegacyCliEnv(process.env)
69
+ if (!args) {
70
+ process.stderr.write('Legacy CLI commands require Node 22.6+ or an available local tsx runtime.\n')
71
+ return 1
72
+ }
39
73
  const child = spawnSync(
40
74
  process.execPath,
41
- ['--no-warnings', '--experimental-strip-types', cliPath, ...argv],
42
- { stdio: 'inherit' },
75
+ args,
76
+ { stdio: 'inherit', env },
43
77
  )
44
78
 
45
79
  if (child.error) {
@@ -50,6 +84,18 @@ function runLegacyTsCli(argv) {
50
84
  return 1
51
85
  }
52
86
 
87
+ function normalizeLegacyCliEnv(env) {
88
+ const nextEnv = { ...env }
89
+ if (!nextEnv.SWARMCLAW_URL && nextEnv.SWARMCLAW_BASE_URL) {
90
+ nextEnv.SWARMCLAW_URL = nextEnv.SWARMCLAW_BASE_URL
91
+ }
92
+ if (!nextEnv.SWARMCLAW_ACCESS_KEY) {
93
+ const key = nextEnv.SWARMCLAW_API_KEY || nextEnv.SC_ACCESS_KEY || ''
94
+ if (key) nextEnv.SWARMCLAW_ACCESS_KEY = key
95
+ }
96
+ return nextEnv
97
+ }
98
+
53
99
  async function runMappedCli(argv) {
54
100
  const cliPath = path.join(__dirname, '..', 'src', 'cli', 'index.js')
55
101
  const cliModule = await import(cliPath)
@@ -66,11 +112,11 @@ async function main() {
66
112
 
67
113
  // Route 'server' and 'update' subcommands to CJS scripts (no TS dependency).
68
114
  if (top === 'server') {
69
- require('./server-cmd.js')
115
+ require('./server-cmd.js').main()
70
116
  return
71
117
  }
72
118
  if (top === 'update') {
73
- require('./update-cmd.js')
119
+ require('./update-cmd.js').main()
74
120
  return
75
121
  }
76
122
 
@@ -86,6 +132,10 @@ if (require.main === module) {
86
132
  }
87
133
 
88
134
  module.exports = {
135
+ buildLegacyTsCliArgs,
136
+ hasTsxRuntime,
89
137
  TS_CLI_ACTIONS,
138
+ normalizeLegacyCliEnv,
139
+ supportsStripTypes,
90
140
  shouldUseLegacyTsCli,
91
141
  }