refactor: move inbound config
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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 },
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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`),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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() },
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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(", ")}`));
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 &&
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user