AUDIT / src /lib /agentLoop /fileTools.ts
Arypulka98's picture
feat(audit): deploy full backend cluster node (part 3)
95eb75a verified
Raw
History Blame Contribute Delete
43.3 kB
// ─── File / VFS tools ─────────────────────────────────────────────────────────
// Contiene: write_file, read_file, iterate_file, list_files, delete_file,
// create_webpage, create_project, find_in_vfs, convert_data,
// generate_mock_data, create_chart
// S560: generate_xlsx · generate_docx · create_zip · edit_image · remove_background · generate_readme
// → estratti in fileToolsGenerators.ts (tryExecuteGenerators)
import { tryExecuteAdvanced } from "./fileToolsAdvanced"; // S742: copy_file, file_info, file_diff, append_to_file, clone_url_to_vfs, batch_file_op
import { vfsAsync, fileBackupsDb } from "../vfsDb";
import { vfsBulkCommit } from "@/lib/vfsBulkCommit"; // GAP-2
import DiffMatchPatch from "diff-match-patch"; // G3-S202: patch engine
import { fullDiffValidation } from "../agent/diffValidator"; // S213: pre-apply gate
import * as stateGraph from "../projectStateGraph"; // S403: State Graph
import { tryExecuteGenerators } from "./fileToolsGenerators";
import { contextBuilder } from "../agent/contextBuilder"; // GAP-3: track file edits for session context // S560: 6 generator tools estratti
import { detectTemplate, getTemplateFiles } from "./templateRegistry"; // P1: scaffolding template-based
// ── Genesis log per-turno (GAP-1) ─────────────────────────────────────────────
// Traccia i file creati ex-novo nel turno corrente (existingBeforeWrite === null).
// Usato da restore_file per distinguere "nessun backup perché file nuovo" vs
// "nessun backup perché file pre-esistente" → nel primo caso: elimina il file (rollback).
let _turnCreatedFiles: string[] = [];
export function resetTurnCreatedFiles(): void { _turnCreatedFiles = []; }
async function _autoSnapshot(path: string, content: string): Promise<void> {
try {
const existing = await fileBackupsDb.list(path);
if (existing.length >= 5) {
const oldest = existing.sort((a, b) => a.ts - b.ts)[0];
await fileBackupsDb.delete(oldest.id);
}
await fileBackupsDb.save({
id: `${path}__${Date.now()}`,
path,
content,
ts: Date.now(),
trigger: "pre-write",
});
} catch { /* non-blocking — mai bloccare il write */ }
}
export async function tryExecute(name: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<string | null> {
if (signal?.aborted) return null; // V-b fix S178
if (name === "apply_patch") {
const { path, patch, mode = "search_replace" } = args as { path: string; patch: string; mode?: string };
const original = await vfsAsync.read(path);
if (original === null) {
const all = await vfsAsync.list();
return `❌ apply_patch: file "${path}" non trovato. Usa write_file per crearlo.\nFile esistenti: ${all.map((f: { path: string }) => f.path).join(", ") || "nessuno"}`;
}
await _autoSnapshot(path, original); // snapshot PRIMA del patch
// S213: gate pre-apply — blocca diff con sintassi non valida o SEARCH non trovata PRIMA dell'apply
const _val = fullDiffValidation(patch, original, path);
if (!_val.preValidation.valid) {
const errs = (_val.preValidation.issues ?? []).join("; ") || "formato diff non valido";
return `❌ apply_patch: validazione pre-apply fallita — ${errs}. Verifica il formato del patch.`;
}
if (_val.searchFound === false) {
return `❌ apply_patch: testo SEARCH non trovato in "${path}" (gate pre-apply S213). Il file potrebbe essere cambiato — usa read_file per leggere il contenuto attuale, poi aggiorna il patch.`;
}
try {
let patched: string;
if (mode === "diff") {
// Unified diff mode via diff-match-patch
const dmp = new DiffMatchPatch();
const patches = dmp.patch_fromText(patch);
const [result, applied] = dmp.patch_apply(patches, original);
const failed = applied.filter((ok: boolean) => !ok).length;
if (failed > 0) {
return `⚠️ apply_patch: ${failed}/${applied.length} chunk non applicati — il file sorgente potrebbe essere cambiato. Usa read_file per verificare il contenuto attuale, poi riprova.`;
}
patched = result;
} else {
// search_replace mode — SEARCH già verificata dal gate S213, procedi direttamente
const parts = patch.split(/\nREPLACE:\n/);
if (parts.length !== 2) return `❌ apply_patch: formato non valido. Usa: SEARCH:\n<testo>\nREPLACE:\n<sostituto>`;
const search = parts[0].replace(/^SEARCH:\n/, "");
const replace = parts[1];
patched = original.replace(search, replace);
}
await vfsAsync.write(path, patched);
const _written = await vfsAsync.read(path);
if (_written === null) return `⚠️ apply_patch: patch applicata ma file non salvato (quota localStorage esaurita).`;
// S710-5: post-apply verify + rollback automatico
// Confronta il contenuto scritto con il patch atteso — se diverge, ripristina l'originale
if (_written !== patched) {
try { await vfsAsync.write(path, original); } catch { /* best-effort rollback */ }
return `❌ apply_patch: post-apply verify fallita — file scritto diverge dal patch (atteso ${patched.length} chars, trovato ${_written.length} chars). Rollback automatico a "${path}". Usa read_file per verificare, poi riprova.`;
}
// GAP-1: syntax rollback — postValidation già calcolato dal gate pre-apply (zero costo extra)
// Copre il caso: patch logicamente valida ma produce codice sintatticamente rotto
// (parentesi sbilanciate, JSX malformato, import non chiusi, ecc.)
// Catena completa: pre-check → apply → byte-verify → syntax-verify → rollback
if (_val.postValidation && !_val.postValidation.syntaxOk && _val.postValidation.syntaxError) {
try { await vfsAsync.write(path, original); } catch { /* best-effort rollback */ }
return `❌ apply_patch: sintassi non valida dopo il patch — ${_val.postValidation.syntaxError}. Rollback automatico a "${path}". Correggi parentesi/tag sbilanciati nel patch e riprova.`;
}
const dmp2 = new DiffMatchPatch();
const diff = dmp2.diff_main(original, patched);
const linesChanged = diff.filter(([op]: [number, string]) => op !== 0).length;
// GAP-6 (S765-2): hint TypeScript BLOCCANTE — formato ACTION_REQUIRED interpretato dall'agent loop
// come step obbligatorio (non opzionale come 💡). Previene silent type errors post-patch.
// L'agent deve: 1) chiamare lint_code 2) se errori → restore_file + correggere il patch
const _tsHint = (path.endsWith(".ts") || path.endsWith(".tsx"))
? `\n⚡ AZIONE OBBLIGATORIA POST-PATCH: chiama lint_code { "path": "${path}" }\n` +
` Se lint_code riporta errori TypeScript → rollback con restore_file "${path}" e correggi.`
: "";
try { contextBuilder.recordFileEdit(path); } catch { /* non-blocking — context track */ }
// WORLDMODEL-SYNC: apply_patch aggiorna il file → sincronizza worldModel (stessa chain di write_file)
try { const { updateFileState } = await import("../worldModel"); updateFileState(path, { exists: true, lastModified: Date.now(), layer: "app" }); } catch { /* non-blocking */ }
return `✅ Patch applicata: "${path}" — ${linesChanged} chunk modificati, file ora ${patched.length} chars.${_tsHint}`;
} catch (e) {
return `❌ apply_patch errore: ${e instanceof Error ? e.message : String(e)}`;
}
}
if (name === "write_file") {
const { path, content, mime_type = "text/plain" } = args as { path: string; content: string; mime_type?: string };
const existingBeforeWrite = await vfsAsync.read(path);
if (!existingBeforeWrite) _turnCreatedFiles.push(path); // GAP-1: genesis log
if (existingBeforeWrite) await _autoSnapshot(path, existingBeforeWrite);
await vfsAsync.write(path, content, mime_type);
// Bug fix: verifica che il file sia stato effettivamente scritto (quota localStorage)
const _written = await vfsAsync.read(path);
if (_written === null) return `⚠️ write_file: file "${path}" non salvato (quota localStorage esaurita). Riduci il contenuto o cancella file vecchi con delete_file.`;
// S403: State Graph — marca il file come generato (non ancora verificato)
stateGraph.markGenerated(path);
try { contextBuilder.recordFileEdit(path); } catch { /* non-blocking — contextBuilder cieco senza questa call */ } // GAP-3
// WORLDMODEL-SYNC: aggiorna worldModel dopo write_file — era mai sincronizzato dal loop agente
// Il system prompt usa renderWorldModel() che era sempre stantio dopo operazioni agente (cerchio non chiuso)
try { const { updateFileState } = await import("../worldModel"); updateFileState(path, { exists: true, lastModified: Date.now(), layer: "app" }); } catch { /* non-blocking */ }
// S407: Architecture Graph — invalida cache e schedula rebuild (debounce 2s)
try { const { scheduleArchRefresh } = await import("@/lib/projectArchGraph"); scheduleArchRefresh(); } catch { /* silent */ }
// P0: Blast radius warning — avvisa se il file modificato è importato da ≥3 altri file
let _blastWarning = "";
try {
const { getLastArchGraph } = await import("@/lib/projectArchGraph");
const _graph = getLastArchGraph();
if (_graph) {
const _node = _graph.nodes[path];
if (_node && (_node.importedBy?.length ?? 0) >= 3) {
const _names = _node.importedBy.slice(0, 5).map((p: string) => `\`${p.split("/").pop()}\``).join(", ");
_blastWarning = `\n⚠️ **Blast radius**: \`${path.split("/").pop()}\` è importato da ${_node.importedBy.length} file (${_names}${_node.importedBy.length > 5 ? ", …" : ""}). Verifica le regressioni.`;
}
}
} catch { /* non-blocking */ }
// AUT-1: auto-queue nel DownloadQueue — il file è subito scaricabile/condivisibile su iPhone
try {
const _aqBlob = new Blob([content], { type: mime_type || "text/plain" });
const _aqUrl = URL.createObjectURL(_aqBlob);
const _aqName = path.split("/").pop() || "file";
window.dispatchEvent(new CustomEvent("agent:download-ready", { detail: { url: _aqUrl, filename: _aqName } }));
} catch { /* non-blocking */ }
// AUT-6: segnala VFS dirty -> AutoGitSync countdown 5 min
try { window.dispatchEvent(new CustomEvent("agent:vfs-dirty")); } catch { /* */ }
return `✅ Scritto: ${path} (${content.length} caratteri)${_blastWarning}`;
}
if (name === "write_files") {
// GAP-2: atomic multi-file commit — o tutti o nessuno (Dexie transaction)
const { patches } = args as { patches: Array<{ op: "write" | "delete"; path: string; content?: string; type?: string }> };
if (!Array.isArray(patches) || patches.length === 0) {
return `❌ write_files: "patches" deve essere un array non vuoto.`;
}
// GAP-1: genesis log — traccia file write che sono nuovi (non esistevano prima)
for (const _gp of patches) {
if (_gp.op === 'write') {
const _gex = await vfsAsync.read(_gp.path).catch(() => null);
if (!_gex && !_turnCreatedFiles.includes(_gp.path)) _turnCreatedFiles.push(_gp.path);
}
}
const result = await vfsBulkCommit.apply(patches);
if (!result.ok) {
return `❌ write_files: commit atomico fallito — ${result.error}\nNessun file è stato modificato (rollback automatico).`;
}
// Post-commit: snapshot, stateGraph, contextBuilder, archGraph per ogni file scritto
for (const p of result.written) {
const written = patches.find(x => x.path === p);
if (written?.content) {
await _autoSnapshot(p, written.content).catch(() => {});
stateGraph.markGenerated(p);
try { contextBuilder.recordFileEdit(p); } catch { /* non-blocking */ }
}
}
try {
const { scheduleArchRefresh } = await import("@/lib/projectArchGraph");
scheduleArchRefresh();
} catch { /* non-blocking */ }
// AUT-1: auto-queue tutti i file scritti — accessibili subito su iPhone
try {
for (const _wfp of result.written) {
const _wfpatch = patches.find((x: { op: string; path: string; content?: string; type?: string }) => x.path === _wfp);
if (_wfpatch?.content) {
const _aqBlob = new Blob([_wfpatch.content], { type: _wfpatch.type || "text/plain" });
const _aqUrl = URL.createObjectURL(_aqBlob);
window.dispatchEvent(new CustomEvent("agent:download-ready", { detail: { url: _aqUrl, filename: _wfp.split("/").pop() || "file" } }));
}
}
} catch { /* non-blocking */ }
// AUT-6: segnala VFS dirty -> AutoGitSync
if (result.written.length > 0) { try { window.dispatchEvent(new CustomEvent("agent:vfs-dirty")); } catch { /* */ } }
const summary = [
result.written.length > 0 ? `✅ Scritti (${result.written.length}): ${result.written.map(p => `\`${p.split("/").pop()}\``).join(", ")}` : null,
result.deleted.length > 0 ? `🗑 Eliminati (${result.deleted.length}): ${result.deleted.map(p => `\`${p.split("/").pop()}\``).join(", ")}` : null,
].filter(Boolean).join("\n");
// WORLDMODEL-SYNC: aggiorna worldModel per ogni file scritto/eliminato nel commit atomico
try {
const { updateFileState } = await import("../worldModel");
for (const _wfp of patches) {
if (_wfp.op === 'write' && _wfp.path) updateFileState(_wfp.path, { exists: true, lastModified: Date.now(), layer: "app" });
if (_wfp.op === 'delete' && _wfp.path) updateFileState(_wfp.path, { exists: false, lastModified: Date.now(), layer: "app" });
}
} catch { /* non-blocking */ }
return `✅ write_files: commit atomico completato\n${summary}`;
}
if (name === "restore_file") {
const { path, version = "latest" } = args as { path: string; version?: string | number };
if (!path) return "❌ restore_file: fornisci path.";
try {
const snapshots = (await fileBackupsDb.list(String(path))) as Array<{ id: string; path: string; content: string; ts: number; trigger: string }>;
if (!snapshots.length) {
// GAP-1: genesis log — file creato ex-novo in questo turno → eliminalo (rollback)
if (_turnCreatedFiles.includes(String(path))) {
await vfsAsync.delete(String(path));
return `🗑 File \`${path}\` creato in questo task rimosso (rollback — nessun backup pre-esistente).`;
}
return `❌ Nessun backup trovato per "${path}". Gli snapshot vengono creati automaticamente ad ogni write_file su file pre-esistenti.`;
}
const sorted = [...snapshots].sort((a, b) => b.ts - a.ts);
const idx = version === "latest" ? 0 : Math.max(0, Number(version) - 1);
const snap = sorted[idx];
if (!snap) return `❌ restore_file: snapshot #${version} non trovato. Disponibili: ${sorted.length}.\nVersioni:\n${sorted.map((s, i) => ` ${i + 1}. ${new Date(s.ts).toLocaleString("it-IT")} (${s.trigger})`).join("\n")}`;
const age = Math.round((Date.now() - snap.ts) / 60_000);
await vfsAsync.write(String(path), snap.content, "text/plain");
// WORLDMODEL-SYNC: restore_file ripristina un file → worldModel deve saperlo
try { const { updateFileState } = await import("../worldModel"); updateFileState(String(path), { exists: true, lastModified: Date.now(), layer: "app" }); } catch { /* non-blocking */ }
return `✅ Ripristinato "${path}" alla versione di ${age} min fa (${snap.content.length} chars, versione ${idx + 1}/${sorted.length}).`;
} catch (e) { return `❌ restore_file: ${e instanceof Error ? e.message : String(e)}`; }
}
if (name === "read_file") {
const { path, offset = 0, limit, offset_page: _op, limit_pages: _lp } = args as { path: string; offset?: number; limit?: number; offset_page?: number; limit_pages?: number };
void _op; void _lp;
const content = await vfsAsync.read(path);
if (content === null) {
const all = await vfsAsync.list();
return `❌ File non trovato: ${path}\nDisponibili: ${all.map((f: { path: string }) => f.path).join(", ") || "nessuno"}`;
}
const MAX_CHUNK = 8000;
const start = Math.min(Number(offset) || 0, content.length);
const end = limit
? Math.min(start + Number(limit), content.length)
: Math.min(start + MAX_CHUNK, content.length);
const slice = content.slice(start, end);
const hasMore = end < content.length;
if (content.length <= MAX_CHUNK && start === 0) return slice;
const header = `📄 **${path}** — ${content.length} chars totali (mostra ${start}${end})`;
return hasMore
? `${header}\n\`\`\`\n${slice}\n\`\`\`\n_…${content.length - end} chars rimanenti — usa \`read_file\` con \`offset=${end}\` per continuare_`
: `${header}\n\`\`\`\n${slice}\n\`\`\``;
}
if (name === "iterate_file") {
const { path, instruction } = args as { path: string; instruction: string };
const content = await vfsAsync.read(path);
if (content === null) {
const all = await vfsAsync.list();
return `❌ File non trovato: ${path}\nDisponibili: ${all.map(f => f.path).join(", ") || "nessuno"}`;
}
const preview = content.length > 6000 ? content.slice(0, 6000) + `\n…(+${content.length - 6000} chars)` : content;
return `📄 **${path}** (${content.length} chars)\n\`\`\`\n${preview}\n\`\`\`\n\n🔧 **Istruzione:** ${instruction}\n\n_Applica le modifiche e usa \`write_file\` con path \`${path}\` per salvare._`;
}
if (name === "list_files") {
const { path: dirFilter } = args as { path?: string };
const allFiles = await vfsAsync.list() as Array<{ path: string; size?: number; mime?: string }>;
const prefix = dirFilter ? (dirFilter.endsWith("/") ? dirFilter : dirFilter + "/") : null;
const files = prefix ? allFiles.filter(f => f.path.startsWith(prefix)) : allFiles;
if (!files.length) {
return dirFilter
? `📁 Nessun file in \`${dirFilter}\` — directory vuota o inesistente.`
: "📁 Filesystem virtuale vuoto — nessun file creato ancora.";
}
const label = dirFilter ? `File in \`${dirFilter}\` (${files.length})` : `File nel VFS (${files.length})`;
const lines = files.map(f => `• \`${f.path}\`${f.size !== undefined ? ` (${f.size} bytes)` : ""}${f.mime ? ` [${f.mime}]` : ""}`);
return `📁 **${label}:**\n${lines.join("\n")}`;
}
if (name === "delete_file") {
const { path } = args as { path: string };
const existing = await vfsAsync.read(path);
if (existing === null) {
const all = await vfsAsync.list();
return `❌ File non trovato: ${path}\nDisponibili: ${all.map((f: { path: string }) => f.path).join(", ") || "nessuno"}`;
}
await vfsAsync.delete(path);
// WORLDMODEL-SYNC: aggiorna worldModel dopo delete_file
try { const { updateFileState } = await import("../worldModel"); updateFileState(path, { exists: false, lastModified: Date.now(), layer: "app" }); } catch { /* non-blocking */ }
return `✅ Cancellato: \`${path}\``;
}
if (name === "move_file") {
const { from, to } = args as { from: string; to: string };
const content = await vfsAsync.read(from);
if (content === null) {
const all = await vfsAsync.list() as Array<{ path: string }>;
return `❌ File non trovato: ${from}\nDisponibili: ${all.map(f => f.path).join(", ") || "nessuno"}`;
}
const files = await vfsAsync.list() as Array<{ path: string; type?: string }>;
const fileInfo = files.find(f => f.path === from);
const mime = fileInfo?.type ?? "text/plain";
const destExisting = await vfsAsync.read(to);
if (destExisting !== null) return `❌ move_file: destinazione \`${to}\` esiste già — cancellala prima con delete_file se vuoi sovrascriverla.`;
await vfsAsync.write(to, content, mime);
await vfsAsync.delete(from);
// WORLDMODEL-SYNC: move_file → from non esiste più, to è nuovo
try { const { updateFileState } = await import("../worldModel"); updateFileState(from, { exists: false, lastModified: Date.now(), layer: "app" }); updateFileState(to, { exists: true, lastModified: Date.now(), layer: "app" }); } catch { /* non-blocking */ }
return `✅ Spostato/Rinominato: \`${from}\` → \`${to}\` (${content.length} chars)`;
}
if (name === "create_webpage") {
const { filename, html, show_preview = true } = args as { filename: string; html: string; show_preview?: boolean };
const path = filename.startsWith("/") ? filename : `/pages/${filename}`;
await vfsAsync.write(path, html, "text/html");
if (show_preview) {
try { window.dispatchEvent(new CustomEvent("agente:preview_open", { detail: { path, html } })); } catch { /* SSR/test safe */ }
}
return `✅ Pagina creata: ${path} (${html.length} chars)${show_preview ? " — anteprima aperta" : ""}`;
}
if (name === "create_project") {
const { files, project_name = "project", template: _tplArg, goal: _tplGoal } = args as {
files: Array<{ path: string; content: string; mime?: string }>;
project_name?: string;
template?: string;
goal?: string;
};
if (!Array.isArray(files) || files.length === 0) return "❌ create_project: fornisci almeno un file.";
const created: string[] = [];
const errors: string[] = [];
// P1: inject template files prima dei file LLM (template-based scaffolding)
const _tplName = detectTemplate(_tplGoal ?? project_name, _tplArg);
const _tplFiles = _tplName ? getTemplateFiles(_tplName) : [];
let _tplNote = "";
if (_tplFiles.length) {
_tplNote = ` (template: ${_tplName}, ${_tplFiles.length} file base)`;
for (const f of _tplFiles) {
// Solo se il file non è già nella lista LLM (override LLM ha precedenza)
const _hasOverride = files.some(lf => lf.path === f.path || lf.path === `/${project_name}/${f.path}`);
if (!_hasOverride) {
try {
const p = f.path.startsWith("/") ? f.path : `/${project_name}/${f.path}`;
await vfsAsync.write(p, f.content, f.mime ?? "text/plain");
created.push(p);
} catch { /* best-effort template file */ }
}
}
}
for (const f of files) {
try {
const p = f.path.startsWith("/") ? f.path : `/${project_name}/${f.path}`;
await vfsAsync.write(p, f.content, f.mime ?? "text/plain");
created.push(p);
} catch (e) { errors.push(`${f.path}: ${e instanceof Error ? e.message : String(e)}`); }
}
let out = `✅ **Progetto "${project_name}" creato — ${created.length} file${_tplNote}**\n\n`;
out += created.map(p => `• \`${p}\``).join("\n");
if (errors.length) out += `\n\n⚠️ Errori:\n${errors.map(e => `• ${e}`).join("\n")}`;
// GAP-1: genesis log — tutti i file del progetto sono nuovi
for (const _cp of created) { if (!_turnCreatedFiles.includes(_cp)) _turnCreatedFiles.push(_cp); }
return out;
}
if (name === "find_in_vfs") {
const { query, case_sensitive = false, file_type, max_results = 15 } = args as { query: string; case_sensitive?: boolean; file_type?: string; max_results?: number };
const files = await vfsAsync.list() as Array<{ path: string; name?: string }>;
const targets = file_type ? files.filter(f => f.path.endsWith(`.${file_type}`)) : files;
let rx: RegExp;
try { rx = new RegExp(query, case_sensitive ? "g" : "gi"); } catch { return `❌ find_in_vfs: regex non valida: "${query}"`; }
const results: string[] = [];
let fileHits = 0;
for (const f of targets) {
if (fileHits >= max_results) { results.push(`\n_…e altri ${targets.length - fileHits} file non mostrati_`); break; }
let content = ""; try { content = await vfsAsync.read(f.path) ?? ""; } catch { continue; }
if (typeof content !== "string") continue;
const lines = content.split("\n");
const matches: string[] = [];
for (let i = 0; i < lines.length; i++) {
rx.lastIndex = 0;
if (rx.test(lines[i])) matches.push(` L${i + 1}: \`${lines[i].trim().slice(0, 100)}\``);
}
if (matches.length) {
results.push(`**${f.path}** (${matches.length} match)\n${matches.slice(0, 6).join("\n")}`);
fileHits++;
}
}
if (!results.length) return `🔍 Nessun risultato per \`${query}\` nel VFS${file_type ? ` (*.${file_type})` : ""}.`;
return `🔍 **Ricerca VFS: \`${query}\`** — ${fileHits} file con match\n\n${results.join("\n\n")}`;
}
if (name === "convert_data") {
const { input, from_format, to_format, save_path } = args as { input: string; from_format: string; to_format: string; save_path?: string };
try {
let result = "";
const parseCsv = (txt: string, delim = ",") => {
const rows = txt.trim().split(/\r?\n/);
const headers = rows[0].split(delim).map(h => h.trim().replace(/^"|"$/g, ""));
return rows.slice(1).map(r => { const v = r.split(delim).map(x => x.trim().replace(/^"|"$/g, "")); return Object.fromEntries(headers.map((h, i) => [h, v[i] ?? ""])); });
};
const toJson = (data: unknown) => JSON.stringify(data, null, 2);
const jsonToCsv = (data: unknown[]) => { const keys = Object.keys(data[0] as object); return [keys.join(","), ...data.map(r => keys.map(k => JSON.stringify((r as Record<string,unknown>)[k] ?? "")).join(","))].join("\n"); };
const mdToHtml = (md: string) => md.replace(/^### (.+)$/gm, "<h3>$1</h3>").replace(/^## (.+)$/gm, "<h2>$1</h2>").replace(/^# (.+)$/gm, "<h1>$1</h1>").replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/\*(.+?)\*/g, "<em>$1</em>").replace(/\n/g, "<br>");
if (from_format === "csv" && to_format === "json") result = toJson(parseCsv(input));
else if (from_format === "tsv" && to_format === "json") result = toJson(parseCsv(input, "\t"));
else if (from_format === "json" && to_format === "csv") { const d = JSON.parse(input); result = jsonToCsv(Array.isArray(d) ? d : [d]); }
else if (from_format === "json" && to_format === "tsv") { const d = JSON.parse(input); const csv = jsonToCsv(Array.isArray(d) ? d : [d]); result = csv.replace(/,/g, "\t"); }
else if (from_format === "json" && to_format === "yaml") {
const toYaml = (obj: unknown, indent = 0): string => {
const pad = " ".repeat(indent);
if (obj === null) return "null";
if (typeof obj !== "object") return typeof obj === "string" ? `"${obj}"` : String(obj);
if (Array.isArray(obj)) return obj.map(v => `${pad}- ${toYaml(v, indent + 2).trimStart()}`).join("\n");
return Object.entries(obj as Record<string, unknown>).map(([k, v]) => typeof v === "object" && v ? `${pad}${k}:\n${toYaml(v, indent + 2)}` : `${pad}${k}: ${toYaml(v, 0)}`).join("\n");
};
result = toYaml(JSON.parse(input));
}
else if (from_format === "yaml" && to_format === "json") {
const mod = await import("https://esm.sh/js-yaml" as never) as { default?: { load: (s:string) => unknown }; load?: (s:string) => unknown };
const load = mod.default?.load ?? mod.load;
result = load ? toJson(load(input)) : "❌ yaml parser non disponibile";
}
else if (from_format === "markdown" && to_format === "html") result = `<!DOCTYPE html><html><head><meta charset="utf-8"></head><body>${mdToHtml(input)}</body></html>`;
else if (from_format === "html" && to_format === "markdown") result = input.replace(/<h[1-3][^>]*>(.+?)<\/h[1-3]>/gi, "### $1\n").replace(/<strong[^>]*>(.+?)<\/strong>/gi, "**$1**").replace(/<em[^>]*>(.+?)<\/em>/gi, "*$1*").replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, "").trim();
else if (from_format === "xml" && to_format === "json") {
const parse = (xml: string): unknown => {
const doc = new DOMParser().parseFromString(xml, "text/xml");
const nodeToObj = (n: Element | Document): unknown => {
if (n.nodeType === 3) return n.textContent?.trim();
const el = n as Element;
const children = [...el.childNodes].filter(c => c.nodeType !== 8);
if (children.length === 1 && children[0].nodeType === 3) return children[0].textContent?.trim();
const obj: Record<string, unknown> = {};
for (const c of children as Element[]) {
const k = c.nodeName; const v = nodeToObj(c);
if (obj[k]) obj[k] = [].concat(obj[k] as never).concat(v as never); else obj[k] = v;
}
return obj;
};
return nodeToObj(doc.documentElement);
};
result = toJson(parse(input));
}
else if (from_format === "markdown" && to_format === "pdf") {
const mdToHtmlLocal = (md: string) => md
.replace(/^### (.+)$/gm, "<h3>$1</h3>").replace(/^## (.+)$/gm, "<h2>$1</h2>")
.replace(/^# (.+)$/gm, "<h1>$1</h1>").replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>")
.replace(/\*(.+?)\*/g, "<em>$1</em>").replace(/`([^`]+)`/g, "<code>$1</code>")
.replace(/^- (.+)$/gm, "<li>$1</li>").replace(/(<li>.*<\/li>\n?)+/g, "<ul>$&</ul>")
.replace(/\n/g, "<br>");
const html = mdToHtmlLocal(input);
const win = typeof window !== "undefined" ? window.open("", "_blank") : null;
if (!win) return "❌ Popup bloccato — abilita i popup per esportare PDF.";
win.document.write(`<!DOCTYPE html><html><head><meta charset="utf-8"><style>@page{margin:2cm}body{font-family:system-ui,sans-serif;line-height:1.6;color:#111}h1,h2,h3{margin-top:1.5em}code{background:#f4f4f4;padding:0.1em 0.4em;border-radius:3px}</style></head><body>${html}</body></html>`);
win.document.close();
setTimeout(() => { try { win.print(); } catch { /**/ } }, 400);
return "✅ Dialogo PDF aperto — salva come PDF dal browser. (Suggerimento: usa A4 verticale con margini normali)";
}
else if (from_format === "json" && to_format === "xml") {
const toXml = (obj: unknown, tag = "root"): string => {
if (obj === null || obj === undefined) return `<${tag}/>`;
if (typeof obj !== "object") return `<${tag}>${String(obj).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;")}</${tag}>`;
if (Array.isArray(obj)) return obj.map((v, i) => toXml(v, `item${i}`)).join("\n");
return `<${tag}>\n${Object.entries(obj as Record<string,unknown>).map(([k,v]) => " " + toXml(v,k)).join("\n")}\n</${tag}>`;
};
result = `<?xml version="1.0" encoding="UTF-8"?>\n${toXml(JSON.parse(input))}`;
}
else if (from_format === "csv" && to_format === "markdown") {
const rows = input.trim().split(/\r?\n/).map(r => r.split(",").map(c => c.trim().replace(/^"|"$/g,"")));
if (!rows.length) result = "";
else { const w = rows[0].map((_h,i) => Math.max(...rows.map(r => (r[i]??'').length),3)); result = [`| ${rows[0].map((h,i)=>h.padEnd(w[i])).join(" | ")} |`, `| ${w.map(n=>"-".repeat(n)).join(" | ")} |`, ...rows.slice(1).map(r => `| ${rows[0].map((_,i)=>(r[i]??"").padEnd(w[i])).join(" | ")} |`)].join("\n"); }
}
else if (from_format === "json" && to_format === "html_table") {
const arr = Array.isArray(JSON.parse(input)) ? JSON.parse(input) as Record<string,unknown>[] : [JSON.parse(input) as Record<string,unknown>];
const hdrs = arr.length ? Object.keys(arr[0]) : [];
const esc2 = (v: unknown) => String(v ?? "").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
result = `<!DOCTYPE html><html><head><meta charset="utf-8"><style>table{border-collapse:collapse;width:100%}th,td{border:1px solid #ddd;padding:8px;text-align:left}th{background:#f5f5f5;font-weight:bold}tr:nth-child(even){background:#fafafa}</style></head><body><table><thead><tr>${hdrs.map(h=>`<th>${esc2(h)}</th>`).join("")}</tr></thead><tbody>${arr.map(row=>`<tr>${hdrs.map(h=>`<td>${esc2(row[h])}</td>`).join("")}</tr>`).join("")}</tbody></table></body></html>`;
}
else if (from_format === "text" && to_format === "base64") { result = btoa(unescape(encodeURIComponent(input))); }
else if (from_format === "base64" && to_format === "text") { try { result = decodeURIComponent(escape(atob(input.trim()))); } catch { result = atob(input.trim()); } }
else if (from_format === "tsv" && to_format === "csv") { result = input.split("\n").map(l => l.split("\t").map(c => c.includes(",") || c.includes('"') ? `"${c.replace(/"/g,'""')}"` : c).join(",")).join("\n"); }
else if (from_format === "json" && to_format === "sql") {
const arr2 = Array.isArray(JSON.parse(input)) ? JSON.parse(input) as Record<string,unknown>[] : [JSON.parse(input) as Record<string,unknown>];
const t2 = arr2.length ? Object.keys(arr2[0]) : [];
const esc3 = (v: unknown) => v === null ? "NULL" : typeof v === "number" ? String(v) : `'${String(v).replace(/'/g,"''")}'`;
result = arr2.map(r => `INSERT INTO table_name (${t2.join(",")}) VALUES (${t2.map(k=>esc3(r[k])).join(",")});`).join("\n");
}
else return `❌ convert_data: conversione ${from_format}${to_format} non supportata.`;
if (save_path) await vfsAsync.write(save_path, result, "text/plain");
return `✅ **Conversione ${from_format.toUpperCase()}${to_format.toUpperCase()}**${save_path ? ` salvata in \`${save_path}\`` : ""}\n\n\`\`\`${to_format}\n${result.slice(0, 3000)}\n\`\`\``;
} catch (e) { return `❌ convert_data: ${e instanceof Error ? e.message : String(e)}`; }
}
if (name === "generate_mock_data") {
const { schema, count = 10, format = "json", save_path } = args as { schema: string; count?: number; format?: string; save_path?: string };
const n = Math.min(Math.max(Number(count) || 10, 1), 100);
const s = schema.toLowerCase();
const rnd = <T>(arr: T[]): T => arr[Math.floor(Math.random() * arr.length)];
const rndInt = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
const FIRST = ["Alice","Bob","Carlo","Diana","Elena","Fabio","Giulia","Hassan","Ivan","Lena","Marco","Nina","Oscar","Paola","Qin","Rosa","Sara","Tom","Uma","Vera"];
const LAST = ["Rossi","Ferrari","Esposito","Bianchi","Romano","Colombo","Ricci","Marino","Greco","Bruno","Gallo","Conti","De Luca","Mancini","Costa"];
const DOMAINS = ["gmail.com","yahoo.com","outlook.com","example.com","mail.it"];
const CATS = ["elettronica","abbigliamento","libri","casa","sport","cibo","beauty","auto","giochi","ufficio"];
const CITIES = ["Roma","Milano","Napoli","Torino","Bologna","Firenze","Venezia","Bari","Palermo","Genova"];
const WORDS = ["lorem","ipsum","dolor","sit","amet","consectetur","adipiscing","elit","sed","do","eiusmod","tempor","incididunt","ut","labore"];
const sent = () => Array.from({length:rndInt(6,14)},()=>WORDS[rndInt(0,WORDS.length-1)]).join(" ") + ".";
const records: Record<string, unknown>[] = [];
for (let i = 0; i < n; i++) {
const firstName = rnd(FIRST), lastName = rnd(LAST);
const r: Record<string, unknown> = {};
if (s.includes("id") || s.includes("user") || s.includes("product") || s.includes("order")) r.id = `${String(rndInt(1000,9999))}`;
if (s.includes("name") || s.includes("user")) r.name = `${firstName} ${lastName}`;
if (s.includes("first")) r.firstName = firstName;
if (s.includes("last")) r.lastName = lastName;
if (s.includes("email")) r.email = `${firstName.toLowerCase()}.${lastName.toLowerCase()}${rndInt(1,99)}@${rnd(DOMAINS)}`;
if (s.includes("age")) r.age = rndInt(18, 70);
if (s.includes("phone")) r.phone = `+39 ${rndInt(300,399)} ${rndInt(1000000,9999999)}`;
if (s.includes("city") || s.includes("address")) r.city = rnd(CITIES);
if (s.includes("address")) r.address = `Via ${rnd(LAST)} ${rndInt(1,150)}, ${rnd(CITIES)}`;
if (s.includes("title") || s.includes("product")) r.title = `${rnd(CATS).charAt(0).toUpperCase() + rnd(CATS).slice(1)} ${rnd(LAST)} ${rndInt(100,999)}`;
if (s.includes("price")) r.price = parseFloat((rndInt(5,500) + Math.random()).toFixed(2));
if (s.includes("category") || s.includes("cat")) r.category = rnd(CATS);
if (s.includes("body") || s.includes("content") || s.includes("description")) r.description = `${sent()} ${sent()}`;
if (s.includes("post") || s.includes("author")) r.author = `${firstName} ${lastName}`;
if (s.includes("date") || s.includes("created")) r.createdAt = new Date(Date.now() - rndInt(0, 365*24*3600*1000)).toISOString();
if (s.includes("status")) r.status = rnd(["active","inactive","pending","archived"]);
if (s.includes("score") || s.includes("rating")) r.rating = parseFloat((rndInt(1,5) + Math.random() * 0.9).toFixed(1));
if (s.includes("lat") || s.includes("geo")) { r.lat = parseFloat((rndInt(36,47) + Math.random()).toFixed(6)); r.lng = parseFloat((rndInt(6,18) + Math.random()).toFixed(6)); }
if (!Object.keys(r).length) { r.id = String(i+1); r.value = rndInt(1,1000); r.label = `Item ${i+1}`; }
records.push(r);
}
let result = "";
if (format === "csv") {
const keys = Object.keys(records[0]);
result = [keys.join(","), ...records.map(r => keys.map(k => JSON.stringify(r[k] ?? "")).join(","))].join("\n");
} else if (format === "typescript") {
const keys = Object.keys(records[0]);
const iface = `export interface MockItem {\n${keys.map(k => ` ${k}: ${typeof records[0][k]};`).join("\n")}\n}\n\nexport const mockData: MockItem[] = ${JSON.stringify(records, null, 2)};`;
result = iface;
} else {
result = JSON.stringify(records, null, 2);
}
if (save_path) await vfsAsync.write(save_path, result, "text/plain");
return `✅ **Mock data generata** (${n} record, ${format.toUpperCase()})${save_path ? ` → \`${save_path}\`` : ""}\n\n\`\`\`${format === "typescript" ? "typescript" : format}\n${result.slice(0, 2000)}\n\`\`\`${result.length > 2000 ? "\n_…troncato_" : ""}`;
}
if (name === "ocr_image") {
const { path, prompt = "Estrai tutto il testo visibile nell'immagine. Restituisci testo grezzo, senza formattazione extra." } =
args as { path: string; prompt?: string };
const dataUrl = await vfsAsync.read(path);
if (!dataUrl) {
const all = await vfsAsync.list();
return `❌ ocr_image: immagine non trovata "${path}"\nDisponibili: ${all.map((f: { path: string }) => f.path).join(", ") || "nessuno"}`;
}
if (!dataUrl.startsWith("data:image/")) {
return `❌ ocr_image: il file "${path}" non è un'immagine (deve essere data URL con prefisso data:image/).`;
}
try {
const { callWithFallback } = await import("../providerChain");
let _hfTok = "";
try { _hfTok = (typeof localStorage !== "undefined" && (localStorage.getItem("hf_token") ?? localStorage.getItem("huggingface_token"))) || ""; } catch { /**/ }
const _ocrRes = await callWithFallback(_hfTok, {
messages: [
{
role: "user",
content: ([
{ type: "image_url", image_url: { url: dataUrl } },
{ type: "text", text: prompt },
] as unknown) as string,
},
],
max_tokens: 2000,
stream: false,
}, signal);
const _ocrData = _ocrRes.ok ? await _ocrRes.json() as { choices?: Array<{ message?: { content?: string } }> } : null;
const text = _ocrData?.choices?.[0]?.message?.content ?? "";
if (!text) return "⚠️ ocr_image: nessun testo estratto dall'immagine.";
const outPath = path.replace(/\.[^.]+$/, "") + "_ocr.txt";
await vfsAsync.write(outPath, text, "text/plain");
return `✅ OCR completato: "${outPath}" (${text.length} chars)\n\n${text.slice(0, 500)}${text.length > 500 ? "\n…(troncato — leggi il file per il testo completo)" : ""}`;
} catch (e) {
return `❌ ocr_image errore: ${e instanceof Error ? e.message : String(e)}`;
}
}
if (name === "create_chart") {
const { type, title = "Chart", labels, datasets, filename = "chart.html" } = args as {
type: string; title?: string; labels: string[]; datasets: Array<{ label: string; data: number[]; color?: string }>; filename?: string;
};
const COLORS = ["#FF6384","#36A2EB","#FFCE56","#4BC0C0","#9966FF","#FF9F40","#FF6B6B","#4ECDC4","#45B7D1","#96CEB4"];
const chartType = type === "area" ? "line" : type === "horizontalBar" ? "bar" : type;
const fill = type === "area";
const indexAxis = type === "horizontalBar" ? "'y'" : "'x'";
const chartDatasets = datasets.map((ds, i) => {
const color = ds.color ?? COLORS[i % COLORS.length];
return `{ label: ${JSON.stringify(ds.label)}, data: ${JSON.stringify(ds.data)}, backgroundColor: '${color}88', borderColor: '${color}', borderWidth: 2, fill: ${fill}, tension: 0.4 }`;
}).join(",\n");
const html = `<!DOCTYPE html>
<html lang="it"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>${title}</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
<style>body{margin:0;padding:20px;background:#0f0f0f;color:#fff;font-family:system-ui}h2{text-align:center;color:#aaa;font-size:16px;margin-bottom:16px}.container{max-width:900px;margin:0 auto;background:#1a1a1a;border-radius:12px;padding:24px;box-shadow:0 4px 24px #0008}</style>
</head><body><div class="container"><h2>${title}</h2>
<canvas id="chart" style="max-height:500px"></canvas></div>
<script>
new Chart(document.getElementById('chart'), {
type: ${JSON.stringify(chartType)},
data: { labels: ${JSON.stringify(labels)}, datasets: [${chartDatasets}] },
options: { responsive: true, indexAxis: ${indexAxis}, plugins: { legend: { labels: { color: '#ccc' } } }, scales: { x: { ticks: { color: '#aaa' }, grid: { color: '#333' } }, y: { ticks: { color: '#aaa' }, grid: { color: '#333' } } } }
});
</script></body></html>`;
const path = filename.startsWith("/") ? filename : `/charts/${filename}`;
await vfsAsync.write(path, html, "text/html");
try { window.dispatchEvent(new CustomEvent("agente:preview_open", { detail: { path, html } })); } catch { /* SSR/test safe */ }
return `✅ **Grafico "${title}" creato** (${type}, ${datasets.length} serie, ${labels.length} etichette)\n📄 File: \`${path}\` — anteprima aperta`;
}
// ── S706: copy_file · file_info · file_diff · append_to_file · clone_url_to_vfs · batch_file_op ──
// → delegati a fileToolsAdvanced.ts (tryExecuteAdvanced) — S742 refactor
const _advResult = await tryExecuteAdvanced(name, args, signal);
if (_advResult !== null) return _advResult;
// ── S560: generate_xlsx · generate_docx · create_zip · edit_image · remove_background · generate_readme ──
// → delegati a fileToolsGenerators.ts (tryExecuteGenerators)
const _genResult = await tryExecuteGenerators(name, args);
if (_genResult !== null) return _genResult;
return null;
}