Aggiunti 10 tool specializzati: generate, edit, inpaint, style_transfer, compose, character, analyze, transcribe, video, models

This commit is contained in:
dev
2026-08-09 20:07:01 +02:00
parent f12199fd2e
commit 491f67b99f
+523 -134
View File
@@ -1,19 +1,13 @@
/**
* agy-pi — Google Antigravity CLI (agy) come subagent multimodale dentro pi.
*
* Permette a pi (agente principale, anche text-only) di delegare a agy task che
* richiedono Gemini multimodale: conversazioni multi-turno, generazione immagini,
* analisi di immagini/audio/video.
* Espone un set completo di strumenti specializzati per generazione ed editing
* di immagini, analisi di file, trascrizione audio, analisi video e gestione
* conversazione. Ogni strumento costruisce un prompt ottimizzato secondo le
* best practice di Gemini/Nano Banana (Keep+Change+Add+Render, un cambiamento
* per turno, ri-attacco dell'immagine base, ecc.).
*
* Strumenti registrati:
* - agy : delega un task a agy (chat, generazione o analisi)
* - agy_conversation : gestione dello stato conversazione (id, list, reset)
*
* Comandi registrati:
* - /agy <prompt> : invia un prompt a agy in modo interattivo
* - /agy:new : forza una nuova conversazione
* - /agy:list : elenca le conversazioni
* - /agy:reset : azzera lo stato conversazione
* L'agente (pi) decide quale strumento usare in base al task.
*/
import { execFile } from "node:child_process";
@@ -46,14 +40,14 @@ function findAgy(): string {
"agy",
].filter(Boolean) as string[];
for (const c of candidates) {
if (c === "agy") return c; // lascia che lo risolva la shell/PATH
if (c === "agy") return c;
if (fs.existsSync(c)) return c;
}
return "agy";
}
// ---------------------------------------------------------------------------
// Helper: ultimo conversation_id (dal file .db più recente in conversations/)
// Helper: ultimo conversation_id
// ---------------------------------------------------------------------------
function getLatestConversationId(): string | null {
try {
@@ -70,9 +64,6 @@ function getLatestConversationId(): string | null {
}
}
// ---------------------------------------------------------------------------
// Helper: leggere/scrivere lo stato conversazione
// ---------------------------------------------------------------------------
function readState(): string | null {
try {
return fs.readFileSync(STATE_FILE, "utf8").trim() || null;
@@ -132,49 +123,80 @@ async function runAgy(
}
// ---------------------------------------------------------------------------
// Helper: costruire gli argomenti agy
// Helper: estrarre il percorso immagine dall'output
// ---------------------------------------------------------------------------
interface AgyOptions {
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;
}
// ---------------------------------------------------------------------------
// Helper: copiare l'immagine generata in una cartella di output
// ---------------------------------------------------------------------------
function copyImage(src: string | undefined, outputDir?: string): string | undefined {
if (!src || !outputDir) return src;
try {
fs.mkdirSync(outputDir, { recursive: true });
const dest = path.join(outputDir, path.basename(src));
fs.copyFileSync(src, dest);
return dest;
} catch {
return src;
}
}
// ---------------------------------------------------------------------------
// Helper: esecuzione comune di un tool agy
// ---------------------------------------------------------------------------
interface AgyExecOptions {
prompt: string;
mode?: "chat" | "image" | "analyze";
model?: string;
effort?: "low" | "medium" | "high";
newConversation?: boolean;
addDir?: string;
filePath?: string;
stateless?: boolean; // non continua la conversazione (ri-attacca l'immagine base)
addDirs?: string[];
filePaths?: string[];
yolo?: boolean;
resumeId?: string;
timeoutMs?: number;
outputDir?: string;
signal?: AbortSignal;
}
function buildArgs(opts: AgyOptions): string[] {
async function executeAgy(opts: AgyExecOptions) {
const args: string[] = ["-p", opts.prompt];
// add-dir (per analisi di file)
// add-dir per i file
const dirs = new Set<string>();
if (opts.addDir) dirs.add(opts.addDir);
if (opts.filePath) dirs.add(path.dirname(opts.filePath));
for (const d of opts.addDirs ?? []) if (d) dirs.add(d);
for (const f of opts.filePaths ?? []) if (f) dirs.add(path.dirname(f));
for (const d of dirs) args.push("--add-dir", d);
// conversazione
const useState = !opts.stateless && !opts.newConversation;
let convId: string | null = null;
if (opts.resumeId) convId = opts.resumeId;
else if (!opts.newConversation) convId = readState();
if (opts.newConversation) convId = null;
else if (opts.stateless) convId = null;
else convId = readState();
if (convId) args.push("--conversation", convId);
if (opts.model) args.push("--model", opts.model);
if (opts.effort) args.push("--effort", opts.effort);
if (opts.yolo) args.push("--dangerously-skip-permissions");
return args;
const timeout = opts.timeoutMs ?? (opts.mode === "image" ? IMAGE_TIMEOUT_MS : DEFAULT_TIMEOUT_MS);
const r = await runAgy(args, timeout, opts.signal);
// aggiorna stato conversazione (solo se non stateless)
if (r.exitCode === 0 && !opts.stateless) {
const latest = getLatestConversationId();
if (latest) writeState(latest);
}
// ---------------------------------------------------------------------------
// Helper: formattare il risultato per il tool
// ---------------------------------------------------------------------------
function formatResult(r: RunResult, convId: string | null): string {
let text = r.output.trim();
if (r.error) {
// agy scrive i log su stderr; mostriamo solo errori significativi
const err = r.error
.split("\n")
.filter((l) => !/logging before google\.Init/i.test(l))
@@ -182,116 +204,489 @@ function formatResult(r: RunResult, convId: string | null): string {
.trim();
if (err && !text) text = err;
}
if (convId) text += `\n\n[conversazione agy: ${convId}]`;
return text;
const imagePath = copyImage(extractImagePath(text), opts.outputDir);
if (imagePath && opts.outputDir) {
text += `\n\n[immagine copiata in: ${imagePath}]`;
}
return {
text,
imagePath,
conversationId: convId,
exitCode: r.exitCode,
};
}
// ---------------------------------------------------------------------------
// Helper: costruire il suffisso "IMAGE_PATH" per i tool immagine
// ---------------------------------------------------------------------------
const IMG_SUFFIX =
"\n\nAlla fine della risposta, scrivi su una riga esattamente: IMAGE_PATH: <percorso assoluto dell'immagine generata>";
// ---------------------------------------------------------------------------
// Estensione
// ---------------------------------------------------------------------------
export default function agyExtension(pi: ExtensionAPI) {
// --- Tool: agy ----------------------------------------------------------
// =========================================================================
// TOOL: agy — generico (chat / image / analyze)
// =========================================================================
pi.registerTool({
name: "agy",
label: "agy (Antigravity subagent)",
description:
"Delega un task a Google Antigravity CLI (agy), che usa Gemini multimodale. " +
"Utile per: conversazioni di ragionamento multi-turno, generazione di immagini, " +
"e analisi di file immagine/audio/video (passa filePath/addDir). " +
"La conversazione viene mantenuta tra le chiamate (multi-shot).",
"Delega un task generico a Google Antigravity CLI (agy), che usa Gemini multimodale. " +
"Utile per conversazioni di ragionamento multi-turno, generazione immagini e analisi di file. " +
"Per task specializzati usa agy_generate, agy_edit, agy_inpaint, agy_style_transfer, agy_compose, " +
"agy_analyze, agy_transcribe, agy_video.",
parameters: Type.Object({
prompt: Type.String({
description:
"Il task/prompt da dare a agy. Per generare un'immagine, descrivila. " +
"Per analizzare un file, chiedi di analizzarlo e indica il percorso.",
}),
prompt: Type.String({ description: "Il task/prompt da dare a agy." }),
mode: Type.Optional(
Type.Union(
[
Type.Literal("chat"),
Type.Literal("image"),
Type.Literal("analyze"),
],
{ description: "chat (default) | image (genera immagine) | analyze (analizza file)" },
),
),
model: Type.Optional(
Type.String({
description:
"Modello agy (es. 'Gemini 3.1 Pro (High)', 'Claude Opus 4.6 (Thinking)'). " +
"Ometti per usare il default.",
Type.Union([Type.Literal("chat"), Type.Literal("image"), Type.Literal("analyze")], {
description: "chat (default) | image | analyze",
}),
),
model: Type.Optional(Type.String({ description: "Modello agy (es. 'Gemini 3.1 Pro (High)')." })),
effort: Type.Optional(
Type.Union(
[Type.Literal("low"), Type.Literal("medium"), Type.Literal("high")],
{ description: "Livello di ragionamento (default: medium)" },
),
),
newConversation: Type.Optional(
Type.Boolean({
description:
"true per forzare una nuova conversazione agy (dimentica i turni precedenti). Default: false.",
}),
),
addDir: Type.Optional(
Type.String({
description:
"Cartella da aggiungere al workspace di agy (necessaria per analizzare file).",
}),
),
filePath: Type.Optional(
Type.String({
description:
"Percorso del file (immagine/audio/video) da analizzare. La sua cartella viene aggiunta automaticamente.",
}),
),
yolo: Type.Optional(
Type.Boolean({
description:
"true per usare --dangerously-skip-permissions (auto-approva gli strumenti). " +
"Necessario per analisi audio/video. Usa solo in ambienti fidati.",
}),
Type.Union([Type.Literal("low"), Type.Literal("medium"), Type.Literal("high")]),
),
newConversation: Type.Optional(Type.Boolean({ description: "true per forzare una nuova conversazione." })),
addDir: Type.Optional(Type.String({ description: "Cartella da aggiungere al workspace agy." })),
filePath: Type.Optional(Type.String({ description: "File (immagine/audio/video) da analizzare." })),
yolo: Type.Optional(Type.Boolean({ description: "true per --dangerously-skip-permissions." })),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
const opts = params as AgyOptions;
const args = buildArgs(opts);
const timeout = opts.mode === "image" ? IMAGE_TIMEOUT_MS : DEFAULT_TIMEOUT_MS;
onUpdate?.(`agy: ${opts.mode === "image" ? "generazione immagine" : "elaborazione"}...`);
const r = await runAgy(args, timeout, signal);
// Aggiorna lo stato conversazione
let convId = readState();
if (r.exitCode === 0) {
const latest = getLatestConversationId();
if (latest) {
writeState(latest);
convId = latest;
}
}
const text = formatResult(r, convId);
// Rileva il percorso di un'immagine generata
const imgMatch = text.match(/IMAGE_PATH:\s*(\S+)/i) || text.match(/(\/[^\s]+\.(?:png|jpe?g|webp|gif))/i);
const imagePath = imgMatch ? imgMatch[1] : undefined;
const p = params as any;
onUpdate?.(`agy: ${p.mode === "image" ? "generazione immagine" : "elaborazione"}...`);
const res = await executeAgy({
prompt: p.prompt,
mode: p.mode,
model: p.model,
effort: p.effort,
newConversation: p.newConversation,
addDirs: p.addDir ? [p.addDir] : [],
filePaths: p.filePath ? [p.filePath] : [],
yolo: p.yolo,
signal,
});
return {
content: [{ type: "text", text }],
details: {
conversationId: convId,
exitCode: r.exitCode,
imagePath,
model: opts.model,
},
content: [{ type: "text", text: res.text }],
details: { conversationId: res.conversationId, exitCode: res.exitCode, imagePath: res.imagePath, model: p.model },
};
},
});
// --- Tool: agy_conversation ---------------------------------------------
// =========================================================================
// TOOL: agy_generate — generazione immagine strutturata
// =========================================================================
pi.registerTool({
name: "agy_generate",
label: "agy generate image",
description:
"Genera un'immagine da zero con parametri strutturati (soggetto, azione, luogo, composizione, stile, luce, formato). " +
"Usa la formula narrativa [Soggetto]+[Azione]+[Luogo]+[Composizione]+[Stile] per il massimo controllo.",
parameters: Type.Object({
subject: Type.String({ description: "Soggetto: chi/cosa è nell'immagine. Sii specifico." }),
action: Type.Optional(Type.String({ description: "Azione: cosa sta succedendo." })),
location: Type.Optional(Type.String({ description: "Luogo/contesto/sfondo." })),
composition: Type.Optional(
Type.String({ description: "Composizione: inquadratura (es. 'close-up', 'wide shot', 'low-angle')." }),
),
style: Type.Optional(Type.String({ description: "Stile: estetica (es. 'fotorealistico', 'watercolor', 'film noir')." })),
lighting: Type.Optional(Type.String({ description: "Illuminazione (es. 'golden hour', 'softbox', 'neon')." })),
aspectRatio: Type.Optional(
Type.String({ description: "Formato (es. '1:1', '16:9', '9:16', '4:3', '21:9')." }),
),
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." })),
outputDir: Type.Optional(Type.String({ description: "Cartella dove copiare l'immagine generata." })),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
const p = params as any;
const parts = [`Genera un'immagine: ${p.subject}`];
if (p.action) parts.push(`Azione: ${p.action}`);
if (p.location) parts.push(`Luogo/contesto: ${p.location}`);
if (p.composition) parts.push(`Composizione: ${p.composition}`);
if (p.style) parts.push(`Stile: ${p.style}`);
if (p.lighting) parts.push(`Illuminazione: ${p.lighting}`);
if (p.aspectRatio) parts.push(`Formato/aspect ratio: ${p.aspectRatio}`);
if (p.text) parts.push(`Includi il testo "${p.text}" nell'immagine.`);
if (p.negative) parts.push(`Evita: ${p.negative}.`);
const prompt = parts.join(". ") + IMG_SUFFIX;
onUpdate?.("agy_generate: generazione immagine...");
const res = await executeAgy({
prompt,
mode: "image",
model: p.model,
stateless: true,
outputDir: p.outputDir,
signal,
});
return {
content: [{ type: "text", text: res.text }],
details: { imagePath: res.imagePath, exitCode: res.exitCode },
};
},
});
// =========================================================================
// TOOL: agy_edit — editing controllato (Keep + Change + Add + Render)
// =========================================================================
pi.registerTool({
name: "agy_edit",
label: "agy edit image",
description:
"Modifica un'immagine esistente in modo controllato usando la formula Keep+Change+Add+Render. " +
"Specifica cosa mantenere invariato, cosa cambiare, cosa aggiungere e il target di resa. " +
"Un solo cambiamento per turno per il massimo controllo.",
parameters: Type.Object({
baseImage: Type.String({ description: "Percorso dell'immagine base da modificare." }),
keep: Type.String({ description: "Cosa mantenere invariato (es. 'soggetto, posa, illuminazione, composizione')." }),
change: Type.String({ description: "Cosa cambiare (es. 'il colore del divano in blu navy')." }),
add: Type.Optional(Type.String({ description: "Cosa aggiungere (es. 'un vaso sul tavolo')." })),
render: Type.Optional(
Type.String({ description: "Target di resa (es. 'foto premium', 'stile editoriale', 'formato 4:5')." }),
),
preserveAspectRatio: Type.Optional(
Type.Boolean({ description: "true per non cambiare l'aspect ratio dell'input." }),
),
model: Type.Optional(Type.String({ description: "Modello agy." })),
outputDir: Type.Optional(Type.String({ description: "Cartella dove copiare l'immagine risultante." })),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
const p = params as any;
const keep = p.preserveAspectRatio ? `${p.keep}. Non cambiare l'aspect ratio dell'immagine di input.` : p.keep;
let prompt = `Usando l'immagine al percorso ${p.baseImage} come base, mantieni ${keep} invariato. Cambia ${p.change}.`;
if (p.add) prompt += ` Aggiungi ${p.add}.`;
if (p.render) prompt += ` Render come ${p.render}.`;
prompt += IMG_SUFFIX;
onUpdate?.("agy_edit: modifica immagine...");
const res = await executeAgy({
prompt,
mode: "image",
model: p.model,
stateless: true,
filePaths: [p.baseImage],
outputDir: p.outputDir,
signal,
});
return {
content: [{ type: "text", text: res.text }],
details: { imagePath: res.imagePath, baseImage: p.baseImage, exitCode: res.exitCode },
};
},
});
// =========================================================================
// TOOL: agy_inpaint — editing di una zona specifica (semantic masking)
// =========================================================================
pi.registerTool({
name: "agy_inpaint",
label: "agy inpaint (edit zona specifica)",
description:
"Modifica SOLO una parte specifica dell'immagine lasciando il resto intatto (inpainting / semantic masking). " +
"Definisci l'elemento target e la sostituzione; tutto il resto resta identico.",
parameters: Type.Object({
baseImage: Type.String({ description: "Percorso dell'immagine base." }),
target: Type.String({ description: "L'elemento specifico da modificare (es. 'la maglietta del soggetto')." }),
replacement: Type.String({ description: "La nuova descrizione dell'elemento (es. 'una maglietta rossa')." }),
keepRest: Type.Optional(
Type.String({ description: "Cosa mantenere identico (default: tutto il resto)." }),
),
model: Type.Optional(Type.String({ description: "Modello agy." })),
outputDir: Type.Optional(Type.String({ description: "Cartella dove copiare l'immagine risultante." })),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
const p = params as any;
const keep = p.keepRest ?? "tutto il resto";
const prompt =
`Usando l'immagine al percorso ${p.baseImage}, cambia SOLO ${p.target} in ${p.replacement}. ` +
`Mantieni ${keep} esattamente identico, preservando stile, illuminazione e composizione originali.` +
IMG_SUFFIX;
onUpdate?.("agy_inpaint: modifica zona specifica...");
const res = await executeAgy({
prompt,
mode: "image",
model: p.model,
stateless: true,
filePaths: [p.baseImage],
outputDir: p.outputDir,
signal,
});
return {
content: [{ type: "text", text: res.text }],
details: { imagePath: res.imagePath, baseImage: p.baseImage, exitCode: res.exitCode },
};
},
});
// =========================================================================
// TOOL: agy_style_transfer — applica uno stile preservando il contenuto
// =========================================================================
pi.registerTool({
name: "agy_style_transfer",
label: "agy style transfer",
description:
"Applica uno stile artistico a un'immagine preservando il contenuto e la composizione originali " +
"(style transfer). Es. trasformare una foto in un dipinto Van Gogh.",
parameters: Type.Object({
baseImage: Type.String({ description: "Percorso dell'immagine base." }),
style: Type.String({ description: "Lo stile da applicare (es. 'pittura Van Gogh', 'architectural drawing', 'film noir')." }),
preserve: Type.Optional(
Type.String({ description: "Cosa preservare (default: 'la composizione originale')." }),
),
model: Type.Optional(Type.String({ description: "Modello agy." })),
outputDir: Type.Optional(Type.String({ description: "Cartella dove copiare l'immagine risultante." })),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
const p = params as any;
const preserve = p.preserve ?? "la composizione originale";
const prompt =
`Usando l'immagine al percorso ${p.baseImage}, trasforma il contenuto nello stile di ${p.style}. ` +
`Preserva ${preserve} ma renderizzala con lo stile richiesto.` +
IMG_SUFFIX;
onUpdate?.("agy_style_transfer: applica stile...");
const res = await executeAgy({
prompt,
mode: "image",
model: p.model,
stateless: true,
filePaths: [p.baseImage],
outputDir: p.outputDir,
signal,
});
return {
content: [{ type: "text", text: res.text }],
details: { imagePath: res.imagePath, baseImage: p.baseImage, exitCode: res.exitCode },
};
},
});
// =========================================================================
// TOOL: agy_compose — combina più immagini
// =========================================================================
pi.registerTool({
name: "agy_compose",
label: "agy compose (combina immagini)",
description:
"Combina più immagini in una nuova composizione (multi-image composition / fusion). " +
"Specifica il ruolo di ciascuna immagine e l'istruzione di fusione.",
parameters: Type.Object({
images: Type.Array(Type.String({ description: "Percorsi delle immagini da combinare." }), {
description: "Lista di percorsi immagine (fino a ~6-14).",
}),
instruction: Type.String({
description: "Istruzione di fusione: cosa prendere da ciascuna immagine e come combinarle.",
}),
model: Type.Optional(Type.String({ description: "Modello agy." })),
outputDir: Type.Optional(Type.String({ description: "Cartella dove copiare l'immagine risultante." })),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
const p = params as any;
const refs = (p.images as string[]).map((img, i) => `Immagine ${i + 1}: ${img}`).join("\n");
const prompt =
`Combina le seguenti immagini in una nuova composizione:\n${refs}\n\n` +
`Istruzione: ${p.instruction}. Specifica il ruolo di ciascuna immagine.` +
IMG_SUFFIX;
onUpdate?.("agy_compose: combina immagini...");
const res = await executeAgy({
prompt,
mode: "image",
model: p.model,
stateless: true,
filePaths: p.images,
outputDir: p.outputDir,
signal,
});
return {
content: [{ type: "text", text: res.text }],
details: { imagePath: res.imagePath, images: p.images, exitCode: res.exitCode },
};
},
});
// =========================================================================
// TOOL: agy_character — consistenza personaggio
// =========================================================================
pi.registerTool({
name: "agy_character",
label: "agy character consistency",
description:
"Mantiene la consistenza di un personaggio/oggetto attraverso più generazioni o edit. " +
"Usa un'immagine di riferimento e un token di consistenza per preservare le caratteristiche.",
parameters: Type.Object({
referenceImage: Type.String({ description: "Percorso dell'immagine di riferimento del personaggio." }),
name: Type.String({ description: "Nome/token del personaggio (es. 'Maya-giacca-blu')." }),
features: Type.String({ description: "Caratteristiche immutabili da preservare (es. 'cicatrice sopracciglio sinistro')." }),
task: Type.String({ description: "Cosa fare con il personaggio (es. 'mettilo in una scena notturna')." }),
model: Type.Optional(Type.String({ description: "Modello agy." })),
outputDir: Type.Optional(Type.String({ description: "Cartella dove copiare l'immagine risultante." })),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
const p = params as any;
const prompt =
`Usando l'immagine al percorso ${p.referenceImage} come riferimento del personaggio '${p.name}', ` +
`${p.task}. Mantieni le caratteristiche del personaggio identiche: ${p.features}. ` +
`Usa il token '${p.name}' per riferirti al personaggio.` +
IMG_SUFFIX;
onUpdate?.("agy_character: mantieni consistenza personaggio...");
const res = await executeAgy({
prompt,
mode: "image",
model: p.model,
stateless: true,
filePaths: [p.referenceImage],
outputDir: p.outputDir,
signal,
});
return {
content: [{ type: "text", text: res.text }],
details: { imagePath: res.imagePath, referenceImage: p.referenceImage, exitCode: res.exitCode },
};
},
});
// =========================================================================
// TOOL: agy_analyze — analisi di un file (immagine/audio/video)
// =========================================================================
pi.registerTool({
name: "agy_analyze",
label: "agy analyze file",
description:
"Analizza un file (immagine, audio o video) con Gemini multimodale. " +
"Per immagini: descrizione, OCR, analisi. Per audio: contenuto, trascrizione. Per video: scene, contenuto.",
parameters: Type.Object({
filePath: Type.String({ description: "Percorso del file da analizzare." }),
question: Type.Optional(Type.String({ description: "Domanda specifica sull'analisi." })),
model: Type.Optional(Type.String({ description: "Modello agy." })),
yolo: Type.Optional(Type.Boolean({ description: "true per --dangerously-skip-permissions (necessario per audio/video)." })),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
const p = params as any;
const prompt = p.question
? `Analizza il file al percorso ${p.filePath}. ${p.question}`
: `Analizza il file al percorso ${p.filePath} e descrivilo in dettaglio.`;
onUpdate?.("agy_analyze: analisi file...");
const res = await executeAgy({
prompt,
mode: "analyze",
model: p.model,
stateless: true,
filePaths: [p.filePath],
yolo: p.yolo,
signal,
});
return {
content: [{ type: "text", text: res.text }],
details: { filePath: p.filePath, exitCode: res.exitCode },
};
},
});
// =========================================================================
// TOOL: agy_transcribe — trascrizione audio
// =========================================================================
pi.registerTool({
name: "agy_transcribe",
label: "agy transcribe audio",
description:
"Trascrive il contenuto di un file audio (voce, discorso) in testo. " +
"Richiede --yolo (auto-approva gli strumenti).",
parameters: Type.Object({
filePath: Type.String({ description: "Percorso del file audio (wav, mp3, m4a, ecc.)." }),
language: Type.Optional(Type.String({ description: "Lingua del contenuto (es. 'italiano', 'english')." })),
model: Type.Optional(Type.String({ description: "Modello agy." })),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
const p = params as any;
const lang = p.language ? ` La lingua del contenuto è ${p.language}.` : "";
const prompt =
`Trascrivi il contenuto del file audio al percorso ${p.filePath}.` +
lang +
` Restituisci la trascrizione testuale completa e fedele.`;
onUpdate?.("agy_transcribe: trascrizione audio...");
const res = await executeAgy({
prompt,
mode: "analyze",
model: p.model,
stateless: true,
filePaths: [p.filePath],
yolo: true,
signal,
});
return {
content: [{ type: "text", text: res.text }],
details: { filePath: p.filePath, exitCode: res.exitCode },
};
},
});
// =========================================================================
// TOOL: agy_video — analisi video
// =========================================================================
pi.registerTool({
name: "agy_video",
label: "agy analyze video",
description:
"Analizza un file video: descrive scene, contenuto, codec, risoluzione, tracce audio. " +
"Richiede --yolo (auto-approva gli strumenti).",
parameters: Type.Object({
filePath: Type.String({ description: "Percorso del file video (mp4, mov, ecc.)." }),
question: Type.Optional(Type.String({ description: "Domanda specifica sul video." })),
model: Type.Optional(Type.String({ description: "Modello agy." })),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
const p = params as any;
const prompt = p.question
? `Analizza il video al percorso ${p.filePath}. ${p.question}`
: `Analizza il video al percorso ${p.filePath}: descrivi cosa mostra, codec, risoluzione e se contiene audio.`;
onUpdate?.("agy_video: analisi video...");
const res = await executeAgy({
prompt,
mode: "analyze",
model: p.model,
stateless: true,
filePaths: [p.filePath],
yolo: true,
signal,
});
return {
content: [{ type: "text", text: res.text }],
details: { filePath: p.filePath, exitCode: res.exitCode },
};
},
});
// =========================================================================
// TOOL: agy_models — elenca i modelli disponibili
// =========================================================================
pi.registerTool({
name: "agy_models",
label: "agy list models",
description: "Elenca i modelli disponibili per agy (Gemini, Claude, ecc.).",
parameters: Type.Object({}),
async execute(toolCallId, params, signal) {
const r = await runAgy(["models"], 30_000, signal);
return {
content: [{ type: "text", text: r.output.trim() || r.error || "(nessun output)" }],
details: { exitCode: r.exitCode },
};
},
});
// =========================================================================
// TOOL: agy_conversation — gestione stato conversazione
// =========================================================================
pi.registerTool({
name: "agy_conversation",
label: "agy conversation state",
@@ -300,22 +695,18 @@ export default function agyExtension(pi: ExtensionAPI) {
parameters: Type.Object({
action: Type.Union(
[Type.Literal("id"), Type.Literal("list"), Type.Literal("reset")],
{ description: "id: mostra l'ID corrente | list: elenca conversazioni | reset: azzera lo stato" },
{ description: "id | list | reset" },
),
}),
async execute(toolCallId, params) {
const action = (params as { action: string }).action;
if (action === "id") {
return {
content: [{ type: "text", text: readState() ?? "(nessuna conversazione attiva)" }],
details: {},
};
return { content: [{ type: "text", text: readState() ?? "(nessuna conversazione attiva)" }], details: {} };
}
if (action === "reset") {
resetState();
return { content: [{ type: "text", text: "Stato conversazione azzerato." }], details: {} };
}
// list
try {
if (!fs.existsSync(CONV_DIR)) {
return { content: [{ type: "text", text: "(nessuna conversazione trovata)" }], details: {} };
@@ -341,7 +732,9 @@ export default function agyExtension(pi: ExtensionAPI) {
},
});
// --- Comando: /agy -------------------------------------------------------
// =========================================================================
// Comandi interattivi
// =========================================================================
pi.registerCommand("agy", {
description: "Invia un prompt a agy (subagent Antigravity). Uso: /agy <prompt>",
handler: async (args, ctx) => {
@@ -350,13 +743,9 @@ export default function agyExtension(pi: ExtensionAPI) {
return;
}
ctx.ui.setStatus("agy", "agy: elaborazione...");
const r = await runAgy(buildArgs({ prompt: args.trim() }), DEFAULT_TIMEOUT_MS);
if (r.exitCode === 0) {
const latest = getLatestConversationId();
if (latest) writeState(latest);
}
const res = await executeAgy({ prompt: args.trim() });
ctx.ui.setStatus("agy", "");
ctx.ui.notify(r.output.trim() || r.error || "(nessun output)", "info");
ctx.ui.notify(res.text || "(nessun output)", "info");
},
});