build(control-ui): prefer bun for UI build

This commit is contained in:
Peter Steinberger
2026-01-06 09:08:25 +01:00
parent 5774b4f300
commit c27dd75135
9 changed files with 141 additions and 43 deletions

View File

@@ -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.

View File

@@ -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:
<a href="https://github.com/scald"><img src="https://avatars.githubusercontent.com/u/1215913?v=4&s=48" width="48" height="48" alt="scald" title="scald"/></a> <a href="https://github.com/sreekaransrinath"><img src="https://avatars.githubusercontent.com/u/50989977?v=4&s=48" width="48" height="48" alt="sreekaransrinath" title="sreekaransrinath"/></a> <a href="https://github.com/ratulsarna"><img src="https://avatars.githubusercontent.com/u/105903728?v=4&s=48" width="48" height="48" alt="ratulsarna" title="ratulsarna"/></a> <a href="https://github.com/osolmaz"><img src="https://avatars.githubusercontent.com/u/2453968?v=4&s=48" width="48" height="48" alt="osolmaz" title="osolmaz"/></a> <a href="https://github.com/conhecendocontato"><img src="https://avatars.githubusercontent.com/u/82890727?v=4&s=48" width="48" height="48" alt="conhecendocontato" title="conhecendocontato"/></a> <a href="https://github.com/hrdwdmrbl"><img src="https://avatars.githubusercontent.com/u/554881?v=4&s=48" width="48" height="48" alt="hrdwdmrbl" title="hrdwdmrbl"/></a> <a href="https://github.com/jayhickey"><img src="https://avatars.githubusercontent.com/u/1676460?v=4&s=48" width="48" height="48" alt="jayhickey" title="jayhickey"/></a> <a href="https://github.com/jamesgroat"><img src="https://avatars.githubusercontent.com/u/2634024?v=4&s=48" width="48" height="48" alt="jamesgroat" title="jamesgroat"/></a> <a href="https://github.com/gtsifrikas"><img src="https://avatars.githubusercontent.com/u/8904378?v=4&s=48" width="48" height="48" alt="gtsifrikas" title="gtsifrikas"/></a> <a href="https://github.com/djangonavarro220"><img src="https://avatars.githubusercontent.com/u/251162586?v=4&s=48" width="48" height="48" alt="djangonavarro220" title="djangonavarro220"/></a>
<a href="https://github.com/azade-c"><img src="https://avatars.githubusercontent.com/u/252790079?v=4&s=48" width="48" height="48" alt="azade-c" title="azade-c"/></a> <a href="https://github.com/andranik-sahakyan"><img src="https://avatars.githubusercontent.com/u/8908029?v=4&s=48" width="48" height="48" alt="andranik-sahakyan" title="andranik-sahakyan"/></a>
<a href="https://github.com/adamgall"><img src="https://avatars.githubusercontent.com/u/706929?v=4&s=48" width="48" height="48" alt="adamgall" title="adamgall"/></a> <a href="https://github.com/jalehman"><img src="https://avatars.githubusercontent.com/u/550978?v=4&s=48" width="48" height="48" alt="jalehman" title="jalehman"/></a> <a href="https://github.com/jarvis-medmatic"><img src="https://avatars.githubusercontent.com/u/252428873?v=4&s=48" width="48" height="48" alt="jarvis-medmatic" title="jarvis-medmatic"/></a> <a href="https://github.com/mneves75"><img src="https://avatars.githubusercontent.com/u/2423436?v=4&s=48" width="48" height="48" alt="mneves75" title="mneves75"/></a> <a href="https://github.com/regenrek"><img src="https://avatars.githubusercontent.com/u/5182020?v=4&s=48" width="48" height="48" alt="regenrek" title="regenrek"/></a> <a href="https://github.com/tobiasbischoff"><img src="https://avatars.githubusercontent.com/u/711564?v=4&s=48" width="48" height="48" alt="tobiasbischoff" title="tobiasbischoff"/></a> <a href="https://github.com/MSch"><img src="https://avatars.githubusercontent.com/u/7475?v=4&s=48" width="48" height="48" alt="MSch" title="MSch"/></a> <a href="https://github.com/obviyus"><img src="https://avatars.githubusercontent.com/u/22031114?v=4&s=48" width="48" height="48" alt="obviyus" title="obviyus"/></a> <a href="https://github.com/dbhurley"><img src="https://avatars.githubusercontent.com/u/5251425?v=4&s=48" width="48" height="48" alt="dbhurley" title="dbhurley"/></a>
<a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="Iamadig" title="Iamadig"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a>
<a href="https://github.com/Asleep123"><img src="https://avatars.githubusercontent.com/u/122379135?v=4&s=48" width="48" height="48" alt="Asleep123" title="Asleep123"/></a> <a href="https://github.com/Iamadig"><img src="https://avatars.githubusercontent.com/u/102129234?v=4&s=48" width="48" height="48" alt="Iamadig" title="Iamadig"/></a> <a href="https://github.com/imfing"><img src="https://avatars.githubusercontent.com/u/5097752?v=4&s=48" width="48" height="48" alt="imfing" title="imfing"/></a> <a href="https://github.com/kitze"><img src="https://avatars.githubusercontent.com/u/1160594?v=4&s=48" width="48" height="48" alt="kitze" title="kitze"/></a> <a href="https://github.com/nachoiacovino"><img src="https://avatars.githubusercontent.com/u/50103937?v=4&s=48" width="48" height="48" alt="nachoiacovino" title="nachoiacovino"/></a> <a href="https://github.com/VACInc"><img src="https://avatars.githubusercontent.com/u/3279061?v=4&s=48" width="48" height="48" alt="VACInc" title="VACInc"/></a> <a href="https://github.com/cash-echo-bot"><img src="https://avatars.githubusercontent.com/u/252747386?v=4&s=48" width="48" height="48" alt="cash-echo-bot" title="cash-echo-bot"/></a> <a href="https://github.com/claude"><img src="https://avatars.githubusercontent.com/u/81847?v=4&s=48" width="48" height="48" alt="claude" title="claude"/></a> <a href="https://github.com/kiranjd"><img src="https://avatars.githubusercontent.com/u/25822851?v=4&s=48" width="48" height="48" alt="kiranjd" title="kiranjd"/></a> <a href="https://github.com/pcty-nextgen-service-account"><img src="https://avatars.githubusercontent.com/u/112553441?v=4&s=48" width="48" height="48" alt="pcty-nextgen-service-account" title="pcty-nextgen-service-account"/></a>
</p>

View File

@@ -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`).

View File

@@ -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
```

View File

@@ -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",

View File

@@ -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

102
scripts/ui.js Normal file
View File

@@ -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 <install|dev|build|test> [...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]);
}

View File

@@ -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;
}

View File

@@ -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,