mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 01:57:28 +00:00
fix: enforce strict config validation
This commit is contained in:
@@ -4,6 +4,9 @@ Docs: https://docs.clawd.bot
|
|||||||
|
|
||||||
## 2026.1.19-1
|
## 2026.1.19-1
|
||||||
|
|
||||||
|
### Breaking
|
||||||
|
- **BREAKING:** Reject invalid/unknown config entries and refuse to start the gateway for safety; run `clawdbot doctor --fix` to repair.
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
- Usage: add `/usage cost` summaries and macOS menu cost submenu with daily charting.
|
- Usage: add `/usage cost` summaries and macOS menu cost submenu with daily charting.
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,19 @@ If the file is missing, Clawdbot uses safe-ish defaults (embedded Pi agent + per
|
|||||||
|
|
||||||
> **New to configuration?** Check out the [Configuration Examples](/gateway/configuration-examples) guide for complete examples with detailed explanations!
|
> **New to configuration?** Check out the [Configuration Examples](/gateway/configuration-examples) guide for complete examples with detailed explanations!
|
||||||
|
|
||||||
|
## Strict config validation
|
||||||
|
|
||||||
|
Clawdbot only accepts configurations that fully match the schema.
|
||||||
|
Unknown keys, malformed types, or invalid values cause the Gateway to **refuse to start** for safety.
|
||||||
|
|
||||||
|
When validation fails:
|
||||||
|
- The Gateway does not boot.
|
||||||
|
- Only diagnostic commands are allowed (for example: `clawdbot doctor`, `clawdbot logs`, `clawdbot health`, `clawdbot status`, `clawdbot service`, `clawdbot help`).
|
||||||
|
- Run `clawdbot doctor` to see the exact issues.
|
||||||
|
- Run `clawdbot doctor --fix` (or `--yes`) to apply migrations/repairs.
|
||||||
|
|
||||||
|
Doctor never writes changes unless you explicitly opt into `--fix`/`--yes`.
|
||||||
|
|
||||||
## Schema + UI hints
|
## Schema + UI hints
|
||||||
|
|
||||||
The Gateway exposes a JSON Schema representation of the config via `config.schema` for UI editors.
|
The Gateway exposes a JSON Schema representation of the config via `config.schema` for UI editors.
|
||||||
|
|||||||
@@ -326,6 +326,22 @@ Clawdbot keeps conversation history in memory.
|
|||||||
|
|
||||||
## Common troubleshooting
|
## Common troubleshooting
|
||||||
|
|
||||||
|
### “Gateway won’t start — configuration invalid”
|
||||||
|
|
||||||
|
Clawdbot now refuses to start when the config contains unknown keys, malformed values, or invalid types.
|
||||||
|
This is intentional for safety.
|
||||||
|
|
||||||
|
Fix it with Doctor:
|
||||||
|
```bash
|
||||||
|
clawdbot doctor
|
||||||
|
clawdbot doctor --fix
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- `clawdbot doctor` reports every invalid entry.
|
||||||
|
- `clawdbot doctor --fix` applies migrations/repairs and rewrites the config.
|
||||||
|
- Diagnostic commands like `clawdbot logs`, `clawdbot health`, `clawdbot status`, and `clawdbot service` still run even if the config is invalid.
|
||||||
|
|
||||||
### “All models failed” — what should I check first?
|
### “All models failed” — what should I check first?
|
||||||
|
|
||||||
- **Credentials** present for the provider(s) being tried (auth profiles + env vars).
|
- **Credentials** present for the provider(s) being tried (auth profiles + env vars).
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
||||||
|
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import { bluebubblesPlugin } from "./src/channel.js";
|
import { bluebubblesPlugin } from "./src/channel.js";
|
||||||
import { handleBlueBubblesWebhookRequest } from "./src/monitor.js";
|
import { handleBlueBubblesWebhookRequest } from "./src/monitor.js";
|
||||||
@@ -8,6 +9,7 @@ const plugin = {
|
|||||||
id: "bluebubbles",
|
id: "bluebubbles",
|
||||||
name: "BlueBubbles",
|
name: "BlueBubbles",
|
||||||
description: "BlueBubbles channel plugin (macOS app)",
|
description: "BlueBubbles channel plugin (macOS app)",
|
||||||
|
configSchema: emptyPluginConfigSchema(),
|
||||||
register(api: ClawdbotPluginApi) {
|
register(api: ClawdbotPluginApi) {
|
||||||
setBlueBubblesRuntime(api.runtime);
|
setBlueBubblesRuntime(api.runtime);
|
||||||
api.registerChannel({ plugin: bluebubblesPlugin });
|
api.registerChannel({ plugin: bluebubblesPlugin });
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
const DEFAULT_BASE_URL = "http://localhost:3000/v1";
|
const DEFAULT_BASE_URL = "http://localhost:3000/v1";
|
||||||
const DEFAULT_API_KEY = "n/a";
|
const DEFAULT_API_KEY = "n/a";
|
||||||
const DEFAULT_CONTEXT_WINDOW = 128_000;
|
const DEFAULT_CONTEXT_WINDOW = 128_000;
|
||||||
@@ -61,6 +63,7 @@ const copilotProxyPlugin = {
|
|||||||
id: "copilot-proxy",
|
id: "copilot-proxy",
|
||||||
name: "Copilot Proxy",
|
name: "Copilot Proxy",
|
||||||
description: "Local Copilot Proxy (VS Code LM) provider plugin",
|
description: "Local Copilot Proxy (VS Code LM) provider plugin",
|
||||||
|
configSchema: emptyPluginConfigSchema(),
|
||||||
register(api) {
|
register(api) {
|
||||||
api.registerProvider({
|
api.registerProvider({
|
||||||
id: "copilot-proxy",
|
id: "copilot-proxy",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
||||||
|
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import { discordPlugin } from "./src/channel.js";
|
import { discordPlugin } from "./src/channel.js";
|
||||||
import { setDiscordRuntime } from "./src/runtime.js";
|
import { setDiscordRuntime } from "./src/runtime.js";
|
||||||
@@ -7,6 +8,7 @@ const plugin = {
|
|||||||
id: "discord",
|
id: "discord",
|
||||||
name: "Discord",
|
name: "Discord",
|
||||||
description: "Discord channel plugin",
|
description: "Discord channel plugin",
|
||||||
|
configSchema: emptyPluginConfigSchema(),
|
||||||
register(api: ClawdbotPluginApi) {
|
register(api: ClawdbotPluginApi) {
|
||||||
setDiscordRuntime(api.runtime);
|
setDiscordRuntime(api.runtime);
|
||||||
api.registerChannel({ plugin: discordPlugin });
|
api.registerChannel({ plugin: discordPlugin });
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createHash, randomBytes } from "node:crypto";
|
import { createHash, randomBytes } from "node:crypto";
|
||||||
import { readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { createServer } from "node:http";
|
import { createServer } from "node:http";
|
||||||
|
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
// OAuth constants - decoded from pi-ai's base64 encoded values to stay in sync
|
// OAuth constants - decoded from pi-ai's base64 encoded values to stay in sync
|
||||||
const decode = (s: string) => Buffer.from(s, "base64").toString();
|
const decode = (s: string) => Buffer.from(s, "base64").toString();
|
||||||
@@ -360,6 +361,7 @@ const antigravityPlugin = {
|
|||||||
id: "google-antigravity-auth",
|
id: "google-antigravity-auth",
|
||||||
name: "Google Antigravity Auth",
|
name: "Google Antigravity Auth",
|
||||||
description: "OAuth flow for Google Antigravity (Cloud Code Assist)",
|
description: "OAuth flow for Google Antigravity (Cloud Code Assist)",
|
||||||
|
configSchema: emptyPluginConfigSchema(),
|
||||||
register(api) {
|
register(api) {
|
||||||
api.registerProvider({
|
api.registerProvider({
|
||||||
id: "google-antigravity",
|
id: "google-antigravity",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import { loginGeminiCliOAuth } from "./oauth.js";
|
import { loginGeminiCliOAuth } from "./oauth.js";
|
||||||
|
|
||||||
const PROVIDER_ID = "google-gemini-cli";
|
const PROVIDER_ID = "google-gemini-cli";
|
||||||
@@ -14,6 +16,7 @@ const geminiCliPlugin = {
|
|||||||
id: "google-gemini-cli-auth",
|
id: "google-gemini-cli-auth",
|
||||||
name: "Google Gemini CLI Auth",
|
name: "Google Gemini CLI Auth",
|
||||||
description: "OAuth flow for Gemini CLI (Google Code Assist)",
|
description: "OAuth flow for Gemini CLI (Google Code Assist)",
|
||||||
|
configSchema: emptyPluginConfigSchema(),
|
||||||
register(api) {
|
register(api) {
|
||||||
api.registerProvider({
|
api.registerProvider({
|
||||||
id: PROVIDER_ID,
|
id: PROVIDER_ID,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
||||||
|
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import { imessagePlugin } from "./src/channel.js";
|
import { imessagePlugin } from "./src/channel.js";
|
||||||
import { setIMessageRuntime } from "./src/runtime.js";
|
import { setIMessageRuntime } from "./src/runtime.js";
|
||||||
@@ -7,6 +8,7 @@ const plugin = {
|
|||||||
id: "imessage",
|
id: "imessage",
|
||||||
name: "iMessage",
|
name: "iMessage",
|
||||||
description: "iMessage channel plugin",
|
description: "iMessage channel plugin",
|
||||||
|
configSchema: emptyPluginConfigSchema(),
|
||||||
register(api: ClawdbotPluginApi) {
|
register(api: ClawdbotPluginApi) {
|
||||||
setIMessageRuntime(api.runtime);
|
setIMessageRuntime(api.runtime);
|
||||||
api.registerChannel({ plugin: imessagePlugin });
|
api.registerChannel({ plugin: imessagePlugin });
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
||||||
|
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import { matrixPlugin } from "./src/channel.js";
|
import { matrixPlugin } from "./src/channel.js";
|
||||||
import { setMatrixRuntime } from "./src/runtime.js";
|
import { setMatrixRuntime } from "./src/runtime.js";
|
||||||
@@ -7,6 +8,7 @@ const plugin = {
|
|||||||
id: "matrix",
|
id: "matrix",
|
||||||
name: "Matrix",
|
name: "Matrix",
|
||||||
description: "Matrix channel plugin (matrix-js-sdk)",
|
description: "Matrix channel plugin (matrix-js-sdk)",
|
||||||
|
configSchema: emptyPluginConfigSchema(),
|
||||||
register(api: ClawdbotPluginApi) {
|
register(api: ClawdbotPluginApi) {
|
||||||
setMatrixRuntime(api.runtime);
|
setMatrixRuntime(api.runtime);
|
||||||
api.registerChannel({ plugin: matrixPlugin });
|
api.registerChannel({ plugin: matrixPlugin });
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
||||||
|
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
const memoryCorePlugin = {
|
const memoryCorePlugin = {
|
||||||
id: "memory-core",
|
id: "memory-core",
|
||||||
name: "Memory (Core)",
|
name: "Memory (Core)",
|
||||||
description: "File-backed memory search tools and CLI",
|
description: "File-backed memory search tools and CLI",
|
||||||
kind: "memory",
|
kind: "memory",
|
||||||
|
configSchema: emptyPluginConfigSchema(),
|
||||||
register(api: ClawdbotPluginApi) {
|
register(api: ClawdbotPluginApi) {
|
||||||
api.registerTool(
|
api.registerTool(
|
||||||
(ctx) => {
|
(ctx) => {
|
||||||
|
|||||||
@@ -24,6 +24,16 @@ const EMBEDDING_DIMENSIONS: Record<string, number> = {
|
|||||||
"text-embedding-3-large": 3072,
|
"text-embedding-3-large": 3072,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function assertAllowedKeys(
|
||||||
|
value: Record<string, unknown>,
|
||||||
|
allowed: string[],
|
||||||
|
label: string,
|
||||||
|
) {
|
||||||
|
const unknown = Object.keys(value).filter((key) => !allowed.includes(key));
|
||||||
|
if (unknown.length === 0) return;
|
||||||
|
throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
export function vectorDimsForModel(model: string): number {
|
export function vectorDimsForModel(model: string): number {
|
||||||
const dims = EMBEDDING_DIMENSIONS[model];
|
const dims = EMBEDDING_DIMENSIONS[model];
|
||||||
if (!dims) {
|
if (!dims) {
|
||||||
@@ -54,11 +64,13 @@ export const memoryConfigSchema = {
|
|||||||
throw new Error("memory config required");
|
throw new Error("memory config required");
|
||||||
}
|
}
|
||||||
const cfg = value as Record<string, unknown>;
|
const cfg = value as Record<string, unknown>;
|
||||||
|
assertAllowedKeys(cfg, ["embedding", "dbPath", "autoCapture", "autoRecall"], "memory config");
|
||||||
|
|
||||||
const embedding = cfg.embedding as Record<string, unknown> | undefined;
|
const embedding = cfg.embedding as Record<string, unknown> | undefined;
|
||||||
if (!embedding || typeof embedding.apiKey !== "string") {
|
if (!embedding || typeof embedding.apiKey !== "string") {
|
||||||
throw new Error("embedding.apiKey is required");
|
throw new Error("embedding.apiKey is required");
|
||||||
}
|
}
|
||||||
|
assertAllowedKeys(embedding, ["apiKey", "model"], "embedding config");
|
||||||
|
|
||||||
const model = resolveEmbeddingModel(embedding);
|
const model = resolveEmbeddingModel(embedding);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
||||||
|
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import { msteamsPlugin } from "./src/channel.js";
|
import { msteamsPlugin } from "./src/channel.js";
|
||||||
import { setMSTeamsRuntime } from "./src/runtime.js";
|
import { setMSTeamsRuntime } from "./src/runtime.js";
|
||||||
@@ -7,6 +8,7 @@ const plugin = {
|
|||||||
id: "msteams",
|
id: "msteams",
|
||||||
name: "Microsoft Teams",
|
name: "Microsoft Teams",
|
||||||
description: "Microsoft Teams channel plugin (Bot Framework)",
|
description: "Microsoft Teams channel plugin (Bot Framework)",
|
||||||
|
configSchema: emptyPluginConfigSchema(),
|
||||||
register(api: ClawdbotPluginApi) {
|
register(api: ClawdbotPluginApi) {
|
||||||
setMSTeamsRuntime(api.runtime);
|
setMSTeamsRuntime(api.runtime);
|
||||||
api.registerChannel({ plugin: msteamsPlugin });
|
api.registerChannel({ plugin: msteamsPlugin });
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import { loginQwenPortalOAuth } from "./oauth.js";
|
import { loginQwenPortalOAuth } from "./oauth.js";
|
||||||
|
|
||||||
const PROVIDER_ID = "qwen-portal";
|
const PROVIDER_ID = "qwen-portal";
|
||||||
@@ -30,6 +32,7 @@ const qwenPortalPlugin = {
|
|||||||
id: "qwen-portal-auth",
|
id: "qwen-portal-auth",
|
||||||
name: "Qwen OAuth",
|
name: "Qwen OAuth",
|
||||||
description: "OAuth flow for Qwen (free-tier) models",
|
description: "OAuth flow for Qwen (free-tier) models",
|
||||||
|
configSchema: emptyPluginConfigSchema(),
|
||||||
register(api) {
|
register(api) {
|
||||||
api.registerProvider({
|
api.registerProvider({
|
||||||
id: PROVIDER_ID,
|
id: PROVIDER_ID,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
||||||
|
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import { signalPlugin } from "./src/channel.js";
|
import { signalPlugin } from "./src/channel.js";
|
||||||
import { setSignalRuntime } from "./src/runtime.js";
|
import { setSignalRuntime } from "./src/runtime.js";
|
||||||
@@ -7,6 +8,7 @@ const plugin = {
|
|||||||
id: "signal",
|
id: "signal",
|
||||||
name: "Signal",
|
name: "Signal",
|
||||||
description: "Signal channel plugin",
|
description: "Signal channel plugin",
|
||||||
|
configSchema: emptyPluginConfigSchema(),
|
||||||
register(api: ClawdbotPluginApi) {
|
register(api: ClawdbotPluginApi) {
|
||||||
setSignalRuntime(api.runtime);
|
setSignalRuntime(api.runtime);
|
||||||
api.registerChannel({ plugin: signalPlugin });
|
api.registerChannel({ plugin: signalPlugin });
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
||||||
|
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import { slackPlugin } from "./src/channel.js";
|
import { slackPlugin } from "./src/channel.js";
|
||||||
import { setSlackRuntime } from "./src/runtime.js";
|
import { setSlackRuntime } from "./src/runtime.js";
|
||||||
@@ -7,6 +8,7 @@ const plugin = {
|
|||||||
id: "slack",
|
id: "slack",
|
||||||
name: "Slack",
|
name: "Slack",
|
||||||
description: "Slack channel plugin",
|
description: "Slack channel plugin",
|
||||||
|
configSchema: emptyPluginConfigSchema(),
|
||||||
register(api: ClawdbotPluginApi) {
|
register(api: ClawdbotPluginApi) {
|
||||||
setSlackRuntime(api.runtime);
|
setSlackRuntime(api.runtime);
|
||||||
api.registerChannel({ plugin: slackPlugin });
|
api.registerChannel({ plugin: slackPlugin });
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
||||||
|
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import { telegramPlugin } from "./src/channel.js";
|
import { telegramPlugin } from "./src/channel.js";
|
||||||
import { setTelegramRuntime } from "./src/runtime.js";
|
import { setTelegramRuntime } from "./src/runtime.js";
|
||||||
@@ -7,6 +8,7 @@ const plugin = {
|
|||||||
id: "telegram",
|
id: "telegram",
|
||||||
name: "Telegram",
|
name: "Telegram",
|
||||||
description: "Telegram channel plugin",
|
description: "Telegram channel plugin",
|
||||||
|
configSchema: emptyPluginConfigSchema(),
|
||||||
register(api: ClawdbotPluginApi) {
|
register(api: ClawdbotPluginApi) {
|
||||||
setTelegramRuntime(api.runtime);
|
setTelegramRuntime(api.runtime);
|
||||||
api.registerChannel({ plugin: telegramPlugin });
|
api.registerChannel({ plugin: telegramPlugin });
|
||||||
|
|||||||
@@ -35,30 +35,36 @@ export type InboundPolicy = z.infer<typeof InboundPolicySchema>;
|
|||||||
// Provider-Specific Configuration
|
// Provider-Specific Configuration
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
export const TelnyxConfigSchema = z.object({
|
export const TelnyxConfigSchema = z
|
||||||
|
.object({
|
||||||
/** Telnyx API v2 key */
|
/** Telnyx API v2 key */
|
||||||
apiKey: z.string().min(1).optional(),
|
apiKey: z.string().min(1).optional(),
|
||||||
/** Telnyx connection ID (from Call Control app) */
|
/** Telnyx connection ID (from Call Control app) */
|
||||||
connectionId: z.string().min(1).optional(),
|
connectionId: z.string().min(1).optional(),
|
||||||
/** Public key for webhook signature verification */
|
/** Public key for webhook signature verification */
|
||||||
publicKey: z.string().min(1).optional(),
|
publicKey: z.string().min(1).optional(),
|
||||||
});
|
})
|
||||||
|
.strict();
|
||||||
export type TelnyxConfig = z.infer<typeof TelnyxConfigSchema>;
|
export type TelnyxConfig = z.infer<typeof TelnyxConfigSchema>;
|
||||||
|
|
||||||
export const TwilioConfigSchema = z.object({
|
export const TwilioConfigSchema = z
|
||||||
|
.object({
|
||||||
/** Twilio Account SID */
|
/** Twilio Account SID */
|
||||||
accountSid: z.string().min(1).optional(),
|
accountSid: z.string().min(1).optional(),
|
||||||
/** Twilio Auth Token */
|
/** Twilio Auth Token */
|
||||||
authToken: z.string().min(1).optional(),
|
authToken: z.string().min(1).optional(),
|
||||||
});
|
})
|
||||||
|
.strict();
|
||||||
export type TwilioConfig = z.infer<typeof TwilioConfigSchema>;
|
export type TwilioConfig = z.infer<typeof TwilioConfigSchema>;
|
||||||
|
|
||||||
export const PlivoConfigSchema = z.object({
|
export const PlivoConfigSchema = z
|
||||||
|
.object({
|
||||||
/** Plivo Auth ID (starts with MA/SA) */
|
/** Plivo Auth ID (starts with MA/SA) */
|
||||||
authId: z.string().min(1).optional(),
|
authId: z.string().min(1).optional(),
|
||||||
/** Plivo Auth Token */
|
/** Plivo Auth Token */
|
||||||
authToken: z.string().min(1).optional(),
|
authToken: z.string().min(1).optional(),
|
||||||
});
|
})
|
||||||
|
.strict();
|
||||||
export type PlivoConfig = z.infer<typeof PlivoConfigSchema>;
|
export type PlivoConfig = z.infer<typeof PlivoConfigSchema>;
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -72,6 +78,7 @@ export const SttConfigSchema = z
|
|||||||
/** Whisper model to use */
|
/** Whisper model to use */
|
||||||
model: z.string().min(1).default("whisper-1"),
|
model: z.string().min(1).default("whisper-1"),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.default({ provider: "openai", model: "whisper-1" });
|
.default({ provider: "openai", model: "whisper-1" });
|
||||||
export type SttConfig = z.infer<typeof SttConfigSchema>;
|
export type SttConfig = z.infer<typeof SttConfigSchema>;
|
||||||
|
|
||||||
@@ -97,6 +104,7 @@ export const TtsConfigSchema = z
|
|||||||
*/
|
*/
|
||||||
instructions: z.string().optional(),
|
instructions: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.default({ provider: "openai", model: "gpt-4o-mini-tts", voice: "coral" });
|
.default({ provider: "openai", model: "gpt-4o-mini-tts", voice: "coral" });
|
||||||
export type TtsConfig = z.infer<typeof TtsConfigSchema>;
|
export type TtsConfig = z.infer<typeof TtsConfigSchema>;
|
||||||
|
|
||||||
@@ -113,6 +121,7 @@ export const VoiceCallServeConfigSchema = z
|
|||||||
/** Webhook path */
|
/** Webhook path */
|
||||||
path: z.string().min(1).default("/voice/webhook"),
|
path: z.string().min(1).default("/voice/webhook"),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.default({ port: 3334, bind: "127.0.0.1", path: "/voice/webhook" });
|
.default({ port: 3334, bind: "127.0.0.1", path: "/voice/webhook" });
|
||||||
export type VoiceCallServeConfig = z.infer<typeof VoiceCallServeConfigSchema>;
|
export type VoiceCallServeConfig = z.infer<typeof VoiceCallServeConfigSchema>;
|
||||||
|
|
||||||
@@ -128,6 +137,7 @@ export const VoiceCallTailscaleConfigSchema = z
|
|||||||
/** Path for Tailscale serve/funnel (should usually match serve.path) */
|
/** Path for Tailscale serve/funnel (should usually match serve.path) */
|
||||||
path: z.string().min(1).default("/voice/webhook"),
|
path: z.string().min(1).default("/voice/webhook"),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.default({ mode: "off", path: "/voice/webhook" });
|
.default({ mode: "off", path: "/voice/webhook" });
|
||||||
export type VoiceCallTailscaleConfig = z.infer<
|
export type VoiceCallTailscaleConfig = z.infer<
|
||||||
typeof VoiceCallTailscaleConfigSchema
|
typeof VoiceCallTailscaleConfigSchema
|
||||||
@@ -161,6 +171,7 @@ export const VoiceCallTunnelConfigSchema = z
|
|||||||
*/
|
*/
|
||||||
allowNgrokFreeTier: z.boolean().default(true),
|
allowNgrokFreeTier: z.boolean().default(true),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.default({ provider: "none", allowNgrokFreeTier: true });
|
.default({ provider: "none", allowNgrokFreeTier: true });
|
||||||
export type VoiceCallTunnelConfig = z.infer<typeof VoiceCallTunnelConfigSchema>;
|
export type VoiceCallTunnelConfig = z.infer<typeof VoiceCallTunnelConfigSchema>;
|
||||||
|
|
||||||
@@ -183,6 +194,7 @@ export const OutboundConfigSchema = z
|
|||||||
/** Seconds to wait after TTS before auto-hangup in notify mode */
|
/** Seconds to wait after TTS before auto-hangup in notify mode */
|
||||||
notifyHangupDelaySec: z.number().int().nonnegative().default(3),
|
notifyHangupDelaySec: z.number().int().nonnegative().default(3),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.default({ defaultMode: "notify", notifyHangupDelaySec: 3 });
|
.default({ defaultMode: "notify", notifyHangupDelaySec: 3 });
|
||||||
export type OutboundConfig = z.infer<typeof OutboundConfigSchema>;
|
export type OutboundConfig = z.infer<typeof OutboundConfigSchema>;
|
||||||
|
|
||||||
@@ -207,6 +219,7 @@ export const VoiceCallStreamingConfigSchema = z
|
|||||||
/** WebSocket path for media stream connections */
|
/** WebSocket path for media stream connections */
|
||||||
streamPath: z.string().min(1).default("/voice/stream"),
|
streamPath: z.string().min(1).default("/voice/stream"),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.default({
|
.default({
|
||||||
enabled: false,
|
enabled: false,
|
||||||
sttProvider: "openai-realtime",
|
sttProvider: "openai-realtime",
|
||||||
@@ -223,7 +236,8 @@ export type VoiceCallStreamingConfig = z.infer<
|
|||||||
// Main Voice Call Configuration
|
// Main Voice Call Configuration
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
export const VoiceCallConfigSchema = z.object({
|
export const VoiceCallConfigSchema = z
|
||||||
|
.object({
|
||||||
/** Enable voice call functionality */
|
/** Enable voice call functionality */
|
||||||
enabled: z.boolean().default(false),
|
enabled: z.boolean().default(false),
|
||||||
|
|
||||||
@@ -307,7 +321,8 @@ export const VoiceCallConfigSchema = z.object({
|
|||||||
|
|
||||||
/** Timeout for response generation in ms (default 30s) */
|
/** Timeout for response generation in ms (default 30s) */
|
||||||
responseTimeoutMs: z.number().int().positive().default(30000),
|
responseTimeoutMs: z.number().int().positive().default(30000),
|
||||||
});
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export type VoiceCallConfig = z.infer<typeof VoiceCallConfigSchema>;
|
export type VoiceCallConfig = z.infer<typeof VoiceCallConfigSchema>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
||||||
|
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import { whatsappPlugin } from "./src/channel.js";
|
import { whatsappPlugin } from "./src/channel.js";
|
||||||
import { setWhatsAppRuntime } from "./src/runtime.js";
|
import { setWhatsAppRuntime } from "./src/runtime.js";
|
||||||
@@ -7,6 +8,7 @@ const plugin = {
|
|||||||
id: "whatsapp",
|
id: "whatsapp",
|
||||||
name: "WhatsApp",
|
name: "WhatsApp",
|
||||||
description: "WhatsApp channel plugin",
|
description: "WhatsApp channel plugin",
|
||||||
|
configSchema: emptyPluginConfigSchema(),
|
||||||
register(api: ClawdbotPluginApi) {
|
register(api: ClawdbotPluginApi) {
|
||||||
setWhatsAppRuntime(api.runtime);
|
setWhatsAppRuntime(api.runtime);
|
||||||
api.registerChannel({ plugin: whatsappPlugin });
|
api.registerChannel({ plugin: whatsappPlugin });
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
||||||
|
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import { zaloDock, zaloPlugin } from "./src/channel.js";
|
import { zaloDock, zaloPlugin } from "./src/channel.js";
|
||||||
import { handleZaloWebhookRequest } from "./src/monitor.js";
|
import { handleZaloWebhookRequest } from "./src/monitor.js";
|
||||||
@@ -8,6 +9,7 @@ const plugin = {
|
|||||||
id: "zalo",
|
id: "zalo",
|
||||||
name: "Zalo",
|
name: "Zalo",
|
||||||
description: "Zalo channel plugin (Bot API)",
|
description: "Zalo channel plugin (Bot API)",
|
||||||
|
configSchema: emptyPluginConfigSchema(),
|
||||||
register(api: ClawdbotPluginApi) {
|
register(api: ClawdbotPluginApi) {
|
||||||
setZaloRuntime(api.runtime);
|
setZaloRuntime(api.runtime);
|
||||||
api.registerChannel({ plugin: zaloPlugin, dock: zaloDock });
|
api.registerChannel({ plugin: zaloPlugin, dock: zaloDock });
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
|
||||||
|
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
|
||||||
|
|
||||||
import { zalouserPlugin } from "./src/channel.js";
|
import { zalouserPlugin } from "./src/channel.js";
|
||||||
import { ZalouserToolSchema, executeZalouserTool } from "./src/tool.js";
|
import { ZalouserToolSchema, executeZalouserTool } from "./src/tool.js";
|
||||||
@@ -8,6 +9,7 @@ const plugin = {
|
|||||||
id: "zalouser",
|
id: "zalouser",
|
||||||
name: "Zalo Personal",
|
name: "Zalo Personal",
|
||||||
description: "Zalo personal account messaging via zca-cli",
|
description: "Zalo personal account messaging via zca-cli",
|
||||||
|
configSchema: emptyPluginConfigSchema(),
|
||||||
register(api: ClawdbotPluginApi) {
|
register(api: ClawdbotPluginApi) {
|
||||||
setZalouserRuntime(api.runtime);
|
setZalouserRuntime(api.runtime);
|
||||||
// Register channel plugin (for onboarding & gateway)
|
// Register channel plugin (for onboarding & gateway)
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ type SandboxHashInput = {
|
|||||||
function isPrimitive(value: unknown): value is string | number | boolean | bigint | symbol | null {
|
function isPrimitive(value: unknown): value is string | number | boolean | bigint | symbol | null {
|
||||||
return value === null || (typeof value !== "object" && typeof value !== "function");
|
return value === null || (typeof value !== "object" && typeof value !== "function");
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeForHash(value: unknown): unknown {
|
function normalizeForHash(value: unknown): unknown {
|
||||||
if (value === undefined) return undefined;
|
if (value === undefined) return undefined;
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ vi.mock("../tui/tui.js", () => ({ runTui }));
|
|||||||
vi.mock("../gateway/call.js", () => ({
|
vi.mock("../gateway/call.js", () => ({
|
||||||
callGateway,
|
callGateway,
|
||||||
randomIdempotencyKey: () => "idem-test",
|
randomIdempotencyKey: () => "idem-test",
|
||||||
|
buildGatewayConnectionDetails: () => ({
|
||||||
|
url: "ws://127.0.0.1:1234",
|
||||||
|
urlSource: "test",
|
||||||
|
message: "Gateway target: ws://127.0.0.1:1234",
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
vi.mock("./deps.js", () => ({ createDefaultDeps: () => ({}) }));
|
vi.mock("./deps.js", () => ({ createDefaultDeps: () => ({}) }));
|
||||||
|
|
||||||
@@ -127,26 +132,32 @@ describe("cli program (nodes basics)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("runs nodes describe and calls node.describe", async () => {
|
it("runs nodes describe and calls node.describe", async () => {
|
||||||
callGateway
|
callGateway.mockImplementation(async (opts: { method?: string }) => {
|
||||||
.mockResolvedValueOnce({
|
if (opts.method === "node.list") {
|
||||||
ts: Date.now(),
|
return {
|
||||||
nodes: [
|
ts: Date.now(),
|
||||||
{
|
nodes: [
|
||||||
nodeId: "ios-node",
|
{
|
||||||
displayName: "iOS Node",
|
nodeId: "ios-node",
|
||||||
remoteIp: "192.168.0.88",
|
displayName: "iOS Node",
|
||||||
connected: true,
|
remoteIp: "192.168.0.88",
|
||||||
},
|
connected: true,
|
||||||
],
|
},
|
||||||
})
|
],
|
||||||
.mockResolvedValueOnce({
|
};
|
||||||
ts: Date.now(),
|
}
|
||||||
nodeId: "ios-node",
|
if (opts.method === "node.describe") {
|
||||||
displayName: "iOS Node",
|
return {
|
||||||
caps: ["canvas", "camera"],
|
ts: Date.now(),
|
||||||
commands: ["canvas.eval", "canvas.snapshot", "camera.snap"],
|
nodeId: "ios-node",
|
||||||
connected: true,
|
displayName: "iOS Node",
|
||||||
});
|
caps: ["canvas", "camera"],
|
||||||
|
commands: ["canvas.eval", "canvas.snapshot", "camera.snap"],
|
||||||
|
connected: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
const program = buildProgram();
|
const program = buildProgram();
|
||||||
runtime.log.mockClear();
|
runtime.log.mockClear();
|
||||||
@@ -154,12 +165,10 @@ describe("cli program (nodes basics)", () => {
|
|||||||
from: "user",
|
from: "user",
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(callGateway).toHaveBeenNthCalledWith(
|
expect(callGateway).toHaveBeenCalledWith(
|
||||||
1,
|
|
||||||
expect.objectContaining({ method: "node.list", params: {} }),
|
expect.objectContaining({ method: "node.list", params: {} }),
|
||||||
);
|
);
|
||||||
expect(callGateway).toHaveBeenNthCalledWith(
|
expect(callGateway).toHaveBeenCalledWith(
|
||||||
2,
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
method: "node.describe",
|
method: "node.describe",
|
||||||
params: { nodeId: "ios-node" },
|
params: { nodeId: "ios-node" },
|
||||||
@@ -189,24 +198,30 @@ describe("cli program (nodes basics)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("runs nodes invoke and calls node.invoke", async () => {
|
it("runs nodes invoke and calls node.invoke", async () => {
|
||||||
callGateway
|
callGateway.mockImplementation(async (opts: { method?: string }) => {
|
||||||
.mockResolvedValueOnce({
|
if (opts.method === "node.list") {
|
||||||
ts: Date.now(),
|
return {
|
||||||
nodes: [
|
ts: Date.now(),
|
||||||
{
|
nodes: [
|
||||||
nodeId: "ios-node",
|
{
|
||||||
displayName: "iOS Node",
|
nodeId: "ios-node",
|
||||||
remoteIp: "192.168.0.88",
|
displayName: "iOS Node",
|
||||||
connected: true,
|
remoteIp: "192.168.0.88",
|
||||||
},
|
connected: true,
|
||||||
],
|
},
|
||||||
})
|
],
|
||||||
.mockResolvedValueOnce({
|
};
|
||||||
ok: true,
|
}
|
||||||
nodeId: "ios-node",
|
if (opts.method === "node.invoke") {
|
||||||
command: "canvas.eval",
|
return {
|
||||||
payload: { result: "ok" },
|
ok: true,
|
||||||
});
|
nodeId: "ios-node",
|
||||||
|
command: "canvas.eval",
|
||||||
|
payload: { result: "ok" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
const program = buildProgram();
|
const program = buildProgram();
|
||||||
runtime.log.mockClear();
|
runtime.log.mockClear();
|
||||||
@@ -224,12 +239,10 @@ describe("cli program (nodes basics)", () => {
|
|||||||
{ from: "user" },
|
{ from: "user" },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(callGateway).toHaveBeenNthCalledWith(
|
expect(callGateway).toHaveBeenCalledWith(
|
||||||
1,
|
|
||||||
expect.objectContaining({ method: "node.list", params: {} }),
|
expect.objectContaining({ method: "node.list", params: {} }),
|
||||||
);
|
);
|
||||||
expect(callGateway).toHaveBeenNthCalledWith(
|
expect(callGateway).toHaveBeenCalledWith(
|
||||||
2,
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
method: "node.invoke",
|
method: "node.invoke",
|
||||||
params: {
|
params: {
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ vi.mock("../tui/tui.js", () => ({ runTui }));
|
|||||||
vi.mock("../gateway/call.js", () => ({
|
vi.mock("../gateway/call.js", () => ({
|
||||||
callGateway,
|
callGateway,
|
||||||
randomIdempotencyKey: () => "idem-test",
|
randomIdempotencyKey: () => "idem-test",
|
||||||
|
buildGatewayConnectionDetails: () => ({
|
||||||
|
url: "ws://127.0.0.1:1234",
|
||||||
|
urlSource: "test",
|
||||||
|
message: "Gateway target: ws://127.0.0.1:1234",
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
vi.mock("./deps.js", () => ({ createDefaultDeps: () => ({}) }));
|
vi.mock("./deps.js", () => ({ createDefaultDeps: () => ({}) }));
|
||||||
|
|
||||||
@@ -56,61 +61,43 @@ describe("cli program (nodes media)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("runs nodes camera snap and prints two MEDIA paths", async () => {
|
it("runs nodes camera snap and prints two MEDIA paths", async () => {
|
||||||
callGateway
|
callGateway.mockImplementation(async (opts: { method?: string }) => {
|
||||||
.mockResolvedValueOnce({
|
if (opts.method === "node.list") {
|
||||||
ts: Date.now(),
|
return {
|
||||||
nodes: [
|
ts: Date.now(),
|
||||||
{
|
nodes: [
|
||||||
nodeId: "ios-node",
|
{
|
||||||
displayName: "iOS Node",
|
nodeId: "ios-node",
|
||||||
remoteIp: "192.168.0.88",
|
displayName: "iOS Node",
|
||||||
connected: true,
|
remoteIp: "192.168.0.88",
|
||||||
},
|
connected: true,
|
||||||
],
|
},
|
||||||
})
|
],
|
||||||
.mockResolvedValueOnce({
|
};
|
||||||
ok: true,
|
}
|
||||||
nodeId: "ios-node",
|
if (opts.method === "node.invoke") {
|
||||||
command: "camera.snap",
|
return {
|
||||||
payload: { format: "jpg", base64: "aGk=", width: 1, height: 1 },
|
ok: true,
|
||||||
})
|
nodeId: "ios-node",
|
||||||
.mockResolvedValueOnce({
|
command: "camera.snap",
|
||||||
ok: true,
|
payload: { format: "jpg", base64: "aGk=", width: 1, height: 1 },
|
||||||
nodeId: "ios-node",
|
};
|
||||||
command: "camera.snap",
|
}
|
||||||
payload: { format: "jpg", base64: "aGk=", width: 1, height: 1 },
|
return { ok: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
const program = buildProgram();
|
const program = buildProgram();
|
||||||
runtime.log.mockClear();
|
runtime.log.mockClear();
|
||||||
await program.parseAsync(["nodes", "camera", "snap", "--node", "ios-node"], { from: "user" });
|
await program.parseAsync(["nodes", "camera", "snap", "--node", "ios-node"], { from: "user" });
|
||||||
|
|
||||||
expect(callGateway).toHaveBeenNthCalledWith(
|
const invokeCalls = callGateway.mock.calls
|
||||||
2,
|
.map((call) => call[0] as { method?: string; params?: Record<string, unknown> })
|
||||||
expect.objectContaining({
|
.filter((call) => call.method === "node.invoke");
|
||||||
method: "node.invoke",
|
const facings = invokeCalls
|
||||||
params: expect.objectContaining({
|
.map((call) => (call.params?.params as { facing?: string } | undefined)?.facing)
|
||||||
nodeId: "ios-node",
|
.filter(Boolean)
|
||||||
command: "camera.snap",
|
.sort((a, b) => a.localeCompare(b));
|
||||||
timeoutMs: 20000,
|
expect(facings).toEqual(["back", "front"]);
|
||||||
idempotencyKey: "idem-test",
|
|
||||||
params: expect.objectContaining({ facing: "front", format: "jpg" }),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(callGateway).toHaveBeenNthCalledWith(
|
|
||||||
3,
|
|
||||||
expect.objectContaining({
|
|
||||||
method: "node.invoke",
|
|
||||||
params: expect.objectContaining({
|
|
||||||
nodeId: "ios-node",
|
|
||||||
command: "camera.snap",
|
|
||||||
timeoutMs: 20000,
|
|
||||||
idempotencyKey: "idem-test",
|
|
||||||
params: expect.objectContaining({ facing: "back", format: "jpg" }),
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const out = String(runtime.log.mock.calls[0]?.[0] ?? "");
|
const out = String(runtime.log.mock.calls[0]?.[0] ?? "");
|
||||||
const mediaPaths = out
|
const mediaPaths = out
|
||||||
@@ -130,29 +117,35 @@ describe("cli program (nodes media)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("runs nodes camera clip and prints one MEDIA path", async () => {
|
it("runs nodes camera clip and prints one MEDIA path", async () => {
|
||||||
callGateway
|
callGateway.mockImplementation(async (opts: { method?: string }) => {
|
||||||
.mockResolvedValueOnce({
|
if (opts.method === "node.list") {
|
||||||
ts: Date.now(),
|
return {
|
||||||
nodes: [
|
ts: Date.now(),
|
||||||
{
|
nodes: [
|
||||||
nodeId: "ios-node",
|
{
|
||||||
displayName: "iOS Node",
|
nodeId: "ios-node",
|
||||||
remoteIp: "192.168.0.88",
|
displayName: "iOS Node",
|
||||||
connected: true,
|
remoteIp: "192.168.0.88",
|
||||||
|
connected: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (opts.method === "node.invoke") {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
nodeId: "ios-node",
|
||||||
|
command: "camera.clip",
|
||||||
|
payload: {
|
||||||
|
format: "mp4",
|
||||||
|
base64: "aGk=",
|
||||||
|
durationMs: 3000,
|
||||||
|
hasAudio: true,
|
||||||
},
|
},
|
||||||
],
|
};
|
||||||
})
|
}
|
||||||
.mockResolvedValueOnce({
|
return { ok: true };
|
||||||
ok: true,
|
});
|
||||||
nodeId: "ios-node",
|
|
||||||
command: "camera.clip",
|
|
||||||
payload: {
|
|
||||||
format: "mp4",
|
|
||||||
base64: "aGk=",
|
|
||||||
durationMs: 3000,
|
|
||||||
hasAudio: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const program = buildProgram();
|
const program = buildProgram();
|
||||||
runtime.log.mockClear();
|
runtime.log.mockClear();
|
||||||
@@ -161,8 +154,7 @@ describe("cli program (nodes media)", () => {
|
|||||||
{ from: "user" },
|
{ from: "user" },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(callGateway).toHaveBeenNthCalledWith(
|
expect(callGateway).toHaveBeenCalledWith(
|
||||||
2,
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
method: "node.invoke",
|
method: "node.invoke",
|
||||||
params: expect.objectContaining({
|
params: expect.objectContaining({
|
||||||
@@ -192,24 +184,30 @@ describe("cli program (nodes media)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("runs nodes camera snap with facing front and passes params", async () => {
|
it("runs nodes camera snap with facing front and passes params", async () => {
|
||||||
callGateway
|
callGateway.mockImplementation(async (opts: { method?: string }) => {
|
||||||
.mockResolvedValueOnce({
|
if (opts.method === "node.list") {
|
||||||
ts: Date.now(),
|
return {
|
||||||
nodes: [
|
ts: Date.now(),
|
||||||
{
|
nodes: [
|
||||||
nodeId: "ios-node",
|
{
|
||||||
displayName: "iOS Node",
|
nodeId: "ios-node",
|
||||||
remoteIp: "192.168.0.88",
|
displayName: "iOS Node",
|
||||||
connected: true,
|
remoteIp: "192.168.0.88",
|
||||||
},
|
connected: true,
|
||||||
],
|
},
|
||||||
})
|
],
|
||||||
.mockResolvedValueOnce({
|
};
|
||||||
ok: true,
|
}
|
||||||
nodeId: "ios-node",
|
if (opts.method === "node.invoke") {
|
||||||
command: "camera.snap",
|
return {
|
||||||
payload: { format: "jpg", base64: "aGk=", width: 1, height: 1 },
|
ok: true,
|
||||||
});
|
nodeId: "ios-node",
|
||||||
|
command: "camera.snap",
|
||||||
|
payload: { format: "jpg", base64: "aGk=", width: 1, height: 1 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
const program = buildProgram();
|
const program = buildProgram();
|
||||||
runtime.log.mockClear();
|
runtime.log.mockClear();
|
||||||
@@ -234,8 +232,7 @@ describe("cli program (nodes media)", () => {
|
|||||||
{ from: "user" },
|
{ from: "user" },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(callGateway).toHaveBeenNthCalledWith(
|
expect(callGateway).toHaveBeenCalledWith(
|
||||||
2,
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
method: "node.invoke",
|
method: "node.invoke",
|
||||||
params: expect.objectContaining({
|
params: expect.objectContaining({
|
||||||
@@ -265,29 +262,35 @@ describe("cli program (nodes media)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("runs nodes camera clip with --no-audio", async () => {
|
it("runs nodes camera clip with --no-audio", async () => {
|
||||||
callGateway
|
callGateway.mockImplementation(async (opts: { method?: string }) => {
|
||||||
.mockResolvedValueOnce({
|
if (opts.method === "node.list") {
|
||||||
ts: Date.now(),
|
return {
|
||||||
nodes: [
|
ts: Date.now(),
|
||||||
{
|
nodes: [
|
||||||
nodeId: "ios-node",
|
{
|
||||||
displayName: "iOS Node",
|
nodeId: "ios-node",
|
||||||
remoteIp: "192.168.0.88",
|
displayName: "iOS Node",
|
||||||
connected: true,
|
remoteIp: "192.168.0.88",
|
||||||
|
connected: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (opts.method === "node.invoke") {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
nodeId: "ios-node",
|
||||||
|
command: "camera.clip",
|
||||||
|
payload: {
|
||||||
|
format: "mp4",
|
||||||
|
base64: "aGk=",
|
||||||
|
durationMs: 3000,
|
||||||
|
hasAudio: false,
|
||||||
},
|
},
|
||||||
],
|
};
|
||||||
})
|
}
|
||||||
.mockResolvedValueOnce({
|
return { ok: true };
|
||||||
ok: true,
|
});
|
||||||
nodeId: "ios-node",
|
|
||||||
command: "camera.clip",
|
|
||||||
payload: {
|
|
||||||
format: "mp4",
|
|
||||||
base64: "aGk=",
|
|
||||||
durationMs: 3000,
|
|
||||||
hasAudio: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const program = buildProgram();
|
const program = buildProgram();
|
||||||
runtime.log.mockClear();
|
runtime.log.mockClear();
|
||||||
@@ -307,8 +310,7 @@ describe("cli program (nodes media)", () => {
|
|||||||
{ from: "user" },
|
{ from: "user" },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(callGateway).toHaveBeenNthCalledWith(
|
expect(callGateway).toHaveBeenCalledWith(
|
||||||
2,
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
method: "node.invoke",
|
method: "node.invoke",
|
||||||
params: expect.objectContaining({
|
params: expect.objectContaining({
|
||||||
@@ -335,29 +337,35 @@ describe("cli program (nodes media)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("runs nodes camera clip with human duration (10s)", async () => {
|
it("runs nodes camera clip with human duration (10s)", async () => {
|
||||||
callGateway
|
callGateway.mockImplementation(async (opts: { method?: string }) => {
|
||||||
.mockResolvedValueOnce({
|
if (opts.method === "node.list") {
|
||||||
ts: Date.now(),
|
return {
|
||||||
nodes: [
|
ts: Date.now(),
|
||||||
{
|
nodes: [
|
||||||
nodeId: "ios-node",
|
{
|
||||||
displayName: "iOS Node",
|
nodeId: "ios-node",
|
||||||
remoteIp: "192.168.0.88",
|
displayName: "iOS Node",
|
||||||
connected: true,
|
remoteIp: "192.168.0.88",
|
||||||
|
connected: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (opts.method === "node.invoke") {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
nodeId: "ios-node",
|
||||||
|
command: "camera.clip",
|
||||||
|
payload: {
|
||||||
|
format: "mp4",
|
||||||
|
base64: "aGk=",
|
||||||
|
durationMs: 10_000,
|
||||||
|
hasAudio: true,
|
||||||
},
|
},
|
||||||
],
|
};
|
||||||
})
|
}
|
||||||
.mockResolvedValueOnce({
|
return { ok: true };
|
||||||
ok: true,
|
});
|
||||||
nodeId: "ios-node",
|
|
||||||
command: "camera.clip",
|
|
||||||
payload: {
|
|
||||||
format: "mp4",
|
|
||||||
base64: "aGk=",
|
|
||||||
durationMs: 10_000,
|
|
||||||
hasAudio: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const program = buildProgram();
|
const program = buildProgram();
|
||||||
runtime.log.mockClear();
|
runtime.log.mockClear();
|
||||||
@@ -366,8 +374,7 @@ describe("cli program (nodes media)", () => {
|
|||||||
{ from: "user" },
|
{ from: "user" },
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(callGateway).toHaveBeenNthCalledWith(
|
expect(callGateway).toHaveBeenCalledWith(
|
||||||
2,
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
method: "node.invoke",
|
method: "node.invoke",
|
||||||
params: expect.objectContaining({
|
params: expect.objectContaining({
|
||||||
@@ -380,24 +387,30 @@ describe("cli program (nodes media)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("runs nodes canvas snapshot and prints MEDIA path", async () => {
|
it("runs nodes canvas snapshot and prints MEDIA path", async () => {
|
||||||
callGateway
|
callGateway.mockImplementation(async (opts: { method?: string }) => {
|
||||||
.mockResolvedValueOnce({
|
if (opts.method === "node.list") {
|
||||||
ts: Date.now(),
|
return {
|
||||||
nodes: [
|
ts: Date.now(),
|
||||||
{
|
nodes: [
|
||||||
nodeId: "ios-node",
|
{
|
||||||
displayName: "iOS Node",
|
nodeId: "ios-node",
|
||||||
remoteIp: "192.168.0.88",
|
displayName: "iOS Node",
|
||||||
connected: true,
|
remoteIp: "192.168.0.88",
|
||||||
},
|
connected: true,
|
||||||
],
|
},
|
||||||
})
|
],
|
||||||
.mockResolvedValueOnce({
|
};
|
||||||
ok: true,
|
}
|
||||||
nodeId: "ios-node",
|
if (opts.method === "node.invoke") {
|
||||||
command: "canvas.snapshot",
|
return {
|
||||||
payload: { format: "png", base64: "aGk=" },
|
ok: true,
|
||||||
});
|
nodeId: "ios-node",
|
||||||
|
command: "canvas.snapshot",
|
||||||
|
payload: { format: "png", base64: "aGk=" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
|
});
|
||||||
|
|
||||||
const program = buildProgram();
|
const program = buildProgram();
|
||||||
runtime.log.mockClear();
|
runtime.log.mockClear();
|
||||||
@@ -418,16 +431,21 @@ describe("cli program (nodes media)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("fails nodes camera snap on invalid facing", async () => {
|
it("fails nodes camera snap on invalid facing", async () => {
|
||||||
callGateway.mockResolvedValueOnce({
|
callGateway.mockImplementation(async (opts: { method?: string }) => {
|
||||||
ts: Date.now(),
|
if (opts.method === "node.list") {
|
||||||
nodes: [
|
return {
|
||||||
{
|
ts: Date.now(),
|
||||||
nodeId: "ios-node",
|
nodes: [
|
||||||
displayName: "iOS Node",
|
{
|
||||||
remoteIp: "192.168.0.88",
|
nodeId: "ios-node",
|
||||||
connected: true,
|
displayName: "iOS Node",
|
||||||
},
|
remoteIp: "192.168.0.88",
|
||||||
],
|
connected: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { ok: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
const program = buildProgram();
|
const program = buildProgram();
|
||||||
@@ -439,6 +457,8 @@ describe("cli program (nodes media)", () => {
|
|||||||
}),
|
}),
|
||||||
).rejects.toThrow(/exit/i);
|
).rejects.toThrow(/exit/i);
|
||||||
|
|
||||||
expect(runtime.error).toHaveBeenCalledWith(expect.stringMatching(/invalid facing/i));
|
expect(runtime.error.mock.calls.some(([msg]) => /invalid facing/i.test(String(msg)))).toBe(
|
||||||
|
true,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ vi.mock("../tui/tui.js", () => ({ runTui }));
|
|||||||
vi.mock("../gateway/call.js", () => ({
|
vi.mock("../gateway/call.js", () => ({
|
||||||
callGateway,
|
callGateway,
|
||||||
randomIdempotencyKey: () => "idem-test",
|
randomIdempotencyKey: () => "idem-test",
|
||||||
|
buildGatewayConnectionDetails: () => ({
|
||||||
|
url: "ws://127.0.0.1:1234",
|
||||||
|
urlSource: "test",
|
||||||
|
message: "Gateway target: ws://127.0.0.1:1234",
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
vi.mock("./deps.js", () => ({ createDefaultDeps: () => ({}) }));
|
vi.mock("./deps.js", () => ({ createDefaultDeps: () => ({}) }));
|
||||||
|
|
||||||
|
|||||||
@@ -1,65 +1,67 @@
|
|||||||
import {
|
import { readConfigFileSnapshot } from "../../config/config.js";
|
||||||
isNixMode,
|
import { loadAndMaybeMigrateDoctorConfig } from "../../commands/doctor-config-flow.js";
|
||||||
loadConfig,
|
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
||||||
migrateLegacyConfig,
|
import { loadClawdbotPlugins } from "../../plugins/loader.js";
|
||||||
readConfigFileSnapshot,
|
|
||||||
writeConfigFile,
|
|
||||||
} from "../../config/config.js";
|
|
||||||
import { danger } from "../../globals.js";
|
|
||||||
import { autoMigrateLegacyState } from "../../infra/state-migrations.js";
|
|
||||||
import type { RuntimeEnv } from "../../runtime.js";
|
import type { RuntimeEnv } from "../../runtime.js";
|
||||||
|
|
||||||
|
const ALLOWED_INVALID_COMMANDS = new Set(["doctor", "logs", "health", "help", "status", "service"]);
|
||||||
|
|
||||||
|
function formatConfigIssues(issues: Array<{ path: string; message: string }>): string[] {
|
||||||
|
return issues.map((issue) => `- ${issue.path || "<root>"}: ${issue.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
export async function ensureConfigReady(params: {
|
export async function ensureConfigReady(params: {
|
||||||
runtime: RuntimeEnv;
|
runtime: RuntimeEnv;
|
||||||
migrateState?: boolean;
|
commandPath?: string[];
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
|
await loadAndMaybeMigrateDoctorConfig({
|
||||||
|
options: { nonInteractive: true },
|
||||||
|
confirm: async () => false,
|
||||||
|
});
|
||||||
|
|
||||||
const snapshot = await readConfigFileSnapshot();
|
const snapshot = await readConfigFileSnapshot();
|
||||||
if (snapshot.legacyIssues.length > 0) {
|
const command = params.commandPath?.[0];
|
||||||
if (isNixMode) {
|
const allowInvalid = command ? ALLOWED_INVALID_COMMANDS.has(command) : false;
|
||||||
params.runtime.error(
|
const issues = snapshot.exists && !snapshot.valid ? formatConfigIssues(snapshot.issues) : [];
|
||||||
danger(
|
const legacyIssues =
|
||||||
"Legacy config entries detected while running in Nix mode. Update your Nix config to the latest schema and retry.",
|
snapshot.legacyIssues.length > 0
|
||||||
),
|
? snapshot.legacyIssues.map((issue) => `- ${issue.path}: ${issue.message}`)
|
||||||
);
|
: [];
|
||||||
params.runtime.exit(1);
|
|
||||||
return;
|
const pluginIssues: string[] = [];
|
||||||
}
|
if (snapshot.valid) {
|
||||||
const migrated = migrateLegacyConfig(snapshot.parsed);
|
const workspaceDir = resolveAgentWorkspaceDir(
|
||||||
if (migrated.config) {
|
snapshot.config,
|
||||||
await writeConfigFile(migrated.config);
|
resolveDefaultAgentId(snapshot.config),
|
||||||
if (migrated.changes.length > 0) {
|
);
|
||||||
params.runtime.log(
|
const registry = loadClawdbotPlugins({
|
||||||
`Migrated legacy config entries:\n${migrated.changes
|
config: snapshot.config,
|
||||||
.map((entry) => `- ${entry}`)
|
workspaceDir: workspaceDir ?? undefined,
|
||||||
.join("\n")}`,
|
cache: false,
|
||||||
);
|
mode: "validate",
|
||||||
}
|
});
|
||||||
} else {
|
for (const diag of registry.diagnostics) {
|
||||||
const issues = snapshot.legacyIssues
|
if (diag.level !== "error") continue;
|
||||||
.map((issue) => `- ${issue.path}: ${issue.message}`)
|
const id = diag.pluginId ? ` ${diag.pluginId}` : "";
|
||||||
.join("\n");
|
pluginIssues.push(`- plugin${id}: ${diag.message}`);
|
||||||
params.runtime.error(
|
|
||||||
danger(
|
|
||||||
`Legacy config entries detected. Run "clawdbot doctor" (or ask your agent) to migrate.\n${issues}`,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
params.runtime.exit(1);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (snapshot.exists && !snapshot.valid) {
|
const invalid = snapshot.exists && (!snapshot.valid || pluginIssues.length > 0);
|
||||||
params.runtime.error(`Config invalid at ${snapshot.path}.`);
|
if (!invalid) return;
|
||||||
for (const issue of snapshot.issues) {
|
|
||||||
params.runtime.error(`- ${issue.path || "<root>"}: ${issue.message}`);
|
params.runtime.error(`Config invalid at ${snapshot.path}.`);
|
||||||
}
|
if (issues.length > 0) {
|
||||||
params.runtime.error("Run `clawdbot doctor` to repair, then retry.");
|
params.runtime.error(issues.join("\n"));
|
||||||
|
}
|
||||||
|
if (legacyIssues.length > 0) {
|
||||||
|
params.runtime.error(`Legacy config keys detected:\n${legacyIssues.join("\n")}`);
|
||||||
|
}
|
||||||
|
if (pluginIssues.length > 0) {
|
||||||
|
params.runtime.error(`Plugin config errors:\n${pluginIssues.join("\n")}`);
|
||||||
|
}
|
||||||
|
params.runtime.error("Run `clawdbot doctor --fix` to repair, then retry.");
|
||||||
|
if (!allowInvalid) {
|
||||||
params.runtime.exit(1);
|
params.runtime.exit(1);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.migrateState !== false) {
|
|
||||||
const cfg = loadConfig();
|
|
||||||
await autoMigrateLegacyState({ cfg });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import { defaultRuntime } from "../../runtime.js";
|
import { defaultRuntime } from "../../runtime.js";
|
||||||
import { emitCliBanner } from "../banner.js";
|
import { emitCliBanner } from "../banner.js";
|
||||||
import { getCommandPath, hasHelpOrVersion, shouldMigrateState } from "../argv.js";
|
import { getCommandPath, hasHelpOrVersion } from "../argv.js";
|
||||||
import { ensureConfigReady } from "./config-guard.js";
|
import { ensureConfigReady } from "./config-guard.js";
|
||||||
|
|
||||||
function setProcessTitleForCommand(actionCommand: Command) {
|
function setProcessTitleForCommand(actionCommand: Command) {
|
||||||
@@ -20,9 +20,8 @@ export function registerPreActionHooks(program: Command, programVersion: string)
|
|||||||
emitCliBanner(programVersion);
|
emitCliBanner(programVersion);
|
||||||
const argv = process.argv;
|
const argv = process.argv;
|
||||||
if (hasHelpOrVersion(argv)) return;
|
if (hasHelpOrVersion(argv)) return;
|
||||||
const [primary] = getCommandPath(argv, 1);
|
const commandPath = getCommandPath(argv, 2);
|
||||||
if (primary === "doctor") return;
|
if (commandPath[0] === "doctor") return;
|
||||||
const migrateState = shouldMigrateState(argv);
|
await ensureConfigReady({ runtime: defaultRuntime, commandPath });
|
||||||
await ensureConfigReady({ runtime: defaultRuntime, migrateState });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export function registerMaintenanceCommands(program: Command) {
|
|||||||
.option("--no-workspace-suggestions", "Disable workspace memory system suggestions", false)
|
.option("--no-workspace-suggestions", "Disable workspace memory system suggestions", false)
|
||||||
.option("--yes", "Accept defaults without prompting", false)
|
.option("--yes", "Accept defaults without prompting", false)
|
||||||
.option("--repair", "Apply recommended repairs without prompting", false)
|
.option("--repair", "Apply recommended repairs without prompting", false)
|
||||||
|
.option("--fix", "Apply recommended repairs (alias for --repair)", false)
|
||||||
.option("--force", "Apply aggressive repairs (overwrites custom service config)", false)
|
.option("--force", "Apply aggressive repairs (overwrites custom service config)", false)
|
||||||
.option("--non-interactive", "Run without prompts (safe migrations only)", false)
|
.option("--non-interactive", "Run without prompts (safe migrations only)", false)
|
||||||
.option("--generate-gateway-token", "Generate and configure a gateway token", false)
|
.option("--generate-gateway-token", "Generate and configure a gateway token", false)
|
||||||
@@ -29,7 +30,7 @@ export function registerMaintenanceCommands(program: Command) {
|
|||||||
await doctorCommand(defaultRuntime, {
|
await doctorCommand(defaultRuntime, {
|
||||||
workspaceSuggestions: opts.workspaceSuggestions,
|
workspaceSuggestions: opts.workspaceSuggestions,
|
||||||
yes: Boolean(opts.yes),
|
yes: Boolean(opts.yes),
|
||||||
repair: Boolean(opts.repair),
|
repair: Boolean(opts.repair) || Boolean(opts.fix),
|
||||||
force: Boolean(opts.force),
|
force: Boolean(opts.force),
|
||||||
nonInteractive: Boolean(opts.nonInteractive),
|
nonInteractive: Boolean(opts.nonInteractive),
|
||||||
generateGatewayToken: Boolean(opts.generateGatewayToken),
|
generateGatewayToken: Boolean(opts.generateGatewayToken),
|
||||||
|
|||||||
@@ -15,18 +15,17 @@ import {
|
|||||||
getVerboseFlag,
|
getVerboseFlag,
|
||||||
hasFlag,
|
hasFlag,
|
||||||
hasHelpOrVersion,
|
hasHelpOrVersion,
|
||||||
shouldMigrateStateFromPath,
|
|
||||||
} from "./argv.js";
|
} from "./argv.js";
|
||||||
import { ensureConfigReady } from "./program/config-guard.js";
|
import { ensureConfigReady } from "./program/config-guard.js";
|
||||||
import { runMemoryStatus } from "./memory-cli.js";
|
import { runMemoryStatus } from "./memory-cli.js";
|
||||||
|
|
||||||
async function prepareRoutedCommand(params: {
|
async function prepareRoutedCommand(params: {
|
||||||
argv: string[];
|
argv: string[];
|
||||||
migrateState: boolean;
|
commandPath: string[];
|
||||||
loadPlugins?: boolean;
|
loadPlugins?: boolean;
|
||||||
}) {
|
}) {
|
||||||
emitCliBanner(VERSION, { argv: params.argv });
|
emitCliBanner(VERSION, { argv: params.argv });
|
||||||
await ensureConfigReady({ runtime: defaultRuntime, migrateState: params.migrateState });
|
await ensureConfigReady({ runtime: defaultRuntime, commandPath: params.commandPath });
|
||||||
if (params.loadPlugins) {
|
if (params.loadPlugins) {
|
||||||
ensurePluginRegistryLoaded();
|
ensurePluginRegistryLoaded();
|
||||||
}
|
}
|
||||||
@@ -39,10 +38,8 @@ export async function tryRouteCli(argv: string[]): Promise<boolean> {
|
|||||||
const path = getCommandPath(argv, 2);
|
const path = getCommandPath(argv, 2);
|
||||||
const [primary, secondary] = path;
|
const [primary, secondary] = path;
|
||||||
if (!primary) return false;
|
if (!primary) return false;
|
||||||
const migrateState = shouldMigrateStateFromPath(path);
|
|
||||||
|
|
||||||
if (primary === "health") {
|
if (primary === "health") {
|
||||||
await prepareRoutedCommand({ argv, migrateState, loadPlugins: true });
|
await prepareRoutedCommand({ argv, commandPath: path, loadPlugins: true });
|
||||||
const json = hasFlag(argv, "--json");
|
const json = hasFlag(argv, "--json");
|
||||||
const verbose = getVerboseFlag(argv, { includeDebug: true });
|
const verbose = getVerboseFlag(argv, { includeDebug: true });
|
||||||
const timeoutMs = getPositiveIntFlagValue(argv, "--timeout");
|
const timeoutMs = getPositiveIntFlagValue(argv, "--timeout");
|
||||||
@@ -53,7 +50,7 @@ export async function tryRouteCli(argv: string[]): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (primary === "status") {
|
if (primary === "status") {
|
||||||
await prepareRoutedCommand({ argv, migrateState, loadPlugins: true });
|
await prepareRoutedCommand({ argv, commandPath: path, loadPlugins: true });
|
||||||
const json = hasFlag(argv, "--json");
|
const json = hasFlag(argv, "--json");
|
||||||
const deep = hasFlag(argv, "--deep");
|
const deep = hasFlag(argv, "--deep");
|
||||||
const all = hasFlag(argv, "--all");
|
const all = hasFlag(argv, "--all");
|
||||||
@@ -67,7 +64,7 @@ export async function tryRouteCli(argv: string[]): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (primary === "sessions") {
|
if (primary === "sessions") {
|
||||||
await prepareRoutedCommand({ argv, migrateState });
|
await prepareRoutedCommand({ argv, commandPath: path });
|
||||||
const json = hasFlag(argv, "--json");
|
const json = hasFlag(argv, "--json");
|
||||||
const verbose = getVerboseFlag(argv);
|
const verbose = getVerboseFlag(argv);
|
||||||
const store = getFlagValue(argv, "--store");
|
const store = getFlagValue(argv, "--store");
|
||||||
@@ -80,7 +77,7 @@ export async function tryRouteCli(argv: string[]): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (primary === "agents" && secondary === "list") {
|
if (primary === "agents" && secondary === "list") {
|
||||||
await prepareRoutedCommand({ argv, migrateState });
|
await prepareRoutedCommand({ argv, commandPath: path });
|
||||||
const json = hasFlag(argv, "--json");
|
const json = hasFlag(argv, "--json");
|
||||||
const bindings = hasFlag(argv, "--bindings");
|
const bindings = hasFlag(argv, "--bindings");
|
||||||
await agentsListCommand({ json, bindings }, defaultRuntime);
|
await agentsListCommand({ json, bindings }, defaultRuntime);
|
||||||
@@ -88,7 +85,7 @@ export async function tryRouteCli(argv: string[]): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (primary === "memory" && secondary === "status") {
|
if (primary === "memory" && secondary === "status") {
|
||||||
await prepareRoutedCommand({ argv, migrateState });
|
await prepareRoutedCommand({ argv, commandPath: path });
|
||||||
const agent = getFlagValue(argv, "--agent");
|
const agent = getFlagValue(argv, "--agent");
|
||||||
if (agent === null) return false;
|
if (agent === null) return false;
|
||||||
const json = hasFlag(argv, "--json");
|
const json = hasFlag(argv, "--json");
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
migrateLegacyConfig,
|
migrateLegacyConfig,
|
||||||
readConfigFileSnapshot,
|
readConfigFileSnapshot,
|
||||||
} from "../config/config.js";
|
} from "../config/config.js";
|
||||||
|
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
|
||||||
import { note } from "../terminal/note.js";
|
import { note } from "../terminal/note.js";
|
||||||
import { normalizeLegacyConfigValues } from "./doctor-legacy-config.js";
|
import { normalizeLegacyConfigValues } from "./doctor-legacy-config.js";
|
||||||
import type { DoctorOptions } from "./doctor-prompter.js";
|
import type { DoctorOptions } from "./doctor-prompter.js";
|
||||||
@@ -45,6 +46,8 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
|
|||||||
options: DoctorOptions;
|
options: DoctorOptions;
|
||||||
confirm: (p: { message: string; initialValue: boolean }) => Promise<boolean>;
|
confirm: (p: { message: string; initialValue: boolean }) => Promise<boolean>;
|
||||||
}) {
|
}) {
|
||||||
|
void params.confirm;
|
||||||
|
const shouldRepair = params.options.repair === true || params.options.yes === true;
|
||||||
const snapshot = await readConfigFileSnapshot();
|
const snapshot = await readConfigFileSnapshot();
|
||||||
let cfg: ClawdbotConfig = snapshot.config ?? {};
|
let cfg: ClawdbotConfig = snapshot.config ?? {};
|
||||||
if (snapshot.exists && !snapshot.valid && snapshot.legacyIssues.length === 0) {
|
if (snapshot.exists && !snapshot.valid && snapshot.legacyIssues.length === 0) {
|
||||||
@@ -56,25 +59,34 @@ export async function loadAndMaybeMigrateDoctorConfig(params: {
|
|||||||
snapshot.legacyIssues.map((issue) => `- ${issue.path}: ${issue.message}`).join("\n"),
|
snapshot.legacyIssues.map((issue) => `- ${issue.path}: ${issue.message}`).join("\n"),
|
||||||
"Legacy config keys detected",
|
"Legacy config keys detected",
|
||||||
);
|
);
|
||||||
const migrate =
|
if (shouldRepair) {
|
||||||
params.options.nonInteractive === true
|
|
||||||
? true
|
|
||||||
: await params.confirm({
|
|
||||||
message: "Migrate legacy config entries now?",
|
|
||||||
initialValue: true,
|
|
||||||
});
|
|
||||||
if (migrate) {
|
|
||||||
// Legacy migration (2026-01-02, commit: 16420e5b) — normalize per-provider allowlists; move WhatsApp gating into channels.whatsapp.allowFrom.
|
// Legacy migration (2026-01-02, commit: 16420e5b) — normalize per-provider allowlists; move WhatsApp gating into channels.whatsapp.allowFrom.
|
||||||
const { config: migrated, changes } = migrateLegacyConfig(snapshot.parsed);
|
const { config: migrated, changes } = migrateLegacyConfig(snapshot.parsed);
|
||||||
if (changes.length > 0) note(changes.join("\n"), "Doctor changes");
|
if (changes.length > 0) note(changes.join("\n"), "Doctor changes");
|
||||||
if (migrated) cfg = migrated;
|
if (migrated) cfg = migrated;
|
||||||
|
} else {
|
||||||
|
note('Run "clawdbot doctor --fix" to apply legacy migrations.', "Doctor");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalized = normalizeLegacyConfigValues(cfg);
|
const normalized = normalizeLegacyConfigValues(cfg);
|
||||||
if (normalized.changes.length > 0) {
|
if (normalized.changes.length > 0) {
|
||||||
note(normalized.changes.join("\n"), "Doctor changes");
|
note(normalized.changes.join("\n"), "Doctor changes");
|
||||||
cfg = normalized.config;
|
if (shouldRepair) {
|
||||||
|
cfg = normalized.config;
|
||||||
|
} else {
|
||||||
|
note('Run "clawdbot doctor --fix" to apply these changes.', "Doctor");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const autoEnable = applyPluginAutoEnable({ config: cfg, env: process.env });
|
||||||
|
if (autoEnable.changes.length > 0) {
|
||||||
|
note(autoEnable.changes.join("\n"), "Doctor changes");
|
||||||
|
if (shouldRepair) {
|
||||||
|
cfg = autoEnable.config;
|
||||||
|
} else {
|
||||||
|
note('Run "clawdbot doctor --fix" to apply these changes.', "Doctor");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
noteOpencodeProviderOverrides(cfg);
|
noteOpencodeProviderOverrides(cfg);
|
||||||
|
|||||||
@@ -356,7 +356,7 @@ describe("doctor command", () => {
|
|||||||
changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."],
|
changes: ["Moved routing.allowFrom → channels.whatsapp.allowFrom."],
|
||||||
});
|
});
|
||||||
|
|
||||||
await doctorCommand(runtime, { nonInteractive: true });
|
await doctorCommand(runtime, { nonInteractive: true, repair: true });
|
||||||
|
|
||||||
expect(writeConfigFile).toHaveBeenCalledTimes(1);
|
expect(writeConfigFile).toHaveBeenCalledTimes(1);
|
||||||
const written = writeConfigFile.mock.calls[0]?.[0] as Record<string, unknown>;
|
const written = writeConfigFile.mock.calls[0]?.[0] as Record<string, unknown>;
|
||||||
|
|||||||
@@ -247,9 +247,13 @@ export async function doctorCommand(
|
|||||||
healthOk,
|
healthOk,
|
||||||
});
|
});
|
||||||
|
|
||||||
cfg = applyWizardMetadata(cfg, { command: "doctor", mode: resolveMode(cfg) });
|
if (prompter.shouldRepair) {
|
||||||
await writeConfigFile(cfg);
|
cfg = applyWizardMetadata(cfg, { command: "doctor", mode: resolveMode(cfg) });
|
||||||
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
await writeConfigFile(cfg);
|
||||||
|
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
|
||||||
|
} else {
|
||||||
|
runtime.log('Run "clawdbot doctor --fix" to apply changes.');
|
||||||
|
}
|
||||||
|
|
||||||
if (options.workspaceSuggestions !== false) {
|
if (options.workspaceSuggestions !== false) {
|
||||||
const workspaceDir = resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg));
|
const workspaceDir = resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg));
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ describe("config env vars", () => {
|
|||||||
path.join(configDir, "clawdbot.json"),
|
path.join(configDir, "clawdbot.json"),
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
env: { OPENROUTER_API_KEY: "config-key" },
|
env: { vars: { OPENROUTER_API_KEY: "config-key" } },
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
2,
|
2,
|
||||||
@@ -36,7 +36,7 @@ describe("config env vars", () => {
|
|||||||
path.join(configDir, "clawdbot.json"),
|
path.join(configDir, "clawdbot.json"),
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
env: { OPENROUTER_API_KEY: "config-key" },
|
env: { vars: { OPENROUTER_API_KEY: "config-key" } },
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
2,
|
2,
|
||||||
|
|||||||
@@ -164,8 +164,6 @@ describe("config identity defaults", () => {
|
|||||||
messages: {
|
messages: {
|
||||||
messagePrefix: "[clawdbot]",
|
messagePrefix: "[clawdbot]",
|
||||||
responsePrefix: "🦞",
|
responsePrefix: "🦞",
|
||||||
// legacy field should be ignored (moved to providers)
|
|
||||||
textChunkLimit: 9999,
|
|
||||||
},
|
},
|
||||||
channels: {
|
channels: {
|
||||||
whatsapp: { allowFrom: ["+15555550123"], textChunkLimit: 4444 },
|
whatsapp: { allowFrom: ["+15555550123"], textChunkLimit: 4444 },
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ describe("legacy config detection", () => {
|
|||||||
const { validateConfigObject } = await import("./config.js");
|
const { validateConfigObject } = await import("./config.js");
|
||||||
const res = validateConfigObject({
|
const res = validateConfigObject({
|
||||||
channels: { imessage: { cliPath: "imsg; rm -rf /" } },
|
channels: { imessage: { cliPath: "imsg; rm -rf /" } },
|
||||||
tools: { audio: { transcription: { args: ["--model", "base"] } } },
|
audio: { transcription: { command: ["whisper", "--model", "base"] } },
|
||||||
});
|
});
|
||||||
expect(res.ok).toBe(false);
|
expect(res.ok).toBe(false);
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@@ -76,7 +76,7 @@ describe("legacy config detection", () => {
|
|||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
const { validateConfigObject } = await import("./config.js");
|
const { validateConfigObject } = await import("./config.js");
|
||||||
const res = validateConfigObject({
|
const res = validateConfigObject({
|
||||||
tools: { audio: { transcription: { args: ["--model", "base"] } } },
|
audio: { transcription: { command: ["whisper", "--model", "base"] } },
|
||||||
});
|
});
|
||||||
expect(res.ok).toBe(true);
|
expect(res.ok).toBe(true);
|
||||||
});
|
});
|
||||||
@@ -85,11 +85,9 @@ describe("legacy config detection", () => {
|
|||||||
const { validateConfigObject } = await import("./config.js");
|
const { validateConfigObject } = await import("./config.js");
|
||||||
const res = validateConfigObject({
|
const res = validateConfigObject({
|
||||||
channels: { imessage: { cliPath: "/Applications/Imsg Tools/imsg" } },
|
channels: { imessage: { cliPath: "/Applications/Imsg Tools/imsg" } },
|
||||||
tools: {
|
audio: {
|
||||||
audio: {
|
transcription: {
|
||||||
transcription: {
|
command: ["whisper", "--model"],
|
||||||
args: ["--model"],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -166,7 +164,7 @@ describe("legacy config detection", () => {
|
|||||||
expect(res.config?.agents?.defaults?.models?.["openai/gpt-4.1-mini"]).toBeTruthy();
|
expect(res.config?.agents?.defaults?.models?.["openai/gpt-4.1-mini"]).toBeTruthy();
|
||||||
expect(res.config?.agent).toBeUndefined();
|
expect(res.config?.agent).toBeUndefined();
|
||||||
});
|
});
|
||||||
it("auto-migrates legacy config in snapshot (no legacyIssues)", async () => {
|
it("flags legacy config in snapshot", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
||||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||||
@@ -176,31 +174,23 @@ describe("legacy config detection", () => {
|
|||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
|
|
||||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
try {
|
const { readConfigFileSnapshot } = await import("./config.js");
|
||||||
const { readConfigFileSnapshot } = await import("./config.js");
|
const snap = await readConfigFileSnapshot();
|
||||||
const snap = await readConfigFileSnapshot();
|
|
||||||
|
|
||||||
expect(snap.valid).toBe(true);
|
expect(snap.valid).toBe(false);
|
||||||
expect(snap.legacyIssues.length).toBe(0);
|
expect(snap.legacyIssues.some((issue) => issue.path === "routing.allowFrom")).toBe(true);
|
||||||
|
|
||||||
const raw = await fs.readFile(configPath, "utf-8");
|
const raw = await fs.readFile(configPath, "utf-8");
|
||||||
const parsed = JSON.parse(raw) as {
|
const parsed = JSON.parse(raw) as {
|
||||||
channels?: { whatsapp?: { allowFrom?: string[] } };
|
routing?: { allowFrom?: string[] };
|
||||||
routing?: unknown;
|
channels?: unknown;
|
||||||
};
|
};
|
||||||
expect(parsed.channels?.whatsapp?.allowFrom).toEqual(["+15555550123"]);
|
expect(parsed.routing?.allowFrom).toEqual(["+15555550123"]);
|
||||||
expect(parsed.routing).toBeUndefined();
|
expect(parsed.channels).toBeUndefined();
|
||||||
expect(
|
|
||||||
warnSpy.mock.calls.some(([msg]) => String(msg).includes("Auto-migrated config")),
|
|
||||||
).toBe(true);
|
|
||||||
} finally {
|
|
||||||
warnSpy.mockRestore();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("auto-migrates claude-cli auth profile mode to oauth", async () => {
|
it("does not auto-migrate claude-cli auth profile mode on load", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
||||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||||
@@ -220,27 +210,19 @@ describe("legacy config detection", () => {
|
|||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
|
|
||||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
try {
|
const { loadConfig } = await import("./config.js");
|
||||||
const { loadConfig } = await import("./config.js");
|
const cfg = loadConfig();
|
||||||
const cfg = loadConfig();
|
expect(cfg.auth?.profiles?.["anthropic:claude-cli"]?.mode).toBe("token");
|
||||||
expect(cfg.auth?.profiles?.["anthropic:claude-cli"]?.mode).toBe("oauth");
|
|
||||||
|
|
||||||
const raw = await fs.readFile(configPath, "utf-8");
|
const raw = await fs.readFile(configPath, "utf-8");
|
||||||
const parsed = JSON.parse(raw) as {
|
const parsed = JSON.parse(raw) as {
|
||||||
auth?: { profiles?: Record<string, { mode?: string }> };
|
auth?: { profiles?: Record<string, { mode?: string }> };
|
||||||
};
|
};
|
||||||
expect(parsed.auth?.profiles?.["anthropic:claude-cli"]?.mode).toBe("oauth");
|
expect(parsed.auth?.profiles?.["anthropic:claude-cli"]?.mode).toBe("token");
|
||||||
expect(
|
|
||||||
warnSpy.mock.calls.some(([msg]) => String(msg).includes("Auto-migrated config")),
|
|
||||||
).toBe(true);
|
|
||||||
} finally {
|
|
||||||
warnSpy.mockRestore();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("auto-migrates legacy provider sections on load and writes back", async () => {
|
it("flags legacy provider sections in snapshot", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
||||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||||
@@ -250,29 +232,23 @@ describe("legacy config detection", () => {
|
|||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
|
|
||||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
try {
|
const { readConfigFileSnapshot } = await import("./config.js");
|
||||||
const { loadConfig } = await import("./config.js");
|
const snap = await readConfigFileSnapshot();
|
||||||
const cfg = loadConfig();
|
|
||||||
|
|
||||||
expect(cfg.channels?.whatsapp?.allowFrom).toEqual(["+1555"]);
|
expect(snap.valid).toBe(false);
|
||||||
const raw = await fs.readFile(configPath, "utf-8");
|
expect(snap.legacyIssues.some((issue) => issue.path === "whatsapp")).toBe(true);
|
||||||
const parsed = JSON.parse(raw) as {
|
|
||||||
channels?: { whatsapp?: { allowFrom?: string[] } };
|
const raw = await fs.readFile(configPath, "utf-8");
|
||||||
whatsapp?: unknown;
|
const parsed = JSON.parse(raw) as {
|
||||||
};
|
channels?: unknown;
|
||||||
expect(parsed.channels?.whatsapp?.allowFrom).toEqual(["+1555"]);
|
whatsapp?: unknown;
|
||||||
expect(parsed.whatsapp).toBeUndefined();
|
};
|
||||||
expect(
|
expect(parsed.channels).toBeUndefined();
|
||||||
warnSpy.mock.calls.some(([msg]) => String(msg).includes("Auto-migrated config")),
|
expect(parsed.whatsapp).toBeTruthy();
|
||||||
).toBe(true);
|
|
||||||
} finally {
|
|
||||||
warnSpy.mockRestore();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("auto-migrates routing.allowFrom on load and writes back", async () => {
|
it("flags routing.allowFrom in snapshot", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
||||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||||
@@ -282,26 +258,23 @@ describe("legacy config detection", () => {
|
|||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
|
|
||||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
try {
|
const { readConfigFileSnapshot } = await import("./config.js");
|
||||||
const { loadConfig } = await import("./config.js");
|
const snap = await readConfigFileSnapshot();
|
||||||
const cfg = loadConfig();
|
|
||||||
|
|
||||||
expect(cfg.channels?.whatsapp?.allowFrom).toEqual(["+1666"]);
|
expect(snap.valid).toBe(false);
|
||||||
const raw = await fs.readFile(configPath, "utf-8");
|
expect(snap.legacyIssues.some((issue) => issue.path === "routing.allowFrom")).toBe(true);
|
||||||
const parsed = JSON.parse(raw) as {
|
|
||||||
channels?: { whatsapp?: { allowFrom?: string[] } };
|
const raw = await fs.readFile(configPath, "utf-8");
|
||||||
routing?: unknown;
|
const parsed = JSON.parse(raw) as {
|
||||||
};
|
channels?: unknown;
|
||||||
expect(parsed.channels?.whatsapp?.allowFrom).toEqual(["+1666"]);
|
routing?: { allowFrom?: string[] };
|
||||||
expect(parsed.routing).toBeUndefined();
|
};
|
||||||
} finally {
|
expect(parsed.channels).toBeUndefined();
|
||||||
warnSpy.mockRestore();
|
expect(parsed.routing?.allowFrom).toEqual(["+1666"]);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("auto-migrates bindings[].match.provider on load and writes back", async () => {
|
it("rejects bindings[].match.provider on load", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
||||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||||
@@ -317,28 +290,21 @@ describe("legacy config detection", () => {
|
|||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
|
|
||||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
try {
|
const { readConfigFileSnapshot } = await import("./config.js");
|
||||||
const { loadConfig } = await import("./config.js");
|
const snap = await readConfigFileSnapshot();
|
||||||
const cfg = loadConfig();
|
|
||||||
expect(cfg.bindings?.[0]?.match?.channel).toBe("slack");
|
|
||||||
|
|
||||||
const raw = await fs.readFile(configPath, "utf-8");
|
expect(snap.valid).toBe(false);
|
||||||
const parsed = JSON.parse(raw) as {
|
expect(snap.issues.length).toBeGreaterThan(0);
|
||||||
bindings?: Array<{ match?: { channel?: string; provider?: string } }>;
|
|
||||||
};
|
const raw = await fs.readFile(configPath, "utf-8");
|
||||||
expect(parsed.bindings?.[0]?.match?.channel).toBe("slack");
|
const parsed = JSON.parse(raw) as {
|
||||||
expect(parsed.bindings?.[0]?.match?.provider).toBeUndefined();
|
bindings?: Array<{ match?: { provider?: string } }>;
|
||||||
expect(
|
};
|
||||||
warnSpy.mock.calls.some(([msg]) => String(msg).includes("Auto-migrated config")),
|
expect(parsed.bindings?.[0]?.match?.provider).toBe("slack");
|
||||||
).toBe(true);
|
|
||||||
} finally {
|
|
||||||
warnSpy.mockRestore();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("auto-migrates bindings[].match.accountID on load and writes back", async () => {
|
it("rejects bindings[].match.accountID on load", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
||||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||||
@@ -354,28 +320,21 @@ describe("legacy config detection", () => {
|
|||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
|
|
||||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
try {
|
const { readConfigFileSnapshot } = await import("./config.js");
|
||||||
const { loadConfig } = await import("./config.js");
|
const snap = await readConfigFileSnapshot();
|
||||||
const cfg = loadConfig();
|
|
||||||
expect(cfg.bindings?.[0]?.match?.accountId).toBe("work");
|
|
||||||
|
|
||||||
const raw = await fs.readFile(configPath, "utf-8");
|
expect(snap.valid).toBe(false);
|
||||||
const parsed = JSON.parse(raw) as {
|
expect(snap.issues.length).toBeGreaterThan(0);
|
||||||
bindings?: Array<{ match?: { accountId?: string; accountID?: string } }>;
|
|
||||||
};
|
const raw = await fs.readFile(configPath, "utf-8");
|
||||||
expect(parsed.bindings?.[0]?.match?.accountId).toBe("work");
|
const parsed = JSON.parse(raw) as {
|
||||||
expect(parsed.bindings?.[0]?.match?.accountID).toBeUndefined();
|
bindings?: Array<{ match?: { accountID?: string } }>;
|
||||||
expect(
|
};
|
||||||
warnSpy.mock.calls.some(([msg]) => String(msg).includes("Auto-migrated config")),
|
expect(parsed.bindings?.[0]?.match?.accountID).toBe("work");
|
||||||
).toBe(true);
|
|
||||||
} finally {
|
|
||||||
warnSpy.mockRestore();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("auto-migrates session.sendPolicy.rules[].match.provider on load and writes back", async () => {
|
it("rejects session.sendPolicy.rules[].match.provider on load", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
||||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||||
@@ -395,34 +354,21 @@ describe("legacy config detection", () => {
|
|||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
|
|
||||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
try {
|
const { readConfigFileSnapshot } = await import("./config.js");
|
||||||
const { loadConfig } = await import("./config.js");
|
const snap = await readConfigFileSnapshot();
|
||||||
const cfg = loadConfig();
|
|
||||||
expect(cfg.session?.sendPolicy?.rules?.[0]?.match?.channel).toBe("telegram");
|
|
||||||
|
|
||||||
const raw = await fs.readFile(configPath, "utf-8");
|
expect(snap.valid).toBe(false);
|
||||||
const parsed = JSON.parse(raw) as {
|
expect(snap.issues.length).toBeGreaterThan(0);
|
||||||
session?: {
|
|
||||||
sendPolicy?: {
|
const raw = await fs.readFile(configPath, "utf-8");
|
||||||
rules?: Array<{
|
const parsed = JSON.parse(raw) as {
|
||||||
match?: { channel?: string; provider?: string };
|
session?: { sendPolicy?: { rules?: Array<{ match?: { provider?: string } }> } };
|
||||||
}>;
|
};
|
||||||
};
|
expect(parsed.session?.sendPolicy?.rules?.[0]?.match?.provider).toBe("telegram");
|
||||||
};
|
|
||||||
};
|
|
||||||
expect(parsed.session?.sendPolicy?.rules?.[0]?.match?.channel).toBe("telegram");
|
|
||||||
expect(parsed.session?.sendPolicy?.rules?.[0]?.match?.provider).toBeUndefined();
|
|
||||||
expect(
|
|
||||||
warnSpy.mock.calls.some(([msg]) => String(msg).includes("Auto-migrated config")),
|
|
||||||
).toBe(true);
|
|
||||||
} finally {
|
|
||||||
warnSpy.mockRestore();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it("auto-migrates messages.queue.byProvider on load and writes back", async () => {
|
it("rejects messages.queue.byProvider on load", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
const configPath = path.join(home, ".clawdbot", "clawdbot.json");
|
||||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||||
@@ -432,30 +378,22 @@ describe("legacy config detection", () => {
|
|||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
|
|
||||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
try {
|
const { readConfigFileSnapshot } = await import("./config.js");
|
||||||
const { loadConfig } = await import("./config.js");
|
const snap = await readConfigFileSnapshot();
|
||||||
const cfg = loadConfig();
|
|
||||||
expect(cfg.messages?.queue?.byChannel?.whatsapp).toBe("queue");
|
|
||||||
|
|
||||||
const raw = await fs.readFile(configPath, "utf-8");
|
expect(snap.valid).toBe(false);
|
||||||
const parsed = JSON.parse(raw) as {
|
expect(snap.issues.length).toBeGreaterThan(0);
|
||||||
messages?: {
|
|
||||||
queue?: {
|
const raw = await fs.readFile(configPath, "utf-8");
|
||||||
byChannel?: Record<string, unknown>;
|
const parsed = JSON.parse(raw) as {
|
||||||
byProvider?: unknown;
|
messages?: {
|
||||||
};
|
queue?: {
|
||||||
|
byProvider?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
expect(parsed.messages?.queue?.byChannel?.whatsapp).toBe("queue");
|
};
|
||||||
expect(parsed.messages?.queue?.byProvider).toBeUndefined();
|
expect(parsed.messages?.queue?.byProvider?.whatsapp).toBe("queue");
|
||||||
expect(
|
|
||||||
warnSpy.mock.calls.some(([msg]) => String(msg).includes("Auto-migrated config")),
|
|
||||||
).toBe(true);
|
|
||||||
} finally {
|
|
||||||
warnSpy.mockRestore();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ describe("multi-agent agentDir validation", () => {
|
|||||||
{ id: "b", agentDir: "~/.clawdbot/agents/shared/agent" },
|
{ id: "b", agentDir: "~/.clawdbot/agents/shared/agent" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
bindings: [{ agentId: "a", match: { provider: "telegram" } }],
|
bindings: [{ agentId: "a", match: { channel: "telegram" } }],
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
2,
|
2,
|
||||||
|
|||||||
@@ -3,21 +3,18 @@ import path from "node:path";
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
import { withTempHome } from "./test-helpers.js";
|
import { withTempHome } from "./test-helpers.js";
|
||||||
|
|
||||||
describe("config preservation on validation failure", () => {
|
describe("config strict validation", () => {
|
||||||
it("preserves unknown fields via passthrough", async () => {
|
it("rejects unknown fields", async () => {
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
const { validateConfigObject } = await import("./config.js");
|
const { validateConfigObject } = await import("./config.js");
|
||||||
const res = validateConfigObject({
|
const res = validateConfigObject({
|
||||||
agents: { list: [{ id: "pi" }] },
|
agents: { list: [{ id: "pi" }] },
|
||||||
customUnknownField: { nested: "value" },
|
customUnknownField: { nested: "value" },
|
||||||
});
|
});
|
||||||
expect(res.ok).toBe(true);
|
expect(res.ok).toBe(false);
|
||||||
expect((res as { config: Record<string, unknown> }).config.customUnknownField).toEqual({
|
|
||||||
nested: "value",
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("preserves config data when validation fails", async () => {
|
it("flags legacy config entries without auto-migrating", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const configDir = path.join(home, ".clawdbot");
|
const configDir = path.join(home, ".clawdbot");
|
||||||
await fs.mkdir(configDir, { recursive: true });
|
await fs.mkdir(configDir, { recursive: true });
|
||||||
@@ -26,7 +23,6 @@ describe("config preservation on validation failure", () => {
|
|||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
agents: { list: [{ id: "pi" }] },
|
agents: { list: [{ id: "pi" }] },
|
||||||
routing: { allowFrom: ["+15555550123"] },
|
routing: { allowFrom: ["+15555550123"] },
|
||||||
customData: { preserved: true },
|
|
||||||
}),
|
}),
|
||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
@@ -35,12 +31,8 @@ describe("config preservation on validation failure", () => {
|
|||||||
const { readConfigFileSnapshot } = await import("./config.js");
|
const { readConfigFileSnapshot } = await import("./config.js");
|
||||||
const snap = await readConfigFileSnapshot();
|
const snap = await readConfigFileSnapshot();
|
||||||
|
|
||||||
expect(snap.valid).toBe(true);
|
expect(snap.valid).toBe(false);
|
||||||
expect(snap.legacyIssues).toHaveLength(0);
|
expect(snap.legacyIssues).not.toHaveLength(0);
|
||||||
expect((snap.config as Record<string, unknown>).customData).toEqual({
|
|
||||||
preserved: true,
|
|
||||||
});
|
|
||||||
expect(snap.config.channels?.whatsapp?.allowFrom).toEqual(["+15555550123"]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
113
src/config/io.ts
113
src/config/io.ts
@@ -24,10 +24,9 @@ import {
|
|||||||
import { VERSION } from "../version.js";
|
import { VERSION } from "../version.js";
|
||||||
import { MissingEnvVarError, resolveConfigEnvVars } from "./env-substitution.js";
|
import { MissingEnvVarError, resolveConfigEnvVars } from "./env-substitution.js";
|
||||||
import { ConfigIncludeError, resolveConfigIncludes } from "./includes.js";
|
import { ConfigIncludeError, resolveConfigIncludes } from "./includes.js";
|
||||||
import { applyLegacyMigrations, findLegacyConfigIssues } from "./legacy.js";
|
import { findLegacyConfigIssues } from "./legacy.js";
|
||||||
import { normalizeConfigPaths } from "./normalize-paths.js";
|
import { normalizeConfigPaths } from "./normalize-paths.js";
|
||||||
import { resolveConfigPath, resolveStateDir } from "./paths.js";
|
import { resolveConfigPath, resolveStateDir } from "./paths.js";
|
||||||
import { applyPluginAutoEnable } from "./plugin-auto-enable.js";
|
|
||||||
import { applyConfigOverrides } from "./runtime-overrides.js";
|
import { applyConfigOverrides } from "./runtime-overrides.js";
|
||||||
import type { ClawdbotConfig, ConfigFileSnapshot, LegacyConfigIssue } from "./types.js";
|
import type { ClawdbotConfig, ConfigFileSnapshot, LegacyConfigIssue } from "./types.js";
|
||||||
import { validateConfigObject } from "./validation.js";
|
import { validateConfigObject } from "./validation.js";
|
||||||
@@ -87,29 +86,6 @@ function coerceConfig(value: unknown): ClawdbotConfig {
|
|||||||
return value as ClawdbotConfig;
|
return value as ClawdbotConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
function rotateConfigBackupsSync(configPath: string, ioFs: typeof fs): void {
|
|
||||||
if (CONFIG_BACKUP_COUNT <= 1) return;
|
|
||||||
const backupBase = `${configPath}.bak`;
|
|
||||||
const maxIndex = CONFIG_BACKUP_COUNT - 1;
|
|
||||||
try {
|
|
||||||
ioFs.unlinkSync(`${backupBase}.${maxIndex}`);
|
|
||||||
} catch {
|
|
||||||
// best-effort
|
|
||||||
}
|
|
||||||
for (let index = maxIndex - 1; index >= 1; index -= 1) {
|
|
||||||
try {
|
|
||||||
ioFs.renameSync(`${backupBase}.${index}`, `${backupBase}.${index + 1}`);
|
|
||||||
} catch {
|
|
||||||
// best-effort
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
ioFs.renameSync(backupBase, `${backupBase}.1`);
|
|
||||||
} catch {
|
|
||||||
// best-effort
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rotateConfigBackups(configPath: string, ioFs: typeof fs.promises): Promise<void> {
|
async function rotateConfigBackups(configPath: string, ioFs: typeof fs.promises): Promise<void> {
|
||||||
if (CONFIG_BACKUP_COUNT <= 1) return;
|
if (CONFIG_BACKUP_COUNT <= 1) return;
|
||||||
const backupBase = `${configPath}.bak`;
|
const backupBase = `${configPath}.bak`;
|
||||||
@@ -147,10 +123,6 @@ function warnOnConfigMiskeys(raw: unknown, logger: Pick<typeof console, "warn">)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatLegacyMigrationLog(changes: string[]): string {
|
|
||||||
return `Auto-migrated config:\n${changes.map((entry) => `- ${entry}`).join("\n")}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function stampConfigVersion(cfg: ClawdbotConfig): ClawdbotConfig {
|
function stampConfigVersion(cfg: ClawdbotConfig): ClawdbotConfig {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
return {
|
return {
|
||||||
@@ -231,56 +203,6 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
|||||||
const deps = normalizeDeps(overrides);
|
const deps = normalizeDeps(overrides);
|
||||||
const configPath = resolveConfigPathForDeps(deps);
|
const configPath = resolveConfigPathForDeps(deps);
|
||||||
|
|
||||||
const writeConfigFileSync = (cfg: ClawdbotConfig) => {
|
|
||||||
const dir = path.dirname(configPath);
|
|
||||||
deps.fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
||||||
const json = JSON.stringify(applyModelDefaults(stampConfigVersion(cfg)), null, 2)
|
|
||||||
.trimEnd()
|
|
||||||
.concat("\n");
|
|
||||||
|
|
||||||
const tmp = path.join(
|
|
||||||
dir,
|
|
||||||
`${path.basename(configPath)}.${process.pid}.${crypto.randomUUID()}.tmp`,
|
|
||||||
);
|
|
||||||
|
|
||||||
deps.fs.writeFileSync(tmp, json, { encoding: "utf-8", mode: 0o600 });
|
|
||||||
|
|
||||||
if (deps.fs.existsSync(configPath)) {
|
|
||||||
rotateConfigBackupsSync(configPath, deps.fs);
|
|
||||||
try {
|
|
||||||
deps.fs.copyFileSync(configPath, `${configPath}.bak`);
|
|
||||||
} catch {
|
|
||||||
// best-effort
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
deps.fs.renameSync(tmp, configPath);
|
|
||||||
} catch (err) {
|
|
||||||
const code = (err as { code?: string }).code;
|
|
||||||
if (code === "EPERM" || code === "EEXIST") {
|
|
||||||
deps.fs.copyFileSync(tmp, configPath);
|
|
||||||
try {
|
|
||||||
deps.fs.chmodSync(configPath, 0o600);
|
|
||||||
} catch {
|
|
||||||
// best-effort
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
deps.fs.unlinkSync(tmp);
|
|
||||||
} catch {
|
|
||||||
// best-effort
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
deps.fs.unlinkSync(tmp);
|
|
||||||
} catch {
|
|
||||||
// best-effort
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function loadConfig(): ClawdbotConfig {
|
function loadConfig(): ClawdbotConfig {
|
||||||
try {
|
try {
|
||||||
if (!deps.fs.existsSync(configPath)) {
|
if (!deps.fs.existsSync(configPath)) {
|
||||||
@@ -307,14 +229,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
|||||||
// Substitute ${VAR} env var references
|
// Substitute ${VAR} env var references
|
||||||
const substituted = resolveConfigEnvVars(resolved, deps.env);
|
const substituted = resolveConfigEnvVars(resolved, deps.env);
|
||||||
|
|
||||||
const migrated = applyLegacyMigrations(substituted);
|
const resolvedConfig = substituted;
|
||||||
let resolvedConfig = migrated.next ?? substituted;
|
|
||||||
const autoEnable = applyPluginAutoEnable({
|
|
||||||
config: coerceConfig(resolvedConfig),
|
|
||||||
env: deps.env,
|
|
||||||
});
|
|
||||||
resolvedConfig = autoEnable.config;
|
|
||||||
const migrationChanges = [...migrated.changes, ...autoEnable.changes];
|
|
||||||
warnOnConfigMiskeys(resolvedConfig, deps.logger);
|
warnOnConfigMiskeys(resolvedConfig, deps.logger);
|
||||||
if (typeof resolvedConfig !== "object" || resolvedConfig === null) return {};
|
if (typeof resolvedConfig !== "object" || resolvedConfig === null) return {};
|
||||||
const validated = ClawdbotSchema.safeParse(resolvedConfig);
|
const validated = ClawdbotSchema.safeParse(resolvedConfig);
|
||||||
@@ -326,14 +241,6 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
warnIfConfigFromFuture(validated.data as ClawdbotConfig, deps.logger);
|
warnIfConfigFromFuture(validated.data as ClawdbotConfig, deps.logger);
|
||||||
if (migrationChanges.length > 0) {
|
|
||||||
deps.logger.warn(formatLegacyMigrationLog(migrationChanges));
|
|
||||||
try {
|
|
||||||
writeConfigFileSync(resolvedConfig as ClawdbotConfig);
|
|
||||||
} catch (err) {
|
|
||||||
deps.logger.warn(`Failed to write migrated config at ${configPath}: ${String(err)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const cfg = applyModelDefaults(
|
const cfg = applyModelDefaults(
|
||||||
applyCompactionDefaults(
|
applyCompactionDefaults(
|
||||||
applyContextPruningDefaults(
|
applyContextPruningDefaults(
|
||||||
@@ -467,14 +374,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const migrated = applyLegacyMigrations(substituted);
|
const resolvedConfigRaw = substituted;
|
||||||
let resolvedConfigRaw = migrated.next ?? substituted;
|
|
||||||
const autoEnable = applyPluginAutoEnable({
|
|
||||||
config: coerceConfig(resolvedConfigRaw),
|
|
||||||
env: deps.env,
|
|
||||||
});
|
|
||||||
resolvedConfigRaw = autoEnable.config;
|
|
||||||
const migrationChanges = [...migrated.changes, ...autoEnable.changes];
|
|
||||||
const legacyIssues = findLegacyConfigIssues(resolvedConfigRaw);
|
const legacyIssues = findLegacyConfigIssues(resolvedConfigRaw);
|
||||||
|
|
||||||
const validated = validateConfigObject(resolvedConfigRaw);
|
const validated = validateConfigObject(resolvedConfigRaw);
|
||||||
@@ -493,13 +393,6 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
warnIfConfigFromFuture(validated.config, deps.logger);
|
warnIfConfigFromFuture(validated.config, deps.logger);
|
||||||
if (migrationChanges.length > 0) {
|
|
||||||
deps.logger.warn(formatLegacyMigrationLog(migrationChanges));
|
|
||||||
await writeConfigFile(validated.config).catch((err) => {
|
|
||||||
deps.logger.warn(`Failed to write migrated config at ${configPath}: ${String(err)}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
path: configPath,
|
path: configPath,
|
||||||
exists: true,
|
exists: true,
|
||||||
|
|||||||
@@ -20,21 +20,25 @@ export const AgentDefaultsSchema = z
|
|||||||
primary: z.string().optional(),
|
primary: z.string().optional(),
|
||||||
fallbacks: z.array(z.string()).optional(),
|
fallbacks: z.array(z.string()).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
imageModel: z
|
imageModel: z
|
||||||
.object({
|
.object({
|
||||||
primary: z.string().optional(),
|
primary: z.string().optional(),
|
||||||
fallbacks: z.array(z.string()).optional(),
|
fallbacks: z.array(z.string()).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
models: z
|
models: z
|
||||||
.record(
|
.record(
|
||||||
z.string(),
|
z.string(),
|
||||||
z.object({
|
z
|
||||||
alias: z.string().optional(),
|
.object({
|
||||||
/** Provider-specific API parameters (e.g., GLM-4.7 thinking mode). */
|
alias: z.string().optional(),
|
||||||
params: z.record(z.string(), z.unknown()).optional(),
|
/** Provider-specific API parameters (e.g., GLM-4.7 thinking mode). */
|
||||||
}),
|
params: z.record(z.string(), z.unknown()).optional(),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
workspace: z.string().optional(),
|
workspace: z.string().optional(),
|
||||||
@@ -62,6 +66,7 @@ export const AgentDefaultsSchema = z
|
|||||||
allow: z.array(z.string()).optional(),
|
allow: z.array(z.string()).optional(),
|
||||||
deny: z.array(z.string()).optional(),
|
deny: z.array(z.string()).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
softTrim: z
|
softTrim: z
|
||||||
.object({
|
.object({
|
||||||
@@ -69,14 +74,17 @@ export const AgentDefaultsSchema = z
|
|||||||
headChars: z.number().int().nonnegative().optional(),
|
headChars: z.number().int().nonnegative().optional(),
|
||||||
tailChars: z.number().int().nonnegative().optional(),
|
tailChars: z.number().int().nonnegative().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
hardClear: z
|
hardClear: z
|
||||||
.object({
|
.object({
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
placeholder: z.string().optional(),
|
placeholder: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
compaction: z
|
compaction: z
|
||||||
.object({
|
.object({
|
||||||
@@ -89,8 +97,10 @@ export const AgentDefaultsSchema = z
|
|||||||
prompt: z.string().optional(),
|
prompt: z.string().optional(),
|
||||||
systemPrompt: z.string().optional(),
|
systemPrompt: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
thinkingDefault: z
|
thinkingDefault: z
|
||||||
.union([
|
.union([
|
||||||
@@ -132,10 +142,11 @@ export const AgentDefaultsSchema = z
|
|||||||
z.object({
|
z.object({
|
||||||
primary: z.string().optional(),
|
primary: z.string().optional(),
|
||||||
fallbacks: z.array(z.string()).optional(),
|
fallbacks: z.array(z.string()).optional(),
|
||||||
}),
|
}).strict(),
|
||||||
])
|
])
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
sandbox: z
|
sandbox: z
|
||||||
.object({
|
.object({
|
||||||
@@ -149,6 +160,8 @@ export const AgentDefaultsSchema = z
|
|||||||
browser: SandboxBrowserSchema,
|
browser: SandboxBrowserSchema,
|
||||||
prune: SandboxPruneSchema,
|
prune: SandboxPruneSchema,
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export const HeartbeatSchema = z
|
|||||||
prompt: z.string().optional(),
|
prompt: z.string().optional(),
|
||||||
ackMaxChars: z.number().int().nonnegative().optional(),
|
ackMaxChars: z.number().int().nonnegative().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.superRefine((val, ctx) => {
|
.superRefine((val, ctx) => {
|
||||||
if (!val.every) return;
|
if (!val.every) return;
|
||||||
try {
|
try {
|
||||||
@@ -69,7 +70,7 @@ export const SandboxDockerSchema = z
|
|||||||
z.object({
|
z.object({
|
||||||
soft: z.number().int().nonnegative().optional(),
|
soft: z.number().int().nonnegative().optional(),
|
||||||
hard: z.number().int().nonnegative().optional(),
|
hard: z.number().int().nonnegative().optional(),
|
||||||
}),
|
}).strict(),
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
@@ -79,6 +80,7 @@ export const SandboxDockerSchema = z
|
|||||||
extraHosts: z.array(z.string()).optional(),
|
extraHosts: z.array(z.string()).optional(),
|
||||||
binds: z.array(z.string()).optional(),
|
binds: z.array(z.string()).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const SandboxBrowserSchema = z
|
export const SandboxBrowserSchema = z
|
||||||
@@ -98,6 +100,7 @@ export const SandboxBrowserSchema = z
|
|||||||
autoStart: z.boolean().optional(),
|
autoStart: z.boolean().optional(),
|
||||||
autoStartTimeoutMs: z.number().int().positive().optional(),
|
autoStartTimeoutMs: z.number().int().positive().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const SandboxPruneSchema = z
|
export const SandboxPruneSchema = z
|
||||||
@@ -105,6 +108,7 @@ export const SandboxPruneSchema = z
|
|||||||
idleHours: z.number().int().nonnegative().optional(),
|
idleHours: z.number().int().nonnegative().optional(),
|
||||||
maxAgeDays: z.number().int().nonnegative().optional(),
|
maxAgeDays: z.number().int().nonnegative().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const ToolPolicySchema = z
|
export const ToolPolicySchema = z
|
||||||
@@ -112,6 +116,7 @@ export const ToolPolicySchema = z
|
|||||||
allow: z.array(z.string()).optional(),
|
allow: z.array(z.string()).optional(),
|
||||||
deny: z.array(z.string()).optional(),
|
deny: z.array(z.string()).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const ToolsWebSearchSchema = z
|
export const ToolsWebSearchSchema = z
|
||||||
@@ -123,6 +128,7 @@ export const ToolsWebSearchSchema = z
|
|||||||
timeoutSeconds: z.number().int().positive().optional(),
|
timeoutSeconds: z.number().int().positive().optional(),
|
||||||
cacheTtlMinutes: z.number().nonnegative().optional(),
|
cacheTtlMinutes: z.number().nonnegative().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const ToolsWebFetchSchema = z
|
export const ToolsWebFetchSchema = z
|
||||||
@@ -133,6 +139,7 @@ export const ToolsWebFetchSchema = z
|
|||||||
cacheTtlMinutes: z.number().nonnegative().optional(),
|
cacheTtlMinutes: z.number().nonnegative().optional(),
|
||||||
userAgent: z.string().optional(),
|
userAgent: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const ToolsWebSchema = z
|
export const ToolsWebSchema = z
|
||||||
@@ -140,17 +147,20 @@ export const ToolsWebSchema = z
|
|||||||
search: ToolsWebSearchSchema,
|
search: ToolsWebSearchSchema,
|
||||||
fetch: ToolsWebFetchSchema,
|
fetch: ToolsWebFetchSchema,
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const ToolProfileSchema = z
|
export const ToolProfileSchema = z
|
||||||
.union([z.literal("minimal"), z.literal("coding"), z.literal("messaging"), z.literal("full")])
|
.union([z.literal("minimal"), z.literal("coding"), z.literal("messaging"), z.literal("full")])
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const ToolPolicyWithProfileSchema = z.object({
|
export const ToolPolicyWithProfileSchema = z
|
||||||
allow: z.array(z.string()).optional(),
|
.object({
|
||||||
deny: z.array(z.string()).optional(),
|
allow: z.array(z.string()).optional(),
|
||||||
profile: ToolProfileSchema,
|
deny: z.array(z.string()).optional(),
|
||||||
});
|
profile: ToolProfileSchema,
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
// Provider docking: allowlists keyed by provider id (no schema updates when adding providers).
|
// Provider docking: allowlists keyed by provider id (no schema updates when adding providers).
|
||||||
export const ElevatedAllowFromSchema = z
|
export const ElevatedAllowFromSchema = z
|
||||||
@@ -169,6 +179,7 @@ export const AgentSandboxSchema = z
|
|||||||
browser: SandboxBrowserSchema,
|
browser: SandboxBrowserSchema,
|
||||||
prune: SandboxPruneSchema,
|
prune: SandboxPruneSchema,
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const AgentToolsSchema = z
|
export const AgentToolsSchema = z
|
||||||
@@ -182,6 +193,7 @@ export const AgentToolsSchema = z
|
|||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
allowFrom: ElevatedAllowFromSchema,
|
allowFrom: ElevatedAllowFromSchema,
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
exec: z
|
exec: z
|
||||||
.object({
|
.object({
|
||||||
@@ -199,15 +211,19 @@ export const AgentToolsSchema = z
|
|||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
allowModels: z.array(z.string()).optional(),
|
allowModels: z.array(z.string()).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
sandbox: z
|
sandbox: z
|
||||||
.object({
|
.object({
|
||||||
tools: ToolPolicySchema,
|
tools: ToolPolicySchema,
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const MemorySearchSchema = z
|
export const MemorySearchSchema = z
|
||||||
@@ -218,6 +234,7 @@ export const MemorySearchSchema = z
|
|||||||
.object({
|
.object({
|
||||||
sessionMemory: z.boolean().optional(),
|
sessionMemory: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
provider: z.union([z.literal("openai"), z.literal("local"), z.literal("gemini")]).optional(),
|
provider: z.union([z.literal("openai"), z.literal("local"), z.literal("gemini")]).optional(),
|
||||||
remote: z
|
remote: z
|
||||||
@@ -233,8 +250,10 @@ export const MemorySearchSchema = z
|
|||||||
pollIntervalMs: z.number().int().nonnegative().optional(),
|
pollIntervalMs: z.number().int().nonnegative().optional(),
|
||||||
timeoutMinutes: z.number().int().positive().optional(),
|
timeoutMinutes: z.number().int().positive().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
fallback: z
|
fallback: z
|
||||||
.union([z.literal("openai"), z.literal("gemini"), z.literal("local"), z.literal("none")])
|
.union([z.literal("openai"), z.literal("gemini"), z.literal("local"), z.literal("none")])
|
||||||
@@ -245,6 +264,7 @@ export const MemorySearchSchema = z
|
|||||||
modelPath: z.string().optional(),
|
modelPath: z.string().optional(),
|
||||||
modelCacheDir: z.string().optional(),
|
modelCacheDir: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
store: z
|
store: z
|
||||||
.object({
|
.object({
|
||||||
@@ -255,14 +275,17 @@ export const MemorySearchSchema = z
|
|||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
extensionPath: z.string().optional(),
|
extensionPath: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
chunking: z
|
chunking: z
|
||||||
.object({
|
.object({
|
||||||
tokens: z.number().int().positive().optional(),
|
tokens: z.number().int().positive().optional(),
|
||||||
overlap: z.number().int().nonnegative().optional(),
|
overlap: z.number().int().nonnegative().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
sync: z
|
sync: z
|
||||||
.object({
|
.object({
|
||||||
@@ -272,6 +295,7 @@ export const MemorySearchSchema = z
|
|||||||
watchDebounceMs: z.number().int().nonnegative().optional(),
|
watchDebounceMs: z.number().int().nonnegative().optional(),
|
||||||
intervalMinutes: z.number().int().nonnegative().optional(),
|
intervalMinutes: z.number().int().nonnegative().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
query: z
|
query: z
|
||||||
.object({
|
.object({
|
||||||
@@ -284,25 +308,32 @@ export const MemorySearchSchema = z
|
|||||||
textWeight: z.number().min(0).max(1).optional(),
|
textWeight: z.number().min(0).max(1).optional(),
|
||||||
candidateMultiplier: z.number().int().positive().optional(),
|
candidateMultiplier: z.number().int().positive().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
cache: z
|
cache: z
|
||||||
.object({
|
.object({
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
maxEntries: z.number().int().positive().optional(),
|
maxEntries: z.number().int().positive().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
export const AgentModelSchema = z.union([
|
export const AgentModelSchema = z.union([
|
||||||
z.string(),
|
z.string(),
|
||||||
z.object({
|
z
|
||||||
primary: z.string().optional(),
|
.object({
|
||||||
fallbacks: z.array(z.string()).optional(),
|
primary: z.string().optional(),
|
||||||
}),
|
fallbacks: z.array(z.string()).optional(),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
]);
|
]);
|
||||||
export const AgentEntrySchema = z.object({
|
export const AgentEntrySchema = z
|
||||||
|
.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
default: z.boolean().optional(),
|
default: z.boolean().optional(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
@@ -323,14 +354,16 @@ export const AgentEntrySchema = z.object({
|
|||||||
z.object({
|
z.object({
|
||||||
primary: z.string().optional(),
|
primary: z.string().optional(),
|
||||||
fallbacks: z.array(z.string()).optional(),
|
fallbacks: z.array(z.string()).optional(),
|
||||||
}),
|
}).strict(),
|
||||||
])
|
])
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
sandbox: AgentSandboxSchema,
|
sandbox: AgentSandboxSchema,
|
||||||
tools: AgentToolsSchema,
|
tools: AgentToolsSchema,
|
||||||
});
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const ToolsSchema = z
|
export const ToolsSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -353,27 +386,33 @@ export const ToolsSchema = z
|
|||||||
prefix: z.string().optional(),
|
prefix: z.string().optional(),
|
||||||
suffix: z.string().optional(),
|
suffix: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
broadcast: z
|
broadcast: z
|
||||||
.object({
|
.object({
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
agentToAgent: z
|
agentToAgent: z
|
||||||
.object({
|
.object({
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
allow: z.array(z.string()).optional(),
|
allow: z.array(z.string()).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
elevated: z
|
elevated: z
|
||||||
.object({
|
.object({
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
allowFrom: ElevatedAllowFromSchema,
|
allowFrom: ElevatedAllowFromSchema,
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
exec: z
|
exec: z
|
||||||
.object({
|
.object({
|
||||||
@@ -391,18 +430,23 @@ export const ToolsSchema = z
|
|||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
allowModels: z.array(z.string()).optional(),
|
allowModels: z.array(z.string()).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
subagents: z
|
subagents: z
|
||||||
.object({
|
.object({
|
||||||
tools: ToolPolicySchema,
|
tools: ToolPolicySchema,
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
sandbox: z
|
sandbox: z
|
||||||
.object({
|
.object({
|
||||||
tools: ToolPolicySchema,
|
tools: ToolPolicySchema,
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|||||||
@@ -8,25 +8,31 @@ export const AgentsSchema = z
|
|||||||
defaults: z.lazy(() => AgentDefaultsSchema).optional(),
|
defaults: z.lazy(() => AgentDefaultsSchema).optional(),
|
||||||
list: z.array(AgentEntrySchema).optional(),
|
list: z.array(AgentEntrySchema).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const BindingsSchema = z
|
export const BindingsSchema = z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z
|
||||||
agentId: z.string(),
|
.object({
|
||||||
match: z.object({
|
agentId: z.string(),
|
||||||
channel: z.string(),
|
match: z
|
||||||
accountId: z.string().optional(),
|
|
||||||
peer: z
|
|
||||||
.object({
|
.object({
|
||||||
kind: z.union([z.literal("dm"), z.literal("group"), z.literal("channel")]),
|
channel: z.string(),
|
||||||
id: z.string(),
|
accountId: z.string().optional(),
|
||||||
|
peer: z
|
||||||
|
.object({
|
||||||
|
kind: z.union([z.literal("dm"), z.literal("group"), z.literal("channel")]),
|
||||||
|
id: z.string(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
.optional(),
|
||||||
|
guildId: z.string().optional(),
|
||||||
|
teamId: z.string().optional(),
|
||||||
})
|
})
|
||||||
.optional(),
|
.strict(),
|
||||||
guildId: z.string().optional(),
|
})
|
||||||
teamId: z.string().optional(),
|
.strict(),
|
||||||
}),
|
|
||||||
}),
|
|
||||||
)
|
)
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
@@ -43,4 +49,5 @@ export const AudioSchema = z
|
|||||||
.object({
|
.object({
|
||||||
transcription: TranscribeAudioSchema,
|
transcription: TranscribeAudioSchema,
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|||||||
@@ -19,40 +19,48 @@ export const ModelCompatSchema = z
|
|||||||
.union([z.literal("max_completion_tokens"), z.literal("max_tokens")])
|
.union([z.literal("max_completion_tokens"), z.literal("max_tokens")])
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const ModelDefinitionSchema = z.object({
|
export const ModelDefinitionSchema = z
|
||||||
|
.object({
|
||||||
id: z.string().min(1),
|
id: z.string().min(1),
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
api: ModelApiSchema.optional(),
|
api: ModelApiSchema.optional(),
|
||||||
reasoning: z.boolean(),
|
reasoning: z.boolean(),
|
||||||
input: z.array(z.union([z.literal("text"), z.literal("image")])),
|
input: z.array(z.union([z.literal("text"), z.literal("image")])),
|
||||||
cost: z.object({
|
cost: z
|
||||||
input: z.number(),
|
.object({
|
||||||
output: z.number(),
|
input: z.number(),
|
||||||
cacheRead: z.number(),
|
output: z.number(),
|
||||||
cacheWrite: z.number(),
|
cacheRead: z.number(),
|
||||||
}),
|
cacheWrite: z.number(),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
contextWindow: z.number().positive(),
|
contextWindow: z.number().positive(),
|
||||||
maxTokens: z.number().positive(),
|
maxTokens: z.number().positive(),
|
||||||
headers: z.record(z.string(), z.string()).optional(),
|
headers: z.record(z.string(), z.string()).optional(),
|
||||||
compat: ModelCompatSchema,
|
compat: ModelCompatSchema,
|
||||||
});
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const ModelProviderSchema = z.object({
|
export const ModelProviderSchema = z
|
||||||
|
.object({
|
||||||
baseUrl: z.string().min(1),
|
baseUrl: z.string().min(1),
|
||||||
apiKey: z.string().optional(),
|
apiKey: z.string().optional(),
|
||||||
api: ModelApiSchema.optional(),
|
api: ModelApiSchema.optional(),
|
||||||
headers: z.record(z.string(), z.string()).optional(),
|
headers: z.record(z.string(), z.string()).optional(),
|
||||||
authHeader: z.boolean().optional(),
|
authHeader: z.boolean().optional(),
|
||||||
models: z.array(ModelDefinitionSchema),
|
models: z.array(ModelDefinitionSchema),
|
||||||
});
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const ModelsConfigSchema = z
|
export const ModelsConfigSchema = z
|
||||||
.object({
|
.object({
|
||||||
mode: z.union([z.literal("merge"), z.literal("replace")]).optional(),
|
mode: z.union([z.literal("merge"), z.literal("replace")]).optional(),
|
||||||
providers: z.record(z.string(), ModelProviderSchema).optional(),
|
providers: z.record(z.string(), ModelProviderSchema).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const GroupChatSchema = z
|
export const GroupChatSchema = z
|
||||||
@@ -60,11 +68,14 @@ export const GroupChatSchema = z
|
|||||||
mentionPatterns: z.array(z.string()).optional(),
|
mentionPatterns: z.array(z.string()).optional(),
|
||||||
historyLimit: z.number().int().positive().optional(),
|
historyLimit: z.number().int().positive().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const DmConfigSchema = z.object({
|
export const DmConfigSchema = z
|
||||||
historyLimit: z.number().int().min(0).optional(),
|
.object({
|
||||||
});
|
historyLimit: z.number().int().min(0).optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const IdentitySchema = z
|
export const IdentitySchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -72,6 +83,7 @@ export const IdentitySchema = z
|
|||||||
theme: z.string().optional(),
|
theme: z.string().optional(),
|
||||||
emoji: z.string().optional(),
|
emoji: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const QueueModeSchema = z.union([
|
export const QueueModeSchema = z.union([
|
||||||
@@ -98,51 +110,59 @@ export const GroupPolicySchema = z.enum(["open", "disabled", "allowlist"]);
|
|||||||
|
|
||||||
export const DmPolicySchema = z.enum(["pairing", "allowlist", "open", "disabled"]);
|
export const DmPolicySchema = z.enum(["pairing", "allowlist", "open", "disabled"]);
|
||||||
|
|
||||||
export const BlockStreamingCoalesceSchema = z.object({
|
export const BlockStreamingCoalesceSchema = z
|
||||||
minChars: z.number().int().positive().optional(),
|
.object({
|
||||||
maxChars: z.number().int().positive().optional(),
|
minChars: z.number().int().positive().optional(),
|
||||||
idleMs: z.number().int().nonnegative().optional(),
|
maxChars: z.number().int().positive().optional(),
|
||||||
});
|
idleMs: z.number().int().nonnegative().optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const BlockStreamingChunkSchema = z.object({
|
export const BlockStreamingChunkSchema = z
|
||||||
minChars: z.number().int().positive().optional(),
|
.object({
|
||||||
maxChars: z.number().int().positive().optional(),
|
minChars: z.number().int().positive().optional(),
|
||||||
breakPreference: z
|
maxChars: z.number().int().positive().optional(),
|
||||||
.union([z.literal("paragraph"), z.literal("newline"), z.literal("sentence")])
|
breakPreference: z
|
||||||
.optional(),
|
.union([z.literal("paragraph"), z.literal("newline"), z.literal("sentence")])
|
||||||
});
|
.optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const HumanDelaySchema = z.object({
|
export const HumanDelaySchema = z
|
||||||
mode: z.union([z.literal("off"), z.literal("natural"), z.literal("custom")]).optional(),
|
.object({
|
||||||
minMs: z.number().int().nonnegative().optional(),
|
mode: z.union([z.literal("off"), z.literal("natural"), z.literal("custom")]).optional(),
|
||||||
maxMs: z.number().int().nonnegative().optional(),
|
minMs: z.number().int().nonnegative().optional(),
|
||||||
});
|
maxMs: z.number().int().nonnegative().optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const CliBackendSchema = z.object({
|
export const CliBackendSchema = z
|
||||||
command: z.string(),
|
.object({
|
||||||
args: z.array(z.string()).optional(),
|
command: z.string(),
|
||||||
output: z.union([z.literal("json"), z.literal("text"), z.literal("jsonl")]).optional(),
|
args: z.array(z.string()).optional(),
|
||||||
resumeOutput: z.union([z.literal("json"), z.literal("text"), z.literal("jsonl")]).optional(),
|
output: z.union([z.literal("json"), z.literal("text"), z.literal("jsonl")]).optional(),
|
||||||
input: z.union([z.literal("arg"), z.literal("stdin")]).optional(),
|
resumeOutput: z.union([z.literal("json"), z.literal("text"), z.literal("jsonl")]).optional(),
|
||||||
maxPromptArgChars: z.number().int().positive().optional(),
|
input: z.union([z.literal("arg"), z.literal("stdin")]).optional(),
|
||||||
env: z.record(z.string(), z.string()).optional(),
|
maxPromptArgChars: z.number().int().positive().optional(),
|
||||||
clearEnv: z.array(z.string()).optional(),
|
env: z.record(z.string(), z.string()).optional(),
|
||||||
modelArg: z.string().optional(),
|
clearEnv: z.array(z.string()).optional(),
|
||||||
modelAliases: z.record(z.string(), z.string()).optional(),
|
modelArg: z.string().optional(),
|
||||||
sessionArg: z.string().optional(),
|
modelAliases: z.record(z.string(), z.string()).optional(),
|
||||||
sessionArgs: z.array(z.string()).optional(),
|
sessionArg: z.string().optional(),
|
||||||
resumeArgs: z.array(z.string()).optional(),
|
sessionArgs: z.array(z.string()).optional(),
|
||||||
sessionMode: z.union([z.literal("always"), z.literal("existing"), z.literal("none")]).optional(),
|
resumeArgs: z.array(z.string()).optional(),
|
||||||
sessionIdFields: z.array(z.string()).optional(),
|
sessionMode: z.union([z.literal("always"), z.literal("existing"), z.literal("none")]).optional(),
|
||||||
systemPromptArg: z.string().optional(),
|
sessionIdFields: z.array(z.string()).optional(),
|
||||||
systemPromptMode: z.union([z.literal("append"), z.literal("replace")]).optional(),
|
systemPromptArg: z.string().optional(),
|
||||||
systemPromptWhen: z
|
systemPromptMode: z.union([z.literal("append"), z.literal("replace")]).optional(),
|
||||||
.union([z.literal("first"), z.literal("always"), z.literal("never")])
|
systemPromptWhen: z
|
||||||
.optional(),
|
.union([z.literal("first"), z.literal("always"), z.literal("never")])
|
||||||
imageArg: z.string().optional(),
|
.optional(),
|
||||||
imageMode: z.union([z.literal("repeat"), z.literal("list")]).optional(),
|
imageArg: z.string().optional(),
|
||||||
serialize: z.boolean().optional(),
|
imageMode: z.union([z.literal("repeat"), z.literal("list")]).optional(),
|
||||||
});
|
serialize: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const normalizeAllowFrom = (values?: Array<string | number>): string[] =>
|
export const normalizeAllowFrom = (values?: Array<string | number>): string[] =>
|
||||||
(values ?? []).map((v) => String(v).trim()).filter(Boolean);
|
(values ?? []).map((v) => String(v).trim()).filter(Boolean);
|
||||||
@@ -173,6 +193,7 @@ export const RetryConfigSchema = z
|
|||||||
maxDelayMs: z.number().int().min(0).optional(),
|
maxDelayMs: z.number().int().min(0).optional(),
|
||||||
jitter: z.number().min(0).max(1).optional(),
|
jitter: z.number().min(0).max(1).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const QueueModeBySurfaceSchema = z
|
export const QueueModeBySurfaceSchema = z
|
||||||
@@ -186,6 +207,7 @@ export const QueueModeBySurfaceSchema = z
|
|||||||
msteams: QueueModeSchema.optional(),
|
msteams: QueueModeSchema.optional(),
|
||||||
webchat: QueueModeSchema.optional(),
|
webchat: QueueModeSchema.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const DebounceMsBySurfaceSchema = z
|
export const DebounceMsBySurfaceSchema = z
|
||||||
@@ -199,6 +221,7 @@ export const DebounceMsBySurfaceSchema = z
|
|||||||
msteams: z.number().int().nonnegative().optional(),
|
msteams: z.number().int().nonnegative().optional(),
|
||||||
webchat: z.number().int().nonnegative().optional(),
|
webchat: z.number().int().nonnegative().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const QueueSchema = z
|
export const QueueSchema = z
|
||||||
@@ -209,6 +232,7 @@ export const QueueSchema = z
|
|||||||
cap: z.number().int().positive().optional(),
|
cap: z.number().int().positive().optional(),
|
||||||
drop: QueueDropSchema.optional(),
|
drop: QueueDropSchema.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const InboundDebounceSchema = z
|
export const InboundDebounceSchema = z
|
||||||
@@ -216,6 +240,7 @@ export const InboundDebounceSchema = z
|
|||||||
debounceMs: z.number().int().nonnegative().optional(),
|
debounceMs: z.number().int().nonnegative().optional(),
|
||||||
byChannel: DebounceMsBySurfaceSchema,
|
byChannel: DebounceMsBySurfaceSchema,
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const TranscribeAudioSchema = z
|
export const TranscribeAudioSchema = z
|
||||||
@@ -232,6 +257,7 @@ export const TranscribeAudioSchema = z
|
|||||||
}),
|
}),
|
||||||
timeoutSeconds: z.number().int().positive().optional(),
|
timeoutSeconds: z.number().int().positive().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const HexColorSchema = z.string().regex(/^#?[0-9a-fA-F]{6}$/, "expected hex color (RRGGBB)");
|
export const HexColorSchema = z.string().regex(/^#?[0-9a-fA-F]{6}$/, "expected hex color (RRGGBB)");
|
||||||
@@ -245,21 +271,25 @@ export const MediaUnderstandingScopeSchema = z
|
|||||||
default: z.union([z.literal("allow"), z.literal("deny")]).optional(),
|
default: z.union([z.literal("allow"), z.literal("deny")]).optional(),
|
||||||
rules: z
|
rules: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z
|
||||||
action: z.union([z.literal("allow"), z.literal("deny")]),
|
.object({
|
||||||
match: z
|
action: z.union([z.literal("allow"), z.literal("deny")]),
|
||||||
.object({
|
match: z
|
||||||
channel: z.string().optional(),
|
.object({
|
||||||
chatType: z
|
channel: z.string().optional(),
|
||||||
.union([z.literal("direct"), z.literal("group"), z.literal("channel")])
|
chatType: z
|
||||||
.optional(),
|
.union([z.literal("direct"), z.literal("group"), z.literal("channel")])
|
||||||
keyPrefix: z.string().optional(),
|
.optional(),
|
||||||
})
|
keyPrefix: z.string().optional(),
|
||||||
.optional(),
|
})
|
||||||
}),
|
.strict()
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const MediaUnderstandingCapabilitiesSchema = z
|
export const MediaUnderstandingCapabilitiesSchema = z
|
||||||
@@ -274,6 +304,7 @@ export const MediaUnderstandingAttachmentsSchema = z
|
|||||||
.union([z.literal("first"), z.literal("last"), z.literal("path"), z.literal("url")])
|
.union([z.literal("first"), z.literal("last"), z.literal("path"), z.literal("url")])
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
const DeepgramAudioSchema = z
|
const DeepgramAudioSchema = z
|
||||||
@@ -282,6 +313,7 @@ const DeepgramAudioSchema = z
|
|||||||
punctuate: z.boolean().optional(),
|
punctuate: z.boolean().optional(),
|
||||||
smartFormat: z.boolean().optional(),
|
smartFormat: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
const ProviderOptionValueSchema = z.union([z.string(), z.number(), z.boolean()]);
|
const ProviderOptionValueSchema = z.union([z.string(), z.number(), z.boolean()]);
|
||||||
@@ -309,6 +341,7 @@ export const MediaUnderstandingModelSchema = z
|
|||||||
profile: z.string().optional(),
|
profile: z.string().optional(),
|
||||||
preferredProfile: z.string().optional(),
|
preferredProfile: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const ToolsMediaUnderstandingSchema = z
|
export const ToolsMediaUnderstandingSchema = z
|
||||||
@@ -327,6 +360,7 @@ export const ToolsMediaUnderstandingSchema = z
|
|||||||
attachments: MediaUnderstandingAttachmentsSchema,
|
attachments: MediaUnderstandingAttachmentsSchema,
|
||||||
models: z.array(MediaUnderstandingModelSchema).optional(),
|
models: z.array(MediaUnderstandingModelSchema).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const ToolsMediaSchema = z
|
export const ToolsMediaSchema = z
|
||||||
@@ -337,6 +371,7 @@ export const ToolsMediaSchema = z
|
|||||||
audio: ToolsMediaUnderstandingSchema.optional(),
|
audio: ToolsMediaUnderstandingSchema.optional(),
|
||||||
video: ToolsMediaUnderstandingSchema.optional(),
|
video: ToolsMediaUnderstandingSchema.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const NativeCommandsSettingSchema = z.union([z.boolean(), z.literal("auto")]);
|
export const NativeCommandsSettingSchema = z.union([z.boolean(), z.literal("auto")]);
|
||||||
@@ -346,4 +381,5 @@ export const ProviderCommandsSchema = z
|
|||||||
native: NativeCommandsSettingSchema.optional(),
|
native: NativeCommandsSettingSchema.optional(),
|
||||||
nativeSkills: NativeCommandsSettingSchema.optional(),
|
nativeSkills: NativeCommandsSettingSchema.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|||||||
@@ -37,22 +37,26 @@ export const HookMappingSchema = z
|
|||||||
module: z.string(),
|
module: z.string(),
|
||||||
export: z.string().optional(),
|
export: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const InternalHookHandlerSchema = z.object({
|
export const InternalHookHandlerSchema = z
|
||||||
event: z.string(),
|
.object({
|
||||||
module: z.string(),
|
event: z.string(),
|
||||||
export: z.string().optional(),
|
module: z.string(),
|
||||||
});
|
export: z.string().optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
const HookConfigSchema = z
|
const HookConfigSchema = z
|
||||||
.object({
|
.object({
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
env: z.record(z.string(), z.string()).optional(),
|
env: z.record(z.string(), z.string()).optional(),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.strict();
|
||||||
|
|
||||||
const HookInstallRecordSchema = z
|
const HookInstallRecordSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -64,7 +68,7 @@ const HookInstallRecordSchema = z
|
|||||||
installedAt: z.string().optional(),
|
installedAt: z.string().optional(),
|
||||||
hooks: z.array(z.string()).optional(),
|
hooks: z.array(z.string()).optional(),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.strict();
|
||||||
|
|
||||||
export const InternalHooksSchema = z
|
export const InternalHooksSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -75,9 +79,11 @@ export const InternalHooksSchema = z
|
|||||||
.object({
|
.object({
|
||||||
extraDirs: z.array(z.string()).optional(),
|
extraDirs: z.array(z.string()).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
installs: z.record(z.string(), HookInstallRecordSchema).optional(),
|
installs: z.record(z.string(), HookInstallRecordSchema).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const HooksGmailSchema = z
|
export const HooksGmailSchema = z
|
||||||
@@ -97,6 +103,7 @@ export const HooksGmailSchema = z
|
|||||||
port: z.number().int().positive().optional(),
|
port: z.number().int().positive().optional(),
|
||||||
path: z.string().optional(),
|
path: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
tailscale: z
|
tailscale: z
|
||||||
.object({
|
.object({
|
||||||
@@ -104,6 +111,7 @@ export const HooksGmailSchema = z
|
|||||||
path: z.string().optional(),
|
path: z.string().optional(),
|
||||||
target: z.string().optional(),
|
target: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
model: z.string().optional(),
|
model: z.string().optional(),
|
||||||
thinking: z
|
thinking: z
|
||||||
@@ -116,4 +124,5 @@ export const HooksGmailSchema = z
|
|||||||
])
|
])
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|||||||
@@ -23,32 +23,40 @@ const TelegramInlineButtonsScopeSchema = z.enum(["off", "dm", "group", "all", "a
|
|||||||
|
|
||||||
const TelegramCapabilitiesSchema = z.union([
|
const TelegramCapabilitiesSchema = z.union([
|
||||||
z.array(z.string()),
|
z.array(z.string()),
|
||||||
z.object({
|
z
|
||||||
inlineButtons: TelegramInlineButtonsScopeSchema.optional(),
|
.object({
|
||||||
}),
|
inlineButtons: TelegramInlineButtonsScopeSchema.optional(),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const TelegramTopicSchema = z.object({
|
export const TelegramTopicSchema = z
|
||||||
requireMention: z.boolean().optional(),
|
.object({
|
||||||
skills: z.array(z.string()).optional(),
|
requireMention: z.boolean().optional(),
|
||||||
enabled: z.boolean().optional(),
|
skills: z.array(z.string()).optional(),
|
||||||
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
enabled: z.boolean().optional(),
|
||||||
systemPrompt: z.string().optional(),
|
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||||
});
|
systemPrompt: z.string().optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const TelegramGroupSchema = z.object({
|
export const TelegramGroupSchema = z
|
||||||
requireMention: z.boolean().optional(),
|
.object({
|
||||||
skills: z.array(z.string()).optional(),
|
requireMention: z.boolean().optional(),
|
||||||
enabled: z.boolean().optional(),
|
skills: z.array(z.string()).optional(),
|
||||||
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
enabled: z.boolean().optional(),
|
||||||
systemPrompt: z.string().optional(),
|
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||||
topics: z.record(z.string(), TelegramTopicSchema.optional()).optional(),
|
systemPrompt: z.string().optional(),
|
||||||
});
|
topics: z.record(z.string(), TelegramTopicSchema.optional()).optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
const TelegramCustomCommandSchema = z.object({
|
const TelegramCustomCommandSchema = z
|
||||||
command: z.string().transform(normalizeTelegramCommandName),
|
.object({
|
||||||
description: z.string().transform(normalizeTelegramCommandDescription),
|
command: z.string().transform(normalizeTelegramCommandName),
|
||||||
});
|
description: z.string().transform(normalizeTelegramCommandDescription),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
const validateTelegramCustomCommands = (
|
const validateTelegramCustomCommands = (
|
||||||
value: { customCommands?: Array<{ command?: string; description?: string }> },
|
value: { customCommands?: Array<{ command?: string; description?: string }> },
|
||||||
@@ -69,7 +77,8 @@ const validateTelegramCustomCommands = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TelegramAccountSchemaBase = z.object({
|
export const TelegramAccountSchemaBase = z
|
||||||
|
.object({
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
capabilities: TelegramCapabilitiesSchema.optional(),
|
capabilities: TelegramCapabilitiesSchema.optional(),
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
@@ -105,10 +114,12 @@ export const TelegramAccountSchemaBase = z.object({
|
|||||||
sendMessage: z.boolean().optional(),
|
sendMessage: z.boolean().optional(),
|
||||||
deleteMessage: z.boolean().optional(),
|
deleteMessage: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
reactionNotifications: z.enum(["off", "own", "all"]).optional(),
|
reactionNotifications: z.enum(["off", "own", "all"]).optional(),
|
||||||
reactionLevel: z.enum(["off", "ack", "minimal", "extensive"]).optional(),
|
reactionLevel: z.enum(["off", "ack", "minimal", "extensive"]).optional(),
|
||||||
});
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const TelegramAccountSchema = TelegramAccountSchemaBase.superRefine((value, ctx) => {
|
export const TelegramAccountSchema = TelegramAccountSchemaBase.superRefine((value, ctx) => {
|
||||||
requireOpenAllowFrom({
|
requireOpenAllowFrom({
|
||||||
@@ -144,6 +155,7 @@ export const DiscordDmSchema = z
|
|||||||
groupEnabled: z.boolean().optional(),
|
groupEnabled: z.boolean().optional(),
|
||||||
groupChannels: z.array(z.union([z.string(), z.number()])).optional(),
|
groupChannels: z.array(z.union([z.string(), z.number()])).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.superRefine((value, ctx) => {
|
.superRefine((value, ctx) => {
|
||||||
requireOpenAllowFrom({
|
requireOpenAllowFrom({
|
||||||
policy: value.policy,
|
policy: value.policy,
|
||||||
@@ -155,25 +167,30 @@ export const DiscordDmSchema = z
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export const DiscordGuildChannelSchema = z.object({
|
export const DiscordGuildChannelSchema = z
|
||||||
allow: z.boolean().optional(),
|
.object({
|
||||||
requireMention: z.boolean().optional(),
|
allow: z.boolean().optional(),
|
||||||
skills: z.array(z.string()).optional(),
|
requireMention: z.boolean().optional(),
|
||||||
enabled: z.boolean().optional(),
|
skills: z.array(z.string()).optional(),
|
||||||
users: z.array(z.union([z.string(), z.number()])).optional(),
|
enabled: z.boolean().optional(),
|
||||||
systemPrompt: z.string().optional(),
|
users: z.array(z.union([z.string(), z.number()])).optional(),
|
||||||
autoThread: z.boolean().optional(),
|
systemPrompt: z.string().optional(),
|
||||||
});
|
autoThread: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const DiscordGuildSchema = z.object({
|
export const DiscordGuildSchema = z
|
||||||
slug: z.string().optional(),
|
.object({
|
||||||
requireMention: z.boolean().optional(),
|
slug: z.string().optional(),
|
||||||
reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(),
|
requireMention: z.boolean().optional(),
|
||||||
users: z.array(z.union([z.string(), z.number()])).optional(),
|
reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(),
|
||||||
channels: z.record(z.string(), DiscordGuildChannelSchema.optional()).optional(),
|
users: z.array(z.union([z.string(), z.number()])).optional(),
|
||||||
});
|
channels: z.record(z.string(), DiscordGuildChannelSchema.optional()).optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const DiscordAccountSchema = z.object({
|
export const DiscordAccountSchema = z
|
||||||
|
.object({
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
capabilities: z.array(z.string()).optional(),
|
capabilities: z.array(z.string()).optional(),
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
@@ -212,11 +229,13 @@ export const DiscordAccountSchema = z.object({
|
|||||||
moderation: z.boolean().optional(),
|
moderation: z.boolean().optional(),
|
||||||
channels: z.boolean().optional(),
|
channels: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
replyToMode: ReplyToModeSchema.optional(),
|
replyToMode: ReplyToModeSchema.optional(),
|
||||||
dm: DiscordDmSchema.optional(),
|
dm: DiscordDmSchema.optional(),
|
||||||
guilds: z.record(z.string(), DiscordGuildSchema.optional()).optional(),
|
guilds: z.record(z.string(), DiscordGuildSchema.optional()).optional(),
|
||||||
});
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const DiscordConfigSchema = DiscordAccountSchema.extend({
|
export const DiscordConfigSchema = DiscordAccountSchema.extend({
|
||||||
accounts: z.record(z.string(), DiscordAccountSchema.optional()).optional(),
|
accounts: z.record(z.string(), DiscordAccountSchema.optional()).optional(),
|
||||||
@@ -230,6 +249,7 @@ export const SlackDmSchema = z
|
|||||||
groupEnabled: z.boolean().optional(),
|
groupEnabled: z.boolean().optional(),
|
||||||
groupChannels: z.array(z.union([z.string(), z.number()])).optional(),
|
groupChannels: z.array(z.union([z.string(), z.number()])).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.superRefine((value, ctx) => {
|
.superRefine((value, ctx) => {
|
||||||
requireOpenAllowFrom({
|
requireOpenAllowFrom({
|
||||||
policy: value.policy,
|
policy: value.policy,
|
||||||
@@ -241,22 +261,27 @@ export const SlackDmSchema = z
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SlackChannelSchema = z.object({
|
export const SlackChannelSchema = z
|
||||||
enabled: z.boolean().optional(),
|
.object({
|
||||||
allow: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
requireMention: z.boolean().optional(),
|
allow: z.boolean().optional(),
|
||||||
allowBots: z.boolean().optional(),
|
requireMention: z.boolean().optional(),
|
||||||
users: z.array(z.union([z.string(), z.number()])).optional(),
|
allowBots: z.boolean().optional(),
|
||||||
skills: z.array(z.string()).optional(),
|
users: z.array(z.union([z.string(), z.number()])).optional(),
|
||||||
systemPrompt: z.string().optional(),
|
skills: z.array(z.string()).optional(),
|
||||||
});
|
systemPrompt: z.string().optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const SlackThreadSchema = z.object({
|
export const SlackThreadSchema = z
|
||||||
historyScope: z.enum(["thread", "channel"]).optional(),
|
.object({
|
||||||
inheritParent: z.boolean().optional(),
|
historyScope: z.enum(["thread", "channel"]).optional(),
|
||||||
});
|
inheritParent: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const SlackAccountSchema = z.object({
|
export const SlackAccountSchema = z
|
||||||
|
.object({
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
mode: z.enum(["socket", "http"]).optional(),
|
mode: z.enum(["socket", "http"]).optional(),
|
||||||
signingSecret: z.string().optional(),
|
signingSecret: z.string().optional(),
|
||||||
@@ -294,6 +319,7 @@ export const SlackAccountSchema = z.object({
|
|||||||
channelInfo: z.boolean().optional(),
|
channelInfo: z.boolean().optional(),
|
||||||
emojiList: z.boolean().optional(),
|
emojiList: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
slashCommand: z
|
slashCommand: z
|
||||||
.object({
|
.object({
|
||||||
@@ -302,10 +328,12 @@ export const SlackAccountSchema = z.object({
|
|||||||
sessionPrefix: z.string().optional(),
|
sessionPrefix: z.string().optional(),
|
||||||
ephemeral: z.boolean().optional(),
|
ephemeral: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
dm: SlackDmSchema.optional(),
|
dm: SlackDmSchema.optional(),
|
||||||
channels: z.record(z.string(), SlackChannelSchema.optional()).optional(),
|
channels: z.record(z.string(), SlackChannelSchema.optional()).optional(),
|
||||||
});
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const SlackConfigSchema = SlackAccountSchema.extend({
|
export const SlackConfigSchema = SlackAccountSchema.extend({
|
||||||
mode: z.enum(["socket", "http"]).optional().default("socket"),
|
mode: z.enum(["socket", "http"]).optional().default("socket"),
|
||||||
@@ -339,7 +367,8 @@ export const SlackConfigSchema = SlackAccountSchema.extend({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SignalAccountSchemaBase = z.object({
|
export const SignalAccountSchemaBase = z
|
||||||
|
.object({
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
capabilities: z.array(z.string()).optional(),
|
capabilities: z.array(z.string()).optional(),
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
@@ -367,7 +396,8 @@ export const SignalAccountSchemaBase = z.object({
|
|||||||
mediaMaxMb: z.number().int().positive().optional(),
|
mediaMaxMb: z.number().int().positive().optional(),
|
||||||
reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(),
|
reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(),
|
||||||
reactionAllowlist: z.array(z.union([z.string(), z.number()])).optional(),
|
reactionAllowlist: z.array(z.union([z.string(), z.number()])).optional(),
|
||||||
});
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const SignalAccountSchema = SignalAccountSchemaBase.superRefine((value, ctx) => {
|
export const SignalAccountSchema = SignalAccountSchemaBase.superRefine((value, ctx) => {
|
||||||
requireOpenAllowFrom({
|
requireOpenAllowFrom({
|
||||||
@@ -391,7 +421,8 @@ export const SignalConfigSchema = SignalAccountSchemaBase.extend({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export const IMessageAccountSchemaBase = z.object({
|
export const IMessageAccountSchemaBase = z
|
||||||
|
.object({
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
capabilities: z.array(z.string()).optional(),
|
capabilities: z.array(z.string()).optional(),
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
@@ -420,10 +451,12 @@ export const IMessageAccountSchemaBase = z.object({
|
|||||||
.object({
|
.object({
|
||||||
requireMention: z.boolean().optional(),
|
requireMention: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
});
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const IMessageAccountSchema = IMessageAccountSchemaBase.superRefine((value, ctx) => {
|
export const IMessageAccountSchema = IMessageAccountSchemaBase.superRefine((value, ctx) => {
|
||||||
requireOpenAllowFrom({
|
requireOpenAllowFrom({
|
||||||
@@ -449,16 +482,20 @@ export const IMessageConfigSchema = IMessageAccountSchemaBase.extend({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export const MSTeamsChannelSchema = z.object({
|
export const MSTeamsChannelSchema = z
|
||||||
requireMention: z.boolean().optional(),
|
.object({
|
||||||
replyStyle: MSTeamsReplyStyleSchema.optional(),
|
requireMention: z.boolean().optional(),
|
||||||
});
|
replyStyle: MSTeamsReplyStyleSchema.optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const MSTeamsTeamSchema = z.object({
|
export const MSTeamsTeamSchema = z
|
||||||
requireMention: z.boolean().optional(),
|
.object({
|
||||||
replyStyle: MSTeamsReplyStyleSchema.optional(),
|
requireMention: z.boolean().optional(),
|
||||||
channels: z.record(z.string(), MSTeamsChannelSchema.optional()).optional(),
|
replyStyle: MSTeamsReplyStyleSchema.optional(),
|
||||||
});
|
channels: z.record(z.string(), MSTeamsChannelSchema.optional()).optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const MSTeamsConfigSchema = z
|
export const MSTeamsConfigSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -473,6 +510,7 @@ export const MSTeamsConfigSchema = z
|
|||||||
port: z.number().int().positive().optional(),
|
port: z.number().int().positive().optional(),
|
||||||
path: z.string().optional(),
|
path: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
dmPolicy: DmPolicySchema.optional().default("pairing"),
|
||||||
allowFrom: z.array(z.string()).optional(),
|
allowFrom: z.array(z.string()).optional(),
|
||||||
@@ -488,6 +526,7 @@ export const MSTeamsConfigSchema = z
|
|||||||
replyStyle: MSTeamsReplyStyleSchema.optional(),
|
replyStyle: MSTeamsReplyStyleSchema.optional(),
|
||||||
teams: z.record(z.string(), MSTeamsTeamSchema.optional()).optional(),
|
teams: z.record(z.string(), MSTeamsTeamSchema.optional()).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.superRefine((value, ctx) => {
|
.superRefine((value, ctx) => {
|
||||||
requireOpenAllowFrom({
|
requireOpenAllowFrom({
|
||||||
policy: value.dmPolicy,
|
policy: value.dmPolicy,
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export const WhatsAppAccountSchema = z
|
|||||||
.object({
|
.object({
|
||||||
requireMention: z.boolean().optional(),
|
requireMention: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
@@ -45,9 +46,11 @@ export const WhatsAppAccountSchema = z
|
|||||||
direct: z.boolean().optional().default(true),
|
direct: z.boolean().optional().default(true),
|
||||||
group: z.enum(["always", "mentions", "never"]).optional().default("mentions"),
|
group: z.enum(["always", "mentions", "never"]).optional().default("mentions"),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
debounceMs: z.number().int().nonnegative().optional().default(0),
|
debounceMs: z.number().int().nonnegative().optional().default(0),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.superRefine((value, ctx) => {
|
.superRefine((value, ctx) => {
|
||||||
if (value.dmPolicy !== "open") return;
|
if (value.dmPolicy !== "open") return;
|
||||||
const allow = (value.allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean);
|
const allow = (value.allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean);
|
||||||
@@ -84,6 +87,7 @@ export const WhatsAppConfigSchema = z
|
|||||||
sendMessage: z.boolean().optional(),
|
sendMessage: z.boolean().optional(),
|
||||||
polls: z.boolean().optional(),
|
polls: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
groups: z
|
groups: z
|
||||||
.record(
|
.record(
|
||||||
@@ -92,6 +96,7 @@ export const WhatsAppConfigSchema = z
|
|||||||
.object({
|
.object({
|
||||||
requireMention: z.boolean().optional(),
|
requireMention: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
@@ -101,9 +106,11 @@ export const WhatsAppConfigSchema = z
|
|||||||
direct: z.boolean().optional().default(true),
|
direct: z.boolean().optional().default(true),
|
||||||
group: z.enum(["always", "mentions", "never"]).optional().default("mentions"),
|
group: z.enum(["always", "mentions", "never"]).optional().default("mentions"),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
debounceMs: z.number().int().nonnegative().optional().default(0),
|
debounceMs: z.number().int().nonnegative().optional().default(0),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.superRefine((value, ctx) => {
|
.superRefine((value, ctx) => {
|
||||||
if (value.dmPolicy !== "open") return;
|
if (value.dmPolicy !== "open") return;
|
||||||
const allow = (value.allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean);
|
const allow = (value.allowFrom ?? []).map((v) => String(v).trim()).filter(Boolean);
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export const ChannelsSchema = z
|
|||||||
.object({
|
.object({
|
||||||
groupPolicy: GroupPolicySchema.optional(),
|
groupPolicy: GroupPolicySchema.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
whatsapp: WhatsAppConfigSchema.optional(),
|
whatsapp: WhatsAppConfigSchema.optional(),
|
||||||
telegram: TelegramConfigSchema.optional(),
|
telegram: TelegramConfigSchema.optional(),
|
||||||
@@ -29,5 +30,5 @@ export const ChannelsSchema = z
|
|||||||
imessage: IMessageConfigSchema.optional(),
|
imessage: IMessageConfigSchema.optional(),
|
||||||
msteams: MSTeamsConfigSchema.optional(),
|
msteams: MSTeamsConfigSchema.optional(),
|
||||||
})
|
})
|
||||||
.catchall(z.unknown())
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import {
|
|||||||
QueueSchema,
|
QueueSchema,
|
||||||
} from "./zod-schema.core.js";
|
} from "./zod-schema.core.js";
|
||||||
|
|
||||||
const SessionResetConfigSchema = z.object({
|
const SessionResetConfigSchema = z
|
||||||
mode: z.union([z.literal("daily"), z.literal("idle")]).optional(),
|
.object({
|
||||||
atHour: z.number().int().min(0).max(23).optional(),
|
mode: z.union([z.literal("daily"), z.literal("idle")]).optional(),
|
||||||
idleMinutes: z.number().int().positive().optional(),
|
atHour: z.number().int().min(0).max(23).optional(),
|
||||||
});
|
idleMinutes: z.number().int().positive().optional(),
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
export const SessionSchema = z
|
export const SessionSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -30,6 +32,7 @@ export const SessionSchema = z
|
|||||||
group: SessionResetConfigSchema.optional(),
|
group: SessionResetConfigSchema.optional(),
|
||||||
thread: SessionResetConfigSchema.optional(),
|
thread: SessionResetConfigSchema.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
store: z.string().optional(),
|
store: z.string().optional(),
|
||||||
typingIntervalSeconds: z.number().int().positive().optional(),
|
typingIntervalSeconds: z.number().int().positive().optional(),
|
||||||
@@ -47,28 +50,34 @@ export const SessionSchema = z
|
|||||||
default: z.union([z.literal("allow"), z.literal("deny")]).optional(),
|
default: z.union([z.literal("allow"), z.literal("deny")]).optional(),
|
||||||
rules: z
|
rules: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z
|
||||||
action: z.union([z.literal("allow"), z.literal("deny")]),
|
.object({
|
||||||
match: z
|
action: z.union([z.literal("allow"), z.literal("deny")]),
|
||||||
.object({
|
match: z
|
||||||
channel: z.string().optional(),
|
.object({
|
||||||
chatType: z
|
channel: z.string().optional(),
|
||||||
.union([z.literal("direct"), z.literal("group"), z.literal("channel")])
|
chatType: z
|
||||||
.optional(),
|
.union([z.literal("direct"), z.literal("group"), z.literal("channel")])
|
||||||
keyPrefix: z.string().optional(),
|
.optional(),
|
||||||
})
|
keyPrefix: z.string().optional(),
|
||||||
.optional(),
|
})
|
||||||
}),
|
.strict()
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
agentToAgent: z
|
agentToAgent: z
|
||||||
.object({
|
.object({
|
||||||
maxPingPongTurns: z.number().int().min(0).max(5).optional(),
|
maxPingPongTurns: z.number().int().min(0).max(5).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const MessagesSchema = z
|
export const MessagesSchema = z
|
||||||
@@ -82,6 +91,7 @@ export const MessagesSchema = z
|
|||||||
ackReactionScope: z.enum(["group-mentions", "group-all", "direct", "all"]).optional(),
|
ackReactionScope: z.enum(["group-mentions", "group-all", "direct", "all"]).optional(),
|
||||||
removeAckAfterReply: z.boolean().optional(),
|
removeAckAfterReply: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional();
|
.optional();
|
||||||
|
|
||||||
export const CommandsSchema = z
|
export const CommandsSchema = z
|
||||||
@@ -96,5 +106,6 @@ export const CommandsSchema = z
|
|||||||
restart: z.boolean().optional(),
|
restart: z.boolean().optional(),
|
||||||
useAccessGroups: z.boolean().optional(),
|
useAccessGroups: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional()
|
.optional()
|
||||||
.default({ native: "auto", nativeSkills: "auto" });
|
.default({ native: "auto", nativeSkills: "auto" });
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export const ClawdbotSchema = z
|
|||||||
lastTouchedVersion: z.string().optional(),
|
lastTouchedVersion: z.string().optional(),
|
||||||
lastTouchedAt: z.string().optional(),
|
lastTouchedAt: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
env: z
|
env: z
|
||||||
.object({
|
.object({
|
||||||
@@ -21,6 +22,7 @@ export const ClawdbotSchema = z
|
|||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
timeoutMs: z.number().int().nonnegative().optional(),
|
timeoutMs: z.number().int().nonnegative().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
vars: z.record(z.string(), z.string()).optional(),
|
vars: z.record(z.string(), z.string()).optional(),
|
||||||
})
|
})
|
||||||
@@ -34,6 +36,7 @@ export const ClawdbotSchema = z
|
|||||||
lastRunCommand: z.string().optional(),
|
lastRunCommand: z.string().optional(),
|
||||||
lastRunMode: z.union([z.literal("local"), z.literal("remote")]).optional(),
|
lastRunMode: z.union([z.literal("local"), z.literal("remote")]).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
logging: z
|
logging: z
|
||||||
.object({
|
.object({
|
||||||
@@ -66,12 +69,14 @@ export const ClawdbotSchema = z
|
|||||||
redactSensitive: z.union([z.literal("off"), z.literal("tools")]).optional(),
|
redactSensitive: z.union([z.literal("off"), z.literal("tools")]).optional(),
|
||||||
redactPatterns: z.array(z.string()).optional(),
|
redactPatterns: z.array(z.string()).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
update: z
|
update: z
|
||||||
.object({
|
.object({
|
||||||
channel: z.union([z.literal("stable"), z.literal("beta")]).optional(),
|
channel: z.union([z.literal("stable"), z.literal("beta")]).optional(),
|
||||||
checkOnStart: z.boolean().optional(),
|
checkOnStart: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
browser: z
|
browser: z
|
||||||
.object({
|
.object({
|
||||||
@@ -99,28 +104,33 @@ export const ClawdbotSchema = z
|
|||||||
driver: z.union([z.literal("clawd"), z.literal("extension")]).optional(),
|
driver: z.union([z.literal("clawd"), z.literal("extension")]).optional(),
|
||||||
color: HexColorSchema,
|
color: HexColorSchema,
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.refine((value) => value.cdpPort || value.cdpUrl, {
|
.refine((value) => value.cdpPort || value.cdpUrl, {
|
||||||
message: "Profile must set cdpPort or cdpUrl",
|
message: "Profile must set cdpPort or cdpUrl",
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
ui: z
|
ui: z
|
||||||
.object({
|
.object({
|
||||||
seamColor: HexColorSchema.optional(),
|
seamColor: HexColorSchema.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
auth: z
|
auth: z
|
||||||
.object({
|
.object({
|
||||||
profiles: z
|
profiles: z
|
||||||
.record(
|
.record(
|
||||||
z.string(),
|
z.string(),
|
||||||
z.object({
|
z
|
||||||
provider: z.string(),
|
.object({
|
||||||
mode: z.union([z.literal("api_key"), z.literal("oauth"), z.literal("token")]),
|
provider: z.string(),
|
||||||
email: z.string().optional(),
|
mode: z.union([z.literal("api_key"), z.literal("oauth"), z.literal("token")]),
|
||||||
}),
|
email: z.string().optional(),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
order: z.record(z.string(), z.array(z.string())).optional(),
|
order: z.record(z.string(), z.array(z.string())).optional(),
|
||||||
@@ -131,8 +141,10 @@ export const ClawdbotSchema = z
|
|||||||
billingMaxHours: z.number().positive().optional(),
|
billingMaxHours: z.number().positive().optional(),
|
||||||
failureWindowHours: z.number().positive().optional(),
|
failureWindowHours: z.number().positive().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
models: ModelsConfigSchema,
|
models: ModelsConfigSchema,
|
||||||
agents: AgentsSchema,
|
agents: AgentsSchema,
|
||||||
@@ -149,6 +161,7 @@ export const ClawdbotSchema = z
|
|||||||
store: z.string().optional(),
|
store: z.string().optional(),
|
||||||
maxConcurrentRuns: z.number().int().positive().optional(),
|
maxConcurrentRuns: z.number().int().positive().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
hooks: z
|
hooks: z
|
||||||
.object({
|
.object({
|
||||||
@@ -162,6 +175,7 @@ export const ClawdbotSchema = z
|
|||||||
gmail: HooksGmailSchema,
|
gmail: HooksGmailSchema,
|
||||||
internal: InternalHooksSchema,
|
internal: InternalHooksSchema,
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
web: z
|
web: z
|
||||||
.object({
|
.object({
|
||||||
@@ -175,8 +189,10 @@ export const ClawdbotSchema = z
|
|||||||
jitter: z.number().min(0).max(1).optional(),
|
jitter: z.number().min(0).max(1).optional(),
|
||||||
maxAttempts: z.number().int().min(0).optional(),
|
maxAttempts: z.number().int().min(0).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
channels: ChannelsSchema,
|
channels: ChannelsSchema,
|
||||||
bridge: z
|
bridge: z
|
||||||
@@ -194,8 +210,10 @@ export const ClawdbotSchema = z
|
|||||||
keyPath: z.string().optional(),
|
keyPath: z.string().optional(),
|
||||||
caPath: z.string().optional(),
|
caPath: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
discovery: z
|
discovery: z
|
||||||
.object({
|
.object({
|
||||||
@@ -203,8 +221,10 @@ export const ClawdbotSchema = z
|
|||||||
.object({
|
.object({
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
canvasHost: z
|
canvasHost: z
|
||||||
.object({
|
.object({
|
||||||
@@ -213,6 +233,7 @@ export const ClawdbotSchema = z
|
|||||||
port: z.number().int().positive().optional(),
|
port: z.number().int().positive().optional(),
|
||||||
liveReload: z.boolean().optional(),
|
liveReload: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
talk: z
|
talk: z
|
||||||
.object({
|
.object({
|
||||||
@@ -223,6 +244,7 @@ export const ClawdbotSchema = z
|
|||||||
apiKey: z.string().optional(),
|
apiKey: z.string().optional(),
|
||||||
interruptOnSpeech: z.boolean().optional(),
|
interruptOnSpeech: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
gateway: z
|
gateway: z
|
||||||
.object({
|
.object({
|
||||||
@@ -236,6 +258,7 @@ export const ClawdbotSchema = z
|
|||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
basePath: z.string().optional(),
|
basePath: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
auth: z
|
auth: z
|
||||||
.object({
|
.object({
|
||||||
@@ -244,12 +267,14 @@ export const ClawdbotSchema = z
|
|||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
allowTailscale: z.boolean().optional(),
|
allowTailscale: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
tailscale: z
|
tailscale: z
|
||||||
.object({
|
.object({
|
||||||
mode: z.union([z.literal("off"), z.literal("serve"), z.literal("funnel")]).optional(),
|
mode: z.union([z.literal("off"), z.literal("serve"), z.literal("funnel")]).optional(),
|
||||||
resetOnExit: z.boolean().optional(),
|
resetOnExit: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
remote: z
|
remote: z
|
||||||
.object({
|
.object({
|
||||||
@@ -259,6 +284,7 @@ export const ClawdbotSchema = z
|
|||||||
sshTarget: z.string().optional(),
|
sshTarget: z.string().optional(),
|
||||||
sshIdentity: z.string().optional(),
|
sshIdentity: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
reload: z
|
reload: z
|
||||||
.object({
|
.object({
|
||||||
@@ -272,6 +298,7 @@ export const ClawdbotSchema = z
|
|||||||
.optional(),
|
.optional(),
|
||||||
debounceMs: z.number().int().min(0).optional(),
|
debounceMs: z.number().int().min(0).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
http: z
|
http: z
|
||||||
.object({
|
.object({
|
||||||
@@ -281,12 +308,16 @@ export const ClawdbotSchema = z
|
|||||||
.object({
|
.object({
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
skills: z
|
skills: z
|
||||||
.object({
|
.object({
|
||||||
@@ -297,6 +328,7 @@ export const ClawdbotSchema = z
|
|||||||
watch: z.boolean().optional(),
|
watch: z.boolean().optional(),
|
||||||
watchDebounceMs: z.number().int().min(0).optional(),
|
watchDebounceMs: z.number().int().min(0).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
install: z
|
install: z
|
||||||
.object({
|
.object({
|
||||||
@@ -305,6 +337,7 @@ export const ClawdbotSchema = z
|
|||||||
.union([z.literal("npm"), z.literal("pnpm"), z.literal("yarn"), z.literal("bun")])
|
.union([z.literal("npm"), z.literal("pnpm"), z.literal("yarn"), z.literal("bun")])
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
entries: z
|
entries: z
|
||||||
.record(
|
.record(
|
||||||
@@ -315,10 +348,11 @@ export const ClawdbotSchema = z
|
|||||||
apiKey: z.string().optional(),
|
apiKey: z.string().optional(),
|
||||||
env: z.record(z.string(), z.string()).optional(),
|
env: z.record(z.string(), z.string()).optional(),
|
||||||
})
|
})
|
||||||
.passthrough(),
|
.strict(),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
plugins: z
|
plugins: z
|
||||||
.object({
|
.object({
|
||||||
@@ -329,11 +363,13 @@ export const ClawdbotSchema = z
|
|||||||
.object({
|
.object({
|
||||||
paths: z.array(z.string()).optional(),
|
paths: z.array(z.string()).optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
slots: z
|
slots: z
|
||||||
.object({
|
.object({
|
||||||
memory: z.string().optional(),
|
memory: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
entries: z
|
entries: z
|
||||||
.record(
|
.record(
|
||||||
@@ -343,7 +379,7 @@ export const ClawdbotSchema = z
|
|||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
config: z.record(z.string(), z.unknown()).optional(),
|
config: z.record(z.string(), z.unknown()).optional(),
|
||||||
})
|
})
|
||||||
.passthrough(),
|
.strict(),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
installs: z
|
installs: z
|
||||||
@@ -358,13 +394,14 @@ export const ClawdbotSchema = z
|
|||||||
version: z.string().optional(),
|
version: z.string().optional(),
|
||||||
installedAt: z.string().optional(),
|
installedAt: z.string().optional(),
|
||||||
})
|
})
|
||||||
.passthrough(),
|
.strict(),
|
||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
|
.strict()
|
||||||
.optional(),
|
.optional(),
|
||||||
})
|
})
|
||||||
.passthrough()
|
.strict()
|
||||||
.superRefine((cfg, ctx) => {
|
.superRefine((cfg, ctx) => {
|
||||||
const agents = cfg.agents?.list ?? [];
|
const agents = cfg.agents?.list ?? [];
|
||||||
if (agents.length === 0) return;
|
if (agents.length === 0) return;
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
} from "../infra/skills-remote.js";
|
} from "../infra/skills-remote.js";
|
||||||
import { scheduleGatewayUpdateCheck } from "../infra/update-startup.js";
|
import { scheduleGatewayUpdateCheck } from "../infra/update-startup.js";
|
||||||
import { setGatewaySigusr1RestartPolicy } from "../infra/restart.js";
|
import { setGatewaySigusr1RestartPolicy } from "../infra/restart.js";
|
||||||
import { autoMigrateLegacyState } from "../infra/state-migrations.js";
|
|
||||||
import { createSubsystemLogger, runtimeForLogger } from "../logging/subsystem.js";
|
import { createSubsystemLogger, runtimeForLogger } from "../logging/subsystem.js";
|
||||||
import type { PluginServicesHandle } from "../plugins/services.js";
|
import type { PluginServicesHandle } from "../plugins/services.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
@@ -175,7 +174,6 @@ export async function startGatewayServer(
|
|||||||
const cfgAtStart = loadConfig();
|
const cfgAtStart = loadConfig();
|
||||||
setGatewaySigusr1RestartPolicy({ allowExternal: cfgAtStart.commands?.restart === true });
|
setGatewaySigusr1RestartPolicy({ allowExternal: cfgAtStart.commands?.restart === true });
|
||||||
initSubagentRegistry();
|
initSubagentRegistry();
|
||||||
await autoMigrateLegacyState({ cfg: cfgAtStart, log });
|
|
||||||
const defaultAgentId = resolveDefaultAgentId(cfgAtStart);
|
const defaultAgentId = resolveDefaultAgentId(cfgAtStart);
|
||||||
const defaultWorkspaceDir = resolveAgentWorkspaceDir(cfgAtStart, defaultAgentId);
|
const defaultWorkspaceDir = resolveAgentWorkspaceDir(cfgAtStart, defaultAgentId);
|
||||||
const baseMethods = listGatewayMethods();
|
const baseMethods = listGatewayMethods();
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ export type {
|
|||||||
export type { ChannelConfigSchema, ChannelPlugin } from "../channels/plugins/types.plugin.js";
|
export type { ChannelConfigSchema, ChannelPlugin } from "../channels/plugins/types.plugin.js";
|
||||||
export type { ClawdbotPluginApi } from "../plugins/types.js";
|
export type { ClawdbotPluginApi } from "../plugins/types.js";
|
||||||
export type { PluginRuntime } from "../plugins/runtime/types.js";
|
export type { PluginRuntime } from "../plugins/runtime/types.js";
|
||||||
|
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
|
||||||
export type { ClawdbotConfig } from "../config/config.js";
|
export type { ClawdbotConfig } from "../config/config.js";
|
||||||
export type { ChannelDock } from "../channels/dock.js";
|
export type { ChannelDock } from "../channels/dock.js";
|
||||||
export { getChatChannelMeta } from "../channels/registry.js";
|
export { getChatChannelMeta } from "../channels/registry.js";
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type TempPlugin = { dir: string; file: string; id: string };
|
|||||||
|
|
||||||
const tempDirs: string[] = [];
|
const tempDirs: string[] = [];
|
||||||
const prevBundledDir = process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR;
|
const prevBundledDir = process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR;
|
||||||
|
const EMPTY_CONFIG_SCHEMA = `configSchema: { safeParse() { return { success: true, data: {} }; }, jsonSchema: { type: "object", additionalProperties: false, properties: {} } },`;
|
||||||
|
|
||||||
function makeTempDir() {
|
function makeTempDir() {
|
||||||
const dir = path.join(os.tmpdir(), `clawdbot-plugin-${randomUUID()}`);
|
const dir = path.join(os.tmpdir(), `clawdbot-plugin-${randomUUID()}`);
|
||||||
@@ -44,7 +45,11 @@ describe("loadClawdbotPlugins", () => {
|
|||||||
it("disables bundled plugins by default", () => {
|
it("disables bundled plugins by default", () => {
|
||||||
const bundledDir = makeTempDir();
|
const bundledDir = makeTempDir();
|
||||||
const bundledPath = path.join(bundledDir, "bundled.ts");
|
const bundledPath = path.join(bundledDir, "bundled.ts");
|
||||||
fs.writeFileSync(bundledPath, "export default function () {}", "utf-8");
|
fs.writeFileSync(
|
||||||
|
bundledPath,
|
||||||
|
`export default { id: "bundled", ${EMPTY_CONFIG_SCHEMA} register() {} };`,
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = bundledDir;
|
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = bundledDir;
|
||||||
|
|
||||||
const registry = loadClawdbotPlugins({
|
const registry = loadClawdbotPlugins({
|
||||||
@@ -100,7 +105,7 @@ describe("loadClawdbotPlugins", () => {
|
|||||||
const bundledPath = path.join(bundledDir, "memory-core.ts");
|
const bundledPath = path.join(bundledDir, "memory-core.ts");
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
bundledPath,
|
bundledPath,
|
||||||
'export default { id: "memory-core", kind: "memory", register() {} };',
|
`export default { id: "memory-core", kind: "memory", ${EMPTY_CONFIG_SCHEMA} register() {} };`,
|
||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = bundledDir;
|
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = bundledDir;
|
||||||
@@ -137,7 +142,7 @@ describe("loadClawdbotPlugins", () => {
|
|||||||
);
|
);
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
path.join(pluginDir, "index.ts"),
|
path.join(pluginDir, "index.ts"),
|
||||||
'export default { id: "memory-core", kind: "memory", name: "Memory (Core)", register() {} };',
|
`export default { id: "memory-core", kind: "memory", name: "Memory (Core)", ${EMPTY_CONFIG_SCHEMA} register() {} };`,
|
||||||
"utf-8",
|
"utf-8",
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -164,7 +169,7 @@ describe("loadClawdbotPlugins", () => {
|
|||||||
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
||||||
const plugin = writePlugin({
|
const plugin = writePlugin({
|
||||||
id: "allowed",
|
id: "allowed",
|
||||||
body: `export default function (api) { api.registerGatewayMethod("allowed.ping", ({ respond }) => respond(true, { ok: true })); }`,
|
body: `export default { id: "allowed", ${EMPTY_CONFIG_SCHEMA} register(api) { api.registerGatewayMethod("allowed.ping", ({ respond }) => respond(true, { ok: true })); } };`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const registry = loadClawdbotPlugins({
|
const registry = loadClawdbotPlugins({
|
||||||
@@ -187,7 +192,7 @@ describe("loadClawdbotPlugins", () => {
|
|||||||
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
||||||
const plugin = writePlugin({
|
const plugin = writePlugin({
|
||||||
id: "blocked",
|
id: "blocked",
|
||||||
body: `export default function () {}`,
|
body: `export default { id: "blocked", ${EMPTY_CONFIG_SCHEMA} register() {} };`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const registry = loadClawdbotPlugins({
|
const registry = loadClawdbotPlugins({
|
||||||
@@ -237,7 +242,7 @@ describe("loadClawdbotPlugins", () => {
|
|||||||
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
||||||
const plugin = writePlugin({
|
const plugin = writePlugin({
|
||||||
id: "channel-demo",
|
id: "channel-demo",
|
||||||
body: `export default function (api) {
|
body: `export default { id: "channel-demo", ${EMPTY_CONFIG_SCHEMA} register(api) {
|
||||||
api.registerChannel({
|
api.registerChannel({
|
||||||
plugin: {
|
plugin: {
|
||||||
id: "demo",
|
id: "demo",
|
||||||
@@ -256,7 +261,7 @@ describe("loadClawdbotPlugins", () => {
|
|||||||
outbound: { deliveryMode: "direct" }
|
outbound: { deliveryMode: "direct" }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};`,
|
} };`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const registry = loadClawdbotPlugins({
|
const registry = loadClawdbotPlugins({
|
||||||
@@ -278,9 +283,9 @@ describe("loadClawdbotPlugins", () => {
|
|||||||
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
||||||
const plugin = writePlugin({
|
const plugin = writePlugin({
|
||||||
id: "http-demo",
|
id: "http-demo",
|
||||||
body: `export default function (api) {
|
body: `export default { id: "http-demo", ${EMPTY_CONFIG_SCHEMA} register(api) {
|
||||||
api.registerHttpHandler(async () => false);
|
api.registerHttpHandler(async () => false);
|
||||||
};`,
|
} };`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const registry = loadClawdbotPlugins({
|
const registry = loadClawdbotPlugins({
|
||||||
@@ -304,7 +309,7 @@ describe("loadClawdbotPlugins", () => {
|
|||||||
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
||||||
const plugin = writePlugin({
|
const plugin = writePlugin({
|
||||||
id: "config-disable",
|
id: "config-disable",
|
||||||
body: `export default function () {}`,
|
body: `export default { id: "config-disable", ${EMPTY_CONFIG_SCHEMA} register() {} };`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const registry = loadClawdbotPlugins({
|
const registry = loadClawdbotPlugins({
|
||||||
@@ -327,11 +332,11 @@ describe("loadClawdbotPlugins", () => {
|
|||||||
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
||||||
const memoryA = writePlugin({
|
const memoryA = writePlugin({
|
||||||
id: "memory-a",
|
id: "memory-a",
|
||||||
body: `export default { id: "memory-a", kind: "memory", register() {} };`,
|
body: `export default { id: "memory-a", kind: "memory", ${EMPTY_CONFIG_SCHEMA} register() {} };`,
|
||||||
});
|
});
|
||||||
const memoryB = writePlugin({
|
const memoryB = writePlugin({
|
||||||
id: "memory-b",
|
id: "memory-b",
|
||||||
body: `export default { id: "memory-b", kind: "memory", register() {} };`,
|
body: `export default { id: "memory-b", kind: "memory", ${EMPTY_CONFIG_SCHEMA} register() {} };`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const registry = loadClawdbotPlugins({
|
const registry = loadClawdbotPlugins({
|
||||||
@@ -354,7 +359,7 @@ describe("loadClawdbotPlugins", () => {
|
|||||||
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
||||||
const memory = writePlugin({
|
const memory = writePlugin({
|
||||||
id: "memory-off",
|
id: "memory-off",
|
||||||
body: `export default { id: "memory-off", kind: "memory", register() {} };`,
|
body: `export default { id: "memory-off", kind: "memory", ${EMPTY_CONFIG_SCHEMA} register() {} };`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const registry = loadClawdbotPlugins({
|
const registry = loadClawdbotPlugins({
|
||||||
@@ -373,12 +378,16 @@ describe("loadClawdbotPlugins", () => {
|
|||||||
|
|
||||||
it("prefers higher-precedence plugins with the same id", () => {
|
it("prefers higher-precedence plugins with the same id", () => {
|
||||||
const bundledDir = makeTempDir();
|
const bundledDir = makeTempDir();
|
||||||
fs.writeFileSync(path.join(bundledDir, "shadow.js"), "export default function () {}", "utf-8");
|
fs.writeFileSync(
|
||||||
|
path.join(bundledDir, "shadow.js"),
|
||||||
|
`export default { id: "shadow", ${EMPTY_CONFIG_SCHEMA} register() {} };`,
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = bundledDir;
|
process.env.CLAWDBOT_BUNDLED_PLUGINS_DIR = bundledDir;
|
||||||
|
|
||||||
const override = writePlugin({
|
const override = writePlugin({
|
||||||
id: "shadow",
|
id: "shadow",
|
||||||
body: `export default function () {}`,
|
body: `export default { id: "shadow", ${EMPTY_CONFIG_SCHEMA} register() {} };`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const registry = loadClawdbotPlugins({
|
const registry = loadClawdbotPlugins({
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export type PluginLoadOptions = {
|
|||||||
logger?: PluginLogger;
|
logger?: PluginLogger;
|
||||||
coreGatewayHandlers?: Record<string, GatewayRequestHandler>;
|
coreGatewayHandlers?: Record<string, GatewayRequestHandler>;
|
||||||
cache?: boolean;
|
cache?: boolean;
|
||||||
|
mode?: "full" | "validate";
|
||||||
};
|
};
|
||||||
|
|
||||||
type NormalizedPluginsConfig = {
|
type NormalizedPluginsConfig = {
|
||||||
@@ -297,6 +298,7 @@ function pushDiagnostics(diagnostics: PluginDiagnostic[], append: PluginDiagnost
|
|||||||
export function loadClawdbotPlugins(options: PluginLoadOptions = {}): PluginRegistry {
|
export function loadClawdbotPlugins(options: PluginLoadOptions = {}): PluginRegistry {
|
||||||
const cfg = options.config ?? {};
|
const cfg = options.config ?? {};
|
||||||
const logger = options.logger ?? defaultLogger();
|
const logger = options.logger ?? defaultLogger();
|
||||||
|
const validateOnly = options.mode === "validate";
|
||||||
const normalized = normalizePluginsConfig(cfg.plugins);
|
const normalized = normalizePluginsConfig(cfg.plugins);
|
||||||
const cacheKey = buildCacheKey({
|
const cacheKey = buildCacheKey({
|
||||||
workspaceDir: options.workspaceDir,
|
workspaceDir: options.workspaceDir,
|
||||||
@@ -437,6 +439,21 @@ export function loadClawdbotPlugins(options: PluginLoadOptions = {}): PluginRegi
|
|||||||
>)
|
>)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
if (!definition?.configSchema) {
|
||||||
|
logger.error(`[plugins] ${record.id} missing config schema`);
|
||||||
|
record.status = "error";
|
||||||
|
record.error = "missing config schema";
|
||||||
|
registry.plugins.push(record);
|
||||||
|
seenIds.set(candidate.idHint, candidate.origin);
|
||||||
|
registry.diagnostics.push({
|
||||||
|
level: "error",
|
||||||
|
pluginId: record.id,
|
||||||
|
source: record.source,
|
||||||
|
message: record.error,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (record.kind === "memory" && memorySlot === record.id) {
|
if (record.kind === "memory" && memorySlot === record.id) {
|
||||||
memorySlotMatched = true;
|
memorySlotMatched = true;
|
||||||
}
|
}
|
||||||
@@ -481,6 +498,12 @@ export function loadClawdbotPlugins(options: PluginLoadOptions = {}): PluginRegi
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (validateOnly) {
|
||||||
|
registry.plugins.push(record);
|
||||||
|
seenIds.set(candidate.idHint, candidate.origin);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof register !== "function") {
|
if (typeof register !== "function") {
|
||||||
logger.error(`[plugins] ${record.id} missing register/activate export`);
|
logger.error(`[plugins] ${record.id} missing register/activate export`);
|
||||||
record.status = "error";
|
record.status = "error";
|
||||||
|
|||||||
@@ -36,8 +36,10 @@ afterEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("resolvePluginTools optional tools", () => {
|
describe("resolvePluginTools optional tools", () => {
|
||||||
|
const emptyConfigSchema =
|
||||||
|
'configSchema: { safeParse() { return { success: true, data: {} }; }, jsonSchema: { type: "object", additionalProperties: false, properties: {} } },';
|
||||||
const pluginBody = `
|
const pluginBody = `
|
||||||
export default function (api) {
|
export default { ${emptyConfigSchema} register(api) {
|
||||||
api.registerTool(
|
api.registerTool(
|
||||||
{
|
{
|
||||||
name: "optional_tool",
|
name: "optional_tool",
|
||||||
@@ -49,7 +51,7 @@ export default function (api) {
|
|||||||
},
|
},
|
||||||
{ optional: true },
|
{ optional: true },
|
||||||
);
|
);
|
||||||
}
|
} }
|
||||||
`;
|
`;
|
||||||
|
|
||||||
it("skips optional tools without explicit allowlist", () => {
|
it("skips optional tools without explicit allowlist", () => {
|
||||||
@@ -138,7 +140,7 @@ export default function (api) {
|
|||||||
const plugin = writePlugin({
|
const plugin = writePlugin({
|
||||||
id: "multi",
|
id: "multi",
|
||||||
body: `
|
body: `
|
||||||
export default function (api) {
|
export default { ${emptyConfigSchema} register(api) {
|
||||||
api.registerTool({
|
api.registerTool({
|
||||||
name: "message",
|
name: "message",
|
||||||
description: "conflict",
|
description: "conflict",
|
||||||
@@ -155,7 +157,7 @@ export default function (api) {
|
|||||||
return { content: [{ type: "text", text: "ok" }] };
|
return { content: [{ type: "text", text: "ok" }] };
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
} }
|
||||||
`,
|
`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user