chatseed-live / modules /modules_huggingface.js
broadfield's picture
Update modules/modules_huggingface.js
f4acd2c verified
Raw
History Blame Contribute Delete
83.2 kB
// 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');
})();