Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50faccaba8 | ||
|
|
4d59d1758c | ||
|
|
b1a555da13 | ||
|
|
c92aaca8b0 | ||
|
|
c3e777e3e1 | ||
|
|
2e3b14187b | ||
|
|
f8a22521bd |
No files matched your search
+2
-1
@@ -28,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)
|
||||
|
||||
|
||||
@@ -12,12 +12,7 @@ import {
|
||||
SYNTHETIC_BASE_URL,
|
||||
SYNTHETIC_MODEL_CATALOG,
|
||||
} from "./synthetic-models.js";
|
||||
import {
|
||||
buildVeniceModelDefinition,
|
||||
discoverVeniceModels,
|
||||
VENICE_BASE_URL,
|
||||
VENICE_MODEL_CATALOG,
|
||||
} from "./venice-models.js";
|
||||
import { discoverVeniceModels, VENICE_BASE_URL } from "./venice-models.js";
|
||||
|
||||
type ModelsConfig = NonNullable<ClawdbotConfig["models"]>;
|
||||
export type ProviderConfig = NonNullable<ModelsConfig["providers"]>[string];
|
||||
|
||||
@@ -48,15 +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 previousKimiCode = process.env.KIMICODE_API_KEY;
|
||||
const previousMinimax = process.env.MINIMAX_API_KEY;
|
||||
const previousMoonshot = process.env.MOONSHOT_API_KEY;
|
||||
const previousSynthetic = process.env.SYNTHETIC_API_KEY;
|
||||
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.KIMICODE_API_KEY;
|
||||
delete process.env.MINIMAX_API_KEY;
|
||||
delete process.env.MOONSHOT_API_KEY;
|
||||
delete process.env.SYNTHETIC_API_KEY;
|
||||
delete process.env.VENICE_API_KEY;
|
||||
|
||||
try {
|
||||
vi.resetModules();
|
||||
@@ -79,12 +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 (previousKimiCode === undefined) delete process.env.KIMICODE_API_KEY;
|
||||
else process.env.KIMICODE_API_KEY = previousKimiCode;
|
||||
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 (previousVenice === undefined) delete process.env.VENICE_API_KEY;
|
||||
else process.env.VENICE_API_KEY = previousVenice;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -340,7 +340,9 @@ export async function discoverVeniceModels(): Promise<ModelDefinitionConfig[]> {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.warn(`[venice-models] Failed to discover models: HTTP ${response.status}, using static catalog`);
|
||||
console.warn(
|
||||
`[venice-models] Failed to discover models: HTTP ${response.status}, using static catalog`,
|
||||
);
|
||||
return VENICE_MODEL_CATALOG.map(buildVeniceModelDefinition);
|
||||
}
|
||||
|
||||
@@ -351,7 +353,9 @@ export async function discoverVeniceModels(): Promise<ModelDefinitionConfig[]> {
|
||||
}
|
||||
|
||||
// Merge discovered models with catalog metadata
|
||||
const catalogById = new Map(VENICE_MODEL_CATALOG.map((m) => [m.id, m]));
|
||||
const catalogById = new Map<string, VeniceCatalogEntry>(
|
||||
VENICE_MODEL_CATALOG.map((m) => [m.id, m]),
|
||||
);
|
||||
const models: ModelDefinitionConfig[] = [];
|
||||
|
||||
for (const apiModel of data.data) {
|
||||
|
||||
+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;
|
||||
|
||||
@@ -512,7 +512,6 @@ export function renderApp(state: AppViewState) {
|
||||
activeSubsection: state.configActiveSubsection,
|
||||
onRawChange: (next) => {
|
||||
state.configRaw = next;
|
||||
state.configFormDirty = true;
|
||||
},
|
||||
onFormModeChange: (mode) => (state.configFormMode = mode),
|
||||
onFormPatch: (path, value) => updateConfigFormValue(state, path, value),
|
||||
|
||||
@@ -96,6 +96,34 @@ describe("config view", () => {
|
||||
expect(applyButton?.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("enables save and apply when raw changes", () => {
|
||||
const container = document.createElement("div");
|
||||
render(
|
||||
renderConfig({
|
||||
...baseProps(),
|
||||
formMode: "raw",
|
||||
raw: "{\n gateway: { mode: \"local\" }\n}\n",
|
||||
originalRaw: "{\n}\n",
|
||||
}),
|
||||
container,
|
||||
);
|
||||
|
||||
const saveButton = Array.from(
|
||||
container.querySelectorAll("button"),
|
||||
).find((btn) => btn.textContent?.trim() === "Save") as
|
||||
| HTMLButtonElement
|
||||
| undefined;
|
||||
const applyButton = Array.from(
|
||||
container.querySelectorAll("button"),
|
||||
).find((btn) => btn.textContent?.trim() === "Apply") as
|
||||
| HTMLButtonElement
|
||||
| undefined;
|
||||
expect(saveButton).not.toBeUndefined();
|
||||
expect(applyButton).not.toBeUndefined();
|
||||
expect(saveButton?.disabled).toBe(false);
|
||||
expect(applyButton?.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it("switches mode via the sidebar toggle", () => {
|
||||
const container = document.createElement("div");
|
||||
const onFormModeChange = vi.fn();
|
||||
|
||||
Reference in new issue
Block a user