Sistema di configurazione persistente: /agy:config con get/set/reset, 12 opzioni configurabili
This commit is contained in:
+178
-13
@@ -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<string> {
|
||||
// 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<string> {
|
||||
// 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<string> {
|
||||
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<string> {
|
||||
],
|
||||
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`, {
|
||||
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<string> {
|
||||
|
||||
// Dispatcher: sceglie il backend di trascrizione (gemini | enne2)
|
||||
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") {
|
||||
return transcribeWithEnne2(file);
|
||||
}
|
||||
@@ -432,7 +520,7 @@ async function transcribeAudio(file: string): Promise<string> {
|
||||
// TTS via Gemini API (nessun engine esterno)
|
||||
// ---------------------------------------------------------------------------
|
||||
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) {
|
||||
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<string | 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 {
|
||||
const body = {
|
||||
contents: [{ parts: [{ text }] }],
|
||||
@@ -1131,8 +1219,9 @@ export default function agyExtension(pi: ExtensionAPI) {
|
||||
}
|
||||
ctx.ui.notify("✅ Trascrizione inviata come prompt a pi", "info");
|
||||
|
||||
// notifica vocale (disattivabile con AGY_TTS_NOTIFY=0)
|
||||
if (process.env.AGY_TTS_NOTIFY !== "0") {
|
||||
// notifica vocale (config ttsNotify o AGY_TTS_NOTIFY=0 per disattivare)
|
||||
const ttsNotify = getConfig("ttsNotify") ?? "true";
|
||||
if (ttsNotify !== "false" && process.env.AGY_TTS_NOTIFY !== "0") {
|
||||
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");
|
||||
},
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// 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",
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user