Spaces:
Running
Running
| // 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 = ` | |
| <div style="display:flex;align-items:center;gap:10px;"> | |
| <span style="font-size:1.5rem;">π€</span> | |
| <span style="font-size:1.1rem;font-weight:600;color:#f3f4f6;">HuggingFace Login</span> | |
| </div> | |
| <button id="chatseed-hf-close-btn" style=" | |
| background:none;border:none;color:#9ca3af;font-size:1.3rem;cursor:pointer; | |
| width:32px;height:32px;border-radius:8px;display:flex;align-items:center; | |
| justify-content:center;transition:background 0.15s; | |
| " onmouseover="this.style.background='#374151'" onmouseout="this.style.background='none'">β</button> | |
| `; | |
| const body = document.createElement('div'); | |
| body.innerHTML = ` | |
| <p style="color:#d1d5db;font-size:0.85rem;margin:0 0 16px 0;line-height:1.5;"> | |
| Paste your HuggingFace access token below. | |
| <strong style="color:#6ee7b7;">Your token is stored directly in your browser's localStorage</strong> | |
| β it never passes through the AI model or any external server (other than HF's own API for verification). | |
| </p> | |
| <div style="background:#111827;border:1px solid #374151;border-radius:12px;padding:12px 16px;margin-bottom:16px;"> | |
| <label style="color:#9ca3af;font-size:0.7rem;text-transform:uppercase;letter-spacing:0.04em;display:block;margin-bottom:6px;"> | |
| π HuggingFace Token | |
| </label> | |
| <input type="password" id="chatseed-hf-token-input" placeholder="hf_..." style=" | |
| width:100%;padding:10px 12px;background:#030712;border:1px solid #4b5563; | |
| border-radius:8px;color:#f3f4f6;font-size:0.9rem;outline:none; | |
| transition:border-color 0.15s;box-sizing:border-box; | |
| "> | |
| <div style="margin-top:6px;display:flex;align-items:center;gap:6px;"> | |
| <input type="checkbox" id="chatseed-hf-show-token" style="accent-color:#059669;"> | |
| <label for="chatseed-hf-show-token" style="color:#6b7280;font-size:0.75rem;cursor:pointer;">Show token</label> | |
| </div> | |
| </div> | |
| <div id="chatseed-hf-login-status" style=" | |
| display:none;font-size:0.8rem;padding:8px 12px;border-radius:8px;margin-bottom:12px; | |
| "></div> | |
| <div style="display:flex;gap:10px;"> | |
| <button id="chatseed-hf-login-submit" style=" | |
| flex:1;padding:10px 16px;background:#059669;color:white;border:none; | |
| border-radius:10px;font-size:0.9rem;font-weight:500;cursor:pointer; | |
| transition:background 0.15s; | |
| " onmouseover="this.style.background='#047857'" onmouseout="this.style.background='#059669'"> | |
| π Set Token | |
| </button> | |
| <button id="chatseed-hf-login-cancel" style=" | |
| padding:10px 16px;background:#374151;color:#d1d5db;border:none; | |
| border-radius:10px;font-size:0.9rem;cursor:pointer; | |
| transition:background 0.15s; | |
| " onmouseover="this.style.background='#4b5563'" onmouseout="this.style.background='#374151'"> | |
| Cancel | |
| </button> | |
| </div> | |
| <p style="color:#6b7280;font-size:0.7rem;margin:12px 0 0 0;text-align:center;"> | |
| Get your token at <a href="https://huggingface.co/settings/tokens" target="_blank" style="color:#60a5fa;">huggingface.co/settings/tokens</a> | |
| </p> | |
| `; | |
| 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 <code>hf_</code>. 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 <id>\` 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'); | |
| })(); |