diff --git a/CHANGELOG.md b/CHANGELOG.md
index 021462476..94fed3878 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -133,7 +133,7 @@
- Env: load global `$CLAWDBOT_STATE_DIR/.env` (`~/.clawdbot/.env`) as a fallback after CWD `.env`.
- Env: optional login-shell env fallback (opt-in; imports expected keys without overriding existing env).
- Agent tools: OpenAI-compatible tool JSON Schemas (fix `browser`, normalize union schemas).
-- Onboarding: when running from source, auto-build missing Control UI assets (`pnpm ui:build`).
+- Onboarding: when running from source, auto-build missing Control UI assets (`bun run ui:build`).
- Discord/Slack: route reaction + system notifications to the correct session (no main-session bleed).
- Agent tools: honor `agent.tools` allow/deny policy even when sandbox is off.
- Discord: avoid duplicate replies when OpenAI emits repeated `message_end` events.
diff --git a/README.md b/README.md
index dd1fb17ca..195fad9eb 100644
--- a/README.md
+++ b/README.md
@@ -40,10 +40,10 @@ Do **not** download prebuilt binaries. Build from source.
git clone https://github.com/clawdbot/clawdbot.git
cd clawdbot
-pnpm install
-pnpm build
-pnpm ui:build
-pnpm clawdbot onboard
+bun install
+bun run build
+bun run ui:build
+bun run clawdbot onboard
```
## Quick start (from source)
@@ -442,5 +442,5 @@ Thanks to all clawtributors:
-
+
diff --git a/docs/control-ui.md b/docs/control-ui.md
index 315e1415c..0e4fe0c32 100644
--- a/docs/control-ui.md
+++ b/docs/control-ui.md
@@ -63,21 +63,21 @@ Paste the token into the UI settings (sent as `connect.params.auth.token`).
The Gateway serves static files from `dist/control-ui`. Build them with:
```bash
-pnpm ui:install
-pnpm ui:build
+bun run ui:install
+bun run ui:build
```
Optional absolute base (when you want fixed asset URLs):
```bash
-CLAWDBOT_CONTROL_UI_BASE_PATH=/clawdbot/ pnpm ui:build
+CLAWDBOT_CONTROL_UI_BASE_PATH=/clawdbot/ bun run ui:build
```
For local development (separate dev server):
```bash
-pnpm ui:install
-pnpm ui:dev
+bun run ui:install
+bun run ui:dev
```
Then point the UI at your Gateway WS URL (e.g. `ws://127.0.0.1:18789`).
diff --git a/docs/web.md b/docs/web.md
index aeb0967f1..6e1a7274a 100644
--- a/docs/web.md
+++ b/docs/web.md
@@ -110,6 +110,6 @@ Open:
The Gateway serves static files from `dist/control-ui`. Build them with:
```bash
-pnpm ui:install
-pnpm ui:build
+bun run ui:install
+bun run ui:build
```
diff --git a/package.json b/package.json
index 4e30de34a..1e2ba8f6d 100644
--- a/package.json
+++ b/package.json
@@ -51,9 +51,9 @@
"docs:build": "cd docs && pnpm dlx mint broken-links",
"build": "tsc -p tsconfig.json && bun scripts/canvas-a2ui-copy.ts",
"release:check": "bun scripts/release-check.ts",
- "ui:install": "pnpm -C ui install",
- "ui:dev": "pnpm -C ui dev",
- "ui:build": "pnpm -C ui build",
+ "ui:install": "node scripts/ui.js install",
+ "ui:dev": "node scripts/ui.js dev",
+ "ui:build": "node scripts/ui.js build",
"start": "bun src/entry.ts",
"clawdbot": "bun src/entry.ts",
"gateway:watch": "bun --watch src/entry.ts gateway --force",
diff --git a/scripts/package-mac-app.sh b/scripts/package-mac-app.sh
index 7c7fe1b1f..b60a6cb75 100755
--- a/scripts/package-mac-app.sh
+++ b/scripts/package-mac-app.sh
@@ -146,8 +146,8 @@ else
fi
if [[ "${SKIP_UI_BUILD:-0}" != "1" ]]; then
- echo "🖥 Building Control UI (pnpm ui:build)"
- (cd "$ROOT_DIR" && pnpm ui:build)
+ echo "🖥 Building Control UI (ui:build)"
+ (cd "$ROOT_DIR" && node scripts/ui.js build)
else
echo "🖥 Skipping Control UI build (SKIP_UI_BUILD=1)"
fi
diff --git a/scripts/ui.js b/scripts/ui.js
new file mode 100644
index 000000000..bb84ebbff
--- /dev/null
+++ b/scripts/ui.js
@@ -0,0 +1,102 @@
+#!/usr/bin/env node
+import { spawn } from "node:child_process";
+import fs from "node:fs";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+
+const here = path.dirname(fileURLToPath(import.meta.url));
+const repoRoot = path.resolve(here, "..");
+const uiDir = path.join(repoRoot, "ui");
+
+function usage() {
+ // keep this tiny; it's invoked from npm scripts too
+ process.stderr.write(
+ "Usage: node scripts/ui.js [...args]\n",
+ );
+}
+
+function which(cmd) {
+ try {
+ const key = process.platform === "win32" ? "Path" : "PATH";
+ const paths = (process.env[key] ?? process.env.PATH ?? "")
+ .split(path.delimiter)
+ .filter(Boolean);
+ const extensions =
+ process.platform === "win32"
+ ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM")
+ .split(";")
+ .filter(Boolean)
+ : [""];
+ for (const entry of paths) {
+ for (const ext of extensions) {
+ const candidate = path.join(entry, process.platform === "win32" ? `${cmd}${ext}` : cmd);
+ try {
+ if (fs.existsSync(candidate)) return candidate;
+ } catch {
+ // ignore
+ }
+ }
+ }
+ } catch {
+ // ignore
+ }
+ return null;
+}
+
+function resolveRunner() {
+ const bun = which("bun");
+ if (bun) return { cmd: bun, kind: "bun" };
+ const pnpm = which("pnpm");
+ if (pnpm) return { cmd: pnpm, kind: "pnpm" };
+ return null;
+}
+
+function run(cmd, args) {
+ const child = spawn(cmd, args, {
+ cwd: uiDir,
+ stdio: "inherit",
+ env: process.env,
+ });
+ child.on("exit", (code, signal) => {
+ if (signal) process.exit(1);
+ process.exit(code ?? 1);
+ });
+}
+
+const [, , action, ...rest] = process.argv;
+if (!action) {
+ usage();
+ process.exit(2);
+}
+
+const runner = resolveRunner();
+if (!runner) {
+ process.stderr.write(
+ "Missing UI runner: install bun or pnpm, then retry.\n",
+ );
+ process.exit(1);
+}
+
+const script =
+ action === "install"
+ ? null
+ : action === "dev"
+ ? "dev"
+ : action === "build"
+ ? "build"
+ : action === "test"
+ ? "test"
+ : null;
+
+if (action !== "install" && !script) {
+ usage();
+ process.exit(2);
+}
+
+if (runner.kind === "bun") {
+ if (action === "install") run(runner.cmd, ["install", ...rest]);
+ else run(runner.cmd, ["run", script, ...rest]);
+} else {
+ if (action === "install") run(runner.cmd, ["install", ...rest]);
+ else run(runner.cmd, ["run", script, ...rest]);
+}
diff --git a/src/gateway/control-ui.ts b/src/gateway/control-ui.ts
index e53f4e352..8f69e806a 100644
--- a/src/gateway/control-ui.ts
+++ b/src/gateway/control-ui.ts
@@ -157,7 +157,7 @@ export function handleControlUiHttpRequest(
res.statusCode = 503;
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end(
- "Control UI assets not found. Build them with `pnpm ui:build` (or run `pnpm ui:dev` during development).",
+ "Control UI assets not found. Build them with `bun run ui:build` (or run `bun run ui:dev` during development).",
);
return true;
}
diff --git a/src/infra/control-ui-assets.ts b/src/infra/control-ui-assets.ts
index e005789dc..0b995a0cd 100644
--- a/src/infra/control-ui-assets.ts
+++ b/src/infra/control-ui-assets.ts
@@ -1,7 +1,7 @@
import fs from "node:fs";
import path from "node:path";
-import { runCommandWithTimeout, runExec } from "../process/exec.js";
+import { runCommandWithTimeout } from "../process/exec.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
export function resolveControlUiRepoRoot(
@@ -76,7 +76,7 @@ export async function ensureControlUiAssetsBuilt(
return {
ok: false,
built: false,
- message: `${hint}. Build them with \`pnpm ui:build\`.`,
+ message: `${hint}. Build them with \`bun run ui:build\`.`,
};
}
@@ -85,35 +85,28 @@ export async function ensureControlUiAssetsBuilt(
return { ok: true, built: false };
}
- const pnpmWhich = process.platform === "win32" ? "where" : "which";
- const pnpm = await runExec(pnpmWhich, ["pnpm"])
- .then(
- (r) =>
- r.stdout
- .split(/\r?\n/g)
- .map((l) => l.trim())
- .find(Boolean) ?? "",
- )
- .catch(() => "");
- if (!pnpm) {
+ const uiScript = path.join(repoRoot, "scripts", "ui.js");
+ if (!fs.existsSync(uiScript)) {
return {
ok: false,
built: false,
- message:
- "Control UI assets not found and pnpm missing. Install pnpm, then run `pnpm ui:build`.",
+ message: `Control UI assets missing but ${uiScript} is unavailable.`,
};
}
- runtime.log("Control UI assets missing; building (pnpm ui:build)…");
+ runtime.log("Control UI assets missing; building (ui:build)…");
const ensureInstalled = !fs.existsSync(
path.join(repoRoot, "ui", "node_modules"),
);
if (ensureInstalled) {
- const install = await runCommandWithTimeout([pnpm, "ui:install"], {
- cwd: repoRoot,
- timeoutMs: opts?.timeoutMs ?? 10 * 60_000,
- });
+ const install = await runCommandWithTimeout(
+ [process.execPath, uiScript, "install"],
+ {
+ cwd: repoRoot,
+ timeoutMs: opts?.timeoutMs ?? 10 * 60_000,
+ },
+ );
if (install.code !== 0) {
return {
ok: false,
@@ -123,10 +116,13 @@ export async function ensureControlUiAssetsBuilt(
}
}
- const build = await runCommandWithTimeout([pnpm, "ui:build"], {
- cwd: repoRoot,
- timeoutMs: opts?.timeoutMs ?? 10 * 60_000,
- });
+ const build = await runCommandWithTimeout(
+ [process.execPath, uiScript, "build"],
+ {
+ cwd: repoRoot,
+ timeoutMs: opts?.timeoutMs ?? 10 * 60_000,
+ },
+ );
if (build.code !== 0) {
return {
ok: false,