diff --git a/extensions/index.ts b/extensions/index.ts index cbc33bf..8464856 100644 --- a/extensions/index.ts +++ b/extensions/index.ts @@ -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 : 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(); - 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: "; + // --------------------------------------------------------------------------- // 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 ", 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"); }, });