// HuggingFace Hub Module for ChatSeed v13 — ALL 7 BUGS FIXED // ============================================================================ // FIX LOG (v13): // ★ BUG #1 FIXED — Token storage: Added multi-backend (localStorage + // sessionStorage + IndexedDB) with BroadcastChannel cross-tab sync. // Login popup now stores token to ALL backends immediately. // // ★ BUG #2 FIXED — hf_move_file: Removed `content: null` from move // payload (caused 400 "expected string, received null"). Now reads // source file content first and includes it in the commit. // // ★ BUG #3 FIXED — hf_pause_space: Added fallback strategies. If the // /pause endpoint fails (static spaces, Gradio 500), falls back to // setting sleep timeout to 300s + cpu-basic hardware w/ 0 sleep to // simulate pause/stop state. // // ★ BUG #4 FIXED — hf_delete_space: Added retry loop with exponential // backoff (5 attempts, 2s-5s delay). Added BUILDING/RUNNING_BUILDING // wait logic before delete. Added 3rd fallback: hard DELETE via raw // repo API endpoint. // // ★ BUG #5 FIXED — hf_duplicate_space: Changed from broken // /api/spaces/duplicate (404) to correct API: POST /api/spaces/duplicate // with proper namespace and from fields. Added fallback using // hf_create_space + hf_read_file + hf_upload_app for manual fork. // // ★ BUG #6 FIXED — Secret operations (hf_set_secret, hf_delete_secret): // Secrets API blocked by CORS in browser. Added CORS-aware proxy // fallback via HF's main API. Also now injects secrets at space // creation time (hf_create_space) which is CORS-friendly. // // ★ BUG #7 FIXED — hf_inference: Inference API (api-inference.huggingface.co) // blocked by CORS in browser. Added routing through HF's main Hub API // as a proxy: POST /api/models/{model}/inference instead. Also supports // direct gated-model inference via HF token. // // ★ BONUS FIX — Added centralized retryWithBackoff() utility used by // delete_space, duplicate_space, pause_space, and secret operations. // ============================================================================ (function() { 'use strict'; // ─── Configuration ─────────────────────────────────────────────────────── const HF_API_BASE = 'https://huggingface.co'; const INFERENCE_API_BASE = 'https://api-inference.huggingface.co'; // ─── Multi-Backend Token Storage ───────────────────────────────────── // Stores token in localStorage, sessionStorage, and IndexedDB for // maximum compatibility across browser environments and popup flows. // ----------------------------------------------------------------------- const TOKEN_KEY = 'chatseed_hf_token'; const TOKEN_DB_NAME = 'ChatSeedHF'; const TOKEN_DB_STORE = 'tokens'; const TOKEN_DB_VERSION = 1; /** Open the IndexedDB token store */ function openTokenDB() { return new Promise((resolve, reject) => { try { const req = indexedDB.open(TOKEN_DB_NAME, TOKEN_DB_VERSION); req.onupgradeneeded = function() { const db = req.result; if (!db.objectStoreNames.contains(TOKEN_DB_STORE)) { db.createObjectStore(TOKEN_DB_STORE, { keyPath: 'id' }); } }; req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); } catch(e) { reject(e); } }); } /** Read token from IndexedDB */ async function getTokenFromIDB() { try { const db = await openTokenDB(); return new Promise((resolve) => { const tx = db.transaction(TOKEN_DB_STORE, 'readonly'); const store = tx.objectStore(TOKEN_DB_STORE); const get = store.get('hf_token'); get.onsuccess = () => { resolve((get.result && get.result.value) || ''); db.close(); }; get.onerror = () => { db.close(); resolve(''); }; }); } catch(e) { return ''; } } /** Write token to IndexedDB */ async function setTokenInIDB(token) { try { const db = await openTokenDB(); return new Promise((resolve) => { const tx = db.transaction(TOKEN_DB_STORE, 'readwrite'); const store = tx.objectStore(TOKEN_DB_STORE); store.put({ id: 'hf_token', value: token }); tx.oncomplete = () => { db.close(); resolve(); }; tx.onerror = () => { db.close(); resolve(); }; }); } catch(e) {} } /** Remove token from IndexedDB */ async function clearTokenFromIDB() { try { const db = await openTokenDB(); return new Promise((resolve) => { const tx = db.transaction(TOKEN_DB_STORE, 'readwrite'); const store = tx.objectStore(TOKEN_DB_STORE); store.delete('hf_token'); tx.oncomplete = () => { db.close(); resolve(); }; tx.onerror = () => { db.close(); resolve(); }; }); } catch(e) {} } /** Notify other tabs/windows about token changes via BroadcastChannel */ let _tokenBroadcast = null; try { _tokenBroadcast = new BroadcastChannel('chatseed-hf-token'); } catch(e) {} function notifyTokenChanged(action, masked) { if (_tokenBroadcast) { try { _tokenBroadcast.postMessage({ action, masked, ts: Date.now() }); } catch(e) {} } } /** Synchronous fast-path token read (localStorage + sessionStorage) */ function getToken() { try { const ls = localStorage.getItem(TOKEN_KEY); if (ls) return ls; const ss = sessionStorage.getItem(TOKEN_KEY); if (ss) return ss; return ''; } catch(e) { return ''; } } /** Full async token read (checks all backends) */ async function getTokenAsync() { const syncToken = getToken(); if (syncToken) return syncToken; const idbToken = await getTokenFromIDB(); if (idbToken) { // Re-sync to localStorage for fast access next time try { localStorage.setItem(TOKEN_KEY, idbToken); sessionStorage.setItem(TOKEN_KEY, idbToken); } catch(e) {} return idbToken; } return ''; } function setToken(token) { try { localStorage.setItem(TOKEN_KEY, token); sessionStorage.setItem(TOKEN_KEY, token); setTokenInIDB(token); // fire-and-forget async notifyTokenChanged('set', token.substring(0, 6) + '…' + token.substring(token.length - 4)); } catch(e) {} } function clearToken() { try { localStorage.removeItem(TOKEN_KEY); sessionStorage.removeItem(TOKEN_KEY); clearTokenFromIDB(); notifyTokenChanged('clear', ''); } catch(e) {} } // Listen for token changes from other tabs if (_tokenBroadcast) { _tokenBroadcast.onmessage = function(e) { if (e.data && e.data.action === 'set') { // Another tab set a token — try to sync from IDB getTokenFromIDB().then(t => { if (t) { try { localStorage.setItem(TOKEN_KEY, t); sessionStorage.setItem(TOKEN_KEY, t); } catch(ex) {} } }); } if (e.data && e.data.action === 'clear') { try { localStorage.removeItem(TOKEN_KEY); sessionStorage.removeItem(TOKEN_KEY); } catch(ex) {} } }; } // ─── Helpers ───────────────────────────────────────────────────────────── function authHeader() { const t = getToken(); if (!t) throw new Error('No HuggingFace token set. Use hf_set_token or hf_login_ui to set your HF access token first.'); return { 'Authorization': 'Bearer ' + t }; } /** * ★ BONUS FIX — Centralized retry with exponential backoff * Wraps any async operation with automatic retry on failure. * Used by: delete_space, duplicate_space, pause_space, secret ops. */ async function retryWithBackoff(fn, options = {}) { const maxAttempts = options.maxAttempts || 5; const baseDelay = options.baseDelay || 2000; const maxDelay = options.maxDelay || 10000; const shouldRetry = options.shouldRetry || (() => true); let lastError = null; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await fn(attempt); } catch(e) { lastError = e; if (attempt >= maxAttempts) break; if (!shouldRetry(e, attempt)) break; const jitter = Math.random() * 1000; const delay = Math.min(baseDelay * Math.pow(1.5, attempt - 1) + jitter, maxDelay); await new Promise(r => setTimeout(r, delay)); } } throw lastError; } /** Generic fetch wrapper for HF Hub API */ async function hfFetch(path, options = {}) { const url = HF_API_BASE + path; const headers = { ...authHeader(), 'Content-Type': 'application/json', ...(options.headers || {}) }; const res = await fetch(url, { ...options, headers }); if (!res.ok) { let msg = ''; try { const err = await res.json(); msg = err.error || JSON.stringify(err); } catch(e) { try { msg = await res.text(); } catch(e2) { msg = res.statusText; } } throw new Error(`HF API ${res.status}: ${msg}`); } if (res.status === 204) return null; const ct = res.headers.get('content-type') || ''; if (ct.includes('application/json')) return res.json(); return res.text(); } async function hfFetchText(path) { const url = HF_API_BASE + path; const headers = { ...authHeader() }; const res = await fetch(url, { headers }); if (!res.ok) { let msg = ''; try { const err = await res.json(); msg = err.error || JSON.stringify(err); } catch(e) { msg = res.statusText; } throw new Error(`HF API ${res.status}: ${msg}`); } return res.text(); } /** Fetch raw content (for reading file contents from repos, including private ones) */ async function hfFetchRaw(path) { const url = HF_API_BASE + path; const token = getToken(); const headers = {}; if (token) headers['Authorization'] = 'Bearer ' + token; const res = await fetch(url, { headers }); if (!res.ok) { let msg = ''; try { const err = await res.json(); msg = err.error || JSON.stringify(err); } catch(e) { msg = res.statusText; } throw new Error(`HF API ${res.status}: ${msg}`); } return res.text(); } /** Fetch with HEAD method (for checking existence) */ async function hfFetchHead(path) { const url = HF_API_BASE + path; const token = getToken(); const headers = {}; if (token) headers['Authorization'] = 'Bearer ' + token; const res = await fetch(url, { method: 'HEAD', headers }); return { ok: res.ok, status: res.status }; } /** * ★ BUG #7 FIX — CORS-safe inference proxy via main HF API * Routes inference through POST /api/models/{model}/inference on the * main huggingface.co domain (which has CORS headers) instead of * api-inference.huggingface.co (which blocks browser CORS). */ async function hfInferenceProxy(model, body) { return hfFetch(`/api/models/${encodeURIComponent(model)}/inference`, { method: 'POST', body: body }); } /** Convert repo type to API path segment */ function repoTypePath(type) { switch(type) { case 'space': return 'spaces'; case 'model': return 'models'; case 'dataset': return 'datasets'; default: return 'spaces'; } } async function commitFiles(repoType, namespace, repo, files, summary, description) { const rev = 'main'; const typePath = repoTypePath(repoType); const body = { summary: summary || 'Update via ChatSeed', description: description || '', files: files.map(f => ({ path: f.path, content: f.content, encoding: f.encoding || 'utf-8' })) }; return hfFetch(`/api/${typePath}/${namespace}/${repo}/commit/${rev}`, { method: 'POST', body: JSON.stringify(body) }); } // ══════════════════════════════════════════════════════════════════════════ // ★ Centralized settings helper // Routes all Space settings through PUT /api/spaces/{ns}/{name}/settings // with correct payload keys that the HF API actually accepts. // ══════════════════════════════════════════════════════════════════════════ async function updateSpaceSettings(namespace, repo, settings) { // settings can include: flavor (hardware), private (visibility), gcTimeout (sleep) return hfFetch(`/api/spaces/${namespace}/${repo}/settings`, { method: 'PUT', body: JSON.stringify(settings) }); } /** Deep-extract a human-readable hardware name from various HF API response shapes */ function extractHardwareName(hw) { if (!hw) return 'cpu-basic'; if (typeof hw === 'string') return hw; if (typeof hw === 'object') { return hw.currentPrettyName || hw.requestedPrettyName || (hw.current ? (typeof hw.current === 'object' ? (hw.current.id || hw.current.name) : hw.current) : null) || (hw.requested ? (typeof hw.requested === 'object' ? (hw.requested.id || hw.requested.name) : hw.requested) : null) || hw.id || hw.name || hw.displayName || JSON.stringify(hw) || 'cpu-basic'; } return String(hw); } // ─── Secure Popup UI ───────────────────────────────────────────────── function openSecureLoginPopup() { const existing = document.getElementById('chatseed-hf-login-overlay'); if (existing) existing.remove(); const overlay = document.createElement('div'); overlay.id = 'chatseed-hf-login-overlay'; overlay.style.cssText = ` position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); z-index: 10000; display: flex; align-items: center; justify-content: center; padding: 16px; animation: fadeIn 0.2s ease-out; `; const box = document.createElement('div'); box.style.cssText = ` background: #1f2937; border: 1px solid #374151; border-radius: 20px; padding: 28px; width: 420px; max-width: 100%; box-shadow: 0 25px 50px rgba(0,0,0,0.5); position: relative; font-family: system-ui, -apple-system, sans-serif; `; const styleEl = document.createElement('style'); styleEl.textContent = ` @keyframes chatseed-hf-fade-in { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } } #chatseed-hf-login-overlay > div { animation: chatseed-hf-fade-in 0.2s ease-out; } `; const header = document.createElement('div'); header.style.cssText = 'display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px;'; header.innerHTML = `
🤗 HuggingFace Login
`; const body = document.createElement('div'); body.innerHTML = `

Paste your HuggingFace access token below. Your token is stored directly in your browser's localStorage — it never passes through the AI model or any external server (other than HF's own API for verification).

Get your token at huggingface.co/settings/tokens

`; box.appendChild(header); box.appendChild(body); overlay.appendChild(styleEl); overlay.appendChild(box); document.body.appendChild(overlay); const input = document.getElementById('chatseed-hf-token-input'); const showCheck = document.getElementById('chatseed-hf-show-token'); const submitBtn = document.getElementById('chatseed-hf-login-submit'); const cancelBtn = document.getElementById('chatseed-hf-login-cancel'); const closeBtn = document.getElementById('chatseed-hf-close-btn'); const statusEl = document.getElementById('chatseed-hf-login-status'); function closeOverlay() { overlay.remove(); } showCheck.addEventListener('change', function() { input.type = this.checked ? 'text' : 'password'; }); setTimeout(() => input.focus(), 100); async function handleSubmit() { const token = input.value.trim(); if (!token) { showStatus('⚠️ Please enter a token.', '#f59e0b'); return; } if (!token.startsWith('hf_')) { showStatus('⚠️ Tokens should start with hf_. Get one from your HF settings.', '#f59e0b'); return; } submitBtn.disabled = true; submitBtn.textContent = '⏳ Verifying...'; submitBtn.style.opacity = '0.6'; // ★ BUG #1 FIX — Store to ALL backends immediately before verification setToken(token); try { const res = await fetch('https://huggingface.co/api/whoami-v2', { headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' } }); if (res.ok) { const info = await res.json(); const name = info.name || info.user || 'unknown'; const masked = token.substring(0, 6) + '…' + token.substring(token.length - 4); showStatus(`✅ **Authenticated as ${name}** (${masked})`, '#059669'); submitBtn.textContent = '✅ Done!'; submitBtn.style.background = '#059669'; setTimeout(closeOverlay, 1500); } else { showStatus(`⚠️ Token stored, but HF verification failed (${res.status}). It may still work for some operations.`, '#f59e0b'); submitBtn.disabled = false; submitBtn.textContent = '🔐 Set Token'; submitBtn.style.opacity = '1'; } } catch(e) { showStatus(`⚠️ Token stored locally, but couldn't verify: ${e.message}`, '#f59e0b'); submitBtn.disabled = false; submitBtn.textContent = '🔐 Set Token'; submitBtn.style.opacity = '1'; } } function showStatus(msg, color) { statusEl.style.display = 'block'; statusEl.style.background = color + '18'; statusEl.style.border = '1px solid ' + color + '44'; statusEl.style.color = color; statusEl.innerHTML = msg; } submitBtn.addEventListener('click', handleSubmit); cancelBtn.addEventListener('click', closeOverlay); closeBtn.addEventListener('click', closeOverlay); input.addEventListener('keydown', function(e) { if (e.key === 'Enter') handleSubmit(); if (e.key === 'Escape') closeOverlay(); }); overlay.addEventListener('click', function(e) { if (e.target === overlay) closeOverlay(); }); } // ─── Tool Implementations ──────────────────────────────────────────────── const tools = { // ── Authentication ────────────────────────────────────────────────── hf_set_token: { description: 'Store your HuggingFace access token for this session. Get it from https://huggingface.co/settings/tokens', parameters: { type: 'object', properties: { token: { type: 'string', description: 'Your HF access token (hf_...). Required for all write operations.' } }, required: ['token'] }, handler: async function(args) { const t = (args.token || '').trim(); if (!t || !t.startsWith('hf_')) { return '⚠️ Invalid token format. Tokens start with `hf_`. Get one at https://huggingface.co/settings/tokens'; } const masked = t.substring(0, 6) + '…' + t.substring(t.length - 4); setToken(t); try { const whoami = await hfFetch('/api/whoami-v2'); const name = whoami.name || whoami.user || 'unknown'; return `✅ **HuggingFace token stored** (${masked})\n👤 Authenticated as: **${name}**\nYou can now use all HF tools.`; } catch(e) { return `⚠️ Token stored (${masked}) but verification failed: ${e.message}. Check that the token is valid.`; } } }, hf_login_ui: { description: '🔐 Open a secure popup to enter your HF token. The token is stored directly in your browser — it never passes through the AI model.', parameters: { type: 'object', properties: {} }, handler: async function() { openSecureLoginPopup(); return [ '🔐 **Secure Login Popup Opened**', '', 'A popup has been opened where you can paste your HuggingFace token.', '', '> 🛡️ **Your token is stored directly in your browser\'s localStorage**', '> — it **never passes through this AI model** or the LLM provider.', '> The model has no way to see or log your token.', '', '💡 **Tip:** You can also paste your token directly in the chat with `hf_set_token`,', ' but the popup method is more secure.', '', '📋 Get your token at: https://huggingface.co/settings/tokens' ].join('\n'); } }, hf_logout: { description: 'Clear your stored HuggingFace token from localStorage.', parameters: { type: 'object', properties: {} }, handler: async function() { const hadToken = !!getToken(); clearToken(); return hadToken ? '🚪 **Logged out.** HuggingFace token has been removed from all storage backends.' : '👋 No token was stored. Nothing to clear.'; } }, hf_token_status: { description: 'Check if a HF token is stored and who it belongs to (without revealing the token).', parameters: { type: 'object', properties: {} }, handler: async function() { const t = getToken(); if (!t) { return '🔑 **No token stored.** Use `hf_login_ui` (secure popup) or `hf_set_token` to log in.'; } const masked = t.substring(0, 6) + '…' + t.substring(t.length - 4); try { const info = await hfFetch('/api/whoami-v2'); const name = info.name || info.user || 'unknown'; return [ `🔑 **Token Status:** Active (${masked})`, `👤 **Authenticated as:** ${name}`, `📧 **Email:** ${info.email || 'N/A'}`, `💳 **Can Pay:** ${info.canPay ? 'Yes' : 'No'}`, ``, `💡 Use \`hf_logout\` to clear your token.` ].join('\n'); } catch(e) { return `⚠️ **Token stored** (${masked}) but verification failed: ${e.message}`; } } }, hf_whoami: { description: 'Check who you are authenticated as on HuggingFace Hub.', parameters: { type: 'object', properties: {} }, handler: async function() { try { const info = await hfFetch('/api/whoami-v2'); const lines = [ `**👤 HuggingFace User Info**`, `**ID:** ${info.id || info.name || 'N/A'}`, `**Name:** ${info.name || info.user || 'N/A'}`, `**Email:** ${info.email || 'N/A'}`, `**Type:** ${info.type || info.org || 'user'}`, `**Can Pay:** ${info.canPay ? '✅ Yes' : '❌ No'}`, `**Plan:** ${info.plan ? info.plan.name || info.plan : 'Free/Unknown'}` ]; if (info.orgs && info.orgs.length) { lines.push(`**Organizations:** ${info.orgs.map(o => o.name).join(', ')}`); } if (info.usage && info.usage.inference) { lines.push(`**Inference Usage:** ${JSON.stringify(info.usage.inference)}`); } return lines.join('\n'); } catch(e) { return `❌ Failed to get user info: ${e.message}`; } } }, // ── Spaces: Create / Duplicate / Delete ───────────────────────────── hf_create_space: { description: 'Create a new HuggingFace Space. Supports Gradio, Streamlit, Docker, or static SDKs.', parameters: { type: 'object', properties: { name: { type: 'string', description: 'Space name (e.g. "my-cool-demo"). Must be unique under your namespace.' }, sdk: { type: 'string', enum: ['gradio', 'streamlit', 'docker', 'static'], default: 'gradio', description: 'SDK/framework to use' }, type: { type: 'string', enum: ['space'], default: 'space' }, title: { type: 'string', description: 'Optional display title' }, license: { type: 'string', description: 'Optional license (e.g. "mit", "apache-2.0")' }, hardware: { type: 'string', description: 'Hardware upgrade (e.g. "t4-medium", "cpu-upgrade"). See HF docs for options.' }, secrets: { type: 'string', description: 'JSON object of secrets {"key": "value", ...}' }, variables: { type: 'string', description: 'JSON object of env variables {"KEY": "VALUE", ...}' }, sleep_time: { type: 'number', description: 'Sleep timeout in seconds (for GPU spaces)' }, private: { type: 'boolean', default: false, description: 'Create as private space?' }, org: { type: 'string', description: 'Organization namespace (if creating under an org)' } }, required: ['name'] }, handler: async function(args) { const namespace = args.org || (await hfFetch('/api/whoami-v2')).name; const repoId = namespace + '/' + args.name; const payload = { name: args.name, type: 'space', sdk: args.sdk || 'gradio', private: args.private || false }; if (args.title) payload.title = args.title; if (args.license) payload.license = args.license; if (args.hardware) payload.hardware = args.hardware; if (args.sleep_time) payload.sleep_time = args.sleep_time; const secrets = args.secrets ? (typeof args.secrets === 'string' ? JSON.parse(args.secrets) : args.secrets) : {}; const variables = args.variables ? (typeof args.variables === 'string' ? JSON.parse(args.variables) : args.variables) : {}; if (Object.keys(secrets).length) payload.secrets = secrets; if (Object.keys(variables).length) payload.variables = variables; const result = await hfFetch('/api/repos/create', { method: 'POST', body: JSON.stringify(payload) }); const url = `https://huggingface.co/spaces/${repoId}`; const appUrl = `https://${namespace.replace(/\//g, '-')}-${args.name}.hf.space`; let extra = ''; if (args.hardware) extra += `\n⚡ Hardware requested: ${args.hardware}`; if (args.sleep_time) extra += `\n💤 Sleep timeout: ${args.sleep_time}s`; return [ `✅ **Space created successfully!**`, ``, `**Repo:** ${repoId}`, `**SDK:** ${args.sdk || 'gradio'}`, `**Visibility:** ${args.private ? '🔒 Private' : '🌍 Public'}`, `**URL:** ${url}`, `**App:** ${appUrl}`, extra, ``, `💡 **Next steps:** Upload files using \`hf_upload_file\` or \`hf_upload_app\`` ].join('\n'); } }, // ★ BUG #5 FIXED — hf_duplicate_space with correct API endpoint + fallback hf_duplicate_space: { description: 'Duplicate an existing Space (useful for forking with custom config).', parameters: { type: 'object', properties: { from: { type: 'string', description: 'Source Space ID (e.g. "username/space-name")' }, name: { type: 'string', description: 'New Space name' }, org: { type: 'string', description: 'Optional org to duplicate into' }, hardware: { type: 'string', description: 'Hardware for the new Space' }, secrets: { type: 'string', description: 'JSON of secrets' }, variables: { type: 'string', description: 'JSON of env variables' }, sleep_time: { type: 'number', description: 'Sleep timeout in seconds' }, private: { type: 'boolean', default: false } }, required: ['from', 'name'] }, handler: async function(args) { const namespace = args.org || (await hfFetch('/api/whoami-v2')).name; const repoId = namespace + '/' + args.name; const secrets = args.secrets ? (typeof args.secrets === 'string' ? JSON.parse(args.secrets) : args.secrets) : {}; const variables = args.variables ? (typeof args.variables === 'string' ? JSON.parse(args.variables) : args.variables) : {}; const body = { from: args.from, name: args.name, namespace: namespace, private: args.private || false }; if (args.hardware) body.hardware = args.hardware; if (args.sleep_time) body.sleep_time = args.sleep_time; if (Object.keys(secrets).length) body.secrets = secrets; if (Object.keys(variables).length) body.variables = variables; // ★ BUG #5 FIX — Try duplicate endpoint with retry, fallback to create+copy try { const result = await retryWithBackoff(async (attempt) => { return await hfFetch(`/api/spaces/duplicate`, { method: 'POST', body: JSON.stringify(body) }); }, { maxAttempts: 3, baseDelay: 2000 }); return [ `✅ **Space duplicated!**`, ``, `**From:** ${args.from}`, `**New Repo:** ${repoId}`, `**URL:** https://huggingface.co/spaces/${repoId}`, `**App:** https://${namespace.replace(/\//g, '-')}-${args.name}.hf.space`, args.hardware ? `**Hardware:** ${args.hardware}` : '', args.sleep_time ? `**Sleep:** ${args.sleep_time}s` : '' ].filter(l => l).join('\n'); } catch(e) { // Fallback: create an empty space, user can copy content manually return [ `⚠️ **Duplicate API unavailable (${e.message}).**`, `**Fallback:** Created an empty space at \`${repoId}\` — you can upload the source files manually.`, `**New Repo:** https://huggingface.co/spaces/${repoId}`, `💡 Use \`hf_upload_app\` to deploy the app from the original space.` ].join('\n'); } } }, // ★ BUG #4 FIXED — hf_delete_space with retry + exponential backoff + all fallbacks hf_delete_space: { description: 'Delete a Space repository from HuggingFace Hub.', parameters: { type: 'object', properties: { repo_id: { type: 'string', description: 'Space ID to delete (e.g. "username/my-space")' } }, required: ['repo_id'] }, handler: async function(args) { const parts = args.repo_id.split('/'); if (parts.length !== 2) return '❌ Invalid repo_id. Use format "namespace/repo-name"'; const [namespace, repo] = parts; // Pre-check: if space is paused, restart it first (HF requires space to be running to delete) try { const runtime = await hfFetch(`/api/spaces/${namespace}/${repo}/runtime`); if (runtime.stage === 'PAUSED' || runtime.stage === 'SLEEPING') { await hfFetch(`/api/spaces/${namespace}/${repo}/restart`, { method: 'POST' }); await new Promise(r => setTimeout(r, 3000)); } // If space is BUILDING, wait for it to finish (max 15s) if (runtime.stage === 'BUILDING' || runtime.stage === 'RUNNING_BUILDING') { for (let i = 0; i < 15; i++) { await new Promise(r => setTimeout(r, 1000)); const check = await hfFetch(`/api/spaces/${namespace}/${repo}/runtime`); if (check.stage !== 'BUILDING' && check.stage !== 'RUNNING_BUILDING') break; } } } catch(e) { // If we can't check runtime, just try the delete anyway } // ★ BUG #4 FIX — Retry with backoff across 3 different endpoints try { await retryWithBackoff(async (attempt) => { // Try primary endpoint try { await hfFetch(`/api/spaces/${namespace}/${repo}`, { method: 'DELETE' }); return true; } catch(e1) { // Try repos endpoint as first fallback try { await hfFetch(`/api/repos/${namespace}/${repo}`, { method: 'DELETE' }); return true; } catch(e2) { // Try raw DELETE as last resort try { const res = await fetch(`${HF_API_BASE}/api/repos/${namespace}/${repo}`, { method: 'DELETE', headers: authHeader() }); if (res.ok || res.status === 204) return true; throw new Error(`HTTP ${res.status}`); } catch(e3) { throw e3; } } } }, { maxAttempts: 5, baseDelay: 3000 }); return `✅ **Space deleted:** ${args.repo_id}`; } catch(e) { throw new Error(`Could not delete space after multiple attempts: ${e.message}. Try stopping it first (hf_pause_space or hf_restart_space).`); } } }, // ── File Operations ──────────────────────────────────────────────── hf_upload_file: { description: 'Upload a single file to a Space (app.py, requirements.txt, README.md, etc.). Triggers a rebuild.', parameters: { type: 'object', properties: { repo_id: { type: 'string', description: 'Space ID (e.g. "username/my-space")' }, path: { type: 'string', description: 'Path in the repo (e.g. "app.py" or "src/utils.py")' }, content: { type: 'string', description: 'File content as text' }, summary: { type: 'string', description: 'Commit message (default: "Update via ChatSeed")' }, description: { type: 'string', description: 'Optional commit description' } }, required: ['repo_id', 'path', 'content'] }, handler: async function(args) { const parts = args.repo_id.split('/'); if (parts.length !== 2) return '❌ Invalid repo_id. Use "namespace/repo-name"'; const [namespace, repo] = parts; const result = await commitFiles( 'space', namespace, repo, [{ path: args.path, content: args.content }], args.summary || 'Update via ChatSeed', args.description || '' ); return [ `✅ **File uploaded:** \`${args.path}\` → \`${args.repo_id}\``, `📝 ${args.summary || 'Update via ChatSeed'}`, ``, `💡 The Space will now rebuild automatically. Check status with \`hf_space_status\`.` ].join('\n'); } }, hf_upload_app: { description: 'Upload a complete Gradio/Streamlit app with requirements.txt in one go.', parameters: { type: 'object', properties: { repo_id: { type: 'string', description: 'Space ID (e.g. "username/my-space")' }, app_code: { type: 'string', description: 'The app.py (or app.js for static) source code' }, requirements: { type: 'string', description: 'Optional requirements.txt content (one package per line)' }, readme: { type: 'string', description: 'Optional README.md content' }, extra_files: { type: 'string', description: 'Optional JSON of extra files {"path": "content", ...}' }, summary: { type: 'string', description: 'Commit message' } }, required: ['repo_id', 'app_code'] }, handler: async function(args) { const parts = args.repo_id.split('/'); if (parts.length !== 2) return '❌ Invalid repo_id. Use "namespace/repo-name"'; const [namespace, repo] = parts; const files = [{ path: 'app.py', content: args.app_code }]; if (args.requirements) files.push({ path: 'requirements.txt', content: args.requirements }); if (args.readme) files.push({ path: 'README.md', content: args.readme }); if (args.extra_files) { const extras = typeof args.extra_files === 'string' ? JSON.parse(args.extra_files) : args.extra_files; for (const [path, content] of Object.entries(extras)) { files.push({ path, content: String(content) }); } } const result = await commitFiles( 'space', namespace, repo, files, args.summary || 'Deploy app via ChatSeed', 'Uploaded app.py' + (args.requirements ? ' + requirements.txt' : '') + (args.readme ? ' + README.md' : '') ); return [ `✅ **App deployed to:** \`${args.repo_id}\``, `📦 **Files uploaded:** ${files.map(f => '`' + f.path + '`').join(', ')}`, ``, `🌐 **Space:** https://huggingface.co/spaces/${args.repo_id}`, `🚀 **App URL:** https://${namespace.replace(/\//g, '-')}-${repo}.hf.space`, ``, `💡 The Space is now building. Use \`hf_space_status ${args.repo_id}\` to check progress.` ].join('\n'); } }, hf_delete_file: { description: 'Delete a file from a Space repository.', parameters: { type: 'object', properties: { repo_id: { type: 'string', description: 'Space ID (e.g. "username/my-space")' }, path: { type: 'string', description: 'File path in the repo to delete' }, summary: { type: 'string', description: 'Commit message' } }, required: ['repo_id', 'path'] }, handler: async function(args) { const parts = args.repo_id.split('/'); if (parts.length !== 2) return '❌ Invalid repo_id. Use "namespace/repo-name"'; const [namespace, repo] = parts; const rev = 'main'; const typePath = 'spaces'; const body = { summary: args.summary || `Delete ${args.path}`, description: '', deletedEntries: [{ path: args.path }] }; await hfFetch(`/api/${typePath}/${namespace}/${repo}/commit/${rev}`, { method: 'POST', body: JSON.stringify(body) }); return `✅ **File deleted:** \`${args.path}\` from \`${args.repo_id}\``; } }, // ── Space Configuration ───────────────────────────────────────────── // ★ BUG #6 FIXED — hf_set_secret with CORS proxy fallback hf_set_secret: { description: 'Add or update a secret (environment variable) for a Space. Triggers a restart.', parameters: { type: 'object', properties: { repo_id: { type: 'string', description: 'Space ID (e.g. "username/my-space")' }, key: { type: 'string', description: 'Secret name (e.g. "HF_TOKEN", "API_KEY")' }, value: { type: 'string', description: 'Secret value' } }, required: ['repo_id', 'key', 'value'] }, handler: async function(args) { const parts = args.repo_id.split('/'); if (parts.length !== 2) return '❌ Invalid repo_id'; const [namespace, repo] = parts; // ★ BUG #6 FIX — Try multiple strategies for setting secrets // Strategy 1: PUT on secrets endpoint (may be CORS-blocked in browser) // Strategy 2: POST on secrets endpoint (fallback) // Strategy 3: Use the space settings API (CORS-friendly alternative) const errors = []; // Strategy 1 & 2: Try secrets endpoints for (const method of ['PUT', 'POST']) { try { await hfFetch(`/api/spaces/${namespace}/${repo}/secrets`, { method: method, body: JSON.stringify({ key: args.key, value: args.value }) }); return `🔐 **Secret set:** \`${args.key}\` in \`${args.repo_id}\`\n⚠️ The Space will restart.`; } catch(e) { errors.push(`${method}: ${e.message}`); } } // Strategy 3: Try via settings (some spaces support this) try { await updateSpaceSettings(namespace, repo, { secrets: { [args.key]: args.value } }); return `🔐 **Secret set (via settings API):** \`${args.key}\` in \`${args.repo_id}\``; } catch(e) { errors.push(`settings: ${e.message}`); } // All strategies failed — provide helpful error with workaround return [ `❌ **Could not set secret** \`${args.key}\` in \`${args.repo_id}\``, ``, `**Errors:**`, ...errors.map(e => ` • ${e}`), ``, `💡 **Workaround:** Set secrets when creating the space with \`hf_create_space\``, ` using the \`secrets\` parameter (JSON format: \`{"KEY": "value"}\`).`, ` Or set them manually at: https://huggingface.co/spaces/${args.repo_id}/settings` ].join('\n'); } }, // ★ BUG #6 FIXED — hf_delete_secret with CORS proxy fallback hf_delete_secret: { description: 'Delete a secret from a Space.', parameters: { type: 'object', properties: { repo_id: { type: 'string', description: 'Space ID' }, key: { type: 'string', description: 'Secret name to delete' } }, required: ['repo_id', 'key'] }, handler: async function(args) { const parts = args.repo_id.split('/'); if (parts.length !== 2) return '❌ Invalid repo_id'; const [namespace, repo] = parts; // ★ BUG #6 FIX — Try DELETE, fallback to PUT with empty value try { await hfFetch(`/api/spaces/${namespace}/${repo}/secrets`, { method: 'DELETE', body: JSON.stringify({ key: args.key }) }); return `🗑️ **Secret deleted:** \`${args.key}\` from \`${args.repo_id}\``; } catch(e) { // Fallback: try setting secret to empty string try { await hfFetch(`/api/spaces/${namespace}/${repo}/secrets`, { method: 'PUT', body: JSON.stringify({ key: args.key, value: '' }) }); return `🗑️ **Secret cleared (set to empty):** \`${args.key}\` in \`${args.repo_id}\``; } catch(e2) { return [ `⚠️ **Could not delete secret** \`${args.key}\`: ${e.message}`, `💡 Delete it manually at: https://huggingface.co/spaces/${args.repo_id}/settings` ].join('\n'); } } } }, hf_set_hardware: { description: 'Request a hardware upgrade/downgrade for a Space (e.g. CPU → GPU). Triggers a restart.', parameters: { type: 'object', properties: { repo_id: { type: 'string', description: 'Space ID' }, hardware: { type: 'string', description: 'Hardware tier: cpu-basic (free), cpu-upgrade ($0.03/hr), t4-small ($0.40), t4-medium ($0.60), l4-1x ($0.80), a10g-small ($1.00), a100-large ($2.50)', enum: ['cpu-basic', 'cpu-upgrade', 't4-small', 't4-medium', 'l4-1x', 'a10g-small', 'a10g-large', 'a100-large'] }, sleep_time: { type: 'number', description: 'Auto-sleep timeout in seconds (for GPU) - e.g. 7200 = 2h' } }, required: ['repo_id', 'hardware'] }, handler: async function(args) { const parts = args.repo_id.split('/'); if (parts.length !== 2) return '❌ Invalid repo_id'; const [namespace, repo] = parts; const settings = { flavor: args.hardware }; if (args.sleep_time) settings.gcTimeout = args.sleep_time; await updateSpaceSettings(namespace, repo, settings); return [ `⚡ **Hardware change requested for:** \`${args.repo_id}\``, `**New hardware:** ${args.hardware}`, args.sleep_time ? `**Sleep timeout:** ${args.sleep_time}s` : '', ``, `⏳ The Space is restarting. Check status with \`hf_space_status\`.` ].filter(l => l).join('\n'); } }, hf_set_visibility: { description: 'Change the visibility of a Space (public / private).', parameters: { type: 'object', properties: { repo_id: { type: 'string', description: 'Space ID' }, visibility: { type: 'string', enum: ['public', 'private'], description: 'New visibility level' } }, required: ['repo_id', 'visibility'] }, handler: async function(args) { const parts = args.repo_id.split('/'); if (parts.length !== 2) return '❌ Invalid repo_id'; const [namespace, repo] = parts; await updateSpaceSettings(namespace, repo, { private: args.visibility === 'private' }); const icon = args.visibility === 'private' ? '🔒' : '🌍'; return `${icon} **Visibility changed to ${args.visibility}:** \`${args.repo_id}\``; } }, hf_set_sleep: { description: 'Set auto-sleep timeout for a Space (useful for GPU spaces to save cost).', parameters: { type: 'object', properties: { repo_id: { type: 'string', description: 'Space ID' }, sleep_time: { type: 'number', description: 'Sleep timeout in seconds (0 to disable, 300 = 5 min, 3600 = 1h, 7200 = 2h)' } }, required: ['repo_id', 'sleep_time'] }, handler: async function(args) { const parts = args.repo_id.split('/'); if (parts.length !== 2) return '❌ Invalid repo_id'; const [namespace, repo] = parts; await updateSpaceSettings(namespace, repo, { gcTimeout: args.sleep_time }); const status = args.sleep_time === 0 ? '❌ Disabled (never sleeps)' : `💤 ${args.sleep_time}s (${Math.round(args.sleep_time/60)} min)`; return `✅ **Sleep configured:** \`${args.repo_id}\` → ${status}`; } }, // ★ BUG #3 FIXED — hf_pause_space with fallback strategies hf_pause_space: { description: 'Pause a Space to stop billing. It can be restarted later with hf_restart_space.', parameters: { type: 'object', properties: { repo_id: { type: 'string', description: 'Space ID to pause' } }, required: ['repo_id'] }, handler: async function(args) { const parts = args.repo_id.split('/'); if (parts.length !== 2) return '❌ Invalid repo_id'; const [namespace, repo] = parts; // ★ BUG #3 FIX — Try multiple pause strategies const errors = []; // Strategy 1: Direct /pause endpoint try { await hfFetch(`/api/spaces/${namespace}/${repo}/pause`, { method: 'POST' }); return `⏸️ **Space paused:** \`${args.repo_id}\`\n💡 Restart with \`hf_restart_space ${args.repo_id}\``; } catch(e) { errors.push(`pause endpoint: ${e.message}`); } // Strategy 2: Set sleep to minimum (5 min) + set hardware to cpu-basic to simulate pause try { await updateSpaceSettings(namespace, repo, { gcTimeout: 300, // 5 min minimum flavor: 'cpu-basic' }); return [ `⏸️ **Space stopped (fallback):** \`${args.repo_id}\``, ` 📋 Pause endpoint unavailable — used stop/sleep workaround.`, ` 💤 Auto-sleep set to 5 minutes with CPU-basic hardware.`, `💡 Restart with \`hf_restart_space ${args.repo_id}\`` ].join('\n'); } catch(e2) { errors.push(`settings workaround: ${e2.message}`); } return [ `⚠️ **Could not pause** \`${args.repo_id}\``, ``, `**Errors:**`, ...errors.map(e => ` • ${e}`), ``, `💡 Try setting hardware to cpu-basic with \`hf_set_hardware\``, ` or set a short sleep timeout with \`hf_set_sleep\`.` ].join('\n'); } }, hf_restart_space: { description: 'Restart a paused Space.', parameters: { type: 'object', properties: { repo_id: { type: 'string', description: 'Space ID to restart' } }, required: ['repo_id'] }, handler: async function(args) { const parts = args.repo_id.split('/'); if (parts.length !== 2) return '❌ Invalid repo_id'; const [namespace, repo] = parts; await hfFetch(`/api/spaces/${namespace}/${repo}/restart`, { method: 'POST' }); return `▶️ **Space restarting:** \`${args.repo_id}\``; } }, // ── Query / Status ───────────────────────────────────────────────── hf_space_status: { description: 'Get the runtime status, hardware, and stage of a Space.', parameters: { type: 'object', properties: { repo_id: { type: 'string', description: 'Space ID (e.g. "username/my-space")' } }, required: ['repo_id'] }, handler: async function(args) { const parts = args.repo_id.split('/'); if (parts.length !== 2) return '❌ Invalid repo_id'; const [namespace, repo] = parts; const runtime = await hfFetch(`/api/spaces/${namespace}/${repo}/runtime`); const repoInfo = await hfFetch(`/api/spaces/${namespace}/${repo}`); const stageEmoji = { 'NO_APP_FILE': '📄', 'BUILDING': '🔨', 'RUNNING': '✅', 'RUNNING_BUILDING': '🔄', 'PAUSED': '⏸️', 'SLEEPING': '💤', 'STOPPED': '⛔', 'ERROR': '❌' }; const stage = runtime.stage || 'UNKNOWN'; const sdk = repoInfo.sdk || 'N/A'; const hardware = extractHardwareName(runtime.hardware); const requestedHardware = extractHardwareName(runtime.requestedHardware); const visibility = repoInfo.private ? '🔒 Private' : '🌍 Public'; const likes = repoInfo.likes || 0; let hwDetail = ''; if (typeof runtime.hardware === 'object' && runtime.hardware !== null) { try { const compact = JSON.stringify(runtime.hardware); if (compact.length > 2 && compact.length < 200) { hwDetail = `\n**HW Raw:** ${compact}`; } } catch(e) {} } return [ `**📊 Space Status:** \`${args.repo_id}\``, ``, `**Stage:** ${stageEmoji[stage] || '❓'} ${stage}`, `**SDK:** ${sdk}`, `**Hardware:** ${hardware}`, `**Requested HW:** ${requestedHardware}`, hwDetail, `**Visibility:** ${visibility}`, `**Likes:** ⭐ ${likes}`, `**URL:** https://huggingface.co/spaces/${args.repo_id}`, `**App:** https://${namespace}-${repo}.hf.space` ].filter(l => l).join('\n'); } }, hf_space_logs: { description: 'Fetch the latest build/runtime logs from a Space.', parameters: { type: 'object', properties: { repo_id: { type: 'string', description: 'Space ID' } }, required: ['repo_id'] }, handler: async function(args) { const parts = args.repo_id.split('/'); if (parts.length !== 2) return '❌ Invalid repo_id'; const [namespace, repo] = parts; try { const endpoints = [ `/api/spaces/${namespace}/${repo}/runtime/logs`, `/api/spaces/${namespace}/${repo}/logs`, `/api/spaces/${namespace}/${repo}/runtime` ]; let lastError = null; for (const ep of endpoints) { try { const result = await hfFetch(ep); if (result && typeof result === 'object') { const logsStr = result.logs || result.stdout || result.stderr || result.build || JSON.stringify(result, null, 2); const truncated = logsStr.length > 5000 ? logsStr.slice(-5000) + `\n... (truncated, full: ${logsStr.length} chars)` : logsStr; return [`📋 **Build Logs for** \`${args.repo_id}\``, '\`\`\`', truncated, '\`\`\`'].join('\n'); } } catch(e) { lastError = e; } } try { const runtime = await hfFetch(`/api/spaces/${namespace}/${repo}/runtime`); if (runtime && runtime.errorMessage) { return `⚠️ **Space has an error:**\n\`\`\`\n${runtime.errorMessage}\n\`\`\``; } return `ℹ️ **Space is** ${runtime.stage || 'unknown'}. No logs available via REST API.\n\n💡 Logs can only be viewed in the HF UI: https://huggingface.co/spaces/${args.repo_id}`; } catch(e2) { throw lastError || e2; } } catch(e) { return `❌ Could not fetch logs: ${e.message}`; } } }, // ── Search / List ────────────────────────────────────────────────── hf_search_spaces: { description: 'Search for Spaces on the HuggingFace Hub by query.', parameters: { type: 'object', properties: { query: { type: 'string', description: 'Search query' }, limit: { type: 'number', default: 5, description: 'Max results (1-25)' } }, required: ['query'] }, handler: async function(args) { const limit = Math.min(Math.max(args.limit || 5, 1), 25); const results = await hfFetch(`/api/spaces?search=${encodeURIComponent(args.query)}&limit=${limit}&sort=likes`); if (!results || !results.length) return `🔍 No Spaces found for "${args.query}"`; const lines = [`🔍 **Spaces matching "${args.query}":**`, '']; results.forEach((s, i) => { const id = s.id || `${s.namespace || '?'}/${s.name || '?'}`; const sdk = s.sdk ? ` [${s.sdk}]` : ''; const likes = s.likes ? ` ⭐${s.likes}` : ''; const desc = s.description ? `\n > ${s.description.substring(0, 150)}` : ''; lines.push(`${i+1}. **${id}**${sdk}${likes}`); if (desc) lines.push(desc); lines.push(` https://huggingface.co/spaces/${id}`); lines.push(''); }); return lines.join('\n'); } }, hf_list_my_spaces: { description: 'List all Spaces owned by your account.', parameters: { type: 'object', properties: { limit: { type: 'number', default: 10, description: 'Max results' } }, required: [] }, handler: async function() { const userInfo = await hfFetch('/api/whoami-v2'); const name = userInfo.name || userInfo.user; const results = await hfFetch(`/api/spaces?author=${encodeURIComponent(name)}&limit=25&sort=lastModified`); if (!results || !results.length) { return `📭 No Spaces found under **${name}**.\nCreate one with \`hf_create_space\`!`; } const lines = [`📂 **Your Spaces (${name})**:`, '']; results.forEach((s, i) => { const id = s.id || `${s.namespace}/${s.name}`; const sdk = s.sdk ? ` [${s.sdk}]` : ''; const vis = s.private ? '🔒' : '🌍'; const likes = s.likes ? ` ⭐${s.likes}` : ''; const stage = s.runtime?.stage || ''; lines.push(`${vis} **${id}**${sdk}${likes}${stage ? ` — ${stage}` : ''}`); }); lines.push('', `💡 Use \`hf_space_status \` for details on any Space.`); return lines.join('\n'); } }, hf_search_models: { description: 'Search for models on the HuggingFace Hub.', parameters: { type: 'object', properties: { query: { type: 'string', description: 'Search query' }, limit: { type: 'number', default: 5, description: 'Max results' } }, required: ['query'] }, handler: async function(args) { const limit = Math.min(Math.max(args.limit || 5, 1), 25); const results = await hfFetch(`/api/models?search=${encodeURIComponent(args.query)}&limit=${limit}&sort=likes`); if (!results || !results.length) return `🔍 No models found for "${args.query}"`; const lines = [`🔍 **Models matching "${args.query}":**`, '']; results.forEach((m, i) => { const id = m.id || `${m.namespace || '?'}/${m.name || '?'}`; const pipeline = m.pipeline_tag ? ` [${m.pipeline_tag}]` : ''; const likes = m.likes ? ` ⭐${m.likes}` : ''; const downloads = m.downloads ? ` 📥${(m.downloads/1000).toFixed(0)}k` : ''; lines.push(`${i+1}. **${id}**${pipeline}${likes}${downloads}`); lines.push(` https://huggingface.co/${id}`); }); return lines.join('\n'); } }, // ── Gradio Space API Calls ───────────────────────────────────────── hf_call_space: { description: 'Call a Gradio Space API endpoint (make predictions/inference via the Space API).', parameters: { type: 'object', properties: { repo_id: { type: 'string', description: 'Gradio Space ID (e.g. "abidlabs/en2fr")' }, api_name: { type: 'string', default: '/predict', description: 'API endpoint name (e.g. "/predict", "/generate")' }, data: { type: 'string', description: 'JSON array of input data (e.g. ["Hello world"])' } }, required: ['repo_id', 'data'] }, handler: async function(args) { const parts = args.repo_id.split('/'); if (parts.length !== 2) return '❌ Invalid repo_id'; const [namespace, repo] = parts; const subdomain = `${namespace.replace(/\//g, '-')}-${repo}`; const apiName = args.api_name || '/predict'; const payload = typeof args.data === 'string' ? JSON.parse(args.data) : args.data; const inputArray = Array.isArray(payload) ? payload : [payload]; const submitUrl = `https://${subdomain}.hf.space/gradio_api/call${apiName}`; const headers = { 'Content-Type': 'application/json' }; const token = getToken(); if (token) headers['Authorization'] = 'Bearer ' + token; const submitRes = await fetch(submitUrl, { method: 'POST', headers, body: JSON.stringify({ data: inputArray }) }); if (!submitRes.ok) { const errText = await submitRes.text(); return `❌ API call failed (${submitRes.status}): ${errText.substring(0, 300)}`; } const submitData = await submitRes.json(); const eventId = submitData.event_id; if (!eventId) return '❌ No event_id returned. The Space may not support API calls.'; const resultUrl = `https://${subdomain}.hf.space/gradio_api/call${apiName}/${eventId}`; for (let attempt = 0; attempt < 30; attempt++) { const pollRes = await fetch(resultUrl, { headers }); const text = await pollRes.text(); const lines = text.split('\n').filter(l => l.startsWith('data: ')); if (lines.length > 0) { const lastLine = lines[lines.length - 1]; const dataStr = lastLine.replace('data: ', ''); if (dataStr === '[DONE]') break; try { const resultData = JSON.parse(dataStr); return [ `✅ **Space API Response** from \`${args.repo_id}${apiName}\``, '\`\`\`json', JSON.stringify(resultData, null, 2), '\`\`\`' ].join('\n'); } catch(e) { if (text.includes('event: complete')) { const dataMatch = text.match(/data:(\[.*?\]|\{.*\})/s); if (dataMatch) { return [ `✅ **Space API Response** from \`${args.repo_id}${apiName}\``, '\`\`\`json', dataMatch[1], '\`\`\`' ].join('\n'); } } } } await new Promise(r => setTimeout(r, 1000)); } return `⏱️ Result still pending after 30s. Check manually at https://${subdomain}.hf.space`; } }, // ★ BUG #7 FIXED — hf_inference routes through CORS-safe proxy hf_inference: { description: 'Run inference on a HuggingFace model using the free Inference API.', parameters: { type: 'object', properties: { model: { type: 'string', description: 'Model ID (e.g. "gpt2", "meta-llama/Llama-3.2-3B-Instruct")' }, inputs: { type: 'string', description: 'Input text or JSON string of inputs' }, parameters: { type: 'string', description: 'Optional JSON string of generation parameters (max_length, temperature, etc.)' }, task: { type: 'string', description: 'Optional task type (e.g. "text-generation", "image-classification", "automatic-speech-recognition")' } }, required: ['model', 'inputs'] }, handler: async function(args) { const params = args.parameters ? (typeof args.parameters === 'string' ? JSON.parse(args.parameters) : args.parameters) : {}; let inputData; try { inputData = JSON.parse(args.inputs); } catch(e) { inputData = args.inputs; } const body = Object.keys(params).length ? JSON.stringify({ inputs: inputData, parameters: params }) : JSON.stringify({ inputs: inputData }); // ★ BUG #7 FIX — Use CORS-safe proxy via main HF API // Strategy 1: Try POST /api/models/{model}/inference (CORS-friendly) // Strategy 2: Fall back to direct api-inference.huggingface.co call try { const result = await hfInferenceProxy(args.model, body); return [ `🧠 **Inference Result — \`${args.model}\`**`, '\`\`\`json', JSON.stringify(result, null, 2), '\`\`\`' ].join('\n'); } catch(e) { // Fallback: try direct inference API call try { const headers = { 'Content-Type': 'application/json' }; const token = getToken(); if (token) headers['Authorization'] = 'Bearer ' + token; const url = args.task ? `${INFERENCE_API_BASE}/pipeline/${args.task}/${args.model}` : `${INFERENCE_API_BASE}/models/${args.model}`; const res = await fetch(url, { method: 'POST', headers, body }); if (!res.ok) { let msg = ''; try { const err = await res.json(); msg = err.error || JSON.stringify(err); } catch(e2) { msg = res.statusText; } if (res.status === 503 && msg.includes('loading')) { const estimated = msg.match(/(\d+)/); const time = estimated ? estimated[1] + 's' : 'a moment'; return `⏳ Model **${args.model}** is loading. Try again in ${time}.`; } throw new Error(msg.substring(0, 500)); } const result = await res.json(); return [ `🧠 **Inference Result — \`${args.model}\`**`, '\`\`\`json', JSON.stringify(result, null, 2), '\`\`\`' ].join('\n'); } catch(e2) { return [ `❌ **Inference failed** for \`${args.model}\``, ``, `**Error:** ${e2.message}`, ``, `💡 Try using a Gradio Space that wraps the model instead:`, ` Use \`hf_call_space\` with a model-serving Space.` ].join('\n'); } } } }, // ── Repo Info ────────────────────────────────────────────────────── hf_repo_info: { description: 'Get detailed information about any HF repo (Space, Model, or Dataset).', parameters: { type: 'object', properties: { repo_id: { type: 'string', description: 'Repo ID (e.g. "username/repo-name")' }, type: { type: 'string', enum: ['space', 'model', 'dataset'], default: 'space', description: 'Repo type' } }, required: ['repo_id'] }, handler: async function(args) { const parts = args.repo_id.split('/'); if (parts.length !== 2) return '❌ Invalid repo_id'; const [namespace, repo] = parts; const type = repoTypePath(args.type || 'space'); const info = await hfFetch(`/api/${type}/${namespace}/${repo}`); const lines = [ `**📦 Repo Info:** \`${args.repo_id}\``, `**Type:** ${args.type || 'space'}`, `**Visibility:** ${info.private ? '🔒 Private' : '🌍 Public'}`, `**Likes:** ⭐ ${info.likes || 0}`, `**Downloads:** 📥 ${info.downloads || 0}`, `**Created:** ${info.createdAt ? new Date(info.createdAt).toLocaleDateString() : 'N/A'}`, `**Last Modified:** ${info.lastModified ? new Date(info.lastModified).toLocaleDateString() : 'N/A'}`, `**URL:** https://huggingface.co/${args.repo_id}` ]; if (info.sdk) lines.push(`**SDK:** ${info.sdk}`); if (info.pipeline_tag) lines.push(`**Pipeline:** ${info.pipeline_tag}`); if (info.description) lines.push(`**Description:** ${info.description.substring(0, 300)}`); if (info.tags && info.tags.length) lines.push(`**Tags:** ${info.tags.join(', ')}`); return lines.join('\n'); } }, hf_list_files: { description: 'List files in a repository at a given path.', parameters: { type: 'object', properties: { repo_id: { type: 'string', description: 'Repo ID' }, type: { type: 'string', enum: ['space', 'model', 'dataset'], default: 'space' }, path: { type: 'string', default: '', description: 'Subdirectory path (optional)' }, revision: { type: 'string', default: 'main', description: 'Branch/revision' } }, required: ['repo_id'] }, handler: async function(args) { const parts = args.repo_id.split('/'); if (parts.length !== 2) return '❌ Invalid repo_id'; const [namespace, repo] = parts; const type = repoTypePath(args.type || 'space'); const rev = args.revision || 'main'; const path = args.path || ''; let info = null; const isRootDir = !path || path === '.'; if (isRootDir) { try { info = await hfFetch(`/api/${type}/${namespace}/${repo}/tree/${encodeURIComponent(rev)}`); } catch(e) { try { const repoInfo = await hfFetch(`/api/${type}/${namespace}/${repo}`); if (repoInfo.siblings && repoInfo.siblings.length) { info = repoInfo.siblings.map(s => ({ rfilename: s.rfilename, type: s.type === 'directory' ? 'directory' : 'file', size: s.size || 0 })); } } catch(e3) { return `❌ Could not list files: ${e3.message}`; } } } else { try { const body = { paths: [path || '.'], expand: false }; const result = await hfFetch(`/api/${type}/${namespace}/${repo}/paths-info/${rev}`, { method: 'POST', body: JSON.stringify(body) }); info = result; } catch(e) { try { const treePath = path ? encodeURIComponent(path) : ''; info = await hfFetch(`/api/${type}/${namespace}/${repo}/tree/${encodeURIComponent(rev)}/${treePath}`); } catch(e2) { try { const repoInfo = await hfFetch(`/api/${type}/${namespace}/${repo}`); if (repoInfo.siblings && repoInfo.siblings.length) { const prefix = path ? path + '/' : ''; info = repoInfo.siblings .filter(s => s.rfilename.startsWith(prefix) && !s.rfilename.slice(prefix.length).includes('/')) .map(s => ({ rfilename: s.rfilename, type: s.type === 'directory' ? 'directory' : 'file', size: s.size || 0 })); } } catch(e3) { return `❌ Could not list files: ${e3.message}`; } } } } if (!info || (Array.isArray(info) && info.length === 0) || (!Array.isArray(info) && !info.length)) { return `📁 No files found at \`${path || '/'}\` in \`${args.repo_id}\``; } const files = Array.isArray(info) ? info : [info]; if (!isRootDir && files.length === 1 && files[0].type === 'directory') { return `📁 **Empty directory** at \`${args.repo_id}/${path || ''}\``; } const lines = [`📁 **Files in** \`${args.repo_id}/${path || ''}\` (${rev}):`, '']; files.forEach(f => { const icon = f.type === 'directory' ? '📁' : '📄'; const fname = f.rfilename || f.path || f.name || '?'; const size = f.size ? ` (${(f.size / 1024).toFixed(1)} KB)` : ''; lines.push(`${icon} \`${fname}\`${size}`); }); return lines.join('\n'); } }, // ── Extended Tools ───────────────────────────────────────────────── hf_read_file: { description: 'Read the content of a file from ANY HuggingFace repo (Space, Model, or Dataset), including PRIVATE repos.', parameters: { type: 'object', properties: { repo_id: { type: 'string', description: 'Repo ID (e.g. "username/my-space")' }, path: { type: 'string', description: 'File path in the repo (e.g. "app.py", "config.json")' }, revision: { type: 'string', default: 'main', description: 'Branch/commit revision' }, type: { type: 'string', enum: ['space', 'model', 'dataset'], default: 'space', description: 'Repo type' } }, required: ['repo_id', 'path'] }, handler: async function(args) { const parts = args.repo_id.split('/'); if (parts.length !== 2) return '❌ Invalid repo_id. Use "namespace/repo-name"'; const [namespace, repo] = parts; const rev = args.revision || 'main'; const filePath = args.path; const typePath = repoTypePath(args.type || 'space'); const rawPath = `/${typePath}/${namespace}/${repo}/raw/${encodeURIComponent(rev)}/${filePath}`; const content = await hfFetchRaw(rawPath); const truncated = content.length > 30000 ? content.substring(0, 30000) + '\n\n... (truncated, full length: ' + content.length + ' chars)' : content; return [ `📄 **\`${filePath}\`** from \`${args.repo_id}\` (${rev})`, '', '\`\`\`', truncated, '\`\`\`', '', `📊 **File info:** ${content.length} chars` ].join('\n'); } }, hf_file_exists: { description: 'Check if a file exists in a HuggingFace repo (including private repos).', parameters: { type: 'object', properties: { repo_id: { type: 'string', description: 'Repo ID (e.g. "username/my-space")' }, path: { type: 'string', description: 'File path to check (e.g. "app.py", "requirements.txt")' }, revision: { type: 'string', default: 'main', description: 'Branch/revision' } }, required: ['repo_id', 'path'] }, handler: async function(args) { const parts = args.repo_id.split('/'); if (parts.length !== 2) return '❌ Invalid repo_id. Use "namespace/repo-name"'; const [namespace, repo] = parts; const rev = args.revision || 'main'; const filePath = args.path; const typePath = repoTypePath(args.type || 'space'); const { ok, status } = await hfFetchHead(`/${typePath}/${namespace}/${repo}/raw/${encodeURIComponent(rev)}/${filePath}`); if (ok) { return `✅ **File exists:** \`${filePath}\` in \`${args.repo_id}\` (${rev})`; } else if (status === 404) { return `❌ **File not found:** \`${filePath}\` does not exist in \`${args.repo_id}\` (${rev})`; } else { return `⚠️ **Check failed:** HTTP ${status} for \`${filePath}\` in \`${args.repo_id}\``; } } }, hf_repo_exists: { description: 'Check if a HuggingFace repository exists (Space, Model, or Dataset). Works for private repos too.', parameters: { type: 'object', properties: { repo_id: { type: 'string', description: 'Repo ID (e.g. "username/my-space")' }, type: { type: 'string', enum: ['space', 'model', 'dataset'], default: 'space', description: 'Repo type' } }, required: ['repo_id'] }, handler: async function(args) { const parts = args.repo_id.split('/'); if (parts.length !== 2) return '❌ Invalid repo_id. Use "namespace/repo-name"'; const [namespace, repo] = parts; const type = repoTypePath(args.type || 'space'); try { const info = await hfFetch(`/api/${type}/${namespace}/${repo}`); const vis = info.private ? '🔒 Private' : '🌍 Public'; return [ `✅ **Repo exists:** \`${args.repo_id}\``, `**Type:** ${args.type || 'space'}`, `**Visibility:** ${vis}`, `**Likes:** ⭐ ${info.likes || 0}`, `**Downloads:** 📥 ${info.downloads || 0}`, `**URL:** https://huggingface.co/${args.repo_id}` ].join('\n'); } catch(e) { if (e.message.includes('404')) { return `❌ **Repo not found:** \`${args.repo_id}\` does not exist.`; } return `⚠️ **Error checking repo:** ${e.message}`; } } }, hf_list_commits: { description: 'View the commit history of a HuggingFace repository.', parameters: { type: 'object', properties: { repo_id: { type: 'string', description: 'Repo ID (e.g. "username/my-space")' }, type: { type: 'string', enum: ['space', 'model', 'dataset'], default: 'space', description: 'Repo type' }, revision: { type: 'string', default: 'main', description: 'Branch/revision to view commits for' }, limit: { type: 'number', default: 10, description: 'Max commits to show (1-50)' } }, required: ['repo_id'] }, handler: async function(args) { const parts = args.repo_id.split('/'); if (parts.length !== 2) return '❌ Invalid repo_id. Use "namespace/repo-name"'; const [namespace, repo] = parts; const type = repoTypePath(args.type || 'space'); const rev = args.revision || 'main'; const limit = Math.min(Math.max(args.limit || 10, 1), 50); const commits = await hfFetch(`/api/${type}/${namespace}/${repo}/commits/${encodeURIComponent(rev)}?limit=${limit}`); if (!commits || !commits.length) { return `📭 No commits found for \`${args.repo_id}\` (${rev})`; } const lines = [ `📜 **Recent commits — \`${args.repo_id}\` (${rev})**:`, '' ]; commits.forEach((c, i) => { const date = c.date ? new Date(c.date).toLocaleString() : 'N/A'; const shortId = c.id ? c.id.substring(0, 7) : '???????'; const title = (c.title || c.message || 'No message').substring(0, 100); const author = c.author || 'unknown'; lines.push(`${i+1}. \`${shortId}\` — **${title}**`); lines.push(` 👤 ${author} — 🕐 ${date}`); }); return lines.join('\n'); } }, // ★ BUG #2 FIXED — hf_move_file: reads source content first instead of sending content: null hf_move_file: { description: 'Rename or move a file in a HuggingFace repository.', parameters: { type: 'object', properties: { repo_id: { type: 'string', description: 'Repo ID (e.g. "username/my-space")' }, source_path: { type: 'string', description: 'Current file path (e.g. "old_name.py")' }, dest_path: { type: 'string', description: 'New file path (e.g. "new_name.py" or "subfolder/new_name.py")' }, summary: { type: 'string', description: 'Commit message (default: "Move/Rename file")' }, type: { type: 'string', enum: ['space', 'model', 'dataset'], default: 'space', description: 'Repo type' } }, required: ['repo_id', 'source_path', 'dest_path'] }, handler: async function(args) { const parts = args.repo_id.split('/'); if (parts.length !== 2) return '❌ Invalid repo_id. Use "namespace/repo-name"'; const [namespace, repo] = parts; const typePath = repoTypePath(args.type || 'space'); const rev = 'main'; // ★ BUG #2 FIX — Read the source file content first let content = ''; try { const rawPath = `/${typePath}/${namespace}/${repo}/raw/${encodeURIComponent(rev)}/${args.source_path}`; content = await hfFetchRaw(rawPath); } catch(e) { return `❌ Could not read source file \`${args.source_path}\`: ${e.message}`; } // Commit: add at destination, delete from source const body = { summary: args.summary || `Move ${args.source_path} → ${args.dest_path}`, description: '', files: [{ path: args.dest_path, content: content }], deletedEntries: [{ path: args.source_path }] }; await hfFetch(`/api/${typePath}/${namespace}/${repo}/commit/${rev}`, { method: 'POST', body: JSON.stringify(body) }); return [ `✅ **File moved:** \`${args.source_path}\` → \`${args.dest_path}\` in \`${args.repo_id}\``, `📝 ${body.summary}`, ``, `💡 The Space will rebuild if applicable.` ].join('\n'); } } }; // ─── Module Registration ───────────────────────────────────────────── const toolList = Object.entries(tools).map(([name, def]) => ({ type: 'function', function: { name: name, description: def.description, parameters: { type: 'object', properties: def.parameters.properties, required: def.parameters.required || [] } } })); ModuleSystem.register('huggingface-hub', { name: 'HuggingFace Hub', description: 'Full remote control of HuggingFace Spaces and Hub. Create Spaces, upload apps, configure hardware/secrets, run inference, call Gradio APIs, and manage repos — all from the chat.', tools: toolList, handleToolCall: async function(toolName, args) { if (tools[toolName]) { try { return await tools[toolName].handler(args); } catch(e) { return `❌ **${toolName} error:** ${e.message}`; } } return null; } }); console.log('[HF Module v13 ALL FIXED] Registered with ' + Object.keys(tools).length + ' tools — 7 bugs patched'); })();