build(control-ui): prefer bun for UI build
This commit is contained in:
@@ -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.
|
||||
|
||||
10
README.md
10
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:
|
||||
<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>
|
||||
|
||||
@@ -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`).
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
102
scripts/ui.js
Normal 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]);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user