feat: unify onboarding + config schema

This commit is contained in:
Peter Steinberger
2026-01-03 16:04:19 +01:00
parent 0f85080d81
commit 53baba71fa
43 changed files with 3478 additions and 1011 deletions

View File

@@ -573,6 +573,12 @@ export type ClawdisConfig = {
* - "message_end": end of the whole assistant message (may include tool blocks)
*/
blockStreamingBreak?: "text_end" | "message_end";
/** Soft block chunking for streamed replies (min/max chars, prefer paragraph/newline). */
blockStreamingChunk?: {
minChars?: number;
maxChars?: number;
breakPreference?: "paragraph" | "newline" | "sentence";
};
timeoutSeconds?: number;
/** Max inbound media size in MB for agent-visible attachments (text note or future image attach). */
mediaMaxMb?: number;
@@ -900,7 +906,7 @@ const HooksGmailSchema = z
})
.optional();
const ClawdisSchema = z.object({
export const ClawdisSchema = z.object({
identity: z
.object({
name: z.string().optional(),
@@ -990,6 +996,19 @@ const ClawdisSchema = z.object({
blockStreamingBreak: z
.union([z.literal("text_end"), z.literal("message_end")])
.optional(),
blockStreamingChunk: z
.object({
minChars: z.number().int().positive().optional(),
maxChars: z.number().int().positive().optional(),
breakPreference: z
.union([
z.literal("paragraph"),
z.literal("newline"),
z.literal("sentence"),
])
.optional(),
})
.optional(),
timeoutSeconds: z.number().int().positive().optional(),
mediaMaxMb: z.number().positive().optional(),
typingIntervalSeconds: z.number().int().positive().optional(),

16
src/config/schema.test.ts Normal file
View File

@@ -0,0 +1,16 @@
import { describe, expect, it } from "vitest";
import { buildConfigSchema } from "./schema.js";
describe("config schema", () => {
it("exports schema + hints", () => {
const res = buildConfigSchema();
const schema = res.schema as { properties?: Record<string, unknown> };
expect(schema.properties?.gateway).toBeTruthy();
expect(schema.properties?.agent).toBeTruthy();
expect(res.uiHints.gateway?.label).toBe("Gateway");
expect(res.uiHints["gateway.auth.token"]?.sensitive).toBe(true);
expect(res.version).toBeTruthy();
expect(res.generatedAt).toBeTruthy();
});
});

161
src/config/schema.ts Normal file
View File

@@ -0,0 +1,161 @@
import { VERSION } from "../version.js";
import { ClawdisSchema } from "./config.js";
export type ConfigUiHint = {
label?: string;
help?: string;
group?: string;
order?: number;
advanced?: boolean;
sensitive?: boolean;
placeholder?: string;
itemTemplate?: unknown;
};
export type ConfigUiHints = Record<string, ConfigUiHint>;
export type ConfigSchema = ReturnType<typeof ClawdisSchema.toJSONSchema>;
export type ConfigSchemaResponse = {
schema: ConfigSchema;
uiHints: ConfigUiHints;
version: string;
generatedAt: string;
};
const GROUP_LABELS: Record<string, string> = {
identity: "Identity",
wizard: "Wizard",
logging: "Logging",
gateway: "Gateway",
agent: "Agent",
models: "Models",
routing: "Routing",
messages: "Messages",
session: "Session",
cron: "Cron",
hooks: "Hooks",
ui: "UI",
browser: "Browser",
talk: "Talk",
telegram: "Telegram",
discord: "Discord",
signal: "Signal",
imessage: "iMessage",
whatsapp: "WhatsApp",
skills: "Skills",
discovery: "Discovery",
presence: "Presence",
voicewake: "Voice Wake",
};
const GROUP_ORDER: Record<string, number> = {
identity: 10,
wizard: 20,
gateway: 30,
agent: 40,
models: 50,
routing: 60,
messages: 70,
session: 80,
cron: 90,
hooks: 100,
ui: 110,
browser: 120,
talk: 130,
telegram: 140,
discord: 150,
signal: 160,
imessage: 170,
whatsapp: 180,
skills: 190,
discovery: 200,
presence: 210,
voicewake: 220,
logging: 900,
};
const FIELD_LABELS: Record<string, string> = {
"gateway.remote.url": "Remote Gateway URL",
"gateway.remote.token": "Remote Gateway Token",
"gateway.remote.password": "Remote Gateway Password",
"gateway.auth.token": "Gateway Token",
"gateway.auth.password": "Gateway Password",
"agent.workspace": "Workspace",
"agent.model": "Default Model",
"ui.seamColor": "Accent Color",
"browser.controlUrl": "Browser Control URL",
"talk.apiKey": "Talk API Key",
"telegram.botToken": "Telegram Bot Token",
"discord.token": "Discord Bot Token",
"signal.account": "Signal Account",
"imessage.cliPath": "iMessage CLI Path",
};
const FIELD_HELP: Record<string, string> = {
"gateway.remote.url": "Remote Gateway WebSocket URL (ws:// or wss://).",
"gateway.auth.token":
"Required for multi-machine access or non-loopback binds.",
"gateway.auth.password": "Required for Tailscale funnel.",
};
const FIELD_PLACEHOLDERS: Record<string, string> = {
"gateway.remote.url": "ws://host:18789",
};
const SENSITIVE_PATTERNS = [/token/i, /password/i, /secret/i, /api.?key/i];
function isSensitivePath(path: string): boolean {
return SENSITIVE_PATTERNS.some((pattern) => pattern.test(path));
}
function buildBaseHints(): ConfigUiHints {
const hints: ConfigUiHints = {};
for (const [group, label] of Object.entries(GROUP_LABELS)) {
hints[group] = {
label,
group: label,
order: GROUP_ORDER[group],
};
}
for (const [path, label] of Object.entries(FIELD_LABELS)) {
hints[path] = { ...(hints[path] ?? {}), label };
}
for (const [path, help] of Object.entries(FIELD_HELP)) {
hints[path] = { ...(hints[path] ?? {}), help };
}
for (const [path, placeholder] of Object.entries(FIELD_PLACEHOLDERS)) {
hints[path] = { ...(hints[path] ?? {}), placeholder };
}
return hints;
}
function applySensitiveHints(hints: ConfigUiHints): ConfigUiHints {
const next = { ...hints };
for (const key of Object.keys(next)) {
if (isSensitivePath(key)) {
next[key] = { ...next[key], sensitive: true };
}
}
return next;
}
let cached: ConfigSchemaResponse | null = null;
export function buildConfigSchema(): ConfigSchemaResponse {
if (cached) return cached;
const schema = ClawdisSchema.toJSONSchema({
target: "draft-07",
unrepresentable: "any",
});
schema.title = "ClawdisConfig";
const hints = applySensitiveHints(buildBaseHints());
const next = {
schema,
uiHints: hints,
version: VERSION,
generatedAt: new Date().toISOString(),
};
cached = next;
return next;
}