Compare commits

...
2 Commits
Author SHA1 Message Date
Matteo Benedetto a0f94c8811 feat(images): direct Antigravity image API for image tools with CLI fallback
Replace the agent/CLI image path with a direct call to the Antigravity
Cloud Code gateway (same OAuth token and project discovery already used by
the provider), keeping the CLI as an automatic fallback.

Protocol validated with 15 live probes on 2026-09-13:
- POST v1internal:generateContent (non-streaming), requestType "image_gen",
  responseModalities ["IMAGE"], imageConfig {aspectRatio, imageSize}
- image returned as candidates[0].content.parts[].inlineData (base64, jpeg);
  parts with thought=true are intermediate images and must be skipped
- 512/2K/4K verified (512x512 .. 5504x3072, 7-25s, 1.1-2.9K output tokens)
- image input works (single, multiple) and editing honours the input when
  imageConfig.aspectRatio is supplied; HTTP 200 without images carries a
  finishMessage explaining the refusal
- gemini-3-pro-image is not entitled on this account (HTTP 404)
- the image model quota is shared and can be exhausted (429 QUOTA_EXHAUSTED
  with a "reset after" delay)

Changes:
- add antgGenerateImages(): direct client with token refresh retry, image
  part parsing and finishMessage surfacing
- add antgSaveImages() and agyImageTask(): direct-first runner with automatic
  CLI fallback, reporting engine, imagePaths, usage and fallback reason
- add antgParseQuotaDelayMs() + blocked-until guard so an exhausted image
  quota skips the direct attempt instead of retrying it every time
- route agy_generate, agy_edit, agy_inpaint, agy_style_transfer, agy_compose
  and agy_character through agyImageTask; add imageSize to agy_generate
- fix extractImagePath(): it matched the IMAGE_PATH placeholder echoed from
  the prompt and returned a bogus path on CLI failures; now only existing
  absolute image paths are accepted

Verified: module loads, tools invoke agy_generate through the direct engine
(8.6s, 1199 output tokens, file saved to outputDir), the CLI fallback fires
with an explicit reason on 429, and extractImagePath passes 5/5 cases tested
against the real function extracted from this file.
2026-09-13 19:49:18 +02:00
Matteo Benedetto 16fba66723 feat: persist Antigravity model list across restarts
The antigravity provider list lived only in a module-scoped variable
initialized from ANTG_PROVIDER_MODELS, so /agy:refresh-models updated the
running process but the list silently reset to the static defaults on
/reload or restart.

- add ~/.config/agy-pi/models-cache.json (mode 0600, atomic write) storing
  version, refreshedAt, project and the fetched model definitions
- load the cache at startup; fall back to ANTG_PROVIDER_MODELS when the file
  is missing, unreadable, corrupt or has an incompatible version
- persist the live catalog in /agy:refresh-models and report the file path
- add /agy:models-reset to drop the cache and restore the defaults

Verified: extension loads with no cache (11 defaults), a valid cache is
honored and registered, corrupt/invalid/version-mismatched caches fall back
silently, and /agy:models-reset deletes the cache and re-registers the
defaults end-to-end over RPC.
2026-09-13 18:53:10 +02:00
+408 -42
View File
@@ -229,10 +229,19 @@ async function runAgy(
// Helper: estrarre il percorso immagine dall'output
// ---------------------------------------------------------------------------
function extractImagePath(text: string): string | undefined {
const m =
text.match(/IMAGE_PATH:\s*(\S+)/i) ||
text.match(/(\/[^\s]+\.(?:png|jpe?g|webp|gif))/i);
return m ? m[1] : undefined;
// Candidati: prima la riga IMAGE_PATH, poi qualsiasi path immagine assoluto nel testo.
// I placeholder ecoati dal prompt (es. "IMAGE_PATH: <percorso assoluto ...>") e i
// path inesistenti vengono scartati: senza il filtro si ottenevano percorsi fasulli.
const candidates: string[] = [];
const m = text.match(/IMAGE_PATH:\s*(\S+)/i);
if (m?.[1]) candidates.push(m[1]);
for (const mm of text.matchAll(/(\/[^\s"'`]+?\.(?:png|jpe?g|webp|gif))/gi)) candidates.push(mm[1]);
for (const candidate of candidates) {
const clean = candidate.replace(/[.,;:)\]}>]+$/, "");
if (!clean || clean.includes("<") || clean.includes(">")) continue;
if (fs.existsSync(clean)) return clean;
}
return undefined;
}
// ---------------------------------------------------------------------------
@@ -643,6 +652,121 @@ async function executeAgy(opts: AgyExecOptions) {
const IMG_SUFFIX =
"\n\nAlla fine della risposta, scrivi su una riga esattamente: IMAGE_PATH: <percorso assoluto dell'immagine generata>";
// ---------------------------------------------------------------------------
// Task immagine unificato: prima il protocollo diretto Antigravity (nessun
// subprocess, ~7-25s), con fallback automatico al percorso agy CLI se il
// modello non è disponibile o la risposta non contiene immagini.
// ---------------------------------------------------------------------------
const AGY_IMAGE_OUT_DIR = path.join(AGY_CHAT_DIR, "generated_images");
function antgSaveImages(images: AntgInlineImage[], outputDir: string | undefined, prefix: string): string[] {
const dir = outputDir ?? AGY_IMAGE_OUT_DIR;
const saved: string[] = [];
try {
fs.mkdirSync(dir, { recursive: true });
} catch {
return saved;
}
const stamp = new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14);
images.forEach((img, i) => {
const ext = img.mimeType.includes("jpeg") ? "jpg" : img.mimeType.includes("webp") ? "webp" : "png";
const file = path.join(dir, `${prefix}_${stamp}_${i}.${ext}`);
try {
fs.writeFileSync(file, Buffer.from(img.data, "base64"));
saved.push(file);
} catch {
/* ignora */
}
});
return saved;
}
async function agyImageTask(opts: {
prompt: string; // prompt in stile CLI (può contenere i percorsi + IMG_SUFFIX)
images?: string[];
aspectRatio?: string;
imageSize?: string;
model?: string;
outputDir?: string;
signal?: AbortSignal;
label: string;
}): Promise<{
text: string;
imagePaths: string[];
engine: "direct" | "cli";
usage?: any;
exitCode?: number;
directError?: string;
}> {
const imageModel = opts.model && /image/i.test(opts.model) ? opts.model : ANTG_IMAGE_MODEL_DEFAULT;
let directError: string | undefined;
const quotaBlocked = Date.now() < antgImageBlockedUntilMs;
try {
if (quotaBlocked) {
const mins = Math.ceil((antgImageBlockedUntilMs - Date.now()) / 60000);
throw new Error(`quota API immagine esaurita (reset stimato tra ~${mins} min): ${antgImageBlockedReason}`);
}
// Prompt per il modello: niente IMG_SUFFIX, percorsi → riferimenti allegati,
// più un vincolo esplicito di composizione (migliora affidabilità su input).
let prompt = opts.prompt.split(IMG_SUFFIX).join("").trim();
for (const file of opts.images ?? []) prompt = prompt.split(file).join("(immagine allegata)");
if (opts.images?.length) {
prompt += "\nMantieni esattamente la composizione e l'aspect ratio dell'immagine di input; non ritagliare e non aggiungere testo.";
}
const res = await antgGenerateImages({
prompt,
images: opts.images,
aspectRatio: opts.aspectRatio,
imageSize: opts.imageSize,
model: imageModel,
signal: opts.signal,
});
if (!res.images.length) {
throw new Error(
res.finishMessage
? `il modello non ha prodotto immagini: ${res.finishMessage.slice(0, 200)}`
: `nessuna immagine nella risposta (finishReason=${res.finishReason ?? "?"})`,
);
}
const imagePaths = antgSaveImages(res.images, opts.outputDir, opts.label.replace(/[^a-z0-9]+/gi, "_"));
const outTok = res.usage?.candidatesTokenCount;
const text =
(res.text ? `${res.text}\n\n` : "") +
`${opts.label}: ${imagePaths.length} immagine/i via API diretta Antigravity (${res.model}${outTok ? `, ${outTok} token output` : ""}).` +
(imagePaths.length ? `\n${imagePaths.map((f) => `- ${f}`).join("\n")}` : "");
return { text, imagePaths, engine: "direct", usage: res.usage };
} catch (e: any) {
directError = e?.message ?? String(e);
}
// Fallback: percorso CLI agy (comportamento precedente all'integrazione diretta)
const res = await executeAgy({
prompt: opts.prompt,
mode: "image",
model: opts.model,
stateless: true,
filePaths: opts.images,
outputDir: opts.outputDir,
yolo: true,
signal: opts.signal,
});
// Il path deve esistere davvero: se il CLI fallisce (es. quota) l'output può
// contenere il placeholder del prompt, che non è un'immagine.
const cliPath = res.imagePath && fs.existsSync(res.imagePath) ? res.imagePath : undefined;
const note = cliPath
? `\n\n️ Fallback CLI agy (percorso diretto non disponibile): ${directError ?? "motivo non specificato"}`
: `\n\n⚠️ Nessuna immagine prodotta. API diretta: ${directError ?? "errore non specificato"}${
res.exitCode ? ` | CLI agy exitCode ${res.exitCode}` : ""
}`;
return {
text: res.text + note,
imagePaths: cliPath ? [cliPath] : [],
engine: "cli",
exitCode: res.exitCode,
directError,
};
}
// ---------------------------------------------------------------------------
// Registrazione microfono (F12) e trascrizione
// ---------------------------------------------------------------------------
@@ -1329,6 +1453,162 @@ async function antgLoadCodeAssist(token: string, base: string): Promise<string>
return String(pid);
}
// ---------------------------------------------------------------------------
// Generazione immagini via protocollo diretto Antigravity (stesso token OAuth
// del canale account). Validato con probe il 13/09/2026:
// POST v1internal:generateContent (NON streaming), requestType "image_gen",
// responseModalities ["IMAGE"], imageConfig {aspectRatio, imageSize};
// immagine in candidates[0].content.parts[].inlineData (base64, image/jpeg).
// I parts con thought=true sono immagini intermedie di ragionamento: ignorarli.
// HTTP 200 può non contenere immagini: in tal caso candidate.finishMessage
// spiega il rifiuto (nessun addebito) → si ricade sul percorso CLI.
// gemini-3-pro-image NON è disponibile su questo account (HTTP 404).
// ---------------------------------------------------------------------------
const ANTG_IMAGE_MODEL_DEFAULT = "gemini-3.1-flash-image"; // Nano Banana 2
const ANTG_IMAGE_MAX_OUTPUT_TOKENS = 32768; // 4K richiede ~2.9K token di output
// Quota immagine: l'API diretta risponde 429 RESOURCE_EXHAUSTED con
// "quota will reset after XhYmZs" quando la capacità del modello è satura.
// Finché il blocco è attivo si va diretti al percorso CLI, evitando round-trip
// inutili.
let antgImageBlockedUntilMs = 0;
let antgImageBlockedReason = "";
function antgParseQuotaDelayMs(message: string): number {
const m = message.match(/reset after\s*(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/i);
if (!m) return 0;
const h = Number(m[1] ?? 0);
const min = Number(m[2] ?? 0);
const sec = Number(m[3] ?? 0);
return ((h * 60 + min) * 60 + sec) * 1000;
}
interface AntgInlineImage {
mimeType: string;
data: string;
}
interface AntgImageCallResult {
images: AntgInlineImage[];
text: string;
finishReason?: string;
finishMessage?: string;
usage?: any;
model: string;
}
function antgMimeFromPath(file: string): string {
switch (path.extname(file).toLowerCase()) {
case ".png":
return "image/png";
case ".webp":
return "image/webp";
case ".gif":
return "image/gif";
case ".jpg":
case ".jpeg":
return "image/jpeg";
default:
return "image/png";
}
}
async function antgGenerateImages(opts: {
prompt: string;
images?: string[];
aspectRatio?: string;
imageSize?: string;
model?: string;
signal?: AbortSignal;
}): Promise<AntgImageCallResult> {
const model = opts.model ?? ANTG_IMAGE_MODEL_DEFAULT;
const { pid, base } = await antgGetProject();
const parts: any[] = [];
for (const file of opts.images ?? []) {
try {
parts.push({
inlineData: { mimeType: antgMimeFromPath(file), data: fs.readFileSync(file).toString("base64") },
});
} catch (e: any) {
throw new Error(`Impossibile leggere l'immagine ${file}: ${e.message}`);
}
}
parts.push({ text: opts.prompt });
const generationConfig: any = {
temperature: 1.0,
topP: 0.95,
topK: 40,
maxOutputTokens: ANTG_IMAGE_MAX_OUTPUT_TOKENS,
responseModalities: ["IMAGE"], // 1 sola immagine (TEXT+IMAGE può duplicarla)
};
const imageConfig: any = {};
if (opts.aspectRatio) imageConfig.aspectRatio = opts.aspectRatio;
if (opts.imageSize) imageConfig.imageSize = opts.imageSize;
if (Object.keys(imageConfig).length) generationConfig.imageConfig = imageConfig;
const envelope = {
project: pid,
model,
requestType: "image_gen",
userAgent: "antigravity",
requestId: `agent-${crypto.randomUUID().replace(/-/g, "")}`,
request: { contents: [{ role: "user", parts }], generationConfig },
};
const call = (tok: string) =>
fetch(`${base}/v1internal:generateContent`, {
method: "POST",
headers: antgHeaders(tok),
body: JSON.stringify(envelope),
signal: antgAbortSignal(opts.signal, 300_000),
});
let resp = await call(await antgGetAccessToken());
if (resp.status === 401 || resp.status === 403) {
antgToken = null;
resp = await call(await antgGetAccessToken());
}
const raw = await resp.text();
if (!resp.ok) {
if (resp.status === 429 || /QUOTA_EXHAUSTED|RESOURCE_EXHAUSTED/.test(raw)) {
const delay = antgParseQuotaDelayMs(raw);
antgImageBlockedUntilMs = Date.now() + (delay || 30 * 60 * 1000);
antgImageBlockedReason = raw.replace(/\s+/g, " ").slice(0, 200);
}
throw new Error(`Antigravity image HTTP ${resp.status}: ${raw.slice(0, 300)}`);
}
let payload: any;
try {
payload = JSON.parse(raw);
} catch {
throw new Error(`Risposta immagine non JSON: ${raw.slice(0, 200)}`);
}
const gem = payload?.response ?? payload;
const candidate = gem?.candidates?.[0];
const responseParts: any[] = candidate?.content?.parts ?? [];
const images: AntgInlineImage[] = [];
const texts: string[] = [];
for (const part of responseParts) {
const inline = part?.inlineData ?? part?.inline_data;
if (inline?.data) {
if (part.thought === true) continue; // immagine intermedia di ragionamento
images.push({ mimeType: inline.mimeType ?? inline.mime_type ?? "image/png", data: inline.data });
} else if (typeof part?.text === "string" && part.thought !== true) {
texts.push(part.text);
}
}
return {
images,
text: texts.join("\n").trim(),
finishReason: candidate?.finishReason,
finishMessage: candidate?.finishMessage,
usage: gem?.usageMetadata,
model,
};
}
async function antgGetProject(): Promise<{ pid: string; base: string }> {
if (antgProject) return antgProject;
const token = await antgGetAccessToken();
@@ -1598,9 +1878,75 @@ const ANTG_PROVIDER_MODELS: AntgProviderModelDef[] = [
{ id: "gpt-oss-120b-medium", name: "GPT-OSS 120B (Medium)", reasoning: false, images: false, contextWindow: 262144, maxTokens: 32768 },
];
// Modelli attualmente registrati nel provider (statici all'avvio, aggiornabili
// con /agy:refresh-models dal catalogo vivo fetchAvailableModels).
let antgRegisteredModels: AntgProviderModelDef[] = ANTG_PROVIDER_MODELS;
// ---------------------------------------------------------------------------
// Cache persistente dei modelli (~/.config/agy-pi/models-cache.json)
// Il catalogo vivo scaricato da /agy:refresh-models viene salvato qui, così la
// lista sopravvive al riavvio dell'estensione. Se il file è assente, illeggibile
// o corrotto si riparte da ANTG_PROVIDER_MODELS senza errori fatali.
// ---------------------------------------------------------------------------
const MODELS_CACHE_FILE = path.join(CONFIG_DIR, "models-cache.json");
const MODELS_CACHE_VERSION = 1;
interface AntgModelsCache {
version: number;
refreshedAt: string;
project?: string;
models: AntgProviderModelDef[];
}
function isAntgModelDef(value: any): value is AntgProviderModelDef {
return (
value !== null &&
typeof value === "object" &&
typeof value.id === "string" &&
value.id.trim() !== "" &&
typeof value.contextWindow === "number" &&
value.contextWindow > 0 &&
typeof value.maxTokens === "number" &&
value.maxTokens > 0
);
}
function loadModelsCache(): AntgModelsCache | null {
try {
const raw = JSON.parse(fs.readFileSync(MODELS_CACHE_FILE, "utf8"));
if (raw?.version !== MODELS_CACHE_VERSION) return null;
if (!Array.isArray(raw.models) || raw.models.length === 0) return null;
if (!raw.models.every(isAntgModelDef)) return null;
return raw as AntgModelsCache;
} catch {
return null; // assente, illeggibile o corrotto → default
}
}
function saveModelsCache(models: AntgProviderModelDef[], projectId?: string) {
try {
fs.mkdirSync(CONFIG_DIR, { recursive: true });
const tmp = `${MODELS_CACHE_FILE}.tmp`;
fs.writeFileSync(
tmp,
JSON.stringify(
{
version: MODELS_CACHE_VERSION,
refreshedAt: new Date().toISOString(),
project: projectId,
models,
},
null,
2,
),
{ mode: 0o600 },
);
fs.renameSync(tmp, MODELS_CACHE_FILE); // scrittura atomica
} catch {
/* ignora */
}
}
// Modelli attualmente registrati nel provider: cache persistente se presente,
// altrimenti i default statici. Aggiornabili con /agy:refresh-models (che ora
// salva su disco) e ripristinabili con /agy:models-reset.
let antgRegisteredModels: AntgProviderModelDef[] = loadModelsCache()?.models ?? ANTG_PROVIDER_MODELS;
// Configurazione del provider "antigravity" (riusata all'avvio e al refresh).
function antgProviderConfig(models: AntgProviderModelDef[]) {
@@ -2056,6 +2402,9 @@ export default function agyExtension(pi: ExtensionAPI) {
text: Type.Optional(Type.String({ description: "Testo da includere nell'immagine (tra virgolette)." })),
negative: Type.Optional(Type.String({ description: "Cosa evitare, in framing positivo (es. 'nessun testo')." })),
model: Type.Optional(Type.String({ description: "Modello agy." })),
imageSize: Type.Optional(
Type.String({ description: 'Risoluzione immagine via API diretta: "512", "1K", "2K", "4K" (default 1K).' }),
),
outputDir: Type.Optional(Type.String({ description: "Cartella dove copiare l'immagine generata." })),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
@@ -2072,17 +2421,18 @@ export default function agyExtension(pi: ExtensionAPI) {
const prompt = parts.join(". ") + IMG_SUFFIX;
toolUpdate(onUpdate, "agy_generate: generazione immagine...");
const res = await executeAgy({
const res = await agyImageTask({
prompt,
mode: "image",
aspectRatio: p.aspectRatio,
imageSize: p.imageSize ?? "1K",
model: p.model,
stateless: true,
outputDir: p.outputDir,
signal,
label: "agy_generate",
});
return {
content: [{ type: "text", text: res.text }],
details: { imagePath: res.imagePath, exitCode: res.exitCode },
details: { imagePath: res.imagePaths[0], imagePaths: res.imagePaths, engine: res.engine, directError: res.directError, exitCode: res.exitCode },
};
},
});
@@ -2119,19 +2469,17 @@ export default function agyExtension(pi: ExtensionAPI) {
prompt += IMG_SUFFIX;
toolUpdate(onUpdate, "agy_edit: modifica immagine...");
const res = await executeAgy({
const res = await agyImageTask({
prompt,
mode: "image",
images: [p.baseImage],
model: p.model,
stateless: true,
filePaths: [p.baseImage],
outputDir: p.outputDir,
yolo: true,
signal,
label: "agy_edit",
});
return {
content: [{ type: "text", text: res.text }],
details: { imagePath: res.imagePath, baseImage: p.baseImage, exitCode: res.exitCode },
details: { imagePath: res.imagePaths[0], imagePaths: res.imagePaths, baseImage: p.baseImage, engine: res.engine, directError: res.directError, exitCode: res.exitCode },
};
},
});
@@ -2164,19 +2512,17 @@ export default function agyExtension(pi: ExtensionAPI) {
IMG_SUFFIX;
toolUpdate(onUpdate, "agy_inpaint: modifica zona specifica...");
const res = await executeAgy({
const res = await agyImageTask({
prompt,
mode: "image",
images: [p.baseImage],
model: p.model,
stateless: true,
filePaths: [p.baseImage],
outputDir: p.outputDir,
yolo: true,
signal,
label: "agy_inpaint",
});
return {
content: [{ type: "text", text: res.text }],
details: { imagePath: res.imagePath, baseImage: p.baseImage, exitCode: res.exitCode },
details: { imagePath: res.imagePaths[0], imagePaths: res.imagePaths, baseImage: p.baseImage, engine: res.engine, directError: res.directError, exitCode: res.exitCode },
};
},
});
@@ -2208,19 +2554,17 @@ export default function agyExtension(pi: ExtensionAPI) {
IMG_SUFFIX;
toolUpdate(onUpdate, "agy_style_transfer: applica stile...");
const res = await executeAgy({
const res = await agyImageTask({
prompt,
mode: "image",
images: [p.baseImage],
model: p.model,
stateless: true,
filePaths: [p.baseImage],
outputDir: p.outputDir,
yolo: true,
signal,
label: "agy_style_transfer",
});
return {
content: [{ type: "text", text: res.text }],
details: { imagePath: res.imagePath, baseImage: p.baseImage, exitCode: res.exitCode },
details: { imagePath: res.imagePaths[0], imagePaths: res.imagePaths, baseImage: p.baseImage, engine: res.engine, directError: res.directError, exitCode: res.exitCode },
};
},
});
@@ -2253,19 +2597,17 @@ export default function agyExtension(pi: ExtensionAPI) {
IMG_SUFFIX;
toolUpdate(onUpdate, "agy_compose: combina immagini...");
const res = await executeAgy({
const res = await agyImageTask({
prompt,
mode: "image",
images: p.images,
model: p.model,
stateless: true,
filePaths: p.images,
outputDir: p.outputDir,
yolo: true,
signal,
label: "agy_compose",
});
return {
content: [{ type: "text", text: res.text }],
details: { imagePath: res.imagePath, images: p.images, exitCode: res.exitCode },
details: { imagePath: res.imagePaths[0], imagePaths: res.imagePaths, images: p.images, engine: res.engine, directError: res.directError, exitCode: res.exitCode },
};
},
});
@@ -2296,19 +2638,17 @@ export default function agyExtension(pi: ExtensionAPI) {
IMG_SUFFIX;
toolUpdate(onUpdate, "agy_character: mantieni consistenza personaggio...");
const res = await executeAgy({
const res = await agyImageTask({
prompt,
mode: "image",
images: [p.referenceImage],
model: p.model,
stateless: true,
filePaths: [p.referenceImage],
outputDir: p.outputDir,
yolo: true,
signal,
label: "agy_character",
});
return {
content: [{ type: "text", text: res.text }],
details: { imagePath: res.imagePath, referenceImage: p.referenceImage, exitCode: res.exitCode },
details: { imagePath: res.imagePaths[0], imagePaths: res.imagePaths, referenceImage: p.referenceImage, engine: res.engine, directError: res.directError, exitCode: res.exitCode },
};
},
});
@@ -2618,10 +2958,11 @@ export default function agyExtension(pi: ExtensionAPI) {
if (!models.length) throw new Error("Il catalogo non ha restituito modelli utilizzabili");
antgRegisteredModels = models;
pi.registerProvider("antigravity", antgProviderConfig(models));
saveModelsCache(models, pid);
ctx.ui.setStatus("agy:refresh-models", "");
const lines = models.map((m) => ` ${m.id}${m.name}${m.reasoning ? " (thinking)" : ""}`).join("\n");
ctx.ui.notify(
`✅ Provider antigravity aggiornato: ${models.length} modelli (project ${pid})\n${lines}`,
`✅ Provider antigravity aggiornato: ${models.length} modelli (project ${pid})\n${lines}\n\n💾 Lista salvata in ${MODELS_CACHE_FILE} (persistente al riavvio).`,
"info",
);
} catch (e: any) {
@@ -2631,6 +2972,31 @@ export default function agyExtension(pi: ExtensionAPI) {
},
});
// =========================================================================
// Comando: /agy:models-reset — rimuove la cache persistente dei modelli e
// ripristina la lista statica di default.
// =========================================================================
pi.registerCommand("agy:models-reset", {
description:
"Rimuove la cache persistente dei modelli (~/.config/agy-pi/models-cache.json) e ri-registra il provider con la lista di default.",
handler: async (_args, ctx) => {
let removed = false;
try {
removed = fs.existsSync(MODELS_CACHE_FILE);
fs.rmSync(MODELS_CACHE_FILE, { force: true });
} catch (e: any) {
ctx.ui.notify(`Errore rimozione cache modelli: ${e.message}`, "error");
return;
}
antgRegisteredModels = ANTG_PROVIDER_MODELS;
pi.registerProvider("antigravity", antgProviderConfig(ANTG_PROVIDER_MODELS));
ctx.ui.notify(
`♻️ ${removed ? "Cache rimossa" : "Nessuna cache presente"}: provider antigravity ripristinato a ${ANTG_PROVIDER_MODELS.length} modelli di default.`,
"info",
);
},
});
// =========================================================================
// Comandi interattivi
// =========================================================================