feat: /agy:key — dialog overlay TUI mascherato per la chiave API Gemini

- Comando /agy:key che apre un overlay TUI (ctx.ui.custom overlay) con campo
  mascherato per inserire la chiave API Gemini senza digitarla in chiaro.
- Enter conferma: salva in config.json e ~/.agy-chat/gemini-key + suona chime.
- Esc annulla, Backspace cancella, filtra caratteri consentiti per chiave API.
- Import dinamico dei componenti pi-tui (Container, Text, DynamicBorder) per non
  rompere il load dell'estensione se pi-tui non fosse disponibile.
- Aggiunto tsconfig.json per il typecheck con mapping ai moduli pi.
- README: documentato il nuovo comando.
This commit is contained in:
2026-08-10 14:37:07 +02:00
parent 9878689cfa
commit f7cc237294
3 changed files with 172 additions and 0 deletions
+131
View File
@@ -1650,4 +1650,135 @@ export default function agyExtension(pi: ExtensionAPI) {
);
},
});
// =========================================================================
// Comando: /agy:key — dialog overlay TUI per inserire/modificare la chiave
// API Gemini in modo interattivo (campo mascherato, Enter conferma, Esc
// annulla). Usa i componenti TUI di pi (overlay in sovrimpressione).
// =========================================================================
pi.registerCommand("agy:key", {
description:
"Apre un dialog overlay per inserire/modificare la chiave API Gemini (campo mascherato)",
handler: async (_args, ctx) => {
// Import dinamico: se pi-tui non fosse disponibile, fallisce solo questo
// comando senza rompere il caricamento dell'intera estensione.
const {
Container,
Text,
matchesKey,
Key,
} = await import("@earendil-works/pi-tui");
const { DynamicBorder } = await import("@earendil-works/pi-coding-agent");
const result = await ctx.ui.custom<string | null>(
(tui, theme, _keybindings, done) => {
let value = "";
const currentKey = getConfig("geminiApiKey") ?? "";
const maskCurrent = currentKey
? `${currentKey.slice(0, 4)}...${currentKey.slice(-4)}`
: "(non impostata)";
const mask = (v: string) => "•".repeat(v.length);
const container = new Container();
const renderDialog = () => {
container.clear();
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
container.addChild(
new Text(theme.fg("accent", theme.bold("🔑 Chiave API Gemini")), 1, 1),
);
container.addChild(
new Text(theme.fg("dim", `Attuale: ${maskCurrent}`), 1, 0),
);
container.addChild(new Text("", 0, 0));
const field = value ? mask(value) : "(vuota)";
container.addChild(
new Text(
theme.fg("text", "Nuova chiave: ") + theme.fg("warning", field),
1,
0,
(s) => theme.bg("toolPendingBg", s),
),
);
container.addChild(new Text("", 0, 0));
container.addChild(
new Text(
theme.fg("dim", "Digita la chiave • Enter conferma • Esc annulla"),
1,
0,
),
);
container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
};
renderDialog();
return {
render: (w) => {
renderDialog();
return container.render(w);
},
invalidate: () => container.invalidate(),
handleInput: (data) => {
if (matchesKey(data, Key.enter)) {
const trimmed = value.trim();
if (trimmed) {
setConfig("geminiApiKey", trimmed);
// Aggiorna anche ~/.agy-chat/gemini-key per coerenza con gli script TTS/STT
try {
fs.mkdirSync(AGY_CHAT_DIR, { recursive: true });
fs.writeFileSync(
path.join(AGY_CHAT_DIR, "gemini-key"),
trimmed,
{ mode: 0o600 },
);
} catch {
/* ignora */
}
done(trimmed);
}
return;
}
if (matchesKey(data, Key.escape)) {
done(null);
return;
}
if (matchesKey(data, Key.backspace)) {
value = value.slice(0, -1);
tui.requestRender();
return;
}
// Caratteri stampabili permessi per una chiave API (A-Z a-z 0-9 _ . -)
if (data.length === 1 && data.charCodeAt(0) >= 32) {
if (/^[A-Za-z0-9._-]+$/.test(data)) {
value += data;
}
tui.requestRender();
}
},
};
},
{
overlay: true,
overlayOptions: {
width: "50%",
minWidth: 54,
anchor: "center",
},
},
);
if (result) {
ctx.ui.notify(
`✅ Chiave API Gemini aggiornata: ${result.slice(0, 4)}...${result.slice(-4)}`,
"info",
);
try {
playSound("done");
} catch {
/* ignora */
}
} else {
ctx.ui.notify("Chiave non modificata.", "info");
}
},
});
}