File size: 52,663 Bytes
3bc0be8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
// HuggingBot Module: HuggingFace Hub v2.0.0 (FULL + EXPANDED)
// ============================================================================
// Full remote control of the HuggingFace Hub from chat:
//   auth, Spaces CRUD, file ops, secrets/hardware/sleep config, search,
//   model inference (Inference API), Gradio Space API, repo browsing.
// Rebuilt for the HuggingBot ModuleSystem from ChatSeed v12 (all API fixes
// applied: flavor/gcTimeout keys, PUT /settings, paused-space delete pre-check,
// deep hardware name extraction) and expanded with:
//   * hf_search_datasets
//   * modern Gradio API routing (/api/{name} with /api/predict fallback)
//   * pipeline-aware Inference API calls (task routing)
//   * binary (image) inference response handling -> inline markdown
//   * multi-repo-type support for file/repo operations (space|model|dataset)
// ============================================================================
(function(MS) {
  'use strict';

  var HF_API_BASE = 'https://huggingface.co';
  var INFER_API_BASE = 'https://api-inference.huggingface.co';
  var TOKEN_KEY = 'chatseed_hf_token';

  // ─── Token helpers ────────────────────────────────────────────────────
  function getToken()   { try { return localStorage.getItem(TOKEN_KEY) || ''; } catch (e) { return ''; } }
  function setToken(t)  { try { localStorage.setItem(TOKEN_KEY, t); } catch (e) {} }
  function clearToken() { try { localStorage.removeItem(TOKEN_KEY); } catch (e) {} }

  function authHeader() {
    var 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 };
  }

  // ─── Fetch helpers ────────────────────────────────────────────────────
  async function hfFetch(path, options) {
    options = options || {};
    var headers = Object.assign({ 'Content-Type': 'application/json' }, authHeader(), options.headers || {});
    var res = await fetch(HF_API_BASE + path, Object.assign({}, options, { headers: headers }));
    if (!res.ok) {
      var msg = '';
      try { var 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;
    return (res.headers.get('content-type') || '').indexOf('application/json') > -1 ? res.json() : res.text();
  }

  async function hfFetchRaw(path) {
    var token = getToken();
    var headers = {};
    if (token) headers['Authorization'] = 'Bearer ' + token;
    var res = await fetch(HF_API_BASE + path, { headers: headers });
    if (!res.ok) {
      var msg = '';
      try { var 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();
  }

  async function hfFetchHead(path) {
    var token = getToken();
    var headers = {};
    if (token) headers['Authorization'] = 'Bearer ' + token;
    var res = await fetch(HF_API_BASE + path, { method: 'HEAD', headers: headers });
    return { ok: res.ok, status: res.status };
  }

  // ─── Repo helpers ─────────────────────────────────────────────────────
  function repoTypePath(type) {
    switch (type) {
      case 'model':   return 'models';
      case 'dataset': return 'datasets';
      case 'space':   return 'spaces';
      default:        return 'spaces';
    }
  }

  function parseRepoId(repoId) {
    var parts = String(repoId || '').split('/');
    if (parts.length !== 2) throw new Error('Invalid repo_id. Use format "namespace/repo-name"');
    return { namespace: parts[0], name: parts[1] };
  }

  async function commitFiles(repoType, namespace, repo, files, deletedEntries, summary, description) {
    var body = {
      summary: summary || 'Update via HuggingBot',
      description: description || '',
      files: (files || []).map(function (f) {
        return { path: f.path, content: f.content, encoding: f.encoding || 'utf-8' };
      })
    };
    if (deletedEntries && deletedEntries.length) body.deletedEntries = deletedEntries;
    return hfFetch('/api/' + repoTypePath(repoType) + '/' + namespace + '/' + repo + '/commit/main', {
      method: 'POST',
      body: JSON.stringify(body)
    });
  }

  // Centralized settings helper β€” routes ALL Space settings through
  // PUT /api/spaces/{ns}/{name}/settings with the keys HF actually accepts.
  async function updateSpaceSettings(namespace, repo, settings) {
    return hfFetch('/api/spaces/' + namespace + '/' + repo + '/settings', {
      method: 'PUT',
      body: JSON.stringify(settings)
    });
  }

  // Deep-extract a human-readable hardware name from varied 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);
  }

  function repoAppUrl(repoId) {
    return 'https://' + repoId.replace(/\//g, '-') + '.hf.space';
  }

  function maskToken(t) {
    return t.substring(0, 6) + '…' + t.substring(t.length - 4);
  }

  function parseJsonArg(v) {
    if (!v) return {};
    if (typeof v === 'object') return v;
    try { return JSON.parse(v); } catch (e) { throw new Error('Invalid JSON argument: ' + v); }
  }

  // ─── Secure Login Popup (token never passes through the model) ────────
  function openLoginPopup() {
    var existing = document.getElementById('huggingbot-hf-login-overlay');
    if (existing) existing.remove();
    var overlay = document.createElement('div');
    overlay.id = 'huggingbot-hf-login-overlay';
    overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.7);z-index:10000;display:flex;align-items:center;justify-content:center;padding:16px;';
    var box = document.createElement('div');
    box.style.cssText = 'background:#111827;border:1px solid #374151;border-radius:16px;padding:24px;width:420px;max-width:100%;box-shadow:0 25px 50px rgba(0,0,0,.5);font-family:system-ui,sans-serif;';
    var header = document.createElement('div');
    header.style.cssText = 'display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;';
    header.innerHTML = '<div style="display:flex;align-items:center;gap:8px;"><span style="font-size:1.4rem;">πŸ€—</span><span style="font-size:1.05rem;font-weight:600;color:#f3f4f6;">HuggingFace Login</span></div>' +
      '<button id="huggingbot-hf-close" style="background:none;border:none;color:#9ca3af;font-size:1.2rem;cursor:pointer;">βœ•</button>';
    var body = document.createElement('div');
    body.innerHTML =
      '<p style="color:#d1d5db;font-size:.82rem;line-height:1.5;margin:0 0 14px;">Paste your HuggingFace access token.</p>' +
      '<input id="huggingbot-hf-token" type="password" placeholder="hf_..." style="width:100%;box-sizing:border-box;padding:10px 12px;background:#030712;border:1px solid #4b5563;border-radius:8px;color:#f3f4f6;font-size:.9rem;outline:none;"/>' +
      '<div style="margin-top:6px;display:flex;align-items:center;gap:6px;"><input id="huggingbot-hf-show" type="checkbox" style="accent-color:#059669;"><label for="huggingbot-hf-show" style="color:#6b7280;font-size:.75rem;cursor:pointer;">Show token</label></div>' +
      '<div id="huggingbot-hf-status" style="display:none;font-size:.8rem;padding:8px 12px;border-radius:8px;margin-top:10px;"></div>' +
      '<div style="display:flex;gap:10px;margin-top:14px;">' +
      '<button id="huggingbot-hf-submit" style="flex:1;padding:10px;background:#059669;color:#fff;border:none;border-radius:10px;font-weight:500;cursor:pointer;">πŸ” Set Token</button>' +
      '<button id="huggingbot-hf-cancel" style="padding:10px 16px;background:#374151;color:#d1d5db;border:none;border-radius:10px;cursor:pointer;">Cancel</button></div>' +
      '<p style="color:#6b7280;font-size:.7rem;margin:12px 0 0;text-align:center;">Get a 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(box);
    document.body.appendChild(overlay);

    var input = document.getElementById('huggingbot-hf-token');
    var statusEl = document.getElementById('huggingbot-hf-status');
    var submitBtn = document.getElementById('huggingbot-hf-submit');
    function close() { overlay.remove(); }
    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;
    }
    document.getElementById('huggingbot-hf-show').addEventListener('change', function () { input.type = this.checked ? 'text' : 'password'; });
    document.getElementById('huggingbot-hf-close').addEventListener('click', close);
    document.getElementById('huggingbot-hf-cancel').addEventListener('click', close);
    submitBtn.addEventListener('click', async function () {
      var token = input.value.trim();
      if (!token) return showStatus('⚠️ Please enter a token.', '#f59e0b');
      if (token.indexOf('hf_') !== 0) return showStatus('⚠️ Tokens should start with <code>hf_</code>.', '#f59e0b');
      setToken(token);
      submitBtn.disabled = true;
      submitBtn.textContent = '⏳ Verifying...';
      try {
        var res = await fetch('https://huggingface.co/api/whoami-v2', { headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' } });
        if (res.ok) {
          var info = await res.json();
          showStatus('βœ… Authenticated as <strong>' + (info.name || info.user || 'unknown') + '</strong> (' + maskToken(token) + ')', '#059669');
          setTimeout(close, 1200);
        } else {
          showStatus('⚠️ Token stored, but verification failed (' + res.status + ').', '#f59e0b');
          submitBtn.disabled = false;
          submitBtn.textContent = 'πŸ” Set Token';
        }
      } catch (e) {
        showStatus('⚠️ Token stored locally, but could not verify: ' + e.message, '#f59e0b');
        submitBtn.disabled = false;
        submitBtn.textContent = 'πŸ” Set Token';
      }
    });
    input.addEventListener('keydown', function (e) { if (e.key === 'Enter') submitBtn.click(); if (e.key === 'Escape') close(); });
    setTimeout(function () { input.focus(); }, 100);
  }

  // ─── Tool schemas (exactly as registered in the runtime) ──────────────
  var TOOLS = [
    { name: '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'] } },
    { name: '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: {} } },
    { name: 'hf_logout', description: 'Clear your stored HuggingFace token from localStorage.', parameters: { type: 'object', properties: {} } },
    { name: 'hf_token_status', description: 'Check if a HF token is stored and who it belongs to (without revealing the token).', parameters: { type: 'object', properties: {} } },
    { name: 'hf_whoami', description: 'Check who you are authenticated as on HuggingFace Hub.', parameters: { type: 'object', properties: {} } },
    { name: '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'] } },
    { name: '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'] } },
    { name: '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'] } },
    { name: '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 HuggingBot")' }, description: { type: 'string', description: 'Optional commit description' } }, required: ['repo_id', 'path', 'content'] } },
    { name: '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'] } },
    { name: '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'] } },
    { name: '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'] } },
    { name: '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'] } },
    { name: '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'] } },
    { name: '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'] } },
    { name: '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'] } },
    { name: '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'] } },
    { name: 'hf_restart_space', description: 'Restart a paused Space.', parameters: { type: 'object', properties: { repo_id: { type: 'string', description: 'Space ID to restart' } }, required: ['repo_id'] } },
    { name: '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'] } },
    { name: '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'] } },
    { name: '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'] } },
    { name: '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: [] } },
    { name: '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'] } },
    { name: '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'] } },
    { name: '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'] } },
    { name: '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'] } },
    { name: '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', description: 'Repo type' }, path: { type: 'string', default: '', description: 'Subdirectory path (optional)' }, revision: { type: 'string', default: 'main', description: 'Branch/revision' } }, required: ['repo_id'] } },
    { name: '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'] } },
    { name: '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", "config.json")' }, revision: { type: 'string', default: 'main', description: 'Branch/revision' } }, required: ['repo_id', 'path'] } },
    { name: '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'] } },
    { name: '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'] } },
    { name: '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'] } },
    // ── Expanded (new in v2.0.0) ────────────────────────────────────────
    { name: 'hf_search_datasets', description: 'Search for datasets on the HuggingFace Hub.', parameters: { type: 'object', properties: { query: { type: 'string', description: 'Search query' }, limit: { type: 'number', default: 5, description: 'Max results (1-25)' } }, required: ['query'] } }
  ];

  // ─── Tool implementations ─────────────────────────────────────────────
  var REGISTRY = {
    hf_set_token: async function (a) {
      var t = String(a.token || '').trim();
      if (!t || t.indexOf('hf_') !== 0) return '⚠️ Invalid token format. Tokens start with `hf_`. Get one at https://huggingface.co/settings/tokens';
      setToken(t);
      try {
        var w = await hfFetch('/api/whoami-v2');
        return 'βœ… **HuggingFace token stored** (' + maskToken(t) + ')\nπŸ‘€ Authenticated as: **' + (w.name || w.user || 'unknown') + '**\nYou can now use all HF tools.';
      } catch (e) {
        return '⚠️ Token stored (' + maskToken(t) + ') but verification failed: ' + e.message + '. Check that the token is valid.';
      }
    },
    hf_login_ui: async function () {
      openLoginPopup();
      return 'πŸ” **Secure Login Popup Opened**\n\nA popup has been opened where you can paste your HuggingFace token.\n\n> πŸ›‘οΈ Your token is stored directly in your browser\'s localStorage β€” it **never passes through this AI model**.\n\nπŸ’‘ Tip: you can also paste your token directly in chat with `hf_set_token`, but the popup is more secure.\n\nπŸ“‹ Get a token at: https://huggingface.co/settings/tokens';
    },
    hf_logout: async function () {
      var had = !!getToken();
      clearToken();
      return had ? 'πŸšͺ **Logged out.** HuggingFace token removed from localStorage.' : 'πŸ‘‹ No token was stored. Nothing to clear.';
    },
    hf_token_status: async function () {
      var t = getToken();
      if (!t) return 'πŸ”‘ **No token stored.** Use `hf_login_ui` (secure popup) or `hf_set_token` to log in.';
      try {
        var info = await hfFetch('/api/whoami-v2');
        return ['πŸ”‘ **Token Status:** Active (' + maskToken(t) + ')', 'πŸ‘€ **Authenticated as:** ' + (info.name || info.user || 'unknown'), 'πŸ“§ **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** (' + maskToken(t) + ') but verification failed: ' + e.message;
      }
    },
    hf_whoami: async function () {
      try {
        var info = await hfFetch('/api/whoami-v2');
        var 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(function (o) { return 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; }
    },
    hf_create_space: async function (a) {
      var me = await hfFetch('/api/whoami-v2');
      var ns = a.org || me.name;
      var payload = { name: a.name, type: 'space', sdk: a.sdk || 'gradio', private: !!a.private };
      if (a.title) payload.title = a.title;
      if (a.license) payload.license = a.license;
      if (a.hardware) payload.hardware = a.hardware;
      if (a.sleep_time) payload.sleep_time = a.sleep_time;
      var secrets = parseJsonArg(a.secrets);
      var variables = parseJsonArg(a.variables);
      if (Object.keys(secrets).length) payload.secrets = secrets;
      if (Object.keys(variables).length) payload.variables = variables;
      await hfFetch('/api/repos/create', { method: 'POST', body: JSON.stringify(payload) });
      var repoId = ns + '/' + a.name;
      var out = ['βœ… **Space created successfully!**', '', '**Repo:** ' + repoId, '**SDK:** ' + (a.sdk || 'gradio'), '**Visibility:** ' + (a.private ? 'πŸ”’ Private' : '🌍 Public'), '**URL:** https://huggingface.co/spaces/' + repoId, '**App:** ' + repoAppUrl(repoId)];
      if (a.hardware) out.push('⚑ Hardware requested: ' + a.hardware);
      if (a.sleep_time) out.push('πŸ’€ Sleep timeout: ' + a.sleep_time + 's');
      out.push('', 'πŸ’‘ **Next steps:** Upload files using `hf_upload_file` or `hf_upload_app`');
      return out.join('\n');
    },
    hf_duplicate_space: async function (a) {
      var me = await hfFetch('/api/whoami-v2');
      var ns = a.org || me.name;
      var body = { from: a.from, name: a.name, namespace: ns, private: !!a.private };
      if (a.hardware) body.hardware = a.hardware;
      if (a.sleep_time) body.sleep_time = a.sleep_time;
      var secrets = parseJsonArg(a.secrets);
      var variables = parseJsonArg(a.variables);
      if (Object.keys(secrets).length) body.secrets = secrets;
      if (Object.keys(variables).length) body.variables = variables;
      await hfFetch('/api/spaces/duplicate', { method: 'POST', body: JSON.stringify(body) });
      var repoId = ns + '/' + a.name;
      var out = ['βœ… **Space duplicated!**', '', '**From:** ' + a.from, '**New Repo:** ' + repoId, '**URL:** https://huggingface.co/spaces/' + repoId, '**App:** ' + repoAppUrl(repoId)];
      if (a.hardware) out.push('**Hardware:** ' + a.hardware);
      if (a.sleep_time) out.push('**Sleep:** ' + a.sleep_time + 's');
      return out.join('\n');
    },
    hf_delete_space: async function (a) {
      var p = parseRepoId(a.repo_id);
      try {
        var rt = await hfFetch('/api/spaces/' + p.namespace + '/' + p.name + '/runtime');
        if (rt.stage === 'PAUSED' || rt.stage === 'SLEEPING') {
          await hfFetch('/api/spaces/' + p.namespace + '/' + p.name + '/restart', { method: 'POST' });
          await new Promise(function (r) { setTimeout(r, 2000); });
        }
      } catch (e) { /* best-effort */ }
      try {
        await hfFetch('/api/spaces/' + p.namespace + '/' + p.name, { method: 'DELETE' });
        return 'βœ… **Space deleted:** ' + a.repo_id;
      } catch (e) {
        try {
          await hfFetch('/api/repos/' + p.namespace + '/' + p.name, { method: 'DELETE' });
          return 'βœ… **Space deleted (via repos endpoint):** ' + a.repo_id;
        } catch (e2) {
          throw new Error('Could not delete space: ' + e.message + '. Try restarting it first with hf_restart_space.');
        }
      }
    },
    hf_upload_file: async function (a) {
      var p = parseRepoId(a.repo_id);
      await commitFiles('space', p.namespace, p.name, [{ path: a.path, content: a.content }], null, a.summary || 'Update via HuggingBot', a.description || '');
      return ['βœ… **File uploaded:** `' + a.path + '` β†’ `' + a.repo_id + '`', 'πŸ“ ' + (a.summary || 'Update via HuggingBot'), '', 'πŸ’‘ The Space will now rebuild automatically. Check status with `hf_space_status`.'].join('\n');
    },
    hf_upload_app: async function (a) {
      var p = parseRepoId(a.repo_id);
      var files = [{ path: 'app.py', content: a.app_code }];
      if (a.requirements) files.push({ path: 'requirements.txt', content: a.requirements });
      if (a.readme) files.push({ path: 'README.md', content: a.readme });
      if (a.extra_files) {
        var extras = parseJsonArg(a.extra_files);
        for (var k in extras) if (Object.prototype.hasOwnProperty.call(extras, k)) files.push({ path: k, content: String(extras[k]) });
      }
      await commitFiles('space', p.namespace, p.name, files, null, a.summary || 'Deploy app via HuggingBot', 'Uploaded app.py' + (a.requirements ? ' + requirements.txt' : '') + (a.readme ? ' + README.md' : ''));
      return ['βœ… **App deployed to:** `' + a.repo_id + '`', 'πŸ“¦ **Files uploaded:** ' + files.map(function (f) { return '`' + f.path + '`'; }).join(', '), '', '🌐 **Space:** https://huggingface.co/spaces/' + a.repo_id, 'πŸš€ **App URL:** ' + repoAppUrl(a.repo_id), '', 'πŸ’‘ The Space is now building. Use `hf_space_status ' + a.repo_id + '` to check progress.'].join('\n');
    },
    hf_delete_file: async function (a) {
      var p = parseRepoId(a.repo_id);
      await commitFiles('space', p.namespace, p.name, null, [{ path: a.path }], a.summary || ('Delete ' + a.path), '');
      return 'βœ… **File deleted:** `' + a.path + '` from `' + a.repo_id + '`';
    },
    hf_set_secret: async function (a) {
      var p = parseRepoId(a.repo_id);
      try {
        await hfFetch('/api/spaces/' + p.namespace + '/' + p.name + '/secrets', { method: 'PUT', body: JSON.stringify({ key: a.key, value: a.value }) });
      } catch (e) {
        if (String(e.message).indexOf('404') > -1 || String(e.message).indexOf('405') > -1) {
          await hfFetch('/api/spaces/' + p.namespace + '/' + p.name + '/secrets', { method: 'POST', body: JSON.stringify({ key: a.key, value: a.value }) });
        } else { throw e; }
      }
      return 'πŸ” **Secret set:** `' + a.key + '` in `' + a.repo_id + '`\n⚠️ The Space will restart.';
    },
    hf_delete_secret: async function (a) {
      var p = parseRepoId(a.repo_id);
      await hfFetch('/api/spaces/' + p.namespace + '/' + p.name + '/secrets', { method: 'DELETE', body: JSON.stringify({ key: a.key }) });
      return 'πŸ—‘οΈ **Secret deleted:** `' + a.key + '` from `' + a.repo_id + '`';
    },
    hf_set_hardware: async function (a) {
      var p = parseRepoId(a.repo_id);
      var settings = { flavor: a.hardware };
      if (a.sleep_time) settings.gcTimeout = a.sleep_time;
      await updateSpaceSettings(p.namespace, p.name, settings);
      var out = ['⚑ **Hardware change requested for:** `' + a.repo_id + '`', '**New hardware:** ' + a.hardware];
      if (a.sleep_time) out.push('**Sleep timeout:** ' + a.sleep_time + 's');
      out.push('', '⏳ The Space is restarting. Check status with `hf_space_status`.');
      return out.join('\n');
    },
    hf_set_visibility: async function (a) {
      var p = parseRepoId(a.repo_id);
      await updateSpaceSettings(p.namespace, p.name, { private: a.visibility === 'private' });
      return (a.visibility === 'private' ? 'πŸ”’' : '🌍') + ' **Visibility changed to ' + a.visibility + ':** `' + a.repo_id + '`';
    },
    hf_set_sleep: async function (a) {
      var p = parseRepoId(a.repo_id);
      await updateSpaceSettings(p.namespace, p.name, { gcTimeout: a.sleep_time });
      var status = a.sleep_time === 0 ? '❌ Disabled (never sleeps)' : 'πŸ’€ ' + a.sleep_time + 's (' + Math.round(a.sleep_time / 60) + ' min)';
      return 'βœ… **Sleep configured:** `' + a.repo_id + '` β†’ ' + status;
    },
    hf_pause_space: async function (a) {
      var p = parseRepoId(a.repo_id);
      await hfFetch('/api/spaces/' + p.namespace + '/' + p.name + '/pause', { method: 'POST' });
      return '⏸️ **Space paused:** `' + a.repo_id + '`\nπŸ’‘ Restart with `hf_restart_space ' + a.repo_id + '`';
    },
    hf_restart_space: async function (a) {
      var p = parseRepoId(a.repo_id);
      await hfFetch('/api/spaces/' + p.namespace + '/' + p.name + '/restart', { method: 'POST' });
      return '▢️ **Space restarting:** `' + a.repo_id + '`';
    },
    hf_space_status: async function (a) {
      var p = parseRepoId(a.repo_id);
      var runtime = await hfFetch('/api/spaces/' + p.namespace + '/' + p.name + '/runtime');
      var repoInfo = await hfFetch('/api/spaces/' + p.namespace + '/' + p.name);
      var emoji = { 'NO_APP_FILE': 'πŸ“„', 'BUILDING': 'πŸ”¨', 'RUNNING': 'βœ…', 'RUNNING_BUILDING': 'πŸ”„', 'PAUSED': '⏸️', 'SLEEPING': 'πŸ’€', 'STOPPED': 'β›”', 'ERROR': '❌' };
      var stage = runtime.stage || 'UNKNOWN';
      var out = ['**πŸ“Š Space Status:** `' + a.repo_id + '`', '', '**Stage:** ' + (emoji[stage] || '❓') + ' ' + stage, '**SDK:** ' + (repoInfo.sdk || 'N/A'), '**Hardware:** ' + extractHardwareName(runtime.hardware), '**Requested HW:** ' + extractHardwareName(runtime.requestedHardware), '**Visibility:** ' + (repoInfo.private ? 'πŸ”’ Private' : '🌍 Public'), '**Likes:** ⭐ ' + (repoInfo.likes || 0), '**URL:** https://huggingface.co/spaces/' + a.repo_id, '**App:** ' + repoAppUrl(a.repo_id)];
      return out.join('\n');
    },
    hf_space_logs: async function (a) {
      var p = parseRepoId(a.repo_id);
      var endpoints = [
        '/api/spaces/' + p.namespace + '/' + p.name + '/runtime/logs',
        '/api/spaces/' + p.namespace + '/' + p.name + '/logs',
        '/api/spaces/' + p.namespace + '/' + p.name + '/runtime'
      ];
      var lastError = null;
      for (var i = 0; i < endpoints.length; i++) {
        try {
          var result = await hfFetch(endpoints[i]);
          if (result && typeof result === 'object') {
            var logsStr = result.logs || result.stdout || result.stderr || result.build || JSON.stringify(result, null, 2);
            var truncated = logsStr.length > 5000 ? logsStr.slice(-5000) + '\n... (truncated, full: ' + logsStr.length + ' chars)' : logsStr;
            return ['πŸ“‹ **Build Logs for** `' + a.repo_id + '`', '```', truncated, '```'].join('\n');
          }
        } catch (e) { lastError = e; }
      }
      try {
        var rt = await hfFetch('/api/spaces/' + p.namespace + '/' + p.name + '/runtime');
        if (rt && rt.errorMessage) return '⚠️ **Space has an error:**\n```\n' + rt.errorMessage + '\n```';
        return 'ℹ️ **Space is** ' + (rt.stage || 'unknown') + '. No logs available via REST API.\n\nπŸ’‘ Logs can be viewed in the HF UI: https://huggingface.co/spaces/' + a.repo_id;
      } catch (e2) { throw lastError || e2; }
    },
    hf_search_spaces: async function (a) {
      var limit = Math.min(Math.max(a.limit || 5, 1), 25);
      var results = await hfFetch('/api/spaces?search=' + encodeURIComponent(a.query) + '&limit=' + limit + '&sort=likes');
      if (!results || !results.length) return 'πŸ” No Spaces found for "' + a.query + '"';
      var lines = ['πŸ” **Spaces matching "' + a.query + '":**', ''];
      results.forEach(function (s, i) {
        var id = s.id || ((s.namespace || '?') + '/' + (s.name || '?'));
        var sdk = s.sdk ? ' [' + s.sdk + ']' : '';
        var likes = s.likes ? ' ⭐' + s.likes : '';
        var 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: async function () {
      var userInfo = await hfFetch('/api/whoami-v2');
      var name = userInfo.name || userInfo.user;
      var 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`!';
      var lines = ['πŸ“‚ **Your Spaces (' + name + '):**', ''];
      results.forEach(function (s) {
        var id = s.id || (s.namespace + '/' + s.name);
        var sdk = s.sdk ? ' [' + s.sdk + ']' : '';
        var vis = s.private ? 'πŸ”’' : '🌍';
        var likes = s.likes ? ' ⭐' + s.likes : '';
        var stage = (s.runtime && 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: async function (a) {
      var limit = Math.min(Math.max(a.limit || 5, 1), 25);
      var results = await hfFetch('/api/models?search=' + encodeURIComponent(a.query) + '&limit=' + limit + '&sort=likes');
      if (!results || !results.length) return 'πŸ” No models found for "' + a.query + '"';
      var lines = ['πŸ” **Models matching "' + a.query + '":**', ''];
      results.forEach(function (m, i) {
        var id = m.id || ((m.namespace || '?') + '/' + (m.name || '?'));
        var pipeline = m.pipeline_tag ? ' [' + m.pipeline_tag + ']' : '';
        var likes = m.likes ? ' ⭐' + m.likes : '';
        var 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');
    },
    hf_search_datasets: async function (a) {
      var limit = Math.min(Math.max(a.limit || 5, 1), 25);
      var results = await hfFetch('/api/datasets?search=' + encodeURIComponent(a.query) + '&limit=' + limit + '&sort=likes');
      if (!results || !results.length) return 'πŸ” No datasets found for "' + a.query + '"';
      var lines = ['πŸ” **Datasets matching "' + a.query + '":**', ''];
      results.forEach(function (d, i) {
        var id = d.id || ((d.namespace || '?') + '/' + (d.name || '?'));
        var likes = d.likes ? ' ⭐' + d.likes : '';
        var downloads = d.downloads ? ' πŸ“₯' + (d.downloads / 1000).toFixed(0) + 'k' : '';
        lines.push((i + 1) + '. **' + id + '**' + likes + downloads);
        lines.push('   https://huggingface.co/datasets/' + id);
      });
      return lines.join('\n');
    },
    hf_call_space: async function (a) {
      var p = parseRepoId(a.repo_id);
      var inputs = parseJsonArg(a.data);
      if (!Array.isArray(inputs)) inputs = [inputs];
      var apiName = String(a.api_name || '/predict').replace(/^\//, '');
      var host = repoAppUrl(a.repo_id);
      // Modern Gradio: POST /api/{name} with {data:[...]}; older: /api/predict
      var tryEndpoints = ['/api/' + apiName, '/api/predict'];
      var lastError = null;
      for (var i = 0; i < tryEndpoints.length; i++) {
        try {
          var res = await fetch(host + tryEndpoints[i], {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ data: inputs })
          });
          if (res.ok) {
            var out = await res.json();
            if (out && out.data !== undefined) return 'βœ… **Result from `' + a.repo_id + '`' + (tryEndpoints[i] === '/api/predict' ? ' (legacy /predict)' : '') + ':**\n\n' + JSON.stringify(out.data, null, 2);
            return 'βœ… **Raw response from `' + a.repo_id + '`:**\n\n' + JSON.stringify(out, null, 2);
          }
          lastError = new Error('HTTP ' + res.status + ' ' + res.statusText);
        } catch (e) { lastError = e; }
      }
      throw new Error('Could not call Gradio API on ' + host + ': ' + (lastError ? lastError.message : 'unknown error') + '. The Space may be paused/sleeping or api_name may differ.');
    },
    hf_inference: async function (a) {
      var token = getToken();
      if (!token) throw new Error('No HuggingFace token set. hf_inference uses your token for higher rate limits.');
      var url = INFER_API_BASE + (a.task ? '/pipeline/' + encodeURIComponent(a.task) + '/' : '/models/') + a.model;
      var payload = { inputs: a.inputs };
      if (a.parameters) payload.parameters = parseJsonArg(a.parameters);
      var res = await fetch(url, {
        method: 'POST',
        headers: { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json' },
        body: JSON.stringify(payload)
      });
      if (!res.ok) {
        var em = '';
        try { var ej = await res.json(); em = ej.error || JSON.stringify(ej); } catch (e) { em = res.statusText; }
        throw new Error('Inference API ' + res.status + ': ' + em);
      }
      var ct = res.headers.get('content-type') || '';
      if (ct.indexOf('image/') > -1 || ct.indexOf('audio/') > -1) {
        var buf = await res.arrayBuffer();
        var bytes = new Uint8Array(buf);
        var bin = '';
        for (var i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
        var b64 = btoa(bin);
        var mime = ct.split(';')[0];
        if (mime.indexOf('image') > -1) return 'βœ… **Generated ' + mime + '** (' + (bytes.length / 1024).toFixed(1) + ' KB)\n\n![inference-result](data:' + mime + ';base64,' + b64 + ')';
        return 'βœ… **Generated ' + mime + '** (' + (bytes.length / 1024).toFixed(1) + ' KB)\n\n' + (mime.indexOf('audio') > -1 ? '[πŸ”Š Audio result](data:' + mime + ';base64,' + b64 + ')' : 'Binary result in base64: `' + b64.substring(0, 120) + '…`');
      }
      var data = await res.json();
      if (Array.isArray(data)) {
        var parts = ['βœ… **Inference result from `' + a.model + '`:**', ''];
        data.forEach(function (item) {
          if (item && typeof item === 'object') {
            var g = item.generated_text || item.label || item.answer || item.summary_text || item.translation_text || JSON.stringify(item);
            parts.push(g);
          } else { parts.push(String(item)); }
        });
        return parts.join('\n');
      }
      return 'βœ… **Inference result from `' + a.model + '`:**\n\n' + (typeof data === 'string' ? data : JSON.stringify(data, null, 2));
    },
    hf_repo_info: async function (a) {
      var type = a.type || 'space';
      var info = await hfFetch('/api/' + repoTypePath(type) + '/' + a.repo_id);
      var lines = ['**πŸ“¦ Repo Info:** `' + a.repo_id + '` (' + type + ')', ''];
      var fields = {
        id: 'ID', name: 'Name', author: 'Author', pipeline_tag: 'Pipeline', library_name: 'Library',
        sdk: 'SDK', sdk_version: 'SDK Version', likes: '⭐ Likes', downloads: 'πŸ“₯ Downloads',
        license: 'License', private: 'Private', lastModified: 'Last Modified', createdAt: 'Created',
        tags: 'Tags', description: 'Description', cardData: 'Card Data', sha: 'Commit SHA'
      };
      for (var k in fields) {
        if (info[k] !== undefined && info[k] !== null && info[k] !== '') {
          var v = info[k];
          if (typeof v === 'object') v = JSON.stringify(v);
          if (k === 'private') v = v ? 'πŸ”’ Yes' : '🌍 No';
          if (String(v).length > 200) v = String(v).substring(0, 200) + '…';
          lines.push('**' + fields[k] + ':** ' + v);
        }
      }
      lines.push('', 'πŸ”— https://huggingface.co/' + type + 's/' + a.repo_id);
      return lines.join('\n');
    },
    hf_list_files: async function (a) {
      var type = a.type || 'space';
      var path = a.path || '';
      var rev = a.revision || 'main';
      var url = '/api/' + repoTypePath(type) + '/' + a.repo_id + '/tree/' + encodeURIComponent(rev) + (path ? '/' + path.split('/').map(encodeURIComponent).join('/') : '') + '?recursive=false&expand=false';
      var files = await hfFetch(url);
      if (!files || !files.length) return 'πŸ“ No files found at `' + (path || '/') + '` in `' + a.repo_id + '`';
      var lines = ['πŸ“‚ **Files in `' + a.repo_id + '`' + (path ? '/' + path : '') + ' (' + rev + '):**', ''];
      files.forEach(function (f) {
        var icon = f.type === 'directory' ? 'πŸ“' : 'πŸ“„';
        var size = f.size !== undefined && f.size !== null ? ' β€” ' + (f.size / 1024).toFixed(1) + ' KB' : '';
        lines.push(icon + ' `' + (f.path || f.name) + '`' + (f.type === 'directory' ? '/' : '') + size);
      });
      return lines.join('\n');
    },
    hf_read_file: async function (a) {
      var type = a.type || 'space';
      var rev = a.revision || 'main';
      var rawPath = a.path.split('/').map(encodeURIComponent).join('/');
      var text = await hfFetchRaw('/' + repoTypePath(type) + '/' + a.repo_id + '/raw/' + encodeURIComponent(rev) + '/' + rawPath);
      var truncated = text.length > 6000 ? text.substring(0, 6000) + '\n\n… (truncated, full: ' + text.length + ' chars)' : text;
      return 'πŸ“„ **`' + a.path + '`** from `' + a.repo_id + '` (' + rev + '):\n\n```\n' + truncated + '\n```';
    },
    hf_file_exists: async function (a) {
      var type = a.type || 'space';
      var rev = a.revision || 'main';
      var rawPath = a.path.split('/').map(encodeURIComponent).join('/');
      var head = await hfFetchHead('/' + repoTypePath(type) + '/' + a.repo_id + '/raw/' + encodeURIComponent(rev) + '/' + rawPath);
      return head.ok
        ? 'βœ… **File exists:** `' + a.path + '` in `' + a.repo_id + '` (HTTP ' + head.status + ')'
        : '❌ **File not found:** `' + a.path + '` in `' + a.repo_id + '` (HTTP ' + head.status + ')';
    },
    hf_repo_exists: async function (a) {
      var type = a.type || 'space';
      try {
        var info = await hfFetch('/api/' + repoTypePath(type) + '/' + a.repo_id);
        return 'βœ… **Repo exists:** `' + a.repo_id + '` (' + type + (info.private ? ', πŸ”’ private' : ', 🌍 public') + ')';
      } catch (e) {
        return '❌ **Repo not found or inaccessible:** `' + a.repo_id + '` (' + e.message + ')';
      }
    },
    hf_list_commits: async function (a) {
      var type = a.type || 'space';
      var rev = a.revision || 'main';
      var limit = Math.min(Math.max(a.limit || 10, 1), 50);
      var commits = await hfFetch('/api/' + repoTypePath(type) + '/' + a.repo_id + '/commits/' + encodeURIComponent(rev) + '?limit=' + limit);
      if (!commits || !commits.length) return 'πŸ“­ No commits found for `' + a.repo_id + '`';
      var lines = ['πŸ—‚οΈ **Commit history for `' + a.repo_id + '` (' + rev + '):**', ''];
      commits.forEach(function (c, i) {
        var date = c.createdAt ? new Date(c.createdAt).toLocaleString() : '?';
        var short = (c.id || '').substring(0, 7);
        lines.push((i + 1) + '. **' + short + '** β€” ' + c.title + '');
        lines.push('   πŸ“… ' + date + (c.author ? ' | πŸ‘€ ' + c.author.name : ''));
      });
      return lines.join('\n');
    },
    hf_move_file: async function (a) {
      var p = parseRepoId(a.repo_id);
      var type = a.type || 'space';
      var rev = 'main';
      // Move = read raw source, write to dest, delete source (works for all repo types via commit API)
      var src = a.source_path.split('/').map(encodeURIComponent).join('/');
      var content = await hfFetchRaw('/' + repoTypePath(type) + '/' + a.repo_id + '/raw/' + rev + '/' + src);
      await commitFiles(type, p.namespace, p.name, [{ path: a.dest_path, content: content }], [{ path: a.source_path }], a.summary || 'Move/Rename file', 'Moved ' + a.source_path + ' β†’ ' + a.dest_path);
      return 'πŸ“¦ **File moved:** `' + a.source_path + '` β†’ `' + a.dest_path + '` in `' + a.repo_id + '`';
    }
  };

  // ─── Register module ───────────────────────────────────────────────────
  MS.register('huggingface', {
    name: 'HuggingFace Hub',
    description: 'Full HuggingFace Hub control: auth, Spaces CRUD, file ops, secrets/hardware/sleep config, hub search, model inference, Gradio API calls, and repo browsing (expanded).',
    tools: TOOLS.map(function (t) { return { type: 'function', function: t }; }),
    handleToolCall: async function (tn, args) {
      var fn = REGISTRY[tn];
      if (!fn) return null;
      try {
        var out = await fn(args || {});
        return typeof out === 'string' ? out : JSON.stringify(out, null, 2);
      } catch (e) {
        return '❌ ' + (e && e.message ? e.message : String(e));
      }
    }
  });
})(ModuleSystem);