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:
@@ -41,6 +41,25 @@ Tutte le impostazioni dell'estensione si gestiscono con il comando `/agy:config`
|
|||||||
/agy:config reset # ripristina i default
|
/agy:config reset # ripristina i default
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Dialog TUI per la chiave Gemini (`/agy:key`)
|
||||||
|
|
||||||
|
Per inserire/modificare la **chiave API Gemini** con un'interfaccia grafica in
|
||||||
|
sovrimpressione (dialog TUI, campo mascherato) usa il comando `/agy:key`:
|
||||||
|
|
||||||
|
```
|
||||||
|
/agy:key
|
||||||
|
```
|
||||||
|
|
||||||
|
Apre un overlay centrato nel terminale:
|
||||||
|
|
||||||
|
- mostra la chiave attuale mascherata (`AIza...yfDY`)
|
||||||
|
- campo di input **mascherato** (digiti la chiave, vedi `•`)
|
||||||
|
- `Enter` conferma (salva in config + `~/.agy-chat/gemini-key`)
|
||||||
|
- `Esc` annulla
|
||||||
|
|
||||||
|
È il modo interattivo e sicuro per impostare `geminiApiKey` senza digitarla in
|
||||||
|
chiaro nella cronologia del terminale.
|
||||||
|
|
||||||
| Chiave | Default | Descrizione |
|
| Chiave | Default | Descrizione |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `geminiApiKey` | — | Chiave API Google Gemini (STT/TTS) |
|
| `geminiApiKey` | — | Chiave API Google Gemini (STT/TTS) |
|
||||||
|
|||||||
@@ -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");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2022", "DOM"],
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@earendil-works/pi-tui": [
|
||||||
|
"/home/enne2/.local/share/pi-node/node-v22.23.2-linux-x64/lib/node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui/dist/index.d.ts"
|
||||||
|
],
|
||||||
|
"@earendil-works/pi-coding-agent": [
|
||||||
|
"/home/enne2/.local/share/pi-node/node-v22.23.2-linux-x64/lib/node_modules/@earendil-works/pi-coding-agent/dist/index.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["extensions/**/*.ts"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user