Merge remote-tracking branch 'origin/main' into upstream-preview-nix-2025-12-20

This commit is contained in:
Peter Steinberger
2026-01-01 09:15:28 +01:00
163 changed files with 10867 additions and 1712 deletions

View File

@@ -231,6 +231,21 @@ export type CanvasHostConfig = {
port?: number;
};
export type TalkConfig = {
/** Default ElevenLabs voice ID for Talk mode. */
voiceId?: string;
/** Optional voice name -> ElevenLabs voice ID map. */
voiceAliases?: Record<string, string>;
/** Default ElevenLabs model ID for Talk mode. */
modelId?: string;
/** Default ElevenLabs output format (e.g. mp3_44100_128). */
outputFormat?: string;
/** ElevenLabs API key (optional; falls back to ELEVENLABS_API_KEY). */
apiKey?: string;
/** Stop speaking when user starts talking (default: true). */
interruptOnSpeech?: boolean;
};
export type GatewayControlUiConfig = {
/** If false, the Gateway will not serve the Control UI (/). Default: true. */
enabled?: boolean;
@@ -345,6 +360,10 @@ export type ClawdisConfig = {
};
logging?: LoggingConfig;
browser?: BrowserConfig;
ui?: {
/** Accent color for Clawdis UI chrome (hex). */
seamColor?: string;
};
skillsLoad?: SkillsLoadConfig;
skillsInstall?: SkillsInstallConfig;
models?: ModelsConfig;
@@ -403,6 +422,7 @@ export type ClawdisConfig = {
bridge?: BridgeConfig;
discovery?: DiscoveryConfig;
canvasHost?: CanvasHostConfig;
talk?: TalkConfig;
gateway?: GatewayConfig;
skills?: Record<string, SkillConfig>;
};
@@ -502,6 +522,10 @@ const TranscribeAudioSchema = z
})
.optional();
const HexColorSchema = z
.string()
.regex(/^#?[0-9a-fA-F]{6}$/, "expected hex color (RRGGBB)");
const SessionSchema = z
.object({
scope: z.union([z.literal("per-sender"), z.literal("global")]).optional(),
@@ -680,6 +704,11 @@ const ClawdisSchema = z.object({
attachOnly: z.boolean().optional(),
})
.optional(),
ui: z
.object({
seamColor: HexColorSchema.optional(),
})
.optional(),
models: ModelsConfigSchema,
agent: z
.object({
@@ -808,6 +837,16 @@ const ClawdisSchema = z.object({
port: z.number().int().positive().optional(),
})
.optional(),
talk: z
.object({
voiceId: z.string().optional(),
voiceAliases: z.record(z.string(), z.string()).optional(),
modelId: z.string().optional(),
outputFormat: z.string().optional(),
apiKey: z.string().optional(),
interruptOnSpeech: z.boolean().optional(),
})
.optional(),
gateway: z
.object({
mode: z.union([z.literal("local"), z.literal("remote")]).optional(),
@@ -967,17 +1006,59 @@ export function parseConfigJson5(
}
}
function readTalkApiKeyFromProfile(): string | null {
const home = os.homedir();
const candidates = [".profile", ".zprofile", ".zshrc", ".bashrc"].map(
(name) => path.join(home, name),
);
for (const candidate of candidates) {
if (!fs.existsSync(candidate)) continue;
try {
const text = fs.readFileSync(candidate, "utf-8");
const match = text.match(
/(?:^|\n)\s*(?:export\s+)?ELEVENLABS_API_KEY\s*=\s*["']?([^\n"']+)["']?/,
);
const value = match?.[1]?.trim();
if (value) return value;
} catch {
// Ignore profile read errors.
}
}
return null;
}
function resolveTalkApiKey(): string | null {
const envValue = (process.env.ELEVENLABS_API_KEY ?? "").trim();
if (envValue) return envValue;
return readTalkApiKeyFromProfile();
}
function applyTalkApiKey(config: ClawdisConfig): ClawdisConfig {
const resolved = resolveTalkApiKey();
if (!resolved) return config;
const existing = config.talk?.apiKey?.trim();
if (existing) return config;
return {
...config,
talk: {
...config.talk,
apiKey: resolved,
},
};
}
export async function readConfigFileSnapshot(): Promise<ConfigFileSnapshot> {
const configPath = CONFIG_PATH_CLAWDIS;
const exists = fs.existsSync(configPath);
if (!exists) {
const config = applyTalkApiKey({});
return {
path: configPath,
exists: false,
raw: null,
parsed: {},
valid: true,
config: {},
config,
issues: [],
};
}
@@ -1018,7 +1099,7 @@ export async function readConfigFileSnapshot(): Promise<ConfigFileSnapshot> {
raw,
parsed: parsedRes.parsed,
valid: true,
config: validated.config,
config: applyTalkApiKey(validated.config),
issues: [],
};
} catch (err) {