refactor: move inbound config

This commit is contained in:
Peter Steinberger
2025-12-24 00:22:52 +00:00
parent 5e07400cd1
commit 93af424ce5
34 changed files with 283 additions and 243 deletions

View File

@@ -66,7 +66,7 @@ struct ConfigSettings: View {
private var header: some View { private var header: some View {
Text("Clawdis CLI config") Text("Clawdis CLI config")
.font(.title3.weight(.semibold)) .font(.title3.weight(.semibold))
Text("Edit ~/.clawdis/clawdis.json (agent / inbound.session).") Text("Edit ~/.clawdis/clawdis.json (agent / session / routing / messages).")
.font(.callout) .font(.callout)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }

View File

@@ -163,8 +163,7 @@ enum DebugActions {
guard guard
let data = try? Data(contentsOf: configURL), let data = try? Data(contentsOf: configURL),
let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let inbound = parsed["inbound"] as? [String: Any], let session = parsed["session"] as? [String: Any],
let session = inbound["session"] as? [String: Any],
let path = session["store"] as? String, let path = session["store"] as? String,
!path.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty !path.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
else { else {

View File

@@ -681,8 +681,7 @@ struct DebugSettings: View {
guard guard
let data = try? Data(contentsOf: url), let data = try? Data(contentsOf: url),
let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let parsed = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let inbound = parsed["inbound"] as? [String: Any], let session = parsed["session"] as? [String: Any],
let session = inbound["session"] as? [String: Any],
let path = session["store"] as? String let path = session["store"] as? String
else { else {
self.sessionStorePath = SessionLoader.defaultStorePath self.sessionStorePath = SessionLoader.defaultStorePath
@@ -701,11 +700,9 @@ struct DebugSettings: View {
root = parsed root = parsed
} }
var inbound = root["inbound"] as? [String: Any] ?? [:] var session = root["session"] as? [String: Any] ?? [:]
var session = inbound["session"] as? [String: Any] ?? [:]
session["store"] = trimmed.isEmpty ? SessionLoader.defaultStorePath : trimmed session["store"] = trimmed.isEmpty ? SessionLoader.defaultStorePath : trimmed
inbound["session"] = session root["session"] = session
root["inbound"] = inbound
do { do {
let data = try JSONSerialization.data(withJSONObject: root, options: [.prettyPrinted, .sortedKeys]) let data = try JSONSerialization.data(withJSONObject: root, options: [.prettyPrinted, .sortedKeys])

View File

@@ -287,15 +287,11 @@ actor GatewayConnection {
extension GatewayConnection { extension GatewayConnection {
struct ConfigGetSnapshot: Decodable, Sendable { struct ConfigGetSnapshot: Decodable, Sendable {
struct SnapshotConfig: Decodable, Sendable { struct SnapshotConfig: Decodable, Sendable {
struct Inbound: Decodable, Sendable { struct Session: Decodable, Sendable {
struct Session: Decodable, Sendable { let mainKey: String?
let mainKey: String?
}
let session: Session?
} }
let inbound: Inbound? let session: Session?
} }
let config: SnapshotConfig? let config: SnapshotConfig?
@@ -303,7 +299,7 @@ extension GatewayConnection {
static func mainSessionKey(fromConfigGetData data: Data) throws -> String { static func mainSessionKey(fromConfigGetData data: Data) throws -> String {
let snapshot = try JSONDecoder().decode(ConfigGetSnapshot.self, from: data) let snapshot = try JSONDecoder().decode(ConfigGetSnapshot.self, from: data)
let raw = snapshot.config?.inbound?.session?.mainKey let raw = snapshot.config?.session?.mainKey
let trimmed = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines) let trimmed = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? "main" : trimmed return trimmed.isEmpty ? "main" : trimmed
} }

View File

@@ -27,7 +27,7 @@ import Testing
"raw": null, "raw": null,
"parsed": {}, "parsed": {},
"valid": true, "valid": true,
"config": { "inbound": { "session": { "mainKey": " primary " } } }, "config": { "session": { "mainKey": " primary " } },
"issues": [] "issues": []
} }
""" """
@@ -38,7 +38,7 @@ import Testing
@Test func configGetSnapshotMainKeyFallsBackWhenEmptyOrWhitespace() throws { @Test func configGetSnapshotMainKeyFallsBackWhenEmptyOrWhitespace() throws {
let json = """ let json = """
{ {
"config": { "inbound": { "session": { "mainKey": " " } } } "config": { "session": { "mainKey": " " } }
} }
""" """
let key = try GatewayConnection.mainSessionKey(fromConfigGetData: Data(json.utf8)) let key = try GatewayConnection.mainSessionKey(fromConfigGetData: Data(json.utf8))

View File

@@ -106,10 +106,10 @@ describe("directive parsing", () => {
model: "claude-opus-4-5", model: "claude-opus-4-5",
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
}, },
inbound: { routing: {
allowFrom: ["*"], allowFrom: ["*"],
session: { store: path.join(home, "sessions.json") },
}, },
session: { store: path.join(home, "sessions.json") },
}, },
); );
@@ -134,9 +134,7 @@ describe("directive parsing", () => {
model: "claude-opus-4-5", model: "claude-opus-4-5",
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
}, },
inbound: { session: { store: path.join(home, "sessions.json") },
session: { store: path.join(home, "sessions.json") },
},
}, },
); );
@@ -189,10 +187,10 @@ describe("directive parsing", () => {
model: "claude-opus-4-5", model: "claude-opus-4-5",
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
}, },
inbound: { routing: {
allowFrom: ["*"], allowFrom: ["*"],
session: { store: storePath },
}, },
session: { store: storePath },
}, },
); );
@@ -251,10 +249,10 @@ describe("directive parsing", () => {
model: "claude-opus-4-5", model: "claude-opus-4-5",
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
}, },
inbound: { routing: {
allowFrom: ["*"], allowFrom: ["*"],
session: { store: storePath },
}, },
session: { store: storePath },
}, },
); );
@@ -284,9 +282,7 @@ describe("directive parsing", () => {
"openai/gpt-4.1-mini", "openai/gpt-4.1-mini",
], ],
}, },
inbound: { session: { store: storePath },
session: { store: storePath },
},
}, },
); );
@@ -313,9 +309,7 @@ describe("directive parsing", () => {
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
allowedModels: ["openai/gpt-4.1-mini"], allowedModels: ["openai/gpt-4.1-mini"],
}, },
inbound: { session: { store: storePath },
session: { store: storePath },
},
}, },
); );
@@ -354,10 +348,10 @@ describe("directive parsing", () => {
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
allowedModels: ["openai/gpt-4.1-mini"], allowedModels: ["openai/gpt-4.1-mini"],
}, },
inbound: { routing: {
allowFrom: ["*"], allowFrom: ["*"],
session: { store: storePath },
}, },
session: { store: storePath },
}, },
); );

View File

@@ -39,10 +39,10 @@ function makeCfg(home: string) {
model: "claude-opus-4-5", model: "claude-opus-4-5",
workspace: join(home, "clawd"), workspace: join(home, "clawd"),
}, },
inbound: { routing: {
allowFrom: ["*"], allowFrom: ["*"],
session: { store: join(home, "sessions.json") },
}, },
session: { store: join(home, "sessions.json") },
}; };
} }
@@ -119,7 +119,7 @@ describe("trigger handling", () => {
const text = Array.isArray(res) ? res[0]?.text : res?.text; const text = Array.isArray(res) ? res[0]?.text : res?.text;
expect(text).toContain("Group activation set to always"); expect(text).toContain("Group activation set to always");
const store = JSON.parse( const store = JSON.parse(
await fs.readFile(cfg.inbound.session.store, "utf-8"), await fs.readFile(cfg.session.store, "utf-8"),
) as Record<string, { groupActivation?: string }>; ) as Record<string, { groupActivation?: string }>;
expect(store["group:123@g.us"]?.groupActivation).toBe("always"); expect(store["group:123@g.us"]?.groupActivation).toBe("always");
expect(runEmbeddedPiAgent).not.toHaveBeenCalled(); expect(runEmbeddedPiAgent).not.toHaveBeenCalled();
@@ -172,11 +172,11 @@ describe("trigger handling", () => {
model: "claude-opus-4-5", model: "claude-opus-4-5",
workspace: join(home, "clawd"), workspace: join(home, "clawd"),
}, },
inbound: { routing: {
allowFrom: ["*"], allowFrom: ["*"],
session: { store: join(home, "sessions.json") },
groupChat: { requireMention: false }, groupChat: { requireMention: false },
}, },
session: { store: join(home, "sessions.json") },
}, },
); );
@@ -214,11 +214,11 @@ describe("trigger handling", () => {
model: "claude-opus-4-5", model: "claude-opus-4-5",
workspace: join(home, "clawd"), workspace: join(home, "clawd"),
}, },
inbound: { routing: {
allowFrom: ["*"], allowFrom: ["*"],
session: { },
store: join(tmpdir(), `clawdis-session-test-${Date.now()}.json`), session: {
}, store: join(tmpdir(), `clawdis-session-test-${Date.now()}.json`),
}, },
}, },
); );
@@ -254,11 +254,11 @@ describe("trigger handling", () => {
model: "claude-opus-4-5", model: "claude-opus-4-5",
workspace: join(home, "clawd"), workspace: join(home, "clawd"),
}, },
inbound: { routing: {
allowFrom: ["*"], allowFrom: ["*"],
session: { },
store: join(tmpdir(), `clawdis-session-test-${Date.now()}.json`), session: {
}, store: join(tmpdir(), `clawdis-session-test-${Date.now()}.json`),
}, },
}, },
); );

View File

@@ -137,7 +137,7 @@ function stripMentions(
cfg: ClawdisConfig | undefined, cfg: ClawdisConfig | undefined,
): string { ): string {
let result = text; let result = text;
const patterns = cfg?.inbound?.groupChat?.mentionPatterns ?? []; const patterns = cfg?.routing?.groupChat?.mentionPatterns ?? [];
for (const p of patterns) { for (const p of patterns) {
try { try {
const re = new RegExp(p, "gi"); const re = new RegExp(p, "gi");
@@ -166,7 +166,7 @@ export async function getReplyFromConfig(
const cfg = configOverride ?? loadConfig(); const cfg = configOverride ?? loadConfig();
const workspaceDirRaw = cfg.agent?.workspace ?? DEFAULT_AGENT_WORKSPACE_DIR; const workspaceDirRaw = cfg.agent?.workspace ?? DEFAULT_AGENT_WORKSPACE_DIR;
const agentCfg = cfg.agent; const agentCfg = cfg.agent;
const sessionCfg = cfg.inbound?.session; const sessionCfg = cfg.session;
const defaultProvider = agentCfg?.provider?.trim() || DEFAULT_PROVIDER; const defaultProvider = agentCfg?.provider?.trim() || DEFAULT_PROVIDER;
const defaultModel = agentCfg?.model?.trim() || DEFAULT_MODEL; const defaultModel = agentCfg?.model?.trim() || DEFAULT_MODEL;
@@ -227,7 +227,7 @@ export async function getReplyFromConfig(
let transcribedText: string | undefined; let transcribedText: string | undefined;
// Optional audio transcription before templating/session handling. // Optional audio transcription before templating/session handling.
if (cfg.inbound?.transcribeAudio && isAudio(ctx.MediaType)) { if (cfg.routing?.transcribeAudio && isAudio(ctx.MediaType)) {
const transcribed = await transcribeInboundAudio(cfg, ctx, defaultRuntime); const transcribed = await transcribeInboundAudio(cfg, ctx, defaultRuntime);
if (transcribed?.text) { if (transcribed?.text) {
transcribedText = transcribed.text; transcribedText = transcribed.text;
@@ -361,7 +361,7 @@ export async function getReplyFromConfig(
sessionCtx.BodyStripped = modelCleaned; sessionCtx.BodyStripped = modelCleaned;
const defaultGroupActivation = () => { const defaultGroupActivation = () => {
const requireMention = cfg.inbound?.groupChat?.requireMention; const requireMention = cfg.routing?.groupChat?.requireMention;
return requireMention === false ? "always" : "mention"; return requireMention === false ? "always" : "mention";
}; };
@@ -611,7 +611,7 @@ export async function getReplyFromConfig(
} }
// Optional allowlist by origin number (E.164 without whatsapp: prefix) // Optional allowlist by origin number (E.164 without whatsapp: prefix)
const configuredAllowFrom = cfg.inbound?.allowFrom; const configuredAllowFrom = cfg.routing?.allowFrom;
const from = (ctx.From ?? "").replace(/^whatsapp:/, ""); const from = (ctx.From ?? "").replace(/^whatsapp:/, "");
const to = (ctx.To ?? "").replace(/^whatsapp:/, ""); const to = (ctx.To ?? "").replace(/^whatsapp:/, "");
const isSamePhone = from && to && from === to; const isSamePhone = from && to && from === to;

View File

@@ -16,7 +16,7 @@ import {
} from "../config/sessions.js"; } from "../config/sessions.js";
import type { ThinkLevel, VerboseLevel } from "./thinking.js"; import type { ThinkLevel, VerboseLevel } from "./thinking.js";
type AgentConfig = NonNullable<ClawdisConfig["inbound"]>["agent"]; type AgentConfig = NonNullable<ClawdisConfig["agent"]>;
type StatusArgs = { type StatusArgs = {
agent: AgentConfig; agent: AgentConfig;

View File

@@ -38,7 +38,7 @@ describe("transcribeInboundAudio", () => {
global.fetch = fetchMock; global.fetch = fetchMock;
const cfg = { const cfg = {
inbound: { routing: {
transcribeAudio: { transcribeAudio: {
command: ["echo", "{{MediaPath}}"], command: ["echo", "{{MediaPath}}"],
timeoutSeconds: 5, timeoutSeconds: 5,
@@ -58,7 +58,7 @@ describe("transcribeInboundAudio", () => {
it("returns undefined when no transcription command", async () => { it("returns undefined when no transcription command", async () => {
const res = await transcribeInboundAudio( const res = await transcribeInboundAudio(
{ inbound: {} } as never, { routing: {} } as never,
{} as never, {} as never,
runtime as never, runtime as never,
); );

View File

@@ -18,7 +18,7 @@ export async function transcribeInboundAudio(
ctx: MsgContext, ctx: MsgContext,
runtime: RuntimeEnv, runtime: RuntimeEnv,
): Promise<{ text: string } | undefined> { ): Promise<{ text: string } | undefined> {
const transcriber = cfg.inbound?.transcribeAudio; const transcriber = cfg.routing?.transcribeAudio;
if (!transcriber?.command?.length) return undefined; if (!transcriber?.command?.length) return undefined;
const timeoutMs = Math.max((transcriber.timeoutSeconds ?? 45) * 1000, 1_000); const timeoutMs = Math.max((transcriber.timeoutSeconds ?? 45) * 1000, 1_000);

View File

@@ -46,7 +46,7 @@ async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
function mockConfig( function mockConfig(
home: string, home: string,
storePath: string, storePath: string,
inboundOverrides?: Partial<NonNullable<ClawdisConfig["inbound"]>>, routingOverrides?: Partial<NonNullable<ClawdisConfig["routing"]>>,
) { ) {
configSpy.mockReturnValue({ configSpy.mockReturnValue({
agent: { agent: {
@@ -54,10 +54,8 @@ function mockConfig(
model: "claude-opus-4-5", model: "claude-opus-4-5",
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
}, },
inbound: { session: { store: storePath, mainKey: "main" },
session: { store: storePath, mainKey: "main" }, routing: routingOverrides ? { ...routingOverrides } : undefined,
...inboundOverrides,
},
}); });
} }

View File

@@ -68,7 +68,7 @@ function resolveSession(opts: {
to?: string; to?: string;
sessionId?: string; sessionId?: string;
}): SessionResolution { }): SessionResolution {
const sessionCfg = opts.cfg.inbound?.session; const sessionCfg = opts.cfg.session;
const scope = sessionCfg?.scope ?? "per-sender"; const scope = sessionCfg?.scope ?? "per-sender";
const mainKey = sessionCfg?.mainKey ?? "main"; const mainKey = sessionCfg?.mainKey ?? "main";
const idleMinutes = Math.max( const idleMinutes = Math.max(
@@ -150,7 +150,7 @@ export async function agentCommand(
}); });
const workspaceDir = workspace.dir; const workspaceDir = workspace.dir;
const allowFrom = (cfg.inbound?.allowFrom ?? []) const allowFrom = (cfg.routing?.allowFrom ?? [])
.map((val) => normalizeE164(val)) .map((val) => normalizeE164(val))
.filter((val) => val.length > 1); .filter((val) => val.length > 1);
@@ -421,7 +421,7 @@ export async function agentCommand(
if (deliver) { if (deliver) {
if (deliveryProvider === "whatsapp" && !whatsappTarget) { if (deliveryProvider === "whatsapp" && !whatsappTarget) {
const err = new Error( const err = new Error(
"Delivering to WhatsApp requires --to <E.164> or inbound.allowFrom[0]", "Delivering to WhatsApp requires --to <E.164> or routing.allowFrom[0]",
); );
if (!bestEffortDeliver) throw err; if (!bestEffortDeliver) throw err;
logDeliveryError(err); logDeliveryError(err);

View File

@@ -32,7 +32,7 @@ describe("getHealthSnapshot", () => {
}); });
it("skips telegram probe when not configured", async () => { it("skips telegram probe when not configured", async () => {
testConfig = { inbound: { reply: { session: { store: "/tmp/x" } } } }; testConfig = { session: { store: "/tmp/x" } };
testStore = { testStore = {
global: { updatedAt: Date.now() }, global: { updatedAt: Date.now() },
unknown: { updatedAt: Date.now() }, unknown: { updatedAt: Date.now() },

View File

@@ -55,7 +55,7 @@ export async function getHealthSnapshot(
const linked = await webAuthExists(); const linked = await webAuthExists();
const authAgeMs = getWebAuthAgeMs(); const authAgeMs = getWebAuthAgeMs();
const heartbeatSeconds = resolveHeartbeatSeconds(cfg, undefined); const heartbeatSeconds = resolveHeartbeatSeconds(cfg, undefined);
const storePath = resolveStorePath(cfg.inbound?.session?.store); const storePath = resolveStorePath(cfg.session?.store);
const store = loadSessionStore(storePath); const store = loadSessionStore(storePath);
const sessions = Object.entries(store) const sessions = Object.entries(store)
.filter(([key]) => key !== "global" && key !== "unknown") .filter(([key]) => key !== "global" && key !== "unknown")

View File

@@ -156,7 +156,7 @@ export async function sessionsCommand(
lookupContextTokens(cfg.agent?.model) ?? lookupContextTokens(cfg.agent?.model) ??
DEFAULT_CONTEXT_TOKENS; DEFAULT_CONTEXT_TOKENS;
const configModel = cfg.agent?.model ?? DEFAULT_MODEL; const configModel = cfg.agent?.model ?? DEFAULT_MODEL;
const storePath = resolveStorePath(opts.store ?? cfg.inbound?.session?.store); const storePath = resolveStorePath(opts.store ?? cfg.session?.store);
const store = loadSessionStore(storePath); const store = loadSessionStore(storePath);
let activeMinutes: number | undefined; let activeMinutes: number | undefined;

View File

@@ -45,7 +45,6 @@ export async function setupCommand(
const existingRaw = await readConfigFileRaw(); const existingRaw = await readConfigFileRaw();
const cfg = existingRaw.parsed; const cfg = existingRaw.parsed;
const inbound = cfg.inbound ?? {};
const agent = cfg.agent ?? {}; const agent = cfg.agent ?? {};
const workspace = const workspace =

View File

@@ -32,7 +32,7 @@ vi.mock("../web/session.js", () => ({
logWebSelfId: mocks.logWebSelfId, logWebSelfId: mocks.logWebSelfId,
})); }));
vi.mock("../config/config.js", () => ({ vi.mock("../config/config.js", () => ({
loadConfig: () => ({ inbound: { reply: { session: {} } } }), loadConfig: () => ({ session: {} }),
})); }));
import { statusCommand } from "./status.js"; import { statusCommand } from "./status.js";

View File

@@ -66,7 +66,7 @@ export async function getStatusSummary(): Promise<StatusSummary> {
lookupContextTokens(configModel) ?? lookupContextTokens(configModel) ??
DEFAULT_CONTEXT_TOKENS; DEFAULT_CONTEXT_TOKENS;
const storePath = resolveStorePath(cfg.inbound?.session?.store); const storePath = resolveStorePath(cfg.session?.store);
const store = loadSessionStore(storePath); const store = loadSessionStore(storePath);
const now = Date.now(); const now = Date.now();
const sessions = Object.entries(store) const sessions = Object.entries(store)

View File

@@ -36,7 +36,8 @@ describe("config identity defaults", () => {
JSON.stringify( JSON.stringify(
{ {
identity: { name: "Samantha", theme: "helpful sloth", emoji: "🦥" }, identity: { name: "Samantha", theme: "helpful sloth", emoji: "🦥" },
inbound: {}, messages: {},
routing: {},
}, },
null, null,
2, 2,
@@ -48,8 +49,8 @@ describe("config identity defaults", () => {
const { loadConfig } = await import("./config.js"); const { loadConfig } = await import("./config.js");
const cfg = loadConfig(); const cfg = loadConfig();
expect(cfg.inbound?.responsePrefix).toBe("🦥"); expect(cfg.messages?.responsePrefix).toBe("🦥");
expect(cfg.inbound?.groupChat?.mentionPatterns).toEqual([ expect(cfg.routing?.groupChat?.mentionPatterns).toEqual([
"\\b@?Samantha\\b", "\\b@?Samantha\\b",
]); ]);
}); });
@@ -68,8 +69,10 @@ describe("config identity defaults", () => {
theme: "space lobster", theme: "space lobster",
emoji: "🦞", emoji: "🦞",
}, },
inbound: { messages: {
responsePrefix: "✅", responsePrefix: "✅",
},
routing: {
groupChat: { mentionPatterns: ["@clawd"] }, groupChat: { mentionPatterns: ["@clawd"] },
}, },
}, },
@@ -83,8 +86,8 @@ describe("config identity defaults", () => {
const { loadConfig } = await import("./config.js"); const { loadConfig } = await import("./config.js");
const cfg = loadConfig(); const cfg = loadConfig();
expect(cfg.inbound?.responsePrefix).toBe("✅"); expect(cfg.messages?.responsePrefix).toBe("✅");
expect(cfg.inbound?.groupChat?.mentionPatterns).toEqual(["@clawd"]); expect(cfg.routing?.groupChat?.mentionPatterns).toEqual(["@clawd"]);
}); });
}); });
@@ -97,7 +100,8 @@ describe("config identity defaults", () => {
JSON.stringify( JSON.stringify(
{ {
identity: { name: "Samantha", theme: "helpful sloth", emoji: "🦥" }, identity: { name: "Samantha", theme: "helpful sloth", emoji: "🦥" },
inbound: {}, messages: {},
routing: {},
}, },
null, null,
2, 2,
@@ -109,12 +113,12 @@ describe("config identity defaults", () => {
const { loadConfig } = await import("./config.js"); const { loadConfig } = await import("./config.js");
const cfg = loadConfig(); const cfg = loadConfig();
expect(cfg.inbound?.responsePrefix).toBe("🦥"); expect(cfg.messages?.responsePrefix).toBe("🦥");
expect(cfg.inbound?.groupChat?.mentionPatterns).toEqual([ expect(cfg.routing?.groupChat?.mentionPatterns).toEqual([
"\\b@?Samantha\\b", "\\b@?Samantha\\b",
]); ]);
expect(cfg.agent).toBeUndefined(); expect(cfg.agent).toBeUndefined();
expect(cfg.inbound?.session).toBeUndefined(); expect(cfg.session).toBeUndefined();
}); });
}); });
}); });

View File

@@ -79,6 +79,22 @@ export type GroupChatConfig = {
historyLimit?: number; historyLimit?: number;
}; };
export type RoutingConfig = {
allowFrom?: string[]; // E.164 numbers allowed to trigger auto-reply (without whatsapp:)
transcribeAudio?: {
// Optional CLI to turn inbound audio into text; templated args, must output transcript to stdout.
command: string[];
timeoutSeconds?: number;
};
groupChat?: GroupChatConfig;
};
export type MessagesConfig = {
messagePrefix?: string; // Prefix added to all inbound messages (default: "[clawdis]" if no allowFrom, else "")
responsePrefix?: string; // Prefix auto-added to all outbound replies (e.g., "🦞")
timestampPrefix?: boolean | string; // true/false or IANA timezone string (default: true with UTC)
};
export type BridgeBindMode = "auto" | "lan" | "tailnet" | "loopback"; export type BridgeBindMode = "auto" | "lan" | "tailnet" | "loopback";
export type BridgeConfig = { export type BridgeConfig = {
@@ -249,19 +265,9 @@ export type ClawdisConfig = {
/** Periodic background heartbeat runs (minutes). 0 disables. */ /** Periodic background heartbeat runs (minutes). 0 disables. */
heartbeatMinutes?: number; heartbeatMinutes?: number;
}; };
inbound?: { routing?: RoutingConfig;
allowFrom?: string[]; // E.164 numbers allowed to trigger auto-reply (without whatsapp:) messages?: MessagesConfig;
messagePrefix?: string; // Prefix added to all inbound messages (default: "[clawdis]" if no allowFrom, else "") session?: SessionConfig;
responsePrefix?: string; // Prefix auto-added to all outbound replies (e.g., "🦞")
timestampPrefix?: boolean | string; // true/false or IANA timezone string (default: true with UTC)
transcribeAudio?: {
// Optional CLI to turn inbound audio into text; templated args, must output transcript to stdout.
command: string[];
timeoutSeconds?: number;
};
groupChat?: GroupChatConfig;
session?: SessionConfig;
};
web?: WebConfig; web?: WebConfig;
telegram?: TelegramConfig; telegram?: TelegramConfig;
cron?: CronConfig; cron?: CronConfig;
@@ -331,6 +337,49 @@ const ModelsConfigSchema = z
}) })
.optional(); .optional();
const GroupChatSchema = z
.object({
requireMention: z.boolean().optional(),
mentionPatterns: z.array(z.string()).optional(),
historyLimit: z.number().int().positive().optional(),
})
.optional();
const TranscribeAudioSchema = z
.object({
command: z.array(z.string()),
timeoutSeconds: z.number().int().positive().optional(),
})
.optional();
const SessionSchema = z
.object({
scope: z.union([z.literal("per-sender"), z.literal("global")]).optional(),
resetTriggers: z.array(z.string()).optional(),
idleMinutes: z.number().int().positive().optional(),
heartbeatIdleMinutes: z.number().int().positive().optional(),
store: z.string().optional(),
typingIntervalSeconds: z.number().int().positive().optional(),
mainKey: z.string().optional(),
})
.optional();
const MessagesSchema = z
.object({
messagePrefix: z.string().optional(),
responsePrefix: z.string().optional(),
timestampPrefix: z.union([z.boolean(), z.string()]).optional(),
})
.optional();
const RoutingSchema = z
.object({
allowFrom: z.array(z.string()).optional(),
groupChat: GroupChatSchema,
transcribeAudio: TranscribeAudioSchema,
})
.optional();
const ClawdisSchema = z.object({ const ClawdisSchema = z.object({
identity: z identity: z
.object({ .object({
@@ -402,40 +451,9 @@ const ClawdisSchema = z.object({
heartbeatMinutes: z.number().nonnegative().optional(), heartbeatMinutes: z.number().nonnegative().optional(),
}) })
.optional(), .optional(),
inbound: z routing: RoutingSchema,
.object({ messages: MessagesSchema,
allowFrom: z.array(z.string()).optional(), session: SessionSchema,
messagePrefix: z.string().optional(),
responsePrefix: z.string().optional(),
timestampPrefix: z.union([z.boolean(), z.string()]).optional(),
groupChat: z
.object({
requireMention: z.boolean().optional(),
mentionPatterns: z.array(z.string()).optional(),
historyLimit: z.number().int().positive().optional(),
})
.optional(),
transcribeAudio: z
.object({
command: z.array(z.string()),
timeoutSeconds: z.number().int().positive().optional(),
})
.optional(),
session: z
.object({
scope: z
.union([z.literal("per-sender"), z.literal("global")])
.optional(),
resetTriggers: z.array(z.string()).optional(),
idleMinutes: z.number().int().positive().optional(),
heartbeatIdleMinutes: z.number().int().positive().optional(),
store: z.string().optional(),
typingIntervalSeconds: z.number().int().positive().optional(),
mainKey: z.string().optional(),
})
.optional(),
})
.optional(),
cron: z cron: z
.object({ .object({
enabled: z.boolean().optional(), enabled: z.boolean().optional(),
@@ -590,14 +608,15 @@ function applyIdentityDefaults(cfg: ClawdisConfig): ClawdisConfig {
const emoji = identity.emoji?.trim(); const emoji = identity.emoji?.trim();
const name = identity.name?.trim(); const name = identity.name?.trim();
const inbound = cfg.inbound ?? {}; const messages = cfg.messages ?? {};
const groupChat = inbound.groupChat ?? {}; const routing = cfg.routing ?? {};
const groupChat = routing.groupChat ?? {};
let mutated = false; let mutated = false;
const next: ClawdisConfig = { ...cfg }; const next: ClawdisConfig = { ...cfg };
if (emoji && !inbound.responsePrefix) { if (emoji && !messages.responsePrefix) {
next.inbound = { ...inbound, responsePrefix: emoji }; next.messages = { ...(next.messages ?? messages), responsePrefix: emoji };
mutated = true; mutated = true;
} }
@@ -605,8 +624,8 @@ function applyIdentityDefaults(cfg: ClawdisConfig): ClawdisConfig {
const parts = name.split(/\s+/).filter(Boolean).map(escapeRegExp); const parts = name.split(/\s+/).filter(Boolean).map(escapeRegExp);
const re = parts.length ? parts.join("\\s+") : escapeRegExp(name); const re = parts.length ? parts.join("\\s+") : escapeRegExp(name);
const pattern = `\\b@?${re}\\b`; const pattern = `\\b@?${re}\\b`;
next.inbound = { next.routing = {
...(next.inbound ?? inbound), ...(next.routing ?? routing),
groupChat: { ...groupChat, mentionPatterns: [pattern] }, groupChat: { ...groupChat, mentionPatterns: [pattern] },
}; };
mutated = true; mutated = true;

View File

@@ -57,9 +57,7 @@ function makeCfg(home: string, storePath: string): ClawdisConfig {
model: "claude-opus-4-5", model: "claude-opus-4-5",
workspace: path.join(home, "clawd"), workspace: path.join(home, "clawd"),
}, },
inbound: { session: { store: storePath, mainKey: "main" },
session: { store: storePath, mainKey: "main" },
},
} as ClawdisConfig; } as ClawdisConfig;
} }

View File

@@ -63,7 +63,7 @@ function resolveDeliveryTarget(
? jobPayload.to.trim() ? jobPayload.to.trim()
: undefined; : undefined;
const sessionCfg = cfg.inbound?.session; const sessionCfg = cfg.session;
const mainKey = (sessionCfg?.mainKey ?? "main").trim() || "main"; const mainKey = (sessionCfg?.mainKey ?? "main").trim() || "main";
const storePath = resolveStorePath(sessionCfg?.store); const storePath = resolveStorePath(sessionCfg?.store);
const store = loadSessionStore(storePath); const store = loadSessionStore(storePath);
@@ -88,7 +88,7 @@ function resolveDeliveryTarget(
const sanitizedWhatsappTo = (() => { const sanitizedWhatsappTo = (() => {
if (channel !== "whatsapp") return to; if (channel !== "whatsapp") return to;
const rawAllow = cfg.inbound?.allowFrom ?? []; const rawAllow = cfg.routing?.allowFrom ?? [];
if (rawAllow.includes("*")) return to; if (rawAllow.includes("*")) return to;
const allowFrom = rawAllow const allowFrom = rawAllow
.map((val) => normalizeE164(val)) .map((val) => normalizeE164(val))
@@ -111,7 +111,7 @@ function resolveCronSession(params: {
sessionKey: string; sessionKey: string;
nowMs: number; nowMs: number;
}) { }) {
const sessionCfg = params.cfg.inbound?.session; const sessionCfg = params.cfg.session;
const idleMinutes = Math.max( const idleMinutes = Math.max(
sessionCfg?.idleMinutes ?? DEFAULT_IDLE_MINUTES, sessionCfg?.idleMinutes ?? DEFAULT_IDLE_MINUTES,
1, 1,

View File

@@ -187,10 +187,10 @@ vi.mock("../config/config.js", () => {
model: "claude-opus-4-5", model: "claude-opus-4-5",
workspace: path.join(os.tmpdir(), "clawd-gateway-test"), workspace: path.join(os.tmpdir(), "clawd-gateway-test"),
}, },
inbound: { routing: {
allowFrom: testAllowFrom, allowFrom: testAllowFrom,
session: { mainKey: "main", store: testSessionStorePath },
}, },
session: { mainKey: "main", store: testSessionStorePath },
gateway: (() => { gateway: (() => {
const gateway: Record<string, unknown> = {}; const gateway: Record<string, unknown> = {};
if (testGatewayBind) gateway.bind = testGatewayBind; if (testGatewayBind) gateway.bind = testGatewayBind;

View File

@@ -732,7 +732,7 @@ function capArrayByJsonBytes<T>(
function loadSessionEntry(sessionKey: string) { function loadSessionEntry(sessionKey: string) {
const cfg = loadConfig(); const cfg = loadConfig();
const sessionCfg = cfg.inbound?.session; const sessionCfg = cfg.session;
const storePath = sessionCfg?.store const storePath = sessionCfg?.store
? resolveStorePath(sessionCfg.store) ? resolveStorePath(sessionCfg.store)
: resolveStorePath(undefined); : resolveStorePath(undefined);
@@ -1885,7 +1885,7 @@ export async function startGatewayServer(
} }
const p = params as SessionsListParams; const p = params as SessionsListParams;
const cfg = loadConfig(); const cfg = loadConfig();
const storePath = resolveStorePath(cfg.inbound?.session?.store); const storePath = resolveStorePath(cfg.session?.store);
const store = loadSessionStore(storePath); const store = loadSessionStore(storePath);
const result = listSessionsFromStore({ const result = listSessionsFromStore({
cfg, cfg,
@@ -1920,7 +1920,7 @@ export async function startGatewayServer(
} }
const cfg = loadConfig(); const cfg = loadConfig();
const storePath = resolveStorePath(cfg.inbound?.session?.store); const storePath = resolveStorePath(cfg.session?.store);
const store = loadSessionStore(storePath); const store = loadSessionStore(storePath);
const now = Date.now(); const now = Date.now();
@@ -2503,7 +2503,7 @@ export async function startGatewayServer(
const sessionKeyRaw = const sessionKeyRaw =
typeof obj.sessionKey === "string" ? obj.sessionKey.trim() : ""; typeof obj.sessionKey === "string" ? obj.sessionKey.trim() : "";
const mainKey = const mainKey =
(loadConfig().inbound?.session?.mainKey ?? "main").trim() || "main"; (loadConfig().session?.mainKey ?? "main").trim() || "main";
const sessionKey = sessionKeyRaw.length > 0 ? sessionKeyRaw : mainKey; const sessionKey = sessionKeyRaw.length > 0 ? sessionKeyRaw : mainKey;
const { storePath, store, entry } = loadSessionEntry(sessionKey); const { storePath, store, entry } = loadSessionEntry(sessionKey);
const now = Date.now(); const now = Date.now();
@@ -4188,7 +4188,7 @@ export async function startGatewayServer(
} }
const p = params as SessionsListParams; const p = params as SessionsListParams;
const cfg = loadConfig(); const cfg = loadConfig();
const storePath = resolveStorePath(cfg.inbound?.session?.store); const storePath = resolveStorePath(cfg.session?.store);
const store = loadSessionStore(storePath); const store = loadSessionStore(storePath);
const result = listSessionsFromStore({ const result = listSessionsFromStore({
cfg, cfg,
@@ -4224,7 +4224,7 @@ export async function startGatewayServer(
} }
const cfg = loadConfig(); const cfg = loadConfig();
const storePath = resolveStorePath(cfg.inbound?.session?.store); const storePath = resolveStorePath(cfg.session?.store);
const store = loadSessionStore(storePath); const store = loadSessionStore(storePath);
const now = Date.now(); const now = Date.now();
@@ -5267,7 +5267,7 @@ export async function startGatewayServer(
} }
resolvedSessionId = sessionId; resolvedSessionId = sessionId;
const mainKey = const mainKey =
(cfg.inbound?.session?.mainKey ?? "main").trim() || "main"; (cfg.session?.mainKey ?? "main").trim() || "main";
if (requestedSessionKey === mainKey) { if (requestedSessionKey === mainKey) {
chatRunSessions.set(sessionId, { chatRunSessions.set(sessionId, {
sessionKey: requestedSessionKey, sessionKey: requestedSessionKey,
@@ -5338,7 +5338,7 @@ export async function startGatewayServer(
if (explicit) return resolvedTo; if (explicit) return resolvedTo;
const cfg = cfgForAgent ?? loadConfig(); const cfg = cfgForAgent ?? loadConfig();
const rawAllow = cfg.inbound?.allowFrom ?? []; const rawAllow = cfg.routing?.allowFrom ?? [];
if (rawAllow.includes("*")) return resolvedTo; if (rawAllow.includes("*")) return resolvedTo;
const allowFrom = rawAllow const allowFrom = rawAllow
.map((val) => normalizeE164(val)) .map((val) => normalizeE164(val))

View File

@@ -33,8 +33,8 @@ export async function buildProviderSummary(
: chalk.cyan("Telegram: not configured"), : chalk.cyan("Telegram: not configured"),
); );
const allowFrom = effective.inbound?.allowFrom?.length const allowFrom = effective.routing?.allowFrom?.length
? effective.inbound.allowFrom.map(normalizeE164).filter(Boolean) ? effective.routing.allowFrom.map(normalizeE164).filter(Boolean)
: []; : [];
if (allowFrom.length) { if (allowFrom.length) {
lines.push(chalk.cyan(`AllowFrom: ${allowFrom.join(", ")}`)); lines.push(chalk.cyan(`AllowFrom: ${allowFrom.join(", ")}`));

View File

@@ -162,7 +162,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
} }
if (!isGroup) { if (!isGroup) {
const sessionCfg = cfg.inbound?.session; const sessionCfg = cfg.session;
const mainKey = (sessionCfg?.mainKey ?? "main").trim() || "main"; const mainKey = (sessionCfg?.mainKey ?? "main").trim() || "main";
const storePath = resolveStorePath(sessionCfg?.store); const storePath = resolveStorePath(sessionCfg?.store);
await updateLastRoute({ await updateLastRoute({

View File

@@ -33,7 +33,7 @@ export function normalizeE164(number: string): string {
/** /**
* "Self-chat mode" heuristic (single phone): the gateway is logged in as the owner's own WhatsApp account, * "Self-chat mode" heuristic (single phone): the gateway is logged in as the owner's own WhatsApp account,
* and `inbound.allowFrom` includes that same number. Used to avoid side-effects that make no sense when the * and `routing.allowFrom` includes that same number. Used to avoid side-effects that make no sense when the
* "bot" and the human are the same WhatsApp identity (e.g. auto read receipts, @mention JID triggers). * "bot" and the human are the same WhatsApp identity (e.g. auto read receipts, @mention JID triggers).
*/ */
export function isSelfChatMode( export function isSelfChatMode(

View File

@@ -158,9 +158,7 @@ describe("heartbeat helpers", () => {
}); });
it("resolves heartbeat minutes with default and overrides", () => { it("resolves heartbeat minutes with default and overrides", () => {
const cfgBase: ClawdisConfig = { const cfgBase: ClawdisConfig = {};
inbound: {},
};
expect(resolveReplyHeartbeatMinutes(cfgBase)).toBe(30); expect(resolveReplyHeartbeatMinutes(cfgBase)).toBe(30);
expect( expect(
resolveReplyHeartbeatMinutes({ resolveReplyHeartbeatMinutes({
@@ -183,10 +181,10 @@ describe("resolveHeartbeatRecipients", () => {
main: { updatedAt: now, lastChannel: "whatsapp", lastTo: "+1000" }, main: { updatedAt: now, lastChannel: "whatsapp", lastTo: "+1000" },
}); });
const cfg: ClawdisConfig = { const cfg: ClawdisConfig = {
inbound: { routing: {
allowFrom: ["+1999"], allowFrom: ["+1999"],
session: { store: store.storePath },
}, },
session: { store: store.storePath },
}; };
const result = resolveHeartbeatRecipients(cfg); const result = resolveHeartbeatRecipients(cfg);
expect(result.source).toBe("session-single"); expect(result.source).toBe("session-single");
@@ -201,10 +199,10 @@ describe("resolveHeartbeatRecipients", () => {
alt: { updatedAt: now - 10, lastChannel: "whatsapp", lastTo: "+2000" }, alt: { updatedAt: now - 10, lastChannel: "whatsapp", lastTo: "+2000" },
}); });
const cfg: ClawdisConfig = { const cfg: ClawdisConfig = {
inbound: { routing: {
allowFrom: ["+1999"], allowFrom: ["+1999"],
session: { store: store.storePath },
}, },
session: { store: store.storePath },
}; };
const result = resolveHeartbeatRecipients(cfg); const result = resolveHeartbeatRecipients(cfg);
expect(result.source).toBe("session-ambiguous"); expect(result.source).toBe("session-ambiguous");
@@ -215,10 +213,10 @@ describe("resolveHeartbeatRecipients", () => {
it("filters wildcard allowFrom when no sessions exist", async () => { it("filters wildcard allowFrom when no sessions exist", async () => {
const store = await makeSessionStore({}); const store = await makeSessionStore({});
const cfg: ClawdisConfig = { const cfg: ClawdisConfig = {
inbound: { routing: {
allowFrom: ["*"], allowFrom: ["*"],
session: { store: store.storePath },
}, },
session: { store: store.storePath },
}; };
const result = resolveHeartbeatRecipients(cfg); const result = resolveHeartbeatRecipients(cfg);
expect(result.recipients).toHaveLength(0); expect(result.recipients).toHaveLength(0);
@@ -232,10 +230,10 @@ describe("resolveHeartbeatRecipients", () => {
main: { updatedAt: now, lastChannel: "whatsapp", lastTo: "+1000" }, main: { updatedAt: now, lastChannel: "whatsapp", lastTo: "+1000" },
}); });
const cfg: ClawdisConfig = { const cfg: ClawdisConfig = {
inbound: { routing: {
allowFrom: ["+1999"], allowFrom: ["+1999"],
session: { store: store.storePath },
}, },
session: { store: store.storePath },
}; };
const result = resolveHeartbeatRecipients(cfg, { all: true }); const result = resolveHeartbeatRecipients(cfg, { all: true });
expect(result.source).toBe("all"); expect(result.source).toBe("all");
@@ -253,7 +251,7 @@ describe("partial reply gating", () => {
const replyResolver = vi.fn().mockResolvedValue({ text: "final reply" }); const replyResolver = vi.fn().mockResolvedValue({ text: "final reply" });
const mockConfig: ClawdisConfig = { const mockConfig: ClawdisConfig = {
inbound: { routing: {
allowFrom: ["*"], allowFrom: ["*"],
}, },
}; };
@@ -300,10 +298,10 @@ describe("partial reply gating", () => {
const replyResolver = vi.fn().mockResolvedValue(undefined); const replyResolver = vi.fn().mockResolvedValue(undefined);
const mockConfig: ClawdisConfig = { const mockConfig: ClawdisConfig = {
inbound: { routing: {
allowFrom: ["*"], allowFrom: ["*"],
session: { store: store.storePath, mainKey: "main" },
}, },
session: { store: store.storePath, mainKey: "main" },
}; };
setLoadConfigMock(mockConfig); setLoadConfigMock(mockConfig);
@@ -391,10 +389,10 @@ describe("runWebHeartbeatOnce", () => {
const resolver = vi.fn(async () => ({ text: HEARTBEAT_TOKEN })); const resolver = vi.fn(async () => ({ text: HEARTBEAT_TOKEN }));
await runWebHeartbeatOnce({ await runWebHeartbeatOnce({
cfg: { cfg: {
inbound: { routing: {
allowFrom: ["+1555"], allowFrom: ["+1555"],
session: { store: store.storePath },
}, },
session: { store: store.storePath },
}, },
to: "+1555", to: "+1555",
verbose: false, verbose: false,
@@ -414,10 +412,10 @@ describe("runWebHeartbeatOnce", () => {
const resolver = vi.fn(async () => ({ text: "ALERT" })); const resolver = vi.fn(async () => ({ text: "ALERT" }));
await runWebHeartbeatOnce({ await runWebHeartbeatOnce({
cfg: { cfg: {
inbound: { routing: {
allowFrom: ["+1555"], allowFrom: ["+1555"],
session: { store: store.storePath },
}, },
session: { store: store.storePath },
}, },
to: "+1555", to: "+1555",
verbose: false, verbose: false,
@@ -443,10 +441,10 @@ describe("runWebHeartbeatOnce", () => {
await fs.writeFile(storePath, JSON.stringify(sessionEntries)); await fs.writeFile(storePath, JSON.stringify(sessionEntries));
await runWebHeartbeatOnce({ await runWebHeartbeatOnce({
cfg: { cfg: {
inbound: { routing: {
allowFrom: ["+1999"], allowFrom: ["+1999"],
session: { store: storePath },
}, },
session: { store: storePath },
}, },
to: "+1999", to: "+1999",
verbose: false, verbose: false,
@@ -472,13 +470,13 @@ describe("runWebHeartbeatOnce", () => {
const sender: typeof sendMessageWhatsApp = vi.fn(); const sender: typeof sendMessageWhatsApp = vi.fn();
const resolver = vi.fn(async () => ({ text: HEARTBEAT_TOKEN })); const resolver = vi.fn(async () => ({ text: HEARTBEAT_TOKEN }));
setLoadConfigMock({ setLoadConfigMock({
inbound: { routing: {
allowFrom: ["+1555"], allowFrom: ["+1555"],
session: { },
store: storePath, session: {
idleMinutes: 60, store: storePath,
heartbeatIdleMinutes: 10, idleMinutes: 60,
}, heartbeatIdleMinutes: 10,
}, },
}); });
@@ -509,19 +507,19 @@ describe("runWebHeartbeatOnce", () => {
setLoadConfigMock(() => ({ setLoadConfigMock(() => ({
agent: { heartbeatMinutes: 0.001 }, agent: { heartbeatMinutes: 0.001 },
inbound: { routing: {
allowFrom: ["+4367"], allowFrom: ["+4367"],
session: { store: storePath, idleMinutes: 60 },
}, },
session: { store: storePath, idleMinutes: 60 },
})); }));
const replyResolver = vi.fn().mockResolvedValue({ text: HEARTBEAT_TOKEN }); const replyResolver = vi.fn().mockResolvedValue({ text: HEARTBEAT_TOKEN });
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() } as never; const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() } as never;
const cfg: ClawdisConfig = { const cfg: ClawdisConfig = {
inbound: { routing: {
allowFrom: ["+4367"], allowFrom: ["+4367"],
session: { store: storePath, idleMinutes: 60 },
}, },
session: { store: storePath, idleMinutes: 60 },
}; };
await runWebHeartbeatOnce({ await runWebHeartbeatOnce({
@@ -547,18 +545,18 @@ describe("runWebHeartbeatOnce", () => {
const sessionId = "override-123"; const sessionId = "override-123";
setLoadConfigMock(() => ({ setLoadConfigMock(() => ({
inbound: { routing: {
allowFrom: ["+1999"], allowFrom: ["+1999"],
session: { store: storePath, idleMinutes: 60 },
}, },
session: { store: storePath, idleMinutes: 60 },
})); }));
const resolver = vi.fn(async () => ({ text: HEARTBEAT_TOKEN })); const resolver = vi.fn(async () => ({ text: HEARTBEAT_TOKEN }));
const cfg: ClawdisConfig = { const cfg: ClawdisConfig = {
inbound: { routing: {
allowFrom: ["+1999"], allowFrom: ["+1999"],
session: { store: storePath, idleMinutes: 60 },
}, },
session: { store: storePath, idleMinutes: 60 },
}; };
await runWebHeartbeatOnce({ await runWebHeartbeatOnce({
cfg, cfg,
@@ -586,10 +584,10 @@ describe("runWebHeartbeatOnce", () => {
const resolver = vi.fn(); const resolver = vi.fn();
await runWebHeartbeatOnce({ await runWebHeartbeatOnce({
cfg: { cfg: {
inbound: { routing: {
allowFrom: ["+1555"], allowFrom: ["+1555"],
session: { store: store.storePath },
}, },
session: { store: store.storePath },
}, },
to: "+1555", to: "+1555",
verbose: false, verbose: false,
@@ -610,10 +608,10 @@ describe("runWebHeartbeatOnce", () => {
const resolver = vi.fn(); const resolver = vi.fn();
await runWebHeartbeatOnce({ await runWebHeartbeatOnce({
cfg: { cfg: {
inbound: { routing: {
allowFrom: ["+1555"], allowFrom: ["+1555"],
session: { store: store.storePath },
}, },
session: { store: store.storePath },
}, },
to: "+1555", to: "+1555",
verbose: false, verbose: false,
@@ -762,10 +760,10 @@ describe("web auto-reply", () => {
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() } as never; const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() } as never;
setLoadConfigMock(() => ({ setLoadConfigMock(() => ({
inbound: { routing: {
allowFrom: ["+1555"], allowFrom: ["+1555"],
session: { store: storePath },
}, },
session: { store: storePath },
})); }));
const controller = new AbortController(); const controller = new AbortController();
@@ -820,11 +818,11 @@ describe("web auto-reply", () => {
const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() } as never; const runtime = { log: vi.fn(), error: vi.fn(), exit: vi.fn() } as never;
setLoadConfigMock(() => ({ setLoadConfigMock(() => ({
inbound: { routing: {
allowFrom: ["+1555"], allowFrom: ["+1555"],
groupChat: { requireMention: true, mentionPatterns: ["@clawd"] }, groupChat: { requireMention: true, mentionPatterns: ["@clawd"] },
session: { store: store.storePath },
}, },
session: { store: store.storePath },
})); }));
const controller = new AbortController(); const controller = new AbortController();
@@ -921,10 +919,10 @@ describe("web auto-reply", () => {
}; };
setLoadConfigMock(() => ({ setLoadConfigMock(() => ({
inbound: { messages: {
timestampPrefix: "UTC", timestampPrefix: "UTC",
session: { store: store.storePath },
}, },
session: { store: store.storePath },
})); }));
await monitorWebProvider(false, listenerFactory, false, resolver); await monitorWebProvider(false, listenerFactory, false, resolver);
@@ -1473,10 +1471,10 @@ describe("web auto-reply", () => {
}); });
setLoadConfigMock(() => ({ setLoadConfigMock(() => ({
inbound: { routing: {
groupChat: { mentionPatterns: ["@clawd"] }, groupChat: { mentionPatterns: ["@clawd"] },
session: { store: storePath },
}, },
session: { store: storePath },
})); }));
let capturedOnMessage: let capturedOnMessage:
@@ -1547,7 +1545,7 @@ describe("web auto-reply", () => {
const resolver = vi.fn().mockResolvedValue({ text: "ok" }); const resolver = vi.fn().mockResolvedValue({ text: "ok" });
setLoadConfigMock(() => ({ setLoadConfigMock(() => ({
inbound: { routing: {
// Self-chat heuristic: allowFrom includes selfE164. // Self-chat heuristic: allowFrom includes selfE164.
allowFrom: ["+999"], allowFrom: ["+999"],
groupChat: { groupChat: {
@@ -1697,8 +1695,10 @@ describe("web auto-reply", () => {
it("prefixes body with same-phone marker when from === to", async () => { it("prefixes body with same-phone marker when from === to", async () => {
// Enable messagePrefix for same-phone mode testing // Enable messagePrefix for same-phone mode testing
setLoadConfigMock(() => ({ setLoadConfigMock(() => ({
inbound: { routing: {
allowFrom: ["*"], allowFrom: ["*"],
},
messages: {
messagePrefix: "[same-phone]", messagePrefix: "[same-phone]",
responsePrefix: undefined, responsePrefix: undefined,
timestampPrefix: false, timestampPrefix: false,
@@ -1820,8 +1820,10 @@ describe("web auto-reply", () => {
it("applies responsePrefix to regular replies", async () => { it("applies responsePrefix to regular replies", async () => {
setLoadConfigMock(() => ({ setLoadConfigMock(() => ({
inbound: { routing: {
allowFrom: ["*"], allowFrom: ["*"],
},
messages: {
messagePrefix: undefined, messagePrefix: undefined,
responsePrefix: "🦞", responsePrefix: "🦞",
timestampPrefix: false, timestampPrefix: false,
@@ -1863,8 +1865,10 @@ describe("web auto-reply", () => {
it("skips responsePrefix for HEARTBEAT_OK responses", async () => { it("skips responsePrefix for HEARTBEAT_OK responses", async () => {
setLoadConfigMock(() => ({ setLoadConfigMock(() => ({
inbound: { routing: {
allowFrom: ["*"], allowFrom: ["*"],
},
messages: {
messagePrefix: undefined, messagePrefix: undefined,
responsePrefix: "🦞", responsePrefix: "🦞",
timestampPrefix: false, timestampPrefix: false,
@@ -1907,8 +1911,10 @@ describe("web auto-reply", () => {
it("does not double-prefix if responsePrefix already present", async () => { it("does not double-prefix if responsePrefix already present", async () => {
setLoadConfigMock(() => ({ setLoadConfigMock(() => ({
inbound: { routing: {
allowFrom: ["*"], allowFrom: ["*"],
},
messages: {
messagePrefix: undefined, messagePrefix: undefined,
responsePrefix: "🦞", responsePrefix: "🦞",
timestampPrefix: false, timestampPrefix: false,
@@ -1951,8 +1957,10 @@ describe("web auto-reply", () => {
it("sends tool summaries immediately with responsePrefix", async () => { it("sends tool summaries immediately with responsePrefix", async () => {
setLoadConfigMock(() => ({ setLoadConfigMock(() => ({
inbound: { routing: {
allowFrom: ["*"], allowFrom: ["*"],
},
messages: {
messagePrefix: undefined, messagePrefix: undefined,
responsePrefix: "🦞", responsePrefix: "🦞",
timestampPrefix: false, timestampPrefix: false,

View File

@@ -113,7 +113,7 @@ type MentionConfig = {
}; };
function buildMentionConfig(cfg: ReturnType<typeof loadConfig>): MentionConfig { function buildMentionConfig(cfg: ReturnType<typeof loadConfig>): MentionConfig {
const gc = cfg.inbound?.groupChat; const gc = cfg.routing?.groupChat;
const mentionRegexes = const mentionRegexes =
gc?.mentionPatterns gc?.mentionPatterns
?.map((p) => { ?.map((p) => {
@@ -124,7 +124,7 @@ function buildMentionConfig(cfg: ReturnType<typeof loadConfig>): MentionConfig {
} }
}) })
.filter((r): r is RegExp => Boolean(r)) ?? []; .filter((r): r is RegExp => Boolean(r)) ?? [];
return { mentionRegexes, allowFrom: cfg.inbound?.allowFrom }; return { mentionRegexes, allowFrom: cfg.routing?.allowFrom };
} }
function isBotMentioned( function isBotMentioned(
@@ -252,12 +252,12 @@ export async function runWebHeartbeatOnce(opts: {
}); });
const cfg = cfgOverride ?? loadConfig(); const cfg = cfgOverride ?? loadConfig();
const sessionCfg = cfg.inbound?.session; const sessionCfg = cfg.session;
const sessionScope = sessionCfg?.scope ?? "per-sender"; const sessionScope = sessionCfg?.scope ?? "per-sender";
const mainKey = sessionCfg?.mainKey; const mainKey = sessionCfg?.mainKey;
const sessionKey = resolveSessionKey(sessionScope, { From: to }, mainKey); const sessionKey = resolveSessionKey(sessionScope, { From: to }, mainKey);
if (sessionId) { if (sessionId) {
const storePath = resolveStorePath(cfg.inbound?.session?.store); const storePath = resolveStorePath(cfg.session?.store);
const store = loadSessionStore(storePath); const store = loadSessionStore(storePath);
const current = store[sessionKey] ?? {}; const current = store[sessionKey] ?? {};
store[sessionKey] = { store[sessionKey] = {
@@ -356,7 +356,7 @@ export async function runWebHeartbeatOnce(opts: {
const stripped = stripHeartbeatToken(replyPayload.text); const stripped = stripHeartbeatToken(replyPayload.text);
if (stripped.shouldSkip && !hasMedia) { if (stripped.shouldSkip && !hasMedia) {
// Don't let heartbeats keep sessions alive: restore previous updatedAt so idle expiry still works. // Don't let heartbeats keep sessions alive: restore previous updatedAt so idle expiry still works.
const storePath = resolveStorePath(cfg.inbound?.session?.store); const storePath = resolveStorePath(cfg.session?.store);
const store = loadSessionStore(storePath); const store = loadSessionStore(storePath);
if (sessionSnapshot.entry && store[sessionSnapshot.key]) { if (sessionSnapshot.entry && store[sessionSnapshot.key]) {
store[sessionSnapshot.key].updatedAt = sessionSnapshot.entry.updatedAt; store[sessionSnapshot.key].updatedAt = sessionSnapshot.entry.updatedAt;
@@ -420,7 +420,7 @@ export async function runWebHeartbeatOnce(opts: {
} }
function getFallbackRecipient(cfg: ReturnType<typeof loadConfig>) { function getFallbackRecipient(cfg: ReturnType<typeof loadConfig>) {
const sessionCfg = cfg.inbound?.session; const sessionCfg = cfg.session;
const storePath = resolveStorePath(sessionCfg?.store); const storePath = resolveStorePath(sessionCfg?.store);
const store = loadSessionStore(storePath); const store = loadSessionStore(storePath);
const mainKey = (sessionCfg?.mainKey ?? "main").trim() || "main"; const mainKey = (sessionCfg?.mainKey ?? "main").trim() || "main";
@@ -433,18 +433,18 @@ function getFallbackRecipient(cfg: ReturnType<typeof loadConfig>) {
} }
const allowFrom = const allowFrom =
Array.isArray(cfg.inbound?.allowFrom) && cfg.inbound.allowFrom.length > 0 Array.isArray(cfg.routing?.allowFrom) && cfg.routing.allowFrom.length > 0
? cfg.inbound.allowFrom.filter((v) => v !== "*") ? cfg.routing.allowFrom.filter((v) => v !== "*")
: []; : [];
if (allowFrom.length === 0) return null; if (allowFrom.length === 0) return null;
return allowFrom[0] ? normalizeE164(allowFrom[0]) : null; return allowFrom[0] ? normalizeE164(allowFrom[0]) : null;
} }
function getSessionRecipients(cfg: ReturnType<typeof loadConfig>) { function getSessionRecipients(cfg: ReturnType<typeof loadConfig>) {
const sessionCfg = cfg.inbound?.session; const sessionCfg = cfg.session;
const scope = sessionCfg?.scope ?? "per-sender"; const scope = sessionCfg?.scope ?? "per-sender";
if (scope === "global") return []; if (scope === "global") return [];
const storePath = resolveStorePath(cfg.inbound?.session?.store); const storePath = resolveStorePath(cfg.session?.store);
const store = loadSessionStore(storePath); const store = loadSessionStore(storePath);
const isGroupKey = (key: string) => const isGroupKey = (key: string) =>
key.startsWith("group:") || key.includes("@g.us"); key.startsWith("group:") || key.includes("@g.us");
@@ -480,8 +480,8 @@ export function resolveHeartbeatRecipients(
const sessionRecipients = getSessionRecipients(cfg); const sessionRecipients = getSessionRecipients(cfg);
const allowFrom = const allowFrom =
Array.isArray(cfg.inbound?.allowFrom) && cfg.inbound.allowFrom.length > 0 Array.isArray(cfg.routing?.allowFrom) && cfg.routing.allowFrom.length > 0
? cfg.inbound.allowFrom.filter((v) => v !== "*").map(normalizeE164) ? cfg.routing.allowFrom.filter((v) => v !== "*").map(normalizeE164)
: []; : [];
const unique = (list: string[]) => [...new Set(list.filter(Boolean))]; const unique = (list: string[]) => [...new Set(list.filter(Boolean))];
@@ -509,7 +509,7 @@ function getSessionSnapshot(
from: string, from: string,
isHeartbeat = false, isHeartbeat = false,
) { ) {
const sessionCfg = cfg.inbound?.session; const sessionCfg = cfg.session;
const scope = sessionCfg?.scope ?? "per-sender"; const scope = sessionCfg?.scope ?? "per-sender";
const key = resolveSessionKey( const key = resolveSessionKey(
scope, scope,
@@ -773,9 +773,9 @@ export async function monitorWebProvider(
); );
const reconnectPolicy = resolveReconnectPolicy(cfg, tuning.reconnect); const reconnectPolicy = resolveReconnectPolicy(cfg, tuning.reconnect);
const mentionConfig = buildMentionConfig(cfg); const mentionConfig = buildMentionConfig(cfg);
const sessionStorePath = resolveStorePath(cfg.inbound?.session?.store); const sessionStorePath = resolveStorePath(cfg.session?.store);
const groupHistoryLimit = const groupHistoryLimit =
cfg.inbound?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT; cfg.routing?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT;
const groupHistories = new Map< const groupHistories = new Map<
string, string,
Array<{ sender: string; body: string; timestamp?: number }> Array<{ sender: string; body: string; timestamp?: number }>
@@ -854,7 +854,7 @@ export async function monitorWebProvider(
: `group:${conversationId}`; : `group:${conversationId}`;
const store = loadSessionStore(sessionStorePath); const store = loadSessionStore(sessionStorePath);
const entry = store[key]; const entry = store[key];
const requireMention = cfg.inbound?.groupChat?.requireMention; const requireMention = cfg.routing?.groupChat?.requireMention;
const defaultActivation = requireMention === false ? "always" : "mention"; const defaultActivation = requireMention === false ? "always" : "mention";
return ( return (
normalizeGroupActivation(entry?.groupActivation) ?? defaultActivation normalizeGroupActivation(entry?.groupActivation) ?? defaultActivation
@@ -953,9 +953,9 @@ export async function monitorWebProvider(
const buildLine = (msg: WebInboundMsg) => { const buildLine = (msg: WebInboundMsg) => {
// Build message prefix: explicit config > default based on allowFrom // Build message prefix: explicit config > default based on allowFrom
let messagePrefix = cfg.inbound?.messagePrefix; let messagePrefix = cfg.messages?.messagePrefix;
if (messagePrefix === undefined) { if (messagePrefix === undefined) {
const hasAllowFrom = (cfg.inbound?.allowFrom?.length ?? 0) > 0; const hasAllowFrom = (cfg.routing?.allowFrom?.length ?? 0) > 0;
messagePrefix = hasAllowFrom ? "" : "[clawdis]"; messagePrefix = hasAllowFrom ? "" : "[clawdis]";
} }
const prefixStr = messagePrefix ? `${messagePrefix} ` : ""; const prefixStr = messagePrefix ? `${messagePrefix} ` : "";
@@ -1045,7 +1045,7 @@ export async function monitorWebProvider(
} }
if (msg.chatType !== "group") { if (msg.chatType !== "group") {
const sessionCfg = cfg.inbound?.session; const sessionCfg = cfg.session;
const mainKey = (sessionCfg?.mainKey ?? "main").trim() || "main"; const mainKey = (sessionCfg?.mainKey ?? "main").trim() || "main";
const storePath = resolveStorePath(sessionCfg?.store); const storePath = resolveStorePath(sessionCfg?.store);
const to = (() => { const to = (() => {
@@ -1075,7 +1075,7 @@ export async function monitorWebProvider(
} }
} }
const responsePrefix = cfg.inbound?.responsePrefix; const responsePrefix = cfg.messages?.responsePrefix;
let didSendReply = false; let didSendReply = false;
let toolSendChain: Promise<void> = Promise.resolve(); let toolSendChain: Promise<void> = Promise.resolve();
const sendToolResult = (payload: ReplyPayload) => { const sendToolResult = (payload: ReplyPayload) => {
@@ -1580,7 +1580,7 @@ export async function monitorWebProvider(
// Apply response prefix if configured (same as regular messages) // Apply response prefix if configured (same as regular messages)
let finalText = stripped.text; let finalText = stripped.text;
const responsePrefix = cfg.inbound?.responsePrefix; const responsePrefix = cfg.messages?.responsePrefix;
if ( if (
responsePrefix && responsePrefix &&
finalText && finalText &&

View File

@@ -7,8 +7,10 @@ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
vi.mock("../config/config.js", () => ({ vi.mock("../config/config.js", () => ({
loadConfig: vi.fn().mockReturnValue({ loadConfig: vi.fn().mockReturnValue({
inbound: { routing: {
allowFrom: ["*"], // Allow all in tests allowFrom: ["*"], // Allow all in tests
},
messages: {
messagePrefix: undefined, messagePrefix: undefined,
responsePrefix: undefined, responsePrefix: undefined,
timestampPrefix: false, timestampPrefix: false,

View File

@@ -145,7 +145,7 @@ export async function monitorWebInbox(options: {
// Filter unauthorized senders early to prevent wasted processing // Filter unauthorized senders early to prevent wasted processing
// and potential session corruption from Bad MAC errors // and potential session corruption from Bad MAC errors
const cfg = loadConfig(); const cfg = loadConfig();
const configuredAllowFrom = cfg.inbound?.allowFrom; const configuredAllowFrom = cfg.routing?.allowFrom;
// Without user config, default to self-only DM access so the owner can talk to themselves // Without user config, default to self-only DM access so the owner can talk to themselves
const defaultAllowFrom = const defaultAllowFrom =
(!configuredAllowFrom || configuredAllowFrom.length === 0) && selfE164 (!configuredAllowFrom || configuredAllowFrom.length === 0) && selfE164

View File

@@ -10,8 +10,10 @@ vi.mock("../media/store.js", () => ({
})); }));
const mockLoadConfig = vi.fn().mockReturnValue({ const mockLoadConfig = vi.fn().mockReturnValue({
inbound: { routing: {
allowFrom: ["*"], // Allow all in tests by default allowFrom: ["*"], // Allow all in tests by default
},
messages: {
messagePrefix: undefined, messagePrefix: undefined,
responsePrefix: undefined, responsePrefix: undefined,
timestampPrefix: false, timestampPrefix: false,
@@ -411,8 +413,10 @@ describe("web monitor inbox", () => {
it("still forwards group messages (with sender info) even when allowFrom is restrictive", async () => { it("still forwards group messages (with sender info) even when allowFrom is restrictive", async () => {
mockLoadConfig.mockReturnValue({ mockLoadConfig.mockReturnValue({
inbound: { routing: {
allowFrom: ["+111"], // does not include +777 allowFrom: ["+111"], // does not include +777
},
messages: {
messagePrefix: undefined, messagePrefix: undefined,
responsePrefix: undefined, responsePrefix: undefined,
timestampPrefix: false, timestampPrefix: false,
@@ -465,8 +469,10 @@ describe("web monitor inbox", () => {
// Test for auto-recovery fix: early allowFrom filtering prevents Bad MAC errors // Test for auto-recovery fix: early allowFrom filtering prevents Bad MAC errors
// from unauthorized senders corrupting sessions // from unauthorized senders corrupting sessions
mockLoadConfig.mockReturnValue({ mockLoadConfig.mockReturnValue({
inbound: { routing: {
allowFrom: ["+111"], // Only allow +111 allowFrom: ["+111"], // Only allow +111
},
messages: {
messagePrefix: undefined, messagePrefix: undefined,
responsePrefix: undefined, responsePrefix: undefined,
timestampPrefix: false, timestampPrefix: false,
@@ -503,8 +509,10 @@ describe("web monitor inbox", () => {
// Reset mock for other tests // Reset mock for other tests
mockLoadConfig.mockReturnValue({ mockLoadConfig.mockReturnValue({
inbound: { routing: {
allowFrom: ["*"], allowFrom: ["*"],
},
messages: {
messagePrefix: undefined, messagePrefix: undefined,
responsePrefix: undefined, responsePrefix: undefined,
timestampPrefix: false, timestampPrefix: false,
@@ -516,9 +524,11 @@ describe("web monitor inbox", () => {
it("skips read receipts in self-chat mode", async () => { it("skips read receipts in self-chat mode", async () => {
mockLoadConfig.mockReturnValue({ mockLoadConfig.mockReturnValue({
inbound: { routing: {
// Self-chat heuristic: allowFrom includes selfE164 (+123). // Self-chat heuristic: allowFrom includes selfE164 (+123).
allowFrom: ["+123"], allowFrom: ["+123"],
},
messages: {
messagePrefix: undefined, messagePrefix: undefined,
responsePrefix: undefined, responsePrefix: undefined,
timestampPrefix: false, timestampPrefix: false,
@@ -551,8 +561,10 @@ describe("web monitor inbox", () => {
// Reset mock for other tests // Reset mock for other tests
mockLoadConfig.mockReturnValue({ mockLoadConfig.mockReturnValue({
inbound: { routing: {
allowFrom: ["*"], allowFrom: ["*"],
},
messages: {
messagePrefix: undefined, messagePrefix: undefined,
responsePrefix: undefined, responsePrefix: undefined,
timestampPrefix: false, timestampPrefix: false,
@@ -564,8 +576,10 @@ describe("web monitor inbox", () => {
it("lets group messages through even when sender not in allowFrom", async () => { it("lets group messages through even when sender not in allowFrom", async () => {
mockLoadConfig.mockReturnValue({ mockLoadConfig.mockReturnValue({
inbound: { routing: {
allowFrom: ["+1234"], allowFrom: ["+1234"],
},
messages: {
messagePrefix: undefined, messagePrefix: undefined,
responsePrefix: undefined, responsePrefix: undefined,
timestampPrefix: false, timestampPrefix: false,
@@ -604,8 +618,10 @@ describe("web monitor inbox", () => {
it("allows messages from senders in allowFrom list", async () => { it("allows messages from senders in allowFrom list", async () => {
mockLoadConfig.mockReturnValue({ mockLoadConfig.mockReturnValue({
inbound: { routing: {
allowFrom: ["+111", "+999"], // Allow +999 allowFrom: ["+111", "+999"], // Allow +999
},
messages: {
messagePrefix: undefined, messagePrefix: undefined,
responsePrefix: undefined, responsePrefix: undefined,
timestampPrefix: false, timestampPrefix: false,
@@ -637,8 +653,10 @@ describe("web monitor inbox", () => {
// Reset mock for other tests // Reset mock for other tests
mockLoadConfig.mockReturnValue({ mockLoadConfig.mockReturnValue({
inbound: { routing: {
allowFrom: ["*"], allowFrom: ["*"],
},
messages: {
messagePrefix: undefined, messagePrefix: undefined,
responsePrefix: undefined, responsePrefix: undefined,
timestampPrefix: false, timestampPrefix: false,
@@ -652,8 +670,10 @@ describe("web monitor inbox", () => {
// Same-phone mode: when from === selfJid, should always be allowed // Same-phone mode: when from === selfJid, should always be allowed
// This allows users to message themselves even with restrictive allowFrom // This allows users to message themselves even with restrictive allowFrom
mockLoadConfig.mockReturnValue({ mockLoadConfig.mockReturnValue({
inbound: { routing: {
allowFrom: ["+111"], // Only allow +111, but self is +123 allowFrom: ["+111"], // Only allow +111, but self is +123
},
messages: {
messagePrefix: undefined, messagePrefix: undefined,
responsePrefix: undefined, responsePrefix: undefined,
timestampPrefix: false, timestampPrefix: false,
@@ -686,8 +706,10 @@ describe("web monitor inbox", () => {
// Reset mock for other tests // Reset mock for other tests
mockLoadConfig.mockReturnValue({ mockLoadConfig.mockReturnValue({
inbound: { routing: {
allowFrom: ["*"], allowFrom: ["*"],
},
messages: {
messagePrefix: undefined, messagePrefix: undefined,
responsePrefix: undefined, responsePrefix: undefined,
timestampPrefix: false, timestampPrefix: false,
@@ -751,8 +773,10 @@ it("defaults to self-only when no config is present", async () => {
// Reset mock for other tests // Reset mock for other tests
mockLoadConfig.mockReturnValue({ mockLoadConfig.mockReturnValue({
inbound: { routing: {
allowFrom: ["*"], allowFrom: ["*"],
},
messages: {
messagePrefix: undefined, messagePrefix: undefined,
responsePrefix: undefined, responsePrefix: undefined,
timestampPrefix: false, timestampPrefix: false,

View File

@@ -6,9 +6,11 @@ import { createMockBaileys } from "../../test/mocks/baileys.js";
// Use globalThis to store the mock config so it survives vi.mock hoisting // Use globalThis to store the mock config so it survives vi.mock hoisting
const CONFIG_KEY = Symbol.for("clawdis:testConfigMock"); const CONFIG_KEY = Symbol.for("clawdis:testConfigMock");
const DEFAULT_CONFIG = { const DEFAULT_CONFIG = {
inbound: { routing: {
// Tests can override; default remains open to avoid surprising fixtures // Tests can override; default remains open to avoid surprising fixtures
allowFrom: ["*"], allowFrom: ["*"],
},
messages: {
messagePrefix: undefined, messagePrefix: undefined,
responsePrefix: undefined, responsePrefix: undefined,
timestampPrefix: false, timestampPrefix: false,