Adds `agent.humanDelay` config option to create natural rhythm between
streamed message bubbles. When enabled, introduces a random delay
(default 800-2500ms) between block replies, making multi-message
responses feel more like natural human texting.
Config example:
```json
{
"agent": {
"blockStreamingDefault": "on",
"humanDelay": {
"enabled": true,
"minMs": 800,
"maxMs": 2500
}
}
}
```
- First message sends immediately
- Subsequent messages wait a random delay before sending
- Works with iMessage, Signal, and Discord providers
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1532 lines
46 KiB
TypeScript
1532 lines
46 KiB
TypeScript
import { z } from "zod";
|
|
|
|
import { parseDurationMs } from "../cli/parse-duration.js";
|
|
|
|
const ModelApiSchema = z.union([
|
|
z.literal("openai-completions"),
|
|
z.literal("openai-responses"),
|
|
z.literal("anthropic-messages"),
|
|
z.literal("google-generative-ai"),
|
|
]);
|
|
|
|
const ModelCompatSchema = z
|
|
.object({
|
|
supportsStore: z.boolean().optional(),
|
|
supportsDeveloperRole: z.boolean().optional(),
|
|
supportsReasoningEffort: z.boolean().optional(),
|
|
maxTokensField: z
|
|
.union([z.literal("max_completion_tokens"), z.literal("max_tokens")])
|
|
.optional(),
|
|
})
|
|
.optional();
|
|
|
|
const ModelDefinitionSchema = z.object({
|
|
id: z.string().min(1),
|
|
name: z.string().min(1),
|
|
api: ModelApiSchema.optional(),
|
|
reasoning: z.boolean(),
|
|
input: z.array(z.union([z.literal("text"), z.literal("image")])),
|
|
cost: z.object({
|
|
input: z.number(),
|
|
output: z.number(),
|
|
cacheRead: z.number(),
|
|
cacheWrite: z.number(),
|
|
}),
|
|
contextWindow: z.number().positive(),
|
|
maxTokens: z.number().positive(),
|
|
headers: z.record(z.string(), z.string()).optional(),
|
|
compat: ModelCompatSchema,
|
|
});
|
|
|
|
const ModelProviderSchema = z.object({
|
|
baseUrl: z.string().min(1),
|
|
apiKey: z.string().optional(),
|
|
api: ModelApiSchema.optional(),
|
|
headers: z.record(z.string(), z.string()).optional(),
|
|
authHeader: z.boolean().optional(),
|
|
models: z.array(ModelDefinitionSchema),
|
|
});
|
|
|
|
const ModelsConfigSchema = z
|
|
.object({
|
|
mode: z.union([z.literal("merge"), z.literal("replace")]).optional(),
|
|
providers: z.record(z.string(), ModelProviderSchema).optional(),
|
|
})
|
|
.optional();
|
|
|
|
const GroupChatSchema = z
|
|
.object({
|
|
mentionPatterns: z.array(z.string()).optional(),
|
|
historyLimit: z.number().int().positive().optional(),
|
|
})
|
|
.optional();
|
|
|
|
const IdentitySchema = z
|
|
.object({
|
|
name: z.string().optional(),
|
|
theme: z.string().optional(),
|
|
emoji: z.string().optional(),
|
|
})
|
|
.optional();
|
|
|
|
const QueueModeSchema = z.union([
|
|
z.literal("steer"),
|
|
z.literal("followup"),
|
|
z.literal("collect"),
|
|
z.literal("steer-backlog"),
|
|
z.literal("steer+backlog"),
|
|
z.literal("queue"),
|
|
z.literal("interrupt"),
|
|
]);
|
|
const QueueDropSchema = z.union([
|
|
z.literal("old"),
|
|
z.literal("new"),
|
|
z.literal("summarize"),
|
|
]);
|
|
const ReplyToModeSchema = z.union([
|
|
z.literal("off"),
|
|
z.literal("first"),
|
|
z.literal("all"),
|
|
]);
|
|
|
|
// GroupPolicySchema: controls how group messages are handled
|
|
// Used with .default("open").optional() pattern:
|
|
// - .optional() allows field omission in input config
|
|
// - .default("open") ensures runtime always resolves to "open" if not provided
|
|
const GroupPolicySchema = z.enum(["open", "disabled", "allowlist"]);
|
|
|
|
const DmPolicySchema = z.enum(["pairing", "allowlist", "open", "disabled"]);
|
|
|
|
const BlockStreamingCoalesceSchema = z.object({
|
|
minChars: z.number().int().positive().optional(),
|
|
maxChars: z.number().int().positive().optional(),
|
|
idleMs: z.number().int().nonnegative().optional(),
|
|
});
|
|
|
|
const HumanDelaySchema = z.object({
|
|
mode: z
|
|
.union([z.literal("off"), z.literal("natural"), z.literal("custom")])
|
|
.optional(),
|
|
minMs: z.number().int().nonnegative().optional(),
|
|
maxMs: z.number().int().nonnegative().optional(),
|
|
});
|
|
|
|
const normalizeAllowFrom = (values?: Array<string | number>): string[] =>
|
|
(values ?? []).map((v) => String(v).trim()).filter(Boolean);
|
|
|
|
const requireOpenAllowFrom = (params: {
|
|
policy?: string;
|
|
allowFrom?: Array<string | number>;
|
|
ctx: z.RefinementCtx;
|
|
path: Array<string | number>;
|
|
message: string;
|
|
}) => {
|
|
if (params.policy !== "open") return;
|
|
const allow = normalizeAllowFrom(params.allowFrom);
|
|
if (allow.includes("*")) return;
|
|
params.ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: params.path,
|
|
message: params.message,
|
|
});
|
|
};
|
|
|
|
const MSTeamsReplyStyleSchema = z.enum(["thread", "top-level"]);
|
|
|
|
const RetryConfigSchema = z
|
|
.object({
|
|
attempts: z.number().int().min(1).optional(),
|
|
minDelayMs: z.number().int().min(0).optional(),
|
|
maxDelayMs: z.number().int().min(0).optional(),
|
|
jitter: z.number().min(0).max(1).optional(),
|
|
})
|
|
.optional();
|
|
|
|
const QueueModeBySurfaceSchema = z
|
|
.object({
|
|
whatsapp: QueueModeSchema.optional(),
|
|
telegram: QueueModeSchema.optional(),
|
|
discord: QueueModeSchema.optional(),
|
|
slack: QueueModeSchema.optional(),
|
|
signal: QueueModeSchema.optional(),
|
|
imessage: QueueModeSchema.optional(),
|
|
msteams: QueueModeSchema.optional(),
|
|
webchat: QueueModeSchema.optional(),
|
|
})
|
|
.optional();
|
|
|
|
const QueueSchema = z
|
|
.object({
|
|
mode: QueueModeSchema.optional(),
|
|
byProvider: QueueModeBySurfaceSchema,
|
|
debounceMs: z.number().int().nonnegative().optional(),
|
|
cap: z.number().int().positive().optional(),
|
|
drop: QueueDropSchema.optional(),
|
|
})
|
|
.optional();
|
|
|
|
const TranscribeAudioSchema = z
|
|
.object({
|
|
command: z.array(z.string()),
|
|
timeoutSeconds: z.number().int().positive().optional(),
|
|
})
|
|
.optional();
|
|
|
|
const HexColorSchema = z
|
|
.string()
|
|
.regex(/^#?[0-9a-fA-F]{6}$/, "expected hex color (RRGGBB)");
|
|
|
|
const TelegramTopicSchema = z.object({
|
|
requireMention: z.boolean().optional(),
|
|
skills: z.array(z.string()).optional(),
|
|
enabled: z.boolean().optional(),
|
|
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
systemPrompt: z.string().optional(),
|
|
});
|
|
|
|
const TelegramGroupSchema = z.object({
|
|
requireMention: z.boolean().optional(),
|
|
skills: z.array(z.string()).optional(),
|
|
enabled: z.boolean().optional(),
|
|
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
systemPrompt: z.string().optional(),
|
|
topics: z.record(z.string(), TelegramTopicSchema.optional()).optional(),
|
|
});
|
|
|
|
const TelegramAccountSchemaBase = z.object({
|
|
name: z.string().optional(),
|
|
capabilities: z.array(z.string()).optional(),
|
|
enabled: z.boolean().optional(),
|
|
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
|
botToken: z.string().optional(),
|
|
tokenFile: z.string().optional(),
|
|
replyToMode: ReplyToModeSchema.optional(),
|
|
groups: z.record(z.string(), TelegramGroupSchema.optional()).optional(),
|
|
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
groupPolicy: GroupPolicySchema.optional().default("open"),
|
|
textChunkLimit: z.number().int().positive().optional(),
|
|
blockStreaming: z.boolean().optional(),
|
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
|
streamMode: z.enum(["off", "partial", "block"]).optional().default("partial"),
|
|
mediaMaxMb: z.number().positive().optional(),
|
|
retry: RetryConfigSchema,
|
|
proxy: z.string().optional(),
|
|
webhookUrl: z.string().optional(),
|
|
webhookSecret: z.string().optional(),
|
|
webhookPath: z.string().optional(),
|
|
actions: z
|
|
.object({
|
|
reactions: z.boolean().optional(),
|
|
})
|
|
.optional(),
|
|
});
|
|
|
|
const TelegramAccountSchema = TelegramAccountSchemaBase.superRefine(
|
|
(value, ctx) => {
|
|
requireOpenAllowFrom({
|
|
policy: value.dmPolicy,
|
|
allowFrom: value.allowFrom,
|
|
ctx,
|
|
path: ["allowFrom"],
|
|
message:
|
|
'telegram.dmPolicy="open" requires telegram.allowFrom to include "*"',
|
|
});
|
|
},
|
|
);
|
|
|
|
const TelegramConfigSchema = TelegramAccountSchemaBase.extend({
|
|
accounts: z.record(z.string(), TelegramAccountSchema.optional()).optional(),
|
|
}).superRefine((value, ctx) => {
|
|
requireOpenAllowFrom({
|
|
policy: value.dmPolicy,
|
|
allowFrom: value.allowFrom,
|
|
ctx,
|
|
path: ["allowFrom"],
|
|
message:
|
|
'telegram.dmPolicy="open" requires telegram.allowFrom to include "*"',
|
|
});
|
|
});
|
|
|
|
const DiscordDmSchema = z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
policy: DmPolicySchema.optional().default("pairing"),
|
|
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
groupEnabled: z.boolean().optional(),
|
|
groupChannels: z.array(z.union([z.string(), z.number()])).optional(),
|
|
})
|
|
.superRefine((value, ctx) => {
|
|
requireOpenAllowFrom({
|
|
policy: value.policy,
|
|
allowFrom: value.allowFrom,
|
|
ctx,
|
|
path: ["allowFrom"],
|
|
message:
|
|
'discord.dm.policy="open" requires discord.dm.allowFrom to include "*"',
|
|
});
|
|
});
|
|
|
|
const DiscordGuildChannelSchema = z.object({
|
|
allow: z.boolean().optional(),
|
|
requireMention: z.boolean().optional(),
|
|
skills: z.array(z.string()).optional(),
|
|
enabled: z.boolean().optional(),
|
|
users: z.array(z.union([z.string(), z.number()])).optional(),
|
|
systemPrompt: z.string().optional(),
|
|
});
|
|
|
|
const DiscordGuildSchema = z.object({
|
|
slug: z.string().optional(),
|
|
requireMention: z.boolean().optional(),
|
|
reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(),
|
|
users: z.array(z.union([z.string(), z.number()])).optional(),
|
|
channels: z
|
|
.record(z.string(), DiscordGuildChannelSchema.optional())
|
|
.optional(),
|
|
});
|
|
|
|
const DiscordAccountSchema = z.object({
|
|
name: z.string().optional(),
|
|
capabilities: z.array(z.string()).optional(),
|
|
enabled: z.boolean().optional(),
|
|
token: z.string().optional(),
|
|
groupPolicy: GroupPolicySchema.optional().default("open"),
|
|
textChunkLimit: z.number().int().positive().optional(),
|
|
blockStreaming: z.boolean().optional(),
|
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
|
maxLinesPerMessage: z.number().int().positive().optional(),
|
|
mediaMaxMb: z.number().positive().optional(),
|
|
historyLimit: z.number().int().min(0).optional(),
|
|
retry: RetryConfigSchema,
|
|
actions: z
|
|
.object({
|
|
reactions: z.boolean().optional(),
|
|
stickers: z.boolean().optional(),
|
|
polls: z.boolean().optional(),
|
|
permissions: z.boolean().optional(),
|
|
messages: z.boolean().optional(),
|
|
threads: z.boolean().optional(),
|
|
pins: z.boolean().optional(),
|
|
search: z.boolean().optional(),
|
|
memberInfo: z.boolean().optional(),
|
|
roleInfo: z.boolean().optional(),
|
|
roles: z.boolean().optional(),
|
|
channelInfo: z.boolean().optional(),
|
|
voiceStatus: z.boolean().optional(),
|
|
events: z.boolean().optional(),
|
|
moderation: z.boolean().optional(),
|
|
})
|
|
.optional(),
|
|
replyToMode: ReplyToModeSchema.optional(),
|
|
dm: DiscordDmSchema.optional(),
|
|
guilds: z.record(z.string(), DiscordGuildSchema.optional()).optional(),
|
|
});
|
|
|
|
const DiscordConfigSchema = DiscordAccountSchema.extend({
|
|
accounts: z.record(z.string(), DiscordAccountSchema.optional()).optional(),
|
|
});
|
|
|
|
const SlackDmSchema = z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
policy: DmPolicySchema.optional().default("pairing"),
|
|
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
groupEnabled: z.boolean().optional(),
|
|
groupChannels: z.array(z.union([z.string(), z.number()])).optional(),
|
|
})
|
|
.superRefine((value, ctx) => {
|
|
requireOpenAllowFrom({
|
|
policy: value.policy,
|
|
allowFrom: value.allowFrom,
|
|
ctx,
|
|
path: ["allowFrom"],
|
|
message:
|
|
'slack.dm.policy="open" requires slack.dm.allowFrom to include "*"',
|
|
});
|
|
});
|
|
|
|
const SlackChannelSchema = z.object({
|
|
enabled: z.boolean().optional(),
|
|
allow: z.boolean().optional(),
|
|
requireMention: z.boolean().optional(),
|
|
allowBots: z.boolean().optional(),
|
|
users: z.array(z.union([z.string(), z.number()])).optional(),
|
|
skills: z.array(z.string()).optional(),
|
|
systemPrompt: z.string().optional(),
|
|
});
|
|
|
|
const SlackAccountSchema = z.object({
|
|
name: z.string().optional(),
|
|
capabilities: z.array(z.string()).optional(),
|
|
enabled: z.boolean().optional(),
|
|
botToken: z.string().optional(),
|
|
appToken: z.string().optional(),
|
|
allowBots: z.boolean().optional(),
|
|
groupPolicy: GroupPolicySchema.optional().default("open"),
|
|
textChunkLimit: z.number().int().positive().optional(),
|
|
blockStreaming: z.boolean().optional(),
|
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
|
mediaMaxMb: z.number().positive().optional(),
|
|
reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(),
|
|
reactionAllowlist: z.array(z.union([z.string(), z.number()])).optional(),
|
|
replyToMode: ReplyToModeSchema.optional(),
|
|
actions: z
|
|
.object({
|
|
reactions: z.boolean().optional(),
|
|
messages: z.boolean().optional(),
|
|
pins: z.boolean().optional(),
|
|
search: z.boolean().optional(),
|
|
permissions: z.boolean().optional(),
|
|
memberInfo: z.boolean().optional(),
|
|
channelInfo: z.boolean().optional(),
|
|
emojiList: z.boolean().optional(),
|
|
})
|
|
.optional(),
|
|
slashCommand: z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
name: z.string().optional(),
|
|
sessionPrefix: z.string().optional(),
|
|
ephemeral: z.boolean().optional(),
|
|
})
|
|
.optional(),
|
|
dm: SlackDmSchema.optional(),
|
|
channels: z.record(z.string(), SlackChannelSchema.optional()).optional(),
|
|
});
|
|
|
|
const SlackConfigSchema = SlackAccountSchema.extend({
|
|
accounts: z.record(z.string(), SlackAccountSchema.optional()).optional(),
|
|
});
|
|
|
|
const SignalAccountSchemaBase = z.object({
|
|
name: z.string().optional(),
|
|
capabilities: z.array(z.string()).optional(),
|
|
enabled: z.boolean().optional(),
|
|
account: z.string().optional(),
|
|
httpUrl: z.string().optional(),
|
|
httpHost: z.string().optional(),
|
|
httpPort: z.number().int().positive().optional(),
|
|
cliPath: z.string().optional(),
|
|
autoStart: z.boolean().optional(),
|
|
receiveMode: z.union([z.literal("on-start"), z.literal("manual")]).optional(),
|
|
ignoreAttachments: z.boolean().optional(),
|
|
ignoreStories: z.boolean().optional(),
|
|
sendReadReceipts: z.boolean().optional(),
|
|
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
|
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
groupPolicy: GroupPolicySchema.optional().default("open"),
|
|
textChunkLimit: z.number().int().positive().optional(),
|
|
blockStreaming: z.boolean().optional(),
|
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
|
mediaMaxMb: z.number().int().positive().optional(),
|
|
reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(),
|
|
reactionAllowlist: z.array(z.union([z.string(), z.number()])).optional(),
|
|
});
|
|
|
|
const SignalAccountSchema = SignalAccountSchemaBase.superRefine(
|
|
(value, ctx) => {
|
|
requireOpenAllowFrom({
|
|
policy: value.dmPolicy,
|
|
allowFrom: value.allowFrom,
|
|
ctx,
|
|
path: ["allowFrom"],
|
|
message:
|
|
'signal.dmPolicy="open" requires signal.allowFrom to include "*"',
|
|
});
|
|
},
|
|
);
|
|
|
|
const SignalConfigSchema = SignalAccountSchemaBase.extend({
|
|
accounts: z.record(z.string(), SignalAccountSchema.optional()).optional(),
|
|
}).superRefine((value, ctx) => {
|
|
requireOpenAllowFrom({
|
|
policy: value.dmPolicy,
|
|
allowFrom: value.allowFrom,
|
|
ctx,
|
|
path: ["allowFrom"],
|
|
message: 'signal.dmPolicy="open" requires signal.allowFrom to include "*"',
|
|
});
|
|
});
|
|
|
|
const IMessageAccountSchemaBase = z.object({
|
|
name: z.string().optional(),
|
|
capabilities: z.array(z.string()).optional(),
|
|
enabled: z.boolean().optional(),
|
|
cliPath: z.string().optional(),
|
|
dbPath: z.string().optional(),
|
|
service: z
|
|
.union([z.literal("imessage"), z.literal("sms"), z.literal("auto")])
|
|
.optional(),
|
|
region: z.string().optional(),
|
|
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
|
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
|
groupPolicy: GroupPolicySchema.optional().default("open"),
|
|
includeAttachments: z.boolean().optional(),
|
|
mediaMaxMb: z.number().int().positive().optional(),
|
|
textChunkLimit: z.number().int().positive().optional(),
|
|
blockStreaming: z.boolean().optional(),
|
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
|
groups: z
|
|
.record(
|
|
z.string(),
|
|
z
|
|
.object({
|
|
requireMention: z.boolean().optional(),
|
|
})
|
|
.optional(),
|
|
)
|
|
.optional(),
|
|
});
|
|
|
|
const IMessageAccountSchema = IMessageAccountSchemaBase.superRefine(
|
|
(value, ctx) => {
|
|
requireOpenAllowFrom({
|
|
policy: value.dmPolicy,
|
|
allowFrom: value.allowFrom,
|
|
ctx,
|
|
path: ["allowFrom"],
|
|
message:
|
|
'imessage.dmPolicy="open" requires imessage.allowFrom to include "*"',
|
|
});
|
|
},
|
|
);
|
|
|
|
const IMessageConfigSchema = IMessageAccountSchemaBase.extend({
|
|
accounts: z.record(z.string(), IMessageAccountSchema.optional()).optional(),
|
|
}).superRefine((value, ctx) => {
|
|
requireOpenAllowFrom({
|
|
policy: value.dmPolicy,
|
|
allowFrom: value.allowFrom,
|
|
ctx,
|
|
path: ["allowFrom"],
|
|
message:
|
|
'imessage.dmPolicy="open" requires imessage.allowFrom to include "*"',
|
|
});
|
|
});
|
|
|
|
const MSTeamsChannelSchema = z.object({
|
|
requireMention: z.boolean().optional(),
|
|
replyStyle: MSTeamsReplyStyleSchema.optional(),
|
|
});
|
|
|
|
const MSTeamsTeamSchema = z.object({
|
|
requireMention: z.boolean().optional(),
|
|
replyStyle: MSTeamsReplyStyleSchema.optional(),
|
|
channels: z.record(z.string(), MSTeamsChannelSchema.optional()).optional(),
|
|
});
|
|
|
|
const MSTeamsConfigSchema = z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
capabilities: z.array(z.string()).optional(),
|
|
appId: z.string().optional(),
|
|
appPassword: z.string().optional(),
|
|
tenantId: z.string().optional(),
|
|
webhook: z
|
|
.object({
|
|
port: z.number().int().positive().optional(),
|
|
path: z.string().optional(),
|
|
})
|
|
.optional(),
|
|
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
|
allowFrom: z.array(z.string()).optional(),
|
|
textChunkLimit: z.number().int().positive().optional(),
|
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
|
mediaAllowHosts: z.array(z.string()).optional(),
|
|
requireMention: z.boolean().optional(),
|
|
replyStyle: MSTeamsReplyStyleSchema.optional(),
|
|
teams: z.record(z.string(), MSTeamsTeamSchema.optional()).optional(),
|
|
})
|
|
.superRefine((value, ctx) => {
|
|
requireOpenAllowFrom({
|
|
policy: value.dmPolicy,
|
|
allowFrom: value.allowFrom,
|
|
ctx,
|
|
path: ["allowFrom"],
|
|
message:
|
|
'msteams.dmPolicy="open" requires msteams.allowFrom to include "*"',
|
|
});
|
|
});
|
|
|
|
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(),
|
|
typingMode: z
|
|
.union([
|
|
z.literal("never"),
|
|
z.literal("instant"),
|
|
z.literal("thinking"),
|
|
z.literal("message"),
|
|
])
|
|
.optional(),
|
|
mainKey: z.string().optional(),
|
|
sendPolicy: z
|
|
.object({
|
|
default: z.union([z.literal("allow"), z.literal("deny")]).optional(),
|
|
rules: z
|
|
.array(
|
|
z.object({
|
|
action: z.union([z.literal("allow"), z.literal("deny")]),
|
|
match: z
|
|
.object({
|
|
provider: z.string().optional(),
|
|
chatType: z
|
|
.union([
|
|
z.literal("direct"),
|
|
z.literal("group"),
|
|
z.literal("room"),
|
|
])
|
|
.optional(),
|
|
keyPrefix: z.string().optional(),
|
|
})
|
|
.optional(),
|
|
}),
|
|
)
|
|
.optional(),
|
|
})
|
|
.optional(),
|
|
agentToAgent: z
|
|
.object({
|
|
maxPingPongTurns: z.number().int().min(0).max(5).optional(),
|
|
})
|
|
.optional(),
|
|
})
|
|
.optional();
|
|
|
|
const MessagesSchema = z
|
|
.object({
|
|
messagePrefix: z.string().optional(),
|
|
responsePrefix: z.string().optional(),
|
|
groupChat: GroupChatSchema,
|
|
queue: QueueSchema,
|
|
ackReaction: z.string().optional(),
|
|
ackReactionScope: z
|
|
.enum(["group-mentions", "group-all", "direct", "all"])
|
|
.optional(),
|
|
removeAckAfterReply: z.boolean().optional(),
|
|
})
|
|
.optional();
|
|
|
|
const CommandsSchema = z
|
|
.object({
|
|
native: z.boolean().optional(),
|
|
text: z.boolean().optional(),
|
|
restart: z.boolean().optional(),
|
|
useAccessGroups: z.boolean().optional(),
|
|
})
|
|
.optional();
|
|
|
|
const HeartbeatSchema = z
|
|
.object({
|
|
every: z.string().optional(),
|
|
model: z.string().optional(),
|
|
target: z
|
|
.union([
|
|
z.literal("last"),
|
|
z.literal("whatsapp"),
|
|
z.literal("telegram"),
|
|
z.literal("discord"),
|
|
z.literal("slack"),
|
|
z.literal("msteams"),
|
|
z.literal("signal"),
|
|
z.literal("imessage"),
|
|
z.literal("none"),
|
|
])
|
|
.optional(),
|
|
to: z.string().optional(),
|
|
prompt: z.string().optional(),
|
|
ackMaxChars: z.number().int().nonnegative().optional(),
|
|
})
|
|
.superRefine((val, ctx) => {
|
|
if (!val.every) return;
|
|
try {
|
|
parseDurationMs(val.every, { defaultUnit: "m" });
|
|
} catch {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ["every"],
|
|
message: "invalid duration (use ms, s, m, h)",
|
|
});
|
|
}
|
|
})
|
|
.optional();
|
|
|
|
const SandboxDockerSchema = z
|
|
.object({
|
|
image: z.string().optional(),
|
|
containerPrefix: z.string().optional(),
|
|
workdir: z.string().optional(),
|
|
readOnlyRoot: z.boolean().optional(),
|
|
tmpfs: z.array(z.string()).optional(),
|
|
network: z.string().optional(),
|
|
user: z.string().optional(),
|
|
capDrop: z.array(z.string()).optional(),
|
|
env: z.record(z.string(), z.string()).optional(),
|
|
setupCommand: z.string().optional(),
|
|
pidsLimit: z.number().int().positive().optional(),
|
|
memory: z.union([z.string(), z.number()]).optional(),
|
|
memorySwap: z.union([z.string(), z.number()]).optional(),
|
|
cpus: z.number().positive().optional(),
|
|
ulimits: z
|
|
.record(
|
|
z.string(),
|
|
z.union([
|
|
z.string(),
|
|
z.number(),
|
|
z.object({
|
|
soft: z.number().int().nonnegative().optional(),
|
|
hard: z.number().int().nonnegative().optional(),
|
|
}),
|
|
]),
|
|
)
|
|
.optional(),
|
|
seccompProfile: z.string().optional(),
|
|
apparmorProfile: z.string().optional(),
|
|
dns: z.array(z.string()).optional(),
|
|
extraHosts: z.array(z.string()).optional(),
|
|
})
|
|
.optional();
|
|
|
|
const SandboxBrowserSchema = z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
image: z.string().optional(),
|
|
containerPrefix: z.string().optional(),
|
|
cdpPort: z.number().int().positive().optional(),
|
|
vncPort: z.number().int().positive().optional(),
|
|
noVncPort: z.number().int().positive().optional(),
|
|
headless: z.boolean().optional(),
|
|
enableNoVnc: z.boolean().optional(),
|
|
autoStart: z.boolean().optional(),
|
|
autoStartTimeoutMs: z.number().int().positive().optional(),
|
|
})
|
|
.optional();
|
|
|
|
const SandboxPruneSchema = z
|
|
.object({
|
|
idleHours: z.number().int().nonnegative().optional(),
|
|
maxAgeDays: z.number().int().nonnegative().optional(),
|
|
})
|
|
.optional();
|
|
|
|
const ToolPolicySchema = z
|
|
.object({
|
|
allow: z.array(z.string()).optional(),
|
|
deny: z.array(z.string()).optional(),
|
|
})
|
|
.optional();
|
|
|
|
const ElevatedAllowFromSchema = z
|
|
.object({
|
|
whatsapp: z.array(z.string()).optional(),
|
|
telegram: z.array(z.union([z.string(), z.number()])).optional(),
|
|
discord: z.array(z.union([z.string(), z.number()])).optional(),
|
|
slack: z.array(z.union([z.string(), z.number()])).optional(),
|
|
signal: z.array(z.union([z.string(), z.number()])).optional(),
|
|
imessage: z.array(z.union([z.string(), z.number()])).optional(),
|
|
webchat: z.array(z.union([z.string(), z.number()])).optional(),
|
|
})
|
|
.optional();
|
|
|
|
const AgentSandboxSchema = z
|
|
.object({
|
|
mode: z
|
|
.union([z.literal("off"), z.literal("non-main"), z.literal("all")])
|
|
.optional(),
|
|
workspaceAccess: z
|
|
.union([z.literal("none"), z.literal("ro"), z.literal("rw")])
|
|
.optional(),
|
|
sessionToolsVisibility: z
|
|
.union([z.literal("spawned"), z.literal("all")])
|
|
.optional(),
|
|
scope: z
|
|
.union([z.literal("session"), z.literal("agent"), z.literal("shared")])
|
|
.optional(),
|
|
perSession: z.boolean().optional(),
|
|
workspaceRoot: z.string().optional(),
|
|
docker: SandboxDockerSchema,
|
|
browser: SandboxBrowserSchema,
|
|
prune: SandboxPruneSchema,
|
|
})
|
|
.optional();
|
|
|
|
const AgentToolsSchema = z
|
|
.object({
|
|
allow: z.array(z.string()).optional(),
|
|
deny: z.array(z.string()).optional(),
|
|
elevated: z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
allowFrom: ElevatedAllowFromSchema,
|
|
})
|
|
.optional(),
|
|
sandbox: z
|
|
.object({
|
|
tools: ToolPolicySchema,
|
|
})
|
|
.optional(),
|
|
})
|
|
.optional();
|
|
|
|
const AgentEntrySchema = z.object({
|
|
id: z.string(),
|
|
default: z.boolean().optional(),
|
|
name: z.string().optional(),
|
|
workspace: z.string().optional(),
|
|
agentDir: z.string().optional(),
|
|
model: z.string().optional(),
|
|
humanDelay: HumanDelaySchema.optional(),
|
|
identity: IdentitySchema,
|
|
groupChat: GroupChatSchema,
|
|
subagents: z
|
|
.object({
|
|
allowAgents: z.array(z.string()).optional(),
|
|
})
|
|
.optional(),
|
|
sandbox: AgentSandboxSchema,
|
|
tools: AgentToolsSchema,
|
|
});
|
|
|
|
const ToolsSchema = z
|
|
.object({
|
|
allow: z.array(z.string()).optional(),
|
|
deny: z.array(z.string()).optional(),
|
|
agentToAgent: z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
allow: z.array(z.string()).optional(),
|
|
})
|
|
.optional(),
|
|
elevated: z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
allowFrom: ElevatedAllowFromSchema,
|
|
})
|
|
.optional(),
|
|
bash: z
|
|
.object({
|
|
backgroundMs: z.number().int().positive().optional(),
|
|
timeoutSec: z.number().int().positive().optional(),
|
|
cleanupMs: z.number().int().positive().optional(),
|
|
})
|
|
.optional(),
|
|
subagents: z
|
|
.object({
|
|
tools: ToolPolicySchema,
|
|
})
|
|
.optional(),
|
|
sandbox: z
|
|
.object({
|
|
tools: ToolPolicySchema,
|
|
})
|
|
.optional(),
|
|
})
|
|
.optional();
|
|
|
|
const AgentsSchema = z
|
|
.object({
|
|
defaults: z.lazy(() => AgentDefaultsSchema).optional(),
|
|
list: z.array(AgentEntrySchema).optional(),
|
|
})
|
|
.optional();
|
|
|
|
const BindingsSchema = z
|
|
.array(
|
|
z.object({
|
|
agentId: z.string(),
|
|
match: z.object({
|
|
provider: z.string(),
|
|
accountId: z.string().optional(),
|
|
peer: z
|
|
.object({
|
|
kind: z.union([
|
|
z.literal("dm"),
|
|
z.literal("group"),
|
|
z.literal("channel"),
|
|
]),
|
|
id: z.string(),
|
|
})
|
|
.optional(),
|
|
guildId: z.string().optional(),
|
|
teamId: z.string().optional(),
|
|
}),
|
|
}),
|
|
)
|
|
.optional();
|
|
|
|
const BroadcastStrategySchema = z.enum(["parallel", "sequential"]);
|
|
|
|
const BroadcastSchema = z
|
|
.object({
|
|
strategy: BroadcastStrategySchema.optional(),
|
|
})
|
|
.catchall(z.array(z.string()))
|
|
.optional();
|
|
|
|
const AudioSchema = z
|
|
.object({
|
|
transcription: TranscribeAudioSchema,
|
|
})
|
|
.optional();
|
|
|
|
const HookMappingSchema = z
|
|
.object({
|
|
id: z.string().optional(),
|
|
match: z
|
|
.object({
|
|
path: z.string().optional(),
|
|
source: z.string().optional(),
|
|
})
|
|
.optional(),
|
|
action: z.union([z.literal("wake"), z.literal("agent")]).optional(),
|
|
wakeMode: z
|
|
.union([z.literal("now"), z.literal("next-heartbeat")])
|
|
.optional(),
|
|
name: z.string().optional(),
|
|
sessionKey: z.string().optional(),
|
|
messageTemplate: z.string().optional(),
|
|
textTemplate: z.string().optional(),
|
|
deliver: z.boolean().optional(),
|
|
provider: z
|
|
.union([
|
|
z.literal("last"),
|
|
z.literal("whatsapp"),
|
|
z.literal("telegram"),
|
|
z.literal("discord"),
|
|
z.literal("slack"),
|
|
z.literal("signal"),
|
|
z.literal("imessage"),
|
|
z.literal("msteams"),
|
|
])
|
|
.optional(),
|
|
to: z.string().optional(),
|
|
model: z.string().optional(),
|
|
thinking: z.string().optional(),
|
|
timeoutSeconds: z.number().int().positive().optional(),
|
|
transform: z
|
|
.object({
|
|
module: z.string(),
|
|
export: z.string().optional(),
|
|
})
|
|
.optional(),
|
|
})
|
|
.optional();
|
|
|
|
const HooksGmailSchema = z
|
|
.object({
|
|
account: z.string().optional(),
|
|
label: z.string().optional(),
|
|
topic: z.string().optional(),
|
|
subscription: z.string().optional(),
|
|
pushToken: z.string().optional(),
|
|
hookUrl: z.string().optional(),
|
|
includeBody: z.boolean().optional(),
|
|
maxBytes: z.number().int().positive().optional(),
|
|
renewEveryMinutes: z.number().int().positive().optional(),
|
|
serve: z
|
|
.object({
|
|
bind: z.string().optional(),
|
|
port: z.number().int().positive().optional(),
|
|
path: z.string().optional(),
|
|
})
|
|
.optional(),
|
|
tailscale: z
|
|
.object({
|
|
mode: z
|
|
.union([z.literal("off"), z.literal("serve"), z.literal("funnel")])
|
|
.optional(),
|
|
path: z.string().optional(),
|
|
})
|
|
.optional(),
|
|
model: z.string().optional(),
|
|
thinking: z
|
|
.union([
|
|
z.literal("off"),
|
|
z.literal("minimal"),
|
|
z.literal("low"),
|
|
z.literal("medium"),
|
|
z.literal("high"),
|
|
])
|
|
.optional(),
|
|
})
|
|
.optional();
|
|
|
|
const AgentDefaultsSchema = z
|
|
.object({
|
|
model: z
|
|
.object({
|
|
primary: z.string().optional(),
|
|
fallbacks: z.array(z.string()).optional(),
|
|
})
|
|
.optional(),
|
|
imageModel: z
|
|
.object({
|
|
primary: z.string().optional(),
|
|
fallbacks: z.array(z.string()).optional(),
|
|
})
|
|
.optional(),
|
|
models: z
|
|
.record(
|
|
z.string(),
|
|
z.object({
|
|
alias: z.string().optional(),
|
|
/** Provider-specific API parameters (e.g., GLM-4.7 thinking mode). */
|
|
params: z.record(z.string(), z.unknown()).optional(),
|
|
}),
|
|
)
|
|
.optional(),
|
|
workspace: z.string().optional(),
|
|
skipBootstrap: z.boolean().optional(),
|
|
userTimezone: z.string().optional(),
|
|
contextTokens: z.number().int().positive().optional(),
|
|
contextPruning: z
|
|
.object({
|
|
mode: z
|
|
.union([
|
|
z.literal("off"),
|
|
z.literal("adaptive"),
|
|
z.literal("aggressive"),
|
|
])
|
|
.optional(),
|
|
keepLastAssistants: z.number().int().nonnegative().optional(),
|
|
softTrimRatio: z.number().min(0).max(1).optional(),
|
|
hardClearRatio: z.number().min(0).max(1).optional(),
|
|
minPrunableToolChars: z.number().int().nonnegative().optional(),
|
|
tools: z
|
|
.object({
|
|
allow: z.array(z.string()).optional(),
|
|
deny: z.array(z.string()).optional(),
|
|
})
|
|
.optional(),
|
|
softTrim: z
|
|
.object({
|
|
maxChars: z.number().int().nonnegative().optional(),
|
|
headChars: z.number().int().nonnegative().optional(),
|
|
tailChars: z.number().int().nonnegative().optional(),
|
|
})
|
|
.optional(),
|
|
hardClear: z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
placeholder: z.string().optional(),
|
|
})
|
|
.optional(),
|
|
})
|
|
.optional(),
|
|
thinkingDefault: z
|
|
.union([
|
|
z.literal("off"),
|
|
z.literal("minimal"),
|
|
z.literal("low"),
|
|
z.literal("medium"),
|
|
z.literal("high"),
|
|
])
|
|
.optional(),
|
|
verboseDefault: z.union([z.literal("off"), z.literal("on")]).optional(),
|
|
elevatedDefault: z.union([z.literal("off"), z.literal("on")]).optional(),
|
|
blockStreamingDefault: z
|
|
.union([z.literal("off"), z.literal("on")])
|
|
.optional(),
|
|
blockStreamingBreak: z
|
|
.union([z.literal("text_end"), z.literal("message_end")])
|
|
.optional(),
|
|
blockStreamingChunk: z
|
|
.object({
|
|
minChars: z.number().int().positive().optional(),
|
|
maxChars: z.number().int().positive().optional(),
|
|
breakPreference: z
|
|
.union([
|
|
z.literal("paragraph"),
|
|
z.literal("newline"),
|
|
z.literal("sentence"),
|
|
])
|
|
.optional(),
|
|
})
|
|
.optional(),
|
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
|
humanDelay: HumanDelaySchema.optional(),
|
|
timeoutSeconds: z.number().int().positive().optional(),
|
|
mediaMaxMb: z.number().positive().optional(),
|
|
typingIntervalSeconds: z.number().int().positive().optional(),
|
|
typingMode: z
|
|
.union([
|
|
z.literal("never"),
|
|
z.literal("instant"),
|
|
z.literal("thinking"),
|
|
z.literal("message"),
|
|
])
|
|
.optional(),
|
|
heartbeat: HeartbeatSchema,
|
|
maxConcurrent: z.number().int().positive().optional(),
|
|
subagents: z
|
|
.object({
|
|
maxConcurrent: z.number().int().positive().optional(),
|
|
archiveAfterMinutes: z.number().int().positive().optional(),
|
|
})
|
|
.optional(),
|
|
sandbox: z
|
|
.object({
|
|
mode: z
|
|
.union([z.literal("off"), z.literal("non-main"), z.literal("all")])
|
|
.optional(),
|
|
workspaceAccess: z
|
|
.union([z.literal("none"), z.literal("ro"), z.literal("rw")])
|
|
.optional(),
|
|
sessionToolsVisibility: z
|
|
.union([z.literal("spawned"), z.literal("all")])
|
|
.optional(),
|
|
scope: z
|
|
.union([
|
|
z.literal("session"),
|
|
z.literal("agent"),
|
|
z.literal("shared"),
|
|
])
|
|
.optional(),
|
|
perSession: z.boolean().optional(),
|
|
workspaceRoot: z.string().optional(),
|
|
docker: SandboxDockerSchema,
|
|
browser: SandboxBrowserSchema,
|
|
prune: SandboxPruneSchema,
|
|
})
|
|
.optional(),
|
|
})
|
|
.optional();
|
|
export const ClawdbotSchema = z
|
|
.object({
|
|
env: z
|
|
.object({
|
|
shellEnv: z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
timeoutMs: z.number().int().nonnegative().optional(),
|
|
})
|
|
.optional(),
|
|
vars: z.record(z.string(), z.string()).optional(),
|
|
})
|
|
.catchall(z.string())
|
|
.optional(),
|
|
wizard: z
|
|
.object({
|
|
lastRunAt: z.string().optional(),
|
|
lastRunVersion: z.string().optional(),
|
|
lastRunCommit: z.string().optional(),
|
|
lastRunCommand: z.string().optional(),
|
|
lastRunMode: z
|
|
.union([z.literal("local"), z.literal("remote")])
|
|
.optional(),
|
|
})
|
|
.optional(),
|
|
logging: z
|
|
.object({
|
|
level: z
|
|
.union([
|
|
z.literal("silent"),
|
|
z.literal("fatal"),
|
|
z.literal("error"),
|
|
z.literal("warn"),
|
|
z.literal("info"),
|
|
z.literal("debug"),
|
|
z.literal("trace"),
|
|
])
|
|
.optional(),
|
|
file: z.string().optional(),
|
|
consoleLevel: z
|
|
.union([
|
|
z.literal("silent"),
|
|
z.literal("fatal"),
|
|
z.literal("error"),
|
|
z.literal("warn"),
|
|
z.literal("info"),
|
|
z.literal("debug"),
|
|
z.literal("trace"),
|
|
])
|
|
.optional(),
|
|
consoleStyle: z
|
|
.union([z.literal("pretty"), z.literal("compact"), z.literal("json")])
|
|
.optional(),
|
|
redactSensitive: z
|
|
.union([z.literal("off"), z.literal("tools")])
|
|
.optional(),
|
|
redactPatterns: z.array(z.string()).optional(),
|
|
})
|
|
.optional(),
|
|
browser: z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
controlUrl: z.string().optional(),
|
|
cdpUrl: z.string().optional(),
|
|
color: z.string().optional(),
|
|
executablePath: z.string().optional(),
|
|
headless: z.boolean().optional(),
|
|
noSandbox: z.boolean().optional(),
|
|
attachOnly: z.boolean().optional(),
|
|
defaultProfile: z.string().optional(),
|
|
profiles: z
|
|
.record(
|
|
z
|
|
.string()
|
|
.regex(
|
|
/^[a-z0-9-]+$/,
|
|
"Profile names must be alphanumeric with hyphens only",
|
|
),
|
|
z
|
|
.object({
|
|
cdpPort: z.number().int().min(1).max(65535).optional(),
|
|
cdpUrl: z.string().optional(),
|
|
color: HexColorSchema,
|
|
})
|
|
.refine((value) => value.cdpPort || value.cdpUrl, {
|
|
message: "Profile must set cdpPort or cdpUrl",
|
|
}),
|
|
)
|
|
.optional(),
|
|
})
|
|
.optional(),
|
|
ui: z
|
|
.object({
|
|
seamColor: HexColorSchema.optional(),
|
|
})
|
|
.optional(),
|
|
auth: z
|
|
.object({
|
|
profiles: z
|
|
.record(
|
|
z.string(),
|
|
z.object({
|
|
provider: z.string(),
|
|
mode: z.union([
|
|
z.literal("api_key"),
|
|
z.literal("oauth"),
|
|
z.literal("token"),
|
|
]),
|
|
email: z.string().optional(),
|
|
}),
|
|
)
|
|
.optional(),
|
|
order: z.record(z.string(), z.array(z.string())).optional(),
|
|
cooldowns: z
|
|
.object({
|
|
billingBackoffHours: z.number().positive().optional(),
|
|
billingBackoffHoursByProvider: z
|
|
.record(z.string(), z.number().positive())
|
|
.optional(),
|
|
billingMaxHours: z.number().positive().optional(),
|
|
failureWindowHours: z.number().positive().optional(),
|
|
})
|
|
.optional(),
|
|
})
|
|
.optional(),
|
|
models: ModelsConfigSchema,
|
|
agents: AgentsSchema,
|
|
tools: ToolsSchema,
|
|
bindings: BindingsSchema,
|
|
broadcast: BroadcastSchema,
|
|
audio: AudioSchema,
|
|
messages: MessagesSchema,
|
|
commands: CommandsSchema,
|
|
session: SessionSchema,
|
|
cron: z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
store: z.string().optional(),
|
|
maxConcurrentRuns: z.number().int().positive().optional(),
|
|
})
|
|
.optional(),
|
|
hooks: z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
path: z.string().optional(),
|
|
token: z.string().optional(),
|
|
maxBodyBytes: z.number().int().positive().optional(),
|
|
presets: z.array(z.string()).optional(),
|
|
transformsDir: z.string().optional(),
|
|
mappings: z.array(HookMappingSchema).optional(),
|
|
gmail: HooksGmailSchema,
|
|
})
|
|
.optional(),
|
|
web: z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
heartbeatSeconds: z.number().int().positive().optional(),
|
|
reconnect: z
|
|
.object({
|
|
initialMs: z.number().positive().optional(),
|
|
maxMs: z.number().positive().optional(),
|
|
factor: z.number().positive().optional(),
|
|
jitter: z.number().min(0).max(1).optional(),
|
|
maxAttempts: z.number().int().min(0).optional(),
|
|
})
|
|
.optional(),
|
|
})
|
|
.optional(),
|
|
whatsapp: z
|
|
.object({
|
|
accounts: z
|
|
.record(
|
|
z.string(),
|
|
z
|
|
.object({
|
|
name: z.string().optional(),
|
|
capabilities: z.array(z.string()).optional(),
|
|
enabled: z.boolean().optional(),
|
|
messagePrefix: z.string().optional(),
|
|
/** Override auth directory for this WhatsApp account (Baileys multi-file auth state). */
|
|
authDir: z.string().optional(),
|
|
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
|
selfChatMode: z.boolean().optional(),
|
|
allowFrom: z.array(z.string()).optional(),
|
|
groupAllowFrom: z.array(z.string()).optional(),
|
|
groupPolicy: GroupPolicySchema.optional().default("open"),
|
|
textChunkLimit: z.number().int().positive().optional(),
|
|
mediaMaxMb: z.number().int().positive().optional(),
|
|
blockStreaming: z.boolean().optional(),
|
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
|
groups: z
|
|
.record(
|
|
z.string(),
|
|
z
|
|
.object({
|
|
requireMention: z.boolean().optional(),
|
|
})
|
|
.optional(),
|
|
)
|
|
.optional(),
|
|
})
|
|
.superRefine((value, ctx) => {
|
|
if (value.dmPolicy !== "open") return;
|
|
const allow = (value.allowFrom ?? [])
|
|
.map((v) => String(v).trim())
|
|
.filter(Boolean);
|
|
if (allow.includes("*")) return;
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ["allowFrom"],
|
|
message:
|
|
'whatsapp.accounts.*.dmPolicy="open" requires allowFrom to include "*"',
|
|
});
|
|
})
|
|
.optional(),
|
|
)
|
|
.optional(),
|
|
capabilities: z.array(z.string()).optional(),
|
|
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
|
messagePrefix: z.string().optional(),
|
|
selfChatMode: z.boolean().optional(),
|
|
allowFrom: z.array(z.string()).optional(),
|
|
groupAllowFrom: z.array(z.string()).optional(),
|
|
groupPolicy: GroupPolicySchema.optional().default("open"),
|
|
textChunkLimit: z.number().int().positive().optional(),
|
|
mediaMaxMb: z.number().int().positive().optional().default(50),
|
|
blockStreaming: z.boolean().optional(),
|
|
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
|
actions: z
|
|
.object({
|
|
reactions: z.boolean().optional(),
|
|
sendMessage: z.boolean().optional(),
|
|
polls: z.boolean().optional(),
|
|
})
|
|
.optional(),
|
|
groups: z
|
|
.record(
|
|
z.string(),
|
|
z
|
|
.object({
|
|
requireMention: z.boolean().optional(),
|
|
})
|
|
.optional(),
|
|
)
|
|
.optional(),
|
|
})
|
|
.superRefine((value, ctx) => {
|
|
if (value.dmPolicy !== "open") return;
|
|
const allow = (value.allowFrom ?? [])
|
|
.map((v) => String(v).trim())
|
|
.filter(Boolean);
|
|
if (allow.includes("*")) return;
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ["allowFrom"],
|
|
message:
|
|
'whatsapp.dmPolicy="open" requires whatsapp.allowFrom to include "*"',
|
|
});
|
|
})
|
|
.optional(),
|
|
telegram: TelegramConfigSchema.optional(),
|
|
discord: DiscordConfigSchema.optional(),
|
|
slack: SlackConfigSchema.optional(),
|
|
signal: SignalConfigSchema.optional(),
|
|
imessage: IMessageConfigSchema.optional(),
|
|
msteams: MSTeamsConfigSchema.optional(),
|
|
bridge: z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
port: z.number().int().positive().optional(),
|
|
bind: z
|
|
.union([
|
|
z.literal("auto"),
|
|
z.literal("lan"),
|
|
z.literal("tailnet"),
|
|
z.literal("loopback"),
|
|
])
|
|
.optional(),
|
|
})
|
|
.optional(),
|
|
discovery: z
|
|
.object({
|
|
wideArea: z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
})
|
|
.optional(),
|
|
})
|
|
.optional(),
|
|
canvasHost: z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
root: z.string().optional(),
|
|
port: z.number().int().positive().optional(),
|
|
liveReload: z.boolean().optional(),
|
|
})
|
|
.optional(),
|
|
talk: z
|
|
.object({
|
|
voiceId: z.string().optional(),
|
|
voiceAliases: z.record(z.string(), z.string()).optional(),
|
|
modelId: z.string().optional(),
|
|
outputFormat: z.string().optional(),
|
|
apiKey: z.string().optional(),
|
|
interruptOnSpeech: z.boolean().optional(),
|
|
})
|
|
.optional(),
|
|
gateway: z
|
|
.object({
|
|
port: z.number().int().positive().optional(),
|
|
mode: z.union([z.literal("local"), z.literal("remote")]).optional(),
|
|
bind: z
|
|
.union([
|
|
z.literal("auto"),
|
|
z.literal("lan"),
|
|
z.literal("tailnet"),
|
|
z.literal("loopback"),
|
|
])
|
|
.optional(),
|
|
controlUi: z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
basePath: z.string().optional(),
|
|
})
|
|
.optional(),
|
|
auth: z
|
|
.object({
|
|
mode: z
|
|
.union([z.literal("token"), z.literal("password")])
|
|
.optional(),
|
|
token: z.string().optional(),
|
|
password: z.string().optional(),
|
|
allowTailscale: z.boolean().optional(),
|
|
})
|
|
.optional(),
|
|
tailscale: z
|
|
.object({
|
|
mode: z
|
|
.union([
|
|
z.literal("off"),
|
|
z.literal("serve"),
|
|
z.literal("funnel"),
|
|
])
|
|
.optional(),
|
|
resetOnExit: z.boolean().optional(),
|
|
})
|
|
.optional(),
|
|
remote: z
|
|
.object({
|
|
url: z.string().optional(),
|
|
token: z.string().optional(),
|
|
password: z.string().optional(),
|
|
sshTarget: z.string().optional(),
|
|
sshIdentity: z.string().optional(),
|
|
})
|
|
.optional(),
|
|
reload: z
|
|
.object({
|
|
mode: z
|
|
.union([
|
|
z.literal("off"),
|
|
z.literal("restart"),
|
|
z.literal("hot"),
|
|
z.literal("hybrid"),
|
|
])
|
|
.optional(),
|
|
debounceMs: z.number().int().min(0).optional(),
|
|
})
|
|
.optional(),
|
|
})
|
|
.optional(),
|
|
skills: z
|
|
.object({
|
|
allowBundled: z.array(z.string()).optional(),
|
|
load: z
|
|
.object({
|
|
extraDirs: z.array(z.string()).optional(),
|
|
})
|
|
.optional(),
|
|
install: z
|
|
.object({
|
|
preferBrew: z.boolean().optional(),
|
|
nodeManager: z
|
|
.union([
|
|
z.literal("npm"),
|
|
z.literal("pnpm"),
|
|
z.literal("yarn"),
|
|
z.literal("bun"),
|
|
])
|
|
.optional(),
|
|
})
|
|
.optional(),
|
|
entries: z
|
|
.record(
|
|
z.string(),
|
|
z
|
|
.object({
|
|
enabled: z.boolean().optional(),
|
|
apiKey: z.string().optional(),
|
|
env: z.record(z.string(), z.string()).optional(),
|
|
})
|
|
.passthrough(),
|
|
)
|
|
.optional(),
|
|
})
|
|
.optional(),
|
|
})
|
|
.superRefine((cfg, ctx) => {
|
|
const agents = cfg.agents?.list ?? [];
|
|
if (agents.length === 0) return;
|
|
const agentIds = new Set(agents.map((agent) => agent.id));
|
|
|
|
const broadcast = cfg.broadcast;
|
|
if (!broadcast) return;
|
|
|
|
for (const [peerId, ids] of Object.entries(broadcast)) {
|
|
if (peerId === "strategy") continue;
|
|
if (!Array.isArray(ids)) continue;
|
|
for (let idx = 0; idx < ids.length; idx += 1) {
|
|
const agentId = ids[idx];
|
|
if (!agentIds.has(agentId)) {
|
|
ctx.addIssue({
|
|
code: z.ZodIssueCode.custom,
|
|
path: ["broadcast", peerId, idx],
|
|
message: `Unknown agent id "${agentId}" (not in agents.list).`,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|