chore: migrate to oxlint and oxfmt
Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
This commit is contained in:
@@ -33,9 +33,7 @@ function collectReferencedAgentIds(cfg: ClawdbotConfig): string[] {
|
||||
|
||||
const agents = Array.isArray(cfg.agents?.list) ? cfg.agents?.list : [];
|
||||
const defaultAgentId =
|
||||
agents.find((agent) => agent?.default)?.id ??
|
||||
agents[0]?.id ??
|
||||
DEFAULT_AGENT_ID;
|
||||
agents.find((agent) => agent?.default)?.id ?? agents[0]?.id ?? DEFAULT_AGENT_ID;
|
||||
ids.add(normalizeAgentId(defaultAgentId));
|
||||
|
||||
for (const entry of agents) {
|
||||
@@ -62,15 +60,11 @@ function resolveEffectiveAgentDir(
|
||||
): string {
|
||||
const id = normalizeAgentId(agentId);
|
||||
const configured = Array.isArray(cfg.agents?.list)
|
||||
? cfg.agents?.list.find((agent) => normalizeAgentId(agent.id) === id)
|
||||
?.agentDir
|
||||
? cfg.agents?.list.find((agent) => normalizeAgentId(agent.id) === id)?.agentDir
|
||||
: undefined;
|
||||
const trimmed = configured?.trim();
|
||||
if (trimmed) return resolveUserPath(trimmed);
|
||||
const root = resolveStateDir(
|
||||
deps?.env ?? process.env,
|
||||
deps?.homedir ?? os.homedir,
|
||||
);
|
||||
const root = resolveStateDir(deps?.env ?? process.env, deps?.homedir ?? os.homedir);
|
||||
return path.join(root, "agents", id, "agent");
|
||||
}
|
||||
|
||||
@@ -94,17 +88,13 @@ export function findDuplicateAgentDirs(
|
||||
return [...byDir.values()].filter((v) => v.agentIds.length > 1);
|
||||
}
|
||||
|
||||
export function formatDuplicateAgentDirError(
|
||||
dups: DuplicateAgentDir[],
|
||||
): string {
|
||||
export function formatDuplicateAgentDirError(dups: DuplicateAgentDir[]): string {
|
||||
const lines: string[] = [
|
||||
"Duplicate agentDir detected (multi-agent config).",
|
||||
"Each agent must have a unique agentDir; sharing it causes auth/session state collisions and token invalidation.",
|
||||
"",
|
||||
"Conflicts:",
|
||||
...dups.map(
|
||||
(d) => `- ${d.agentDir}: ${d.agentIds.map((id) => `"${id}"`).join(", ")}`,
|
||||
),
|
||||
...dups.map((d) => `- ${d.agentDir}: ${d.agentIds.map((id) => `"${id}"`).join(", ")}`),
|
||||
"",
|
||||
"Fix: remove the shared agents.list[].agentDir override (or give each agent its own directory).",
|
||||
"If you want to share credentials, copy auth-profiles.json instead of sharing the entire agentDir.",
|
||||
|
||||
@@ -5,12 +5,8 @@ import type { ClawdbotConfig } from "./config.js";
|
||||
describe("resolveChannelCapabilities", () => {
|
||||
it("returns undefined for missing inputs", () => {
|
||||
expect(resolveChannelCapabilities({})).toBeUndefined();
|
||||
expect(
|
||||
resolveChannelCapabilities({ cfg: {} as ClawdbotConfig }),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
resolveChannelCapabilities({ cfg: {} as ClawdbotConfig, channel: "" }),
|
||||
).toBeUndefined();
|
||||
expect(resolveChannelCapabilities({ cfg: {} as ClawdbotConfig })).toBeUndefined();
|
||||
expect(resolveChannelCapabilities({ cfg: {} as ClawdbotConfig, channel: "" })).toBeUndefined();
|
||||
});
|
||||
|
||||
it("normalizes and prefers per-account capabilities", () => {
|
||||
|
||||
@@ -2,9 +2,7 @@ import { normalizeChannelId } from "../channels/registry.js";
|
||||
import { normalizeAccountId } from "../routing/session-key.js";
|
||||
import type { ClawdbotConfig } from "./config.js";
|
||||
|
||||
function normalizeCapabilities(
|
||||
capabilities: string[] | undefined,
|
||||
): string[] | undefined {
|
||||
function normalizeCapabilities(capabilities: string[] | undefined): string[] | undefined {
|
||||
if (!capabilities) return undefined;
|
||||
const normalized = capabilities.map((entry) => entry.trim()).filter(Boolean);
|
||||
return normalized.length > 0 ? normalized : undefined;
|
||||
@@ -24,20 +22,14 @@ function resolveAccountCapabilities(params: {
|
||||
if (accounts && typeof accounts === "object") {
|
||||
const direct = accounts[normalizedAccountId];
|
||||
if (direct) {
|
||||
return (
|
||||
normalizeCapabilities(direct.capabilities) ??
|
||||
normalizeCapabilities(cfg.capabilities)
|
||||
);
|
||||
return normalizeCapabilities(direct.capabilities) ?? normalizeCapabilities(cfg.capabilities);
|
||||
}
|
||||
const matchKey = Object.keys(accounts).find(
|
||||
(key) => key.toLowerCase() === normalizedAccountId.toLowerCase(),
|
||||
);
|
||||
const match = matchKey ? accounts[matchKey] : undefined;
|
||||
if (match) {
|
||||
return (
|
||||
normalizeCapabilities(match.capabilities) ??
|
||||
normalizeCapabilities(cfg.capabilities)
|
||||
);
|
||||
return normalizeCapabilities(match.capabilities) ?? normalizeCapabilities(cfg.capabilities);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,8 +46,7 @@ export function resolveChannelCapabilities(params: {
|
||||
if (!cfg || !channel) return undefined;
|
||||
|
||||
const channelsConfig = cfg.channels as Record<string, unknown> | undefined;
|
||||
const channelConfig = (channelsConfig?.[channel] ??
|
||||
(cfg as Record<string, unknown>)[channel]) as
|
||||
const channelConfig = (channelsConfig?.[channel] ?? (cfg as Record<string, unknown>)[channel]) as
|
||||
| {
|
||||
accounts?: Record<string, { capabilities?: string[] }>;
|
||||
capabilities?: string[];
|
||||
|
||||
@@ -16,8 +16,7 @@ export function resolveNativeCommandsEnabled(params: {
|
||||
globalSetting?: NativeCommandsSetting;
|
||||
}): boolean {
|
||||
const { providerId, providerSetting, globalSetting } = params;
|
||||
const setting =
|
||||
providerSetting === undefined ? globalSetting : providerSetting;
|
||||
const setting = providerSetting === undefined ? globalSetting : providerSetting;
|
||||
if (setting === true) return true;
|
||||
if (setting === false) return false;
|
||||
// auto or undefined -> heuristic
|
||||
|
||||
@@ -27,11 +27,7 @@ export function parseConfigPath(raw: string): {
|
||||
return { ok: true, path: parts };
|
||||
}
|
||||
|
||||
export function setConfigValueAtPath(
|
||||
root: PathNode,
|
||||
path: string[],
|
||||
value: unknown,
|
||||
): void {
|
||||
export function setConfigValueAtPath(root: PathNode, path: string[], value: unknown): void {
|
||||
let cursor: PathNode = root;
|
||||
for (let idx = 0; idx < path.length - 1; idx += 1) {
|
||||
const key = path[idx];
|
||||
@@ -44,10 +40,7 @@ export function setConfigValueAtPath(
|
||||
cursor[path[path.length - 1]] = value;
|
||||
}
|
||||
|
||||
export function unsetConfigValueAtPath(
|
||||
root: PathNode,
|
||||
path: string[],
|
||||
): boolean {
|
||||
export function unsetConfigValueAtPath(root: PathNode, path: string[]): boolean {
|
||||
const stack: Array<{ node: PathNode; key: string }> = [];
|
||||
let cursor: PathNode = root;
|
||||
for (let idx = 0; idx < path.length - 1; idx += 1) {
|
||||
|
||||
@@ -39,18 +39,10 @@ describe("config compaction settings", () => {
|
||||
|
||||
expect(cfg.agents?.defaults?.compaction?.reserveTokensFloor).toBe(12_345);
|
||||
expect(cfg.agents?.defaults?.compaction?.mode).toBe("safeguard");
|
||||
expect(cfg.agents?.defaults?.compaction?.memoryFlush?.enabled).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
cfg.agents?.defaults?.compaction?.memoryFlush?.softThresholdTokens,
|
||||
).toBe(1234);
|
||||
expect(cfg.agents?.defaults?.compaction?.memoryFlush?.prompt).toBe(
|
||||
"Write notes.",
|
||||
);
|
||||
expect(cfg.agents?.defaults?.compaction?.memoryFlush?.systemPrompt).toBe(
|
||||
"Flush memory now.",
|
||||
);
|
||||
expect(cfg.agents?.defaults?.compaction?.memoryFlush?.enabled).toBe(false);
|
||||
expect(cfg.agents?.defaults?.compaction?.memoryFlush?.softThresholdTokens).toBe(1234);
|
||||
expect(cfg.agents?.defaults?.compaction?.memoryFlush?.prompt).toBe("Write notes.");
|
||||
expect(cfg.agents?.defaults?.compaction?.memoryFlush?.systemPrompt).toBe("Flush memory now.");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,12 +57,8 @@ describe("config discord", () => {
|
||||
expect(cfg.channels?.discord?.enabled).toBe(true);
|
||||
expect(cfg.channels?.discord?.dm?.groupEnabled).toBe(true);
|
||||
expect(cfg.channels?.discord?.dm?.groupChannels).toEqual(["clawd-dm"]);
|
||||
expect(cfg.channels?.discord?.guilds?.["123"]?.slug).toBe(
|
||||
"friends-of-clawd",
|
||||
);
|
||||
expect(
|
||||
cfg.channels?.discord?.guilds?.["123"]?.channels?.general?.allow,
|
||||
).toBe(true);
|
||||
expect(cfg.channels?.discord?.guilds?.["123"]?.slug).toBe("friends-of-clawd");
|
||||
expect(cfg.channels?.discord?.guilds?.["123"]?.channels?.general?.allow).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,14 +44,11 @@ describe("config env vars", () => {
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
await withEnvOverride(
|
||||
{ OPENROUTER_API_KEY: "existing-key" },
|
||||
async () => {
|
||||
const { loadConfig } = await import("./config.js");
|
||||
loadConfig();
|
||||
expect(process.env.OPENROUTER_API_KEY).toBe("existing-key");
|
||||
},
|
||||
);
|
||||
await withEnvOverride({ OPENROUTER_API_KEY: "existing-key" }, async () => {
|
||||
const { loadConfig } = await import("./config.js");
|
||||
loadConfig();
|
||||
expect(process.env.OPENROUTER_API_KEY).toBe("existing-key");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -149,9 +149,7 @@ describe("config identity defaults", () => {
|
||||
const cfg = loadConfig();
|
||||
|
||||
expect(cfg.messages?.responsePrefix).toBe("✅");
|
||||
expect(cfg.agents?.list?.[0]?.groupChat?.mentionPatterns).toEqual([
|
||||
"@clawd",
|
||||
]);
|
||||
expect(cfg.agents?.list?.[0]?.groupChat?.mentionPatterns).toEqual(["@clawd"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -198,8 +196,7 @@ describe("config identity defaults", () => {
|
||||
expect(cfg.channels?.signal?.textChunkLimit).toBe(2222);
|
||||
expect(cfg.channels?.imessage?.textChunkLimit).toBe(1111);
|
||||
|
||||
const legacy = (cfg.messages as unknown as Record<string, unknown>)
|
||||
.textChunkLimit;
|
||||
const legacy = (cfg.messages as unknown as Record<string, unknown>).textChunkLimit;
|
||||
expect(legacy).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -249,9 +246,7 @@ describe("config identity defaults", () => {
|
||||
const { loadConfig } = await import("./config.js");
|
||||
const cfg = loadConfig();
|
||||
|
||||
expect(cfg.models?.providers?.minimax?.baseUrl).toBe(
|
||||
"https://api.minimax.io/anthropic",
|
||||
);
|
||||
expect(cfg.models?.providers?.minimax?.baseUrl).toBe("https://api.minimax.io/anthropic");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -69,9 +69,7 @@ describe("legacy config detection", () => {
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
expect(
|
||||
res.issues.some((i) => i.path === "channels.imessage.cliPath"),
|
||||
).toBe(true);
|
||||
expect(res.issues.some((i) => i.path === "channels.imessage.cliPath")).toBe(true);
|
||||
}
|
||||
});
|
||||
it("accepts tools audio transcription without cli", async () => {
|
||||
@@ -139,9 +137,7 @@ describe("legacy config detection", () => {
|
||||
expect(res.changes).toContain(
|
||||
'Moved telegram.requireMention → channels.telegram.groups."*".requireMention.',
|
||||
);
|
||||
expect(res.config?.channels?.telegram?.groups?.["*"]?.requireMention).toBe(
|
||||
false,
|
||||
);
|
||||
expect(res.config?.channels?.telegram?.groups?.["*"]?.requireMention).toBe(false);
|
||||
expect(res.config?.channels?.telegram?.requireMention).toBeUndefined();
|
||||
});
|
||||
it("migrates legacy model config to agent.models + model lists", async () => {
|
||||
@@ -158,24 +154,16 @@ describe("legacy config detection", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.config?.agents?.defaults?.model?.primary).toBe(
|
||||
"anthropic/claude-opus-4-5",
|
||||
);
|
||||
expect(res.config?.agents?.defaults?.model?.fallbacks).toEqual([
|
||||
"openai/gpt-4.1-mini",
|
||||
]);
|
||||
expect(res.config?.agents?.defaults?.imageModel?.primary).toBe(
|
||||
"openai/gpt-4.1-mini",
|
||||
);
|
||||
expect(res.config?.agents?.defaults?.model?.primary).toBe("anthropic/claude-opus-4-5");
|
||||
expect(res.config?.agents?.defaults?.model?.fallbacks).toEqual(["openai/gpt-4.1-mini"]);
|
||||
expect(res.config?.agents?.defaults?.imageModel?.primary).toBe("openai/gpt-4.1-mini");
|
||||
expect(res.config?.agents?.defaults?.imageModel?.fallbacks).toEqual([
|
||||
"anthropic/claude-opus-4-5",
|
||||
]);
|
||||
expect(
|
||||
res.config?.agents?.defaults?.models?.["anthropic/claude-opus-4-5"],
|
||||
).toMatchObject({ alias: "Opus" });
|
||||
expect(
|
||||
res.config?.agents?.defaults?.models?.["openai/gpt-4.1-mini"],
|
||||
).toBeTruthy();
|
||||
expect(res.config?.agents?.defaults?.models?.["anthropic/claude-opus-4-5"]).toMatchObject({
|
||||
alias: "Opus",
|
||||
});
|
||||
expect(res.config?.agents?.defaults?.models?.["openai/gpt-4.1-mini"]).toBeTruthy();
|
||||
expect(res.config?.agent).toBeUndefined();
|
||||
});
|
||||
it("auto-migrates legacy config in snapshot (no legacyIssues)", async () => {
|
||||
@@ -205,9 +193,7 @@ describe("legacy config detection", () => {
|
||||
expect(parsed.channels?.whatsapp?.allowFrom).toEqual(["+15555550123"]);
|
||||
expect(parsed.routing).toBeUndefined();
|
||||
expect(
|
||||
warnSpy.mock.calls.some(([msg]) =>
|
||||
String(msg).includes("Auto-migrated config"),
|
||||
),
|
||||
warnSpy.mock.calls.some(([msg]) => String(msg).includes("Auto-migrated config")),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
warnSpy.mockRestore();
|
||||
@@ -239,9 +225,7 @@ describe("legacy config detection", () => {
|
||||
expect(parsed.channels?.whatsapp?.allowFrom).toEqual(["+1555"]);
|
||||
expect(parsed.whatsapp).toBeUndefined();
|
||||
expect(
|
||||
warnSpy.mock.calls.some(([msg]) =>
|
||||
String(msg).includes("Auto-migrated config"),
|
||||
),
|
||||
warnSpy.mock.calls.some(([msg]) => String(msg).includes("Auto-migrated config")),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
warnSpy.mockRestore();
|
||||
@@ -307,9 +291,7 @@ describe("legacy config detection", () => {
|
||||
expect(parsed.bindings?.[0]?.match?.channel).toBe("slack");
|
||||
expect(parsed.bindings?.[0]?.match?.provider).toBeUndefined();
|
||||
expect(
|
||||
warnSpy.mock.calls.some(([msg]) =>
|
||||
String(msg).includes("Auto-migrated config"),
|
||||
),
|
||||
warnSpy.mock.calls.some(([msg]) => String(msg).includes("Auto-migrated config")),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
warnSpy.mockRestore();
|
||||
@@ -341,9 +323,7 @@ describe("legacy config detection", () => {
|
||||
try {
|
||||
const { loadConfig } = await import("./config.js");
|
||||
const cfg = loadConfig();
|
||||
expect(cfg.session?.sendPolicy?.rules?.[0]?.match?.channel).toBe(
|
||||
"telegram",
|
||||
);
|
||||
expect(cfg.session?.sendPolicy?.rules?.[0]?.match?.channel).toBe("telegram");
|
||||
|
||||
const raw = await fs.readFile(configPath, "utf-8");
|
||||
const parsed = JSON.parse(raw) as {
|
||||
@@ -355,16 +335,10 @@ describe("legacy config detection", () => {
|
||||
};
|
||||
};
|
||||
};
|
||||
expect(parsed.session?.sendPolicy?.rules?.[0]?.match?.channel).toBe(
|
||||
"telegram",
|
||||
);
|
||||
expect(parsed.session?.sendPolicy?.rules?.[0]?.match?.channel).toBe("telegram");
|
||||
expect(parsed.session?.sendPolicy?.rules?.[0]?.match?.provider).toBeUndefined();
|
||||
expect(
|
||||
parsed.session?.sendPolicy?.rules?.[0]?.match?.provider,
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
warnSpy.mock.calls.some(([msg]) =>
|
||||
String(msg).includes("Auto-migrated config"),
|
||||
),
|
||||
warnSpy.mock.calls.some(([msg]) => String(msg).includes("Auto-migrated config")),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
warnSpy.mockRestore();
|
||||
@@ -377,11 +351,7 @@ describe("legacy config detection", () => {
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
JSON.stringify(
|
||||
{ messages: { queue: { byProvider: { whatsapp: "queue" } } } },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
JSON.stringify({ messages: { queue: { byProvider: { whatsapp: "queue" } } } }, null, 2),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
@@ -404,9 +374,7 @@ describe("legacy config detection", () => {
|
||||
expect(parsed.messages?.queue?.byChannel?.whatsapp).toBe("queue");
|
||||
expect(parsed.messages?.queue?.byProvider).toBeUndefined();
|
||||
expect(
|
||||
warnSpy.mock.calls.some(([msg]) =>
|
||||
String(msg).includes("Auto-migrated config"),
|
||||
),
|
||||
warnSpy.mock.calls.some(([msg]) => String(msg).includes("Auto-migrated config")),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
warnSpy.mockRestore();
|
||||
|
||||
@@ -29,9 +29,7 @@ describe("legacy config detection", () => {
|
||||
const res = migrateLegacyConfig({
|
||||
routing: { allowFrom: ["+15555550123"] },
|
||||
});
|
||||
expect(res.changes).toContain(
|
||||
"Moved routing.allowFrom → channels.whatsapp.allowFrom.",
|
||||
);
|
||||
expect(res.changes).toContain("Moved routing.allowFrom → channels.whatsapp.allowFrom.");
|
||||
expect(res.config?.channels?.whatsapp?.allowFrom).toEqual(["+15555550123"]);
|
||||
expect(res.config?.routing?.allowFrom).toBeUndefined();
|
||||
});
|
||||
@@ -50,15 +48,9 @@ describe("legacy config detection", () => {
|
||||
expect(res.changes).toContain(
|
||||
'Moved routing.groupChat.requireMention → channels.imessage.groups."*".requireMention.',
|
||||
);
|
||||
expect(res.config?.channels?.whatsapp?.groups?.["*"]?.requireMention).toBe(
|
||||
false,
|
||||
);
|
||||
expect(res.config?.channels?.telegram?.groups?.["*"]?.requireMention).toBe(
|
||||
false,
|
||||
);
|
||||
expect(res.config?.channels?.imessage?.groups?.["*"]?.requireMention).toBe(
|
||||
false,
|
||||
);
|
||||
expect(res.config?.channels?.whatsapp?.groups?.["*"]?.requireMention).toBe(false);
|
||||
expect(res.config?.channels?.telegram?.groups?.["*"]?.requireMention).toBe(false);
|
||||
expect(res.config?.channels?.imessage?.groups?.["*"]?.requireMention).toBe(false);
|
||||
expect(res.config?.routing?.groupChat?.requireMention).toBeUndefined();
|
||||
});
|
||||
it("migrates routing.groupChat.mentionPatterns to messages.groupChat.mentionPatterns", async () => {
|
||||
@@ -70,9 +62,7 @@ describe("legacy config detection", () => {
|
||||
expect(res.changes).toContain(
|
||||
"Moved routing.groupChat.mentionPatterns → messages.groupChat.mentionPatterns.",
|
||||
);
|
||||
expect(res.config?.messages?.groupChat?.mentionPatterns).toEqual([
|
||||
"@clawd",
|
||||
]);
|
||||
expect(res.config?.messages?.groupChat?.mentionPatterns).toEqual(["@clawd"]);
|
||||
expect(res.config?.routing?.groupChat?.mentionPatterns).toBeUndefined();
|
||||
});
|
||||
it("migrates routing agentToAgent/queue/transcribeAudio to tools/messages/audio", async () => {
|
||||
@@ -88,13 +78,9 @@ describe("legacy config detection", () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(res.changes).toContain(
|
||||
"Moved routing.agentToAgent → tools.agentToAgent.",
|
||||
);
|
||||
expect(res.changes).toContain("Moved routing.agentToAgent → tools.agentToAgent.");
|
||||
expect(res.changes).toContain("Moved routing.queue → messages.queue.");
|
||||
expect(res.changes).toContain(
|
||||
"Moved routing.transcribeAudio → tools.audio.transcription.",
|
||||
);
|
||||
expect(res.changes).toContain("Moved routing.transcribeAudio → tools.audio.transcription.");
|
||||
expect(res.config?.tools?.agentToAgent).toEqual({
|
||||
enabled: true,
|
||||
allow: ["main"],
|
||||
@@ -126,12 +112,8 @@ describe("legacy config detection", () => {
|
||||
expect(res.changes).toContain("Moved agent.tools.deny → tools.deny.");
|
||||
expect(res.changes).toContain("Moved agent.elevated → tools.elevated.");
|
||||
expect(res.changes).toContain("Moved agent.bash → tools.exec.");
|
||||
expect(res.changes).toContain(
|
||||
"Moved agent.sandbox.tools → tools.sandbox.tools.",
|
||||
);
|
||||
expect(res.changes).toContain(
|
||||
"Moved agent.subagents.tools → tools.subagents.tools.",
|
||||
);
|
||||
expect(res.changes).toContain("Moved agent.sandbox.tools → tools.sandbox.tools.");
|
||||
expect(res.changes).toContain("Moved agent.subagents.tools → tools.subagents.tools.");
|
||||
expect(res.changes).toContain("Moved agent → agents.defaults.");
|
||||
expect(res.config?.agents?.defaults?.model).toEqual({
|
||||
primary: "openai/gpt-5.2",
|
||||
@@ -192,9 +174,7 @@ describe("legacy config detection", () => {
|
||||
});
|
||||
expect(res.ok).toBe(false);
|
||||
if (!res.ok) {
|
||||
expect(
|
||||
res.issues.some((issue) => issue.path === "telegram.requireMention"),
|
||||
).toBe(true);
|
||||
expect(res.issues.some((issue) => issue.path === "telegram.requireMention")).toBe(true);
|
||||
}
|
||||
});
|
||||
it("rejects gateway.token", async () => {
|
||||
@@ -226,12 +206,8 @@ describe("legacy config detection", () => {
|
||||
gateway: { bind: "tailnet" as const },
|
||||
bridge: { bind: "tailnet" as const },
|
||||
});
|
||||
expect(res.changes).toContain(
|
||||
"Migrated gateway.bind from 'tailnet' to 'auto'.",
|
||||
);
|
||||
expect(res.changes).toContain(
|
||||
"Migrated bridge.bind from 'tailnet' to 'auto'.",
|
||||
);
|
||||
expect(res.changes).toContain("Migrated gateway.bind from 'tailnet' to 'auto'.");
|
||||
expect(res.changes).toContain("Migrated bridge.bind from 'tailnet' to 'auto'.");
|
||||
expect(res.config?.gateway?.bind).toBe("auto");
|
||||
expect(res.config?.bridge?.bind).toBe("auto");
|
||||
});
|
||||
@@ -384,13 +360,9 @@ describe("legacy config detection", () => {
|
||||
expect(res.ok).toBe(true);
|
||||
if (res.ok) {
|
||||
expect(res.config.channels?.whatsapp?.historyLimit).toBe(9);
|
||||
expect(res.config.channels?.whatsapp?.accounts?.work?.historyLimit).toBe(
|
||||
4,
|
||||
);
|
||||
expect(res.config.channels?.whatsapp?.accounts?.work?.historyLimit).toBe(4);
|
||||
expect(res.config.channels?.telegram?.historyLimit).toBe(8);
|
||||
expect(res.config.channels?.telegram?.accounts?.ops?.historyLimit).toBe(
|
||||
3,
|
||||
);
|
||||
expect(res.config.channels?.telegram?.accounts?.ops?.historyLimit).toBe(3);
|
||||
expect(res.config.channels?.slack?.historyLimit).toBe(7);
|
||||
expect(res.config.channels?.slack?.accounts?.ops?.historyLimit).toBe(2);
|
||||
expect(res.config.channels?.signal?.historyLimit).toBe(6);
|
||||
|
||||
@@ -22,13 +22,10 @@ describe("config msteams", () => {
|
||||
expect(res.ok).toBe(true);
|
||||
if (res.ok) {
|
||||
expect(res.config.channels?.msteams?.replyStyle).toBe("top-level");
|
||||
expect(res.config.channels?.msteams?.teams?.team123?.replyStyle).toBe(
|
||||
"thread",
|
||||
expect(res.config.channels?.msteams?.teams?.team123?.replyStyle).toBe("thread");
|
||||
expect(res.config.channels?.msteams?.teams?.team123?.channels?.chan456?.replyStyle).toBe(
|
||||
"top-level",
|
||||
);
|
||||
expect(
|
||||
res.config.channels?.msteams?.teams?.team123?.channels?.chan456
|
||||
?.replyStyle,
|
||||
).toBe("top-level");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -43,13 +43,10 @@ describe("Nix integration (U3, U5, U9)", () => {
|
||||
});
|
||||
|
||||
it("STATE_DIR_CLAWDBOT respects CLAWDBOT_STATE_DIR override", async () => {
|
||||
await withEnvOverride(
|
||||
{ CLAWDBOT_STATE_DIR: "/custom/state/dir" },
|
||||
async () => {
|
||||
const { STATE_DIR_CLAWDBOT } = await import("./config.js");
|
||||
expect(STATE_DIR_CLAWDBOT).toBe(path.resolve("/custom/state/dir"));
|
||||
},
|
||||
);
|
||||
await withEnvOverride({ CLAWDBOT_STATE_DIR: "/custom/state/dir" }, async () => {
|
||||
const { STATE_DIR_CLAWDBOT } = await import("./config.js");
|
||||
expect(STATE_DIR_CLAWDBOT).toBe(path.resolve("/custom/state/dir"));
|
||||
});
|
||||
});
|
||||
|
||||
it("CONFIG_PATH_CLAWDBOT defaults to ~/.clawdbot/clawdbot.json when env not set", async () => {
|
||||
@@ -57,36 +54,24 @@ describe("Nix integration (U3, U5, U9)", () => {
|
||||
{ CLAWDBOT_CONFIG_PATH: undefined, CLAWDBOT_STATE_DIR: undefined },
|
||||
async () => {
|
||||
const { CONFIG_PATH_CLAWDBOT } = await import("./config.js");
|
||||
expect(CONFIG_PATH_CLAWDBOT).toMatch(
|
||||
/\.clawdbot[\\/]clawdbot\.json$/,
|
||||
);
|
||||
expect(CONFIG_PATH_CLAWDBOT).toMatch(/\.clawdbot[\\/]clawdbot\.json$/);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("CONFIG_PATH_CLAWDBOT respects CLAWDBOT_CONFIG_PATH override", async () => {
|
||||
await withEnvOverride(
|
||||
{ CLAWDBOT_CONFIG_PATH: "/nix/store/abc/clawdbot.json" },
|
||||
async () => {
|
||||
const { CONFIG_PATH_CLAWDBOT } = await import("./config.js");
|
||||
expect(CONFIG_PATH_CLAWDBOT).toBe(
|
||||
path.resolve("/nix/store/abc/clawdbot.json"),
|
||||
);
|
||||
},
|
||||
);
|
||||
await withEnvOverride({ CLAWDBOT_CONFIG_PATH: "/nix/store/abc/clawdbot.json" }, async () => {
|
||||
const { CONFIG_PATH_CLAWDBOT } = await import("./config.js");
|
||||
expect(CONFIG_PATH_CLAWDBOT).toBe(path.resolve("/nix/store/abc/clawdbot.json"));
|
||||
});
|
||||
});
|
||||
|
||||
it("CONFIG_PATH_CLAWDBOT expands ~ in CLAWDBOT_CONFIG_PATH override", async () => {
|
||||
await withTempHome(async (home) => {
|
||||
await withEnvOverride(
|
||||
{ CLAWDBOT_CONFIG_PATH: "~/.clawdbot/custom.json" },
|
||||
async () => {
|
||||
const { CONFIG_PATH_CLAWDBOT } = await import("./config.js");
|
||||
expect(CONFIG_PATH_CLAWDBOT).toBe(
|
||||
path.join(home, ".clawdbot", "custom.json"),
|
||||
);
|
||||
},
|
||||
);
|
||||
await withEnvOverride({ CLAWDBOT_CONFIG_PATH: "~/.clawdbot/custom.json" }, async () => {
|
||||
const { CONFIG_PATH_CLAWDBOT } = await import("./config.js");
|
||||
expect(CONFIG_PATH_CLAWDBOT).toBe(path.join(home, ".clawdbot", "custom.json"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -151,21 +136,13 @@ describe("Nix integration (U3, U5, U9)", () => {
|
||||
const { loadConfig } = await import("./config.js");
|
||||
const cfg = loadConfig();
|
||||
|
||||
expect(cfg.plugins?.load?.paths?.[0]).toBe(
|
||||
path.join(home, "plugins", "demo-plugin"),
|
||||
);
|
||||
expect(cfg.agents?.defaults?.workspace).toBe(
|
||||
path.join(home, "ws-default"),
|
||||
);
|
||||
expect(cfg.agents?.list?.[0]?.workspace).toBe(
|
||||
path.join(home, "ws-agent"),
|
||||
);
|
||||
expect(cfg.plugins?.load?.paths?.[0]).toBe(path.join(home, "plugins", "demo-plugin"));
|
||||
expect(cfg.agents?.defaults?.workspace).toBe(path.join(home, "ws-default"));
|
||||
expect(cfg.agents?.list?.[0]?.workspace).toBe(path.join(home, "ws-agent"));
|
||||
expect(cfg.agents?.list?.[0]?.agentDir).toBe(
|
||||
path.join(home, ".clawdbot", "agents", "main"),
|
||||
);
|
||||
expect(cfg.agents?.list?.[0]?.sandbox?.workspaceRoot).toBe(
|
||||
path.join(home, "sandbox-root"),
|
||||
);
|
||||
expect(cfg.agents?.list?.[0]?.sandbox?.workspaceRoot).toBe(path.join(home, "sandbox-root"));
|
||||
expect(cfg.channels?.whatsapp?.accounts?.personal?.authDir).toBe(
|
||||
path.join(home, ".clawdbot", "credentials", "wa-personal"),
|
||||
);
|
||||
@@ -176,9 +153,7 @@ describe("Nix integration (U3, U5, U9)", () => {
|
||||
describe("U6: gateway port resolution", () => {
|
||||
it("uses default when env and config are unset", async () => {
|
||||
await withEnvOverride({ CLAWDBOT_GATEWAY_PORT: undefined }, async () => {
|
||||
const { DEFAULT_GATEWAY_PORT, resolveGatewayPort } = await import(
|
||||
"./config.js"
|
||||
);
|
||||
const { DEFAULT_GATEWAY_PORT, resolveGatewayPort } = await import("./config.js");
|
||||
expect(resolveGatewayPort({})).toBe(DEFAULT_GATEWAY_PORT);
|
||||
});
|
||||
});
|
||||
@@ -234,9 +209,7 @@ describe("Nix integration (U3, U5, U9)", () => {
|
||||
vi.resetModules();
|
||||
const { loadConfig } = await import("./config.js");
|
||||
const cfg = loadConfig();
|
||||
expect(cfg.channels?.telegram?.tokenFile).toBe(
|
||||
"/run/agenix/telegram-token",
|
||||
);
|
||||
expect(cfg.channels?.telegram?.tokenFile).toBe("/run/agenix/telegram-token");
|
||||
expect(cfg.channels?.telegram?.botToken).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -262,9 +235,7 @@ describe("Nix integration (U3, U5, U9)", () => {
|
||||
const { loadConfig } = await import("./config.js");
|
||||
const cfg = loadConfig();
|
||||
expect(cfg.channels?.telegram?.botToken).toBe("fallback:token");
|
||||
expect(cfg.channels?.telegram?.tokenFile).toBe(
|
||||
"/run/agenix/telegram-token",
|
||||
);
|
||||
expect(cfg.channels?.telegram?.tokenFile).toBe("/run/agenix/telegram-token");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,9 +12,7 @@ describe("config preservation on validation failure", () => {
|
||||
customUnknownField: { nested: "value" },
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
expect(
|
||||
(res as { config: Record<string, unknown> }).config.customUnknownField,
|
||||
).toEqual({
|
||||
expect((res as { config: Record<string, unknown> }).config.customUnknownField).toEqual({
|
||||
nested: "value",
|
||||
});
|
||||
});
|
||||
@@ -42,9 +40,7 @@ describe("config preservation on validation failure", () => {
|
||||
expect((snap.config as Record<string, unknown>).customData).toEqual({
|
||||
preserved: true,
|
||||
});
|
||||
expect(snap.config.channels?.whatsapp?.allowFrom).toEqual([
|
||||
"+15555550123",
|
||||
]);
|
||||
expect(snap.config.channels?.whatsapp?.allowFrom).toEqual(["+15555550123"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,11 +28,7 @@ describe("config pruning defaults", () => {
|
||||
await fs.mkdir(configDir, { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(configDir, "clawdbot.json"),
|
||||
JSON.stringify(
|
||||
{ agents: { defaults: { contextPruning: { mode: "off" } } } },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
JSON.stringify({ agents: { defaults: { contextPruning: { mode: "off" } } } }, null, 2),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
|
||||
@@ -118,9 +118,7 @@ export function applyLoggingDefaults(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||
};
|
||||
}
|
||||
|
||||
export function applyContextPruningDefaults(
|
||||
cfg: ClawdbotConfig,
|
||||
): ClawdbotConfig {
|
||||
export function applyContextPruningDefaults(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||
const defaults = cfg.agents?.defaults;
|
||||
if (!defaults) return cfg;
|
||||
const contextPruning = defaults?.contextPruning;
|
||||
|
||||
@@ -51,14 +51,11 @@ export function resolveChannelGroupPolicy(params: {
|
||||
const normalizedId = params.groupId?.trim();
|
||||
const groupConfig = normalizedId && groups ? groups[normalizedId] : undefined;
|
||||
const defaultConfig = groups?.["*"];
|
||||
const allowAll =
|
||||
allowlistEnabled && Boolean(groups && Object.hasOwn(groups, "*"));
|
||||
const allowAll = allowlistEnabled && Boolean(groups && Object.hasOwn(groups, "*"));
|
||||
const allowed =
|
||||
!allowlistEnabled ||
|
||||
allowAll ||
|
||||
(normalizedId
|
||||
? Boolean(groups && Object.hasOwn(groups, normalizedId))
|
||||
: false);
|
||||
(normalizedId ? Boolean(groups && Object.hasOwn(groups, normalizedId)) : false);
|
||||
return {
|
||||
allowlistEnabled,
|
||||
allowed,
|
||||
@@ -84,17 +81,11 @@ export function resolveChannelGroupRequireMention(params: {
|
||||
? defaultConfig.requireMention
|
||||
: undefined;
|
||||
|
||||
if (
|
||||
overrideOrder === "before-config" &&
|
||||
typeof requireMentionOverride === "boolean"
|
||||
) {
|
||||
if (overrideOrder === "before-config" && typeof requireMentionOverride === "boolean") {
|
||||
return requireMentionOverride;
|
||||
}
|
||||
if (typeof configMention === "boolean") return configMention;
|
||||
if (
|
||||
overrideOrder !== "before-config" &&
|
||||
typeof requireMentionOverride === "boolean"
|
||||
) {
|
||||
if (overrideOrder !== "before-config" && typeof requireMentionOverride === "boolean") {
|
||||
return requireMentionOverride;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -40,11 +40,7 @@ function createMockResolver(files: Record<string, unknown>): IncludeResolver {
|
||||
};
|
||||
}
|
||||
|
||||
function resolve(
|
||||
obj: unknown,
|
||||
files: Record<string, unknown> = {},
|
||||
basePath = DEFAULT_BASE_PATH,
|
||||
) {
|
||||
function resolve(obj: unknown, files: Record<string, unknown> = {}, basePath = DEFAULT_BASE_PATH) {
|
||||
return resolveConfigIncludes(obj, basePath, createMockResolver(files));
|
||||
}
|
||||
|
||||
@@ -163,12 +159,12 @@ describe("resolveConfigIncludes", () => {
|
||||
parseJson: JSON.parse,
|
||||
};
|
||||
const obj = { $include: "./bad.json" };
|
||||
expect(() =>
|
||||
resolveConfigIncludes(obj, DEFAULT_BASE_PATH, resolver),
|
||||
).toThrow(ConfigIncludeError);
|
||||
expect(() =>
|
||||
resolveConfigIncludes(obj, DEFAULT_BASE_PATH, resolver),
|
||||
).toThrow(/Failed to parse include file/);
|
||||
expect(() => resolveConfigIncludes(obj, DEFAULT_BASE_PATH, resolver)).toThrow(
|
||||
ConfigIncludeError,
|
||||
);
|
||||
expect(() => resolveConfigIncludes(obj, DEFAULT_BASE_PATH, resolver)).toThrow(
|
||||
/Failed to parse include file/,
|
||||
);
|
||||
});
|
||||
|
||||
it("throws CircularIncludeError for circular includes", () => {
|
||||
@@ -193,9 +189,7 @@ describe("resolveConfigIncludes", () => {
|
||||
} catch (err) {
|
||||
expect(err).toBeInstanceOf(CircularIncludeError);
|
||||
const circular = err as CircularIncludeError;
|
||||
expect(circular.chain).toEqual(
|
||||
expect.arrayContaining([DEFAULT_BASE_PATH, aPath, bPath]),
|
||||
);
|
||||
expect(circular.chain).toEqual(expect.arrayContaining([DEFAULT_BASE_PATH, aPath, bPath]));
|
||||
expect(circular.message).toMatch(/Circular include detected/);
|
||||
expect(circular.message).toContain("a.json");
|
||||
expect(circular.message).toContain("b.json");
|
||||
@@ -261,12 +255,8 @@ describe("resolveConfigIncludes", () => {
|
||||
};
|
||||
}
|
||||
failFiles[configPath("fail10.json")] = { done: true };
|
||||
expect(() => resolve({ $include: "./fail0.json" }, failFiles)).toThrow(
|
||||
ConfigIncludeError,
|
||||
);
|
||||
expect(() => resolve({ $include: "./fail0.json" }, failFiles)).toThrow(
|
||||
/Maximum include depth/,
|
||||
);
|
||||
expect(() => resolve({ $include: "./fail0.json" }, failFiles)).toThrow(ConfigIncludeError);
|
||||
expect(() => resolve({ $include: "./fail0.json" }, failFiles)).toThrow(/Maximum include depth/);
|
||||
});
|
||||
|
||||
it("handles relative paths correctly", () => {
|
||||
@@ -361,11 +351,7 @@ describe("real-world config patterns", () => {
|
||||
};
|
||||
|
||||
const obj = {
|
||||
$include: [
|
||||
"./gateway.json",
|
||||
"./channels/whatsapp.json",
|
||||
"./agents/defaults.json",
|
||||
],
|
||||
$include: ["./gateway.json", "./channels/whatsapp.json", "./agents/defaults.json"],
|
||||
};
|
||||
|
||||
expect(resolve(obj, files)).toEqual({
|
||||
|
||||
@@ -44,10 +44,7 @@ export class ConfigIncludeError extends Error {
|
||||
|
||||
export class CircularIncludeError extends ConfigIncludeError {
|
||||
constructor(public readonly chain: string[]) {
|
||||
super(
|
||||
`Circular include detected: ${chain.join(" -> ")}`,
|
||||
chain[chain.length - 1],
|
||||
);
|
||||
super(`Circular include detected: ${chain.join(" -> ")}`, chain[chain.length - 1]);
|
||||
this.name = "CircularIncludeError";
|
||||
}
|
||||
}
|
||||
@@ -73,8 +70,7 @@ export function deepMerge(target: unknown, source: unknown): unknown {
|
||||
if (isPlainObject(target) && isPlainObject(source)) {
|
||||
const result: Record<string, unknown> = { ...target };
|
||||
for (const key of Object.keys(source)) {
|
||||
result[key] =
|
||||
key in result ? deepMerge(result[key], source[key]) : source[key];
|
||||
result[key] = key in result ? deepMerge(result[key], source[key]) : source[key];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -213,11 +209,7 @@ class IncludeProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
private parseFile(
|
||||
includePath: string,
|
||||
resolvedPath: string,
|
||||
raw: string,
|
||||
): unknown {
|
||||
private parseFile(includePath: string, resolvedPath: string, raw: string): unknown {
|
||||
try {
|
||||
return this.resolver.parseJson(raw);
|
||||
} catch (err) {
|
||||
|
||||
@@ -10,10 +10,7 @@ import {
|
||||
resolveShellEnvFallbackTimeoutMs,
|
||||
shouldEnableShellEnvFallback,
|
||||
} from "../infra/shell-env.js";
|
||||
import {
|
||||
DuplicateAgentDirError,
|
||||
findDuplicateAgentDirs,
|
||||
} from "./agent-dirs.js";
|
||||
import { DuplicateAgentDirError, findDuplicateAgentDirs } from "./agent-dirs.js";
|
||||
import {
|
||||
applyContextPruningDefaults,
|
||||
applyLoggingDefaults,
|
||||
@@ -27,19 +24,12 @@ import { applyLegacyMigrations, findLegacyConfigIssues } from "./legacy.js";
|
||||
import { normalizeConfigPaths } from "./normalize-paths.js";
|
||||
import { resolveConfigPath, resolveStateDir } from "./paths.js";
|
||||
import { applyConfigOverrides } from "./runtime-overrides.js";
|
||||
import type {
|
||||
ClawdbotConfig,
|
||||
ConfigFileSnapshot,
|
||||
LegacyConfigIssue,
|
||||
} from "./types.js";
|
||||
import type { ClawdbotConfig, ConfigFileSnapshot, LegacyConfigIssue } from "./types.js";
|
||||
import { validateConfigObject } from "./validation.js";
|
||||
import { ClawdbotSchema } from "./zod-schema.js";
|
||||
|
||||
// Re-export for backwards compatibility
|
||||
export {
|
||||
CircularIncludeError,
|
||||
ConfigIncludeError,
|
||||
} from "./includes.js";
|
||||
export { CircularIncludeError, ConfigIncludeError } from "./includes.js";
|
||||
|
||||
const SHELL_ENV_EXPECTED_KEYS = [
|
||||
"OPENAI_API_KEY",
|
||||
@@ -59,9 +49,7 @@ const SHELL_ENV_EXPECTED_KEYS = [
|
||||
"CLAWDBOT_GATEWAY_PASSWORD",
|
||||
];
|
||||
|
||||
export type ParseConfigJson5Result =
|
||||
| { ok: true; parsed: unknown }
|
||||
| { ok: false; error: string };
|
||||
export type ParseConfigJson5Result = { ok: true; parsed: unknown } | { ok: false; error: string };
|
||||
|
||||
export type ConfigIoDeps = {
|
||||
fs?: typeof fs;
|
||||
@@ -72,10 +60,7 @@ export type ConfigIoDeps = {
|
||||
logger?: Pick<typeof console, "error" | "warn">;
|
||||
};
|
||||
|
||||
function warnOnConfigMiskeys(
|
||||
raw: unknown,
|
||||
logger: Pick<typeof console, "warn">,
|
||||
): void {
|
||||
function warnOnConfigMiskeys(raw: unknown, logger: Pick<typeof console, "warn">): void {
|
||||
if (!raw || typeof raw !== "object") return;
|
||||
const gateway = (raw as Record<string, unknown>).gateway;
|
||||
if (!gateway || typeof gateway !== "object") return;
|
||||
@@ -149,9 +134,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
const writeConfigFileSync = (cfg: ClawdbotConfig) => {
|
||||
const dir = path.dirname(configPath);
|
||||
deps.fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||
const json = JSON.stringify(applyModelDefaults(cfg), null, 2)
|
||||
.trimEnd()
|
||||
.concat("\n");
|
||||
const json = JSON.stringify(applyModelDefaults(cfg), null, 2).trimEnd().concat("\n");
|
||||
|
||||
const tmp = path.join(
|
||||
dir,
|
||||
@@ -219,8 +202,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
const migrated = applyLegacyMigrations(resolved);
|
||||
const resolvedConfig = migrated.next ?? resolved;
|
||||
warnOnConfigMiskeys(resolvedConfig, deps.logger);
|
||||
if (typeof resolvedConfig !== "object" || resolvedConfig === null)
|
||||
return {};
|
||||
if (typeof resolvedConfig !== "object" || resolvedConfig === null) return {};
|
||||
const validated = ClawdbotSchema.safeParse(resolvedConfig);
|
||||
if (!validated.success) {
|
||||
deps.logger.error("Invalid config:");
|
||||
@@ -234,17 +216,13 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
try {
|
||||
writeConfigFileSync(resolvedConfig as ClawdbotConfig);
|
||||
} catch (err) {
|
||||
deps.logger.warn(
|
||||
`Failed to write migrated config at ${configPath}: ${String(err)}`,
|
||||
);
|
||||
deps.logger.warn(`Failed to write migrated config at ${configPath}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
const cfg = applyModelDefaults(
|
||||
applyContextPruningDefaults(
|
||||
applySessionDefaults(
|
||||
applyLoggingDefaults(
|
||||
applyMessageDefaults(validated.data as ClawdbotConfig),
|
||||
),
|
||||
applyLoggingDefaults(applyMessageDefaults(validated.data as ClawdbotConfig)),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -260,18 +238,14 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
|
||||
applyConfigEnv(cfg, deps.env);
|
||||
|
||||
const enabled =
|
||||
shouldEnableShellEnvFallback(deps.env) ||
|
||||
cfg.env?.shellEnv?.enabled === true;
|
||||
const enabled = shouldEnableShellEnvFallback(deps.env) || cfg.env?.shellEnv?.enabled === true;
|
||||
if (enabled) {
|
||||
loadShellEnvFallback({
|
||||
enabled: true,
|
||||
env: deps.env,
|
||||
expectedKeys: SHELL_ENV_EXPECTED_KEYS,
|
||||
logger: deps.logger,
|
||||
timeoutMs:
|
||||
cfg.env?.shellEnv?.timeoutMs ??
|
||||
resolveShellEnvFallbackTimeoutMs(deps.env),
|
||||
timeoutMs: cfg.env?.shellEnv?.timeoutMs ?? resolveShellEnvFallbackTimeoutMs(deps.env),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -291,9 +265,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
if (!exists) {
|
||||
const config = applyTalkApiKey(
|
||||
applyModelDefaults(
|
||||
applyContextPruningDefaults(
|
||||
applySessionDefaults(applyMessageDefaults({})),
|
||||
),
|
||||
applyContextPruningDefaults(applySessionDefaults(applyMessageDefaults({}))),
|
||||
),
|
||||
);
|
||||
const legacyIssues: LegacyConfigIssue[] = [];
|
||||
@@ -320,9 +292,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
parsed: {},
|
||||
valid: false,
|
||||
config: {},
|
||||
issues: [
|
||||
{ path: "", message: `JSON5 parse failed: ${parsedRes.error}` },
|
||||
],
|
||||
issues: [{ path: "", message: `JSON5 parse failed: ${parsedRes.error}` }],
|
||||
legacyIssues: [],
|
||||
};
|
||||
}
|
||||
@@ -376,9 +346,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
if (migrated.next && migrated.changes.length > 0) {
|
||||
deps.logger.warn(formatLegacyMigrationLog(migrated.changes));
|
||||
await writeConfigFile(validated.config).catch((err) => {
|
||||
deps.logger.warn(
|
||||
`Failed to write migrated config at ${configPath}: ${String(err)}`,
|
||||
);
|
||||
deps.logger.warn(`Failed to write migrated config at ${configPath}: ${String(err)}`);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -391,9 +359,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
config: normalizeConfigPaths(
|
||||
applyTalkApiKey(
|
||||
applyModelDefaults(
|
||||
applySessionDefaults(
|
||||
applyLoggingDefaults(applyMessageDefaults(validated.config)),
|
||||
),
|
||||
applySessionDefaults(applyLoggingDefaults(applyMessageDefaults(validated.config))),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -417,9 +383,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
async function writeConfigFile(cfg: ClawdbotConfig) {
|
||||
const dir = path.dirname(configPath);
|
||||
await deps.fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
|
||||
const json = JSON.stringify(applyModelDefaults(cfg), null, 2)
|
||||
.trimEnd()
|
||||
.concat("\n");
|
||||
const json = JSON.stringify(applyModelDefaults(cfg), null, 2).trimEnd().concat("\n");
|
||||
|
||||
const tmp = path.join(
|
||||
dir,
|
||||
@@ -431,11 +395,9 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
||||
mode: 0o600,
|
||||
});
|
||||
|
||||
await deps.fs.promises
|
||||
.copyFile(configPath, `${configPath}.bak`)
|
||||
.catch(() => {
|
||||
// best-effort
|
||||
});
|
||||
await deps.fs.promises.copyFile(configPath, `${configPath}.bak`).catch(() => {
|
||||
// best-effort
|
||||
});
|
||||
|
||||
try {
|
||||
await deps.fs.promises.rename(tmp, configPath);
|
||||
@@ -481,7 +443,5 @@ export async function readConfigFileSnapshot(): Promise<ConfigFileSnapshot> {
|
||||
}
|
||||
|
||||
export async function writeConfigFile(cfg: ClawdbotConfig): Promise<void> {
|
||||
await createConfigIO({ configPath: resolveConfigPath() }).writeConfigFile(
|
||||
cfg,
|
||||
);
|
||||
await createConfigIO({ configPath: resolveConfigPath() }).writeConfigFile(cfg);
|
||||
}
|
||||
|
||||
@@ -10,9 +10,7 @@ export function migrateLegacyConfig(raw: unknown): {
|
||||
if (!next) return { config: null, changes: [] };
|
||||
const validated = validateConfigObject(next);
|
||||
if (!validated.ok) {
|
||||
changes.push(
|
||||
"Migration applied, but config still invalid; fix remaining issues manually.",
|
||||
);
|
||||
changes.push("Migration applied, but config still invalid; fix remaining issues manually.");
|
||||
return { config: null, changes };
|
||||
}
|
||||
return { config: validated.config, changes };
|
||||
|
||||
@@ -20,8 +20,7 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
|
||||
const match = getRecord(entry.match);
|
||||
if (!match) continue;
|
||||
if (typeof match.channel === "string" && match.channel.trim()) continue;
|
||||
const provider =
|
||||
typeof match.provider === "string" ? match.provider.trim() : "";
|
||||
const provider = typeof match.provider === "string" ? match.provider.trim() : "";
|
||||
if (!provider) continue;
|
||||
match.channel = provider;
|
||||
delete match.provider;
|
||||
@@ -31,9 +30,7 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
|
||||
|
||||
if (touched) {
|
||||
raw.bindings = bindings;
|
||||
changes.push(
|
||||
"Moved bindings[].match.provider → bindings[].match.channel.",
|
||||
);
|
||||
changes.push("Moved bindings[].match.provider → bindings[].match.channel.");
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -54,8 +51,7 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
|
||||
const match = getRecord(rule.match);
|
||||
if (!match) continue;
|
||||
if (typeof match.channel === "string" && match.channel.trim()) continue;
|
||||
const provider =
|
||||
typeof match.provider === "string" ? match.provider.trim() : "";
|
||||
const provider = typeof match.provider === "string" ? match.provider.trim() : "";
|
||||
if (!provider) continue;
|
||||
match.channel = provider;
|
||||
delete match.provider;
|
||||
@@ -67,9 +63,7 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
|
||||
sendPolicy.rules = rules;
|
||||
session.sendPolicy = sendPolicy;
|
||||
raw.session = session;
|
||||
changes.push(
|
||||
"Moved session.sendPolicy.rules[].match.provider → match.channel.",
|
||||
);
|
||||
changes.push("Moved session.sendPolicy.rules[].match.provider → match.channel.");
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -84,13 +78,9 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
|
||||
if (queue.byProvider === undefined) return;
|
||||
if (queue.byChannel === undefined) {
|
||||
queue.byChannel = queue.byProvider;
|
||||
changes.push(
|
||||
"Moved messages.queue.byProvider → messages.queue.byChannel.",
|
||||
);
|
||||
changes.push("Moved messages.queue.byProvider → messages.queue.byChannel.");
|
||||
} else {
|
||||
changes.push(
|
||||
"Removed messages.queue.byProvider (messages.queue.byChannel already set).",
|
||||
);
|
||||
changes.push("Removed messages.queue.byProvider (messages.queue.byChannel already set).");
|
||||
}
|
||||
delete queue.byProvider;
|
||||
messages.queue = queue;
|
||||
@@ -123,9 +113,7 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
|
||||
channels[key] = channelEntry;
|
||||
delete raw[key];
|
||||
changes.push(
|
||||
hadEntries
|
||||
? `Merged ${key} → channels.${key}.`
|
||||
: `Moved ${key} → channels.${key}.`,
|
||||
hadEntries ? `Merged ${key} → channels.${key}.` : `Moved ${key} → channels.${key}.`,
|
||||
);
|
||||
}
|
||||
raw.channels = channels;
|
||||
@@ -150,9 +138,7 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
|
||||
whatsapp.allowFrom = allowFrom;
|
||||
changes.push("Moved routing.allowFrom → channels.whatsapp.allowFrom.");
|
||||
} else {
|
||||
changes.push(
|
||||
"Removed routing.allowFrom (channels.whatsapp.allowFrom already set).",
|
||||
);
|
||||
changes.push("Removed routing.allowFrom (channels.whatsapp.allowFrom already set).");
|
||||
}
|
||||
|
||||
delete (routing as Record<string, unknown>).allowFrom;
|
||||
@@ -165,18 +151,14 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
|
||||
},
|
||||
{
|
||||
id: "routing.groupChat.requireMention->groups.*.requireMention",
|
||||
describe:
|
||||
"Move routing.groupChat.requireMention to channels.whatsapp/telegram/imessage groups",
|
||||
describe: "Move routing.groupChat.requireMention to channels.whatsapp/telegram/imessage groups",
|
||||
apply: (raw, changes) => {
|
||||
const routing = raw.routing;
|
||||
if (!routing || typeof routing !== "object") return;
|
||||
const groupChat =
|
||||
(routing as Record<string, unknown>).groupChat &&
|
||||
typeof (routing as Record<string, unknown>).groupChat === "object"
|
||||
? ((routing as Record<string, unknown>).groupChat as Record<
|
||||
string,
|
||||
unknown
|
||||
>)
|
||||
? ((routing as Record<string, unknown>).groupChat as Record<string, unknown>)
|
||||
: null;
|
||||
if (!groupChat) return;
|
||||
const requireMention = groupChat.requireMention;
|
||||
@@ -256,23 +238,18 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
|
||||
},
|
||||
{
|
||||
id: "telegram.requireMention->channels.telegram.groups.*.requireMention",
|
||||
describe:
|
||||
"Move telegram.requireMention to channels.telegram.groups.*.requireMention",
|
||||
describe: "Move telegram.requireMention to channels.telegram.groups.*.requireMention",
|
||||
apply: (raw, changes) => {
|
||||
const channels = ensureRecord(raw, "channels");
|
||||
const telegram = channels.telegram;
|
||||
if (!telegram || typeof telegram !== "object") return;
|
||||
const requireMention = (telegram as Record<string, unknown>)
|
||||
.requireMention;
|
||||
const requireMention = (telegram as Record<string, unknown>).requireMention;
|
||||
if (requireMention === undefined) return;
|
||||
|
||||
const groups =
|
||||
(telegram as Record<string, unknown>).groups &&
|
||||
typeof (telegram as Record<string, unknown>).groups === "object"
|
||||
? ((telegram as Record<string, unknown>).groups as Record<
|
||||
string,
|
||||
unknown
|
||||
>)
|
||||
? ((telegram as Record<string, unknown>).groups as Record<string, unknown>)
|
||||
: {};
|
||||
const defaultKey = "*";
|
||||
const entry =
|
||||
@@ -288,9 +265,7 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_1: LegacyConfigMigration[] = [
|
||||
'Moved telegram.requireMention → channels.telegram.groups."*".requireMention.',
|
||||
);
|
||||
} else {
|
||||
changes.push(
|
||||
'Removed telegram.requireMention (channels.telegram.groups."*" already set).',
|
||||
);
|
||||
changes.push('Removed telegram.requireMention (channels.telegram.groups."*" already set).');
|
||||
}
|
||||
|
||||
delete (telegram as Record<string, unknown>).requireMention;
|
||||
|
||||
@@ -21,12 +21,9 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_2: LegacyConfigMigration[] = [
|
||||
if (!agent) return;
|
||||
const label = agentRoot ? "agent" : "agents.defaults";
|
||||
|
||||
const legacyModel =
|
||||
typeof agent.model === "string" ? String(agent.model) : undefined;
|
||||
const legacyModel = typeof agent.model === "string" ? String(agent.model) : undefined;
|
||||
const legacyImageModel =
|
||||
typeof agent.imageModel === "string"
|
||||
? String(agent.imageModel)
|
||||
: undefined;
|
||||
typeof agent.imageModel === "string" ? String(agent.imageModel) : undefined;
|
||||
const legacyAllowed = Array.isArray(agent.allowedModels)
|
||||
? (agent.allowedModels as unknown[]).map(String)
|
||||
: [];
|
||||
@@ -96,8 +93,7 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_2: LegacyConfigMigration[] = [
|
||||
}
|
||||
if (
|
||||
legacyModelFallbacks.length > 0 &&
|
||||
(!Array.isArray(currentModel.fallbacks) ||
|
||||
currentModel.fallbacks.length === 0)
|
||||
(!Array.isArray(currentModel.fallbacks) || currentModel.fallbacks.length === 0)
|
||||
) {
|
||||
currentModel.fallbacks = legacyModelFallbacks;
|
||||
}
|
||||
@@ -119,8 +115,7 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_2: LegacyConfigMigration[] = [
|
||||
}
|
||||
if (
|
||||
legacyImageModelFallbacks.length > 0 &&
|
||||
(!Array.isArray(currentImageModel.fallbacks) ||
|
||||
currentImageModel.fallbacks.length === 0)
|
||||
(!Array.isArray(currentImageModel.fallbacks) || currentImageModel.fallbacks.length === 0)
|
||||
) {
|
||||
currentImageModel.fallbacks = legacyImageModelFallbacks;
|
||||
}
|
||||
@@ -128,41 +123,29 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_2: LegacyConfigMigration[] = [
|
||||
} else if (legacyImageModel || legacyImageModelFallbacks.length > 0) {
|
||||
agent.imageModel = {
|
||||
primary: legacyImageModel,
|
||||
fallbacks: legacyImageModelFallbacks.length
|
||||
? legacyImageModelFallbacks
|
||||
: [],
|
||||
fallbacks: legacyImageModelFallbacks.length ? legacyImageModelFallbacks : [],
|
||||
};
|
||||
}
|
||||
|
||||
agent.models = models;
|
||||
|
||||
if (legacyModel !== undefined) {
|
||||
changes.push(
|
||||
`Migrated ${label}.model string → ${label}.model.primary.`,
|
||||
);
|
||||
changes.push(`Migrated ${label}.model string → ${label}.model.primary.`);
|
||||
}
|
||||
if (legacyModelFallbacks.length > 0) {
|
||||
changes.push(
|
||||
`Migrated ${label}.modelFallbacks → ${label}.model.fallbacks.`,
|
||||
);
|
||||
changes.push(`Migrated ${label}.modelFallbacks → ${label}.model.fallbacks.`);
|
||||
}
|
||||
if (legacyImageModel !== undefined) {
|
||||
changes.push(
|
||||
`Migrated ${label}.imageModel string → ${label}.imageModel.primary.`,
|
||||
);
|
||||
changes.push(`Migrated ${label}.imageModel string → ${label}.imageModel.primary.`);
|
||||
}
|
||||
if (legacyImageModelFallbacks.length > 0) {
|
||||
changes.push(
|
||||
`Migrated ${label}.imageModelFallbacks → ${label}.imageModel.fallbacks.`,
|
||||
);
|
||||
changes.push(`Migrated ${label}.imageModelFallbacks → ${label}.imageModel.fallbacks.`);
|
||||
}
|
||||
if (legacyAllowed.length > 0) {
|
||||
changes.push(`Migrated ${label}.allowedModels → ${label}.models.`);
|
||||
}
|
||||
if (Object.keys(legacyAliases).length > 0) {
|
||||
changes.push(
|
||||
`Migrated ${label}.modelAliases → ${label}.models.*.alias.`,
|
||||
);
|
||||
changes.push(`Migrated ${label}.modelAliases → ${label}.models.*.alias.`);
|
||||
}
|
||||
|
||||
delete agent.allowedModels;
|
||||
@@ -237,13 +220,10 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_2: LegacyConfigMigration[] = [
|
||||
}
|
||||
|
||||
const defaultAgentId =
|
||||
typeof routing.defaultAgentId === "string"
|
||||
? routing.defaultAgentId.trim()
|
||||
: "";
|
||||
typeof routing.defaultAgentId === "string" ? routing.defaultAgentId.trim() : "";
|
||||
if (defaultAgentId) {
|
||||
const hasDefault = list.some(
|
||||
(entry): entry is Record<string, unknown> =>
|
||||
isRecord(entry) && entry.default === true,
|
||||
(entry): entry is Record<string, unknown> => isRecord(entry) && entry.default === true,
|
||||
);
|
||||
if (!hasDefault) {
|
||||
const entry = ensureAgentEntry(list, defaultAgentId);
|
||||
@@ -252,9 +232,7 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_2: LegacyConfigMigration[] = [
|
||||
`Moved routing.defaultAgentId → agents.list (id "${defaultAgentId}").default.`,
|
||||
);
|
||||
} else {
|
||||
changes.push(
|
||||
"Removed routing.defaultAgentId (agents.list default already set).",
|
||||
);
|
||||
changes.push("Removed routing.defaultAgentId (agents.list default already set).");
|
||||
}
|
||||
delete routing.defaultAgentId;
|
||||
}
|
||||
@@ -270,8 +248,7 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_2: LegacyConfigMigration[] = [
|
||||
},
|
||||
{
|
||||
id: "routing.config-v2",
|
||||
describe:
|
||||
"Move routing bindings/groupChat/queue/agentToAgent/transcribeAudio",
|
||||
describe: "Move routing bindings/groupChat/queue/agentToAgent/transcribeAudio",
|
||||
apply: (raw, changes) => {
|
||||
const routing = getRecord(raw.routing);
|
||||
if (!routing) return;
|
||||
@@ -292,9 +269,7 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_2: LegacyConfigMigration[] = [
|
||||
tools.agentToAgent = routing.agentToAgent;
|
||||
changes.push("Moved routing.agentToAgent → tools.agentToAgent.");
|
||||
} else {
|
||||
changes.push(
|
||||
"Removed routing.agentToAgent (tools.agentToAgent already set).",
|
||||
);
|
||||
changes.push("Removed routing.agentToAgent (tools.agentToAgent already set).");
|
||||
}
|
||||
delete routing.agentToAgent;
|
||||
}
|
||||
@@ -318,9 +293,7 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_2: LegacyConfigMigration[] = [
|
||||
const messagesGroup = ensureRecord(messages, "groupChat");
|
||||
if (messagesGroup.historyLimit === undefined) {
|
||||
messagesGroup.historyLimit = historyLimit;
|
||||
changes.push(
|
||||
"Moved routing.groupChat.historyLimit → messages.groupChat.historyLimit.",
|
||||
);
|
||||
changes.push("Moved routing.groupChat.historyLimit → messages.groupChat.historyLimit.");
|
||||
} else {
|
||||
changes.push(
|
||||
"Removed routing.groupChat.historyLimit (messages.groupChat.historyLimit already set).",
|
||||
@@ -360,18 +333,14 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_2: LegacyConfigMigration[] = [
|
||||
const toolsAudio = ensureRecord(tools, "audio");
|
||||
if (toolsAudio.transcription === undefined) {
|
||||
toolsAudio.transcription = mapped;
|
||||
changes.push(
|
||||
"Moved routing.transcribeAudio → tools.audio.transcription.",
|
||||
);
|
||||
changes.push("Moved routing.transcribeAudio → tools.audio.transcription.");
|
||||
} else {
|
||||
changes.push(
|
||||
"Removed routing.transcribeAudio (tools.audio.transcription already set).",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
changes.push(
|
||||
"Removed routing.transcribeAudio (unsupported transcription CLI).",
|
||||
);
|
||||
changes.push("Removed routing.transcribeAudio (unsupported transcription CLI).");
|
||||
}
|
||||
delete routing.transcribeAudio;
|
||||
}
|
||||
@@ -384,22 +353,16 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_2: LegacyConfigMigration[] = [
|
||||
const toolsAudio = ensureRecord(tools, "audio");
|
||||
if (toolsAudio.transcription === undefined) {
|
||||
toolsAudio.transcription = mapped;
|
||||
changes.push(
|
||||
"Moved audio.transcription → tools.audio.transcription.",
|
||||
);
|
||||
changes.push("Moved audio.transcription → tools.audio.transcription.");
|
||||
} else {
|
||||
changes.push(
|
||||
"Removed audio.transcription (tools.audio.transcription already set).",
|
||||
);
|
||||
changes.push("Removed audio.transcription (tools.audio.transcription already set).");
|
||||
}
|
||||
delete audio.transcription;
|
||||
if (Object.keys(audio).length === 0) delete raw.audio;
|
||||
else raw.audio = audio;
|
||||
} else {
|
||||
delete audio.transcription;
|
||||
changes.push(
|
||||
"Removed audio.transcription (unsupported transcription CLI).",
|
||||
);
|
||||
changes.push("Removed audio.transcription (unsupported transcription CLI).");
|
||||
if (Object.keys(audio).length === 0) delete raw.audio;
|
||||
else raw.audio = audio;
|
||||
}
|
||||
|
||||
@@ -106,9 +106,7 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [
|
||||
const entry = ensureAgentEntry(list, defaultId);
|
||||
if (entry.identity === undefined) {
|
||||
entry.identity = identity;
|
||||
changes.push(
|
||||
`Moved identity → agents.list (id "${defaultId}").identity.`,
|
||||
);
|
||||
changes.push(`Moved identity → agents.list (id "${defaultId}").identity.`);
|
||||
} else {
|
||||
changes.push("Removed identity (agents.list identity already set).");
|
||||
}
|
||||
@@ -121,10 +119,7 @@ export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [
|
||||
id: "bind-tailnet->auto",
|
||||
describe: "Remap gateway/bridge bind 'tailnet' to 'auto'",
|
||||
apply: (raw, changes) => {
|
||||
const migrateBind = (
|
||||
obj: Record<string, unknown> | null | undefined,
|
||||
key: string,
|
||||
) => {
|
||||
const migrateBind = (obj: Record<string, unknown> | null | undefined, key: string) => {
|
||||
if (!obj) return;
|
||||
const bind = obj.bind;
|
||||
if (bind === "tailnet") {
|
||||
|
||||
@@ -3,18 +3,15 @@ import type { LegacyConfigRule } from "./legacy.shared.js";
|
||||
export const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [
|
||||
{
|
||||
path: ["whatsapp"],
|
||||
message:
|
||||
"whatsapp config moved to channels.whatsapp (auto-migrated on load).",
|
||||
message: "whatsapp config moved to channels.whatsapp (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["telegram"],
|
||||
message:
|
||||
"telegram config moved to channels.telegram (auto-migrated on load).",
|
||||
message: "telegram config moved to channels.telegram (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["discord"],
|
||||
message:
|
||||
"discord config moved to channels.discord (auto-migrated on load).",
|
||||
message: "discord config moved to channels.discord (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["slack"],
|
||||
@@ -26,13 +23,11 @@ export const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [
|
||||
},
|
||||
{
|
||||
path: ["imessage"],
|
||||
message:
|
||||
"imessage config moved to channels.imessage (auto-migrated on load).",
|
||||
message: "imessage config moved to channels.imessage (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["msteams"],
|
||||
message:
|
||||
"msteams config moved to channels.msteams (auto-migrated on load).",
|
||||
message: "msteams config moved to channels.msteams (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["routing", "allowFrom"],
|
||||
@@ -41,13 +36,11 @@ export const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [
|
||||
},
|
||||
{
|
||||
path: ["routing", "bindings"],
|
||||
message:
|
||||
"routing.bindings was moved; use top-level bindings instead (auto-migrated on load).",
|
||||
message: "routing.bindings was moved; use top-level bindings instead (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["routing", "agents"],
|
||||
message:
|
||||
"routing.agents was moved; use agents.list instead (auto-migrated on load).",
|
||||
message: "routing.agents was moved; use agents.list instead (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["routing", "defaultAgentId"],
|
||||
@@ -71,8 +64,7 @@ export const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [
|
||||
},
|
||||
{
|
||||
path: ["routing", "queue"],
|
||||
message:
|
||||
"routing.queue was moved; use messages.queue instead (auto-migrated on load).",
|
||||
message: "routing.queue was moved; use messages.queue instead (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["routing", "transcribeAudio"],
|
||||
@@ -86,8 +78,7 @@ export const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [
|
||||
},
|
||||
{
|
||||
path: ["identity"],
|
||||
message:
|
||||
"identity was moved; use agents.list[].identity instead (auto-migrated on load).",
|
||||
message: "identity was moved; use agents.list[].identity instead (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["agent"],
|
||||
@@ -108,8 +99,7 @@ export const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [
|
||||
},
|
||||
{
|
||||
path: ["agent", "allowedModels"],
|
||||
message:
|
||||
"agent.allowedModels was replaced by agents.defaults.models (auto-migrated on load).",
|
||||
message: "agent.allowedModels was replaced by agents.defaults.models (auto-migrated on load).",
|
||||
},
|
||||
{
|
||||
path: ["agent", "modelAliases"],
|
||||
@@ -128,7 +118,6 @@ export const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [
|
||||
},
|
||||
{
|
||||
path: ["gateway", "token"],
|
||||
message:
|
||||
"gateway.token is ignored; use gateway.auth.token instead (auto-migrated on load).",
|
||||
message: "gateway.token is ignored; use gateway.auth.token instead (auto-migrated on load).",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -27,10 +27,7 @@ export const ensureRecord = (
|
||||
return next;
|
||||
};
|
||||
|
||||
export const mergeMissing = (
|
||||
target: Record<string, unknown>,
|
||||
source: Record<string, unknown>,
|
||||
) => {
|
||||
export const mergeMissing = (target: Record<string, unknown>, source: Record<string, unknown>) => {
|
||||
for (const [key, value] of Object.entries(source)) {
|
||||
if (value === undefined) continue;
|
||||
const existing = target[key];
|
||||
@@ -46,13 +43,9 @@ export const mergeMissing = (
|
||||
|
||||
const AUDIO_TRANSCRIPTION_CLI_ALLOWLIST = new Set(["whisper"]);
|
||||
|
||||
export const mapLegacyAudioTranscription = (
|
||||
value: unknown,
|
||||
): Record<string, unknown> | null => {
|
||||
export const mapLegacyAudioTranscription = (value: unknown): Record<string, unknown> | null => {
|
||||
const transcriber = getRecord(value);
|
||||
const command = Array.isArray(transcriber?.command)
|
||||
? transcriber?.command
|
||||
: null;
|
||||
const command = Array.isArray(transcriber?.command) ? transcriber?.command : null;
|
||||
if (!command || command.length === 0) return null;
|
||||
const rawExecutable = String(command[0] ?? "").trim();
|
||||
if (!rawExecutable) return null;
|
||||
@@ -61,9 +54,7 @@ export const mapLegacyAudioTranscription = (
|
||||
|
||||
const args = command.slice(1).map((part) => String(part));
|
||||
const timeoutSeconds =
|
||||
typeof transcriber?.timeoutSeconds === "number"
|
||||
? transcriber?.timeoutSeconds
|
||||
: undefined;
|
||||
typeof transcriber?.timeoutSeconds === "number" ? transcriber?.timeoutSeconds : undefined;
|
||||
|
||||
const result: Record<string, unknown> = {};
|
||||
if (args.length > 0) result.args = args;
|
||||
@@ -89,9 +80,7 @@ export const resolveDefaultAgentIdFromRaw = (raw: Record<string, unknown>) => {
|
||||
if (defaultEntry) return defaultEntry.id.trim();
|
||||
const routing = getRecord(raw.routing);
|
||||
const routingDefault =
|
||||
typeof routing?.defaultAgentId === "string"
|
||||
? routing.defaultAgentId.trim()
|
||||
: "";
|
||||
typeof routing?.defaultAgentId === "string" ? routing.defaultAgentId.trim() : "";
|
||||
if (routingDefault) return routingDefault;
|
||||
const firstEntry = list.find(
|
||||
(entry): entry is { id: string } =>
|
||||
@@ -101,16 +90,11 @@ export const resolveDefaultAgentIdFromRaw = (raw: Record<string, unknown>) => {
|
||||
return "main";
|
||||
};
|
||||
|
||||
export const ensureAgentEntry = (
|
||||
list: unknown[],
|
||||
id: string,
|
||||
): Record<string, unknown> => {
|
||||
export const ensureAgentEntry = (list: unknown[], id: string): Record<string, unknown> => {
|
||||
const normalized = id.trim();
|
||||
const existing = list.find(
|
||||
(entry): entry is Record<string, unknown> =>
|
||||
isRecord(entry) &&
|
||||
typeof entry.id === "string" &&
|
||||
entry.id.trim() === normalized,
|
||||
isRecord(entry) && typeof entry.id === "string" && entry.id.trim() === normalized,
|
||||
);
|
||||
if (existing) return existing;
|
||||
const created: Record<string, unknown> = { id: normalized };
|
||||
|
||||
@@ -16,12 +16,8 @@ describe("applyModelDefaults", () => {
|
||||
} satisfies ClawdbotConfig;
|
||||
const next = applyModelDefaults(cfg);
|
||||
|
||||
expect(
|
||||
next.agents?.defaults?.models?.["anthropic/claude-opus-4-5"]?.alias,
|
||||
).toBe("opus");
|
||||
expect(next.agents?.defaults?.models?.["openai/gpt-5.2"]?.alias).toBe(
|
||||
"gpt",
|
||||
);
|
||||
expect(next.agents?.defaults?.models?.["anthropic/claude-opus-4-5"]?.alias).toBe("opus");
|
||||
expect(next.agents?.defaults?.models?.["openai/gpt-5.2"]?.alias).toBe("gpt");
|
||||
});
|
||||
|
||||
it("does not override existing aliases", () => {
|
||||
@@ -37,9 +33,7 @@ describe("applyModelDefaults", () => {
|
||||
|
||||
const next = applyModelDefaults(cfg);
|
||||
|
||||
expect(
|
||||
next.agents?.defaults?.models?.["anthropic/claude-opus-4-5"]?.alias,
|
||||
).toBe("Opus");
|
||||
expect(next.agents?.defaults?.models?.["anthropic/claude-opus-4-5"]?.alias).toBe("Opus");
|
||||
});
|
||||
|
||||
it("respects explicit empty alias disables", () => {
|
||||
@@ -56,11 +50,9 @@ describe("applyModelDefaults", () => {
|
||||
|
||||
const next = applyModelDefaults(cfg);
|
||||
|
||||
expect(
|
||||
next.agents?.defaults?.models?.["google/gemini-3-pro-preview"]?.alias,
|
||||
).toBe("");
|
||||
expect(
|
||||
next.agents?.defaults?.models?.["google/gemini-3-flash-preview"]?.alias,
|
||||
).toBe("gemini-flash");
|
||||
expect(next.agents?.defaults?.models?.["google/gemini-3-pro-preview"]?.alias).toBe("");
|
||||
expect(next.agents?.defaults?.models?.["google/gemini-3-flash-preview"]?.alias).toBe(
|
||||
"gemini-flash",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,12 +45,8 @@ describe("normalizeConfigPaths", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(cfg.plugins?.load?.paths?.[0]).toBe(
|
||||
path.join(home, "plugins", "a"),
|
||||
);
|
||||
expect(cfg.logging?.file).toBe(
|
||||
path.join(home, ".clawdbot", "logs", "clawdbot.log"),
|
||||
);
|
||||
expect(cfg.plugins?.load?.paths?.[0]).toBe(path.join(home, "plugins", "a"));
|
||||
expect(cfg.logging?.file).toBe(path.join(home, ".clawdbot", "logs", "clawdbot.log"));
|
||||
expect(cfg.hooks?.path).toBe(path.join(home, ".clawdbot", "hooks.json5"));
|
||||
expect(cfg.hooks?.transformsDir).toBe(path.join(home, "hooks-xform"));
|
||||
expect(cfg.channels?.telegram?.accounts?.personal?.tokenFile).toBe(
|
||||
@@ -59,18 +55,10 @@ describe("normalizeConfigPaths", () => {
|
||||
expect(cfg.channels?.imessage?.accounts?.personal?.dbPath).toBe(
|
||||
path.join(home, "Library", "Messages", "chat.db"),
|
||||
);
|
||||
expect(cfg.agents?.defaults?.workspace).toBe(
|
||||
path.join(home, "ws-default"),
|
||||
);
|
||||
expect(cfg.agents?.list?.[0]?.workspace).toBe(
|
||||
path.join(home, "ws-agent"),
|
||||
);
|
||||
expect(cfg.agents?.list?.[0]?.agentDir).toBe(
|
||||
path.join(home, ".clawdbot", "agents", "main"),
|
||||
);
|
||||
expect(cfg.agents?.list?.[0]?.sandbox?.workspaceRoot).toBe(
|
||||
path.join(home, "sandbox-root"),
|
||||
);
|
||||
expect(cfg.agents?.defaults?.workspace).toBe(path.join(home, "ws-default"));
|
||||
expect(cfg.agents?.list?.[0]?.workspace).toBe(path.join(home, "ws-agent"));
|
||||
expect(cfg.agents?.list?.[0]?.agentDir).toBe(path.join(home, ".clawdbot", "agents", "main"));
|
||||
expect(cfg.agents?.list?.[0]?.sandbox?.workspaceRoot).toBe(path.join(home, "sandbox-root"));
|
||||
|
||||
// Non-path key => do not treat "~" as home expansion.
|
||||
expect(cfg.agents?.list?.[0]?.identity?.name).toBe("~not-a-path");
|
||||
|
||||
@@ -10,9 +10,7 @@ describe("oauth paths", () => {
|
||||
CLAWDBOT_STATE_DIR: "/custom/state",
|
||||
} as NodeJS.ProcessEnv;
|
||||
|
||||
expect(resolveOAuthDir(env, "/custom/state")).toBe(
|
||||
path.resolve("/custom/oauth"),
|
||||
);
|
||||
expect(resolveOAuthDir(env, "/custom/state")).toBe(path.resolve("/custom/oauth"));
|
||||
expect(resolveOAuthPath(env, "/custom/state")).toBe(
|
||||
path.join(path.resolve("/custom/oauth"), "oauth.json"),
|
||||
);
|
||||
@@ -23,9 +21,7 @@ describe("oauth paths", () => {
|
||||
CLAWDBOT_STATE_DIR: "/custom/state",
|
||||
} as NodeJS.ProcessEnv;
|
||||
|
||||
expect(resolveOAuthDir(env, "/custom/state")).toBe(
|
||||
path.join("/custom/state", "credentials"),
|
||||
);
|
||||
expect(resolveOAuthDir(env, "/custom/state")).toBe(path.join("/custom/state", "credentials"));
|
||||
expect(resolveOAuthPath(env, "/custom/state")).toBe(
|
||||
path.join("/custom/state", "credentials", "oauth.json"),
|
||||
);
|
||||
|
||||
@@ -9,9 +9,7 @@ import type { ClawdbotConfig } from "./types.js";
|
||||
* - Missing dependencies should produce actionable Nix-specific error messages
|
||||
* - Config is managed externally (read-only from Nix perspective)
|
||||
*/
|
||||
export function resolveIsNixMode(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): boolean {
|
||||
export function resolveIsNixMode(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
return env.CLAWDBOT_NIX_MODE === "1";
|
||||
}
|
||||
|
||||
@@ -26,8 +24,7 @@ export function resolveStateDir(
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
homedir: () => string = os.homedir,
|
||||
): string {
|
||||
const override =
|
||||
env.CLAWDBOT_STATE_DIR?.trim() || env.CLAWDIS_STATE_DIR?.trim();
|
||||
const override = env.CLAWDBOT_STATE_DIR?.trim() || env.CLAWDIS_STATE_DIR?.trim();
|
||||
if (override) return resolveUserPath(override);
|
||||
return path.join(homedir(), ".clawdbot");
|
||||
}
|
||||
|
||||
@@ -30,18 +30,10 @@ export function deriveDefaultCanvasHostPort(gatewayPort: number): number {
|
||||
return derivePort(gatewayPort, 4, DEFAULT_CANVAS_HOST_PORT);
|
||||
}
|
||||
|
||||
export function deriveDefaultBrowserCdpPortRange(
|
||||
browserControlPort: number,
|
||||
): PortRange {
|
||||
const start = derivePort(
|
||||
browserControlPort,
|
||||
9,
|
||||
DEFAULT_BROWSER_CDP_PORT_RANGE_START,
|
||||
);
|
||||
export function deriveDefaultBrowserCdpPortRange(browserControlPort: number): PortRange {
|
||||
const start = derivePort(browserControlPort, 9, DEFAULT_BROWSER_CDP_PORT_RANGE_START);
|
||||
const end = clampPort(
|
||||
start +
|
||||
(DEFAULT_BROWSER_CDP_PORT_RANGE_END -
|
||||
DEFAULT_BROWSER_CDP_PORT_RANGE_START),
|
||||
start + (DEFAULT_BROWSER_CDP_PORT_RANGE_END - DEFAULT_BROWSER_CDP_PORT_RANGE_START),
|
||||
DEFAULT_BROWSER_CDP_PORT_RANGE_END,
|
||||
);
|
||||
if (end < start) return { start, end: start };
|
||||
|
||||
@@ -41,11 +41,7 @@ describe("runtime overrides", () => {
|
||||
});
|
||||
|
||||
it("rejects prototype pollution paths", () => {
|
||||
const attempts = [
|
||||
"__proto__.polluted",
|
||||
"constructor.polluted",
|
||||
"prototype.polluted",
|
||||
];
|
||||
const attempts = ["__proto__.polluted", "constructor.polluted", "prototype.polluted"];
|
||||
for (const path of attempts) {
|
||||
const result = setConfigOverride(path, true);
|
||||
expect(result.ok).toBe(false);
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
parseConfigPath,
|
||||
setConfigValueAtPath,
|
||||
unsetConfigValueAtPath,
|
||||
} from "./config-paths.js";
|
||||
import { parseConfigPath, setConfigValueAtPath, unsetConfigValueAtPath } from "./config-paths.js";
|
||||
import type { ClawdbotConfig } from "./types.js";
|
||||
|
||||
type OverrideTree = Record<string, unknown>;
|
||||
|
||||
@@ -30,15 +30,10 @@ describe("config schema", () => {
|
||||
});
|
||||
|
||||
expect(res.uiHints["plugins.entries.voice-call"]?.label).toBe("Voice Call");
|
||||
expect(res.uiHints["plugins.entries.voice-call.config"]?.label).toBe(
|
||||
"Voice Call Config",
|
||||
expect(res.uiHints["plugins.entries.voice-call.config"]?.label).toBe("Voice Call Config");
|
||||
expect(res.uiHints["plugins.entries.voice-call.config.twilio.authToken"]?.label).toBe(
|
||||
"Auth Token",
|
||||
);
|
||||
expect(
|
||||
res.uiHints["plugins.entries.voice-call.config.twilio.authToken"]?.label,
|
||||
).toBe("Auth Token");
|
||||
expect(
|
||||
res.uiHints["plugins.entries.voice-call.config.twilio.authToken"]
|
||||
?.sensitive,
|
||||
).toBe(true);
|
||||
expect(res.uiHints["plugins.entries.voice-call.config.twilio.authToken"]?.sensitive).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,10 +29,7 @@ export type PluginUiMetadata = {
|
||||
description?: string;
|
||||
configUiHints?: Record<
|
||||
string,
|
||||
Pick<
|
||||
ConfigUiHint,
|
||||
"label" | "help" | "advanced" | "sensitive" | "placeholder"
|
||||
>
|
||||
Pick<ConfigUiHint, "label" | "help" | "advanced" | "sensitive" | "placeholder">
|
||||
>;
|
||||
};
|
||||
|
||||
@@ -95,15 +92,13 @@ const FIELD_LABELS: Record<string, string> = {
|
||||
"gateway.auth.token": "Gateway Token",
|
||||
"gateway.auth.password": "Gateway Password",
|
||||
"tools.audio.transcription.args": "Audio Transcription Args",
|
||||
"tools.audio.transcription.timeoutSeconds":
|
||||
"Audio Transcription Timeout (sec)",
|
||||
"tools.audio.transcription.timeoutSeconds": "Audio Transcription Timeout (sec)",
|
||||
"tools.profile": "Tool Profile",
|
||||
"agents.list[].tools.profile": "Agent Tool Profile",
|
||||
"tools.exec.applyPatch.enabled": "Enable apply_patch",
|
||||
"tools.exec.applyPatch.allowModels": "apply_patch Model Allowlist",
|
||||
"gateway.controlUi.basePath": "Control UI Base Path",
|
||||
"gateway.http.endpoints.chatCompletions.enabled":
|
||||
"OpenAI Chat Completions Endpoint",
|
||||
"gateway.http.endpoints.chatCompletions.enabled": "OpenAI Chat Completions Endpoint",
|
||||
"gateway.reload.mode": "Config Reload Mode",
|
||||
"gateway.reload.debounceMs": "Config Reload Debounce (ms)",
|
||||
"agents.defaults.workspace": "Workspace",
|
||||
@@ -119,13 +114,11 @@ const FIELD_LABELS: Record<string, string> = {
|
||||
"agents.defaults.memorySearch.local.modelPath": "Local Embedding Model Path",
|
||||
"agents.defaults.memorySearch.store.path": "Memory Search Index Path",
|
||||
"agents.defaults.memorySearch.chunking.tokens": "Memory Chunk Tokens",
|
||||
"agents.defaults.memorySearch.chunking.overlap":
|
||||
"Memory Chunk Overlap Tokens",
|
||||
"agents.defaults.memorySearch.chunking.overlap": "Memory Chunk Overlap Tokens",
|
||||
"agents.defaults.memorySearch.sync.onSessionStart": "Index on Session Start",
|
||||
"agents.defaults.memorySearch.sync.onSearch": "Index on Search (Lazy)",
|
||||
"agents.defaults.memorySearch.sync.watch": "Watch Memory Files",
|
||||
"agents.defaults.memorySearch.sync.watchDebounceMs":
|
||||
"Memory Watch Debounce (ms)",
|
||||
"agents.defaults.memorySearch.sync.watchDebounceMs": "Memory Watch Debounce (ms)",
|
||||
"agents.defaults.memorySearch.query.maxResults": "Memory Search Max Results",
|
||||
"agents.defaults.memorySearch.query.minScore": "Memory Search Min Score",
|
||||
"auth.profiles": "Auth Profiles",
|
||||
@@ -169,8 +162,7 @@ const FIELD_LABELS: Record<string, string> = {
|
||||
"channels.telegram.streamMode": "Telegram Draft Stream Mode",
|
||||
"channels.telegram.draftChunk.minChars": "Telegram Draft Chunk Min Chars",
|
||||
"channels.telegram.draftChunk.maxChars": "Telegram Draft Chunk Max Chars",
|
||||
"channels.telegram.draftChunk.breakPreference":
|
||||
"Telegram Draft Chunk Break Preference",
|
||||
"channels.telegram.draftChunk.breakPreference": "Telegram Draft Chunk Break Preference",
|
||||
"channels.telegram.retry.attempts": "Telegram Retry Attempts",
|
||||
"channels.telegram.retry.minDelayMs": "Telegram Retry Min Delay (ms)",
|
||||
"channels.telegram.retry.maxDelayMs": "Telegram Retry Max Delay (ms)",
|
||||
@@ -206,19 +198,15 @@ const FIELD_HELP: Record<string, string> = {
|
||||
"gateway.remote.url": "Remote Gateway WebSocket URL (ws:// or wss://).",
|
||||
"gateway.remote.sshTarget":
|
||||
"Remote gateway over SSH (tunnels the gateway port to localhost). Format: user@host or user@host:port.",
|
||||
"gateway.remote.sshIdentity":
|
||||
"Optional SSH identity file path (passed to ssh -i).",
|
||||
"gateway.auth.token":
|
||||
"Recommended for all gateways; required for non-loopback binds.",
|
||||
"gateway.remote.sshIdentity": "Optional SSH identity file path (passed to ssh -i).",
|
||||
"gateway.auth.token": "Recommended for all gateways; required for non-loopback binds.",
|
||||
"gateway.auth.password": "Required for Tailscale funnel.",
|
||||
"gateway.controlUi.basePath":
|
||||
"Optional URL prefix where the Control UI is served (e.g. /clawdbot).",
|
||||
"gateway.http.endpoints.chatCompletions.enabled":
|
||||
"Enable the OpenAI-compatible `POST /v1/chat/completions` endpoint (default: false).",
|
||||
"gateway.reload.mode":
|
||||
'Hot reload strategy for config changes ("hybrid" recommended).',
|
||||
"gateway.reload.debounceMs":
|
||||
"Debounce window (ms) before applying config changes.",
|
||||
"gateway.reload.mode": 'Hot reload strategy for config changes ("hybrid" recommended).',
|
||||
"gateway.reload.debounceMs": "Debounce window (ms) before applying config changes.",
|
||||
"tools.exec.applyPatch.enabled":
|
||||
"Experimental. Enables apply_patch for OpenAI models when allowed by tool policy.",
|
||||
"tools.exec.applyPatch.allowModels":
|
||||
@@ -226,28 +214,22 @@ const FIELD_HELP: Record<string, string> = {
|
||||
"channels.slack.allowBots":
|
||||
"Allow bot-authored messages to trigger Slack replies (default: false).",
|
||||
"auth.profiles": "Named auth profiles (provider + mode + optional email).",
|
||||
"auth.order":
|
||||
"Ordered auth profile IDs per provider (used for automatic failover).",
|
||||
"auth.order": "Ordered auth profile IDs per provider (used for automatic failover).",
|
||||
"auth.cooldowns.billingBackoffHours":
|
||||
"Base backoff (hours) when a profile fails due to billing/insufficient credits (default: 5).",
|
||||
"auth.cooldowns.billingBackoffHoursByProvider":
|
||||
"Optional per-provider overrides for billing backoff (hours).",
|
||||
"auth.cooldowns.billingMaxHours":
|
||||
"Cap (hours) for billing backoff (default: 24).",
|
||||
"auth.cooldowns.failureWindowHours":
|
||||
"Failure window (hours) for backoff counters (default: 24).",
|
||||
"auth.cooldowns.billingMaxHours": "Cap (hours) for billing backoff (default: 24).",
|
||||
"auth.cooldowns.failureWindowHours": "Failure window (hours) for backoff counters (default: 24).",
|
||||
"agents.defaults.bootstrapMaxChars":
|
||||
"Max characters of each workspace bootstrap file injected into the system prompt before truncation (default: 20000).",
|
||||
"agents.defaults.models":
|
||||
"Configured model catalog (keys are full provider/model IDs).",
|
||||
"agents.defaults.models": "Configured model catalog (keys are full provider/model IDs).",
|
||||
"agents.defaults.memorySearch":
|
||||
"Vector search over MEMORY.md and memory/*.md (per-agent overrides supported).",
|
||||
"agents.defaults.memorySearch.provider":
|
||||
'Embedding provider ("openai" or "local").',
|
||||
"agents.defaults.memorySearch.provider": 'Embedding provider ("openai" or "local").',
|
||||
"agents.defaults.memorySearch.remote.baseUrl":
|
||||
"Custom OpenAI-compatible base URL (e.g. for Gemini/OpenRouter proxies).",
|
||||
"agents.defaults.memorySearch.remote.apiKey":
|
||||
"Custom API key for the remote embedding provider.",
|
||||
"agents.defaults.memorySearch.remote.apiKey": "Custom API key for the remote embedding provider.",
|
||||
"agents.defaults.memorySearch.remote.headers":
|
||||
"Extra headers for remote embeddings (merged; remote overrides OpenAI headers).",
|
||||
"agents.defaults.memorySearch.local.modelPath":
|
||||
@@ -258,34 +240,24 @@ const FIELD_HELP: Record<string, string> = {
|
||||
"SQLite index path (default: ~/.clawdbot/memory/{agentId}.sqlite).",
|
||||
"agents.defaults.memorySearch.sync.onSearch":
|
||||
"Lazy sync: reindex on first search after a change.",
|
||||
"agents.defaults.memorySearch.sync.watch":
|
||||
"Watch memory files for changes (chokidar).",
|
||||
"agents.defaults.memorySearch.sync.watch": "Watch memory files for changes (chokidar).",
|
||||
"plugins.enabled": "Enable plugin/extension loading (default: true).",
|
||||
"plugins.allow":
|
||||
"Optional allowlist of plugin ids; when set, only listed plugins load.",
|
||||
"plugins.allow": "Optional allowlist of plugin ids; when set, only listed plugins load.",
|
||||
"plugins.deny": "Optional denylist of plugin ids; deny wins over allowlist.",
|
||||
"plugins.load.paths": "Additional plugin files or directories to load.",
|
||||
"plugins.entries":
|
||||
"Per-plugin settings keyed by plugin id (enable/disable + config payloads).",
|
||||
"plugins.entries.*.enabled":
|
||||
"Overrides plugin enable/disable for this entry (restart required).",
|
||||
"plugins.entries.*.config":
|
||||
"Plugin-defined config payload (schema is provided by the plugin).",
|
||||
"plugins.entries": "Per-plugin settings keyed by plugin id (enable/disable + config payloads).",
|
||||
"plugins.entries.*.enabled": "Overrides plugin enable/disable for this entry (restart required).",
|
||||
"plugins.entries.*.config": "Plugin-defined config payload (schema is provided by the plugin).",
|
||||
"agents.defaults.model.primary": "Primary model (provider/model).",
|
||||
"agents.defaults.model.fallbacks":
|
||||
"Ordered fallback models (provider/model). Used when the primary model fails.",
|
||||
"agents.defaults.imageModel.primary":
|
||||
"Optional image model (provider/model) used when the primary model lacks image input.",
|
||||
"agents.defaults.imageModel.fallbacks":
|
||||
"Ordered fallback image models (provider/model).",
|
||||
"agents.defaults.cliBackends":
|
||||
"Optional CLI backends for text-only fallback (claude-cli, etc.).",
|
||||
"agents.defaults.humanDelay.mode":
|
||||
'Delay style for block replies ("off", "natural", "custom").',
|
||||
"agents.defaults.humanDelay.minMs":
|
||||
"Minimum delay in ms for custom humanDelay (default: 800).",
|
||||
"agents.defaults.humanDelay.maxMs":
|
||||
"Maximum delay in ms for custom humanDelay (default: 2500).",
|
||||
"agents.defaults.imageModel.fallbacks": "Ordered fallback image models (provider/model).",
|
||||
"agents.defaults.cliBackends": "Optional CLI backends for text-only fallback (claude-cli, etc.).",
|
||||
"agents.defaults.humanDelay.mode": 'Delay style for block replies ("off", "natural", "custom").',
|
||||
"agents.defaults.humanDelay.minMs": "Minimum delay in ms for custom humanDelay (default: 800).",
|
||||
"agents.defaults.humanDelay.maxMs": "Maximum delay in ms for custom humanDelay (default: 2500).",
|
||||
"commands.native":
|
||||
"Register native commands with channels that support it (Discord/Slack/Telegram).",
|
||||
"commands.text": "Allow text command parsing (slash commands only).",
|
||||
@@ -293,24 +265,16 @@ const FIELD_HELP: Record<string, string> = {
|
||||
"Allow bash chat command (`!`; `/bash` alias) to run host shell commands (default: false; requires tools.elevated).",
|
||||
"commands.bashForegroundMs":
|
||||
"How long bash waits before backgrounding (default: 2000; 0 backgrounds immediately).",
|
||||
"commands.config":
|
||||
"Allow /config chat command to read/write config on disk (default: false).",
|
||||
"commands.debug":
|
||||
"Allow /debug chat command for runtime-only overrides (default: false).",
|
||||
"commands.restart":
|
||||
"Allow /restart and gateway restart tool actions (default: false).",
|
||||
"commands.useAccessGroups":
|
||||
"Enforce access-group allowlists/policies for commands.",
|
||||
"channels.discord.commands.native":
|
||||
'Override native commands for Discord (bool or "auto").',
|
||||
"channels.telegram.commands.native":
|
||||
'Override native commands for Telegram (bool or "auto").',
|
||||
"channels.slack.commands.native":
|
||||
'Override native commands for Slack (bool or "auto").',
|
||||
"commands.config": "Allow /config chat command to read/write config on disk (default: false).",
|
||||
"commands.debug": "Allow /debug chat command for runtime-only overrides (default: false).",
|
||||
"commands.restart": "Allow /restart and gateway restart tool actions (default: false).",
|
||||
"commands.useAccessGroups": "Enforce access-group allowlists/policies for commands.",
|
||||
"channels.discord.commands.native": 'Override native commands for Discord (bool or "auto").',
|
||||
"channels.telegram.commands.native": 'Override native commands for Telegram (bool or "auto").',
|
||||
"channels.slack.commands.native": 'Override native commands for Slack (bool or "auto").',
|
||||
"session.agentToAgent.maxPingPongTurns":
|
||||
"Max reply-back turns between requester and target (0–5).",
|
||||
"messages.ackReaction":
|
||||
"Emoji reaction used to acknowledge inbound messages (empty disables).",
|
||||
"messages.ackReaction": "Emoji reaction used to acknowledge inbound messages (empty disables).",
|
||||
"messages.ackReactionScope":
|
||||
'When to send ack reactions ("group-mentions", "group-all", "direct", "all").',
|
||||
"channels.telegram.dmPolicy":
|
||||
@@ -325,18 +289,15 @@ const FIELD_HELP: Record<string, string> = {
|
||||
"Preferred breakpoints for Telegram draft chunks (paragraph | newline | sentence). Default: paragraph.",
|
||||
"channels.telegram.retry.attempts":
|
||||
"Max retry attempts for outbound Telegram API calls (default: 3).",
|
||||
"channels.telegram.retry.minDelayMs":
|
||||
"Minimum retry delay in ms for Telegram outbound calls.",
|
||||
"channels.telegram.retry.minDelayMs": "Minimum retry delay in ms for Telegram outbound calls.",
|
||||
"channels.telegram.retry.maxDelayMs":
|
||||
"Maximum retry delay cap in ms for Telegram outbound calls.",
|
||||
"channels.telegram.retry.jitter":
|
||||
"Jitter factor (0-1) applied to Telegram retry delays.",
|
||||
"channels.telegram.retry.jitter": "Jitter factor (0-1) applied to Telegram retry delays.",
|
||||
"channels.telegram.timeoutSeconds":
|
||||
"Max seconds before Telegram API requests are aborted (default: 500 per grammY).",
|
||||
"channels.whatsapp.dmPolicy":
|
||||
'Direct message access control ("pairing" recommended). "open" requires channels.whatsapp.allowFrom=["*"].',
|
||||
"channels.whatsapp.selfChatMode":
|
||||
"Same-phone setup (bot uses your personal WhatsApp number).",
|
||||
"channels.whatsapp.selfChatMode": "Same-phone setup (bot uses your personal WhatsApp number).",
|
||||
"channels.signal.dmPolicy":
|
||||
'Direct message access control ("pairing" recommended). "open" requires channels.signal.allowFrom=["*"].',
|
||||
"channels.imessage.dmPolicy":
|
||||
@@ -345,14 +306,10 @@ const FIELD_HELP: Record<string, string> = {
|
||||
'Direct message access control ("pairing" recommended). "open" requires channels.discord.dm.allowFrom=["*"].',
|
||||
"channels.discord.retry.attempts":
|
||||
"Max retry attempts for outbound Discord API calls (default: 3).",
|
||||
"channels.discord.retry.minDelayMs":
|
||||
"Minimum retry delay in ms for Discord outbound calls.",
|
||||
"channels.discord.retry.maxDelayMs":
|
||||
"Maximum retry delay cap in ms for Discord outbound calls.",
|
||||
"channels.discord.retry.jitter":
|
||||
"Jitter factor (0-1) applied to Discord retry delays.",
|
||||
"channels.discord.maxLinesPerMessage":
|
||||
"Soft max line count per Discord message (default: 17).",
|
||||
"channels.discord.retry.minDelayMs": "Minimum retry delay in ms for Discord outbound calls.",
|
||||
"channels.discord.retry.maxDelayMs": "Maximum retry delay cap in ms for Discord outbound calls.",
|
||||
"channels.discord.retry.jitter": "Jitter factor (0-1) applied to Discord retry delays.",
|
||||
"channels.discord.maxLinesPerMessage": "Soft max line count per Discord message (default: 17).",
|
||||
"channels.slack.dm.policy":
|
||||
'Direct message access control ("pairing" recommended). "open" requires channels.slack.dm.allowFrom=["*"].',
|
||||
};
|
||||
@@ -403,10 +360,7 @@ function applySensitiveHints(hints: ConfigUiHints): ConfigUiHints {
|
||||
return next;
|
||||
}
|
||||
|
||||
function applyPluginHints(
|
||||
hints: ConfigUiHints,
|
||||
plugins: PluginUiMetadata[],
|
||||
): ConfigUiHints {
|
||||
function applyPluginHints(hints: ConfigUiHints, plugins: PluginUiMetadata[]): ConfigUiHints {
|
||||
const next: ConfigUiHints = { ...hints };
|
||||
for (const plugin of plugins) {
|
||||
const id = plugin.id.trim();
|
||||
@@ -465,9 +419,7 @@ function buildBaseConfigSchema(): ConfigSchemaResponse {
|
||||
return next;
|
||||
}
|
||||
|
||||
export function buildConfigSchema(params?: {
|
||||
plugins?: PluginUiMetadata[];
|
||||
}): ConfigSchemaResponse {
|
||||
export function buildConfigSchema(params?: { plugins?: PluginUiMetadata[] }): ConfigSchemaResponse {
|
||||
const base = buildBaseConfigSchema();
|
||||
const plugins = params?.plugins ?? [];
|
||||
if (plugins.length === 0) return base;
|
||||
|
||||
@@ -17,9 +17,7 @@ import {
|
||||
|
||||
describe("sessions", () => {
|
||||
it("returns normalized per-sender key", () => {
|
||||
expect(deriveSessionKey("per-sender", { From: "whatsapp:+1555" })).toBe(
|
||||
"+1555",
|
||||
);
|
||||
expect(deriveSessionKey("per-sender", { From: "whatsapp:+1555" })).toBe("+1555");
|
||||
});
|
||||
|
||||
it("falls back to unknown when sender missing", () => {
|
||||
@@ -31,9 +29,7 @@ describe("sessions", () => {
|
||||
});
|
||||
|
||||
it("keeps group chats distinct", () => {
|
||||
expect(deriveSessionKey("per-sender", { From: "12345-678@g.us" })).toBe(
|
||||
"group:12345-678@g.us",
|
||||
);
|
||||
expect(deriveSessionKey("per-sender", { From: "12345-678@g.us" })).toBe("group:12345-678@g.us");
|
||||
});
|
||||
|
||||
it("prefixes group keys with provider when available", () => {
|
||||
@@ -48,11 +44,7 @@ describe("sessions", () => {
|
||||
|
||||
it("keeps explicit provider when provided in group key", () => {
|
||||
expect(
|
||||
resolveSessionKey(
|
||||
"per-sender",
|
||||
{ From: "group:discord:12345", ChatType: "group" },
|
||||
"main",
|
||||
),
|
||||
resolveSessionKey("per-sender", { From: "group:discord:12345", ChatType: "group" }, "main"),
|
||||
).toBe("agent:main:discord:group:12345");
|
||||
});
|
||||
|
||||
@@ -69,9 +61,7 @@ describe("sessions", () => {
|
||||
});
|
||||
|
||||
it("collapses direct chats to main by default", () => {
|
||||
expect(resolveSessionKey("per-sender", { From: "+1555" })).toBe(
|
||||
"agent:main:main",
|
||||
);
|
||||
expect(resolveSessionKey("per-sender", { From: "+1555" })).toBe("agent:main:main");
|
||||
});
|
||||
|
||||
it("collapses direct chats to main even when sender missing", () => {
|
||||
@@ -79,9 +69,9 @@ describe("sessions", () => {
|
||||
});
|
||||
|
||||
it("maps direct chats to main key when provided", () => {
|
||||
expect(
|
||||
resolveSessionKey("per-sender", { From: "whatsapp:+1555" }, "main"),
|
||||
).toBe("agent:main:main");
|
||||
expect(resolveSessionKey("per-sender", { From: "whatsapp:+1555" }, "main")).toBe(
|
||||
"agent:main:main",
|
||||
);
|
||||
});
|
||||
|
||||
it("uses custom main key when provided", () => {
|
||||
@@ -95,9 +85,9 @@ describe("sessions", () => {
|
||||
});
|
||||
|
||||
it("leaves groups untouched even with main key", () => {
|
||||
expect(
|
||||
resolveSessionKey("per-sender", { From: "12345-678@g.us" }, "main"),
|
||||
).toBe("agent:main:group:12345-678@g.us");
|
||||
expect(resolveSessionKey("per-sender", { From: "12345-678@g.us" }, "main")).toBe(
|
||||
"agent:main:group:12345-678@g.us",
|
||||
);
|
||||
});
|
||||
|
||||
it("updateLastRoute persists channel and target", async () => {
|
||||
@@ -169,10 +159,7 @@ describe("sessions", () => {
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const store = loadSessionStore(storePath) as unknown as Record<
|
||||
string,
|
||||
Record<string, unknown>
|
||||
>;
|
||||
const store = loadSessionStore(storePath) as unknown as Record<string, Record<string, unknown>>;
|
||||
const entry = store[mainSessionKey] ?? {};
|
||||
expect(entry.channel).toBe("slack");
|
||||
expect(entry.provider).toBeUndefined();
|
||||
@@ -185,9 +172,7 @@ describe("sessions", () => {
|
||||
{ CLAWDBOT_STATE_DIR: "/custom/state" } as NodeJS.ProcessEnv,
|
||||
() => "/home/ignored",
|
||||
);
|
||||
expect(dir).toBe(
|
||||
path.join(path.resolve("/custom/state"), "agents", "main", "sessions"),
|
||||
);
|
||||
expect(dir).toBe(path.join(path.resolve("/custom/state"), "agents", "main", "sessions"));
|
||||
});
|
||||
|
||||
it("falls back to CLAWDIS_STATE_DIR for session transcripts dir", () => {
|
||||
@@ -195,9 +180,7 @@ describe("sessions", () => {
|
||||
{ CLAWDIS_STATE_DIR: "/legacy/state" } as NodeJS.ProcessEnv,
|
||||
() => "/home/ignored",
|
||||
);
|
||||
expect(dir).toBe(
|
||||
path.join(path.resolve("/legacy/state"), "agents", "main", "sessions"),
|
||||
);
|
||||
expect(dir).toBe(path.join(path.resolve("/legacy/state"), "agents", "main", "sessions"));
|
||||
});
|
||||
|
||||
it("includes topic ids in session transcript filenames", () => {
|
||||
@@ -231,13 +214,7 @@ describe("sessions", () => {
|
||||
agentId: "codex",
|
||||
});
|
||||
expect(sessionFile).toBe(
|
||||
path.join(
|
||||
path.resolve("/custom/state"),
|
||||
"agents",
|
||||
"codex",
|
||||
"sessions",
|
||||
"sess-2.jsonl",
|
||||
),
|
||||
path.join(path.resolve("/custom/state"), "agents", "codex", "sessions", "sess-2.jsonl"),
|
||||
);
|
||||
} finally {
|
||||
if (prev === undefined) {
|
||||
@@ -289,9 +266,7 @@ describe("sessions", () => {
|
||||
]);
|
||||
|
||||
const store = loadSessionStore(storePath);
|
||||
expect(store[mainSessionKey]?.modelOverride).toBe(
|
||||
"anthropic/claude-opus-4-5",
|
||||
);
|
||||
expect(store[mainSessionKey]?.modelOverride).toBe("anthropic/claude-opus-4-5");
|
||||
expect(store[mainSessionKey]?.thinkingLevel).toBe("high");
|
||||
await expect(fs.stat(`${storePath}.lock`)).rejects.toThrow();
|
||||
});
|
||||
|
||||
@@ -44,20 +44,13 @@ export function buildGroupDisplayName(params: {
|
||||
if (!params.room && token.startsWith("#")) {
|
||||
token = token.replace(/^#+/, "");
|
||||
}
|
||||
if (
|
||||
token &&
|
||||
!/^[@#]/.test(token) &&
|
||||
!token.startsWith("g-") &&
|
||||
!token.includes("#")
|
||||
) {
|
||||
if (token && !/^[@#]/.test(token) && !token.startsWith("g-") && !token.includes("#")) {
|
||||
token = `g-${token}`;
|
||||
}
|
||||
return token ? `${providerKey}:${token}` : providerKey;
|
||||
}
|
||||
|
||||
export function resolveGroupSessionKey(
|
||||
ctx: MsgContext,
|
||||
): GroupKeyResolution | null {
|
||||
export function resolveGroupSessionKey(ctx: MsgContext): GroupKeyResolution | null {
|
||||
const from = typeof ctx.From === "string" ? ctx.From.trim() : "";
|
||||
if (!from) return null;
|
||||
const chatType = ctx.ChatType?.trim().toLowerCase();
|
||||
@@ -71,9 +64,7 @@ export function resolveGroupSessionKey(
|
||||
|
||||
const providerHint = ctx.Provider?.trim().toLowerCase();
|
||||
const hasLegacyGroupPrefix = from.startsWith("group:");
|
||||
const raw = (
|
||||
hasLegacyGroupPrefix ? from.slice("group:".length) : from
|
||||
).trim();
|
||||
const raw = (hasLegacyGroupPrefix ? from.slice("group:".length) : from).trim();
|
||||
|
||||
let provider: string | undefined;
|
||||
let kind: "group" | "channel" | undefined;
|
||||
|
||||
@@ -15,9 +15,7 @@ export function resolveMainSessionKey(cfg?: {
|
||||
if (cfg?.session?.scope === "global") return "global";
|
||||
const agents = cfg?.agents?.list ?? [];
|
||||
const defaultAgentId =
|
||||
agents.find((agent) => agent?.default)?.id ??
|
||||
agents[0]?.id ??
|
||||
DEFAULT_AGENT_ID;
|
||||
agents.find((agent) => agent?.default)?.id ?? agents[0]?.id ?? DEFAULT_AGENT_ID;
|
||||
const agentId = normalizeAgentId(defaultAgentId);
|
||||
const mainKey = normalizeMainKey(cfg?.session?.mainKey);
|
||||
return buildAgentMainSessionKey({ agentId, mainKey });
|
||||
@@ -54,10 +52,7 @@ export function canonicalizeMainSessionAlias(params: {
|
||||
});
|
||||
|
||||
const isMainAlias =
|
||||
raw === "main" ||
|
||||
raw === mainKey ||
|
||||
raw === agentMainSessionKey ||
|
||||
raw === agentMainAliasKey;
|
||||
raw === "main" || raw === mainKey || raw === agentMainSessionKey || raw === agentMainAliasKey;
|
||||
|
||||
if (params.cfg?.session?.scope === "global" && isMainAlias) return "global";
|
||||
if (isMainAlias) return agentMainSessionKey;
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import {
|
||||
DEFAULT_AGENT_ID,
|
||||
normalizeAgentId,
|
||||
} from "../../routing/session-key.js";
|
||||
import { DEFAULT_AGENT_ID, normalizeAgentId } from "../../routing/session-key.js";
|
||||
import { resolveStateDir } from "../paths.js";
|
||||
import type { SessionEntry } from "./types.js";
|
||||
|
||||
@@ -42,9 +39,7 @@ export function resolveSessionTranscriptPath(
|
||||
topicId?: number,
|
||||
): string {
|
||||
const fileName =
|
||||
topicId !== undefined
|
||||
? `${sessionId}-topic-${topicId}.jsonl`
|
||||
: `${sessionId}.jsonl`;
|
||||
topicId !== undefined ? `${sessionId}-topic-${topicId}.jsonl` : `${sessionId}.jsonl`;
|
||||
return path.join(resolveAgentSessionsDir(agentId), fileName);
|
||||
}
|
||||
|
||||
@@ -54,9 +49,7 @@ export function resolveSessionFilePath(
|
||||
opts?: { agentId?: string },
|
||||
): string {
|
||||
const candidate = entry?.sessionFile?.trim();
|
||||
return candidate
|
||||
? candidate
|
||||
: resolveSessionTranscriptPath(sessionId, opts?.agentId);
|
||||
return candidate ? candidate : resolveSessionTranscriptPath(sessionId, opts?.agentId);
|
||||
}
|
||||
|
||||
export function resolveStorePath(store?: string, opts?: { agentId?: string }) {
|
||||
@@ -69,7 +62,6 @@ export function resolveStorePath(store?: string, opts?: { agentId?: string }) {
|
||||
}
|
||||
return path.resolve(expanded);
|
||||
}
|
||||
if (store.startsWith("~"))
|
||||
return path.resolve(store.replace(/^~(?=$|[\\/])/, os.homedir()));
|
||||
if (store.startsWith("~")) return path.resolve(store.replace(/^~(?=$|[\\/])/, os.homedir()));
|
||||
return path.resolve(store);
|
||||
}
|
||||
|
||||
@@ -21,11 +21,7 @@ export function deriveSessionKey(scope: SessionScope, ctx: MsgContext) {
|
||||
* Resolve the session key with a canonical direct-chat bucket (default: "main").
|
||||
* All non-group direct chats collapse to this bucket; groups stay isolated.
|
||||
*/
|
||||
export function resolveSessionKey(
|
||||
scope: SessionScope,
|
||||
ctx: MsgContext,
|
||||
mainKey?: string,
|
||||
) {
|
||||
export function resolveSessionKey(scope: SessionScope, ctx: MsgContext, mainKey?: string) {
|
||||
const explicit = ctx.SessionKey?.trim();
|
||||
if (explicit) return explicit;
|
||||
const raw = deriveSessionKey(scope, ctx);
|
||||
@@ -35,10 +31,7 @@ export function resolveSessionKey(
|
||||
agentId: DEFAULT_AGENT_ID,
|
||||
mainKey: canonicalMainKey,
|
||||
});
|
||||
const isGroup =
|
||||
raw.startsWith("group:") ||
|
||||
raw.includes(":group:") ||
|
||||
raw.includes(":channel:");
|
||||
const isGroup = raw.startsWith("group:") || raw.includes(":group:") || raw.includes(":channel:");
|
||||
if (!isGroup) return canonical;
|
||||
return `agent:${DEFAULT_AGENT_ID}:${raw}`;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import JSON5 from "json5";
|
||||
import {
|
||||
getFileMtimeMs,
|
||||
isCacheEnabled,
|
||||
resolveCacheTtlMs,
|
||||
} from "../cache-utils.js";
|
||||
import { getFileMtimeMs, isCacheEnabled, resolveCacheTtlMs } from "../cache-utils.js";
|
||||
import { mergeSessionEntry, type SessionEntry } from "./types.js";
|
||||
|
||||
// ============================================================================
|
||||
@@ -49,9 +45,7 @@ export function clearSessionStoreCacheForTest(): void {
|
||||
SESSION_STORE_CACHE.clear();
|
||||
}
|
||||
|
||||
export function loadSessionStore(
|
||||
storePath: string,
|
||||
): Record<string, SessionEntry> {
|
||||
export function loadSessionStore(storePath: string): Record<string, SessionEntry> {
|
||||
// Check cache first if enabled
|
||||
if (isSessionStoreCacheEnabled()) {
|
||||
const cached = SESSION_STORE_CACHE.get(storePath);
|
||||
@@ -87,10 +81,7 @@ export function loadSessionStore(
|
||||
rec.channel = rec.provider;
|
||||
delete rec.provider;
|
||||
}
|
||||
if (
|
||||
typeof rec.lastChannel !== "string" &&
|
||||
typeof rec.lastProvider === "string"
|
||||
) {
|
||||
if (typeof rec.lastChannel !== "string" && typeof rec.lastProvider === "string") {
|
||||
rec.lastChannel = rec.lastProvider;
|
||||
delete rec.lastProvider;
|
||||
}
|
||||
@@ -288,9 +279,7 @@ export async function updateLastRoute(params: {
|
||||
updatedAt: Math.max(existing?.updatedAt ?? 0, now),
|
||||
lastChannel: channel,
|
||||
lastTo: to?.trim() ? to.trim() : undefined,
|
||||
lastAccountId: accountId?.trim()
|
||||
? accountId.trim()
|
||||
: existing?.lastAccountId,
|
||||
lastAccountId: accountId?.trim() ? accountId.trim() : existing?.lastAccountId,
|
||||
});
|
||||
store[sessionKey] = next;
|
||||
await saveSessionStoreUnlocked(storePath, store);
|
||||
|
||||
@@ -67,13 +67,8 @@ export function mergeSessionEntry(
|
||||
existing: SessionEntry | undefined,
|
||||
patch: Partial<SessionEntry>,
|
||||
): SessionEntry {
|
||||
const sessionId =
|
||||
patch.sessionId ?? existing?.sessionId ?? crypto.randomUUID();
|
||||
const updatedAt = Math.max(
|
||||
existing?.updatedAt ?? 0,
|
||||
patch.updatedAt ?? 0,
|
||||
Date.now(),
|
||||
);
|
||||
const sessionId = patch.sessionId ?? existing?.sessionId ?? crypto.randomUUID();
|
||||
const updatedAt = Math.max(existing?.updatedAt ?? 0, patch.updatedAt ?? 0, Date.now());
|
||||
if (!existing) return { ...patch, sessionId, updatedAt };
|
||||
return { ...existing, ...patch, sessionId, updatedAt };
|
||||
}
|
||||
|
||||
@@ -8,16 +8,14 @@ type TalkApiKeyDeps = {
|
||||
path?: typeof path;
|
||||
};
|
||||
|
||||
export function readTalkApiKeyFromProfile(
|
||||
deps: TalkApiKeyDeps = {},
|
||||
): string | null {
|
||||
export function readTalkApiKeyFromProfile(deps: TalkApiKeyDeps = {}): string | null {
|
||||
const fsImpl = deps.fs ?? fs;
|
||||
const osImpl = deps.os ?? os;
|
||||
const pathImpl = deps.path ?? path;
|
||||
|
||||
const home = osImpl.homedir();
|
||||
const candidates = [".profile", ".zprofile", ".zshrc", ".bashrc"].map(
|
||||
(name) => pathImpl.join(home, name),
|
||||
const candidates = [".profile", ".zprofile", ".zshrc", ".bashrc"].map((name) =>
|
||||
pathImpl.join(home, name),
|
||||
);
|
||||
for (const candidate of candidates) {
|
||||
if (!fsImpl.existsSync(candidate)) continue;
|
||||
|
||||
@@ -2,9 +2,7 @@ import { vi } from "vitest";
|
||||
|
||||
import { withTempHome as withTempHomeBase } from "../../test/helpers/temp-home.js";
|
||||
|
||||
export async function withTempHome<T>(
|
||||
fn: (home: string) => Promise<T>,
|
||||
): Promise<T> {
|
||||
export async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
|
||||
return withTempHomeBase(fn, { prefix: "clawdbot-config-" });
|
||||
}
|
||||
|
||||
|
||||
@@ -71,14 +71,7 @@ export type SessionConfig = {
|
||||
export type LoggingConfig = {
|
||||
level?: "silent" | "fatal" | "error" | "warn" | "info" | "debug" | "trace";
|
||||
file?: string;
|
||||
consoleLevel?:
|
||||
| "silent"
|
||||
| "fatal"
|
||||
| "error"
|
||||
| "warn"
|
||||
| "info"
|
||||
| "debug"
|
||||
| "trace";
|
||||
consoleLevel?: "silent" | "fatal" | "error" | "warn" | "info" | "debug" | "trace";
|
||||
consoleStyle?: "pretty" | "compact" | "json";
|
||||
/** Redact sensitive tokens in tool summaries. Default: "tools". */
|
||||
redactSensitive?: "off" | "tools";
|
||||
@@ -102,9 +95,7 @@ export type WebConfig = {
|
||||
};
|
||||
|
||||
// Provider docking: allowlists keyed by provider id (and internal "webchat").
|
||||
export type AgentElevatedAllowFromConfig = Partial<
|
||||
Record<string, Array<string | number>>
|
||||
>;
|
||||
export type AgentElevatedAllowFromConfig = Partial<Record<string, Array<string | number>>>;
|
||||
|
||||
export type IdentityConfig = {
|
||||
name?: string;
|
||||
|
||||
@@ -33,11 +33,7 @@ export type DiscordGuildChannelConfig = {
|
||||
systemPrompt?: string;
|
||||
};
|
||||
|
||||
export type DiscordReactionNotificationMode =
|
||||
| "off"
|
||||
| "own"
|
||||
| "all"
|
||||
| "allowlist";
|
||||
export type DiscordReactionNotificationMode = "off" | "own" | "all" | "allowlist";
|
||||
|
||||
export type DiscordGuildEntry = {
|
||||
slug?: string;
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import type {
|
||||
BlockStreamingCoalesceConfig,
|
||||
DmPolicy,
|
||||
GroupPolicy,
|
||||
} from "./types.base.js";
|
||||
import type { BlockStreamingCoalesceConfig, DmPolicy, GroupPolicy } from "./types.base.js";
|
||||
import type { DmConfig } from "./types.messages.js";
|
||||
|
||||
export type IMessageAccountConfig = {
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import type {
|
||||
QueueDropPolicy,
|
||||
QueueMode,
|
||||
QueueModeByProvider,
|
||||
} from "./types.queue.js";
|
||||
import type { QueueDropPolicy, QueueMode, QueueModeByProvider } from "./types.queue.js";
|
||||
|
||||
export type GroupChatConfig = {
|
||||
mentionPatterns?: string[];
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import type {
|
||||
BlockStreamingCoalesceConfig,
|
||||
DmPolicy,
|
||||
GroupPolicy,
|
||||
} from "./types.base.js";
|
||||
import type { BlockStreamingCoalesceConfig, DmPolicy, GroupPolicy } from "./types.base.js";
|
||||
import type { DmConfig } from "./types.messages.js";
|
||||
|
||||
export type MSTeamsWebhookConfig = {
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import type {
|
||||
BlockStreamingCoalesceConfig,
|
||||
DmPolicy,
|
||||
GroupPolicy,
|
||||
} from "./types.base.js";
|
||||
import type { BlockStreamingCoalesceConfig, DmPolicy, GroupPolicy } from "./types.base.js";
|
||||
import type { DmConfig } from "./types.messages.js";
|
||||
|
||||
export type SignalReactionNotificationMode =
|
||||
| "off"
|
||||
| "own"
|
||||
| "all"
|
||||
| "allowlist";
|
||||
export type SignalReactionNotificationMode = "off" | "own" | "all" | "allowlist";
|
||||
|
||||
export type SignalAccountConfig = {
|
||||
/** Optional display name for this account (used in CLI/UI lists). */
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import type {
|
||||
BlockStreamingCoalesceConfig,
|
||||
DmPolicy,
|
||||
GroupPolicy,
|
||||
} from "./types.base.js";
|
||||
import type { BlockStreamingCoalesceConfig, DmPolicy, GroupPolicy } from "./types.base.js";
|
||||
import type { DmConfig } from "./types.messages.js";
|
||||
|
||||
export type WhatsAppActionConfig = {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
findDuplicateAgentDirs,
|
||||
formatDuplicateAgentDirError,
|
||||
} from "./agent-dirs.js";
|
||||
import { findDuplicateAgentDirs, formatDuplicateAgentDirError } from "./agent-dirs.js";
|
||||
import { applyModelDefaults, applySessionDefaults } from "./defaults.js";
|
||||
import { findLegacyConfigIssues } from "./legacy.js";
|
||||
import type { ClawdbotConfig, ConfigValidationIssue } from "./types.js";
|
||||
@@ -9,9 +6,7 @@ import { ClawdbotSchema } from "./zod-schema.js";
|
||||
|
||||
export function validateConfigObject(
|
||||
raw: unknown,
|
||||
):
|
||||
| { ok: true; config: ClawdbotConfig }
|
||||
| { ok: false; issues: ConfigValidationIssue[] } {
|
||||
): { ok: true; config: ClawdbotConfig } | { ok: false; issues: ConfigValidationIssue[] } {
|
||||
const legacyIssues = findLegacyConfigIssues(raw);
|
||||
if (legacyIssues.length > 0) {
|
||||
return {
|
||||
@@ -46,8 +41,6 @@ export function validateConfigObject(
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
config: applyModelDefaults(
|
||||
applySessionDefaults(validated.data as ClawdbotConfig),
|
||||
),
|
||||
config: applyModelDefaults(applySessionDefaults(validated.data as ClawdbotConfig)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -47,11 +47,7 @@ export const AgentDefaultsSchema = z
|
||||
contextPruning: z
|
||||
.object({
|
||||
mode: z
|
||||
.union([
|
||||
z.literal("off"),
|
||||
z.literal("adaptive"),
|
||||
z.literal("aggressive"),
|
||||
])
|
||||
.union([z.literal("off"), z.literal("adaptive"), z.literal("aggressive")])
|
||||
.optional(),
|
||||
keepLastAssistants: z.number().int().nonnegative().optional(),
|
||||
softTrimRatio: z.number().min(0).max(1).optional(),
|
||||
@@ -80,9 +76,7 @@ export const AgentDefaultsSchema = z
|
||||
.optional(),
|
||||
compaction: z
|
||||
.object({
|
||||
mode: z
|
||||
.union([z.literal("default"), z.literal("safeguard")])
|
||||
.optional(),
|
||||
mode: z.union([z.literal("default"), z.literal("safeguard")]).optional(),
|
||||
reserveTokensFloor: z.number().int().nonnegative().optional(),
|
||||
memoryFlush: z
|
||||
.object({
|
||||
@@ -106,12 +100,8 @@ export const AgentDefaultsSchema = z
|
||||
.optional(),
|
||||
verboseDefault: z.union([z.literal("off"), z.literal("on")]).optional(),
|
||||
elevatedDefault: z.union([z.literal("off"), z.literal("on")]).optional(),
|
||||
blockStreamingDefault: z
|
||||
.union([z.literal("off"), z.literal("on")])
|
||||
.optional(),
|
||||
blockStreamingBreak: z
|
||||
.union([z.literal("text_end"), z.literal("message_end")])
|
||||
.optional(),
|
||||
blockStreamingDefault: z.union([z.literal("off"), z.literal("on")]).optional(),
|
||||
blockStreamingBreak: z.union([z.literal("text_end"), z.literal("message_end")]).optional(),
|
||||
blockStreamingChunk: BlockStreamingChunkSchema.optional(),
|
||||
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
||||
humanDelay: HumanDelaySchema.optional(),
|
||||
@@ -145,22 +135,10 @@ export const AgentDefaultsSchema = z
|
||||
.optional(),
|
||||
sandbox: z
|
||||
.object({
|
||||
mode: z
|
||||
.union([z.literal("off"), z.literal("non-main"), z.literal("all")])
|
||||
.optional(),
|
||||
workspaceAccess: z
|
||||
.union([z.literal("none"), z.literal("ro"), z.literal("rw")])
|
||||
.optional(),
|
||||
sessionToolsVisibility: z
|
||||
.union([z.literal("spawned"), z.literal("all")])
|
||||
.optional(),
|
||||
scope: z
|
||||
.union([
|
||||
z.literal("session"),
|
||||
z.literal("agent"),
|
||||
z.literal("shared"),
|
||||
])
|
||||
.optional(),
|
||||
mode: z.union([z.literal("off"), z.literal("non-main"), z.literal("all")]).optional(),
|
||||
workspaceAccess: z.union([z.literal("none"), z.literal("ro"), z.literal("rw")]).optional(),
|
||||
sessionToolsVisibility: z.union([z.literal("spawned"), z.literal("all")]).optional(),
|
||||
scope: z.union([z.literal("session"), z.literal("agent"), z.literal("shared")]).optional(),
|
||||
perSession: z.boolean().optional(),
|
||||
workspaceRoot: z.string().optional(),
|
||||
docker: SandboxDockerSchema,
|
||||
|
||||
@@ -114,12 +114,7 @@ export const ToolPolicySchema = z
|
||||
.optional();
|
||||
|
||||
export const ToolProfileSchema = z
|
||||
.union([
|
||||
z.literal("minimal"),
|
||||
z.literal("coding"),
|
||||
z.literal("messaging"),
|
||||
z.literal("full"),
|
||||
])
|
||||
.union([z.literal("minimal"), z.literal("coding"), z.literal("messaging"), z.literal("full")])
|
||||
.optional();
|
||||
|
||||
// Provider docking: allowlists keyed by provider id (no schema updates when adding providers).
|
||||
@@ -129,18 +124,10 @@ export const ElevatedAllowFromSchema = z
|
||||
|
||||
export const AgentSandboxSchema = z
|
||||
.object({
|
||||
mode: z
|
||||
.union([z.literal("off"), z.literal("non-main"), z.literal("all")])
|
||||
.optional(),
|
||||
workspaceAccess: z
|
||||
.union([z.literal("none"), z.literal("ro"), z.literal("rw")])
|
||||
.optional(),
|
||||
sessionToolsVisibility: z
|
||||
.union([z.literal("spawned"), z.literal("all")])
|
||||
.optional(),
|
||||
scope: z
|
||||
.union([z.literal("session"), z.literal("agent"), z.literal("shared")])
|
||||
.optional(),
|
||||
mode: z.union([z.literal("off"), z.literal("non-main"), z.literal("all")]).optional(),
|
||||
workspaceAccess: z.union([z.literal("none"), z.literal("ro"), z.literal("rw")]).optional(),
|
||||
sessionToolsVisibility: z.union([z.literal("spawned"), z.literal("all")]).optional(),
|
||||
scope: z.union([z.literal("session"), z.literal("agent"), z.literal("shared")]).optional(),
|
||||
perSession: z.boolean().optional(),
|
||||
workspaceRoot: z.string().optional(),
|
||||
docker: SandboxDockerSchema,
|
||||
|
||||
@@ -19,11 +19,7 @@ export const BindingsSchema = z
|
||||
accountId: z.string().optional(),
|
||||
peer: z
|
||||
.object({
|
||||
kind: z.union([
|
||||
z.literal("dm"),
|
||||
z.literal("group"),
|
||||
z.literal("channel"),
|
||||
]),
|
||||
kind: z.union([z.literal("dm"), z.literal("group"), z.literal("channel")]),
|
||||
id: z.string(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
@@ -88,11 +88,7 @@ export const QueueDropSchema = z.union([
|
||||
z.literal("new"),
|
||||
z.literal("summarize"),
|
||||
]);
|
||||
export const ReplyToModeSchema = z.union([
|
||||
z.literal("off"),
|
||||
z.literal("first"),
|
||||
z.literal("all"),
|
||||
]);
|
||||
export const ReplyToModeSchema = z.union([z.literal("off"), z.literal("first"), z.literal("all")]);
|
||||
|
||||
// GroupPolicySchema: controls how group messages are handled
|
||||
// Used with .default("allowlist").optional() pattern:
|
||||
@@ -100,12 +96,7 @@ export const ReplyToModeSchema = z.union([
|
||||
// - .default("allowlist") ensures runtime always resolves to "allowlist" if not provided
|
||||
export const GroupPolicySchema = z.enum(["open", "disabled", "allowlist"]);
|
||||
|
||||
export const DmPolicySchema = z.enum([
|
||||
"pairing",
|
||||
"allowlist",
|
||||
"open",
|
||||
"disabled",
|
||||
]);
|
||||
export const DmPolicySchema = z.enum(["pairing", "allowlist", "open", "disabled"]);
|
||||
|
||||
export const BlockStreamingCoalesceSchema = z.object({
|
||||
minChars: z.number().int().positive().optional(),
|
||||
@@ -117,18 +108,12 @@ export const BlockStreamingChunkSchema = z.object({
|
||||
minChars: z.number().int().positive().optional(),
|
||||
maxChars: z.number().int().positive().optional(),
|
||||
breakPreference: z
|
||||
.union([
|
||||
z.literal("paragraph"),
|
||||
z.literal("newline"),
|
||||
z.literal("sentence"),
|
||||
])
|
||||
.union([z.literal("paragraph"), z.literal("newline"), z.literal("sentence")])
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const HumanDelaySchema = z.object({
|
||||
mode: z
|
||||
.union([z.literal("off"), z.literal("natural"), z.literal("custom")])
|
||||
.optional(),
|
||||
mode: z.union([z.literal("off"), z.literal("natural"), z.literal("custom")]).optional(),
|
||||
minMs: z.number().int().nonnegative().optional(),
|
||||
maxMs: z.number().int().nonnegative().optional(),
|
||||
});
|
||||
@@ -136,12 +121,8 @@ export const HumanDelaySchema = z.object({
|
||||
export const CliBackendSchema = z.object({
|
||||
command: z.string(),
|
||||
args: z.array(z.string()).optional(),
|
||||
output: z
|
||||
.union([z.literal("json"), z.literal("text"), z.literal("jsonl")])
|
||||
.optional(),
|
||||
resumeOutput: z
|
||||
.union([z.literal("json"), z.literal("text"), z.literal("jsonl")])
|
||||
.optional(),
|
||||
output: z.union([z.literal("json"), z.literal("text"), z.literal("jsonl")]).optional(),
|
||||
resumeOutput: z.union([z.literal("json"), z.literal("text"), z.literal("jsonl")]).optional(),
|
||||
input: z.union([z.literal("arg"), z.literal("stdin")]).optional(),
|
||||
maxPromptArgChars: z.number().int().positive().optional(),
|
||||
env: z.record(z.string(), z.string()).optional(),
|
||||
@@ -151,14 +132,10 @@ export const CliBackendSchema = z.object({
|
||||
sessionArg: z.string().optional(),
|
||||
sessionArgs: z.array(z.string()).optional(),
|
||||
resumeArgs: z.array(z.string()).optional(),
|
||||
sessionMode: z
|
||||
.union([z.literal("always"), z.literal("existing"), z.literal("none")])
|
||||
.optional(),
|
||||
sessionMode: z.union([z.literal("always"), z.literal("existing"), z.literal("none")]).optional(),
|
||||
sessionIdFields: z.array(z.string()).optional(),
|
||||
systemPromptArg: z.string().optional(),
|
||||
systemPromptMode: z
|
||||
.union([z.literal("append"), z.literal("replace")])
|
||||
.optional(),
|
||||
systemPromptMode: z.union([z.literal("append"), z.literal("replace")]).optional(),
|
||||
systemPromptWhen: z
|
||||
.union([z.literal("first"), z.literal("always"), z.literal("never")])
|
||||
.optional(),
|
||||
@@ -237,9 +214,7 @@ export const TranscribeAudioSchema = z
|
||||
})
|
||||
.optional();
|
||||
|
||||
export const HexColorSchema = z
|
||||
.string()
|
||||
.regex(/^#?[0-9a-fA-F]{6}$/, "expected hex color (RRGGBB)");
|
||||
export const HexColorSchema = z.string().regex(/^#?[0-9a-fA-F]{6}$/, "expected hex color (RRGGBB)");
|
||||
|
||||
export const ExecutableTokenSchema = z
|
||||
.string()
|
||||
@@ -252,10 +227,7 @@ export const ToolsAudioTranscriptionSchema = z
|
||||
})
|
||||
.optional();
|
||||
|
||||
export const NativeCommandsSettingSchema = z.union([
|
||||
z.boolean(),
|
||||
z.literal("auto"),
|
||||
]);
|
||||
export const NativeCommandsSettingSchema = z.union([z.boolean(), z.literal("auto")]);
|
||||
|
||||
export const ProviderCommandsSchema = z
|
||||
.object({
|
||||
|
||||
@@ -10,9 +10,7 @@ export const HookMappingSchema = z
|
||||
})
|
||||
.optional(),
|
||||
action: z.union([z.literal("wake"), z.literal("agent")]).optional(),
|
||||
wakeMode: z
|
||||
.union([z.literal("now"), z.literal("next-heartbeat")])
|
||||
.optional(),
|
||||
wakeMode: z.union([z.literal("now"), z.literal("next-heartbeat")]).optional(),
|
||||
name: z.string().optional(),
|
||||
sessionKey: z.string().optional(),
|
||||
messageTemplate: z.string().optional(),
|
||||
@@ -63,9 +61,7 @@ export const HooksGmailSchema = z
|
||||
.optional(),
|
||||
tailscale: z
|
||||
.object({
|
||||
mode: z
|
||||
.union([z.literal("off"), z.literal("serve"), z.literal("funnel")])
|
||||
.optional(),
|
||||
mode: z.union([z.literal("off"), z.literal("serve"), z.literal("funnel")]).optional(),
|
||||
path: z.string().optional(),
|
||||
target: z.string().optional(),
|
||||
})
|
||||
|
||||
@@ -66,18 +66,16 @@ export const TelegramAccountSchemaBase = z.object({
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const TelegramAccountSchema = TelegramAccountSchemaBase.superRefine(
|
||||
(value, ctx) => {
|
||||
requireOpenAllowFrom({
|
||||
policy: value.dmPolicy,
|
||||
allowFrom: value.allowFrom,
|
||||
ctx,
|
||||
path: ["allowFrom"],
|
||||
message:
|
||||
'channels.telegram.dmPolicy="open" requires channels.telegram.allowFrom to include "*"',
|
||||
});
|
||||
},
|
||||
);
|
||||
export const TelegramAccountSchema = TelegramAccountSchemaBase.superRefine((value, ctx) => {
|
||||
requireOpenAllowFrom({
|
||||
policy: value.dmPolicy,
|
||||
allowFrom: value.allowFrom,
|
||||
ctx,
|
||||
path: ["allowFrom"],
|
||||
message:
|
||||
'channels.telegram.dmPolicy="open" requires channels.telegram.allowFrom to include "*"',
|
||||
});
|
||||
});
|
||||
|
||||
export const TelegramConfigSchema = TelegramAccountSchemaBase.extend({
|
||||
accounts: z.record(z.string(), TelegramAccountSchema.optional()).optional(),
|
||||
@@ -126,9 +124,7 @@ export const DiscordGuildSchema = z.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(),
|
||||
users: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
channels: z
|
||||
.record(z.string(), DiscordGuildChannelSchema.optional())
|
||||
.optional(),
|
||||
channels: z.record(z.string(), DiscordGuildChannelSchema.optional()).optional(),
|
||||
});
|
||||
|
||||
export const DiscordAccountSchema = z.object({
|
||||
@@ -281,18 +277,15 @@ export const SignalAccountSchemaBase = z.object({
|
||||
reactionAllowlist: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
});
|
||||
|
||||
export const SignalAccountSchema = SignalAccountSchemaBase.superRefine(
|
||||
(value, ctx) => {
|
||||
requireOpenAllowFrom({
|
||||
policy: value.dmPolicy,
|
||||
allowFrom: value.allowFrom,
|
||||
ctx,
|
||||
path: ["allowFrom"],
|
||||
message:
|
||||
'channels.signal.dmPolicy="open" requires channels.signal.allowFrom to include "*"',
|
||||
});
|
||||
},
|
||||
);
|
||||
export const SignalAccountSchema = SignalAccountSchemaBase.superRefine((value, ctx) => {
|
||||
requireOpenAllowFrom({
|
||||
policy: value.dmPolicy,
|
||||
allowFrom: value.allowFrom,
|
||||
ctx,
|
||||
path: ["allowFrom"],
|
||||
message: 'channels.signal.dmPolicy="open" requires channels.signal.allowFrom to include "*"',
|
||||
});
|
||||
});
|
||||
|
||||
export const SignalConfigSchema = SignalAccountSchemaBase.extend({
|
||||
accounts: z.record(z.string(), SignalAccountSchema.optional()).optional(),
|
||||
@@ -302,8 +295,7 @@ export const SignalConfigSchema = SignalAccountSchemaBase.extend({
|
||||
allowFrom: value.allowFrom,
|
||||
ctx,
|
||||
path: ["allowFrom"],
|
||||
message:
|
||||
'channels.signal.dmPolicy="open" requires channels.signal.allowFrom to include "*"',
|
||||
message: 'channels.signal.dmPolicy="open" requires channels.signal.allowFrom to include "*"',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -313,9 +305,7 @@ export const IMessageAccountSchemaBase = z.object({
|
||||
enabled: z.boolean().optional(),
|
||||
cliPath: ExecutableTokenSchema.optional(),
|
||||
dbPath: z.string().optional(),
|
||||
service: z
|
||||
.union([z.literal("imessage"), z.literal("sms"), z.literal("auto")])
|
||||
.optional(),
|
||||
service: z.union([z.literal("imessage"), z.literal("sms"), z.literal("auto")]).optional(),
|
||||
region: z.string().optional(),
|
||||
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
||||
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
@@ -341,18 +331,16 @@ export const IMessageAccountSchemaBase = z.object({
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const IMessageAccountSchema = IMessageAccountSchemaBase.superRefine(
|
||||
(value, ctx) => {
|
||||
requireOpenAllowFrom({
|
||||
policy: value.dmPolicy,
|
||||
allowFrom: value.allowFrom,
|
||||
ctx,
|
||||
path: ["allowFrom"],
|
||||
message:
|
||||
'channels.imessage.dmPolicy="open" requires channels.imessage.allowFrom to include "*"',
|
||||
});
|
||||
},
|
||||
);
|
||||
export const IMessageAccountSchema = IMessageAccountSchemaBase.superRefine((value, ctx) => {
|
||||
requireOpenAllowFrom({
|
||||
policy: value.dmPolicy,
|
||||
allowFrom: value.allowFrom,
|
||||
ctx,
|
||||
path: ["allowFrom"],
|
||||
message:
|
||||
'channels.imessage.dmPolicy="open" requires channels.imessage.allowFrom to include "*"',
|
||||
});
|
||||
});
|
||||
|
||||
export const IMessageConfigSchema = IMessageAccountSchemaBase.extend({
|
||||
accounts: z.record(z.string(), IMessageAccountSchema.optional()).optional(),
|
||||
|
||||
@@ -41,24 +41,18 @@ export const WhatsAppAccountSchema = z
|
||||
.object({
|
||||
emoji: z.string().optional(),
|
||||
direct: z.boolean().optional().default(true),
|
||||
group: z
|
||||
.enum(["always", "mentions", "never"])
|
||||
.optional()
|
||||
.default("mentions"),
|
||||
group: z.enum(["always", "mentions", "never"]).optional().default("mentions"),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.superRefine((value, ctx) => {
|
||||
if (value.dmPolicy !== "open") return;
|
||||
const allow = (value.allowFrom ?? [])
|
||||
.map((v) => String(v).trim())
|
||||
.filter(Boolean);
|
||||
const allow = (value.allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean);
|
||||
if (allow.includes("*")) return;
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
path: ["allowFrom"],
|
||||
message:
|
||||
'channels.whatsapp.accounts.*.dmPolicy="open" requires allowFrom to include "*"',
|
||||
message: 'channels.whatsapp.accounts.*.dmPolicy="open" requires allowFrom to include "*"',
|
||||
});
|
||||
});
|
||||
|
||||
@@ -100,18 +94,13 @@ export const WhatsAppConfigSchema = z
|
||||
.object({
|
||||
emoji: z.string().optional(),
|
||||
direct: z.boolean().optional().default(true),
|
||||
group: z
|
||||
.enum(["always", "mentions", "never"])
|
||||
.optional()
|
||||
.default("mentions"),
|
||||
group: z.enum(["always", "mentions", "never"]).optional().default("mentions"),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.superRefine((value, ctx) => {
|
||||
if (value.dmPolicy !== "open") return;
|
||||
const allow = (value.allowFrom ?? [])
|
||||
.map((v) => String(v).trim())
|
||||
.filter(Boolean);
|
||||
const allow = (value.allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean);
|
||||
if (allow.includes("*")) return;
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
GroupChatSchema,
|
||||
NativeCommandsSettingSchema,
|
||||
QueueSchema,
|
||||
} from "./zod-schema.core.js";
|
||||
import { GroupChatSchema, NativeCommandsSettingSchema, QueueSchema } from "./zod-schema.core.js";
|
||||
|
||||
export const SessionSchema = z
|
||||
.object({
|
||||
@@ -34,11 +30,7 @@ export const SessionSchema = z
|
||||
.object({
|
||||
channel: z.string().optional(),
|
||||
chatType: z
|
||||
.union([
|
||||
z.literal("direct"),
|
||||
z.literal("group"),
|
||||
z.literal("room"),
|
||||
])
|
||||
.union([z.literal("direct"), z.literal("group"), z.literal("room")])
|
||||
.optional(),
|
||||
keyPrefix: z.string().optional(),
|
||||
})
|
||||
@@ -63,9 +55,7 @@ export const MessagesSchema = z
|
||||
groupChat: GroupChatSchema,
|
||||
queue: QueueSchema,
|
||||
ackReaction: z.string().optional(),
|
||||
ackReactionScope: z
|
||||
.enum(["group-mentions", "group-all", "direct", "all"])
|
||||
.optional(),
|
||||
ackReactionScope: z.enum(["group-mentions", "group-all", "direct", "all"]).optional(),
|
||||
removeAckAfterReply: z.boolean().optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
import { z } from "zod";
|
||||
import { ToolsSchema } from "./zod-schema.agent-runtime.js";
|
||||
import {
|
||||
AgentsSchema,
|
||||
AudioSchema,
|
||||
BindingsSchema,
|
||||
BroadcastSchema,
|
||||
} from "./zod-schema.agents.js";
|
||||
import { AgentsSchema, AudioSchema, BindingsSchema, BroadcastSchema } from "./zod-schema.agents.js";
|
||||
import { HexColorSchema, ModelsConfigSchema } from "./zod-schema.core.js";
|
||||
import { HookMappingSchema, HooksGmailSchema } from "./zod-schema.hooks.js";
|
||||
import { ChannelsSchema } from "./zod-schema.providers.js";
|
||||
import {
|
||||
CommandsSchema,
|
||||
MessagesSchema,
|
||||
SessionSchema,
|
||||
} from "./zod-schema.session.js";
|
||||
import { CommandsSchema, MessagesSchema, SessionSchema } from "./zod-schema.session.js";
|
||||
|
||||
export const ClawdbotSchema = z
|
||||
.object({
|
||||
@@ -35,9 +26,7 @@ export const ClawdbotSchema = z
|
||||
lastRunVersion: z.string().optional(),
|
||||
lastRunCommit: z.string().optional(),
|
||||
lastRunCommand: z.string().optional(),
|
||||
lastRunMode: z
|
||||
.union([z.literal("local"), z.literal("remote")])
|
||||
.optional(),
|
||||
lastRunMode: z.union([z.literal("local"), z.literal("remote")]).optional(),
|
||||
})
|
||||
.optional(),
|
||||
logging: z
|
||||
@@ -68,9 +57,7 @@ export const ClawdbotSchema = z
|
||||
consoleStyle: z
|
||||
.union([z.literal("pretty"), z.literal("compact"), z.literal("json")])
|
||||
.optional(),
|
||||
redactSensitive: z
|
||||
.union([z.literal("off"), z.literal("tools")])
|
||||
.optional(),
|
||||
redactSensitive: z.union([z.literal("off"), z.literal("tools")]).optional(),
|
||||
redactPatterns: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
@@ -89,10 +76,7 @@ export const ClawdbotSchema = z
|
||||
.record(
|
||||
z
|
||||
.string()
|
||||
.regex(
|
||||
/^[a-z0-9-]+$/,
|
||||
"Profile names must be alphanumeric with hyphens only",
|
||||
),
|
||||
.regex(/^[a-z0-9-]+$/, "Profile names must be alphanumeric with hyphens only"),
|
||||
z
|
||||
.object({
|
||||
cdpPort: z.number().int().min(1).max(65535).optional(),
|
||||
@@ -118,11 +102,7 @@ export const ClawdbotSchema = z
|
||||
z.string(),
|
||||
z.object({
|
||||
provider: z.string(),
|
||||
mode: z.union([
|
||||
z.literal("api_key"),
|
||||
z.literal("oauth"),
|
||||
z.literal("token"),
|
||||
]),
|
||||
mode: z.union([z.literal("api_key"), z.literal("oauth"), z.literal("token")]),
|
||||
email: z.string().optional(),
|
||||
}),
|
||||
)
|
||||
@@ -131,9 +111,7 @@ export const ClawdbotSchema = z
|
||||
cooldowns: z
|
||||
.object({
|
||||
billingBackoffHours: z.number().positive().optional(),
|
||||
billingBackoffHoursByProvider: z
|
||||
.record(z.string(), z.number().positive())
|
||||
.optional(),
|
||||
billingBackoffHoursByProvider: z.record(z.string(), z.number().positive()).optional(),
|
||||
billingMaxHours: z.number().positive().optional(),
|
||||
failureWindowHours: z.number().positive().optional(),
|
||||
})
|
||||
@@ -189,12 +167,7 @@ export const ClawdbotSchema = z
|
||||
enabled: z.boolean().optional(),
|
||||
port: z.number().int().positive().optional(),
|
||||
bind: z
|
||||
.union([
|
||||
z.literal("auto"),
|
||||
z.literal("lan"),
|
||||
z.literal("tailnet"),
|
||||
z.literal("loopback"),
|
||||
])
|
||||
.union([z.literal("auto"), z.literal("lan"), z.literal("tailnet"), z.literal("loopback")])
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
@@ -230,12 +203,7 @@ export const ClawdbotSchema = z
|
||||
port: z.number().int().positive().optional(),
|
||||
mode: z.union([z.literal("local"), z.literal("remote")]).optional(),
|
||||
bind: z
|
||||
.union([
|
||||
z.literal("auto"),
|
||||
z.literal("lan"),
|
||||
z.literal("tailnet"),
|
||||
z.literal("loopback"),
|
||||
])
|
||||
.union([z.literal("auto"), z.literal("lan"), z.literal("tailnet"), z.literal("loopback")])
|
||||
.optional(),
|
||||
controlUi: z
|
||||
.object({
|
||||
@@ -245,9 +213,7 @@ export const ClawdbotSchema = z
|
||||
.optional(),
|
||||
auth: z
|
||||
.object({
|
||||
mode: z
|
||||
.union([z.literal("token"), z.literal("password")])
|
||||
.optional(),
|
||||
mode: z.union([z.literal("token"), z.literal("password")]).optional(),
|
||||
token: z.string().optional(),
|
||||
password: z.string().optional(),
|
||||
allowTailscale: z.boolean().optional(),
|
||||
@@ -255,13 +221,7 @@ export const ClawdbotSchema = z
|
||||
.optional(),
|
||||
tailscale: z
|
||||
.object({
|
||||
mode: z
|
||||
.union([
|
||||
z.literal("off"),
|
||||
z.literal("serve"),
|
||||
z.literal("funnel"),
|
||||
])
|
||||
.optional(),
|
||||
mode: z.union([z.literal("off"), z.literal("serve"), z.literal("funnel")]).optional(),
|
||||
resetOnExit: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
@@ -314,12 +274,7 @@ export const ClawdbotSchema = z
|
||||
.object({
|
||||
preferBrew: z.boolean().optional(),
|
||||
nodeManager: z
|
||||
.union([
|
||||
z.literal("npm"),
|
||||
z.literal("pnpm"),
|
||||
z.literal("yarn"),
|
||||
z.literal("bun"),
|
||||
])
|
||||
.union([z.literal("npm"), z.literal("pnpm"), z.literal("yarn"), z.literal("bun")])
|
||||
.optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
Reference in New Issue
Block a user