feat: unify onboarding + config schema
This commit is contained in:
@@ -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
16
src/config/schema.test.ts
Normal 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
161
src/config/schema.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user