// ─── 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 { 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, signal?: AbortSignal): Promise { 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\nREPLACE:\n`; 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)[k] ?? "")).join(","))].join("\n"); }; const mdToHtml = (md: string) => md.replace(/^### (.+)$/gm, "

$1

").replace(/^## (.+)$/gm, "

$1

").replace(/^# (.+)$/gm, "

$1

").replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/\n/g, "
"); 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).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 = `${mdToHtml(input)}`; else if (from_format === "html" && to_format === "markdown") result = input.replace(/]*>(.+?)<\/h[1-3]>/gi, "### $1\n").replace(/]*>(.+?)<\/strong>/gi, "**$1**").replace(/]*>(.+?)<\/em>/gi, "*$1*").replace(//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 = {}; 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, "

$1

").replace(/^## (.+)$/gm, "

$1

") .replace(/^# (.+)$/gm, "

$1

").replace(/\*\*(.+?)\*\*/g, "$1") .replace(/\*(.+?)\*/g, "$1").replace(/`([^`]+)`/g, "$1") .replace(/^- (.+)$/gm, "
  • $1
  • ").replace(/(
  • .*<\/li>\n?)+/g, "
      $&
    ") .replace(/\n/g, "
    "); 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(`${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,"&").replace(//g,">")}`; if (Array.isArray(obj)) return obj.map((v, i) => toXml(v, `item${i}`)).join("\n"); return `<${tag}>\n${Object.entries(obj as Record).map(([k,v]) => " " + toXml(v,k)).join("\n")}\n`; }; result = `\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[] : [JSON.parse(input) as Record]; const hdrs = arr.length ? Object.keys(arr[0]) : []; const esc2 = (v: unknown) => String(v ?? "").replace(/&/g,"&").replace(//g,">"); result = `${hdrs.map(h=>``).join("")}${arr.map(row=>`${hdrs.map(h=>``).join("")}`).join("")}
    ${esc2(h)}
    ${esc2(row[h])}
    `; } 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[] : [JSON.parse(input) as Record]; 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 = (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[] = []; for (let i = 0; i < n; i++) { const firstName = rnd(FIRST), lastName = rnd(LAST); const r: Record = {}; 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 = ` ${title}

    ${title}

    `; 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; }