Sistema di configurazione persistente: /agy:config con get/set/reset, 12 opzioni configurabili

This commit is contained in:
dev
2026-08-09 23:01:15 +02:00
parent a1edd73079
commit f36194afa1
+178 -13
View File
@@ -30,11 +30,91 @@ const CONV_DIR = path.join(os.homedir(), ".gemini", "antigravity-cli", "conversa
const DEFAULT_TIMEOUT_MS = 180_000; // 3 min const DEFAULT_TIMEOUT_MS = 180_000; // 3 min
const IMAGE_TIMEOUT_MS = 300_000; // 5 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 // Helper: trovare il binario agy
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function findAgy(): string { function findAgy(): string {
const candidates = [ const candidates = [
getConfig("agyBin"),
process.env.AGY_BIN, process.env.AGY_BIN,
path.join(os.homedir(), ".local", "bin", "agy"), path.join(os.homedir(), ".local", "bin", "agy"),
"agy", "agy",
@@ -195,11 +275,15 @@ async function executeAgy(opts: AgyExecOptions) {
else convId = readState(); else convId = readState();
if (convId) args.push("--conversation", convId); 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.effort) args.push("--effort", opts.effort);
if (opts.yolo) args.push("--dangerously-skip-permissions"); 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); const r = await runAgy(args, timeout, opts.signal);
// aggiorna stato conversazione (solo se non stateless) // aggiorna stato conversazione (solo se non stateless)
@@ -250,10 +334,11 @@ interface Recording {
let recording: Recording | null = null; let recording: Recording | null = null;
function startRecording(): string { function startRecording(): string {
const maxDur = Number(getConfig("sttMaxDuration") ?? 120);
const file = path.join(os.tmpdir(), `agy-rec-${Date.now()}.wav`); const file = path.join(os.tmpdir(), `agy-rec-${Date.now()}.wav`);
const proc = spawn( const proc = spawn(
"ffmpeg", "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" }, { stdio: "ignore" },
); );
recording = { proc, file }; recording = { proc, file };
@@ -311,8 +396,8 @@ async function optimizeAudio(input: string): Promise<string> {
// Trascrizione affidabile e veloce via Gemini API diretta // Trascrizione affidabile e veloce via Gemini API diretta
// (agy CLI non supporta audio; la API supporta audio/wav nativamente) // (agy CLI non supporta audio; la API supporta audio/wav nativamente)
async function transcribeWithGeminiAPI(file: string): Promise<string> { async function transcribeWithGeminiAPI(file: string): Promise<string> {
// key da env var o file config // key da config file o env var
let key = process.env.GEMINI_API_KEY ?? ""; let key = getConfig("geminiApiKey") ?? process.env.GEMINI_API_KEY ?? "";
if (!key) { if (!key) {
try { try {
key = fs.readFileSync(path.join(AGY_CHAT_DIR, "gemini-key"), "utf8").trim(); 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) // Trascrizione via server locale ai.enne2.net (gemma4:E4B supporta audio)
async function transcribeWithEnne2(file: string): Promise<string> { async function transcribeWithEnne2(file: string): Promise<string> {
const baseUrl = process.env.AGY_STT_URL ?? "https://ai.enne2.net"; const baseUrl = getConfig("sttUrl") ?? "https://ai.enne2.net";
const model = process.env.AGY_STT_MODEL ?? "gemma4:E4B"; const model = getConfig("sttModel") ?? "gemma4:E4B";
const apiKey = getConfig("enne2ApiKey") ?? process.env.ENNE2_API_KEY ?? "";
try { try {
const b64 = fs.readFileSync(file).toString("base64"); const b64 = fs.readFileSync(file).toString("base64");
const body = { const body = {
@@ -405,9 +491,11 @@ async function transcribeWithEnne2(file: string): Promise<string> {
], ],
max_tokens: 500, max_tokens: 500,
}; };
const headers: Record<string, string> = { "Content-Type": "application/json" };
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
const res = await fetch(`${baseUrl}/v1/chat/completions`, { const res = await fetch(`${baseUrl}/v1/chat/completions`, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers,
body: JSON.stringify(body), body: JSON.stringify(body),
signal: AbortSignal.timeout(90_000), signal: AbortSignal.timeout(90_000),
}); });
@@ -421,7 +509,7 @@ async function transcribeWithEnne2(file: string): Promise<string> {
// Dispatcher: sceglie il backend di trascrizione (gemini | enne2) // Dispatcher: sceglie il backend di trascrizione (gemini | enne2)
async function transcribeAudio(file: string): Promise<string> { async function transcribeAudio(file: string): Promise<string> {
const backend = process.env.AGY_STT_BACKEND ?? "gemini"; const backend = getConfig("sttBackend") ?? "gemini";
if (backend === "enne2" || backend === "local") { if (backend === "enne2" || backend === "local") {
return transcribeWithEnne2(file); return transcribeWithEnne2(file);
} }
@@ -432,7 +520,7 @@ async function transcribeAudio(file: string): Promise<string> {
// TTS via Gemini API (nessun engine esterno) // TTS via Gemini API (nessun engine esterno)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function ttsSpeak(text: string, outputDir?: string): Promise<string | null> { async function ttsSpeak(text: string, outputDir?: string): Promise<string | null> {
let key = process.env.GEMINI_API_KEY ?? ""; let key = getConfig("geminiApiKey") ?? process.env.GEMINI_API_KEY ?? "";
if (!key) { if (!key) {
try { try {
key = fs.readFileSync(path.join(AGY_CHAT_DIR, "gemini-key"), "utf8").trim(); key = fs.readFileSync(path.join(AGY_CHAT_DIR, "gemini-key"), "utf8").trim();
@@ -442,7 +530,7 @@ async function ttsSpeak(text: string, outputDir?: string): Promise<string | null
} }
if (!key) return null; if (!key) return null;
const model = process.env.AGY_TTS_MODEL ?? "gemini-2.5-flash-preview-tts"; const model = getConfig("ttsModel") ?? "gemini-2.5-flash-preview-tts";
try { try {
const body = { const body = {
contents: [{ parts: [{ text }] }], contents: [{ parts: [{ text }] }],
@@ -1131,8 +1219,9 @@ export default function agyExtension(pi: ExtensionAPI) {
} }
ctx.ui.notify("✅ Trascrizione inviata come prompt a pi", "info"); ctx.ui.notify("✅ Trascrizione inviata come prompt a pi", "info");
// notifica vocale (disattivabile con AGY_TTS_NOTIFY=0) // notifica vocale (config ttsNotify o AGY_TTS_NOTIFY=0 per disattivare)
if (process.env.AGY_TTS_NOTIFY !== "0") { const ttsNotify = getConfig("ttsNotify") ?? "true";
if (ttsNotify !== "false" && process.env.AGY_TTS_NOTIFY !== "0") {
ttsSpeak("Trascrizione completata e inviata.").catch(() => {}); ttsSpeak("Trascrizione completata e inviata.").catch(() => {});
} }
} }
@@ -1176,4 +1265,80 @@ export default function agyExtension(pi: ExtensionAPI) {
else ctx.ui.notify("TTS fallito (key Gemini mancante?)", "error"); 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 <key>
if (action === "get" && key) {
const v = getConfig(key);
ctx.ui.notify(`${key} = ${v ?? "(non impostata)"}`, "info");
return;
}
// /agy:config set <key> <value>
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 <chiave> | /agy:config set <chiave> <valore> | /agy:config reset",
"warning",
);
},
});
} }