From f36194afa1bcb7319f147c67c791b798bf913fb4 Mon Sep 17 00:00:00 2001 From: dev Date: Sun, 9 Aug 2026 23:01:15 +0200 Subject: [PATCH] Sistema di configurazione persistente: /agy:config con get/set/reset, 12 opzioni configurabili --- extensions/index.ts | 191 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 178 insertions(+), 13 deletions(-) diff --git a/extensions/index.ts b/extensions/index.ts index 4c7aa44..6844def 100644 --- a/extensions/index.ts +++ b/extensions/index.ts @@ -30,11 +30,91 @@ const CONV_DIR = path.join(os.homedir(), ".gemini", "antigravity-cli", "conversa const DEFAULT_TIMEOUT_MS = 180_000; // 3 min const IMAGE_TIMEOUT_MS = 300_000; // 5 min +// --------------------------------------------------------------------------- +// Configurazione persistente (~/.config/agy-pi/config.json) +// --------------------------------------------------------------------------- +const CONFIG_DIR = path.join(os.homedir(), ".config", "agy-pi"); +const CONFIG_FILE = path.join(CONFIG_DIR, "config.json"); + +interface AgyConfig { + geminiApiKey?: string; + enne2ApiKey?: string; + sttBackend?: string; // gemini | enne2 + sttUrl?: string; + sttModel?: string; + sttMaxDuration?: number; // secondi + ttsBackend?: string; // gemini | enne2 + ttsNotify?: boolean; + ttsModel?: string; + agyBin?: string; + agyDefaultModel?: string; + agyTimeoutMs?: number; +} + +const CONFIG_DEFAULTS: AgyConfig = { + sttBackend: "gemini", + sttUrl: "https://ai.enne2.net", + sttModel: "gemma4:E4B", + sttMaxDuration: 120, + ttsBackend: "gemini", + ttsNotify: true, + ttsModel: "gemini-2.5-flash-preview-tts", + agyTimeoutMs: 180_000, +}; + +function loadConfig(): AgyConfig { + try { + return { ...CONFIG_DEFAULTS, ...JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8")) }; + } catch { + return { ...CONFIG_DEFAULTS }; + } +} + +function saveConfig(cfg: AgyConfig) { + try { + fs.mkdirSync(CONFIG_DIR, { recursive: true }); + fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2), { mode: 0o600 }); + } catch { + /* ignora */ + } +} + +// Legge una chiave: config file → env var → default +function getConfig(key: keyof AgyConfig): string | undefined { + const cfg = loadConfig(); + const v = cfg[key]; + if (v === undefined || v === "") return undefined; + return String(v); +} + +function setConfig(key: keyof AgyConfig, value: string) { + const cfg = loadConfig(); + const numKeys: (keyof AgyConfig)[] = ["sttMaxDuration", "agyTimeoutMs"]; + const boolKeys: (keyof AgyConfig)[] = ["ttsNotify"]; + if (numKeys.includes(key)) { + (cfg as any)[key] = Number(value); + } else if (boolKeys.includes(key)) { + (cfg as any)[key] = value === "true" || value === "1" || value === "yes"; + } else { + (cfg as any)[key] = value; + } + saveConfig(cfg); +} + +function resetConfig() { + try { + fs.rmSync(CONFIG_FILE, { force: true }); + } catch { + /* ignora */ + } +} + // --------------------------------------------------------------------------- // Helper: trovare il binario agy // --------------------------------------------------------------------------- function findAgy(): string { const candidates = [ + getConfig("agyBin"), process.env.AGY_BIN, path.join(os.homedir(), ".local", "bin", "agy"), "agy", @@ -195,11 +275,15 @@ async function executeAgy(opts: AgyExecOptions) { else convId = readState(); if (convId) args.push("--conversation", convId); - if (opts.model) args.push("--model", opts.model); + const model = opts.model ?? getConfig("agyDefaultModel"); + if (model) args.push("--model", model); if (opts.effort) args.push("--effort", opts.effort); if (opts.yolo) args.push("--dangerously-skip-permissions"); - const timeout = opts.timeoutMs ?? (opts.mode === "image" ? IMAGE_TIMEOUT_MS : DEFAULT_TIMEOUT_MS); + const timeout = + opts.timeoutMs ?? + Number(getConfig("agyTimeoutMs") ?? DEFAULT_TIMEOUT_MS) ?? + (opts.mode === "image" ? IMAGE_TIMEOUT_MS : DEFAULT_TIMEOUT_MS); const r = await runAgy(args, timeout, opts.signal); // aggiorna stato conversazione (solo se non stateless) @@ -250,10 +334,11 @@ interface Recording { let recording: Recording | null = null; function startRecording(): string { + const maxDur = Number(getConfig("sttMaxDuration") ?? 120); const file = path.join(os.tmpdir(), `agy-rec-${Date.now()}.wav`); const proc = spawn( "ffmpeg", - ["-y", "-f", "pulse", "-i", "default", "-ac", "1", "-ar", "16000", "-t", "120", file], + ["-y", "-f", "pulse", "-i", "default", "-ac", "1", "-ar", "16000", "-t", String(maxDur), file], { stdio: "ignore" }, ); recording = { proc, file }; @@ -311,8 +396,8 @@ async function optimizeAudio(input: string): Promise { // Trascrizione affidabile e veloce via Gemini API diretta // (agy CLI non supporta audio; la API supporta audio/wav nativamente) async function transcribeWithGeminiAPI(file: string): Promise { - // key da env var o file config - let key = process.env.GEMINI_API_KEY ?? ""; + // key da config file o env var + let key = getConfig("geminiApiKey") ?? process.env.GEMINI_API_KEY ?? ""; if (!key) { try { key = fs.readFileSync(path.join(AGY_CHAT_DIR, "gemini-key"), "utf8").trim(); @@ -388,8 +473,9 @@ function getConversationContext(ctx: any, maxEntries = 8): string { // Trascrizione via server locale ai.enne2.net (gemma4:E4B supporta audio) async function transcribeWithEnne2(file: string): Promise { - const baseUrl = process.env.AGY_STT_URL ?? "https://ai.enne2.net"; - const model = process.env.AGY_STT_MODEL ?? "gemma4:E4B"; + const baseUrl = getConfig("sttUrl") ?? "https://ai.enne2.net"; + const model = getConfig("sttModel") ?? "gemma4:E4B"; + const apiKey = getConfig("enne2ApiKey") ?? process.env.ENNE2_API_KEY ?? ""; try { const b64 = fs.readFileSync(file).toString("base64"); const body = { @@ -405,9 +491,11 @@ async function transcribeWithEnne2(file: string): Promise { ], max_tokens: 500, }; + const headers: Record = { "Content-Type": "application/json" }; + if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; const res = await fetch(`${baseUrl}/v1/chat/completions`, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers, body: JSON.stringify(body), signal: AbortSignal.timeout(90_000), }); @@ -421,7 +509,7 @@ async function transcribeWithEnne2(file: string): Promise { // Dispatcher: sceglie il backend di trascrizione (gemini | enne2) async function transcribeAudio(file: string): Promise { - const backend = process.env.AGY_STT_BACKEND ?? "gemini"; + const backend = getConfig("sttBackend") ?? "gemini"; if (backend === "enne2" || backend === "local") { return transcribeWithEnne2(file); } @@ -432,7 +520,7 @@ async function transcribeAudio(file: string): Promise { // TTS via Gemini API (nessun engine esterno) // --------------------------------------------------------------------------- async function ttsSpeak(text: string, outputDir?: string): Promise { - let key = process.env.GEMINI_API_KEY ?? ""; + let key = getConfig("geminiApiKey") ?? process.env.GEMINI_API_KEY ?? ""; if (!key) { try { key = fs.readFileSync(path.join(AGY_CHAT_DIR, "gemini-key"), "utf8").trim(); @@ -442,7 +530,7 @@ async function ttsSpeak(text: string, outputDir?: string): Promise {}); } } @@ -1176,4 +1265,80 @@ export default function agyExtension(pi: ExtensionAPI) { else ctx.ui.notify("TTS fallito (key Gemini mancante?)", "error"); }, }); + + // ========================================================================= + // Comando: /agy:config — gestione configurazione persistente + // ========================================================================= + const CONFIG_KEYS: { key: keyof AgyConfig; desc: string }[] = [ + { key: "geminiApiKey", desc: "Chiave API Google Gemini (STT/TTS)" }, + { key: "enne2ApiKey", desc: "Token per il server proxy ai.enne2.net (opzionale)" }, + { key: "sttBackend", desc: "Backend trascrizione: gemini | enne2" }, + { key: "sttUrl", desc: "URL base backend enne2" }, + { key: "sttModel", desc: "Modello STT backend enne2" }, + { key: "sttMaxDuration", desc: "Durata max registrazione (secondi)" }, + { key: "ttsBackend", desc: "Backend TTS: gemini | enne2" }, + { key: "ttsNotify", desc: "Notifiche vocali automatiche: true | false" }, + { key: "ttsModel", desc: "Modello TTS Gemini" }, + { key: "agyBin", desc: "Path del binario agy" }, + { key: "agyDefaultModel", desc: "Modello predefinito per le chiamate agy" }, + { key: "agyTimeoutMs", desc: "Timeout esecuzione agy (ms)" }, + ]; + + pi.registerCommand("agy:config", { + description: + "Gestisce la configurazione dell'estensione. Uso: /agy:config [get|set|reset] [chiave] [valore]", + handler: async (args, ctx) => { + const parts = (args ?? "").trim().split(/\s+/); + const action = parts[0] ?? ""; + const key = parts[1] as keyof AgyConfig | undefined; + const value = parts.slice(2).join(" "); + + // /agy:config — elenca tutto + if (!action) { + const cfg = loadConfig(); + const lines = CONFIG_KEYS.map(({ key: k, desc }) => { + const v = (cfg as any)[k]; + const masked = + k === "geminiApiKey" || k === "enne2ApiKey" + ? v + ? `${String(v).slice(0, 4)}...${String(v).slice(-4)}` + : "(non impostata)" + : v ?? "(non impostata)"; + return `${k} = ${masked} — ${desc}`; + }); + ctx.ui.notify(`Config agy-pi (${CONFIG_FILE}):\n${lines.join("\n")}`, "info"); + return; + } + + // /agy:config get + if (action === "get" && key) { + const v = getConfig(key); + ctx.ui.notify(`${key} = ${v ?? "(non impostata)"}`, "info"); + return; + } + + // /agy:config set + if (action === "set" && key && value) { + if (!CONFIG_KEYS.some((c) => c.key === key)) { + ctx.ui.notify(`Chiave sconosciuta: ${key}`, "error"); + return; + } + setConfig(key, value); + ctx.ui.notify(`✅ ${key} impostato. File: ${CONFIG_FILE}`, "info"); + return; + } + + // /agy:config reset + if (action === "reset") { + resetConfig(); + ctx.ui.notify("Configurazione azzerata (default ripristinati).", "info"); + return; + } + + ctx.ui.notify( + "Uso: /agy:config | /agy:config get | /agy:config set | /agy:config reset", + "warning", + ); + }, + }); }