Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e9ecd257e | ||
|
|
595b874798 |
No files matched your search
+2
-2
@@ -21,6 +21,7 @@ Docs: https://docs.clawd.bot
|
|||||||
- BlueBubbles: keep part-index GUIDs in reply tags when short IDs are missing.
|
- 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: 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: 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.
|
- Heartbeat: normalize target identifiers for consistent routing.
|
||||||
- TUI: reload history after gateway reconnect to restore session state. (#1663)
|
- 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)
|
- Telegram: use wrapped fetch for long-polling on Node to normalize AbortSignal handling. (#1639)
|
||||||
@@ -28,13 +29,12 @@ Docs: https://docs.clawd.bot
|
|||||||
- Agents: auto-compact on context overflow prompt errors before failing. (#1627) Thanks @rodrigouroz.
|
- Agents: auto-compact on context overflow prompt errors before failing. (#1627) Thanks @rodrigouroz.
|
||||||
- Agents: use the active auth profile for auto-compaction recovery.
|
- Agents: use the active auth profile for auto-compaction recovery.
|
||||||
- Models: default missing custom provider fields so minimal configs are accepted.
|
- Models: default missing custom provider fields so minimal configs are accepted.
|
||||||
- Gateway: skip Tailscale DNS probing when tailscale.mode is off. (#1671)
|
- Gateway: honor trusted proxy client IPs for local pairing + HTTP checks. (#1654) Thanks @ndbroadbent.
|
||||||
- Gateway: reduce log noise for late invokes + remote node probes; debounce skills refresh. (#1607) Thanks @petter-b.
|
- 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.
|
- 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)
|
- 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: tighten email allowlist matching, typing cleanup, media caps, and onboarding/docs/tests. (#1635) Thanks @iHildy.
|
||||||
- Google Chat: normalize space targets without double `spaces/` prefix.
|
- 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
|
## 2026.1.23-1
|
||||||
|
|
||||||
|
|||||||
@@ -66,7 +66,6 @@ Quick answers plus deeper troubleshooting for real-world setups (local dev, VPS,
|
|||||||
- [How does memory work?](#how-does-memory-work)
|
- [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)
|
- [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)
|
- [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 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)
|
- [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)
|
- [What’s the recommended backup strategy?](#whats-the-recommended-backup-strategy)
|
||||||
@@ -919,20 +918,6 @@ models — see [Memory](/concepts/memory) for the setup details.
|
|||||||
|
|
||||||
## Where things live on disk
|
## 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?
|
### Where does Clawdbot store its data?
|
||||||
|
|
||||||
Everything lives under `$CLAWDBOT_STATE_DIR` (default: `~/.clawdbot`):
|
Everything lives under `$CLAWDBOT_STATE_DIR` (default: `~/.clawdbot`):
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ read_when:
|
|||||||
# Windows (WSL2)
|
# Windows (WSL2)
|
||||||
|
|
||||||
Clawdbot on Windows is recommended **via WSL2** (Ubuntu recommended). The
|
Clawdbot on Windows is recommended **via WSL2** (Ubuntu recommended). The
|
||||||
CLI + Gateway run inside Linux, which keeps the runtime consistent and makes
|
CLI + Gateway run inside Linux, which keeps the runtime consistent. Native
|
||||||
tooling far more compatible (Node/Bun/pnpm, Linux binaries, skills). Native
|
|
||||||
Windows installs are untested and more problematic.
|
Windows installs are untested and more problematic.
|
||||||
|
|
||||||
Native Windows companion apps are planned.
|
Native Windows companion apps are planned.
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ run on host, set an explicit per-agent override:
|
|||||||
See [Web tools](/tools/web).
|
See [Web tools](/tools/web).
|
||||||
|
|
||||||
macOS: if you plan to build the apps, install Xcode / CLT. For the CLI + gateway only, Node is enough.
|
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, more problematic, and has poorer tool compatibility. 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 and more problematic. Install WSL2 first, then run the Linux steps inside WSL. See [Windows (WSL2)](/platforms/windows).
|
||||||
|
|
||||||
## 1) Install the CLI (recommended)
|
## 1) Install the CLI (recommended)
|
||||||
|
|
||||||
|
|||||||
@@ -48,19 +48,29 @@ describe("models-config", () => {
|
|||||||
const previous = process.env.COPILOT_GITHUB_TOKEN;
|
const previous = process.env.COPILOT_GITHUB_TOKEN;
|
||||||
const previousGh = process.env.GH_TOKEN;
|
const previousGh = process.env.GH_TOKEN;
|
||||||
const previousGithub = process.env.GITHUB_TOKEN;
|
const previousGithub = process.env.GITHUB_TOKEN;
|
||||||
|
const previousVenice = process.env.VENICE_API_KEY;
|
||||||
const previousKimiCode = process.env.KIMICODE_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 previousMinimax = process.env.MINIMAX_API_KEY;
|
||||||
const previousMoonshot = process.env.MOONSHOT_API_KEY;
|
const previousMoonshot = process.env.MOONSHOT_API_KEY;
|
||||||
const previousSynthetic = process.env.SYNTHETIC_API_KEY;
|
const previousSynthetic = process.env.SYNTHETIC_API_KEY;
|
||||||
const previousVenice = process.env.VENICE_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;
|
||||||
delete process.env.COPILOT_GITHUB_TOKEN;
|
delete process.env.COPILOT_GITHUB_TOKEN;
|
||||||
delete process.env.GH_TOKEN;
|
delete process.env.GH_TOKEN;
|
||||||
delete process.env.GITHUB_TOKEN;
|
delete process.env.GITHUB_TOKEN;
|
||||||
|
delete process.env.VENICE_API_KEY;
|
||||||
delete process.env.KIMICODE_API_KEY;
|
delete process.env.KIMICODE_API_KEY;
|
||||||
|
delete process.env.KIMI_CODE_API_KEY;
|
||||||
delete process.env.MINIMAX_API_KEY;
|
delete process.env.MINIMAX_API_KEY;
|
||||||
delete process.env.MOONSHOT_API_KEY;
|
delete process.env.MOONSHOT_API_KEY;
|
||||||
delete process.env.SYNTHETIC_API_KEY;
|
delete process.env.SYNTHETIC_API_KEY;
|
||||||
delete process.env.VENICE_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;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
@@ -83,16 +93,26 @@ describe("models-config", () => {
|
|||||||
else process.env.GH_TOKEN = previousGh;
|
else process.env.GH_TOKEN = previousGh;
|
||||||
if (previousGithub === undefined) delete process.env.GITHUB_TOKEN;
|
if (previousGithub === undefined) delete process.env.GITHUB_TOKEN;
|
||||||
else process.env.GITHUB_TOKEN = previousGithub;
|
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;
|
if (previousKimiCode === undefined) delete process.env.KIMICODE_API_KEY;
|
||||||
else process.env.KIMICODE_API_KEY = previousKimiCode;
|
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;
|
if (previousMinimax === undefined) delete process.env.MINIMAX_API_KEY;
|
||||||
else process.env.MINIMAX_API_KEY = previousMinimax;
|
else process.env.MINIMAX_API_KEY = previousMinimax;
|
||||||
if (previousMoonshot === undefined) delete process.env.MOONSHOT_API_KEY;
|
if (previousMoonshot === undefined) delete process.env.MOONSHOT_API_KEY;
|
||||||
else process.env.MOONSHOT_API_KEY = previousMoonshot;
|
else process.env.MOONSHOT_API_KEY = previousMoonshot;
|
||||||
if (previousSynthetic === undefined) delete process.env.SYNTHETIC_API_KEY;
|
if (previousSynthetic === undefined) delete process.env.SYNTHETIC_API_KEY;
|
||||||
else process.env.SYNTHETIC_API_KEY = previousSynthetic;
|
else process.env.SYNTHETIC_API_KEY = previousSynthetic;
|
||||||
if (previousVenice === undefined) delete process.env.VENICE_API_KEY;
|
if (previousAwsAccessKey === undefined) delete process.env.AWS_ACCESS_KEY_ID;
|
||||||
else process.env.VENICE_API_KEY = previousVenice;
|
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;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+29
-28
@@ -1,36 +1,37 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
const registerSubCliByName = vi.fn(async () => true);
|
import { rewriteUpdateFlagArgv } from "./run-main.js";
|
||||||
const parseAsync = vi.fn(async () => undefined);
|
|
||||||
const buildProgram = vi.fn(() => ({ parseAsync }));
|
|
||||||
|
|
||||||
vi.mock("../infra/dotenv.js", () => ({ loadDotEnv: vi.fn() }));
|
describe("rewriteUpdateFlagArgv", () => {
|
||||||
vi.mock("../infra/env.js", () => ({ normalizeEnv: vi.fn() }));
|
it("leaves argv unchanged when --update is absent", () => {
|
||||||
vi.mock("../infra/path-env.js", () => ({ ensureClawdbotCliOnPath: vi.fn() }));
|
const argv = ["node", "entry.js", "status"];
|
||||||
vi.mock("../infra/runtime-guard.js", () => ({ assertSupportedRuntime: vi.fn() }));
|
expect(rewriteUpdateFlagArgv(argv)).toBe(argv);
|
||||||
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("registers the primary subcommand before parsing", async () => {
|
it("rewrites --update into the update command", () => {
|
||||||
const argv = ["/usr/bin/node-22", "/opt/clawdbot/entry.js", "gateway", "--port", "18789"];
|
expect(rewriteUpdateFlagArgv(["node", "entry.js", "--update"])).toEqual([
|
||||||
|
"node",
|
||||||
|
"entry.js",
|
||||||
|
"update",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
await runCli(argv);
|
it("preserves global flags that appear before --update", () => {
|
||||||
|
expect(rewriteUpdateFlagArgv(["node", "entry.js", "--profile", "p", "--update"])).toEqual([
|
||||||
|
"node",
|
||||||
|
"entry.js",
|
||||||
|
"--profile",
|
||||||
|
"p",
|
||||||
|
"update",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
expect(registerSubCliByName).toHaveBeenCalledTimes(1);
|
it("keeps update options after the rewritten command", () => {
|
||||||
expect(registerSubCliByName).toHaveBeenCalledWith(expect.any(Object), "gateway");
|
expect(rewriteUpdateFlagArgv(["node", "entry.js", "--update", "--json"])).toEqual([
|
||||||
expect(parseAsync).toHaveBeenCalledWith(argv);
|
"node",
|
||||||
|
"entry.js",
|
||||||
|
"update",
|
||||||
|
"--json",
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+7
-6
@@ -11,7 +11,7 @@ import { assertSupportedRuntime } from "../infra/runtime-guard.js";
|
|||||||
import { formatUncaughtError } from "../infra/errors.js";
|
import { formatUncaughtError } from "../infra/errors.js";
|
||||||
import { installUnhandledRejectionHandler } from "../infra/unhandled-rejections.js";
|
import { installUnhandledRejectionHandler } from "../infra/unhandled-rejections.js";
|
||||||
import { enableConsoleCapture } from "../logging.js";
|
import { enableConsoleCapture } from "../logging.js";
|
||||||
import { getPrimaryCommand } from "./argv.js";
|
import { getPrimaryCommand, hasHelpOrVersion } from "./argv.js";
|
||||||
import { tryRouteCli } from "./route.js";
|
import { tryRouteCli } from "./route.js";
|
||||||
|
|
||||||
export function rewriteUpdateFlagArgv(argv: string[]): string[] {
|
export function rewriteUpdateFlagArgv(argv: string[]): string[] {
|
||||||
@@ -50,11 +50,12 @@ export async function runCli(argv: string[] = process.argv) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const parseArgv = rewriteUpdateFlagArgv(normalizedArgv);
|
const parseArgv = rewriteUpdateFlagArgv(normalizedArgv);
|
||||||
// Register the primary subcommand if one exists (for lazy-loading)
|
if (hasHelpOrVersion(parseArgv)) {
|
||||||
const primary = getPrimaryCommand(parseArgv);
|
const primary = getPrimaryCommand(parseArgv);
|
||||||
if (primary) {
|
if (primary) {
|
||||||
const { registerSubCliByName } = await import("./program/register.subclis.js");
|
const { registerSubCliByName } = await import("./program/register.subclis.js");
|
||||||
await registerSubCliByName(program, primary);
|
await registerSubCliByName(program, primary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await program.parseAsync(parseArgv);
|
await program.parseAsync(parseArgv);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -353,7 +353,9 @@ export function applyVeniceProviderConfig(cfg: ClawdbotConfig): ClawdbotConfig {
|
|||||||
const veniceModels = VENICE_MODEL_CATALOG.map(buildVeniceModelDefinition);
|
const veniceModels = VENICE_MODEL_CATALOG.map(buildVeniceModelDefinition);
|
||||||
const mergedModels = [
|
const mergedModels = [
|
||||||
...existingModels,
|
...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<
|
const { apiKey: existingApiKey, ...existingProviderRest } = (existingProvider ?? {}) as Record<
|
||||||
string,
|
string,
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ export {
|
|||||||
SYNTHETIC_DEFAULT_MODEL_ID,
|
SYNTHETIC_DEFAULT_MODEL_ID,
|
||||||
SYNTHETIC_DEFAULT_MODEL_REF,
|
SYNTHETIC_DEFAULT_MODEL_REF,
|
||||||
} from "../agents/synthetic-models.js";
|
} 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 {
|
export {
|
||||||
applyAuthProfileConfig,
|
applyAuthProfileConfig,
|
||||||
applyKimiCodeConfig,
|
applyKimiCodeConfig,
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export async function startGatewayDiscovery(params: {
|
|||||||
gatewayTls?: { enabled: boolean; fingerprintSha256?: string };
|
gatewayTls?: { enabled: boolean; fingerprintSha256?: string };
|
||||||
canvasPort?: number;
|
canvasPort?: number;
|
||||||
wideAreaDiscoveryEnabled: boolean;
|
wideAreaDiscoveryEnabled: boolean;
|
||||||
tailscaleMode: "off" | "serve" | "funnel";
|
|
||||||
logDiscovery: { info: (msg: string) => void; warn: (msg: string) => void };
|
logDiscovery: { info: (msg: string) => void; warn: (msg: string) => void };
|
||||||
}) {
|
}) {
|
||||||
let bonjourStop: (() => Promise<void>) | null = null;
|
let bonjourStop: (() => Promise<void>) | null = null;
|
||||||
@@ -21,11 +20,8 @@ export async function startGatewayDiscovery(params: {
|
|||||||
process.env.CLAWDBOT_DISABLE_BONJOUR !== "1" &&
|
process.env.CLAWDBOT_DISABLE_BONJOUR !== "1" &&
|
||||||
process.env.NODE_ENV !== "test" &&
|
process.env.NODE_ENV !== "test" &&
|
||||||
!process.env.VITEST;
|
!process.env.VITEST;
|
||||||
const tailscaleEnabled = params.tailscaleMode !== "off";
|
|
||||||
const needsTailnetDns = bonjourEnabled || params.wideAreaDiscoveryEnabled;
|
const needsTailnetDns = bonjourEnabled || params.wideAreaDiscoveryEnabled;
|
||||||
const tailnetDns = needsTailnetDns
|
const tailnetDns = needsTailnetDns ? await resolveTailnetDnsHint() : undefined;
|
||||||
? await resolveTailnetDnsHint({ enabled: tailscaleEnabled })
|
|
||||||
: undefined;
|
|
||||||
const sshPortEnv = process.env.CLAWDBOT_SSH_PORT?.trim();
|
const sshPortEnv = process.env.CLAWDBOT_SSH_PORT?.trim();
|
||||||
const sshPortParsed = sshPortEnv ? Number.parseInt(sshPortEnv, 10) : NaN;
|
const sshPortParsed = sshPortEnv ? Number.parseInt(sshPortEnv, 10) : NaN;
|
||||||
const sshPort = Number.isFinite(sshPortParsed) && sshPortParsed > 0 ? sshPortParsed : undefined;
|
const sshPort = Number.isFinite(sshPortParsed) && sshPortParsed > 0 ? sshPortParsed : undefined;
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
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,13 +55,11 @@ export function resolveBonjourCliPath(opts: ResolveBonjourCliPathOptions = {}):
|
|||||||
export async function resolveTailnetDnsHint(opts?: {
|
export async function resolveTailnetDnsHint(opts?: {
|
||||||
env?: NodeJS.ProcessEnv;
|
env?: NodeJS.ProcessEnv;
|
||||||
exec?: typeof runExec;
|
exec?: typeof runExec;
|
||||||
enabled?: boolean;
|
|
||||||
}): Promise<string | undefined> {
|
}): Promise<string | undefined> {
|
||||||
const env = opts?.env ?? process.env;
|
const env = opts?.env ?? process.env;
|
||||||
const envRaw = env.CLAWDBOT_TAILNET_DNS?.trim();
|
const envRaw = env.CLAWDBOT_TAILNET_DNS?.trim();
|
||||||
const envValue = envRaw && envRaw.length > 0 ? envRaw.replace(/\.$/, "") : "";
|
const envValue = envRaw && envRaw.length > 0 ? envRaw.replace(/\.$/, "") : "";
|
||||||
if (envValue) return envValue;
|
if (envValue) return envValue;
|
||||||
if (opts?.enabled === false) return undefined;
|
|
||||||
|
|
||||||
const exec =
|
const exec =
|
||||||
opts?.exec ??
|
opts?.exec ??
|
||||||
|
|||||||
@@ -339,7 +339,6 @@ export async function startGatewayServer(
|
|||||||
? { enabled: true, fingerprintSha256: gatewayTls.fingerprintSha256 }
|
? { enabled: true, fingerprintSha256: gatewayTls.fingerprintSha256 }
|
||||||
: undefined,
|
: undefined,
|
||||||
wideAreaDiscoveryEnabled: cfgAtStart.discovery?.wideArea?.enabled === true,
|
wideAreaDiscoveryEnabled: cfgAtStart.discovery?.wideArea?.enabled === true,
|
||||||
tailscaleMode,
|
|
||||||
logDiscovery,
|
logDiscovery,
|
||||||
});
|
});
|
||||||
bonjourStop = discovery.bonjourStop;
|
bonjourStop = discovery.bonjourStop;
|
||||||
|
|||||||
@@ -134,7 +134,10 @@ export function connectGateway(host: GatewayHost) {
|
|||||||
},
|
},
|
||||||
onClose: ({ code, reason }) => {
|
onClose: ({ code, reason }) => {
|
||||||
host.connected = false;
|
host.connected = false;
|
||||||
host.lastError = `disconnected (${code}): ${reason || "no reason"}`;
|
// Code 1012 = Service Restart (expected during config saves, don't show as error)
|
||||||
|
if (code !== 1012) {
|
||||||
|
host.lastError = `disconnected (${code}): ${reason || "no reason"}`;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onEvent: (evt) => handleGatewayEvent(host, evt),
|
onEvent: (evt) => handleGatewayEvent(host, evt),
|
||||||
onGap: ({ expected, received }) => {
|
onGap: ({ expected, received }) => {
|
||||||
|
|||||||
@@ -234,8 +234,9 @@ export function renderConfig(props: ConfigProps) {
|
|||||||
const hasChanges = props.formMode === "form" ? diff.length > 0 : hasRawChanges;
|
const hasChanges = props.formMode === "form" ? diff.length > 0 : hasRawChanges;
|
||||||
|
|
||||||
// Save/apply buttons require actual changes to be enabled
|
// Save/apply buttons require actual changes to be enabled
|
||||||
|
// Note: formUnsafe warns about unsupported schema paths but shouldn't block saving
|
||||||
const canSaveForm =
|
const canSaveForm =
|
||||||
Boolean(props.formValue) && !props.loading && !formUnsafe;
|
Boolean(props.formValue) && !props.loading;
|
||||||
const canSave =
|
const canSave =
|
||||||
props.connected &&
|
props.connected &&
|
||||||
!props.saving &&
|
!props.saving &&
|
||||||
|
|||||||
Reference in new issue
Block a user