Compare commits

...

11 Commits

Author SHA1 Message Date
Peter Steinberger
d148c71368 ci: run Windows job in pwsh (#569) (thanks @bjesuiter) 2026-01-09 14:57:23 +01:00
Peter Steinberger
bdb2a660d7 ci: pin Windows runner image (#569) (thanks @bjesuiter) 2026-01-09 14:51:08 +01:00
Peter Steinberger
81ab281b51 ci: silence vitest on Windows only (#569) (thanks @bjesuiter) 2026-01-09 14:45:57 +01:00
Peter Steinberger
81271f1b1c ci: silence vitest on Windows (#569) (thanks @bjesuiter) 2026-01-09 14:44:53 +01:00
Peter Steinberger
49a2db78c9 ci: fix Windows runner encoding (#569) (thanks @bjesuiter) 2026-01-09 14:39:24 +01:00
Peter Steinberger
6640b43876 ci: move UTF-8 step to windows job 2026-01-09 14:28:52 +01:00
Peter Steinberger
64f50e5c49 ci: force UTF-8 on Windows runners (#569) (thanks @bjesuiter) 2026-01-09 14:27:04 +01:00
Peter Steinberger
bd6e881eba fix: sanitize Windows CI output safely (#569) (thanks @bjesuiter) 2026-01-09 14:20:53 +01:00
Peter Steinberger
22b224a343 fix: sanitize Windows CI test output (#569) (thanks @bjesuiter) 2026-01-09 14:16:39 +01:00
Peter Steinberger
ce9769ab09 fix: land #569 and restore gate (thanks @bjesuiter) 2026-01-09 14:03:33 +01:00
Benjamin Jesuiter
71ecfb0b86 fix(ui): default to relative paths for control UI assets
Changes the default base path from "/" to "./" so the control UI works
correctly when served under a custom basePath (e.g., /jbclawd/).

Previously, assets were referenced with absolute paths like /assets/...,
which failed when the UI was served under a subpath. With relative paths
(./assets/...), the browser resolves them relative to the HTML location,
making the UI work regardless of the configured basePath.
2026-01-09 13:53:17 +01:00
14 changed files with 121 additions and 66 deletions

View File

@ -91,10 +91,10 @@ jobs:
run: ${{ matrix.command }} run: ${{ matrix.command }}
checks-windows: checks-windows:
runs-on: windows-latest runs-on: windows-2022
defaults: defaults:
run: run:
shell: bash shell: pwsh
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@ -104,7 +104,7 @@ jobs:
command: pnpm lint command: pnpm lint
- runtime: node - runtime: node
task: test task: test
command: pnpm test command: pnpm test -- --run --silent
- runtime: node - runtime: node
task: build task: build
command: pnpm build command: pnpm build
@ -117,17 +117,22 @@ jobs:
with: with:
submodules: false submodules: false
- name: Force UTF-8 output (Windows)
run: |
chcp.com 65001
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
"LANG=en_US.UTF-8" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
"LC_ALL=en_US.UTF-8" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- name: Checkout submodules (retry) - name: Checkout submodules (retry)
run: | run: |
set -euo pipefail
git submodule sync --recursive git submodule sync --recursive
for attempt in 1 2 3 4 5; do for ($attempt = 1; $attempt -le 5; $attempt++) {
if git -c protocol.version=2 submodule update --init --force --depth=1 --recursive; then git -c protocol.version=2 submodule update --init --force --depth=1 --recursive
exit 0 if ($LASTEXITCODE -eq 0) { exit 0 }
fi Write-Host "Submodule update failed (attempt $attempt/5). Retrying..."
echo "Submodule update failed (attempt $attempt/5). Retrying…" Start-Sleep -Seconds ($attempt * 10)
sleep $((attempt * 10)) }
done
exit 1 exit 1
- name: Setup Node.js - name: Setup Node.js
@ -148,7 +153,9 @@ jobs:
bun -v bun -v
- name: Capture node path - name: Capture node path
run: echo "NODE_BIN=$(dirname \"$(node -p \"process.execPath\")\")" >> "$GITHUB_ENV" run: |
$nodeBin = Split-Path -Parent (node -p "process.execPath")
"NODE_BIN=$nodeBin" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- name: Enable corepack and pin pnpm - name: Enable corepack and pin pnpm
run: | run: |
@ -160,14 +167,15 @@ jobs:
env: env:
CI: true CI: true
run: | run: |
export PATH="$NODE_BIN:$PATH" $env:PATH = "$env:NODE_BIN;$env:PATH"
which node Get-Command node | Format-List
node -v node -v
pnpm -v pnpm -v
pnpm install --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || pnpm install --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true pnpm install --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true || pnpm install --ignore-scripts=false --config.engine-strict=false --config.enable-pre-post-scripts=true
- name: Run ${{ matrix.task }} (${{ matrix.runtime }}) - name: Run ${{ matrix.task }} (${{ matrix.runtime }})
run: ${{ matrix.command }} run: |
${{ matrix.command }}
macos-app: macos-app:
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'

View File

@ -46,6 +46,7 @@
- Control UI: show/patch per-session reasoning level and render extracted reasoning in chat. - Control UI: show/patch per-session reasoning level and render extracted reasoning in chat.
- Control UI: queue outgoing chat messages, add Enter-to-send, and show queued items. (#527) — thanks @YuriNachos - Control UI: queue outgoing chat messages, add Enter-to-send, and show queued items. (#527) — thanks @YuriNachos
- Control UI: drop explicit `ui:install` step; `ui:build` now auto-installs UI deps (docs + update flow). - Control UI: drop explicit `ui:install` step; `ui:build` now auto-installs UI deps (docs + update flow).
- Control UI: default control UI asset base to relative paths for subpath hosting. (#569) — thanks @bjesuiter
- Telegram: retry long-polling conflicts with backoff to avoid fatal exits. - Telegram: retry long-polling conflicts with backoff to avoid fatal exits.
- Telegram: fix grammY fetch type mismatch when injecting `fetch`. (#512) — thanks @YuriNachos - Telegram: fix grammY fetch type mismatch when injecting `fetch`. (#512) — thanks @YuriNachos
- WhatsApp: resolve @lid JIDs via Baileys mapping to unblock inbound messages. (#415) - WhatsApp: resolve @lid JIDs via Baileys mapping to unblock inbound messages. (#415)

View File

@ -80,7 +80,7 @@ describe("Agent-specific sandbox config", () => {
expect(context).toBeDefined(); expect(context).toBeDefined();
expect(context?.enabled).toBe(true); expect(context?.enabled).toBe(true);
}); }, 20000);
it("should allow agent-specific docker setupCommand overrides", async () => { it("should allow agent-specific docker setupCommand overrides", async () => {
const { resolveSandboxContext } = await import("./sandbox.js"); const { resolveSandboxContext } = await import("./sandbox.js");

View File

@ -179,14 +179,22 @@ async function ensureDevGatewayConfig(opts: { reset?: boolean }) {
mode: "local", mode: "local",
bind: "loopback", bind: "loopback",
}, },
agent: { agents: {
workspace, defaults: {
skipBootstrap: true, workspace,
}, skipBootstrap: true,
identity: { },
name: DEV_IDENTITY_NAME, list: [
theme: DEV_IDENTITY_THEME, {
emoji: DEV_IDENTITY_EMOJI, id: "main",
default: true,
identity: {
name: DEV_IDENTITY_NAME,
theme: DEV_IDENTITY_THEME,
emoji: DEV_IDENTITY_EMOJI,
},
},
],
}, },
}); });
await ensureDevWorkspace(workspace); await ensureDevWorkspace(workspace);

View File

@ -289,7 +289,7 @@ describe("doctor", () => {
"+15555550123", "+15555550123",
]); ]);
expect(written.routing).toBeUndefined(); expect(written.routing).toBeUndefined();
}); }, 20000);
it("migrates legacy Clawdis services", async () => { it("migrates legacy Clawdis services", async () => {
readConfigFileSnapshot.mockResolvedValue({ readConfigFileSnapshot.mockResolvedValue({

View File

@ -10,12 +10,6 @@ const DEFAULT_MINIMAX_CONTEXT_WINDOW = 200000;
const DEFAULT_MINIMAX_MAX_TOKENS = 8192; const DEFAULT_MINIMAX_MAX_TOKENS = 8192;
export const MINIMAX_HOSTED_MODEL_REF = `minimax/${MINIMAX_HOSTED_MODEL_ID}`; export const MINIMAX_HOSTED_MODEL_REF = `minimax/${MINIMAX_HOSTED_MODEL_ID}`;
const DEFAULT_MINIMAX_BASE_URL = "https://api.minimax.io/v1";
export const MINIMAX_HOSTED_MODEL_ID = "MiniMax-M2.1";
const DEFAULT_MINIMAX_CONTEXT_WINDOW = 200000;
const DEFAULT_MINIMAX_MAX_TOKENS = 8192;
export const MINIMAX_HOSTED_MODEL_REF = `minimax/${MINIMAX_HOSTED_MODEL_ID}`;
export async function writeOAuthCredentials( export async function writeOAuthCredentials(
provider: OAuthProvider, provider: OAuthProvider,
creds: OAuthCredentials, creds: OAuthCredentials,
@ -176,7 +170,7 @@ export function applyMinimaxHostedProviderConfig(
cfg: ClawdbotConfig, cfg: ClawdbotConfig,
params?: { baseUrl?: string }, params?: { baseUrl?: string },
): ClawdbotConfig { ): ClawdbotConfig {
const models = { ...cfg.agent?.models }; const models = { ...cfg.agents?.defaults?.models };
models[MINIMAX_HOSTED_MODEL_REF] = { models[MINIMAX_HOSTED_MODEL_REF] = {
...models[MINIMAX_HOSTED_MODEL_REF], ...models[MINIMAX_HOSTED_MODEL_REF],
alias: models[MINIMAX_HOSTED_MODEL_REF]?.alias ?? "Minimax", alias: models[MINIMAX_HOSTED_MODEL_REF]?.alias ?? "Minimax",
@ -212,9 +206,12 @@ export function applyMinimaxHostedProviderConfig(
return { return {
...cfg, ...cfg,
agent: { agents: {
...cfg.agent, ...cfg.agents,
models, defaults: {
...cfg.agents?.defaults,
models,
},
}, },
models: { models: {
mode: cfg.models?.mode ?? "merge", mode: cfg.models?.mode ?? "merge",
@ -254,17 +251,21 @@ export function applyMinimaxHostedConfig(
const next = applyMinimaxHostedProviderConfig(cfg, params); const next = applyMinimaxHostedProviderConfig(cfg, params);
return { return {
...next, ...next,
agent: { agents: {
...next.agent, ...next.agents,
model: { defaults: {
...(next.agent?.model && ...next.agents?.defaults,
"fallbacks" in (next.agent.model as Record<string, unknown>) model: {
? { ...(next.agents?.defaults?.model &&
fallbacks: (next.agent.model as { fallbacks?: string[] }) "fallbacks" in (next.agents.defaults.model as Record<string, unknown>)
.fallbacks, ? {
} fallbacks: (
: undefined), next.agents.defaults.model as { fallbacks?: string[] }
primary: MINIMAX_HOSTED_MODEL_REF, ).fallbacks,
}
: undefined),
primary: MINIMAX_HOSTED_MODEL_REF,
},
}, },
}, },
}; };

View File

@ -546,7 +546,8 @@ async function promptWhatsAppAllowFrom(
"WhatsApp number", "WhatsApp number",
); );
const entry = await prompter.text({ const entry = await prompter.text({
message: "Your personal WhatsApp number (the phone you will message from)", message:
"Your personal WhatsApp number (the phone you will message from)",
placeholder: "+15555550123", placeholder: "+15555550123",
initialValue: existingAllowFrom[0], initialValue: existingAllowFrom[0],
validate: (value) => { validate: (value) => {
@ -613,7 +614,8 @@ async function promptWhatsAppAllowFrom(
"WhatsApp number", "WhatsApp number",
); );
const entry = await prompter.text({ const entry = await prompter.text({
message: "Your personal WhatsApp number (the phone you will message from)", message:
"Your personal WhatsApp number (the phone you will message from)",
placeholder: "+15555550123", placeholder: "+15555550123",
initialValue: existingAllowFrom[0], initialValue: existingAllowFrom[0],
validate: (value) => { validate: (value) => {

View File

@ -1202,7 +1202,7 @@ export type AgentDefaultsConfig = {
every?: string; every?: string;
/** Heartbeat model override (provider/model). */ /** Heartbeat model override (provider/model). */
model?: string; model?: string;
/** Delivery target (last|whatsapp|telegram|discord|signal|imessage|none). */ /** Delivery target (last|whatsapp|telegram|discord|slack|signal|imessage|msteams|none). */
target?: target?:
| "last" | "last"
| "whatsapp" | "whatsapp"
@ -1211,6 +1211,7 @@ export type AgentDefaultsConfig = {
| "slack" | "slack"
| "signal" | "signal"
| "imessage" | "imessage"
| "msteams"
| "none"; | "none";
/** Optional delivery override (E.164 for WhatsApp, chat id for Telegram). */ /** Optional delivery override (E.164 for WhatsApp, chat id for Telegram). */
to?: string; to?: string;

View File

@ -603,6 +603,7 @@ const HeartbeatSchema = z
z.literal("slack"), z.literal("slack"),
z.literal("signal"), z.literal("signal"),
z.literal("imessage"), z.literal("imessage"),
z.literal("msteams"),
z.literal("none"), z.literal("none"),
]) ])
.optional(), .optional(),

View File

@ -56,8 +56,9 @@ export async function monitorMSTeamsProvider(
const textLimit = resolveTextChunkLimit(cfg, "msteams"); const textLimit = resolveTextChunkLimit(cfg, "msteams");
const MB = 1024 * 1024; const MB = 1024 * 1024;
const mediaMaxBytes = const mediaMaxBytes =
typeof cfg.agent?.mediaMaxMb === "number" && cfg.agent.mediaMaxMb > 0 typeof cfg.agents?.defaults?.mediaMaxMb === "number" &&
? Math.floor(cfg.agent.mediaMaxMb * MB) cfg.agents.defaults.mediaMaxMb > 0
? Math.floor(cfg.agents.defaults.mediaMaxMb * MB)
: 8 * MB; : 8 * MB;
const conversationStore = const conversationStore =
opts.conversationStore ?? createMSTeamsConversationStoreFs(); opts.conversationStore ?? createMSTeamsConversationStoreFs();

View File

@ -122,9 +122,7 @@ export async function sendMessageTelegram(
const client: ApiClientOptions | undefined = fetchImpl const client: ApiClientOptions | undefined = fetchImpl
? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] } ? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] }
: undefined; : undefined;
const api = const api = opts.api ?? new Bot(token, client ? { client } : undefined).api;
opts.api ??
new Bot(token, client ? { client } : undefined).api;
const mediaUrl = opts.mediaUrl?.trim(); const mediaUrl = opts.mediaUrl?.trim();
// Build optional params for forum topics and reply threading. // Build optional params for forum topics and reply threading.
@ -296,9 +294,7 @@ export async function reactMessageTelegram(
const client: ApiClientOptions | undefined = fetchImpl const client: ApiClientOptions | undefined = fetchImpl
? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] } ? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] }
: undefined; : undefined;
const api = const api = opts.api ?? new Bot(token, client ? { client } : undefined).api;
opts.api ??
new Bot(token, client ? { client } : undefined).api;
const request = createTelegramRetryRunner({ const request = createTelegramRetryRunner({
retry: opts.retry, retry: opts.retry,
configRetry: account.config.retry, configRetry: account.config.retry,

View File

@ -11,10 +11,7 @@ export async function setTelegramWebhook(opts: {
const client: ApiClientOptions | undefined = fetchImpl const client: ApiClientOptions | undefined = fetchImpl
? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] } ? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] }
: undefined; : undefined;
const bot = new Bot( const bot = new Bot(opts.token, client ? { client } : undefined);
opts.token,
client ? { client } : undefined,
);
await bot.api.setWebhook(opts.url, { await bot.api.setWebhook(opts.url, {
secret_token: opts.secret, secret_token: opts.secret,
drop_pending_updates: opts.dropPendingUpdates ?? false, drop_pending_updates: opts.dropPendingUpdates ?? false,
@ -26,9 +23,6 @@ export async function deleteTelegramWebhook(opts: { token: string }) {
const client: ApiClientOptions | undefined = fetchImpl const client: ApiClientOptions | undefined = fetchImpl
? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] } ? { fetch: fetchImpl as unknown as ApiClientOptions["fetch"] }
: undefined; : undefined;
const bot = new Bot( const bot = new Bot(opts.token, client ? { client } : undefined);
opts.token,
client ? { client } : undefined,
);
await bot.api.deleteWebhook(); await bot.api.deleteWebhook();
} }

View File

@ -2,6 +2,48 @@ import fs from "node:fs";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
const shouldSanitizeConsoleOutput =
process.platform === "win32" && process.env.GITHUB_ACTIONS === "true";
if (shouldSanitizeConsoleOutput) {
const sanitize = (value: string) => {
let out = "";
for (const ch of value) {
const code = ch.charCodeAt(0);
if (code === 9 || code === 10 || code === 13) {
out += ch;
continue;
}
if (code >= 32 && code <= 126) {
out += ch;
continue;
}
out += "?";
}
return out;
};
const patchStream = (stream: NodeJS.WriteStream) => {
const originalWrite = stream.write.bind(stream);
stream.write = ((chunk: unknown, encoding?: unknown, cb?: unknown) => {
if (typeof chunk === "string") {
return originalWrite(sanitize(chunk), encoding as never, cb as never);
}
if (Buffer.isBuffer(chunk)) {
return originalWrite(
sanitize(chunk.toString("utf8")),
encoding as never,
cb as never,
);
}
return originalWrite(chunk as never, encoding as never, cb as never);
}) as typeof stream.write;
};
patchStream(process.stdout);
patchStream(process.stderr);
}
const originalHome = process.env.HOME; const originalHome = process.env.HOME;
const originalUserProfile = process.env.USERPROFILE; const originalUserProfile = process.env.USERPROFILE;
const originalXdgConfigHome = process.env.XDG_CONFIG_HOME; const originalXdgConfigHome = process.env.XDG_CONFIG_HOME;

View File

@ -14,7 +14,7 @@ function normalizeBase(input: string): string {
export default defineConfig(({ command }) => { export default defineConfig(({ command }) => {
const envBase = process.env.CLAWDBOT_CONTROL_UI_BASE_PATH?.trim(); const envBase = process.env.CLAWDBOT_CONTROL_UI_BASE_PATH?.trim();
const base = envBase ? normalizeBase(envBase) : "/"; const base = envBase ? normalizeBase(envBase) : "./";
return { return {
base, base,
publicDir: path.resolve(here, "public"), publicDir: path.resolve(here, "public"),