Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50faccaba8 | ||
|
|
4d59d1758c | ||
|
|
b1a555da13 | ||
|
|
c92aaca8b0 | ||
|
|
c3e777e3e1 | ||
|
|
2e3b14187b | ||
|
|
f8a22521bd |
No files matched your search
+2
-2
@@ -21,7 +21,6 @@ Docs: https://docs.clawd.bot
|
||||
- BlueBubbles: keep part-index GUIDs in reply tags when short IDs are missing.
|
||||
- Web UI: hide internal `message_id` hints in chat bubbles.
|
||||
- Web UI: show Stop button during active runs, swap back to New session when idle. (#1664) Thanks @ndbroadbent.
|
||||
- Web UI: keep raw config edits from toggling channel save state; enable save/apply on raw changes only. (#1673) Thanks @Glucksberg.
|
||||
- Heartbeat: normalize target identifiers for consistent routing.
|
||||
- TUI: reload history after gateway reconnect to restore session state. (#1663)
|
||||
- Telegram: use wrapped fetch for long-polling on Node to normalize AbortSignal handling. (#1639)
|
||||
@@ -29,12 +28,13 @@ Docs: https://docs.clawd.bot
|
||||
- Agents: auto-compact on context overflow prompt errors before failing. (#1627) Thanks @rodrigouroz.
|
||||
- Agents: use the active auth profile for auto-compaction recovery.
|
||||
- Models: default missing custom provider fields so minimal configs are accepted.
|
||||
- Gateway: honor trusted proxy client IPs for local pairing + HTTP checks. (#1654) Thanks @ndbroadbent.
|
||||
- Gateway: skip Tailscale DNS probing when tailscale.mode is off. (#1671)
|
||||
- Gateway: reduce log noise for late invokes + remote node probes; debounce skills refresh. (#1607) Thanks @petter-b.
|
||||
- macOS: default direct-transport `ws://` URLs to port 18789; document `gateway.remote.transport`. (#1603) Thanks @ngutman.
|
||||
- Voice Call: return stream TwiML for outbound conversation calls on initial Twilio webhook. (#1634)
|
||||
- Google Chat: tighten email allowlist matching, typing cleanup, media caps, and onboarding/docs/tests. (#1635) Thanks @iHildy.
|
||||
- Google Chat: normalize space targets without double `spaces/` prefix.
|
||||
- CLI: register lazy subcommands before parse so non-help invocations work. (#1683) Thanks @grrowl.
|
||||
|
||||
## 2026.1.23-1
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
||||
- [How does memory work?](#how-does-memory-work)
|
||||
- [Does semantic memory search require an OpenAI API key?](#does-semantic-memory-search-require-an-openai-api-key)
|
||||
- [Where things live on disk](#where-things-live-on-disk)
|
||||
- [Is all data used with Clawdbot saved locally?](#is-all-data-used-with-clawdbot-saved-locally)
|
||||
- [Where does Clawdbot store its data?](#where-does-clawdbot-store-its-data)
|
||||
- [Where should AGENTS.md / SOUL.md / USER.md / MEMORY.md live?](#where-should-agentsmd--soulmd--usermd--memorymd-live)
|
||||
- [What’s the recommended backup strategy?](#whats-the-recommended-backup-strategy)
|
||||
@@ -918,6 +919,20 @@ models — see [Memory](/concepts/memory) for the setup details.
|
||||
|
||||
## Where things live on disk
|
||||
|
||||
### Is all data used with Clawdbot saved locally?
|
||||
|
||||
No — **Clawdbot’s state is local**, but **external services still see what you send them**.
|
||||
|
||||
- **Local by default:** sessions, memory files, config, and workspace live on the Gateway host
|
||||
(`~/.clawdbot` + your workspace directory).
|
||||
- **Remote by necessity:** messages you send to model providers (Anthropic/OpenAI/etc.) go to
|
||||
their APIs, and chat platforms (WhatsApp/Telegram/Slack/etc.) store message data on their
|
||||
servers.
|
||||
- **You control the footprint:** using local models keeps prompts on your machine, but channel
|
||||
traffic still goes through the channel’s servers.
|
||||
|
||||
Related: [Agent workspace](/concepts/agent-workspace), [Memory](/concepts/memory).
|
||||
|
||||
### Where does Clawdbot store its data?
|
||||
|
||||
Everything lives under `$CLAWDBOT_STATE_DIR` (default: `~/.clawdbot`):
|
||||
|
||||
@@ -7,7 +7,8 @@ read_when:
|
||||
# Windows (WSL2)
|
||||
|
||||
Clawdbot on Windows is recommended **via WSL2** (Ubuntu recommended). The
|
||||
CLI + Gateway run inside Linux, which keeps the runtime consistent. Native
|
||||
CLI + Gateway run inside Linux, which keeps the runtime consistent and makes
|
||||
tooling far more compatible (Node/Bun/pnpm, Linux binaries, skills). Native
|
||||
Windows installs are untested and more problematic.
|
||||
|
||||
Native Windows companion apps are planned.
|
||||
|
||||
@@ -45,7 +45,7 @@ run on host, set an explicit per-agent override:
|
||||
See [Web tools](/tools/web).
|
||||
|
||||
macOS: if you plan to build the apps, install Xcode / CLT. For the CLI + gateway only, Node is enough.
|
||||
Windows: use **WSL2** (Ubuntu recommended). WSL2 is strongly recommended; native Windows is untested and more problematic. Install WSL2 first, then run the Linux steps inside WSL. See [Windows (WSL2)](/platforms/windows).
|
||||
Windows: use **WSL2** (Ubuntu recommended). WSL2 is strongly recommended; native Windows is untested, more problematic, and has poorer tool compatibility. Install WSL2 first, then run the Linux steps inside WSL. See [Windows (WSL2)](/platforms/windows).
|
||||
|
||||
## 1) Install the CLI (recommended)
|
||||
|
||||
|
||||
@@ -48,29 +48,19 @@ describe("models-config", () => {
|
||||
const previous = process.env.COPILOT_GITHUB_TOKEN;
|
||||
const previousGh = process.env.GH_TOKEN;
|
||||
const previousGithub = process.env.GITHUB_TOKEN;
|
||||
const previousVenice = process.env.VENICE_API_KEY;
|
||||
const previousKimiCode = process.env.KIMICODE_API_KEY;
|
||||
const previousKimiCodeAlt = process.env.KIMI_CODE_API_KEY;
|
||||
const previousMinimax = process.env.MINIMAX_API_KEY;
|
||||
const previousMoonshot = process.env.MOONSHOT_API_KEY;
|
||||
const previousSynthetic = process.env.SYNTHETIC_API_KEY;
|
||||
const previousAwsAccessKey = process.env.AWS_ACCESS_KEY_ID;
|
||||
const previousAwsSecretKey = process.env.AWS_SECRET_ACCESS_KEY;
|
||||
const previousAwsProfile = process.env.AWS_PROFILE;
|
||||
const previousAwsBearer = process.env.AWS_BEARER_TOKEN;
|
||||
const previousVenice = process.env.VENICE_API_KEY;
|
||||
delete process.env.COPILOT_GITHUB_TOKEN;
|
||||
delete process.env.GH_TOKEN;
|
||||
delete process.env.GITHUB_TOKEN;
|
||||
delete process.env.VENICE_API_KEY;
|
||||
delete process.env.KIMICODE_API_KEY;
|
||||
delete process.env.KIMI_CODE_API_KEY;
|
||||
delete process.env.MINIMAX_API_KEY;
|
||||
delete process.env.MOONSHOT_API_KEY;
|
||||
delete process.env.SYNTHETIC_API_KEY;
|
||||
delete process.env.AWS_ACCESS_KEY_ID;
|
||||
delete process.env.AWS_SECRET_ACCESS_KEY;
|
||||
delete process.env.AWS_PROFILE;
|
||||
delete process.env.AWS_BEARER_TOKEN;
|
||||
delete process.env.VENICE_API_KEY;
|
||||
|
||||
try {
|
||||
vi.resetModules();
|
||||
@@ -93,26 +83,16 @@ describe("models-config", () => {
|
||||
else process.env.GH_TOKEN = previousGh;
|
||||
if (previousGithub === undefined) delete process.env.GITHUB_TOKEN;
|
||||
else process.env.GITHUB_TOKEN = previousGithub;
|
||||
if (previousVenice === undefined) delete process.env.VENICE_API_KEY;
|
||||
else process.env.VENICE_API_KEY = previousVenice;
|
||||
if (previousKimiCode === undefined) delete process.env.KIMICODE_API_KEY;
|
||||
else process.env.KIMICODE_API_KEY = previousKimiCode;
|
||||
if (previousKimiCodeAlt === undefined) delete process.env.KIMI_CODE_API_KEY;
|
||||
else process.env.KIMI_CODE_API_KEY = previousKimiCodeAlt;
|
||||
if (previousMinimax === undefined) delete process.env.MINIMAX_API_KEY;
|
||||
else process.env.MINIMAX_API_KEY = previousMinimax;
|
||||
if (previousMoonshot === undefined) delete process.env.MOONSHOT_API_KEY;
|
||||
else process.env.MOONSHOT_API_KEY = previousMoonshot;
|
||||
if (previousSynthetic === undefined) delete process.env.SYNTHETIC_API_KEY;
|
||||
else process.env.SYNTHETIC_API_KEY = previousSynthetic;
|
||||
if (previousAwsAccessKey === undefined) delete process.env.AWS_ACCESS_KEY_ID;
|
||||
else process.env.AWS_ACCESS_KEY_ID = previousAwsAccessKey;
|
||||
if (previousAwsSecretKey === undefined) delete process.env.AWS_SECRET_ACCESS_KEY;
|
||||
else process.env.AWS_SECRET_ACCESS_KEY = previousAwsSecretKey;
|
||||
if (previousAwsProfile === undefined) delete process.env.AWS_PROFILE;
|
||||
else process.env.AWS_PROFILE = previousAwsProfile;
|
||||
if (previousAwsBearer === undefined) delete process.env.AWS_BEARER_TOKEN;
|
||||
else process.env.AWS_BEARER_TOKEN = previousAwsBearer;
|
||||
if (previousVenice === undefined) delete process.env.VENICE_API_KEY;
|
||||
else process.env.VENICE_API_KEY = previousVenice;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
+28
-29
@@ -1,37 +1,36 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { rewriteUpdateFlagArgv } from "./run-main.js";
|
||||
const registerSubCliByName = vi.fn(async () => true);
|
||||
const parseAsync = vi.fn(async () => undefined);
|
||||
const buildProgram = vi.fn(() => ({ parseAsync }));
|
||||
|
||||
describe("rewriteUpdateFlagArgv", () => {
|
||||
it("leaves argv unchanged when --update is absent", () => {
|
||||
const argv = ["node", "entry.js", "status"];
|
||||
expect(rewriteUpdateFlagArgv(argv)).toBe(argv);
|
||||
vi.mock("../infra/dotenv.js", () => ({ loadDotEnv: vi.fn() }));
|
||||
vi.mock("../infra/env.js", () => ({ normalizeEnv: vi.fn() }));
|
||||
vi.mock("../infra/path-env.js", () => ({ ensureClawdbotCliOnPath: vi.fn() }));
|
||||
vi.mock("../infra/runtime-guard.js", () => ({ assertSupportedRuntime: vi.fn() }));
|
||||
vi.mock("../infra/errors.js", () => ({ formatUncaughtError: vi.fn(() => "error") }));
|
||||
vi.mock("../infra/unhandled-rejections.js", () => ({ installUnhandledRejectionHandler: vi.fn() }));
|
||||
vi.mock("../logging.js", () => ({ enableConsoleCapture: vi.fn() }));
|
||||
vi.mock("./program.js", () => ({ buildProgram }));
|
||||
vi.mock("./program/register.subclis.js", () => ({ registerSubCliByName }));
|
||||
vi.mock("./route.js", () => ({ tryRouteCli: vi.fn(async () => false) }));
|
||||
|
||||
const { runCli } = await import("./run-main.js");
|
||||
|
||||
describe("runCli", () => {
|
||||
afterEach(() => {
|
||||
registerSubCliByName.mockClear();
|
||||
parseAsync.mockClear();
|
||||
buildProgram.mockClear();
|
||||
});
|
||||
|
||||
it("rewrites --update into the update command", () => {
|
||||
expect(rewriteUpdateFlagArgv(["node", "entry.js", "--update"])).toEqual([
|
||||
"node",
|
||||
"entry.js",
|
||||
"update",
|
||||
]);
|
||||
});
|
||||
it("registers the primary subcommand before parsing", async () => {
|
||||
const argv = ["/usr/bin/node-22", "/opt/clawdbot/entry.js", "gateway", "--port", "18789"];
|
||||
|
||||
it("preserves global flags that appear before --update", () => {
|
||||
expect(rewriteUpdateFlagArgv(["node", "entry.js", "--profile", "p", "--update"])).toEqual([
|
||||
"node",
|
||||
"entry.js",
|
||||
"--profile",
|
||||
"p",
|
||||
"update",
|
||||
]);
|
||||
});
|
||||
await runCli(argv);
|
||||
|
||||
it("keeps update options after the rewritten command", () => {
|
||||
expect(rewriteUpdateFlagArgv(["node", "entry.js", "--update", "--json"])).toEqual([
|
||||
"node",
|
||||
"entry.js",
|
||||
"update",
|
||||
"--json",
|
||||
]);
|
||||
expect(registerSubCliByName).toHaveBeenCalledTimes(1);
|
||||
expect(registerSubCliByName).toHaveBeenCalledWith(expect.any(Object), "gateway");
|
||||
expect(parseAsync).toHaveBeenCalledWith(argv);
|
||||
});
|
||||
});
|
||||
+6
-7
@@ -11,7 +11,7 @@ import { assertSupportedRuntime } from "../infra/runtime-guard.js";
|
||||
import { formatUncaughtError } from "../infra/errors.js";
|
||||
import { installUnhandledRejectionHandler } from "../infra/unhandled-rejections.js";
|
||||
import { enableConsoleCapture } from "../logging.js";
|
||||
import { getPrimaryCommand, hasHelpOrVersion } from "./argv.js";
|
||||
import { getPrimaryCommand } from "./argv.js";
|
||||
import { tryRouteCli } from "./route.js";
|
||||
|
||||
export function rewriteUpdateFlagArgv(argv: string[]): string[] {
|
||||
@@ -50,12 +50,11 @@ export async function runCli(argv: string[] = process.argv) {
|
||||
});
|
||||
|
||||
const parseArgv = rewriteUpdateFlagArgv(normalizedArgv);
|
||||
if (hasHelpOrVersion(parseArgv)) {
|
||||
const primary = getPrimaryCommand(parseArgv);
|
||||
if (primary) {
|
||||
const { registerSubCliByName } = await import("./program/register.subclis.js");
|
||||
await registerSubCliByName(program, primary);
|
||||
}
|
||||
// Register the primary subcommand if one exists (for lazy-loading)
|
||||
const primary = getPrimaryCommand(parseArgv);
|
||||
if (primary) {
|
||||
const { registerSubCliByName } = await import("./program/register.subclis.js");
|
||||
await registerSubCliByName(program, primary);
|
||||
}
|
||||
await program.parseAsync(parseArgv);
|
||||
}
|
||||
|
||||
@@ -353,9 +353,7 @@ export function applyVeniceProviderConfig(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||
const veniceModels = VENICE_MODEL_CATALOG.map(buildVeniceModelDefinition);
|
||||
const mergedModels = [
|
||||
...existingModels,
|
||||
...veniceModels.filter(
|
||||
(model) => !existingModels.some((existing) => existing.id === model.id),
|
||||
),
|
||||
...veniceModels.filter((model) => !existingModels.some((existing) => existing.id === model.id)),
|
||||
];
|
||||
const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record<
|
||||
string,
|
||||
|
||||
@@ -2,10 +2,7 @@ export {
|
||||
SYNTHETIC_DEFAULT_MODEL_ID,
|
||||
SYNTHETIC_DEFAULT_MODEL_REF,
|
||||
} from "../agents/synthetic-models.js";
|
||||
export {
|
||||
VENICE_DEFAULT_MODEL_ID,
|
||||
VENICE_DEFAULT_MODEL_REF,
|
||||
} from "../agents/venice-models.js";
|
||||
export { VENICE_DEFAULT_MODEL_ID, VENICE_DEFAULT_MODEL_REF } from "../agents/venice-models.js";
|
||||
export {
|
||||
applyAuthProfileConfig,
|
||||
applyKimiCodeConfig,
|
||||
|
||||
@@ -13,6 +13,7 @@ export async function startGatewayDiscovery(params: {
|
||||
gatewayTls?: { enabled: boolean; fingerprintSha256?: string };
|
||||
canvasPort?: number;
|
||||
wideAreaDiscoveryEnabled: boolean;
|
||||
tailscaleMode: "off" | "serve" | "funnel";
|
||||
logDiscovery: { info: (msg: string) => void; warn: (msg: string) => void };
|
||||
}) {
|
||||
let bonjourStop: (() => Promise<void>) | null = null;
|
||||
@@ -20,8 +21,11 @@ export async function startGatewayDiscovery(params: {
|
||||
process.env.CLAWDBOT_DISABLE_BONJOUR !== "1" &&
|
||||
process.env.NODE_ENV !== "test" &&
|
||||
!process.env.VITEST;
|
||||
const tailscaleEnabled = params.tailscaleMode !== "off";
|
||||
const needsTailnetDns = bonjourEnabled || params.wideAreaDiscoveryEnabled;
|
||||
const tailnetDns = needsTailnetDns ? await resolveTailnetDnsHint() : undefined;
|
||||
const tailnetDns = needsTailnetDns
|
||||
? await resolveTailnetDnsHint({ enabled: tailscaleEnabled })
|
||||
: undefined;
|
||||
const sshPortEnv = process.env.CLAWDBOT_SSH_PORT?.trim();
|
||||
const sshPortParsed = sshPortEnv ? Number.parseInt(sshPortEnv, 10) : NaN;
|
||||
const sshPort = Number.isFinite(sshPortParsed) && sshPortParsed > 0 ? sshPortParsed : undefined;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
|
||||
const getTailnetHostname = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("../infra/tailscale.js", () => ({ getTailnetHostname }));
|
||||
|
||||
import { resolveTailnetDnsHint } from "./server-discovery.js";
|
||||
|
||||
describe("resolveTailnetDnsHint", () => {
|
||||
const prevTailnetDns = { value: undefined as string | undefined };
|
||||
|
||||
beforeEach(() => {
|
||||
prevTailnetDns.value = process.env.CLAWDBOT_TAILNET_DNS;
|
||||
delete process.env.CLAWDBOT_TAILNET_DNS;
|
||||
getTailnetHostname.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (prevTailnetDns.value === undefined) {
|
||||
delete process.env.CLAWDBOT_TAILNET_DNS;
|
||||
} else {
|
||||
process.env.CLAWDBOT_TAILNET_DNS = prevTailnetDns.value;
|
||||
}
|
||||
});
|
||||
|
||||
test("returns env hint when disabled", async () => {
|
||||
process.env.CLAWDBOT_TAILNET_DNS = "studio.tailnet.ts.net.";
|
||||
const value = await resolveTailnetDnsHint({ enabled: false });
|
||||
expect(value).toBe("studio.tailnet.ts.net");
|
||||
expect(getTailnetHostname).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("skips tailscale lookup when disabled", async () => {
|
||||
const value = await resolveTailnetDnsHint({ enabled: false });
|
||||
expect(value).toBeUndefined();
|
||||
expect(getTailnetHostname).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("uses tailscale lookup when enabled", async () => {
|
||||
getTailnetHostname.mockResolvedValue("host.tailnet.ts.net");
|
||||
const value = await resolveTailnetDnsHint({ enabled: true });
|
||||
expect(value).toBe("host.tailnet.ts.net");
|
||||
expect(getTailnetHostname).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -55,11 +55,13 @@ export function resolveBonjourCliPath(opts: ResolveBonjourCliPathOptions = {}):
|
||||
export async function resolveTailnetDnsHint(opts?: {
|
||||
env?: NodeJS.ProcessEnv;
|
||||
exec?: typeof runExec;
|
||||
enabled?: boolean;
|
||||
}): Promise<string | undefined> {
|
||||
const env = opts?.env ?? process.env;
|
||||
const envRaw = env.CLAWDBOT_TAILNET_DNS?.trim();
|
||||
const envValue = envRaw && envRaw.length > 0 ? envRaw.replace(/\.$/, "") : "";
|
||||
if (envValue) return envValue;
|
||||
if (opts?.enabled === false) return undefined;
|
||||
|
||||
const exec =
|
||||
opts?.exec ??
|
||||
|
||||
@@ -339,6 +339,7 @@ export async function startGatewayServer(
|
||||
? { enabled: true, fingerprintSha256: gatewayTls.fingerprintSha256 }
|
||||
: undefined,
|
||||
wideAreaDiscoveryEnabled: cfgAtStart.discovery?.wideArea?.enabled === true,
|
||||
tailscaleMode,
|
||||
logDiscovery,
|
||||
});
|
||||
bonjourStop = discovery.bonjourStop;
|
||||
|
||||
@@ -134,10 +134,7 @@ export function connectGateway(host: GatewayHost) {
|
||||
},
|
||||
onClose: ({ code, reason }) => {
|
||||
host.connected = false;
|
||||
// Code 1012 = Service Restart (expected during config saves, don't show as error)
|
||||
if (code !== 1012) {
|
||||
host.lastError = `disconnected (${code}): ${reason || "no reason"}`;
|
||||
}
|
||||
host.lastError = `disconnected (${code}): ${reason || "no reason"}`;
|
||||
},
|
||||
onEvent: (evt) => handleGatewayEvent(host, evt),
|
||||
onGap: ({ expected, received }) => {
|
||||
|
||||
@@ -234,9 +234,8 @@ export function renderConfig(props: ConfigProps) {
|
||||
const hasChanges = props.formMode === "form" ? diff.length > 0 : hasRawChanges;
|
||||
|
||||
// Save/apply buttons require actual changes to be enabled
|
||||
// Note: formUnsafe warns about unsupported schema paths but shouldn't block saving
|
||||
const canSaveForm =
|
||||
Boolean(props.formValue) && !props.loading;
|
||||
Boolean(props.formValue) && !props.loading && !formUnsafe;
|
||||
const canSave =
|
||||
props.connected &&
|
||||
!props.saving &&
|
||||
|
||||
Reference in new issue
Block a user