Merge branch 'main' into main
This commit is contained in:
commit
85782aa4f7
2
.gitignore
vendored
2
.gitignore
vendored
@ -40,6 +40,8 @@ apps/ios/*.xcfilelist
|
||||
|
||||
# Vendor build artifacts
|
||||
vendor/a2ui/renderers/lit/dist/
|
||||
src/canvas-host/a2ui/*.bundle.js
|
||||
src/canvas-host/a2ui/*.map
|
||||
.bundle.hash
|
||||
|
||||
# fastlane (iOS)
|
||||
|
||||
@ -39,6 +39,7 @@ Status: unreleased.
|
||||
- Browser: route browser control via gateway/node; remove standalone browser control command and control URL config.
|
||||
- Browser: route `browser.request` via node proxies when available; honor proxy timeouts; derive browser ports from `gateway.port`.
|
||||
- Update: ignore dist/control-ui for dirty checks and restore after ui builds. (#1976) Thanks @Glucksberg.
|
||||
- Build: bundle A2UI assets during build and stop tracking generated bundles. (#2455) Thanks @0oAstro.
|
||||
- Telegram: allow caption param for media sends. (#1888) Thanks @mguellsegarra.
|
||||
- Telegram: support plugin sendPayload channelData (media/buttons) and validate plugin commands. (#1917) Thanks @JoshuaLelon.
|
||||
- Telegram: avoid block replies when streaming is disabled. (#1885) Thanks @ivancasco.
|
||||
|
||||
@ -39,7 +39,7 @@ export default defineConfig({
|
||||
output: {
|
||||
file: outputFile,
|
||||
format: "esm",
|
||||
inlineDynamicImports: true,
|
||||
codeSplitting: false,
|
||||
sourcemap: false,
|
||||
},
|
||||
});
|
||||
|
||||
1
dist/control-ui/assets/index-08nzABV3.css
vendored
1
dist/control-ui/assets/index-08nzABV3.css
vendored
File diff suppressed because one or more lines are too long
3119
dist/control-ui/assets/index-DQcOTEYz.js
vendored
3119
dist/control-ui/assets/index-DQcOTEYz.js
vendored
File diff suppressed because one or more lines are too long
1
dist/control-ui/assets/index-DQcOTEYz.js.map
vendored
1
dist/control-ui/assets/index-DQcOTEYz.js.map
vendored
File diff suppressed because one or more lines are too long
15
dist/control-ui/index.html
vendored
15
dist/control-ui/index.html
vendored
@ -1,15 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Clawdbot Control</title>
|
||||
<meta name="color-scheme" content="dark light" />
|
||||
<link rel="icon" href="./favicon.ico" sizes="any" />
|
||||
<script type="module" crossorigin src="./assets/index-DQcOTEYz.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-08nzABV3.css">
|
||||
</head>
|
||||
<body>
|
||||
<clawdbot-app></clawdbot-app>
|
||||
</body>
|
||||
</html>
|
||||
@ -175,6 +175,7 @@ clawdbot onboard --install-daemon
|
||||
```
|
||||
|
||||
If you don’t have a global install yet, run the onboarding step via `pnpm clawdbot ...` from the repo.
|
||||
`pnpm build` also bundles A2UI assets; if you need to run just that step, use `pnpm canvas:a2ui:bundle`.
|
||||
|
||||
Gateway (from this repo):
|
||||
|
||||
|
||||
@ -67,7 +67,8 @@ Example:
|
||||
- macOS: `/opt/homebrew/bin`, `/usr/local/bin`, `/usr/bin`, `/bin`
|
||||
- Linux: `/usr/local/bin`, `/usr/bin`, `/bin`
|
||||
- `host=sandbox`: runs `sh -lc` (login shell) inside the container, so `/etc/profile` may reset `PATH`.
|
||||
Clawdbot prepends `env.PATH` after profile sourcing; `tools.exec.pathPrepend` applies here too.
|
||||
Clawdbot prepends `env.PATH` after profile sourcing via an internal env var (no shell interpolation);
|
||||
`tools.exec.pathPrepend` applies here too.
|
||||
- `host=node`: only env overrides you pass are sent to the node. `tools.exec.pathPrepend` only applies
|
||||
if the exec call already sets `env.PATH`. Headless node hosts accept `PATH` only when it prepends
|
||||
the node host PATH (no replacement). macOS nodes drop `PATH` overrides entirely.
|
||||
|
||||
@ -82,7 +82,7 @@
|
||||
"docs:bin": "node scripts/build-docs-list.mjs",
|
||||
"docs:dev": "cd docs && mint dev",
|
||||
"docs:build": "cd docs && pnpm dlx --reporter append-only mint broken-links",
|
||||
"build": "tsc -p tsconfig.json && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/write-build-info.ts",
|
||||
"build": "pnpm canvas:a2ui:bundle && tsc -p tsconfig.json && node --import tsx scripts/canvas-a2ui-copy.ts && node --import tsx scripts/copy-hook-metadata.ts && node --import tsx scripts/write-build-info.ts",
|
||||
"plugins:sync": "node --import tsx scripts/sync-plugin-versions.ts",
|
||||
"release:check": "node --import tsx scripts/release-check.ts",
|
||||
"ui:install": "node scripts/ui.js install",
|
||||
|
||||
@ -1,8 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
on_error() {
|
||||
echo "A2UI bundling failed. Re-run with: pnpm canvas:a2ui:bundle" >&2
|
||||
echo "If this persists, verify pnpm deps and try again." >&2
|
||||
}
|
||||
trap on_error ERR
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
HASH_FILE="$ROOT_DIR/src/canvas-host/a2ui/.bundle.hash"
|
||||
OUTPUT_FILE="$ROOT_DIR/src/canvas-host/a2ui/a2ui.bundle.js"
|
||||
|
||||
INPUT_PATHS=(
|
||||
"$ROOT_DIR/package.json"
|
||||
@ -33,7 +40,7 @@ compute_hash() {
|
||||
current_hash="$(compute_hash)"
|
||||
if [[ -f "$HASH_FILE" ]]; then
|
||||
previous_hash="$(cat "$HASH_FILE")"
|
||||
if [[ "$previous_hash" == "$current_hash" ]]; then
|
||||
if [[ "$previous_hash" == "$current_hash" && -f "$OUTPUT_FILE" ]]; then
|
||||
echo "A2UI bundle up to date; skipping."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@ -1,19 +1,44 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const srcDir = path.join(repoRoot, "src", "canvas-host", "a2ui");
|
||||
const outDir = path.join(repoRoot, "dist", "canvas-host", "a2ui");
|
||||
|
||||
async function main() {
|
||||
await fs.stat(path.join(srcDir, "index.html"));
|
||||
await fs.stat(path.join(srcDir, "a2ui.bundle.js"));
|
||||
export function getA2uiPaths(env = process.env) {
|
||||
const srcDir =
|
||||
env.CLAWDBOT_A2UI_SRC_DIR ?? path.join(repoRoot, "src", "canvas-host", "a2ui");
|
||||
const outDir =
|
||||
env.CLAWDBOT_A2UI_OUT_DIR ?? path.join(repoRoot, "dist", "canvas-host", "a2ui");
|
||||
return { srcDir, outDir };
|
||||
}
|
||||
|
||||
export async function copyA2uiAssets({
|
||||
srcDir,
|
||||
outDir,
|
||||
}: {
|
||||
srcDir: string;
|
||||
outDir: string;
|
||||
}) {
|
||||
try {
|
||||
await fs.stat(path.join(srcDir, "index.html"));
|
||||
await fs.stat(path.join(srcDir, "a2ui.bundle.js"));
|
||||
} catch (err) {
|
||||
const message =
|
||||
'Missing A2UI bundle assets. Run "pnpm canvas:a2ui:bundle" and retry.';
|
||||
throw new Error(message, { cause: err });
|
||||
}
|
||||
await fs.mkdir(path.dirname(outDir), { recursive: true });
|
||||
await fs.cp(srcDir, outDir, { recursive: true });
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(String(err));
|
||||
process.exit(1);
|
||||
});
|
||||
async function main() {
|
||||
const { srcDir, outDir } = getA2uiPaths();
|
||||
await copyA2uiAssets({ srcDir, outDir });
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
main().catch((err) => {
|
||||
console.error(String(err));
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
@ -60,11 +60,18 @@ export function buildDockerExecArgs(params: {
|
||||
for (const [key, value] of Object.entries(params.env)) {
|
||||
args.push("-e", `${key}=${value}`);
|
||||
}
|
||||
const hasCustomPath = typeof params.env.PATH === "string" && params.env.PATH.length > 0;
|
||||
if (hasCustomPath) {
|
||||
// Avoid interpolating PATH into the shell command; pass it via env instead.
|
||||
args.push("-e", `CLAWDBOT_PREPEND_PATH=${params.env.PATH}`);
|
||||
}
|
||||
// Login shell (-l) sources /etc/profile which resets PATH to a minimal set,
|
||||
// overriding both Docker ENV and -e PATH=... environment variables.
|
||||
// Prepend custom PATH after profile sourcing to ensure custom tools are accessible
|
||||
// while preserving system paths that /etc/profile may have added.
|
||||
const pathExport = params.env.PATH ? `export PATH="${params.env.PATH}:$PATH"; ` : "";
|
||||
const pathExport = hasCustomPath
|
||||
? 'export PATH="${CLAWDBOT_PREPEND_PATH}:$PATH"; unset CLAWDBOT_PREPEND_PATH; '
|
||||
: "";
|
||||
args.push(params.containerName, "sh", "-lc", `${pathExport}${params.command}`);
|
||||
return args;
|
||||
}
|
||||
|
||||
@ -318,9 +318,30 @@ describe("buildDockerExecArgs", () => {
|
||||
});
|
||||
|
||||
const commandArg = args[args.length - 1];
|
||||
expect(commandArg).toContain('export PATH="/custom/bin:/usr/local/bin:/usr/bin:$PATH"');
|
||||
expect(args).toContain("CLAWDBOT_PREPEND_PATH=/custom/bin:/usr/local/bin:/usr/bin");
|
||||
expect(commandArg).toContain('export PATH="${CLAWDBOT_PREPEND_PATH}:$PATH"');
|
||||
expect(commandArg).toContain("echo hello");
|
||||
expect(commandArg).toBe('export PATH="/custom/bin:/usr/local/bin:/usr/bin:$PATH"; echo hello');
|
||||
expect(commandArg).toBe(
|
||||
'export PATH="${CLAWDBOT_PREPEND_PATH}:$PATH"; unset CLAWDBOT_PREPEND_PATH; echo hello',
|
||||
);
|
||||
});
|
||||
|
||||
it("does not interpolate PATH into the shell command", () => {
|
||||
const injectedPath = "$(touch /tmp/clawdbot-path-injection)";
|
||||
const args = buildDockerExecArgs({
|
||||
containerName: "test-container",
|
||||
command: "echo hello",
|
||||
env: {
|
||||
PATH: injectedPath,
|
||||
HOME: "/home/user",
|
||||
},
|
||||
tty: false,
|
||||
});
|
||||
|
||||
const commandArg = args[args.length - 1];
|
||||
expect(args).toContain(`CLAWDBOT_PREPEND_PATH=${injectedPath}`);
|
||||
expect(commandArg).not.toContain(injectedPath);
|
||||
expect(commandArg).toContain("CLAWDBOT_PREPEND_PATH");
|
||||
});
|
||||
|
||||
it("does not add PATH export when PATH is not in env", () => {
|
||||
|
||||
@ -48,7 +48,7 @@ export async function fetchBrowserJson<T>(
|
||||
const timeoutMs = init?.timeoutMs ?? 5000;
|
||||
try {
|
||||
if (isAbsoluteHttp(url)) {
|
||||
return await fetchHttpJson<T>(url, { ...(init ?? {}), timeoutMs });
|
||||
return await fetchHttpJson<T>(url, init ? { ...init, timeoutMs } : { timeoutMs });
|
||||
}
|
||||
const started = await startBrowserControlServiceFromConfig();
|
||||
if (!started) {
|
||||
|
||||
@ -1 +1 @@
|
||||
a99455ba0c4d0aad0a110bf25440c208b798198d5524b269f0f2d3f984262ae4
|
||||
6d63b866aa0e917b278c6bef42229e8cd1f43c8ba31c845a96b4d9d5ce780265
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -27,7 +27,7 @@ export function registerBrowserElementCommands(
|
||||
.filter(Boolean)
|
||||
: undefined;
|
||||
try {
|
||||
const result = await callBrowserAct({
|
||||
const result = await callBrowserAct<{ url?: string }>({
|
||||
parent,
|
||||
profile,
|
||||
body: {
|
||||
|
||||
@ -26,7 +26,7 @@ export function registerBrowserFilesAndDownloadsCommands(
|
||||
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
|
||||
try {
|
||||
const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined;
|
||||
const result = await callBrowserRequest(
|
||||
const result = await callBrowserRequest<{ download: { path: string } }>(
|
||||
parent,
|
||||
{
|
||||
method: "POST",
|
||||
@ -68,7 +68,7 @@ export function registerBrowserFilesAndDownloadsCommands(
|
||||
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
|
||||
try {
|
||||
const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined;
|
||||
const result = await callBrowserRequest(
|
||||
const result = await callBrowserRequest<{ download: { path: string } }>(
|
||||
parent,
|
||||
{
|
||||
method: "POST",
|
||||
@ -108,7 +108,7 @@ export function registerBrowserFilesAndDownloadsCommands(
|
||||
const { parent, profile } = resolveBrowserActionContext(cmd, parentOpts);
|
||||
try {
|
||||
const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined;
|
||||
const result = await callBrowserRequest(
|
||||
const result = await callBrowserRequest<{ download: { path: string } }>(
|
||||
parent,
|
||||
{
|
||||
method: "POST",
|
||||
|
||||
@ -21,7 +21,7 @@ export function registerBrowserFormWaitEvalCommands(
|
||||
fields: opts.fields,
|
||||
fieldsFile: opts.fieldsFile,
|
||||
});
|
||||
const result = await callBrowserAct({
|
||||
const result = await callBrowserAct<{ result?: unknown }>({
|
||||
parent,
|
||||
profile,
|
||||
body: {
|
||||
@ -66,7 +66,7 @@ export function registerBrowserFormWaitEvalCommands(
|
||||
? (opts.load as "load" | "domcontentloaded" | "networkidle")
|
||||
: undefined;
|
||||
const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : undefined;
|
||||
const result = await callBrowserAct({
|
||||
const result = await callBrowserAct<{ result?: unknown }>({
|
||||
parent,
|
||||
profile,
|
||||
body: {
|
||||
@ -108,7 +108,7 @@ export function registerBrowserFormWaitEvalCommands(
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await callBrowserAct({
|
||||
const result = await callBrowserAct<{ result?: unknown }>({
|
||||
parent,
|
||||
profile,
|
||||
body: {
|
||||
|
||||
40
src/scripts/canvas-a2ui-copy.test.ts
Normal file
40
src/scripts/canvas-a2ui-copy.test.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { copyA2uiAssets } from "../../scripts/canvas-a2ui-copy.js";
|
||||
|
||||
describe("canvas a2ui copy", () => {
|
||||
it("throws a helpful error when assets are missing", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-a2ui-"));
|
||||
|
||||
try {
|
||||
await expect(copyA2uiAssets({ srcDir: dir, outDir: path.join(dir, "out") })).rejects.toThrow(
|
||||
'Run "pnpm canvas:a2ui:bundle"',
|
||||
);
|
||||
} finally {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("copies bundled assets to dist", async () => {
|
||||
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-a2ui-"));
|
||||
const srcDir = path.join(dir, "src");
|
||||
const outDir = path.join(dir, "dist");
|
||||
|
||||
try {
|
||||
await fs.mkdir(srcDir, { recursive: true });
|
||||
await fs.writeFile(path.join(srcDir, "index.html"), "<html></html>", "utf8");
|
||||
await fs.writeFile(path.join(srcDir, "a2ui.bundle.js"), "console.log(1);", "utf8");
|
||||
|
||||
await copyA2uiAssets({ srcDir, outDir });
|
||||
|
||||
await expect(fs.stat(path.join(outDir, "index.html"))).resolves.toBeTruthy();
|
||||
await expect(fs.stat(path.join(outDir, "a2ui.bundle.js"))).resolves.toBeTruthy();
|
||||
} finally {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -2,7 +2,7 @@ import { listChannelPlugins } from "../channels/plugins/index.js";
|
||||
import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
|
||||
import type { ChannelId } from "../channels/plugins/types.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { resolveBrowserConfig } from "../browser/config.js";
|
||||
import { resolveBrowserConfig, resolveProfile } from "../browser/config.js";
|
||||
import { resolveConfigPath, resolveStateDir } from "../config/paths.js";
|
||||
import { resolveGatewayAuth } from "../gateway/auth.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { generateUUID } from "./uuid";
|
||||
|
||||
@ -26,7 +26,13 @@ describe("generateUUID", () => {
|
||||
});
|
||||
|
||||
it("still returns a v4 UUID when crypto is missing", () => {
|
||||
const id = generateUUID(null);
|
||||
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
try {
|
||||
const id = generateUUID(null);
|
||||
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
|
||||
expect(warnSpy).toHaveBeenCalled();
|
||||
} finally {
|
||||
warnSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -3,6 +3,8 @@ export type CryptoLike = {
|
||||
getRandomValues?: ((array: Uint8Array) => Uint8Array) | undefined;
|
||||
};
|
||||
|
||||
let warnedWeakCrypto = false;
|
||||
|
||||
function uuidFromBytes(bytes: Uint8Array): string {
|
||||
bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
|
||||
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 1
|
||||
@ -29,6 +31,12 @@ function weakRandomBytes(): Uint8Array {
|
||||
return bytes;
|
||||
}
|
||||
|
||||
function warnWeakCryptoOnce() {
|
||||
if (warnedWeakCrypto) return;
|
||||
warnedWeakCrypto = true;
|
||||
console.warn("[uuid] crypto API missing; falling back to weak randomness");
|
||||
}
|
||||
|
||||
export function generateUUID(cryptoLike: CryptoLike | null = globalThis.crypto): string {
|
||||
if (cryptoLike && typeof cryptoLike.randomUUID === "function") return cryptoLike.randomUUID();
|
||||
|
||||
@ -38,5 +46,6 @@ export function generateUUID(cryptoLike: CryptoLike | null = globalThis.crypto):
|
||||
return uuidFromBytes(bytes);
|
||||
}
|
||||
|
||||
warnWeakCryptoOnce();
|
||||
return uuidFromBytes(weakRandomBytes());
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user