agy-pi: estensione pi per Antigravity CLI (agy) subagent multimodale
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
*.log
|
||||
.DS_Store
|
||||
@@ -0,0 +1,94 @@
|
||||
# agy-pi
|
||||
|
||||
Estensione per **pi** che integra **Google Antigravity CLI (`agy`)** come subagent multimodale.
|
||||
|
||||
Anche se il modello di pi è text-only, questa estensione permette a pi di delegare a agy
|
||||
(che usa **Gemini multimodale**) task che richiedono immagini, audio e video, oltre a
|
||||
conversazioni di ragionamento multi-turno.
|
||||
|
||||
## Capacità
|
||||
|
||||
| Capacità | Tool | Esempio |
|
||||
|---|---|---|
|
||||
| Conversazione multi-turno | `agy` | `agy(prompt="Analizza questo problema...")` |
|
||||
| Generazione immagini | `agy` | `agy(prompt="Genera un'immagine di un gatto cyberpunk", mode="image")` |
|
||||
| Analisi immagini | `agy` | `agy(prompt="Descrivi l'immagine", filePath="/path/foto.jpg")` |
|
||||
| Analisi audio | `agy` | `agy(prompt="Trascrivi l'audio", filePath="/path/voce.wav", yolo=true)` |
|
||||
| Analisi video | `agy` | `agy(prompt="Descrivi il video", filePath="/path/clip.mp4", yolo=true)` |
|
||||
| Gestione stato conversazione | `agy_conversation` | `agy_conversation(action="id" \| "list" \| "reset")` |
|
||||
|
||||
## Installazione
|
||||
|
||||
Requisito: `agy` installato e autenticato (una volta: `agy`, poi login OAuth nel browser).
|
||||
|
||||
```bash
|
||||
# da un repo git
|
||||
pi install git:github.com/<tuo-utente>/agy-pi
|
||||
|
||||
# oppure da una cartella locale
|
||||
pi install /percorso/a/agy-pi
|
||||
```
|
||||
|
||||
Per provare senza installare:
|
||||
|
||||
```bash
|
||||
pi -e /percorso/a/agy-pi
|
||||
```
|
||||
|
||||
## Uso
|
||||
|
||||
### Come tool (chiamato automaticamente dal modello)
|
||||
|
||||
Quando chiedi a pi di generare un'immagine, analizzare un file, o ragionare insieme a un
|
||||
secondo agente, pi può chiamare il tool `agy`. Esempi di prompt:
|
||||
|
||||
```
|
||||
Genera un'immagine di un paesaggio marziano al tramonto.
|
||||
Analizza l'immagine /home/utente/foto.jpg e descrivila.
|
||||
Trascrivi il file audio /home/utente/voce.wav.
|
||||
Chiedi a agy di ragionare su questo problema e poi confronta la sua risposta con la tua.
|
||||
```
|
||||
|
||||
### Come comando interattivo
|
||||
|
||||
```
|
||||
/agy <prompt> # invia un prompt a agy
|
||||
/agy:new # forza una nuova conversazione
|
||||
/agy:list # mostra la conversazione corrente
|
||||
/agy:reset # azzera lo stato conversazione
|
||||
```
|
||||
|
||||
## Parametri del tool `agy`
|
||||
|
||||
| Parametro | Tipo | Descrizione |
|
||||
|---|---|---|
|
||||
| `prompt` | string (obbligatorio) | Il task/prompt per agy |
|
||||
| `mode` | `chat` \| `image` \| `analyze` | Tipo di operazione (default `chat`) |
|
||||
| `model` | string | Modello agy (es. `Gemini 3.1 Pro (High)`, `Claude Opus 4.6 (Thinking)`) |
|
||||
| `effort` | `low` \| `medium` \| `high` | Livello di ragionamento |
|
||||
| `newConversation` | boolean | Forza una nuova conversazione |
|
||||
| `addDir` | string | Cartella da aggiungere al workspace agy |
|
||||
| `filePath` | string | File (immagine/audio/video) da analizzare |
|
||||
| `yolo` | boolean | `--dangerously-skip-permissions` (necessario per audio/video) |
|
||||
|
||||
## Multi-turno
|
||||
|
||||
L'estensione mantiene l'ID della conversazione agy in `~/.agy-chat/conversation_id`.
|
||||
Ogni chiamata a `agy` continua la conversazione precedente, così agy ricorda i turni
|
||||
precedenti (ragionamento multi-shot). Usa `newConversation: true` per ripartire da zero.
|
||||
|
||||
## Note di sicurezza
|
||||
|
||||
- `yolo: true` auto-approva tutti gli strumenti di agy. Usalo solo in ambienti fidati.
|
||||
- Le estensioni pi girano con i permessi completi del sistema. Rivedi il codice prima di
|
||||
installare pacchetti di terze parti.
|
||||
|
||||
## Struttura
|
||||
|
||||
```
|
||||
agy-pi/
|
||||
├── package.json # manifest pi
|
||||
├── extensions/index.ts # estensione (tool + comandi)
|
||||
├── bin/agy-chat.sh # wrapper bash standalone (opzionale)
|
||||
└── README.md
|
||||
```
|
||||
Executable
+133
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# agy-chat.sh — pi <-> agy multi-turn conversation wrapper
|
||||
#
|
||||
# Permette a pi (agente principale) di conversare avanti e indietro con agy
|
||||
# (subagent), mantenendo la memoria della conversazione tra i turni.
|
||||
#
|
||||
# USO:
|
||||
# ./agy-chat.sh "prompt" # avvia o continua la conversazione
|
||||
# ./agy-chat.sh --new "prompt" # forza una nuova conversazione
|
||||
# ./agy-chat.sh --model "Gemini 3.1 Pro (High)" "prompt"
|
||||
# ./agy-chat.sh --effort high "prompt"
|
||||
# ./agy-chat.sh --id # stampa l'ID conversazione corrente
|
||||
# ./agy-chat.sh --list # elenca le conversazioni recenti
|
||||
# ./agy-chat.sh --resume <id> "prompt" # riprende una conversazione specifica
|
||||
# ./agy-chat.sh --reset # azzera lo stato (parte da zero)
|
||||
#
|
||||
# VARIABILI:
|
||||
# AGY_CHAT_STATE cartella di stato (default ~/.agy-chat)
|
||||
# AGY_BIN percorso del binario agy (default: agy su PATH)
|
||||
# =============================================================================
|
||||
set -uo pipefail
|
||||
|
||||
STATE_DIR="${AGY_CHAT_STATE:-$HOME/.agy-chat}"
|
||||
STATE_FILE="$STATE_DIR/conversation_id"
|
||||
DB="${AGY_CHAT_DB:-$HOME/.gemini/antigravity-cli/conversation_summaries.db}"
|
||||
CONV_DIR="${AGY_CHAT_CONV_DIR:-$HOME/.gemini/antigravity-cli/conversations}"
|
||||
AGY_BIN="${AGY_BIN:-agy}"
|
||||
mkdir -p "$STATE_DIR"
|
||||
|
||||
# --- helper: ultimo conversation_id (dal file .db più recente in conversations/) ---
|
||||
latest_id() {
|
||||
# il nome del file .db nella cartella conversations è l'ID conversazione
|
||||
ls -t "$CONV_DIR"/*.db 2>/dev/null | head -1 | xargs -r -n1 basename 2>/dev/null | sed 's/\.db$//'
|
||||
}
|
||||
|
||||
# --- helper: elenca conversazioni recenti ------------------------------------
|
||||
list_convs() {
|
||||
if [[ ! -d "$CONV_DIR" ]] || [[ -z "$(ls -A "$CONV_DIR"/*.db 2>/dev/null)" ]]; then
|
||||
echo "(nessuna conversazione trovata)"
|
||||
return 0
|
||||
fi
|
||||
for f in $(ls -t "$CONV_DIR"/*.db 2>/dev/null); do
|
||||
id="$(basename "$f" .db)"
|
||||
mtime="$(stat -c '%y' "$f" 2>/dev/null | cut -d. -f1)"
|
||||
echo "$id\t$mtime"
|
||||
done
|
||||
}
|
||||
|
||||
# --- parse argomenti ----------------------------------------------------------
|
||||
NEW=0
|
||||
MODEL=""
|
||||
EFFORT=""
|
||||
RESUME_ID=""
|
||||
PROMPT=""
|
||||
ADD_DIRS=()
|
||||
YOLO=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--new) NEW=1; shift ;;
|
||||
--reset) rm -f "$STATE_FILE"; echo "stato azzerato"; exit 0 ;;
|
||||
--id) if [[ -f "$STATE_FILE" ]]; then cat "$STATE_FILE"; else echo "(nessuna conversazione attiva)"; fi; exit 0 ;;
|
||||
--list) list_convs; exit 0 ;;
|
||||
--model) MODEL="$2"; shift 2 ;;
|
||||
--effort) EFFORT="$2"; shift 2 ;;
|
||||
--resume) RESUME_ID="$2"; shift 2 ;;
|
||||
--add-dir) ADD_DIRS+=("$2"); shift 2 ;;
|
||||
--yolo) YOLO=1; shift ;;
|
||||
*) PROMPT="$PROMPT $1"; shift ;;
|
||||
esac
|
||||
done
|
||||
PROMPT="$(echo "$PROMPT" | sed 's/^ *//;s/ *$//')"
|
||||
|
||||
if [[ -z "$PROMPT" ]]; then
|
||||
echo "uso: ./agy-chat.sh [--new] [--model NOME] [--effort low|medium|high] \"prompt\"" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- determina conversation_id da usare ---------------------------------------
|
||||
CONV_ID=""
|
||||
if [[ $NEW -eq 0 && -z "$RESUME_ID" && -f "$STATE_FILE" ]]; then
|
||||
CONV_ID="$(cat "$STATE_FILE")"
|
||||
elif [[ -n "$RESUME_ID" ]]; then
|
||||
CONV_ID="$RESUME_ID"
|
||||
fi
|
||||
|
||||
# --- costruisci comando -------------------------------------------------------
|
||||
CMD=("$AGY_BIN" -p "$PROMPT")
|
||||
for d in "${ADD_DIRS[@]}"; do
|
||||
CMD+=(--add-dir "$d")
|
||||
done
|
||||
if [[ -n "$CONV_ID" ]]; then
|
||||
CMD+=(--conversation "$CONV_ID")
|
||||
fi
|
||||
if [[ -n "$MODEL" ]]; then
|
||||
CMD+=(--model "$MODEL")
|
||||
fi
|
||||
if [[ -n "$EFFORT" ]]; then
|
||||
CMD+=(--effort "$EFFORT")
|
||||
fi
|
||||
if [[ $YOLO -eq 1 ]]; then
|
||||
CMD+=(--dangerously-skip-permissions)
|
||||
fi
|
||||
|
||||
echo ">>> agy (conversation: ${CONV_ID:-nuova})" >&2
|
||||
OUTPUT="$("${CMD[@]}" 2>&1)"
|
||||
RC=$?
|
||||
|
||||
# --- stampa output ------------------------------------------------------------
|
||||
if [[ -n "$OUTPUT" ]]; then
|
||||
echo "$OUTPUT"
|
||||
fi
|
||||
|
||||
# --- aggiorna stato con l'ultimo conversation_id ------------------------------
|
||||
if [[ $RC -eq 0 ]]; then
|
||||
NEW_ID="$(latest_id)"
|
||||
if [[ -n "$NEW_ID" ]]; then
|
||||
echo "$NEW_ID" > "$STATE_FILE"
|
||||
fi
|
||||
else
|
||||
# se fallisce per mancato login, avvisa
|
||||
if echo "$OUTPUT" | grep -qiE "sign in|signin|authenticate|login|Authentication required|visit the URL"; then
|
||||
echo "" >&2
|
||||
echo "⚠️ agy NON è autenticato. Devi fare il login UNA volta:" >&2
|
||||
echo " 1) apri un terminale e lancia: agy" >&2
|
||||
echo " 2) visita l'URL stampato, fai login con l'account Antigravity" >&2
|
||||
echo " 3) incolla il codice di autorizzazione nel terminale" >&2
|
||||
echo " Poi torna qui e riprova il wrapper." >&2
|
||||
fi
|
||||
fi
|
||||
|
||||
exit $RC
|
||||
@@ -0,0 +1,386 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
|
||||
import { execFile } from "node:child_process";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
import { Type } from "typebox";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Configurazione
|
||||
// ---------------------------------------------------------------------------
|
||||
const AGY_CHAT_DIR = path.join(os.homedir(), ".agy-chat");
|
||||
const STATE_FILE = path.join(AGY_CHAT_DIR, "conversation_id");
|
||||
const CONV_DIR = path.join(os.homedir(), ".gemini", "antigravity-cli", "conversations");
|
||||
|
||||
const DEFAULT_TIMEOUT_MS = 180_000; // 3 min
|
||||
const IMAGE_TIMEOUT_MS = 300_000; // 5 min
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: trovare il binario agy
|
||||
// ---------------------------------------------------------------------------
|
||||
function findAgy(): string {
|
||||
const candidates = [
|
||||
process.env.AGY_BIN,
|
||||
path.join(os.homedir(), ".local", "bin", "agy"),
|
||||
"agy",
|
||||
].filter(Boolean) as string[];
|
||||
for (const c of candidates) {
|
||||
if (c === "agy") return c; // lascia che lo risolva la shell/PATH
|
||||
if (fs.existsSync(c)) return c;
|
||||
}
|
||||
return "agy";
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: ultimo conversation_id (dal file .db più recente in conversations/)
|
||||
// ---------------------------------------------------------------------------
|
||||
function getLatestConversationId(): string | null {
|
||||
try {
|
||||
if (!fs.existsSync(CONV_DIR)) return null;
|
||||
const files = fs
|
||||
.readdirSync(CONV_DIR)
|
||||
.filter((f) => f.endsWith(".db"))
|
||||
.map((f) => path.join(CONV_DIR, f))
|
||||
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
|
||||
if (files.length === 0) return null;
|
||||
return path.basename(files[0], ".db");
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: leggere/scrivere lo stato conversazione
|
||||
// ---------------------------------------------------------------------------
|
||||
function readState(): string | null {
|
||||
try {
|
||||
return fs.readFileSync(STATE_FILE, "utf8").trim() || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writeState(id: string) {
|
||||
try {
|
||||
fs.mkdirSync(AGY_CHAT_DIR, { recursive: true });
|
||||
fs.writeFileSync(STATE_FILE, id, "utf8");
|
||||
} catch {
|
||||
/* ignora */
|
||||
}
|
||||
}
|
||||
|
||||
function resetState() {
|
||||
try {
|
||||
fs.rmSync(STATE_FILE, { force: true });
|
||||
} catch {
|
||||
/* ignora */
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: eseguire agy
|
||||
// ---------------------------------------------------------------------------
|
||||
interface RunResult {
|
||||
output: string;
|
||||
error: string | null;
|
||||
exitCode: number;
|
||||
}
|
||||
|
||||
async function runAgy(
|
||||
args: string[],
|
||||
timeoutMs: number,
|
||||
signal?: AbortSignal,
|
||||
): Promise<RunResult> {
|
||||
const bin = findAgy();
|
||||
try {
|
||||
const { stdout, stderr } = await execFileAsync(bin, args, {
|
||||
timeout: timeoutMs,
|
||||
maxBuffer: 20 * 1024 * 1024,
|
||||
signal,
|
||||
env: { ...process.env, PATH: `${path.join(os.homedir(), ".local", "bin")}:${process.env.PATH ?? ""}` },
|
||||
});
|
||||
return { output: stdout, error: stderr || null, exitCode: 0 };
|
||||
} catch (err: any) {
|
||||
const code = typeof err.code === "number" ? err.code : 1;
|
||||
return {
|
||||
output: err.stdout || "",
|
||||
error: err.stderr || err.message || String(err),
|
||||
exitCode: code,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: costruire gli argomenti agy
|
||||
// ---------------------------------------------------------------------------
|
||||
interface AgyOptions {
|
||||
prompt: string;
|
||||
mode?: "chat" | "image" | "analyze";
|
||||
model?: string;
|
||||
effort?: "low" | "medium" | "high";
|
||||
newConversation?: boolean;
|
||||
addDir?: string;
|
||||
filePath?: string;
|
||||
yolo?: boolean;
|
||||
resumeId?: string;
|
||||
}
|
||||
|
||||
function buildArgs(opts: AgyOptions): string[] {
|
||||
const args: string[] = ["-p", opts.prompt];
|
||||
|
||||
// add-dir (per analisi di 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 dirs) args.push("--add-dir", d);
|
||||
|
||||
// conversazione
|
||||
let convId: string | null = null;
|
||||
if (opts.resumeId) convId = opts.resumeId;
|
||||
else if (!opts.newConversation) 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;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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))
|
||||
.join("\n")
|
||||
.trim();
|
||||
if (err && !text) text = err;
|
||||
}
|
||||
if (convId) text += `\n\n[conversazione agy: ${convId}]`;
|
||||
return text;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Estensione
|
||||
// ---------------------------------------------------------------------------
|
||||
export default function agyExtension(pi: ExtensionAPI) {
|
||||
// --- Tool: agy ----------------------------------------------------------
|
||||
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).",
|
||||
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.",
|
||||
}),
|
||||
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.",
|
||||
}),
|
||||
),
|
||||
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.",
|
||||
}),
|
||||
),
|
||||
}),
|
||||
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;
|
||||
|
||||
return {
|
||||
content: [{ type: "text", text }],
|
||||
details: {
|
||||
conversationId: convId,
|
||||
exitCode: r.exitCode,
|
||||
imagePath,
|
||||
model: opts.model,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
// --- Tool: agy_conversation ---------------------------------------------
|
||||
pi.registerTool({
|
||||
name: "agy_conversation",
|
||||
label: "agy conversation state",
|
||||
description:
|
||||
"Gestisce lo stato della conversazione agy: mostra l'ID corrente, elenca le conversazioni, o azzera lo stato.",
|
||||
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" },
|
||||
),
|
||||
}),
|
||||
async execute(toolCallId, params) {
|
||||
const action = (params as { action: string }).action;
|
||||
if (action === "id") {
|
||||
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: {} };
|
||||
}
|
||||
const files = fs
|
||||
.readdirSync(CONV_DIR)
|
||||
.filter((f) => f.endsWith(".db"))
|
||||
.map((f) => path.join(CONV_DIR, f))
|
||||
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)
|
||||
.slice(0, 15);
|
||||
const lines = files.map((f) => {
|
||||
const id = path.basename(f, ".db");
|
||||
const mtime = new Date(fs.statSync(f).mtimeMs).toISOString().replace("T", " ").slice(0, 19);
|
||||
return `${id}\t${mtime}`;
|
||||
});
|
||||
return {
|
||||
content: [{ type: "text", text: lines.join("\n") || "(nessuna conversazione trovata)" }],
|
||||
details: {},
|
||||
};
|
||||
} catch (e: any) {
|
||||
return { content: [{ type: "text", text: `Errore: ${e.message}` }], details: {}, isError: true };
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// --- Comando: /agy -------------------------------------------------------
|
||||
pi.registerCommand("agy", {
|
||||
description: "Invia un prompt a agy (subagent Antigravity). Uso: /agy <prompt>",
|
||||
handler: async (args, ctx) => {
|
||||
if (!args?.trim()) {
|
||||
ctx.ui.notify("Uso: /agy <prompt>", "error");
|
||||
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);
|
||||
}
|
||||
ctx.ui.setStatus("agy", "");
|
||||
ctx.ui.notify(r.output.trim() || r.error || "(nessun output)", "info");
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand("agy:new", {
|
||||
description: "Forza una nuova conversazione agy",
|
||||
handler: async (_args, ctx) => {
|
||||
resetState();
|
||||
ctx.ui.notify("Nuova conversazione agy pronta.", "info");
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand("agy:list", {
|
||||
description: "Elenca le conversazioni agy",
|
||||
handler: async (_args, ctx) => {
|
||||
const id = readState();
|
||||
ctx.ui.notify(`Conversazione corrente: ${id ?? "(nessuna)"}`, "info");
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerCommand("agy:reset", {
|
||||
description: "Azzera lo stato conversazione agy",
|
||||
handler: async (_args, ctx) => {
|
||||
resetState();
|
||||
ctx.ui.notify("Stato conversazione azzerato.", "info");
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "agy-pi",
|
||||
"version": "0.1.0",
|
||||
"description": "Integra Google Antigravity CLI (agy) come subagent multimodale dentro pi: conversazioni multi-turno, generazione e analisi di immagini/audio/video.",
|
||||
"keywords": ["pi-package", "agy", "antigravity", "gemini", "multimodal", "subagent"],
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"pi": {
|
||||
"extensions": ["./extensions"]
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@earendil-works/pi-coding-agent": "*",
|
||||
"typebox": "*"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user