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 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",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user